From 6e66dfbae88f29da011581dd9e0502b9cb02defb Mon Sep 17 00:00:00 2001 From: haelyra <49814733+haelyra@users.noreply.github.com> Date: Mon, 24 Aug 2026 20:08:41 -0400 Subject: [PATCH] test(release): add ECC 2.2 readiness regressions --- .../release-packed-artifact-workflow.test.js | 9 ++ .../install-state-selective-reinstall.test.js | 84 +++++++++++++++++++ tests/lib/install-targets.test.js | 7 +- tests/lib/multi-harness-setup.test.js | 21 +++++ tests/scripts/npm-publish-surface.test.js | 6 +- tests/scripts/release-publish.test.js | 18 +++- 6 files changed, 139 insertions(+), 6 deletions(-) create mode 100644 tests/lib/install-state-selective-reinstall.test.js diff --git a/tests/ci/release-packed-artifact-workflow.test.js b/tests/ci/release-packed-artifact-workflow.test.js index 3f2f0e3b2..1ee838ecf 100644 --- a/tests/ci/release-packed-artifact-workflow.test.js +++ b/tests/ci/release-packed-artifact-workflow.test.js @@ -161,6 +161,15 @@ test('packed lifecycle invokes installed public bins, including setup help', () assert.doesNotMatch(lifecycleRunnerSource, /node_modules.*scripts.*ecc\.js/); }); +test('packed lifecycle validates canonical Antigravity and OpenCode installs', () => { + assert.match(lifecycleRunnerSource, /'--target', 'antigravity'/); + assert.match(lifecycleRunnerSource, /path\.join\(projectDir, '\.agents'\)/); + assert.match(lifecycleRunnerSource, /'--target', 'opencode'/); + assert.match(lifecycleRunnerSource, /path\.join\(homeDir, '\.config', 'opencode'\)/); + assert.match(lifecycleRunnerSource, /doctor.*antigravity/s); + assert.match(lifecycleRunnerSource, /doctor.*opencode/s); +}); + test('packed lifecycle installs and verifies the opt-in Ito distribution surface', () => { assert.match( lifecycleRunnerSource, diff --git a/tests/lib/install-state-selective-reinstall.test.js b/tests/lib/install-state-selective-reinstall.test.js new file mode 100644 index 000000000..4af47a506 --- /dev/null +++ b/tests/lib/install-state-selective-reinstall.test.js @@ -0,0 +1,84 @@ +'use strict'; + +const assert = require('assert'); +const fs = require('fs'); +const os = require('os'); +const path = require('path'); + +const { applyInstallPlan } = require('../../scripts/lib/install/apply'); +const { readInstallState } = require('../../scripts/lib/install-state'); +const { uninstallInstalledStates } = require('../../scripts/lib/install-lifecycle'); + +function makePlan(root, moduleId, fileName) { + const targetRoot = path.join(root, '.cursor'); + const installStatePath = path.join(targetRoot, 'ecc-install-state.json'); + const sourcePath = path.join(root, 'source', moduleId, fileName); + const destinationPath = path.join(targetRoot, 'skills', moduleId, fileName); + fs.mkdirSync(path.dirname(sourcePath), { recursive: true }); + fs.writeFileSync(sourcePath, `${moduleId}\n`); + const operation = { + kind: 'copy-file', + moduleId, + sourcePath, + sourceRelativePath: path.join('skills', moduleId, fileName), + destinationPath, + strategy: 'preserve-relative-path', + ownership: 'managed', + scaffoldOnly: false, + }; + return { + mode: 'manifest', + target: 'cursor', + adapter: { id: 'cursor-project', target: 'cursor', kind: 'project' }, + targetRoot, + installRoot: targetRoot, + installStatePath, + operations: [operation], + statePreview: { + schemaVersion: 'ecc.install.v1', + installedAt: new Date().toISOString(), + target: { + id: 'cursor-project', + target: 'cursor', + kind: 'project', + root: targetRoot, + installStatePath, + }, + request: { + profile: null, + modules: [moduleId], + includeComponents: [], + excludeComponents: [], + legacyLanguages: [], + legacyMode: false, + }, + resolution: { selectedModules: [moduleId], skippedModules: [] }, + source: { manifestVersion: 1 }, + operations: [operation], + }, + warnings: [], + }; +} + +const root = fs.mkdtempSync(path.join(os.tmpdir(), 'ecc-selective-reinstall-')); +try { + const first = makePlan(root, 'first-module', 'FIRST.md'); + const second = makePlan(root, 'second-module', 'SECOND.md'); + applyInstallPlan(first); + applyInstallPlan(second); + + const state = readInstallState(first.installStatePath); + assert.deepStrictEqual( + new Set(state.operations.map(operation => operation.moduleId)), + new Set(['first-module', 'second-module']), + 'a later selective install must preserve earlier managed ownership' + ); + + const result = uninstallInstalledStates({ projectRoot: root, targets: ['cursor'] }); + assert.strictEqual(result.summary.errorCount, 0); + assert.ok(!fs.existsSync(first.operations[0].destinationPath)); + assert.ok(!fs.existsSync(second.operations[0].destinationPath)); + console.log(' ✓ selective reinstall preserves cumulative ownership and uninstall removes it'); +} finally { + fs.rmSync(root, { recursive: true, force: true }); +} diff --git a/tests/lib/install-targets.test.js b/tests/lib/install-targets.test.js index 0a1ddc805..94f55ae42 100644 --- a/tests/lib/install-targets.test.js +++ b/tests/lib/install-targets.test.js @@ -1073,8 +1073,11 @@ function runTests() { assert.strictEqual(adapter.id, 'opencode-home'); assert.strictEqual(adapter.target, 'opencode'); assert.strictEqual(adapter.kind, 'home'); - assert.strictEqual(root, path.join(homeDir, '.opencode')); - assert.strictEqual(statePath, path.join(homeDir, '.opencode', 'ecc-install-state.json')); + assert.strictEqual(root, path.join(homeDir, '.config', 'opencode')); + assert.strictEqual( + statePath, + path.join(homeDir, '.config', 'opencode', 'ecc-install-state.json') + ); })) passed++; else failed++; if (test('opencode adapter validate reports an error when compiled plugin is missing', () => { diff --git a/tests/lib/multi-harness-setup.test.js b/tests/lib/multi-harness-setup.test.js index 1910affff..51598ecc8 100644 --- a/tests/lib/multi-harness-setup.test.js +++ b/tests/lib/multi-harness-setup.test.js @@ -175,6 +175,27 @@ function writeManagedState(plan, overrides = {}) { } }); + await test('rejects managed preflight plans without an install-state path', () => { + const root = tempDir('ecc-guided-missing-state-'); + try { + const source = path.join(root, 'source.md'); + writeFile(source, 'ecc\n'); + const plan = managedPlan(root, [{ + kind: 'copy-file', + sourcePath: source, + destinationPath: path.join(root, 'AGENTS.md'), + }]); + delete plan.installStatePath; + + assert.throws( + () => preflightManagedPlan(plan), + /install-state path is required/i + ); + } finally { + fs.rmSync(root, { recursive: true, force: true }); + } + }); + await test('rejects valid install-state from a different managed target identity', () => { const root = tempDir('ecc-guided-forged-target-'); try { diff --git a/tests/scripts/npm-publish-surface.test.js b/tests/scripts/npm-publish-surface.test.js index 4f6a8d48d..6ccdbf685 100644 --- a/tests/scripts/npm-publish-surface.test.js +++ b/tests/scripts/npm-publish-surface.test.js @@ -201,6 +201,7 @@ function main() { "schemas/install-state.schema.json", "schemas/memory.schema.json", "skills/backend-patterns/SKILL.md", + "skills/skill-comply/SKILL.md", "skills/unified-memory/SKILL.md", ]) { assert.ok( @@ -214,7 +215,6 @@ function main() { "examples/CLAUDE.md", "plugins/README.md", "scripts/ci/catalog.js", - "skills/skill-comply/SKILL.md", ]) { assert.ok( !packagedPaths.has(excludedPath), @@ -231,6 +231,10 @@ function main() { !/\.py[cod]$/.test(packagedPath), `npm pack should not include Python bytecode file ${packagedPath}` ) + assert.ok( + !packagedPath.includes(".pytest_cache/"), + `npm pack should not include pytest cache path ${packagedPath}` + ) } }], ] diff --git a/tests/scripts/release-publish.test.js b/tests/scripts/release-publish.test.js index 0788b9391..54b23f808 100644 --- a/tests/scripts/release-publish.test.js +++ b/tests/scripts/release-publish.test.js @@ -51,6 +51,18 @@ for (const workflow of [ test(`${workflow} checks whether the tagged npm version already exists`, () => { assert.match(content, /Check npm publish state/); assert.match(content, /npm view "\$\{PACKAGE_NAME\}@\$\{PACKAGE_VERSION\}" version/); + assert.match(content, /E404/); + assert.match(content, /npm registry lookup failed/i); + }); + + test(`${workflow} requires the release commit to equal origin main`, () => { + assert.match(content, /git fetch origin main --no-tags/); + assert.match(content, /git rev-parse origin\/main/); + assert.match(content, /release commit.*origin\/main/i); + }); + + test(`${workflow} uses the reviewed 2.2 release notes`, () => { + assert.match(content, /docs\/releases\/2\.2\.0\/RELEASE_NOTES\.md/); }); test(`${workflow} publishes new tag versions to npm`, () => { @@ -59,15 +71,15 @@ for (const workflow of [ assert.match(content, /NODE_AUTH_TOKEN:\s*\$\{\{\s*secrets\.NPM_TOKEN\s*\}\}/); }); - test(`${workflow} creates the GitHub Release before publishing to npm`, () => { + test(`${workflow} publishes to npm before creating the GitHub Release`, () => { const releaseIndex = content.indexOf('name: Create GitHub Release'); const publishIndex = content.indexOf('name: Publish npm package'); assert.ok(releaseIndex >= 0, `${workflow} should create a GitHub Release`); assert.ok(publishIndex >= 0, `${workflow} should publish the npm package`); assert.ok( - releaseIndex < publishIndex, - `${workflow} should not publish to npm until GitHub Release creation has succeeded` + publishIndex < releaseIndex, + `${workflow} should publish the verified package before creating the GitHub Release` ); }); }