Files
ECC/tests/lib/context-profile-native-claude.test.js
T

241 lines
14 KiB
JavaScript

'use strict';
const assert = require('node:assert/strict');
const fs = require('node:fs');
const os = require('node:os');
const path = require('node:path');
const test = require('node:test');
const { withFixture } = require('./helpers/context-fixture');
const store = require('../../scripts/lib/context-profile-store');
const native = () => require('../../scripts/lib/context-profile-native');
function fixture(callback) {
return withFixture(repoRoot => {
const parent = fs.mkdtempSync(path.join(fs.realpathSync(os.tmpdir()), 'ecc-native-claude-test-'));
const options = { stateRoot: path.join(parent, 'managed'), nativeRoot: path.join(parent, 'native'), claudePath: process.execPath };
try {
store.applyStore({ repoRoot, stateRoot: options.stateRoot, target: 'claude' });
return callback(options, repoRoot, parent);
} finally { fs.rmSync(parent, { recursive: true, force: true }); }
});
}
function details(pluginDir, overrides) {
const skills = fs.readdirSync(path.join(pluginDir, 'skills')).sort();
const counts = { agents: 0, hooks: 0, mcp: 0, lsp: 0, ...overrides.counts };
return ['ecc-context-carrier', ' Source: ecc-context-carrier@inline', '', 'Component inventory',
` Skills (${skills.length}) ${skills.join(', ')}`, ` Agents (${counts.agents})`, ` Hooks (${counts.hooks})`,
` MCP servers (${counts.mcp})`, ` LSP servers (${counts.lsp})`, '', 'Projected token cost',
` Always-on: ${overrides.alwaysOn || '~1,398 tok'} added to every session`, ''].join('\n');
}
function provider(overrides = {}) {
const calls = [];
const execute = (command, args, options) => {
calls.push({ command, args, options });
assert.equal(options.killSignal, 'SIGKILL');
assert.equal(options.shell, false);
for (const key of ['ANTHROPIC_API_KEY', 'OPENAI_API_KEY', 'NODE_OPTIONS', 'HTTP_PROXY', 'AWS_ACCESS_KEY_ID', 'CODEX_HOME']) {
assert.equal(options.env[key], undefined);
}
assert.equal(options.env.CLAUDE_CONFIG_DIR, path.join(options.env.HOME, '.claude'));
assert.equal(options.env.CLAUDE_CODE_DISABLE_NONESSENTIAL_TRAFFIC, '1');
assert.equal(options.env.CLAUDE_CODE_DISABLE_OFFICIAL_MARKETPLACE_AUTOINSTALL, '1');
assert.equal(options.cwd, path.join(path.dirname(options.env.HOME), 'project'));
if (args[0] === '--version') return { status: 0, stdout: `${overrides.version || '2.1.292 (Claude Code)'}\n` };
if (args[0] === 'plugin' && args[1] === 'validate') {
const report = { success: true, manifest: { errors: [], warnings: [] }, contents: [] };
return { status: 0, stdout: JSON.stringify(overrides.validate ? overrides.validate(report) : report) };
}
const pluginDir = args[1];
if (overrides.tamper) overrides.tamper(pluginDir);
if (args[3] === 'list') {
const plugins = [{ id: 'ecc-context-carrier@inline', version: 'unknown', scope: 'session', enabled: true, installPath: pluginDir }];
return { status: 0, stdout: JSON.stringify(overrides.list ? overrides.list(plugins) : plugins) };
}
if (args[3] === 'details') return { status: 0, stdout: details(pluginDir, overrides) };
return { status: 1, stderr: 'unexpected command' };
};
return { execute, calls, ...overrides };
}
test('Claude preview is deterministic and never invokes the provider or creates a home', () => fixture(options => {
const first = native().previewNativeProfile(options);
assert.deepEqual(first, native().previewNativeProfile(options));
assert.equal(first.target, 'claude');
assert.equal(first.status, 'proposed');
assert.deepEqual(first.supportedProviderVersions, ['>=2.1.292 <2.2.0']);
assert.equal(fs.existsSync(options.nativeRoot), false);
}));
test('Claude prepare verifies session-only discovery and records the host token projection', () => fixture(options => {
const dependency = provider();
const result = native().prepareNativeProfile(options, dependency);
assert.equal(result.status, 'ready');
assert.equal(result.target, 'claude');
assert.equal(result.providerVersion, '2.1.292');
assert.equal(result.discovery, 'verified');
assert.equal(result.codexHome, null);
assert.equal(result.claudeConfigDir, path.join(result.home, '.claude'));
assert.equal(result.pluginDir, path.join(path.dirname(result.home), 'plugin'));
assert.equal(result.claudePath, process.execPath);
assert.deepEqual(result.nativeObservation, { surface: 'plugin-always-on-listing',
method: 'claude-plugin-details@2.1.292', tokens: 1398, approximate: true });
assert.equal(result.selectedIds.length, 3);
assert.match(fs.readFileSync(path.join(result.claudeConfigDir, 'CLAUDE.md'), 'utf8'), /grants no tools/);
assert.equal(fs.existsSync(path.join(result.home, '.codex')), false);
const pluginCalls = dependency.calls.filter(call => call.args.includes('--plugin-dir'));
assert.deepEqual(pluginCalls.map(call => call.args.slice(2)), [['plugin', 'list', '--json'], ['plugin', 'details', 'ecc-context-carrier']]);
assert.ok(pluginCalls.every(call => call.args[1] === result.pluginDir));
assert.equal(native().getNativeProfileStatus(options).status, 'ready');
}));
for (const version of ['2.1.293 (Claude Code)', '2.1.1000 (Claude Code)']) {
test(`Claude version gate admits later 2.1 patch "${version}" and records it exactly`, () => fixture(options => {
const result = native().prepareNativeProfile(options, provider({ version }));
assert.equal(result.providerVersion, version.split(' ')[0]);
assert.equal(native().getNativeProfileStatus(options).providerVersion, version.split(' ')[0]);
}));
}
for (const version of ['2.1.291 (Claude Code)', '2.1.292', '2.1.292 (Claude Code) extra', '2.2.0 (Claude Code)',
'2.1.292-beta (Claude Code)', '3.1.292 (Claude Code)', '02.1.292 (Claude Code)']) {
test(`Claude version gate rejects "${version}" before plugin calls`, () => fixture(options => {
const dependency = provider({ version });
assert.throws(() => native().prepareNativeProfile(options, dependency), /version/i);
assert.equal(dependency.calls.some(call => call.args.includes('plugin')), false);
}));
}
const corruptions = {
'validation error': [{ validate: report => ({ ...report, success: false, manifest: { errors: [{ path: 'skills' }] } }) }, /plugin validation failed/],
'null validation entry': [{ validate: report => ({ ...report, contents: [null] }) }, /plugin validation failed/],
'validation entry without errors': [{ validate: report => ({ ...report, contents: [{ type: 'skill' }] }) }, /plugin validation failed/],
'validation entry with non-array errors': [{ validate: report => ({ ...report, contents: [{ type: 'skill', errors: 'bad' }] }) },
/plugin validation failed/],
'validation entry error': [{ validate: report => ({ ...report, contents: [{ type: 'skill', errors: [{ path: 'name' }] }] }) },
/plugin validation failed/],
'extra plugin': [{ list: plugins => [...plugins, { id: 'other@market', scope: 'user', enabled: true, installPath: '/elsewhere' }] }, /plugin discovery mismatch/],
'moved plugin': [{ list: plugins => [{ ...plugins[0], installPath: '/elsewhere' }] }, /plugin discovery mismatch/],
'disabled plugin': [{ list: plugins => [{ ...plugins[0], enabled: false }] }, /plugin discovery mismatch/],
'extra agent': [{ counts: { agents: 1 } }, /component discovery mismatch/],
'extra hook': [{ counts: { hooks: 1 } }, /component discovery mismatch/],
'extra MCP server': [{ counts: { mcp: 1 } }, /component discovery mismatch/],
'unparseable token projection': [{ alwaysOn: 'unknown' }, /token projection/],
'tampered plugin bytes': [{ tamper: pluginDir => fs.appendFileSync(path.join(pluginDir, 'skills/ecc-guide/SKILL.md'), 'tamper') },
/file set or digest mismatch/],
};
for (const [label, [overrides, expected]] of Object.entries(corruptions)) {
test(`Claude ${label} never commits a ready pointer`, () => fixture(options => {
assert.throws(() => native().prepareNativeProfile(options, provider(overrides)), expected);
assert.equal(fs.existsSync(path.join(options.nativeRoot, 'state.json')), false);
}));
}
test('Claude details skill mismatch never commits a ready pointer', () => fixture(options => {
const dependency = provider();
const execute = dependency.execute;
dependency.execute = (command, args, config) => {
const result = execute(command, args, config);
return args[3] === 'details' ? { ...result, stdout: result.stdout.replace(', ecc-guide', '') } : result;
};
assert.throws(() => native().prepareNativeProfile(options, dependency), /component discovery mismatch/);
assert.equal(fs.existsSync(path.join(options.nativeRoot, 'state.json')), false);
}));
test('Claude plugin bytes and added user skills reject static readiness', () => fixture(options => {
const prepared = native().prepareNativeProfile(options, provider());
fs.mkdirSync(path.join(prepared.claudeConfigDir, 'skills/extra'), { recursive: true });
fs.writeFileSync(path.join(prepared.claudeConfigDir, 'skills/extra/SKILL.md'), 'extra');
assert.throws(() => native().getNativeProfileStatus(options), /changed/);
fs.rmSync(path.join(prepared.claudeConfigDir, 'skills'), { recursive: true });
assert.equal(native().getNativeProfileStatus(options).ready, true);
fs.appendFileSync(path.join(prepared.pluginDir, 'skills/ecc-guide/SKILL.md'), 'tamper');
assert.throws(() => native().getNativeProfileStatus(options), /changed/);
}));
test('Claude provider runtime state outside discovery controls keeps readiness', () => fixture(options => {
const prepared = native().prepareNativeProfile(options, provider());
fs.writeFileSync(path.join(prepared.claudeConfigDir, '.claude.json'), '{"numStartups":1}');
fs.mkdirSync(path.join(prepared.claudeConfigDir, 'projects/session'), { recursive: true });
assert.equal(native().getNativeProfileStatus(options).status, 'ready');
}));
test('Claude provider homes are never accepted as the native root', () => fixture(options => {
const dependency = provider();
const previous = process.env.CLAUDE_CONFIG_DIR;
const configured = path.join(path.dirname(options.nativeRoot), 'configured-claude');
process.env.CLAUDE_CONFIG_DIR = configured;
try {
for (const root of [path.join(os.homedir(), '.claude'), configured]) {
assert.throws(() => native().prepareNativeProfile({ ...options, nativeRoot: root }, dependency), /dedicated/);
}
} finally {
if (previous === undefined) delete process.env.CLAUDE_CONFIG_DIR; else process.env.CLAUDE_CONFIG_DIR = previous;
}
assert.equal(dependency.calls.length, 0);
}));
test('a Codex generation becomes stale when the managed store targets Claude', () => fixture((options, repoRoot) => {
store.applyStore({ repoRoot, stateRoot: options.stateRoot, target: 'codex' });
const codex = provider();
codex.execute = (_command, args) => args[0] === '--version' ? { status: 0, stdout: 'codex-cli 0.154.0' } : { status: 0, stdout: '{}' };
codex.discover = (_command, config) => {
const base = path.dirname(config.env.HOME);
const name = JSON.parse(fs.readFileSync(path.join(base, 'marketplace/.agents/plugins/marketplace.json'))).name;
const cache = path.join(config.env.CODEX_HOME, 'plugins/cache', name, 'ecc-context-carrier/local');
fs.mkdirSync(path.dirname(cache), { recursive: true });
fs.cpSync(path.join(base, 'marketplace/carrier'), cache, { recursive: true });
return { data: [{ cwd: config.cwd, errors: [], skills: fs.readdirSync(path.join(cache, 'skills')).map(skill => ({
name: `ecc-context-carrier:${skill}`, pluginId: `ecc-context-carrier@${name}`, enabled: true, scope: 'user',
path: path.join(cache, 'skills', skill, 'SKILL.md') })) }] };
};
const first = native().prepareNativeProfile({ ...options, codexPath: process.execPath, claudePath: undefined }, codex);
assert.equal(first.target, 'codex');
store.applyStore({ repoRoot, stateRoot: options.stateRoot, target: 'claude' });
assert.equal(native().getNativeProfileStatus(options).status, 'stale');
const second = native().prepareNativeProfile(options, provider());
assert.equal(second.target, 'claude');
assert.equal(second.revision, first.revision + 1);
}));
test('Claude task launch loads the verified plugin directory in the isolated home', () => fixture(options => {
const prepared = native().prepareNativeProfile(options, provider());
const { launchTaskContext } = require('../../scripts/lib/context-profile-launch');
let called = false;
launchTaskContext({ task: { sessionId: 's', taskId: 't', revision: 1, phase: 'p', query: 'hello', noWorkflow: true },
target: 'claude', nativeEnvironment: { home: prepared.home, claudeConfigDir: prepared.claudeConfigDir,
claudePath: prepared.claudePath, pluginDir: prepared.pluginDir, executableDigest: prepared.executableDigest },
execute(command, args, config) {
called = true;
assert.equal(command, prepared.claudePath);
assert.deepEqual(args, ['--plugin-dir', prepared.pluginDir, '--print']);
assert.equal(config.env.CLAUDE_CONFIG_DIR, prepared.claudeConfigDir);
assert.equal(config.env.CLAUDE_CODE_DISABLE_OFFICIAL_MARKETPLACE_AUTOINSTALL, '1');
assert.equal(config.env.CODEX_HOME, undefined);
return { status: 0, stdout: 'ok' };
} });
assert.equal(called, true);
}));
test('Claude interactive start launches the pinned binary with the verified plugin directory', () => {
const parent = fs.mkdtempSync(path.join(fs.realpathSync(os.tmpdir()), 'ecc-native-claude-start-'));
const options = { stateRoot: path.join(parent, 'managed'), nativeRoot: path.join(parent, 'native') };
try {
store.applyStore({ stateRoot: options.stateRoot, target: 'claude' });
const prepared = native().prepareNativeProfile({ ...options, claudePath: process.execPath }, provider());
const { startInteractiveProfile } = require('../../scripts/lib/context-profile-interactive');
let call;
const result = startInteractiveProfile(options,
{ execute: (command, args, config) => { call = { command, args, config }; return { status: 0 }; } });
assert.equal(call.command, prepared.claudePath);
assert.deepEqual(call.args, ['--plugin-dir', prepared.pluginDir]);
assert.equal(call.config.env.CLAUDE_CONFIG_DIR, prepared.claudeConfigDir);
assert.equal(call.config.env.CLAUDE_CODE_DISABLE_OFFICIAL_MARKETPLACE_AUTOINSTALL, '1');
assert.equal(call.config.env.CODEX_HOME, undefined);
assert.equal(call.config.stdio, 'inherit');
assert.equal(result.status, 'exited');
assert.match(result.bootstrapDigest, /^[a-f0-9]{64}$/);
} finally { fs.rmSync(parent, { recursive: true, force: true }); }
});