mirror of
https://github.com/affaan-m/ECC.git
synced 2026-08-17 21:15:40 +02:00
* feat(install): add guided Claude plugin setup * fix: support Claude command shims on Windows * feat: support safe Claude plugin scope migration * fix(install): preserve interactive setup terminal * fix(install): auto-migrate setup scope changes * feat(install): add guided multi-harness installer * fix(install): sync Yarn binary metadata * fix(install): handle wizard EOF on Node 18 * ci: allow installer matrix tests to finish * test(install): allow slower PowerShell delegation * fix(install): harden guided provider reconciliation * test(install): harden packaged and local compatibility * chore: prepare guided installer release 2.2.0 * fix(install): report refreshed Codex marketplace state * fix(install): verify managed content provenance * test(install): allow empty Yarn smoke fixture * test(install): invoke Windows package shims safely * fix(install): close cross-platform release gaps * fix(install): require trusted GitHub origins * fix(install): preserve hook profile precedence * refactor(install): centralize trusted GitHub origins * ci: retrigger workflow run after merge of main Co-Authored-By: Claude Fable 5 <noreply@anthropic.com> --------- Co-authored-by: Claude Fable 5 <noreply@anthropic.com>
646 lines
23 KiB
JavaScript
646 lines
23 KiB
JavaScript
'use strict';
|
|
|
|
const assert = require('assert');
|
|
const crypto = require('crypto');
|
|
const fs = require('fs');
|
|
const os = require('os');
|
|
const path = require('path');
|
|
|
|
const {
|
|
applyMultiHarnessPlan,
|
|
createMultiHarnessPlan,
|
|
normalizeGuidedInstallRequest,
|
|
preflightManagedPlan,
|
|
} = require('../../scripts/lib/multi-harness-setup');
|
|
const { createInstallState } = require('../../scripts/lib/install-state');
|
|
|
|
let passed = 0;
|
|
let failed = 0;
|
|
|
|
async function test(name, fn) {
|
|
try {
|
|
await fn();
|
|
console.log(` ✓ ${name}`);
|
|
passed += 1;
|
|
} catch (error) {
|
|
console.log(` ✗ ${name}`);
|
|
console.log(` Error: ${error.message}`);
|
|
failed += 1;
|
|
}
|
|
}
|
|
|
|
function tempDir(prefix) {
|
|
return fs.mkdtempSync(path.join(os.tmpdir(), prefix));
|
|
}
|
|
|
|
function writeFile(filePath, content) {
|
|
fs.mkdirSync(path.dirname(filePath), { recursive: true });
|
|
fs.writeFileSync(filePath, content, 'utf8');
|
|
}
|
|
|
|
function sha256(content) {
|
|
return crypto.createHash('sha256').update(content).digest('hex');
|
|
}
|
|
|
|
function stateOperation(destinationPath, overrides = {}) {
|
|
return {
|
|
kind: 'copy-file',
|
|
moduleId: 'core',
|
|
sourceRelativePath: 'rules/security.md',
|
|
destinationPath,
|
|
strategy: 'preserve-relative-path',
|
|
ownership: 'managed',
|
|
scaffoldOnly: false,
|
|
...overrides,
|
|
};
|
|
}
|
|
|
|
function stateOperationFrom(operation) {
|
|
return stateOperation(operation.destinationPath, {
|
|
kind: operation.kind,
|
|
moduleId: operation.moduleId || 'core',
|
|
sourceRelativePath: operation.sourceRelativePath || 'rules/security.md',
|
|
strategy: operation.strategy || (operation.kind === 'merge-json' ? 'merge-json' : 'preserve-relative-path'),
|
|
ownership: operation.ownership || 'managed',
|
|
scaffoldOnly: Boolean(operation.scaffoldOnly),
|
|
});
|
|
}
|
|
|
|
function managedPlan(root, operations, owned = []) {
|
|
const installStatePath = path.join(root, '.kimi-code', 'ecc-install-state.json');
|
|
const plan = {
|
|
adapter: { id: 'kimi-project', target: 'kimi', kind: 'project' },
|
|
installStatePath,
|
|
operations,
|
|
target: 'kimi',
|
|
targetRoot: root,
|
|
};
|
|
plan.statePreview = createInstallState({
|
|
adapter: plan.adapter,
|
|
installStatePath,
|
|
operations: operations.map(stateOperationFrom),
|
|
request: {},
|
|
resolution: {},
|
|
source: { manifestVersion: 1 },
|
|
targetRoot: root,
|
|
});
|
|
if (owned.length > 0) {
|
|
writeManagedState(plan, {
|
|
operations: owned.map(destinationPath => stateOperation(destinationPath, {
|
|
contentSha256: sha256(fs.readFileSync(destinationPath)),
|
|
})),
|
|
});
|
|
}
|
|
return plan;
|
|
}
|
|
|
|
function writeManagedState(plan, overrides = {}) {
|
|
const state = createInstallState({
|
|
adapter: plan.adapter,
|
|
installStatePath: plan.installStatePath,
|
|
operations: [],
|
|
request: {},
|
|
resolution: {},
|
|
source: { manifestVersion: 1 },
|
|
targetRoot: plan.targetRoot,
|
|
});
|
|
const nextState = {
|
|
...state,
|
|
...overrides,
|
|
target: { ...state.target, ...(overrides.target || {}) },
|
|
operations: overrides.operations || state.operations,
|
|
};
|
|
writeFile(plan.installStatePath, `${JSON.stringify(nextState, null, 2)}\n`);
|
|
return nextState;
|
|
}
|
|
|
|
(async () => {
|
|
console.log('\n=== Multi-harness guided setup tests ===\n');
|
|
|
|
await test('normalizes provider-specific options without inventing shared semantics', () => {
|
|
assert.deepStrictEqual(normalizeGuidedInstallRequest({
|
|
harnesses: ['kimi', 'claude', 'kimi'],
|
|
claudeHooks: 'minimal',
|
|
claudeScope: 'local',
|
|
profile: 'developer',
|
|
}), {
|
|
harnesses: ['claude', 'kimi'],
|
|
claudeHooks: 'minimal',
|
|
claudeScope: 'local',
|
|
dryRun: false,
|
|
json: false,
|
|
profile: 'developer',
|
|
yes: false,
|
|
});
|
|
|
|
assert.throws(
|
|
() => normalizeGuidedInstallRequest({ harnesses: ['kimi'], claudeScope: 'user' }),
|
|
/Claude.*selected/i
|
|
);
|
|
assert.throws(
|
|
() => normalizeGuidedInstallRequest({ harnesses: ['codex'], profile: 'core' }),
|
|
/Kimi.*selected/i
|
|
);
|
|
});
|
|
|
|
await test('classifies missing, identical, managed, and JSON merge destinations', () => {
|
|
const root = tempDir('ecc-guided-preflight-');
|
|
try {
|
|
const sourceSame = path.join(root, 'sources', 'same.md');
|
|
const sourceManaged = path.join(root, 'sources', 'managed.md');
|
|
const destinationSame = path.join(root, 'same.md');
|
|
const destinationManaged = path.join(root, 'managed.md');
|
|
const destinationJson = path.join(root, 'mcp.json');
|
|
writeFile(sourceSame, 'same\n');
|
|
writeFile(sourceManaged, 'new\n');
|
|
writeFile(destinationSame, 'same\n');
|
|
writeFile(destinationManaged, 'old\n');
|
|
writeFile(destinationJson, '{"other":true}\n');
|
|
const plan = managedPlan(root, [
|
|
{ kind: 'copy-file', sourcePath: sourceSame, destinationPath: destinationSame },
|
|
stateOperation(destinationManaged, { sourcePath: sourceManaged }),
|
|
{ kind: 'merge-json', destinationPath: destinationJson, mergePayload: { ecc: true } },
|
|
{ kind: 'copy-file', sourcePath: sourceSame, destinationPath: path.join(root, 'new.md') },
|
|
], [destinationManaged]);
|
|
|
|
const result = preflightManagedPlan(plan);
|
|
assert.deepStrictEqual(result.operations.map(item => item.classification), [
|
|
'identical',
|
|
'managed-update',
|
|
'json-merge',
|
|
'create',
|
|
]);
|
|
} 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 {
|
|
const source = path.join(root, 'source.md');
|
|
const destination = path.join(root, 'AGENTS.md');
|
|
writeFile(source, 'ecc\n');
|
|
writeFile(destination, 'user\n');
|
|
const plan = managedPlan(root, [
|
|
stateOperation(destination, { sourcePath: source }),
|
|
]);
|
|
writeManagedState(plan, {
|
|
target: { id: 'cursor-project', target: 'cursor' },
|
|
operations: [stateOperation(destination)],
|
|
});
|
|
|
|
assert.throws(
|
|
() => preflightManagedPlan(plan),
|
|
/install-state.*target identity|does not belong.*Kimi/i
|
|
);
|
|
assert.strictEqual(fs.readFileSync(destination, 'utf8'), 'user\n');
|
|
} finally {
|
|
fs.rmSync(root, { recursive: true, force: true });
|
|
}
|
|
});
|
|
|
|
await test('rejects install-state with mismatched canonical root or state path', () => {
|
|
const root = tempDir('ecc-guided-forged-paths-');
|
|
const otherRoot = tempDir('ecc-guided-forged-other-');
|
|
try {
|
|
const source = path.join(root, 'source.md');
|
|
const destination = path.join(root, 'AGENTS.md');
|
|
writeFile(source, 'ecc\n');
|
|
writeFile(destination, 'user\n');
|
|
const plan = managedPlan(root, [
|
|
stateOperation(destination, { sourcePath: source }),
|
|
]);
|
|
|
|
for (const target of [
|
|
{ root: otherRoot },
|
|
{ installStatePath: path.join(otherRoot, 'ecc-install-state.json') },
|
|
]) {
|
|
writeManagedState(plan, {
|
|
target,
|
|
operations: [stateOperation(destination)],
|
|
});
|
|
assert.throws(
|
|
() => preflightManagedPlan(plan),
|
|
/install-state.*(root|path).*does not match/i
|
|
);
|
|
}
|
|
} finally {
|
|
fs.rmSync(root, { recursive: true, force: true });
|
|
fs.rmSync(otherRoot, { recursive: true, force: true });
|
|
}
|
|
});
|
|
|
|
await test('rejects install-state ownership claims outside the canonical target root', () => {
|
|
const root = tempDir('ecc-guided-forged-containment-');
|
|
const outside = tempDir('ecc-guided-forged-outside-');
|
|
try {
|
|
const source = path.join(root, 'source.md');
|
|
const destination = path.join(root, 'AGENTS.md');
|
|
writeFile(source, 'ecc\n');
|
|
writeFile(destination, 'user\n');
|
|
const plan = managedPlan(root, [
|
|
stateOperation(destination, { sourcePath: source }),
|
|
]);
|
|
writeManagedState(plan, {
|
|
operations: [stateOperation(path.join(outside, 'AGENTS.md'))],
|
|
});
|
|
|
|
assert.throws(
|
|
() => preflightManagedPlan(plan),
|
|
/install-state.*outside|outside the install root/i
|
|
);
|
|
} finally {
|
|
fs.rmSync(root, { recursive: true, force: true });
|
|
fs.rmSync(outside, { recursive: true, force: true });
|
|
}
|
|
});
|
|
|
|
await test('refuses an unowned differing managed-target file', () => {
|
|
const root = tempDir('ecc-guided-collision-');
|
|
try {
|
|
const source = path.join(root, 'source.md');
|
|
const destination = path.join(root, 'AGENTS.md');
|
|
writeFile(source, 'ecc\n');
|
|
writeFile(destination, 'user\n');
|
|
assert.throws(
|
|
() => preflightManagedPlan(managedPlan(root, [
|
|
{ kind: 'copy-file', sourcePath: source, destinationPath: destination },
|
|
])),
|
|
/unowned.*AGENTS\.md/i
|
|
);
|
|
} finally {
|
|
fs.rmSync(root, { recursive: true, force: true });
|
|
}
|
|
});
|
|
|
|
await test('same-target state without a content digest cannot claim a user file', () => {
|
|
const root = tempDir('ecc-guided-forged-same-target-');
|
|
try {
|
|
const source = path.join(root, 'source.md');
|
|
const destination = path.join(root, 'AGENTS.md');
|
|
writeFile(source, 'ecc\n');
|
|
writeFile(destination, 'user\n');
|
|
const operation = stateOperation(destination, { sourcePath: source });
|
|
const plan = managedPlan(root, [operation]);
|
|
writeManagedState(plan, { operations: [stateOperation(destination)] });
|
|
|
|
assert.throws(
|
|
() => preflightManagedPlan(plan),
|
|
/unverified ownership|content digest|unowned existing file/i
|
|
);
|
|
assert.strictEqual(fs.readFileSync(destination, 'utf8'), 'user\n');
|
|
} finally {
|
|
fs.rmSync(root, { recursive: true, force: true });
|
|
}
|
|
});
|
|
|
|
await test('managed ownership requires an exact operation identity and content digest', () => {
|
|
const root = tempDir('ecc-guided-managed-digest-');
|
|
try {
|
|
const source = path.join(root, 'source.md');
|
|
const destination = path.join(root, 'AGENTS.md');
|
|
writeFile(source, 'new ecc\n');
|
|
writeFile(destination, 'old ecc\n');
|
|
const operation = stateOperation(destination, { sourcePath: source });
|
|
const plan = managedPlan(root, [operation]);
|
|
|
|
writeManagedState(plan, {
|
|
operations: [stateOperation(destination, {
|
|
contentSha256: sha256('old ecc\n'),
|
|
})],
|
|
});
|
|
assert.strictEqual(
|
|
preflightManagedPlan(plan).operations[0].classification,
|
|
'managed-update'
|
|
);
|
|
|
|
writeManagedState(plan, {
|
|
operations: [stateOperation(destination, {
|
|
contentSha256: sha256('old ecc\n'),
|
|
sourceRelativePath: 'rules/other.md',
|
|
})],
|
|
});
|
|
assert.throws(
|
|
() => preflightManagedPlan(plan),
|
|
/operation identity|unverified ownership|unowned existing file/i
|
|
);
|
|
|
|
writeManagedState(plan, {
|
|
operations: [stateOperation(destination, {
|
|
contentSha256: sha256('different bytes\n'),
|
|
})],
|
|
});
|
|
assert.throws(
|
|
() => preflightManagedPlan(plan),
|
|
/content digest|unverified ownership|unowned existing file/i
|
|
);
|
|
} finally {
|
|
fs.rmSync(root, { recursive: true, force: true });
|
|
}
|
|
});
|
|
|
|
await test('refuses a conflicting key in an unowned JSON merge destination', () => {
|
|
const root = tempDir('ecc-guided-json-collision-');
|
|
try {
|
|
const destination = path.join(root, 'mcp.json');
|
|
writeFile(destination, JSON.stringify({
|
|
mcpServers: { github: { command: 'user-owned-server' } },
|
|
}));
|
|
assert.throws(
|
|
() => preflightManagedPlan(managedPlan(root, [
|
|
{
|
|
kind: 'merge-json',
|
|
destinationPath: destination,
|
|
mergePayload: { mcpServers: { github: { command: 'ecc-server' } } },
|
|
},
|
|
])),
|
|
/unowned JSON.*mcpServers\.github\.command/i
|
|
);
|
|
} finally {
|
|
fs.rmSync(root, { recursive: true, force: true });
|
|
}
|
|
});
|
|
|
|
await test('rejects symlinked managed ancestors during batch preflight', () => {
|
|
const root = tempDir('ecc-guided-symlink-root-');
|
|
const outside = tempDir('ecc-guided-symlink-outside-');
|
|
try {
|
|
const source = path.join(root, 'source.md');
|
|
const linkedDirectory = path.join(root, 'rules');
|
|
writeFile(source, 'ecc\n');
|
|
fs.symlinkSync(outside, linkedDirectory, process.platform === 'win32' ? 'junction' : 'dir');
|
|
assert.throws(
|
|
() => preflightManagedPlan(managedPlan(root, [
|
|
{
|
|
kind: 'copy-file',
|
|
sourcePath: source,
|
|
destinationPath: path.join(linkedDirectory, 'security.md'),
|
|
},
|
|
])),
|
|
/outside the install root|symlinked path/i
|
|
);
|
|
assert.strictEqual(fs.existsSync(path.join(outside, 'security.md')), false);
|
|
} finally {
|
|
fs.rmSync(root, { recursive: true, force: true });
|
|
fs.rmSync(outside, { recursive: true, force: true });
|
|
}
|
|
});
|
|
|
|
await test('rejects an unwritable Kimi destination during preflight', () => {
|
|
const root = tempDir('ecc-guided-unwritable-');
|
|
try {
|
|
const destination = path.join(root, '.kimi-code', 'rules', 'security.md');
|
|
const accessChecks = [];
|
|
const accessError = new Error('permission denied');
|
|
accessError.code = 'EACCES';
|
|
assert.throws(
|
|
() => preflightManagedPlan(managedPlan(root, [
|
|
{ kind: 'copy-file', destinationPath: destination },
|
|
]), {
|
|
accessSync(candidatePath, mode) {
|
|
accessChecks.push({ candidatePath, mode });
|
|
throw accessError;
|
|
},
|
|
}),
|
|
error => (
|
|
/Kimi destination is not writable by the current user/i.test(error.message)
|
|
&& error.message.includes(root)
|
|
)
|
|
);
|
|
assert.deepStrictEqual(accessChecks, [{
|
|
candidatePath: root,
|
|
mode: fs.constants.W_OK | fs.constants.X_OK,
|
|
}]);
|
|
} finally {
|
|
fs.rmSync(root, { recursive: true, force: true });
|
|
}
|
|
});
|
|
|
|
await test('real filesystem preflight rejects an unwritable project root', () => {
|
|
if (process.platform === 'win32' || (typeof process.getuid === 'function' && process.getuid() === 0)) {
|
|
return;
|
|
}
|
|
const root = tempDir('ecc-guided-real-permissions-');
|
|
const projectRoot = path.join(root, 'project');
|
|
fs.mkdirSync(projectRoot, { mode: 0o755 });
|
|
try {
|
|
fs.chmodSync(projectRoot, 0o555);
|
|
assert.throws(
|
|
() => preflightManagedPlan(managedPlan(projectRoot, [
|
|
{
|
|
kind: 'copy-file',
|
|
destinationPath: path.join(projectRoot, '.kimi-code', 'rules', 'security.md'),
|
|
},
|
|
])),
|
|
/Kimi destination is not writable by the current user/i
|
|
);
|
|
assert.strictEqual(fs.existsSync(path.join(projectRoot, '.kimi-code')), false);
|
|
} finally {
|
|
fs.chmodSync(projectRoot, 0o755);
|
|
fs.rmSync(root, { recursive: true, force: true });
|
|
}
|
|
});
|
|
|
|
await test('preflights every selected harness before applying any mutation', async () => {
|
|
const events = [];
|
|
const request = normalizeGuidedInstallRequest({
|
|
harnesses: ['claude', 'codex', 'kimi'],
|
|
claudeHooks: 'standard',
|
|
claudeScope: 'user',
|
|
profile: 'core',
|
|
});
|
|
await assert.rejects(
|
|
() => createMultiHarnessPlan(request, {
|
|
previewClaude: async () => events.push('preview:claude'),
|
|
previewCodex: async () => events.push('preview:codex'),
|
|
createManagedPlan: async () => ({ target: 'kimi' }),
|
|
preflightManaged: async () => {
|
|
events.push('preview:kimi');
|
|
throw new Error('unowned collision');
|
|
},
|
|
}),
|
|
/collision/
|
|
);
|
|
assert.deepStrictEqual(events, ['preview:claude', 'preview:codex', 'preview:kimi']);
|
|
});
|
|
|
|
await test('refuses a copy-file destination created after preview but before apply', async () => {
|
|
const root = tempDir('ecc-guided-late-copy-collision-');
|
|
try {
|
|
const source = path.join(root, 'source.md');
|
|
const destination = path.join(root, '.kimi-code', 'rules', 'security.md');
|
|
writeFile(source, 'ecc\n');
|
|
const plan = managedPlan(root, [stateOperation(destination, { sourcePath: source })]);
|
|
const preview = preflightManagedPlan(plan);
|
|
|
|
const result = await applyMultiHarnessPlan({
|
|
harnesses: [{ id: 'kimi', preview }],
|
|
request: { harnesses: ['kimi'] },
|
|
}, {
|
|
preflightManaged(candidatePlan) {
|
|
const latestPreview = preflightManagedPlan(candidatePlan);
|
|
writeFile(destination, 'user\n');
|
|
return latestPreview;
|
|
},
|
|
});
|
|
|
|
assert.strictEqual(result.status, 'failed');
|
|
assert.match(result.failure.message, /unowned existing file/i);
|
|
assert.deepStrictEqual(result.retryHarnesses, ['kimi']);
|
|
assert.strictEqual(fs.readFileSync(destination, 'utf8'), 'user\n');
|
|
} finally {
|
|
fs.rmSync(root, { recursive: true, force: true });
|
|
}
|
|
});
|
|
|
|
await test('refuses a late unowned copy even when its bytes match the ECC source', async () => {
|
|
const root = tempDir('ecc-guided-late-identical-copy-');
|
|
try {
|
|
const source = path.join(root, 'source.md');
|
|
const destination = path.join(root, '.kimi-code', 'rules', 'security.md');
|
|
writeFile(source, 'ecc\n');
|
|
const plan = managedPlan(root, [stateOperation(destination, { sourcePath: source })]);
|
|
const preview = preflightManagedPlan(plan);
|
|
|
|
const result = await applyMultiHarnessPlan({
|
|
harnesses: [{ id: 'kimi', preview }],
|
|
request: { harnesses: ['kimi'] },
|
|
}, {
|
|
preflightManaged(candidatePlan) {
|
|
const latestPreview = preflightManagedPlan(candidatePlan);
|
|
writeFile(destination, 'ecc\n');
|
|
return latestPreview;
|
|
},
|
|
});
|
|
|
|
assert.strictEqual(result.status, 'failed');
|
|
assert.match(result.failure.message, /destination changed after Kimi preflight/i);
|
|
assert.deepStrictEqual(result.retryHarnesses, ['kimi']);
|
|
assert.strictEqual(fs.readFileSync(destination, 'utf8'), 'ecc\n');
|
|
} finally {
|
|
fs.rmSync(root, { recursive: true, force: true });
|
|
}
|
|
});
|
|
|
|
await test('refuses conflicting JSON created after preview but before apply', async () => {
|
|
const root = tempDir('ecc-guided-late-json-collision-');
|
|
try {
|
|
const destination = path.join(root, '.kimi-code', 'mcp.json');
|
|
const operation = stateOperation(destination, {
|
|
kind: 'merge-json',
|
|
mergePayload: { mcpServers: { github: { command: 'ecc-server' } } },
|
|
sourceRelativePath: '.mcp.json',
|
|
strategy: 'merge-json',
|
|
});
|
|
const plan = managedPlan(root, [operation]);
|
|
const preview = preflightManagedPlan(plan);
|
|
|
|
const result = await applyMultiHarnessPlan({
|
|
harnesses: [{ id: 'kimi', preview }],
|
|
request: { harnesses: ['kimi'] },
|
|
}, {
|
|
preflightManaged(candidatePlan) {
|
|
const latestPreview = preflightManagedPlan(candidatePlan);
|
|
writeFile(destination, JSON.stringify({
|
|
mcpServers: { github: { command: 'user-server' } },
|
|
}));
|
|
return latestPreview;
|
|
},
|
|
});
|
|
|
|
assert.strictEqual(result.status, 'failed');
|
|
assert.match(result.failure.message, /unowned JSON.*mcpServers\.github\.command/i);
|
|
assert.deepStrictEqual(result.retryHarnesses, ['kimi']);
|
|
assert.deepStrictEqual(JSON.parse(fs.readFileSync(destination, 'utf8')), {
|
|
mcpServers: { github: { command: 'user-server' } },
|
|
});
|
|
} finally {
|
|
fs.rmSync(root, { recursive: true, force: true });
|
|
}
|
|
});
|
|
|
|
await test('refuses an install-state file created after preview instead of overwriting it', async () => {
|
|
const root = tempDir('ecc-guided-late-state-collision-');
|
|
try {
|
|
const source = path.join(root, 'source.md');
|
|
const destination = path.join(root, '.kimi-code', 'rules', 'security.md');
|
|
writeFile(source, 'ecc\n');
|
|
const plan = managedPlan(root, [stateOperation(destination, { sourcePath: source })]);
|
|
const preview = preflightManagedPlan(plan);
|
|
const unexpectedState = '{"user":"owned"}\n';
|
|
|
|
const result = await applyMultiHarnessPlan({
|
|
harnesses: [{ id: 'kimi', preview }],
|
|
request: { harnesses: ['kimi'] },
|
|
}, {
|
|
preflightManaged(candidatePlan) {
|
|
const latestPreview = preflightManagedPlan(candidatePlan);
|
|
writeFile(plan.installStatePath, unexpectedState);
|
|
return latestPreview;
|
|
},
|
|
});
|
|
|
|
assert.strictEqual(result.status, 'failed');
|
|
assert.match(result.failure.message, /unowned or changed install-state/i);
|
|
assert.deepStrictEqual(result.retryHarnesses, ['kimi']);
|
|
assert.strictEqual(fs.existsSync(destination), false);
|
|
assert.strictEqual(fs.readFileSync(plan.installStatePath, 'utf8'), unexpectedState);
|
|
} finally {
|
|
fs.rmSync(root, { recursive: true, force: true });
|
|
}
|
|
});
|
|
|
|
await test('applies in catalog order and reports partial completion with an exact retry set', async () => {
|
|
const plan = {
|
|
harnesses: [
|
|
{ id: 'claude', preview: {} },
|
|
{ id: 'codex', preview: {} },
|
|
{ id: 'kimi', preview: {} },
|
|
],
|
|
request: { harnesses: ['claude', 'codex', 'kimi'] },
|
|
};
|
|
const events = [];
|
|
const result = await applyMultiHarnessPlan(plan, {
|
|
applyClaude: async () => { events.push('claude'); return { action: 'installed' }; },
|
|
applyCodex: async () => { events.push('codex'); throw new Error('verification failed'); },
|
|
applyManaged: async () => { events.push('kimi'); return { applied: true }; },
|
|
});
|
|
assert.deepStrictEqual(events, ['claude', 'codex']);
|
|
assert.strictEqual(result.status, 'partial');
|
|
assert.deepStrictEqual(result.completed.map(item => item.id), ['claude']);
|
|
assert.strictEqual(result.failure.id, 'codex');
|
|
assert.deepStrictEqual(result.retryHarnesses, ['codex', 'kimi']);
|
|
});
|
|
|
|
await test('a late Kimi permission failure retries only Kimi', async () => {
|
|
const plan = {
|
|
harnesses: [
|
|
{ id: 'claude', preview: {} },
|
|
{ id: 'codex', preview: {} },
|
|
{ id: 'kimi', preview: {} },
|
|
],
|
|
request: { harnesses: ['claude', 'codex', 'kimi'] },
|
|
};
|
|
const events = [];
|
|
const result = await applyMultiHarnessPlan(plan, {
|
|
applyClaude: async () => { events.push('claude'); return { action: 'installed' }; },
|
|
applyCodex: async () => { events.push('codex'); return { action: 'installed' }; },
|
|
applyManaged: async () => {
|
|
events.push('kimi');
|
|
const error = new Error('permission denied');
|
|
error.code = 'EACCES';
|
|
throw error;
|
|
},
|
|
});
|
|
assert.deepStrictEqual(events, ['claude', 'codex', 'kimi']);
|
|
assert.strictEqual(result.status, 'partial');
|
|
assert.deepStrictEqual(result.completed.map(item => item.id), ['claude', 'codex']);
|
|
assert.strictEqual(result.failure.id, 'kimi');
|
|
assert.deepStrictEqual(result.retryHarnesses, ['kimi']);
|
|
});
|
|
|
|
console.log(`\nResults: Passed: ${passed}, Failed: ${failed}`);
|
|
process.exitCode = failed > 0 ? 1 : 0;
|
|
})();
|