Merge remote-tracking branch 'upstream/main' into feat/story-lifecycle-skill

This commit is contained in:
t0kubetsu
2026-08-04 08:54:04 +02:00
14 changed files with 198 additions and 90 deletions
+1 -1
View File
@@ -11,7 +11,7 @@ Update ECC from its upstream repo and regenerate the current context's managed i
```bash
# Preview the update without mutating anything
ECC_ROOT="${CLAUDE_PLUGIN_ROOT:-$(node -e "var r=(function(){var p=require('path'),f=require('fs'),o=require('os');var e=process.env.CLAUDE_PLUGIN_ROOT;if(e&&e.trim())return e.trim();var d=p.join(o.homedir(),'.claude');function L(x){try{return require(p.join(x,'scripts','lib','resolve-ecc-root')).resolveEccRoot()}catch(_){return null}}var r=L(d);if(r)return r;var s=['ecc','ecc@ecc','marketplaces/ecc','everything-claude-code','everything-claude-code@everything-claude-code','marketplaces/everything-claude-code'];for(var i=0;i<s.length;i++){r=L(p.join(d,'plugins',s[i]));if(r)return r}try{var g=['ecc','everything-claude-code'];for(var j=0;j<g.length;j++){var c=p.join(d,'plugins','cache',g[j]);var O=f.readdirSync(c);for(var k=0;k<O.length;k++){var q=p.join(c,O[k]);var V=f.readdirSync(q);for(var m=0;m<V.length;m++){r=L(p.join(q,V[m]));if(r)return r}}}}catch(_){}return d})();console.log(r)")}"
ECC_ROOT="${CLAUDE_PLUGIN_ROOT:-$(node -e "var r=(function(){var p=require('path'),f=require('fs'),o=require('os');var e=process.env.CLAUDE_PLUGIN_ROOT;if(e&&e.trim())return e.trim();var d=p.join(o.homedir(),'.claude');function L(x){try{return require(p.join(x,'scripts','lib','resolve-ecc-root')).resolveEccRoot({probe:p.join('scripts','auto-update.js')})}catch(_){return null}}var r=L(d);if(r)return r;var s=['ecc','ecc@ecc','marketplaces/ecc','everything-claude-code','everything-claude-code@everything-claude-code','marketplaces/everything-claude-code'];for(var i=0;i<s.length;i++){r=L(p.join(d,'plugins',s[i]));if(r)return r}try{var g=['ecc','everything-claude-code'];for(var j=0;j<g.length;j++){var c=p.join(d,'plugins','cache',g[j]);var O=f.readdirSync(c);for(var k=0;k<O.length;k++){var q=p.join(c,O[k]);var V=f.readdirSync(q);for(var m=0;m<V.length;m++){r=L(p.join(q,V[m]));if(r)return r}}}}catch(_){}return d})();console.log(r)")}"
node "$ECC_ROOT/scripts/auto-update.js" --dry-run
# Update only Cursor-managed files in the current project
+1 -1
View File
@@ -11,7 +11,7 @@ ECCをアップストリームリポジトリから更新し、元のインス
```bash
# 何も変更せずに更新をプレビュー
ECC_ROOT="${CLAUDE_PLUGIN_ROOT:-$(node -e "var r=(function(){var p=require('path'),f=require('fs'),o=require('os');var e=process.env.CLAUDE_PLUGIN_ROOT;if(e&&e.trim())return e.trim();var d=p.join(o.homedir(),'.claude');function L(x){try{return require(p.join(x,'scripts','lib','resolve-ecc-root')).resolveEccRoot()}catch(_){return null}}var r=L(d);if(r)return r;var s=['ecc','ecc@ecc','marketplaces/ecc','everything-claude-code','everything-claude-code@everything-claude-code','marketplaces/everything-claude-code'];for(var i=0;i<s.length;i++){r=L(p.join(d,'plugins',s[i]));if(r)return r}try{var g=['ecc','everything-claude-code'];for(var j=0;j<g.length;j++){var c=p.join(d,'plugins','cache',g[j]);var O=f.readdirSync(c);for(var k=0;k<O.length;k++){var q=p.join(c,O[k]);var V=f.readdirSync(q);for(var m=0;m<V.length;m++){r=L(p.join(q,V[m]));if(r)return r}}}}catch(_){}return d})();console.log(r)")}"
ECC_ROOT="${CLAUDE_PLUGIN_ROOT:-$(node -e "var r=(function(){var p=require('path'),f=require('fs'),o=require('os');var e=process.env.CLAUDE_PLUGIN_ROOT;if(e&&e.trim())return e.trim();var d=p.join(o.homedir(),'.claude');function L(x){try{return require(p.join(x,'scripts','lib','resolve-ecc-root')).resolveEccRoot({probe:p.join('scripts','auto-update.js')})}catch(_){return null}}var r=L(d);if(r)return r;var s=['ecc','ecc@ecc','marketplaces/ecc','everything-claude-code','everything-claude-code@everything-claude-code','marketplaces/everything-claude-code'];for(var i=0;i<s.length;i++){r=L(p.join(d,'plugins',s[i]));if(r)return r}try{var g=['ecc','everything-claude-code'];for(var j=0;j<g.length;j++){var c=p.join(d,'plugins','cache',g[j]);var O=f.readdirSync(c);for(var k=0;k<O.length;k++){var q=p.join(c,O[k]);var V=f.readdirSync(q);for(var m=0;m<V.length;m++){r=L(p.join(q,V[m]));if(r)return r}}}}catch(_){}return d})();console.log(r)")}"
node "$ECC_ROOT/scripts/auto-update.js" --dry-run
# 現在のプロジェクトのCursor管理ファイルのみ更新
+1 -1
View File
@@ -11,7 +11,7 @@ disable-model-invocation: true
```bash
# Preview the update without mutating anything
ECC_ROOT="${CLAUDE_PLUGIN_ROOT:-$(node -e "var r=(function(){var p=require('path'),f=require('fs'),o=require('os');var e=process.env.CLAUDE_PLUGIN_ROOT;if(e&&e.trim())return e.trim();var d=p.join(o.homedir(),'.claude');function L(x){try{return require(p.join(x,'scripts','lib','resolve-ecc-root')).resolveEccRoot()}catch(_){return null}}var r=L(d);if(r)return r;var s=['ecc','ecc@ecc','marketplaces/ecc','everything-claude-code','everything-claude-code@everything-claude-code','marketplaces/everything-claude-code'];for(var i=0;i<s.length;i++){r=L(p.join(d,'plugins',s[i]));if(r)return r}try{var g=['ecc','everything-claude-code'];for(var j=0;j<g.length;j++){var c=p.join(d,'plugins','cache',g[j]);var O=f.readdirSync(c);for(var k=0;k<O.length;k++){var q=p.join(c,O[k]);var V=f.readdirSync(q);for(var m=0;m<V.length;m++){r=L(p.join(q,V[m]));if(r)return r}}}}catch(_){}return d})();console.log(r)")}"
ECC_ROOT="${CLAUDE_PLUGIN_ROOT:-$(node -e "var r=(function(){var p=require('path'),f=require('fs'),o=require('os');var e=process.env.CLAUDE_PLUGIN_ROOT;if(e&&e.trim())return e.trim();var d=p.join(o.homedir(),'.claude');function L(x){try{return require(p.join(x,'scripts','lib','resolve-ecc-root')).resolveEccRoot({probe:p.join('scripts','auto-update.js')})}catch(_){return null}}var r=L(d);if(r)return r;var s=['ecc','ecc@ecc','marketplaces/ecc','everything-claude-code','everything-claude-code@everything-claude-code','marketplaces/everything-claude-code'];for(var i=0;i<s.length;i++){r=L(p.join(d,'plugins',s[i]));if(r)return r}try{var g=['ecc','everything-claude-code'];for(var j=0;j<g.length;j++){var c=p.join(d,'plugins','cache',g[j]);var O=f.readdirSync(c);for(var k=0;k<O.length;k++){var q=p.join(c,O[k]);var V=f.readdirSync(q);for(var m=0;m<V.length;m++){r=L(p.join(q,V[m]));if(r)return r}}}}catch(_){}return d})();console.log(r)")}"
node "$ECC_ROOT/scripts/auto-update.js" --dry-run
# Update only Cursor-managed files in the current project
+4 -4
View File
@@ -2369,9 +2369,9 @@ dependencies = [
[[package]]
name = "toml"
version = "1.1.3+spec-1.1.0"
version = "1.1.4+spec-1.1.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "53c96ecdfa941c8fc4fcaed14f99ada8ebed502eef533015095a07e3301d4c3c"
checksum = "3aace63f4bbcdfc2c965b059de67119c89c4017a70d633be6c104910f67056f5"
dependencies = [
"indexmap",
"serde_core",
@@ -2393,9 +2393,9 @@ dependencies = [
[[package]]
name = "toml_parser"
version = "1.1.2+spec-1.1.0"
version = "1.1.3+spec-1.1.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "a2abe9b86193656635d2411dc43050282ca48aa31c2451210f4202550afb7526"
checksum = "1d38ac1cf9b95face32296c0a3ede1fdc270627c9d9c02a7274dd6d960dc4d56"
dependencies = [
"winnow 1.0.3",
]
+1
View File
@@ -35,6 +35,7 @@ const LARGE_WINDOW_MODEL_MARKER = '[1m]';
// Checked in order, first match wins. Best-effort and expected to lag new
// releases; the env override remains the escape hatch for unlisted models.
const KNOWN_MODEL_WINDOW_TOKENS = [
['claude-opus-5', LARGE_CONTEXT_WINDOW_TOKENS],
['claude-fable-5', LARGE_CONTEXT_WINDOW_TOKENS],
['claude-mythos-5', LARGE_CONTEXT_WINDOW_TOKENS]
];
+4 -1
View File
@@ -437,7 +437,10 @@ function createMemoryMcpService(options = {}) {
!isRecord(params)
|| typeof name !== 'string'
|| !TOOL_BY_NAME.has(name)
|| Object.keys(params).some(key => !['name', 'arguments'].includes(key))
// `_meta` is reserved by MCP for request metadata (e.g. progressToken); accept it,
// but when present it must be a metadata object — reject null, arrays, and scalars.
|| (Object.prototype.hasOwnProperty.call(params, '_meta') && !isRecord(params._meta))
|| Object.keys(params).some(key => !['name', 'arguments', '_meta'].includes(key))
) {
return jsonRpcError(message.id, -32602, 'Unknown or missing memory tool.');
}
+23
View File
@@ -50,6 +50,29 @@ test('instinct-status command uses shared inline resolver (no stale legacy fallb
);
});
test('auto-update command probes for the script it runs, not just scripts/lib', () => {
// A partial install can carry full resolver evidence (script tree plus
// sentinel ECC skill, per #2544/#2577) yet still lack scripts/auto-update.js.
// The command's inline resolver must probe for the script it actually
// executes so such roots don't shadow the complete plugin root.
const autoUpdateDocs = [
path.join(__dirname, '..', '..', 'commands', 'auto-update.md'),
path.join(__dirname, '..', '..', 'docs', 'ja-JP', 'commands', 'auto-update.md'),
path.join(__dirname, '..', '..', 'docs', 'zh-CN', 'commands', 'auto-update.md'),
];
for (const docPath of autoUpdateDocs) {
const doc = fs.readFileSync(docPath, 'utf8');
assert.strictEqual(
(doc.match(/scripts','lib','resolve-ecc-root/g) || []).length, 1,
`${docPath} should embed the shared inline resolver`
);
assert.ok(
doc.includes("resolveEccRoot({probe:p.join('scripts','auto-update.js')})"),
`${docPath} should probe for scripts/auto-update.js`
);
}
});
test('resolveEccRoot module covers current and legacy marketplace plugin roots', () => {
const { resolveEccRoot } = require('../../scripts/lib/resolve-ecc-root');
assert.ok(typeof resolveEccRoot === 'function');
+13 -24
View File
@@ -25,24 +25,14 @@ const {
verifyDependenciesClosed,
} = require('../../scripts/lib/github-coordination/state');
function test(name, fn) {
try {
fn();
console.log(` ✓ ${name}`);
return true;
} catch (err) {
console.log(` ✗ ${name}`);
console.log(` Error: ${err.message}`);
return false;
}
}
const { test, banner, section, summary } = require('./helpers/mini-test-runner');
let passed = 0;
let failed = 0;
console.log('\n=== parsing.js — uncovered branches ===\n');
banner('parsing.js — uncovered branches');
console.log('normalizeBodyForComparison:');
section('normalizeBodyForComparison:');
if (test('handles null body (uses empty string fallback)', () => {
const result = normalizeBodyForComparison(null);
@@ -61,7 +51,7 @@ if (test('normalizes lastSyncAt timestamps in body text', () => {
assert.ok(!result.includes('2024-01-01'));
})) passed++; else failed++;
console.log('\nparseStringList:');
section('parseStringList:');
if (test('returns empty array for null', () => {
assert.deepStrictEqual(parseStringList(null), []);
@@ -83,7 +73,7 @@ if (test('filters out empty parts from double-commas', () => {
assert.deepStrictEqual(parseStringList('a,,b'), ['a', 'b']);
})) passed++; else failed++;
console.log('\nmergeIssueBody — empty body branch:');
section('mergeIssueBody — empty body branch:');
if (test('returns rendered state when issue body is empty string', () => {
const state = { status: 'available', schemaVersion: 'v1', kind: 'epic', owner: null, branch: null, validation: 'pending', review: 'not-requested', project: { state: 'backlog', fields: {} }, dependencies: [], tasks: [], labels: [], lastAction: 'sync' };
@@ -97,9 +87,9 @@ if (test('returns rendered state when issue body is null', () => {
assert.ok(result.includes('ecc-coordination:start'));
})) passed++; else failed++;
console.log('\n=== state.js — uncovered branches ===\n');
banner('state.js — uncovered branches');
console.log('buildIssueStateFromAction — options absent (false branches):');
section('buildIssueStateFromAction — options absent (false branches):');
const baseIssue = { number: 1, labels: [], body: '' };
const baseState = {
@@ -133,7 +123,7 @@ if (test('buildIssueStateFromAction — currentState.tasks not array → re-extr
assert.ok(Array.isArray(result.tasks));
})) passed++; else failed++;
console.log('\ndesiredLabelsForState — uncovered status/review/validation branches:');
section('desiredLabelsForState — uncovered status/review/validation branches:');
if (test('includes published label for status "published"', () => {
const labels = desiredLabelsForState({ status: 'published' });
@@ -160,7 +150,7 @@ if (test('includes review-changes-requested label for review "changes-requested"
assert.ok(labels.includes('coordination:review-changes-requested'));
})) passed++; else failed++;
console.log('\nmapStateToWorkItemStatus — uncovered switch cases:');
section('mapStateToWorkItemStatus — uncovered switch cases:');
if (test('"validated" → "in-progress"', () => {
assert.strictEqual(mapStateToWorkItemStatus('validated'), 'in-progress');
@@ -182,7 +172,7 @@ if (test('"unknown-state" → "open" (default)', () => {
assert.strictEqual(mapStateToWorkItemStatus('unknown-state'), 'open');
})) passed++; else failed++;
console.log('\nassertIssueClaimable:');
section('assertIssueClaimable:');
if (test('throws when issue is not open', () => {
assert.throws(
@@ -204,7 +194,7 @@ if (test('does not throw for open, unclaimed issue', () => {
});
})) passed++; else failed++;
console.log('\nverifyDependenciesClosed:');
section('verifyDependenciesClosed:');
if (test('returns empty array when dependencyNumbers is not an array', () => {
const result = verifyDependenciesClosed('r/r', null, {}, []);
@@ -240,7 +230,7 @@ if (test('warns via stderr and skips when dependency issue is not in allIssues l
assert.ok(stderrOutput.includes('dependency issue #5 not found'), `expected stderr warning, got: ${stderrOutput}`);
})) passed++; else failed++;
console.log('\ndefaultCoordinationState — edge branches:');
section('defaultCoordinationState — edge branches:');
if (test('owner is null when issue has no author', () => {
const result = defaultCoordinationState({ number: 1, labels: [] });
@@ -264,5 +254,4 @@ if (test('handles null issue', () => {
assert.deepStrictEqual(result.tasks, []);
})) passed++; else failed++;
console.log(`\n Results: ${passed} passed, ${failed} failed`);
if (failed > 0) process.exit(1);
summary(passed, failed);
+14 -25
View File
@@ -19,17 +19,7 @@ const {
DEFAULT_SECTION_MARKER,
} = require('../../scripts/lib/github-coordination/policy');
function test(name, fn) {
try {
fn();
console.log(` ✓ ${name}`);
return true;
} catch (err) {
console.log(` ✗ ${name}`);
console.log(` Error: ${err.message}`);
return false;
}
}
const { test, banner, section, summary } = require('./helpers/mini-test-runner');
function withTempDir(fn) {
const tmpDir = fs.mkdtempSync(path.join(os.tmpdir(), 'ecc-policy-test-'));
@@ -51,9 +41,9 @@ function writeConfig(tmpDir, content) {
let passed = 0;
let failed = 0;
console.log('\n=== Testing github-coordination/policy.js ===\n');
banner('Testing github-coordination/policy.js');
console.log('loadPolicy — no config file:');
section('loadPolicy — no config file:');
if (test('returns default policy when no config file exists in rootDir', () => {
withTempDir(tmpDir => {
@@ -74,7 +64,7 @@ if (test('returns default policy when custom configPath does not exist', () => {
});
})) passed++; else failed++;
console.log('\nloadPolicy — configPath argument:');
section('loadPolicy — configPath argument:');
if (test('uses configPath when explicitly provided', () => {
withTempDir(tmpDir => {
@@ -95,7 +85,7 @@ if (test('falls back to rootDir config file when configPath is null', () => {
});
})) passed++; else failed++;
console.log('\nloadPolicy — invalid JSON:');
section('loadPolicy — invalid JSON:');
if (test('throws on invalid JSON', () => {
withTempDir(tmpDir => {
@@ -104,7 +94,7 @@ if (test('throws on invalid JSON', () => {
});
})) passed++; else failed++;
console.log('\nloadPolicy — non-object JSON:');
section('loadPolicy — non-object JSON:');
if (test('throws when top-level JSON is null', () => {
withTempDir(tmpDir => {
@@ -127,7 +117,7 @@ if (test('throws when top-level JSON is a string', () => {
});
})) passed++; else failed++;
console.log('\nloadPolicy — labels merging:');
section('loadPolicy — labels merging:');
if (test('merges labels when parsed.labels is a plain object', () => {
withTempDir(tmpDir => {
@@ -162,7 +152,7 @@ if (test('falls back to empty labels when parsed.labels is a string', () => {
});
})) passed++; else failed++;
console.log('\nloadPolicy — review merging:');
section('loadPolicy — review merging:');
if (test('merges review when parsed.review is a plain object', () => {
withTempDir(tmpDir => {
@@ -197,7 +187,7 @@ if (test('falls back when parsed.review is an array', () => {
});
})) passed++; else failed++;
console.log('\nloadPolicy — validation merging:');
section('loadPolicy — validation merging:');
if (test('merges validation when parsed.validation is a plain object', () => {
withTempDir(tmpDir => {
@@ -215,7 +205,7 @@ if (test('falls back when parsed.validation is not an object', () => {
});
})) passed++; else failed++;
console.log('\nloadPolicy — branchModel merging:');
section('loadPolicy — branchModel merging:');
if (test('merges branchModel when parsed.branchModel is a plain object', () => {
withTempDir(tmpDir => {
@@ -234,7 +224,7 @@ if (test('falls back when parsed.branchModel is not an object', () => {
});
})) passed++; else failed++;
console.log('\nloadPolicy — project merging:');
section('loadPolicy — project merging:');
if (test('merges project when parsed.project is a plain object', () => {
withTempDir(tmpDir => {
@@ -261,7 +251,7 @@ if (test('falls back when parsed.project is null', () => {
});
})) passed++; else failed++;
console.log('\nloadPolicy — project.fieldNames merging:');
section('loadPolicy — project.fieldNames merging:');
if (test('merges fieldNames when project.fieldNames is a plain object', () => {
withTempDir(tmpDir => {
@@ -296,7 +286,7 @@ if (test('falls back when project.fieldNames is an array', () => {
});
})) passed++; else failed++;
console.log('\nloadPolicy — sourcePath:');
section('loadPolicy — sourcePath:');
if (test('sets sourcePath to the resolved config file path', () => {
withTempDir(tmpDir => {
@@ -306,5 +296,4 @@ if (test('sets sourcePath to the resolved config file path', () => {
});
})) passed++; else failed++;
console.log(`\n Results: ${passed} passed, ${failed} failed`);
if (failed > 0) process.exit(1);
summary(passed, failed);
+10 -33
View File
@@ -16,17 +16,7 @@ const {
const { DEFAULT_SCHEMA_VERSION, DEFAULT_POLICY } = require('../../scripts/lib/github-coordination/policy');
function test(name, fn) {
try {
fn();
console.log(` ✓ ${name}`);
return true;
} catch (err) {
console.log(` ✗ ${name}`);
console.log(` Error: ${err.message}`);
return false;
}
}
const { test, testAsync, banner, section, summary, fatal } = require('./helpers/mini-test-runner');
function makeStore() {
const calls = [];
@@ -42,15 +32,15 @@ function makeStore() {
let passed = 0;
let failed = 0;
console.log('\n=== Testing github-coordination/store.js ===\n');
banner('Testing github-coordination/store.js');
console.log('epicWorkItemId:');
section('epicWorkItemId:');
if (test('produces a stable ID from repo and issue number', () => {
assert.strictEqual(epicWorkItemId('acme/my-repo', 42), 'github-acme-my-repo-epic-42');
})) passed++; else failed++;
console.log('\nupsertCoordinationWorkItem — null store:');
section('upsertCoordinationWorkItem — null store:');
if (test('returns null when store is null', () => {
const result = upsertCoordinationWorkItem(null, 'r/r', { number: 1 }, {}, 'sync');
@@ -62,7 +52,7 @@ if (test('returns null when store is undefined', () => {
assert.strictEqual(result, null);
})) passed++; else failed++;
console.log('\nupsertCoordinationWorkItem — with store:');
section('upsertCoordinationWorkItem — with store:');
if (test('passes schemaVersion from state when present', () => {
const store = makeStore();
@@ -179,30 +169,17 @@ if (test('sets sessionId to null when options.sessionId absent', () => {
assert.strictEqual(store.calls[0].sessionId, null);
})) passed++; else failed++;
console.log('\nopenStore — dbPath: false:');
section('openStore — dbPath: false:');
async function runAsyncTests() {
let asyncPassed = 0;
let asyncFailed = 0;
try {
if (await testAsync('returns null when dbPath is false', async () => {
const result = await openStore({ dbPath: false });
assert.strictEqual(result, null);
console.log(' ✓ returns null when dbPath is false');
asyncPassed++;
} catch (err) {
console.log(' ✗ returns null when dbPath is false');
console.log(` Error: ${err.message}`);
asyncFailed++;
}
})) passed++; else failed++;
const totalPassed = passed + asyncPassed;
const totalFailed = failed + asyncFailed;
console.log(`\n Results: ${totalPassed} passed, ${totalFailed} failed`);
if (totalFailed > 0) process.exit(1);
summary(passed, failed);
}
runAsyncTests().catch(err => {
console.error(`Unexpected async test failure: ${err.message}`);
process.exit(1);
fatal(`Unexpected async test failure: ${err.message}`);
});
+56
View File
@@ -0,0 +1,56 @@
/**
* Shared mini test harness for standalone tests/lib/*.test.js scripts.
*
* Centralizes test execution and console reporting so individual test
* files don't duplicate the runner or log directly.
*/
'use strict';
function report(message) {
console.log(message);
}
function test(name, fn) {
try {
fn();
report(` ✓ ${name}`);
return true;
} catch (err) {
report(` ✗ ${name}`);
report(` Error: ${err.message}`);
return false;
}
}
async function testAsync(name, fn) {
try {
await fn();
report(` ✓ ${name}`);
return true;
} catch (err) {
report(` ✗ ${name}`);
report(` Error: ${err.message}`);
return false;
}
}
function banner(title) {
report(`\n=== ${title} ===`);
}
function section(label) {
report(`\n${label}`);
}
function summary(passed, failed) {
report(`\n Results: ${passed} passed, ${failed} failed`);
if (failed > 0) process.exit(1);
}
function fatal(message) {
console.error(message);
process.exit(1);
}
module.exports = { test, testAsync, banner, section, summary, fatal };
+31
View File
@@ -363,6 +363,37 @@ function runTests() {
}
})) passed++; else failed++;
if (test('custom probe skips a qualifying root that lacks the probed script (auto-update)', () => {
// The surviving failure shape after #2544/#2577: a partial install can
// carry full resolver evidence (script tree + sentinel ECC skill) yet
// still lack the top-level script that auto-update will execute. The
// default probe rightly accepts such a root; a caller probing for the
// script it runs must skip it and reach the complete plugin root.
const homeDir = createTempDir();
try {
const claudeDir = setupStandardInstall(homeDir);
const marketplaceRoot = setupLegacyPluginInstall(homeDir, ['marketplaces', 'ecc']);
fs.writeFileSync(path.join(marketplaceRoot, 'scripts', 'auto-update.js'), '// stub');
assert.strictEqual(
resolveEccRoot({ envRoot: '', homeDir }),
claudeDir,
'default probe accepts a root with full resolver evidence'
);
assert.strictEqual(
resolveEccRoot({
envRoot: '',
homeDir,
probe: path.join('scripts', 'auto-update.js'),
}),
marketplaceRoot,
'auto-update probe must skip roots that lack the script it will execute'
);
} finally {
fs.rmSync(homeDir, { recursive: true, force: true });
}
})) passed++; else failed++;
// ─── INLINE_RESOLVE ───
if (test('INLINE_RESOLVE is a non-empty string', () => {
+4
View File
@@ -188,6 +188,10 @@ test('recognizes claude-mythos-5 as a 1M window from the known-model table (#246
assert.strictEqual(resolveContextWindowTokens(50000, 'claude-mythos-5'), LARGE_CONTEXT_WINDOW_TOKENS);
});
test('recognizes claude-opus-5 as a 1M window from the known-model table', () => {
assert.strictEqual(resolveContextWindowTokens(50000, 'claude-opus-5'), LARGE_CONTEXT_WINDOW_TOKENS);
});
test('recognizes dated/prefixed variants of known large-window model ids (#2461)', () => {
assert.strictEqual(resolveContextWindowTokens(50000, 'us.anthropic.claude-fable-5-20260115-v1:0'), LARGE_CONTEXT_WINDOW_TOKENS);
});
+35
View File
@@ -136,6 +136,7 @@ async function withClient(fn, options = {}) {
'tools/call',
{ name, arguments: toolArguments }
),
callToolRaw: params => request('tools/call', params),
};
try {
@@ -180,6 +181,40 @@ async function main() {
});
});
await test('accepts the reserved _meta param on tools/call and rejects malformed values', async () => {
await withClient(async client => {
// A valid `_meta` object (e.g. progressToken) must not block the tool call.
const withMeta = await client.callToolRaw({
name: 'memory_doctor',
arguments: {},
_meta: { progressToken: 'progress-123' },
});
assert.ok(Array.isArray(withMeta.content));
// Baseline: no `_meta` still works.
const withoutMeta = await client.callToolRaw({
name: 'memory_doctor',
arguments: {},
});
assert.ok(Array.isArray(withoutMeta.content));
// A malformed `_meta` (null, array, or scalar) must be rejected.
for (const badMeta of [null, ['not', 'an', 'object'], 'string', 42, true]) {
await assert.rejects(
client.callToolRaw({ name: 'memory_doctor', arguments: {}, _meta: badMeta }),
/-32602/,
`expected _meta=${JSON.stringify(badMeta)} to be rejected`
);
}
// Unrelated top-level params must still be rejected.
await assert.rejects(
client.callToolRaw({ name: 'memory_doctor', arguments: {}, unexpected: true }),
/-32602/
);
});
});
await test('starts when the npm bin invokes the server through a symlink', async () => {
const binRoot = fs.mkdtempSync(path.join(os.tmpdir(), 'ecc-memory-bin-'));
const binPath = path.join(binRoot, 'ecc-memory-mcp');