185 lines
13 KiB
JavaScript
185 lines
13 KiB
JavaScript
import { test } from 'node:test';
|
|
import assert from 'node:assert/strict';
|
|
import { readFile, readdir, stat, lstat, writeFile, mkdir, symlink } from 'node:fs/promises';
|
|
import { readFileSync } from 'node:fs';
|
|
import { resolve } from 'node:path';
|
|
import { resolveFixtureExecution } from '../src/execution.mjs';
|
|
import { createFixtureCredentialStore } from '../src/fixture-store.mjs';
|
|
import { createFixtureWorkspace } from '../src/materialize-fixture.mjs';
|
|
const base = resolve(import.meta.dirname, 'fixtures/valid');
|
|
const json = name => JSON.parse(readFileSync(`${base}/${name}`, 'utf8'));
|
|
function registry() {
|
|
const account = json('auth/accounts/openai-codex/homelab-openai/account.json');
|
|
const profile = json('auth/settings/research-default.json');
|
|
profile.allowedAccounts.push('openai-codex/personal');
|
|
return { errors: [], entries: {
|
|
providers: { 'openai-codex': json('auth/providers/openai-codex.json'), 'ollama-remote': json('auth/providers/ollama-remote.json') },
|
|
profiles: { 'research-default': profile }, harnesses: { pi: json('harnesses/pi.json') }, selections: {},
|
|
accounts: { 'openai-codex/homelab-openai': account, 'openai-codex/personal': { ...account, id: 'personal', name: 'Personal' } },
|
|
} };
|
|
}
|
|
const credential = suffix => ({ type: 'oauth', access: `FIXTURE_ACCESS_${suffix}`, refresh: `FIXTURE_REFRESH_${suffix}`, expires: 9000000000000 });
|
|
const store = () => createFixtureCredentialStore({ 'openai-codex/homelab-openai': credential('A'), 'openai-codex/personal': credential('B') });
|
|
const request = (executionId = 'exec-a') => ({ fixtureOnly: true, agentId: 'rocko', projectId: 'project-a', workspaceId: 'workspace-a', sessionId: 'session-a', executionId, profile: 'research-default' });
|
|
const read = async p => JSON.parse(await readFile(p, 'utf8'));
|
|
async function workspace(t) { const w = await createFixtureWorkspace(); t.after(() => w.close()); return w; }
|
|
|
|
test('pure resolution selects current default or explicit enrolled account', () => {
|
|
const r = registry(), req = request();
|
|
const p = resolveFixtureExecution(r, req);
|
|
assert.equal(p.accounts['openai-codex'], 'openai-codex/homelab-openai');
|
|
assert.equal(p.models.providers['ollama-remote'].models[0].id, 'qwen2.5-coder:7b');
|
|
r.entries.profiles['research-default'].defaultAccounts['openai-codex'] = 'openai-codex/personal';
|
|
assert.equal(resolveFixtureExecution(r, req).accounts['openai-codex'], 'openai-codex/personal');
|
|
assert.equal(p.accounts['openai-codex'], 'openai-codex/homelab-openai');
|
|
});
|
|
test('scope is explicit, bounded and never inferred', () => {
|
|
for (const key of ['agentId', 'projectId', 'workspaceId', 'sessionId', 'executionId']) {
|
|
const r = request(); delete r[key]; assert.throws(() => resolveFixtureExecution(registry(), r), /invalid-fixture-input/);
|
|
r[key] = '../outside'; assert.throws(() => resolveFixtureExecution(registry(), r), /invalid-scope/);
|
|
}
|
|
assert.throws(() => resolveFixtureExecution(registry(), { ...request(), fixtureOnly: false }), /fixture-only/);
|
|
});
|
|
test('fork pin is preserved against default change, override, missing account and revocation', () => {
|
|
const r = registry(), q = { ...request(), fork: { sourceSessionId: 'source', accounts: { 'openai-codex': 'openai-codex/homelab-openai' } } };
|
|
r.entries.profiles['research-default'].defaultAccounts['openai-codex'] = 'openai-codex/personal';
|
|
assert.equal(resolveFixtureExecution(r, q).accounts['openai-codex'], 'openai-codex/homelab-openai');
|
|
assert.throws(() => resolveFixtureExecution(r, { ...q, accounts: { 'openai-codex': 'openai-codex/personal' } }), /fork-account-change/);
|
|
assert.throws(() => resolveFixtureExecution(r, { ...q, fork: { sourceSessionId: 'source', accounts: {} } }), /missing-fork-pin/);
|
|
delete r.entries.accounts['openai-codex/homelab-openai'];
|
|
assert.throws(() => resolveFixtureExecution(r, q), /missing-account/);
|
|
r.entries.profiles['research-default'].allowedAccounts = ['openai-codex/personal'];
|
|
assert.throws(() => resolveFixtureExecution(r, q), /account-not-enrolled/);
|
|
});
|
|
test('unenrolled account/provider, missing harness, model expansion and native model ceiling refuse', () => {
|
|
assert.throws(() => resolveFixtureExecution(registry(), { ...request(), accounts: { unknown: 'unknown/a' } }), /provider-not-enrolled/);
|
|
assert.throws(() => resolveFixtureExecution(registry(), { ...request(), accounts: { 'openai-codex': 'other/a' } }), /account-not-enrolled/);
|
|
const r = registry(); delete r.entries.harnesses.pi;
|
|
assert.throws(() => resolveFixtureExecution(r, request()), /unsupported-harness/);
|
|
const s = registry(); s.entries.profiles['research-default'].models['ollama-remote'] = ['not-allowed'];
|
|
assert.throws(() => resolveFixtureExecution(s, request()), /model-not-enrolled/);
|
|
s.entries.profiles['research-default'].models = { 'openai-codex': ['not-enforceable'] };
|
|
assert.throws(() => resolveFixtureExecution(s, request()), /native-model-enforcement-unavailable/);
|
|
});
|
|
test('only explicit synthetic credential forms and internal fixture stores admitted', async t => {
|
|
assert.throws(() => createFixtureCredentialStore({ 'openai-codex/homelab-openai': { type: 'oauth', access: 'not-fixture', refresh: 'not-fixture', expires: 3 } }), /invalid-fixture-credential/);
|
|
const w = await workspace(t);
|
|
await assert.rejects(w.generate(registry(), request(), { fixtureOnly: true, transaction() { throw Error('must not execute'); } }), /fixture-store-required/);
|
|
});
|
|
test('two concurrent workspaces of the same agent publish distinct complete private generations', async t => {
|
|
const w = await workspace(t), s = store();
|
|
const a = request('one'), b = { ...request('two'), sessionId: 'session-b', workspaceId: 'workspace-b', accounts: { 'openai-codex': 'openai-codex/personal' } };
|
|
const [x, y] = await Promise.all([w.generate(registry(), a, s), w.generate(registry(), b, s)]);
|
|
assert.notEqual(x.generationPath, y.generationPath);
|
|
assert.equal((await read(`${x.generationPath}/auth.json`))['openai-codex'].access, 'FIXTURE_ACCESS_A');
|
|
assert.equal((await read(`${y.generationPath}/auth.json`))['openai-codex'].access, 'FIXTURE_ACCESS_B');
|
|
for (const g of [x, y]) {
|
|
assert.deepEqual((await readdir(g.generationPath)).sort(), ['auth.json', 'manifest.json', 'models.json']);
|
|
assert.equal((await stat(g.generationPath)).mode & 0o777, 0o700);
|
|
for (const file of ['auth.json', 'manifest.json', 'models.json']) assert.equal((await stat(`${g.generationPath}/${file}`)).mode & 0o777, 0o600);
|
|
const manifest = await readFile(`${g.generationPath}/manifest.json`, 'utf8');
|
|
assert.ok(!/FIXTURE_ACCESS|FIXTURE_REFRESH|expires|sha256|credentialHash/.test(manifest));
|
|
assert.equal(g.manifest.state, 'ready');
|
|
}
|
|
assert.deepEqual(await readdir(`${w.path}/pending`), []);
|
|
});
|
|
test('same execution ID is exclusively claimed and cannot overwrite a published generation', async t => {
|
|
const w = await workspace(t), s = store();
|
|
const attempts = await Promise.allSettled([w.generate(registry(), request(), s), w.generate(registry(), request(), s)]);
|
|
assert.equal(attempts.filter(a => a.status === 'fulfilled').length, 1);
|
|
const g = attempts.find(a => a.status === 'fulfilled').value;
|
|
const before = await readFile(`${g.generationPath}/auth.json`);
|
|
await assert.rejects(w.generate(registry(), request(), s), /execution-already-claimed/);
|
|
assert.deepEqual(await readFile(`${g.generationPath}/auth.json`), before);
|
|
});
|
|
for (const fault of ['after-auth', 'before-publish']) test(`failed generation ${fault} preserves prior files, records failure and refuses blind same-ID retry`, async t => {
|
|
const w = await workspace(t), s = store(); const first = await w.generate(registry(), request('good'), s);
|
|
const before = await readFile(`${first.generationPath}/auth.json`);
|
|
await assert.rejects(w.generate(registry(), request('bad'), s, { fault }), /injected-generation-failure/);
|
|
assert.deepEqual(await readdir(`${w.path}/generations`), ['good']);
|
|
assert.deepEqual(await readdir(`${w.path}/pending`), []);
|
|
assert.deepEqual(await readFile(`${first.generationPath}/auth.json`), before);
|
|
assert.equal((await read(`${w.path}/claims/bad/failure.json`)).state, 'failed');
|
|
await assert.rejects(w.generate(registry(), request('bad'), s), /execution-already-claimed/);
|
|
});
|
|
test('credential lock contention refuses without duplicate side effects', async t => {
|
|
const w = await workspace(t), s = store(); let release, entered;
|
|
const ready = new Promise(r => { entered = r; });
|
|
const held = s.transaction(['openai-codex/homelab-openai'], async () => { entered(); await new Promise(r => { release = r; }); });
|
|
await ready;
|
|
try { await assert.rejects(w.generate(registry(), request(), s), /credential-busy/); }
|
|
finally { release(); await held; }
|
|
assert.deepEqual(await readdir(`${w.path}/generations`), []);
|
|
});
|
|
test('symlinked pre-existing final target is refused and never followed', async t => {
|
|
const w = await workspace(t); await mkdir(`${w.path}/outside`, { mode: 0o700 });
|
|
await writeFile(`${w.path}/outside/sentinel`, 'unchanged');
|
|
await symlink(`${w.path}/outside`, `${w.path}/generations/exec-a`);
|
|
await assert.rejects(w.generate(registry(), request(), store()), /generation-exists/);
|
|
assert.equal(await readFile(`${w.path}/outside/sentinel`, 'utf8'), 'unchanged');
|
|
assert.ok((await lstat(`${w.path}/generations/exec-a`)).isSymbolicLink());
|
|
});
|
|
test('invalid registry cannot resolve; no fallback to supplied partial entries', () => {
|
|
const r = registry(); r.errors = [{ code: 'refused' }];
|
|
assert.throws(() => resolveFixtureExecution(r, request()), /invalid-registry/);
|
|
});
|
|
|
|
|
|
test('post-publication failure records uncertainty, preserves complete generation and prevents replay', async t => {
|
|
const w = await workspace(t), s = store();
|
|
await assert.rejects(w.generate(registry(), request('uncertain'), s, { fault: 'after-publish' }), /publication-uncertain/);
|
|
const dir = `${w.path}/generations/uncertain`;
|
|
assert.deepEqual((await readdir(dir)).sort(), ['auth.json', 'manifest.json', 'models.json']);
|
|
const before = await readFile(`${dir}/auth.json`);
|
|
assert.equal((await read(`${w.path}/claims/uncertain/failure.json`)).state, 'uncertain');
|
|
await assert.rejects(w.generate(registry(), request('uncertain'), s), /execution-already-claimed/);
|
|
assert.deepEqual(await readFile(`${dir}/auth.json`), before);
|
|
assert.deepEqual(await readdir(`${w.path}/pending`), []);
|
|
});
|
|
|
|
|
|
test('expired credentials refresh under transaction and subsequent generation reuses rotation', async t => {
|
|
const w = await workspace(t), s = createFixtureCredentialStore({
|
|
'openai-codex/homelab-openai': { ...credential('OLD'), expires: 1 },
|
|
});
|
|
const first = await w.generate(registry(), request('rotate'), s);
|
|
const auth = await read(`${first.generationPath}/auth.json`);
|
|
assert.equal(auth['openai-codex'].access, 'FIXTURE_ROTATED_ACCESS');
|
|
assert.equal(auth['openai-codex'].refresh, 'FIXTURE_ROTATED_REFRESH');
|
|
const second = await w.generate(registry(), request('reuse'), s);
|
|
assert.deepEqual(await read(`${second.generationPath}/auth.json`), auth);
|
|
await s.transaction(['openai-codex/homelab-openai'], draft => {
|
|
assert.equal(draft.get('openai-codex/homelab-openai').refresh, 'FIXTURE_ROTATED_REFRESH');
|
|
});
|
|
});
|
|
for (const mode of ['failure', 'timeout', 'malformed']) test(`refresh ${mode} retains prior generation and store state`, async t => {
|
|
const w = await workspace(t), s = store();
|
|
const prior = await w.generate(registry(), request('prior'), s);
|
|
const bytes = await readFile(`${prior.generationPath}/auth.json`);
|
|
await assert.rejects(w.generate(registry(), request('failed-refresh'), s,
|
|
{ refresh: { mode, timeoutMs: mode === 'timeout' ? 100 : 2000 } }));
|
|
assert.deepEqual(await readdir(`${w.path}/generations`), ['prior']);
|
|
assert.deepEqual(await readFile(`${prior.generationPath}/auth.json`), bytes);
|
|
await s.transaction(['openai-codex/homelab-openai'], draft => {
|
|
assert.equal(draft.get('openai-codex/homelab-openai').access, 'FIXTURE_ACCESS_A');
|
|
});
|
|
await assert.rejects(w.generate(registry(), request('failed-refresh'), s), /execution-already-claimed/);
|
|
});
|
|
test('concurrent refresh on same account refuses contention while unrelated account proceeds', async t => {
|
|
const w = await workspace(t), s = store();
|
|
const results = await Promise.allSettled([
|
|
w.generate(registry(), request('busy-a'), s, { refresh: { mode: 'timeout', timeoutMs: 200 } }),
|
|
w.generate(registry(), request('busy-b'), s, { refresh: { mode: 'timeout', timeoutMs: 200 } }),
|
|
w.generate(registry(), { ...request('independent'), accounts: { 'openai-codex': 'openai-codex/personal' } }, s),
|
|
]);
|
|
const codes = results.slice(0, 2).map(r => r.reason?.code).sort();
|
|
assert.deepEqual(codes, ['credential-busy', 'refresh-timeout']);
|
|
assert.equal(results[2].status, 'fulfilled');
|
|
assert.deepEqual(await readdir(`${w.path}/generations`), ['independent']);
|
|
});
|
|
test('invalid refresh options refuse before burning claim', async t => {
|
|
const w = await workspace(t), s = store();
|
|
await assert.rejects(w.generate(registry(), request(), s, { refresh: { executable: '/bin/false' } }), /invalid-fixture-input/);
|
|
await w.generate(registry(), request(), s);
|
|
});
|