mirror of
https://github.com/affaan-m/ECC.git
synced 2026-08-17 21:15:40 +02:00
Make Antigravity 2.0 installs native and safely migrate legacy state. Ensure doctor, repair, status projection, repeat installs, legacy Codex sync, and uninstall converge without losing user files. Exclude Python bytecode and harden repo-scan bootstrap guidance. Gate publishing and pull-request merges on one exact packed artifact completing install, repeat, drift, repair, status, and uninstall across Linux, macOS, and Windows. Co-authored-by: lorencifernando-coder <lorenci.fernando@gmail.com> Co-authored-by: Suliman Abdulrazzaq <suliman9000a@gmail.com> Co-authored-by: Wu Shuwen <mikewushuwen@outlook.com>
510 lines
15 KiB
JavaScript
510 lines
15 KiB
JavaScript
/**
|
|
* Tests for scripts/auto-update.js
|
|
*/
|
|
|
|
const assert = require('assert');
|
|
const fs = require('fs');
|
|
const os = require('os');
|
|
const path = require('path');
|
|
|
|
const {
|
|
parseArgs,
|
|
deriveRepoRootFromState,
|
|
buildInstallApplyArgs,
|
|
determineInstallCwd,
|
|
runAutoUpdate,
|
|
} = require('../../scripts/auto-update');
|
|
const {
|
|
createInstallState,
|
|
} = require('../../scripts/lib/install-state');
|
|
|
|
function test(name, fn) {
|
|
try {
|
|
fn();
|
|
console.log(` \u2713 ${name}`);
|
|
return true;
|
|
} catch (error) {
|
|
console.log(` \u2717 ${name}`);
|
|
console.log(` Error: ${error.message}`);
|
|
return false;
|
|
}
|
|
}
|
|
|
|
function createTempDir(prefix) {
|
|
return fs.mkdtempSync(path.join(os.tmpdir(), prefix));
|
|
}
|
|
|
|
function cleanup(dirPath) {
|
|
fs.rmSync(dirPath, { recursive: true, force: true });
|
|
}
|
|
|
|
function makeRecord({ repoRoot, homeDir, projectRoot, adapter, request, resolution, operations }) {
|
|
const targetRoot = adapter.kind === 'project'
|
|
? path.join(projectRoot, `.${adapter.target}`)
|
|
: path.join(homeDir, '.claude');
|
|
const installStatePath = adapter.kind === 'project'
|
|
? path.join(targetRoot, 'ecc-install-state.json')
|
|
: path.join(targetRoot, 'ecc', 'install-state.json');
|
|
|
|
const state = createInstallState({
|
|
adapter,
|
|
targetRoot,
|
|
installStatePath,
|
|
request,
|
|
resolution,
|
|
operations,
|
|
source: {
|
|
repoVersion: '1.10.0',
|
|
repoCommit: 'abc123',
|
|
manifestVersion: 1,
|
|
},
|
|
});
|
|
|
|
return {
|
|
adapter,
|
|
targetRoot,
|
|
installStatePath,
|
|
exists: true,
|
|
state,
|
|
error: null,
|
|
repoRoot,
|
|
};
|
|
}
|
|
|
|
function ensureFakeRepo(repoRoot) {
|
|
fs.mkdirSync(path.join(repoRoot, 'scripts'), { recursive: true });
|
|
fs.writeFileSync(
|
|
path.join(repoRoot, 'package.json'),
|
|
JSON.stringify({ name: 'everything-claude-code', version: '1.10.0' }, null, 2)
|
|
);
|
|
fs.writeFileSync(path.join(repoRoot, 'scripts', 'install-apply.js'), '#!/usr/bin/env node\n');
|
|
}
|
|
|
|
function runTests() {
|
|
console.log('\n=== Testing auto-update.js ===\n');
|
|
|
|
let passed = 0;
|
|
let failed = 0;
|
|
|
|
if (test('parseArgs reads repo-root, target, dry-run, and json flags', () => {
|
|
const parsed = parseArgs([
|
|
'node',
|
|
'scripts/auto-update.js',
|
|
'--target',
|
|
'cursor',
|
|
'--repo-root',
|
|
'/tmp/ecc',
|
|
'--dry-run',
|
|
'--json',
|
|
]);
|
|
|
|
assert.deepStrictEqual(parsed.targets, ['cursor']);
|
|
assert.strictEqual(parsed.repoRoot, '/tmp/ecc');
|
|
assert.strictEqual(parsed.dryRun, true);
|
|
assert.strictEqual(parsed.json, true);
|
|
})) passed += 1; else failed += 1;
|
|
|
|
if (test('parseArgs rejects unknown arguments', () => {
|
|
assert.throws(
|
|
() => parseArgs(['node', 'scripts/auto-update.js', '--bogus']),
|
|
/Unknown argument: --bogus/
|
|
);
|
|
})) passed += 1; else failed += 1;
|
|
|
|
if (test('deriveRepoRootFromState uses sourcePath and sourceRelativePath', () => {
|
|
const state = {
|
|
operations: [
|
|
{
|
|
sourcePath: path.join('/tmp', 'ecc', 'scripts', 'setup-package-manager.js'),
|
|
sourceRelativePath: path.join('scripts', 'setup-package-manager.js'),
|
|
},
|
|
],
|
|
};
|
|
|
|
assert.strictEqual(
|
|
deriveRepoRootFromState(state),
|
|
path.resolve(path.join('/tmp', 'ecc'))
|
|
);
|
|
})) passed += 1; else failed += 1;
|
|
|
|
if (test('deriveRepoRootFromState fails when source metadata is unavailable', () => {
|
|
assert.throws(
|
|
() => deriveRepoRootFromState({ operations: [{ destinationPath: '/tmp/file' }] }),
|
|
/Unable to infer ECC repo root/
|
|
);
|
|
})) passed += 1; else failed += 1;
|
|
|
|
if (test('buildInstallApplyArgs reconstructs legacy installs', () => {
|
|
const record = {
|
|
adapter: { target: 'claude', kind: 'home' },
|
|
state: {
|
|
target: { target: 'claude' },
|
|
request: {
|
|
profile: null,
|
|
modules: [],
|
|
includeComponents: [],
|
|
excludeComponents: [],
|
|
legacyLanguages: ['typescript', 'python'],
|
|
legacyMode: true,
|
|
},
|
|
},
|
|
};
|
|
|
|
assert.deepStrictEqual(buildInstallApplyArgs(record), [
|
|
'--target', 'claude',
|
|
'typescript',
|
|
'python',
|
|
]);
|
|
})) passed += 1; else failed += 1;
|
|
|
|
if (test('buildInstallApplyArgs reconstructs manifest installs', () => {
|
|
const record = {
|
|
adapter: { target: 'cursor', kind: 'project' },
|
|
state: {
|
|
target: { target: 'cursor' },
|
|
request: {
|
|
profile: 'developer',
|
|
modules: ['platform-configs'],
|
|
includeComponents: ['component:alpha'],
|
|
excludeComponents: ['component:beta'],
|
|
legacyLanguages: [],
|
|
legacyMode: false,
|
|
},
|
|
},
|
|
};
|
|
|
|
assert.deepStrictEqual(buildInstallApplyArgs(record), [
|
|
'--target', 'cursor',
|
|
'--profile', 'developer',
|
|
'--modules', 'platform-configs',
|
|
'--with', 'component:alpha',
|
|
'--without', 'component:beta',
|
|
]);
|
|
})) passed += 1; else failed += 1;
|
|
|
|
if (test('determineInstallCwd uses the project root for project installs', () => {
|
|
const record = {
|
|
adapter: { kind: 'project' },
|
|
state: {
|
|
target: {
|
|
root: path.join('/tmp', 'project', '.cursor'),
|
|
},
|
|
},
|
|
};
|
|
|
|
assert.strictEqual(determineInstallCwd(record, '/tmp/ecc'), path.join('/tmp', 'project'));
|
|
})) passed += 1; else failed += 1;
|
|
|
|
if (test('runAutoUpdate reports when no install-state files are present', () => {
|
|
const result = runAutoUpdate(
|
|
{
|
|
homeDir: '/tmp/home',
|
|
projectRoot: '/tmp/project',
|
|
dryRun: true,
|
|
},
|
|
{
|
|
discoverInstalledStates: () => [],
|
|
}
|
|
);
|
|
|
|
assert.strictEqual(result.results.length, 0);
|
|
assert.strictEqual(result.summary.checkedCount, 0);
|
|
assert.strictEqual(result.summary.errorCount, 0);
|
|
})) passed += 1; else failed += 1;
|
|
|
|
if (test('runAutoUpdate rejects mixed inferred repo roots', () => {
|
|
const homeDir = createTempDir('auto-update-home-');
|
|
const projectRoot = createTempDir('auto-update-project-');
|
|
const repoOne = createTempDir('auto-update-repo-');
|
|
const repoTwo = createTempDir('auto-update-repo-');
|
|
|
|
try {
|
|
ensureFakeRepo(repoOne);
|
|
ensureFakeRepo(repoTwo);
|
|
|
|
const records = [
|
|
makeRecord({
|
|
repoRoot: repoOne,
|
|
homeDir,
|
|
projectRoot,
|
|
adapter: { id: 'claude-home', target: 'claude', kind: 'home' },
|
|
request: {
|
|
profile: null,
|
|
modules: [],
|
|
includeComponents: [],
|
|
excludeComponents: [],
|
|
legacyLanguages: ['typescript'],
|
|
legacyMode: true,
|
|
},
|
|
resolution: { selectedModules: ['legacy-claude-rules'], skippedModules: [] },
|
|
operations: [
|
|
{
|
|
kind: 'copy-file',
|
|
moduleId: 'legacy-claude-rules',
|
|
sourcePath: path.join(repoOne, 'rules', 'common', 'coding-style.md'),
|
|
sourceRelativePath: path.join('rules', 'common', 'coding-style.md'),
|
|
destinationPath: path.join(homeDir, '.claude', 'rules', 'common', 'coding-style.md'),
|
|
strategy: 'preserve-relative-path',
|
|
ownership: 'managed',
|
|
scaffoldOnly: false,
|
|
},
|
|
],
|
|
}),
|
|
makeRecord({
|
|
repoRoot: repoTwo,
|
|
homeDir,
|
|
projectRoot,
|
|
adapter: { id: 'cursor-project', target: 'cursor', kind: 'project' },
|
|
request: {
|
|
profile: 'core',
|
|
modules: [],
|
|
includeComponents: [],
|
|
excludeComponents: [],
|
|
legacyLanguages: [],
|
|
legacyMode: false,
|
|
},
|
|
resolution: { selectedModules: ['rules-core'], skippedModules: [] },
|
|
operations: [
|
|
{
|
|
kind: 'copy-file',
|
|
moduleId: 'rules-core',
|
|
sourcePath: path.join(repoTwo, '.cursor', 'mcp.json'),
|
|
sourceRelativePath: path.join('.cursor', 'mcp.json'),
|
|
destinationPath: path.join(projectRoot, '.cursor', 'mcp.json'),
|
|
strategy: 'sync-root-children',
|
|
ownership: 'managed',
|
|
scaffoldOnly: false,
|
|
},
|
|
],
|
|
}),
|
|
];
|
|
|
|
assert.throws(
|
|
() => runAutoUpdate(
|
|
{
|
|
homeDir,
|
|
projectRoot,
|
|
dryRun: true,
|
|
},
|
|
{
|
|
discoverInstalledStates: () => records,
|
|
}
|
|
),
|
|
/Multiple ECC repo roots detected/
|
|
);
|
|
} finally {
|
|
cleanup(homeDir);
|
|
cleanup(projectRoot);
|
|
cleanup(repoOne);
|
|
cleanup(repoTwo);
|
|
}
|
|
})) passed += 1; else failed += 1;
|
|
|
|
if (test('runAutoUpdate fetches, pulls, and reinstalls using reconstructed args', () => {
|
|
const homeDir = createTempDir('auto-update-home-');
|
|
const projectRoot = createTempDir('auto-update-project-');
|
|
const repoRoot = createTempDir('auto-update-repo-');
|
|
|
|
try {
|
|
ensureFakeRepo(repoRoot);
|
|
|
|
const records = [
|
|
makeRecord({
|
|
repoRoot,
|
|
homeDir,
|
|
projectRoot,
|
|
adapter: { id: 'cursor-project', target: 'cursor', kind: 'project' },
|
|
request: {
|
|
profile: 'developer',
|
|
modules: [],
|
|
includeComponents: ['component:alpha'],
|
|
excludeComponents: ['component:beta'],
|
|
legacyLanguages: [],
|
|
legacyMode: false,
|
|
},
|
|
resolution: { selectedModules: ['rules-core'], skippedModules: [] },
|
|
operations: [
|
|
{
|
|
kind: 'copy-file',
|
|
moduleId: 'platform-configs',
|
|
sourcePath: path.join(repoRoot, '.cursor', 'mcp.json'),
|
|
sourceRelativePath: path.join('.cursor', 'mcp.json'),
|
|
destinationPath: path.join(projectRoot, '.cursor', 'mcp.json'),
|
|
strategy: 'sync-root-children',
|
|
ownership: 'managed',
|
|
scaffoldOnly: false,
|
|
},
|
|
],
|
|
}),
|
|
];
|
|
|
|
const commands = [];
|
|
const result = runAutoUpdate(
|
|
{
|
|
homeDir,
|
|
projectRoot,
|
|
dryRun: false,
|
|
},
|
|
{
|
|
discoverInstalledStates: () => records,
|
|
runExternalCommand: (command, args, options) => {
|
|
commands.push({ command, args, options });
|
|
if (command === process.execPath) {
|
|
return {
|
|
stdout: JSON.stringify({
|
|
dryRun: false,
|
|
result: {
|
|
installStatePath: path.join(projectRoot, '.cursor', 'ecc-install-state.json'),
|
|
},
|
|
}),
|
|
stderr: '',
|
|
};
|
|
}
|
|
|
|
return { stdout: '', stderr: '' };
|
|
},
|
|
}
|
|
);
|
|
|
|
assert.strictEqual(result.summary.checkedCount, 1);
|
|
assert.strictEqual(result.summary.updatedCount, 1);
|
|
assert.deepStrictEqual(commands.map(entry => [entry.command, entry.args[0]]), [
|
|
['git', 'fetch'],
|
|
['git', 'pull'],
|
|
[process.execPath, path.join(repoRoot, 'scripts', 'install-apply.js')],
|
|
]);
|
|
assert.deepStrictEqual(commands[2].args.slice(1), [
|
|
'--target', 'cursor',
|
|
'--profile', 'developer',
|
|
'--with', 'component:alpha',
|
|
'--without', 'component:beta',
|
|
'--json',
|
|
]);
|
|
assert.strictEqual(commands[2].options.cwd, projectRoot);
|
|
} finally {
|
|
cleanup(homeDir);
|
|
cleanup(projectRoot);
|
|
cleanup(repoRoot);
|
|
}
|
|
})) passed += 1; else failed += 1;
|
|
|
|
if (test('runAutoUpdate excludes residual legacy Antigravity records', () => {
|
|
const homeDir = createTempDir('auto-update-home-');
|
|
const projectRoot = createTempDir('auto-update-project-');
|
|
const repoRoot = createTempDir('auto-update-repo-');
|
|
|
|
try {
|
|
ensureFakeRepo(repoRoot);
|
|
const canonical = makeRecord({
|
|
repoRoot,
|
|
homeDir,
|
|
projectRoot,
|
|
adapter: { id: 'antigravity-project', target: 'antigravity', kind: 'project' },
|
|
request: {
|
|
profile: null,
|
|
modules: [],
|
|
includeComponents: [],
|
|
excludeComponents: [],
|
|
legacyLanguages: ['typescript'],
|
|
legacyMode: true,
|
|
},
|
|
resolution: { selectedModules: ['legacy-antigravity-install'], skippedModules: [] },
|
|
operations: [],
|
|
});
|
|
const legacy = {
|
|
...canonical,
|
|
installStatePath: path.join(projectRoot, '.agent', 'ecc-install-state.json'),
|
|
legacy: true,
|
|
};
|
|
const commands = [];
|
|
|
|
const result = runAutoUpdate(
|
|
{
|
|
homeDir,
|
|
projectRoot,
|
|
repoRoot,
|
|
dryRun: true,
|
|
},
|
|
{
|
|
discoverInstalledStates: () => [canonical, legacy],
|
|
runExternalCommand(command, args) {
|
|
commands.push({ command, args });
|
|
return {
|
|
stdout: JSON.stringify({ dryRun: true, plan: {} }),
|
|
stderr: '',
|
|
};
|
|
},
|
|
}
|
|
);
|
|
|
|
assert.strictEqual(result.summary.checkedCount, 1);
|
|
assert.strictEqual(result.summary.updatedCount, 1);
|
|
assert.strictEqual(commands.length, 1);
|
|
assert.strictEqual(commands[0].command, process.execPath);
|
|
} finally {
|
|
cleanup(homeDir);
|
|
cleanup(projectRoot);
|
|
cleanup(repoRoot);
|
|
}
|
|
})) passed += 1; else failed += 1;
|
|
|
|
if (test('runAutoUpdate explains a legacy-only Antigravity install', () => {
|
|
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: 'antigravity-project', target: 'antigravity', kind: 'project' },
|
|
request: {
|
|
profile: null,
|
|
modules: [],
|
|
includeComponents: [],
|
|
excludeComponents: [],
|
|
legacyLanguages: ['typescript'],
|
|
legacyMode: true,
|
|
},
|
|
resolution: { selectedModules: ['legacy-antigravity-install'], skippedModules: [] },
|
|
operations: [],
|
|
}),
|
|
installStatePath: path.join(projectRoot, '.agent', 'ecc-install-state.json'),
|
|
legacy: true,
|
|
};
|
|
const commands = [];
|
|
|
|
const result = runAutoUpdate(
|
|
{ homeDir, projectRoot, repoRoot, dryRun: true },
|
|
{
|
|
discoverInstalledStates: () => [legacy],
|
|
runExternalCommand(command, args) {
|
|
commands.push({ command, args });
|
|
},
|
|
}
|
|
);
|
|
|
|
assert.deepStrictEqual(result.results, []);
|
|
assert.strictEqual(result.summary.checkedCount, 0);
|
|
assert.strictEqual(result.summary.updatedCount, 0);
|
|
assert.strictEqual(result.summary.errorCount, 0);
|
|
assert.strictEqual(commands.length, 0);
|
|
assert.ok(result.warnings.some(warning => warning.includes(
|
|
'Run the Antigravity installer once to migrate it to .agents'
|
|
)));
|
|
} 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);
|
|
}
|
|
|
|
runTests();
|