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>
631 lines
22 KiB
JavaScript
631 lines
22 KiB
JavaScript
'use strict';
|
|
|
|
const assert = require('assert');
|
|
|
|
const {
|
|
CodexPluginSetupError,
|
|
OFFICIAL_MARKETPLACE_REPO,
|
|
executeFile,
|
|
normalizeGitHubGitOrigin,
|
|
parseMarketplaceInventory,
|
|
parseMarketplaceUpgradeResult,
|
|
parsePluginInventory,
|
|
reconcileCodexPlugin,
|
|
resolveMarketplaceRepository,
|
|
} = require('../../scripts/lib/codex-plugin-setup');
|
|
|
|
const MARKETPLACE_LIST = ['plugin', 'marketplace', 'list', '--json'];
|
|
const PLUGIN_LIST = ['plugin', 'list', '--json'];
|
|
const MARKETPLACE_ADD = [
|
|
'plugin', 'marketplace', 'add', OFFICIAL_MARKETPLACE_REPO, '--json',
|
|
];
|
|
const MARKETPLACE_UPGRADE = [
|
|
'plugin', 'marketplace', 'upgrade', 'ecc', '--json',
|
|
];
|
|
const PLUGIN_ADD = ['plugin', 'add', 'ecc@ecc', '--json'];
|
|
|
|
function marketplaceInventory(installed = false) {
|
|
return JSON.stringify({
|
|
marketplaces: installed ? [{ name: 'ecc', root: '/cache/ecc' }] : [],
|
|
});
|
|
}
|
|
|
|
function pluginInventory(installed = false, overrides = {}) {
|
|
const ecc = {
|
|
pluginId: 'ecc@ecc',
|
|
name: 'ecc',
|
|
marketplaceName: 'ecc',
|
|
version: '2.0.0',
|
|
installed: true,
|
|
enabled: true,
|
|
...overrides,
|
|
};
|
|
return JSON.stringify({
|
|
installed: installed ? [ecc] : [],
|
|
available: [],
|
|
});
|
|
}
|
|
|
|
function marketplaceUpgradeResult(overrides = {}) {
|
|
return JSON.stringify({
|
|
selectedMarketplaces: ['ecc'],
|
|
upgradedRoots: ['/cache/ecc'],
|
|
errors: [],
|
|
...overrides,
|
|
});
|
|
}
|
|
|
|
function createExecFile(steps) {
|
|
const calls = [];
|
|
const execFile = (command, args, options, callback) => {
|
|
calls.push({ command, args: [...args], options: { ...options } });
|
|
const step = steps[calls.length - 1];
|
|
if (!step) {
|
|
callback(new Error(`Unexpected Codex invocation: ${args.join(' ')}`));
|
|
return;
|
|
}
|
|
if (step.command) assert.strictEqual(command, step.command);
|
|
assert.deepStrictEqual(args, step.args);
|
|
callback(step.error || null, step.stdout || '', step.stderr || '');
|
|
};
|
|
return { calls, execFile };
|
|
}
|
|
|
|
function dependenciesFor(fake, overrides = {}) {
|
|
return {
|
|
execFile: fake.execFile,
|
|
resolveMarketplaceRepository: async () => 'https://github.com/affaan-m/ECC.git',
|
|
...overrides,
|
|
};
|
|
}
|
|
|
|
async function expectSetupError(promise, code, messagePattern) {
|
|
await assert.rejects(promise, error => {
|
|
assert.ok(error instanceof CodexPluginSetupError);
|
|
assert.strictEqual(error.code, code);
|
|
assert.match(error.message, messagePattern);
|
|
return true;
|
|
});
|
|
}
|
|
|
|
async function test(name, fn) {
|
|
try {
|
|
await fn();
|
|
console.log(` \u2713 ${name}`);
|
|
return true;
|
|
} catch (error) {
|
|
console.log(` \u2717 ${name}`);
|
|
console.log(` Error: ${error.stack || error.message}`);
|
|
return false;
|
|
}
|
|
}
|
|
|
|
async function runTests() {
|
|
console.log('\n=== Codex native plugin setup library tests ===\n');
|
|
let passed = 0;
|
|
let failed = 0;
|
|
|
|
const cases = [
|
|
['parses current Codex marketplace and plugin JSON inventory shapes', () => {
|
|
assert.deepStrictEqual(
|
|
parseMarketplaceInventory(marketplaceInventory(true)),
|
|
[{ name: 'ecc', root: '/cache/ecc' }]
|
|
);
|
|
assert.strictEqual(
|
|
parsePluginInventory(pluginInventory(true)).installed[0].pluginId,
|
|
'ecc@ecc'
|
|
);
|
|
assert.strictEqual(
|
|
normalizeGitHubGitOrigin('git@github.com:affaan-m/ECC.git'),
|
|
'affaan-m/ecc'
|
|
);
|
|
}],
|
|
['resolves marketplace provenance with execFile and exact Git argv', async () => {
|
|
const fake = createExecFile([{
|
|
command: 'git',
|
|
args: ['-C', '/cache/ecc', 'remote', 'get-url', 'origin'],
|
|
stdout: 'https://github.com/affaan-m/ECC.git\n',
|
|
}]);
|
|
|
|
const repository = await resolveMarketplaceRepository(
|
|
{ name: 'ecc', root: '/cache/ecc' },
|
|
{ cwd: '/workspace with spaces' },
|
|
{ execFile: fake.execFile }
|
|
);
|
|
|
|
assert.strictEqual(repository, 'https://github.com/affaan-m/ECC.git');
|
|
assert.strictEqual(fake.calls[0].options.shell, false);
|
|
assert.strictEqual(fake.calls[0].options.cwd, '/workspace with spaces');
|
|
assert.ok(fake.calls[0].options.timeout > 0);
|
|
assert.strictEqual(fake.calls[0].options.killSignal, 'SIGKILL');
|
|
}],
|
|
['preserves the original execFile error and attaches callback output', async () => {
|
|
const original = new Error('provider failed');
|
|
const execFile = (command, args, options, callback) => {
|
|
callback(original, 'partial stdout', 'partial stderr');
|
|
};
|
|
|
|
await assert.rejects(
|
|
executeFile(execFile, 'codex', ['plugin', 'list'], {}),
|
|
error => {
|
|
assert.strictEqual(error, original);
|
|
assert.strictEqual(error.stdout, 'partial stdout');
|
|
assert.strictEqual(error.stderr, 'partial stderr');
|
|
assert.match(error.stack, /provider failed/);
|
|
return true;
|
|
}
|
|
);
|
|
}],
|
|
['maps provider and provenance timeouts to distinct structured errors', async () => {
|
|
const providerTimeout = Object.assign(new Error('timed out'), {
|
|
code: 'ETIMEDOUT',
|
|
killed: true,
|
|
signal: 'SIGKILL',
|
|
});
|
|
const provider = createExecFile([{ args: MARKETPLACE_LIST, error: providerTimeout }]);
|
|
await expectSetupError(
|
|
reconcileCodexPlugin({}, dependenciesFor(provider)),
|
|
'CODEX_COMMAND_TIMEOUT',
|
|
/timed out/i
|
|
);
|
|
|
|
const provenanceTimeout = Object.assign(new Error('timed out'), {
|
|
code: 'ETIMEDOUT',
|
|
killed: true,
|
|
signal: 'SIGKILL',
|
|
});
|
|
const provenance = createExecFile([{
|
|
command: 'git',
|
|
args: ['-C', '/cache/ecc', 'remote', 'get-url', 'origin'],
|
|
error: provenanceTimeout,
|
|
}]);
|
|
await expectSetupError(
|
|
resolveMarketplaceRepository(
|
|
{ name: 'ecc', root: '/cache/ecc' },
|
|
{},
|
|
{ execFile: provenance.execFile }
|
|
),
|
|
'MARKETPLACE_PROVENANCE_TIMEOUT',
|
|
/timed out/i
|
|
);
|
|
assert.ok(provenance.calls[0].options.timeout > 0);
|
|
assert.strictEqual(provenance.calls[0].options.killSignal, 'SIGKILL');
|
|
}],
|
|
['rejects unverifiable Git provenance without using a shell', async () => {
|
|
const gitFailure = Object.assign(new Error('no origin'), {
|
|
stderr: 'fatal: No such remote origin',
|
|
});
|
|
const fake = createExecFile([{
|
|
command: 'git',
|
|
args: ['-C', '/cache/ecc', 'remote', 'get-url', 'origin'],
|
|
error: gitFailure,
|
|
}]);
|
|
|
|
await expectSetupError(
|
|
resolveMarketplaceRepository(
|
|
{ name: 'ecc', root: '/cache/ecc' },
|
|
{},
|
|
{ execFile: fake.execFile }
|
|
),
|
|
'MARKETPLACE_COLLISION',
|
|
/provenance could not be verified/i
|
|
);
|
|
assert.strictEqual(fake.calls[0].options.shell, false);
|
|
}],
|
|
['fresh install uses exact native Codex argv and verifies both mutations', async () => {
|
|
const fake = createExecFile([
|
|
{ args: MARKETPLACE_LIST, stdout: marketplaceInventory(false) },
|
|
{ args: PLUGIN_LIST, stdout: pluginInventory(false) },
|
|
{ args: MARKETPLACE_ADD, stdout: '{"alreadyAdded":false}' },
|
|
{ args: MARKETPLACE_LIST, stdout: marketplaceInventory(true) },
|
|
{ args: PLUGIN_ADD, stdout: '{"pluginId":"ecc@ecc"}' },
|
|
{ args: PLUGIN_LIST, stdout: pluginInventory(true) },
|
|
]);
|
|
|
|
const result = await reconcileCodexPlugin(
|
|
{ cwd: '/workspace with spaces' },
|
|
dependenciesFor(fake)
|
|
);
|
|
|
|
assert.deepStrictEqual(result, {
|
|
action: 'installed',
|
|
marketplaceAction: 'added',
|
|
pluginId: 'ecc@ecc',
|
|
restartRequired: true,
|
|
});
|
|
assert.deepStrictEqual(fake.calls.map(call => call.args), [
|
|
MARKETPLACE_LIST,
|
|
PLUGIN_LIST,
|
|
MARKETPLACE_ADD,
|
|
MARKETPLACE_LIST,
|
|
PLUGIN_ADD,
|
|
PLUGIN_LIST,
|
|
]);
|
|
for (const call of fake.calls) {
|
|
assert.strictEqual(call.command, 'codex');
|
|
assert.strictEqual(call.options.cwd, '/workspace with spaces');
|
|
assert.strictEqual(call.options.shell, false);
|
|
assert.ok(call.options.timeout > 0);
|
|
assert.strictEqual(call.options.killSignal, 'SIGKILL');
|
|
assert.ok(!call.args.includes('--config'));
|
|
assert.ok(!call.args.includes('-c'));
|
|
}
|
|
}],
|
|
['already installed and enabled is refreshed and strongly verified', async () => {
|
|
const fake = createExecFile([
|
|
{ args: MARKETPLACE_LIST, stdout: marketplaceInventory(true) },
|
|
{ args: PLUGIN_LIST, stdout: pluginInventory(true) },
|
|
{ args: MARKETPLACE_UPGRADE, stdout: marketplaceUpgradeResult() },
|
|
{ args: MARKETPLACE_LIST, stdout: marketplaceInventory(true) },
|
|
{ args: PLUGIN_LIST, stdout: pluginInventory(true) },
|
|
]);
|
|
|
|
const result = await reconcileCodexPlugin({}, dependenciesFor(fake));
|
|
|
|
assert.deepStrictEqual(result, {
|
|
action: 'updated',
|
|
marketplaceAction: 'upgraded',
|
|
pluginId: 'ecc@ecc',
|
|
restartRequired: true,
|
|
});
|
|
assert.deepStrictEqual(fake.calls.map(call => call.args), [
|
|
MARKETPLACE_LIST,
|
|
PLUGIN_LIST,
|
|
MARKETPLACE_UPGRADE,
|
|
MARKETPLACE_LIST,
|
|
PLUGIN_LIST,
|
|
]);
|
|
}],
|
|
['fails closed when native refresh does not confirm the marketplace root', async () => {
|
|
const fake = createExecFile([
|
|
{ args: MARKETPLACE_LIST, stdout: marketplaceInventory(true) },
|
|
{ args: PLUGIN_LIST, stdout: pluginInventory(true) },
|
|
{
|
|
args: MARKETPLACE_UPGRADE,
|
|
stdout: marketplaceUpgradeResult({ upgradedRoots: [] }),
|
|
},
|
|
]);
|
|
|
|
await assert.rejects(
|
|
reconcileCodexPlugin({}, dependenciesFor(fake)),
|
|
error => {
|
|
assert.strictEqual(error.code, 'MARKETPLACE_REFRESH_FAILED');
|
|
assert.strictEqual(error.phase, 'marketplace-upgrade');
|
|
assert.deepStrictEqual(error.argv, MARKETPLACE_UPGRADE);
|
|
assert.match(error.message, /did not confirm.*refreshed/i);
|
|
return true;
|
|
}
|
|
);
|
|
assert.strictEqual(fake.calls.length, 3);
|
|
}],
|
|
['accepts the provider root across Windows separator and case differences', () => {
|
|
const result = parseMarketplaceUpgradeResult(
|
|
marketplaceUpgradeResult({
|
|
upgradedRoots: ['c:/users/hira/.codex/marketplaces/ecc'],
|
|
}),
|
|
{ name: 'ecc', root: 'C:\\Users\\Hira\\.codex\\marketplaces\\ecc' }
|
|
);
|
|
|
|
assert.deepStrictEqual(result.selectedMarketplaces, ['ecc']);
|
|
}],
|
|
['rejects ambiguous native refresh results for a targeted upgrade', () => {
|
|
assert.throws(
|
|
() => parseMarketplaceUpgradeResult(
|
|
marketplaceUpgradeResult({
|
|
selectedMarketplaces: ['ecc', 'other'],
|
|
upgradedRoots: ['/cache/ecc', '/cache/other'],
|
|
}),
|
|
{ name: 'ecc', root: '/cache/ecc' }
|
|
),
|
|
error => (
|
|
error.code === 'MARKETPLACE_REFRESH_FAILED'
|
|
&& error.phase === 'marketplace-upgrade'
|
|
)
|
|
);
|
|
assert.throws(
|
|
() => parseMarketplaceUpgradeResult(
|
|
marketplaceUpgradeResult({ errors: [{ message: 'dirty checkout' }] }),
|
|
{ name: 'ecc', root: '/cache/ecc' }
|
|
),
|
|
error => error.code === 'MARKETPLACE_REFRESH_FAILED'
|
|
);
|
|
}],
|
|
['fails closed when post-refresh marketplace provenance changes', async () => {
|
|
const fake = createExecFile([
|
|
{ args: MARKETPLACE_LIST, stdout: marketplaceInventory(true) },
|
|
{ args: PLUGIN_LIST, stdout: pluginInventory(true) },
|
|
{ args: MARKETPLACE_UPGRADE, stdout: marketplaceUpgradeResult() },
|
|
{ args: MARKETPLACE_LIST, stdout: marketplaceInventory(true) },
|
|
]);
|
|
let provenanceChecks = 0;
|
|
|
|
await expectSetupError(
|
|
reconcileCodexPlugin({}, dependenciesFor(fake, {
|
|
resolveMarketplaceRepository: async () => {
|
|
provenanceChecks += 1;
|
|
return provenanceChecks === 1
|
|
? 'https://github.com/affaan-m/ECC.git'
|
|
: 'https://github.com/attacker/ecc.git';
|
|
},
|
|
})),
|
|
'MARKETPLACE_COLLISION',
|
|
/not the official/i
|
|
);
|
|
assert.strictEqual(provenanceChecks, 2);
|
|
assert.strictEqual(fake.calls.length, 4);
|
|
}],
|
|
['fails closed on malformed post-refresh plugin inventory', async () => {
|
|
const fake = createExecFile([
|
|
{ args: MARKETPLACE_LIST, stdout: marketplaceInventory(true) },
|
|
{ args: PLUGIN_LIST, stdout: pluginInventory(true) },
|
|
{ args: MARKETPLACE_UPGRADE, stdout: marketplaceUpgradeResult() },
|
|
{ args: MARKETPLACE_LIST, stdout: marketplaceInventory(true) },
|
|
{ args: PLUGIN_LIST, stdout: '{not json' },
|
|
]);
|
|
|
|
await assert.rejects(
|
|
reconcileCodexPlugin({}, dependenciesFor(fake)),
|
|
error => {
|
|
assert.strictEqual(error.code, 'INVALID_PLUGIN_INVENTORY');
|
|
assert.strictEqual(error.phase, 'plugin-verification');
|
|
return true;
|
|
}
|
|
);
|
|
assert.strictEqual(fake.calls.length, 5);
|
|
}],
|
|
['dry-run fresh install is inventory-only planning', async () => {
|
|
const fake = createExecFile([
|
|
{ args: MARKETPLACE_LIST, stdout: marketplaceInventory(false) },
|
|
{ args: PLUGIN_LIST, stdout: pluginInventory(false) },
|
|
]);
|
|
|
|
const result = await reconcileCodexPlugin(
|
|
{ dryRun: true },
|
|
dependenciesFor(fake)
|
|
);
|
|
|
|
assert.deepStrictEqual(result, {
|
|
action: 'would-install',
|
|
dryRun: true,
|
|
marketplaceAction: 'would-add',
|
|
pluginId: 'ecc@ecc',
|
|
restartRequired: true,
|
|
});
|
|
assert.deepStrictEqual(fake.calls.map(call => call.args), [
|
|
MARKETPLACE_LIST,
|
|
PLUGIN_LIST,
|
|
]);
|
|
}],
|
|
['dry-run repair is inventory-only update planning', async () => {
|
|
const fake = createExecFile([
|
|
{ args: MARKETPLACE_LIST, stdout: marketplaceInventory(false) },
|
|
{ args: PLUGIN_LIST, stdout: pluginInventory(true, { enabled: false }) },
|
|
]);
|
|
|
|
const result = await reconcileCodexPlugin(
|
|
{ dryRun: true },
|
|
dependenciesFor(fake)
|
|
);
|
|
|
|
assert.deepStrictEqual(result, {
|
|
action: 'would-update',
|
|
dryRun: true,
|
|
marketplaceAction: 'would-add',
|
|
pluginId: 'ecc@ecc',
|
|
restartRequired: true,
|
|
});
|
|
assert.strictEqual(fake.calls.length, 2);
|
|
}],
|
|
['dry-run keeps reconciled state unchanged without mutation', async () => {
|
|
const fake = createExecFile([
|
|
{ args: MARKETPLACE_LIST, stdout: marketplaceInventory(true) },
|
|
{ args: PLUGIN_LIST, stdout: pluginInventory(true) },
|
|
]);
|
|
|
|
const result = await reconcileCodexPlugin(
|
|
{ dryRun: true },
|
|
dependenciesFor(fake)
|
|
);
|
|
|
|
assert.deepStrictEqual(result, {
|
|
action: 'unchanged',
|
|
dryRun: true,
|
|
marketplaceAction: 'would-upgrade',
|
|
pluginId: 'ecc@ecc',
|
|
restartRequired: false,
|
|
});
|
|
assert.strictEqual(fake.calls.length, 2);
|
|
}],
|
|
['upgrades an existing marketplace before installing a missing plugin', async () => {
|
|
const fake = createExecFile([
|
|
{ args: MARKETPLACE_LIST, stdout: marketplaceInventory(true) },
|
|
{ args: PLUGIN_LIST, stdout: pluginInventory(false) },
|
|
{ args: MARKETPLACE_UPGRADE, stdout: marketplaceUpgradeResult() },
|
|
{ args: MARKETPLACE_LIST, stdout: marketplaceInventory(true) },
|
|
{ args: PLUGIN_LIST, stdout: pluginInventory(false) },
|
|
{ args: PLUGIN_ADD, stdout: '{"pluginId":"ecc@ecc"}' },
|
|
{ args: PLUGIN_LIST, stdout: pluginInventory(true) },
|
|
]);
|
|
|
|
const result = await reconcileCodexPlugin({}, dependenciesFor(fake));
|
|
|
|
assert.strictEqual(result.action, 'installed');
|
|
assert.strictEqual(result.marketplaceAction, 'upgraded');
|
|
assert.deepStrictEqual(fake.calls.map(call => call.args), [
|
|
MARKETPLACE_LIST,
|
|
PLUGIN_LIST,
|
|
MARKETPLACE_UPGRADE,
|
|
MARKETPLACE_LIST,
|
|
PLUGIN_LIST,
|
|
PLUGIN_ADD,
|
|
PLUGIN_LIST,
|
|
]);
|
|
}],
|
|
['fails closed when the ecc marketplace has untrusted provenance', async () => {
|
|
const fake = createExecFile([
|
|
{ args: MARKETPLACE_LIST, stdout: marketplaceInventory(true) },
|
|
{ args: PLUGIN_LIST, stdout: pluginInventory(false) },
|
|
]);
|
|
|
|
await expectSetupError(
|
|
reconcileCodexPlugin({}, dependenciesFor(fake, {
|
|
resolveMarketplaceRepository: async marketplace => {
|
|
assert.strictEqual(marketplace.root, '/cache/ecc');
|
|
return 'https://github.com/attacker/ecc.git';
|
|
},
|
|
})),
|
|
'MARKETPLACE_COLLISION',
|
|
/refusing.*ecc.*marketplace/i
|
|
);
|
|
assert.deepStrictEqual(fake.calls.map(call => call.args), [
|
|
MARKETPLACE_LIST,
|
|
PLUGIN_LIST,
|
|
]);
|
|
}],
|
|
['rejects relative and insecure Git origins before marketplace mutation', async () => {
|
|
for (const origin of [
|
|
'affaan-m/ecc',
|
|
'http://github.com/affaan-m/ECC.git',
|
|
]) {
|
|
const fake = createExecFile([
|
|
{ args: MARKETPLACE_LIST, stdout: marketplaceInventory(true) },
|
|
{ args: PLUGIN_LIST, stdout: pluginInventory(false) },
|
|
]);
|
|
await expectSetupError(
|
|
reconcileCodexPlugin({}, dependenciesFor(fake, {
|
|
resolveMarketplaceRepository: async () => origin,
|
|
})),
|
|
'MARKETPLACE_COLLISION',
|
|
/not the official/i
|
|
);
|
|
assert.deepStrictEqual(fake.calls.map(call => call.args), [
|
|
MARKETPLACE_LIST,
|
|
PLUGIN_LIST,
|
|
]);
|
|
}
|
|
}],
|
|
['reports a missing Codex CLI without attempting another command', async () => {
|
|
const missing = Object.assign(new Error('spawn codex ENOENT'), { code: 'ENOENT' });
|
|
const fake = createExecFile([{ args: MARKETPLACE_LIST, error: missing }]);
|
|
|
|
await expectSetupError(
|
|
reconcileCodexPlugin({}, dependenciesFor(fake)),
|
|
'CODEX_NOT_FOUND',
|
|
/not installed|not on path/i
|
|
);
|
|
assert.strictEqual(fake.calls.length, 1);
|
|
}],
|
|
['rejects malformed marketplace and plugin JSON inventories', async () => {
|
|
assert.throws(
|
|
() => parseMarketplaceInventory('{not json'),
|
|
error => error.code === 'INVALID_MARKETPLACE_INVENTORY'
|
|
);
|
|
assert.throws(
|
|
() => parsePluginInventory('{"installed":{}}'),
|
|
error => error.code === 'INVALID_PLUGIN_INVENTORY'
|
|
);
|
|
assert.throws(
|
|
() => parseMarketplaceInventory(JSON.stringify({
|
|
marketplaces: [
|
|
{ name: 'ecc', root: '/one' },
|
|
{ name: 'ecc', root: '/two' },
|
|
],
|
|
})),
|
|
error => error.code === 'INVALID_MARKETPLACE_INVENTORY'
|
|
);
|
|
assert.throws(
|
|
() => parseMarketplaceInventory('{"marketplaces":[{"name":"ecc","root":""}]}'),
|
|
error => error.code === 'INVALID_MARKETPLACE_INVENTORY'
|
|
);
|
|
assert.throws(
|
|
() => parsePluginInventory('{"installed":[],"available":[{}]}'),
|
|
error => error.code === 'INVALID_PLUGIN_INVENTORY'
|
|
);
|
|
assert.throws(
|
|
() => parsePluginInventory(JSON.stringify({
|
|
installed: [
|
|
{ pluginId: 'ecc@ecc', installed: true, enabled: true },
|
|
{ pluginId: 'ecc@ecc', installed: true, enabled: true },
|
|
],
|
|
available: [],
|
|
})),
|
|
error => error.code === 'INVALID_PLUGIN_INVENTORY'
|
|
);
|
|
assert.strictEqual(normalizeGitHubGitOrigin(null), null);
|
|
assert.strictEqual(normalizeGitHubGitOrigin('not a repository'), null);
|
|
assert.strictEqual(normalizeGitHubGitOrigin('affaan-m/ECC'), null);
|
|
assert.strictEqual(
|
|
normalizeGitHubGitOrigin('http://github.com/affaan-m/ECC.git'),
|
|
null
|
|
);
|
|
}],
|
|
['surfaces mutation command failures with phase and exact argv', async () => {
|
|
const commandFailure = Object.assign(new Error('upgrade failed'), {
|
|
code: 1,
|
|
stderr: 'network unavailable',
|
|
});
|
|
const fake = createExecFile([
|
|
{ args: MARKETPLACE_LIST, stdout: marketplaceInventory(true) },
|
|
{ args: PLUGIN_LIST, stdout: pluginInventory(false) },
|
|
{ args: MARKETPLACE_UPGRADE, error: commandFailure },
|
|
]);
|
|
|
|
await assert.rejects(
|
|
reconcileCodexPlugin({}, dependenciesFor(fake)),
|
|
error => {
|
|
assert.strictEqual(error.code, 'CODEX_COMMAND_FAILED');
|
|
assert.strictEqual(error.phase, 'marketplace-upgrade');
|
|
assert.deepStrictEqual(error.argv, MARKETPLACE_UPGRADE);
|
|
assert.match(error.message, /network unavailable/);
|
|
return true;
|
|
}
|
|
);
|
|
}],
|
|
['fails when post-install verification does not find enabled ECC', async () => {
|
|
const fake = createExecFile([
|
|
{ args: MARKETPLACE_LIST, stdout: marketplaceInventory(false) },
|
|
{ args: PLUGIN_LIST, stdout: pluginInventory(false) },
|
|
{ args: MARKETPLACE_ADD, stdout: '{"alreadyAdded":false}' },
|
|
{ args: MARKETPLACE_LIST, stdout: marketplaceInventory(true) },
|
|
{ args: PLUGIN_ADD, stdout: '{"pluginId":"ecc@ecc"}' },
|
|
{ args: PLUGIN_LIST, stdout: pluginInventory(false) },
|
|
]);
|
|
|
|
await expectSetupError(
|
|
reconcileCodexPlugin({}, dependenciesFor(fake)),
|
|
'PLUGIN_VERIFICATION_FAILED',
|
|
/verify.*ecc@ecc/i
|
|
);
|
|
}],
|
|
['fails when marketplace verification cannot observe ECC', async () => {
|
|
const fake = createExecFile([
|
|
{ args: MARKETPLACE_LIST, stdout: marketplaceInventory(false) },
|
|
{ args: PLUGIN_LIST, stdout: pluginInventory(false) },
|
|
{ args: MARKETPLACE_ADD, stdout: '{"alreadyAdded":false}' },
|
|
{ args: MARKETPLACE_LIST, stdout: marketplaceInventory(false) },
|
|
]);
|
|
|
|
await expectSetupError(
|
|
reconcileCodexPlugin({}, dependenciesFor(fake)),
|
|
'MARKETPLACE_VERIFICATION_FAILED',
|
|
/verify.*ecc marketplace/i
|
|
);
|
|
assert.strictEqual(fake.calls.length, 4);
|
|
}],
|
|
];
|
|
|
|
for (const [name, fn] of cases) {
|
|
if (await test(name, fn)) passed += 1;
|
|
else failed += 1;
|
|
}
|
|
|
|
console.log(`\nPassed: ${passed}`);
|
|
console.log(`Failed: ${failed}`);
|
|
if (failed > 0) process.exit(1);
|
|
}
|
|
|
|
runTests().catch(error => {
|
|
console.error(error);
|
|
process.exit(1);
|
|
});
|