mirror of
https://github.com/affaan-m/ECC.git
synced 2026-10-19 06:23:46 +02:00
Merge remote-tracking branch 'upstream/main' into feat/story-lifecycle-skill
This commit is contained in:
@@ -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
|
||||
|
||||
@@ -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管理ファイルのみ更新
|
||||
|
||||
@@ -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
|
||||
|
||||
Generated
+4
-4
@@ -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",
|
||||
]
|
||||
|
||||
@@ -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]
|
||||
];
|
||||
|
||||
@@ -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.');
|
||||
}
|
||||
|
||||
@@ -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');
|
||||
|
||||
@@ -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);
|
||||
|
||||
@@ -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);
|
||||
|
||||
@@ -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}`);
|
||||
});
|
||||
|
||||
@@ -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 };
|
||||
@@ -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', () => {
|
||||
|
||||
@@ -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);
|
||||
});
|
||||
|
||||
@@ -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');
|
||||
|
||||
Reference in New Issue
Block a user