The cli tests that launch pi deleted only the session login names, so pi's preflight passed or refused by whatever provider keys the host exported. A shared helper now deletes every credential pi's preflight reads and gives pi launches one made-up key. The refusal itself is unchanged. pi bundle() takes a default argument again, so a call with none returns the non-proxy bundle, as before P3. pi.bundle leaves out a launch.models entry whose provider has no proxy route instead of failing every pi launch (F2); malformed proxy entries still refuse. The invalid session proxy context BundleError gets the code invalid-proxy-context. New tests cover F2, the code, and the three proxy account branches in the launcher (F3).
147 lines
8.1 KiB
JavaScript
147 lines
8.1 KiB
JavaScript
// P3: generate real harness config through buildBundle; credentials never enter disk artifacts.
|
|
import test from 'node:test';
|
|
import assert from 'node:assert/strict';
|
|
import { mkdirSync, readFileSync, statSync } from 'node:fs';
|
|
import { join } from 'node:path';
|
|
import { buildBundle } from '../src/bundle.mjs';
|
|
import { HARNESS, REPO_SKILLS, resolvedFor, scratch } from './helpers.mjs';
|
|
|
|
const models = {
|
|
a: { provider: 'proxy', model: 'claude-fixture', route: 'anthropic' },
|
|
z: { provider: 'proxy', model: 'glm-fixture', route: 'glm' },
|
|
g: { provider: 'proxy', model: 'gpt-fixture', route: 'codex' },
|
|
};
|
|
function build(t, selection, proxy, declared) {
|
|
const root = scratch(t, 'p332-'), runDir = join(root, 'run');
|
|
mkdirSync(runDir, { mode: 0o700 });
|
|
const valid = resolvedFor(t, 'pm');
|
|
assert.ok(Object.hasOwn(declared, selection), 'fixture selection must exist in declared launch.models');
|
|
const resolved = { ...valid, vars: { ...valid.vars, harness: 'pi', model: selection } };
|
|
const bundle = buildBundle({
|
|
dir: join(runDir, 'bundle'), resolved, models: declared,
|
|
contract: '# Fixture\n', harness: HARNESS.pi, harnessVersion: 'fixture',
|
|
skillsDir: REPO_SKILLS, proxy,
|
|
session: { run: 'r332', launchedBy: 'chief', workspace: join(root, 'ws'),
|
|
toolSocket: join(runDir, 'tools.sock'), turnMarker: join(runDir, 'turn.done') },
|
|
});
|
|
const file = join(runDir, 'home', '.pi', 'agent', 'models.json');
|
|
return { ...bundle, file, runDir, text: readFileSync(file, 'utf8') };
|
|
}
|
|
function assertSafe(value) {
|
|
if (typeof value === 'string') assert.ok(!value.startsWith('!'), value);
|
|
else if (value && typeof value === 'object') for (const item of Object.values(value)) assertSafe(item);
|
|
}
|
|
|
|
test('P3 Anthropic route uses messages at proxy root and the declared model id', t => {
|
|
const b = build(t, 'a', { baseUrl: 'http://127.0.0.1:12345', key: 'fx332-agent-a' }, { a: models.a });
|
|
const { anthropic } = JSON.parse(b.text).providers;
|
|
assert.equal(anthropic.api, 'anthropic-messages');
|
|
assert.equal(anthropic.baseUrl, 'http://127.0.0.1:12345');
|
|
assert.deepEqual(anthropic.models.map(m => m.id), ['claude-fixture']);
|
|
assert.equal(b.env.PI_PROVIDER, 'anthropic');
|
|
assert.equal(b.env.PI_MODEL, 'claude-fixture');
|
|
});
|
|
|
|
test('P3 omitted route refuses proxy-route-missing before model config generation', t => {
|
|
assert.throws(() => build(t, 'a', { baseUrl: 'http://127.0.0.1:12345', key: 'fx332-agent-a' }, {
|
|
a: { provider: 'proxy', model: 'claude-fixture' },
|
|
}), { code: 'proxy-route-missing' });
|
|
});
|
|
|
|
test('P3 GLM route uses completions at proxy /v1 with explicit z.ai compatibility', t => {
|
|
const b = build(t, 'z', { baseUrl: 'http://127.0.0.1:12345', key: 'fx332-agent-a' }, { z: models.z });
|
|
const glm = JSON.parse(b.text).providers['mosaic-proxy-glm'];
|
|
assert.equal(glm.api, 'openai-completions');
|
|
assert.equal(glm.baseUrl, 'http://127.0.0.1:12345/v1');
|
|
assert.deepEqual(glm.models.map(m => m.id), ['glm-fixture']);
|
|
assert.equal(glm.compat.thinkingFormat, 'zai');
|
|
assert.equal(glm.compat.zaiToolStream, true);
|
|
assert.equal(b.env.PI_PROVIDER, 'mosaic-proxy-glm');
|
|
});
|
|
|
|
test('P3 GPT route selects a stack-owned provider instead of pi Codex OAuth', t => {
|
|
const b = build(t, 'g', { baseUrl: 'http://127.0.0.1:12345', key: 'fx332-agent-a' }, { g: models.g });
|
|
const providers = JSON.parse(b.text).providers;
|
|
assert.equal(Object.hasOwn(providers, 'openai-codex'), false);
|
|
const gpt = providers['mosaic-proxy-codex'];
|
|
assert.equal(gpt.api, 'openai-responses');
|
|
assert.equal(gpt.baseUrl, 'http://127.0.0.1:12345/v1');
|
|
assert.deepEqual(gpt.models.map(m => m.id), ['gpt-fixture']);
|
|
assert.equal(b.env.PI_PROVIDER, 'mosaic-proxy-codex');
|
|
});
|
|
|
|
test('P3 a declared Codex route generates only the single build-time responses provider', t => {
|
|
const b = build(t, 'g', { baseUrl: 'http://127.0.0.1:12345', key: 'fx332-agent-a' }, {
|
|
g: models.g,
|
|
});
|
|
const providers = JSON.parse(b.text).providers;
|
|
assert.deepEqual(Object.keys(providers), ['mosaic-proxy-codex']);
|
|
assert.equal(providers['mosaic-proxy-codex'].api, 'openai-responses');
|
|
assert.equal(providers['mosaic-proxy-codex'].apiKey, '$ANTHROPIC_API_KEY');
|
|
});
|
|
|
|
test('P3 models file is private, references the one session key, and stores no executable values or keys', t => {
|
|
const key = 'fx332-agent-a';
|
|
const b = build(t, 'a', { baseUrl: 'http://127.0.0.1:12345', key }, { a: models.a, z: models.z });
|
|
const document = JSON.parse(b.text);
|
|
assert.deepEqual(Object.keys(document.providers).sort(), ['anthropic', 'mosaic-proxy-glm']);
|
|
for (const provider of Object.values(document.providers)) assert.equal(provider.apiKey, '$ANTHROPIC_API_KEY');
|
|
assertSafe(document);
|
|
assert.ok(!b.text.includes(key));
|
|
assert.equal(b.env.ANTHROPIC_API_KEY, key);
|
|
assert.equal(statSync(b.file).mode & 0o777, 0o600);
|
|
for (const dir of ['home', 'home/.pi', 'home/.pi/agent']) assert.equal(statSync(join(b.runDir, dir)).mode & 0o777, 0o700);
|
|
for (const file of Object.values(b.files)) assert.ok(!readFileSync(file, 'utf8').includes(key), file);
|
|
assert.ok(!JSON.stringify(b.manifest).includes(key));
|
|
});
|
|
|
|
test('P3 every launch regenerates proxy port, declared models and own key without caching', t => {
|
|
const first = build(t, 'a', { baseUrl: 'http://127.0.0.1:12345', key: 'fx332-agent-a' }, { a: models.a, z: models.z });
|
|
const second = build(t, 'a', { baseUrl: 'http://127.0.0.1:23456', key: 'fx332-agent-b' }, {
|
|
a: { provider: 'proxy', model: 'changed-model-id', route: 'anthropic' },
|
|
z: { provider: 'proxy', model: 'changed-glm-id', route: 'glm' },
|
|
});
|
|
assert.equal(second.env.ANTHROPIC_API_KEY, 'fx332-agent-b');
|
|
assert.ok(!JSON.stringify(second).includes('fx332-agent-a'));
|
|
const { providers } = JSON.parse(second.text);
|
|
assert.deepEqual(Object.keys(providers), ['anthropic', 'mosaic-proxy-glm']);
|
|
assert.equal(providers.anthropic.baseUrl, 'http://127.0.0.1:23456');
|
|
assert.deepEqual(providers.anthropic.models.map(m => m.id), ['changed-model-id']);
|
|
assert.equal(providers['mosaic-proxy-glm'].baseUrl, 'http://127.0.0.1:23456/v1');
|
|
assert.deepEqual(providers['mosaic-proxy-glm'].models.map(m => m.id), ['changed-glm-id']);
|
|
assert.equal(JSON.parse(first.text).providers.anthropic.baseUrl, 'http://127.0.0.1:12345');
|
|
});
|
|
|
|
// Row 332 R2 (#1830, review F2): launch.models is open table data. An entry whose provider has no
|
|
// proxy route is one this session can't use, so pi leaves it out of models.json instead of failing
|
|
// the launch; the selected entry was routed by sessionModel first. A malformed proxy entry still refuses.
|
|
test('row 332 R2 an unroutable model table entry is left out of models.json and the launch goes on', t => {
|
|
const declared = {
|
|
opus: { provider: 'anthropic', model: 'claude-fixture' },
|
|
glm: { provider: 'zai', model: 'glm-fixture' },
|
|
router: { provider: 'openrouter', model: 'x' },
|
|
};
|
|
const b = build(t, 'opus', { baseUrl: 'http://127.0.0.1:12345', key: 'fx332-agent-a' }, declared);
|
|
const { providers } = JSON.parse(b.text);
|
|
assert.deepEqual(Object.keys(providers).sort(), ['anthropic', 'mosaic-proxy-glm']);
|
|
assert.deepEqual(providers.anthropic.models.map(m => m.id), ['claude-fixture']);
|
|
assert.deepEqual(providers['mosaic-proxy-glm'].models.map(m => m.id), ['glm-fixture']);
|
|
assert.ok(!Object.values(providers).some(p => p.models.some(m => m.id === 'x')), 'the openrouter entry is not written');
|
|
assert.equal(b.env.PI_MODEL, 'claude-fixture');
|
|
assert.throws(() => build(t, 'opus', { baseUrl: 'http://127.0.0.1:12345', key: 'fx332-agent-a' }, {
|
|
...declared, broken: { provider: 'proxy', model: 'claude-fixture' },
|
|
}), { code: 'proxy-route-missing' });
|
|
});
|
|
|
|
// Row 332 R2 (#1830, review F3): a proxy context of the wrong shape refuses with its own code.
|
|
test('row 332 R2 a session proxy context without a key or loopback URL refuses invalid-proxy-context', t => {
|
|
for (const proxy of [
|
|
{ baseUrl: 'http://127.0.0.1:12345', key: '' },
|
|
{ baseUrl: 'http://localhost:12345', key: 'fx332-agent-a' },
|
|
{ baseUrl: 'http://127.0.0.1:12345' },
|
|
]) {
|
|
assert.throws(() => build(t, 'a', proxy, { a: models.a }),
|
|
{ code: 'invalid-proxy-context', message: 'invalid session proxy context' }, JSON.stringify(proxy));
|
|
}
|
|
});
|