diff --git a/tests/lib/opencode-legacy-migration.test.js b/tests/lib/opencode-legacy-migration.test.js index 071e0f24e..ab4c95ca6 100644 --- a/tests/lib/opencode-legacy-migration.test.js +++ b/tests/lib/opencode-legacy-migration.test.js @@ -62,6 +62,23 @@ function seedLegacyInstall(homeDir, options = {}) { scaffoldOnly: false, contentSha256: digest(sourceContent), }; + const operations = [operation]; + if (options.includeJsonOperation) { + const configPath = path.join(targetRoot, 'opencode.json'); + fs.writeFileSync(configPath, JSON.stringify({ plugin: ['ecc'] }, null, 2) + '\n'); + operations.push({ + kind: 'merge-json', + moduleId: 'opencode-plugin', + sourceRelativePath: '.opencode/opencode.json', + destinationPath: configPath, + strategy: 'merge-json', + ownership: 'managed', + scaffoldOnly: false, + mergePayload: { plugin: ['ecc'] }, + previousExists: false, + previousContent: null, + }); + } const state = createInstallState({ adapter: { id: 'opencode-home', target: 'opencode', kind: 'home' }, targetRoot, @@ -80,19 +97,20 @@ function seedLegacyInstall(homeDir, options = {}) { repoCommit: 'legacy-opencode-test', manifestVersion: require('../../manifests/install-modules.json').version, }, - operations: [operation], + operations, }); writeInstallState(installStatePath, state); return { targetRoot, installStatePath, destinationPath }; } -function canonicalPlan(homeDir) { +function canonicalPlan(homeDir, env) { return createManifestInstallPlan({ sourceRoot: REPO_ROOT, target: 'opencode', moduleIds: ['workflow-quality'], projectRoot: homeDir, homeDir, + ...(env ? { env } : {}), exemptValidationCodes: ['opencode-plugin-not-built'], }); } @@ -182,6 +200,55 @@ test('a canonical install migrates unchanged legacy ownership', () => { } }); +test('a canonical install migrates legacy ownership when its config root is overridden', () => { + const homeDir = fs.mkdtempSync(path.join(os.tmpdir(), 'opencode-legacy-custom-root-')); + try { + const legacy = seedLegacyInstall(homeDir); + const configRoot = path.join(homeDir, 'custom', 'opencode'); + const result = applyInstallPlan(canonicalPlan(homeDir, { + OPENCODE_CONFIG_DIR: configRoot, + })); + assert.ok(result.applied); + assert.ok(fs.existsSync(path.join(configRoot, 'ecc-install-state.json'))); + assert.ok(!fs.existsSync(legacy.installStatePath)); + assert.ok(!fs.existsSync(legacy.destinationPath)); + } finally { + fs.rmSync(homeDir, { recursive: true, force: true }); + } +}); + +test('legacy non-file operations do not block canonical cleanup or repair', () => { + const applyHome = fs.mkdtempSync(path.join(os.tmpdir(), 'opencode-legacy-json-apply-')); + const repairHome = fs.mkdtempSync(path.join(os.tmpdir(), 'opencode-legacy-json-repair-')); + try { + const legacyApply = seedLegacyInstall(applyHome, { includeJsonOperation: true }); + applyInstallPlan(canonicalPlan(applyHome)); + assert.ok(!fs.existsSync(legacyApply.installStatePath)); + assert.ok(fs.existsSync(path.join(legacyApply.targetRoot, 'opencode.json'))); + + const legacyRepair = seedLegacyInstall(repairHome, { includeJsonOperation: true }); + const result = repairInstalledStates({ + repoRoot: REPO_ROOT, + homeDir: repairHome, + projectRoot: repairHome, + targets: ['opencode'], + }); + const canonicalStatePath = path.join( + repairHome, + '.config', + 'opencode', + 'ecc-install-state.json' + ); + assert.strictEqual(result.summary.errorCount, 0, JSON.stringify(result)); + assert.ok(fs.existsSync(canonicalStatePath)); + assert.ok(!fs.existsSync(legacyRepair.installStatePath)); + assert.ok(fs.existsSync(path.join(legacyRepair.targetRoot, 'opencode.json'))); + } finally { + fs.rmSync(applyHome, { recursive: true, force: true }); + fs.rmSync(repairHome, { recursive: true, force: true }); + } +}); + test('repair migrates a legacy install while preserving modified legacy files', () => { const homeDir = fs.mkdtempSync(path.join(os.tmpdir(), 'opencode-legacy-repair-')); try { diff --git a/tests/scripts/auto-update.test.js b/tests/scripts/auto-update.test.js index 6d21a2c08..2479f7301 100644 --- a/tests/scripts/auto-update.test.js +++ b/tests/scripts/auto-update.test.js @@ -502,6 +502,52 @@ function runTests() { } })) passed += 1; else failed += 1; + if (test('runAutoUpdate gives legacy-only OpenCode migration guidance', () => { + const homeDir = createTempDir('auto-update-home-'); + const projectRoot = createTempDir('auto-update-project-'); + const repoRoot = createTempDir('auto-update-repo-'); + + try { + ensureFakeRepo(repoRoot); + const legacy = { + ...makeRecord({ + repoRoot, + homeDir, + projectRoot, + adapter: { id: 'opencode-home', target: 'opencode', kind: 'home' }, + request: { + profile: null, + modules: ['workflow-quality'], + includeComponents: [], + excludeComponents: [], + legacyLanguages: [], + legacyMode: false, + }, + resolution: { selectedModules: ['workflow-quality'], skippedModules: [] }, + operations: [], + }), + installStatePath: path.join(homeDir, '.opencode', 'ecc-install-state.json'), + legacy: true, + legacyLayout: 'opencode', + }; + + const result = runAutoUpdate( + { homeDir, projectRoot, repoRoot, dryRun: true }, + { discoverInstalledStates: () => [legacy] } + ); + + assert.deepStrictEqual(result.results, []); + assert.ok(result.warnings.some(warning => warning.includes( + 'Run the OpenCode installer once to migrate it to the configured OpenCode directory' + ))); + assert.ok(result.warnings.every(warning => !warning.includes('Antigravity'))); + } finally { + cleanup(homeDir); + cleanup(projectRoot); + cleanup(repoRoot); + } + })) passed += 1; else failed += 1; + console.log(`\nResults: Passed: ${passed}, Failed: ${failed}`); process.exit(failed > 0 ? 1 : 0); }