diff --git a/tests/ci/release-packed-artifact-workflow.test.js b/tests/ci/release-packed-artifact-workflow.test.js index a79e96bb2..a37c8f4bd 100644 --- a/tests/ci/release-packed-artifact-workflow.test.js +++ b/tests/ci/release-packed-artifact-workflow.test.js @@ -86,6 +86,15 @@ for (const workflowPath of workflowPaths) { ); }); + test(`${workflowPath} disables generated additions to reviewed release notes`, () => { + const publish = jobBlock(source, 'publish'); + assert.match( + publish, + /body_path:\s*release_body\.md[\s\S]{0,160}generate_release_notes:\s*false/ + ); + assert.doesNotMatch(publish, /generate_release_notes:\s*(?:true|\$\{\{)/); + }); + test(`${workflowPath} uploads the one packed tgz as the release artifact`, () => { const verify = jobBlock(source, 'verify', 'lifecycle'); const packIndex = verify.indexOf('name: Pack npm artifact'); diff --git a/tests/lib/harness-capabilities.test.js b/tests/lib/harness-capabilities.test.js index a35bfe57f..98264111e 100644 --- a/tests/lib/harness-capabilities.test.js +++ b/tests/lib/harness-capabilities.test.js @@ -83,6 +83,11 @@ function runTests() { assert.deepStrictEqual(kimi.scopes, [ { id: 'project', targetId: 'kimi', root: './.kimi-code' }, ]); + + const opencode = getHarnessCapability('opencode'); + assert.match(opencode.destinationResolution, /OPENCODE_CONFIG_DIR/); + assert.match(opencode.destinationResolution, /XDG_CONFIG_HOME/); + assert.match(opencode.destinationResolution, /~\/\.config\/opencode/); })) passed++; else failed++; if (test('keeps every advanced target attached to its registered root and scope', () => { diff --git a/tests/lib/install-state-selective-reinstall.test.js b/tests/lib/install-state-selective-reinstall.test.js index a75a6b024..74d92e170 100644 --- a/tests/lib/install-state-selective-reinstall.test.js +++ b/tests/lib/install-state-selective-reinstall.test.js @@ -68,6 +68,7 @@ try { const first = makePlan(root, 'first-module', 'FIRST.md'); const second = makePlan(root, 'second-module', 'SECOND.md'); applyInstallPlan(first); + fs.writeFileSync(first.operations[0].destinationPath, 'user-modified\n'); applyInstallPlan(second); const state = readInstallState(first.installStatePath); @@ -79,9 +80,13 @@ try { const result = uninstallInstalledStates({ projectRoot: root, targets: ['cursor'] }); assert.strictEqual(result.summary.errorCount, 0); - assert.ok(!fs.existsSync(first.operations[0].destinationPath)); + assert.strictEqual( + fs.readFileSync(first.operations[0].destinationPath, 'utf8'), + 'user-modified\n', + 'selective reinstall must not claim modified retained content' + ); assert.ok(!fs.existsSync(second.operations[0].destinationPath)); - console.log(' ✓ selective reinstall preserves cumulative ownership and uninstall removes it'); + console.log(' ✓ selective reinstall preserves cumulative ownership without claiming user changes'); passed += 1; } catch (error) { console.log(` ✗ ${error.message}`); @@ -90,5 +95,38 @@ try { fs.rmSync(root, { recursive: true, force: true }); } +const partialRoot = fs.mkdtempSync(path.join(os.tmpdir(), 'ecc-partial-non-claude-')); +try { + const copied = makePlan(partialRoot, 'copied-module', 'COPIED.md'); + const missing = makePlan(partialRoot, 'missing-module', 'MISSING.md'); + fs.rmSync(missing.operations[0].sourcePath); + const partialPlan = { + ...copied, + operations: [copied.operations[0], missing.operations[0]], + statePreview: { + ...copied.statePreview, + operations: [copied.operations[0], missing.operations[0]], + }, + }; + + assert.throws(() => applyInstallPlan(partialPlan), /ENOENT/); + assert.ok(fs.existsSync(copied.operations[0].destinationPath)); + const checkpoint = readInstallState(copied.installStatePath); + assert.ok(checkpoint.operations.some(operation => ( + operation.destinationPath === copied.operations[0].destinationPath + ))); + + const result = uninstallInstalledStates({ projectRoot: partialRoot, targets: ['cursor'] }); + assert.strictEqual(result.summary.errorCount, 0); + assert.ok(!fs.existsSync(copied.operations[0].destinationPath)); + console.log(' ✓ failed non-Claude install checkpoints managed files for uninstall'); + passed += 1; +} catch (error) { + console.log(` ✗ ${error.message}`); + failed += 1; +} finally { + fs.rmSync(partialRoot, { recursive: true, force: true }); +} + console.log(`\nResults: Passed: ${passed}, Failed: ${failed}`); process.exit(failed > 0 ? 1 : 0); diff --git a/tests/lib/install-targets.test.js b/tests/lib/install-targets.test.js index 94f55ae42..7bd937733 100644 --- a/tests/lib/install-targets.test.js +++ b/tests/lib/install-targets.test.js @@ -629,8 +629,8 @@ function runTests() { if (test('resolves qwen adapter root and install-state path from home dir', () => { const adapter = getInstallTargetAdapter('qwen'); const homeDir = '/Users/example'; - const root = adapter.resolveRoot({ homeDir }); - const statePath = adapter.getInstallStatePath({ homeDir }); + const root = adapter.resolveRoot({ homeDir, env: {} }); + const statePath = adapter.getInstallStatePath({ homeDir, env: {} }); assert.strictEqual(adapter.id, 'qwen-home'); assert.strictEqual(adapter.target, 'qwen'); @@ -639,6 +639,35 @@ function runTests() { assert.strictEqual(statePath, path.join(homeDir, '.qwen', 'ecc-install-state.json')); })) passed++; else failed++; + if (test('opencode adapter honors config overrides in priority order', () => { + const adapter = getInstallTargetAdapter('opencode'); + const homeDir = '/Users/example'; + const xdgRoot = path.join(homeDir, 'xdg'); + const explicitRoot = path.join(homeDir, 'custom-opencode'); + + assert.strictEqual( + adapter.resolveRoot({ + homeDir, + env: { + XDG_CONFIG_HOME: xdgRoot, + OPENCODE_CONFIG_DIR: explicitRoot, + }, + }), + explicitRoot + ); + assert.strictEqual( + adapter.resolveRoot({ homeDir, env: { XDG_CONFIG_HOME: xdgRoot } }), + path.join(xdgRoot, 'opencode') + ); + assert.strictEqual( + adapter.getInstallStatePath({ + homeDir, + env: { OPENCODE_CONFIG_DIR: explicitRoot }, + }), + path.join(explicitRoot, 'ecc-install-state.json') + ); + })) passed++; else failed++; + if (test('qwen adapter supports lookup by target and adapter id', () => { const byTarget = getInstallTargetAdapter('qwen'); const byId = getInstallTargetAdapter('qwen-home'); diff --git a/tests/lib/mcp-inventory.test.js b/tests/lib/mcp-inventory.test.js index 1b113b8b9..f6df78822 100644 --- a/tests/lib/mcp-inventory.test.js +++ b/tests/lib/mcp-inventory.test.js @@ -184,6 +184,38 @@ test('opencode reader splits command array and reads environment', () => { assert.strictEqual(records.find(r => r.name === 'disabledtool').enabled, false); }); +test('opencode reader honors OPENCODE_CONFIG_DIR before XDG_CONFIG_HOME', () => { + const home = tmpHome(); + const explicitRoot = path.join(home, 'explicit-opencode'); + const xdgRoot = path.join(home, 'xdg'); + for (const root of [explicitRoot, path.join(xdgRoot, 'opencode')]) { + fs.mkdirSync(root, { recursive: true }); + fs.writeFileSync(path.join(root, 'opencode.json'), JSON.stringify({ + mcp: { + [root === explicitRoot ? 'explicit' : 'xdg']: { + type: 'local', + command: ['node'], + }, + }, + }), 'utf8'); + } + + const explicit = readOpencodeMcp({ + homeDir: home, + env: { + OPENCODE_CONFIG_DIR: explicitRoot, + XDG_CONFIG_HOME: xdgRoot, + }, + }); + assert.deepStrictEqual(explicit.map(record => record.name), ['explicit']); + + const xdg = readOpencodeMcp({ + homeDir: home, + env: { XDG_CONFIG_HOME: xdgRoot }, + }); + assert.deepStrictEqual(xdg.map(record => record.name), ['xdg']); +}); + test('collectMcpInventory merges harnesses, detects fragmentation + drift, redacts secrets', () => { const home = tmpHome(); // claude + opencode agree on github (consistent); codex github uses a diff --git a/tests/scripts/release-publish.test.js b/tests/scripts/release-publish.test.js index a6c319f2d..5127e565d 100644 --- a/tests/scripts/release-publish.test.js +++ b/tests/scripts/release-publish.test.js @@ -66,6 +66,11 @@ for (const workflow of [ assert.match(content, /docs\/releases\/\$\{RELEASE_VERSION\}\/release-notes\.md/); }); + test(`${workflow} publishes only the reviewed release notes`, () => { + assert.match(content, /body_path:\s*release_body\.md[\s\S]{0,160}generate_release_notes:\s*false/); + assert.doesNotMatch(content, /generate_release_notes:\s*(?:true|\$\{\{)/); + }); + test(`${workflow} publishes new tag versions to npm`, () => { assert.match(content, /ECC_RELEASE_PACKAGE:\s*\$\{\{ needs\.verify\.outputs\.package_file \}\}/); assert.match(content, /npm publish "\.\/\$\{ECC_RELEASE_PACKAGE\}" --access public --provenance/); @@ -85,6 +90,10 @@ for (const workflow of [ }); } +test('reusable release workflow has no generated-notes input', () => { + assert.doesNotMatch(load('.github/workflows/reusable-release.yml'), /generate-notes:/); +}); + if (failed > 0) { console.log(`\nFailed: ${failed}`); process.exit(1);