Files
open-design/apps/daemon/tests/chat-route.test.ts
Ray XiandGitHub 784025c04a fix(daemon): resolve the AMR default model from the caller's catalog (#8519)
* fix(daemon): resolve the AMR default model from the caller's catalog

A run that asked for `default` before the caller's `vela model list` had
arrived was concretized from the bundled `vela model preset` seed. The
seed is the same for every plan and OD orders deepseek-v4-flash first, so
free-tier callers (entitled only to deepseek-v4.1-flash since 09-24) had
Vela reject `session/set_model` before any work began. On 0.24.1 that was
22 runs from 21 users in the last 7 days, 13 of 16 bundled runs showing
the user's choice as `default`.

- When `default` is requested and only the preset is cached, wait up to
  12s for the caller's remote catalog (`getAuthoritative`).
- Remember remote catalogs separately; a later preset seed never displaces
  one when a default model is chosen.
- Order deepseek-v4.1-flash first: it is the one chat model every plan,
  including free, is entitled to, so a flagless list still lands on it.

* fix(daemon): bound the AMR default-model catalog wait per refresh

CI follow-up. A `default` run looked the catalog up twice (the resume probe
and the run itself) and each lookup waited up to 12 s for the caller's
remote catalog, so a slow or broken `vela model list` held the run queued
for up to 24 s. The AMR e2e runs timed out at 20 s that way.

- The wait is counted from the start of the remote refresh, so one run, or
  several concurrent runs, share a single wait.
- When the last refresh failed, the preset is used at once; the next
  refresh still starts in the background.
- e2e fake vela: answer `model list --all --format json`, the form the
  daemon actually sends. It only matched the flag-less form and hung.

* test(e2e): let the fake vela answer the daemon's `model list --all` probe

The catalog probe spawns `vela model list --all --format json`. The matcher
stripped `--all` with its preceding space only, so `' --format json'` never
equalled `'--format json'`: the probe fell through to the login check, the
cache recorded a failed refresh, and default runs settled on the preset. Match
the subcommand and the `--format json` flags in any order, as fake-vela.mjs and
turn.test.ts already do, and cover the exact daemon argv for both the catalog
and the 401 path.
2026-10-15 03:57:51 +00:00

4743 lines
179 KiB
TypeScript

import type http from 'node:http';
import Database from 'better-sqlite3';
import { randomUUID } from 'node:crypto';
import {
chmodSync,
existsSync,
mkdirSync,
mkdtempSync,
promises as fsp,
readFileSync,
realpathSync,
rmSync,
writeFileSync,
} from 'node:fs';
import { execFileSync } from 'node:child_process';
import { tmpdir } from 'node:os';
import { fileURLToPath } from 'node:url';
import { delimiter, join, resolve } from 'node:path';
import { afterAll, afterEach, beforeAll, describe, expect, it } from 'vitest';
import {
composeLiveInstructionPrompt,
describeStablePromptCache,
designSystemIdFromPluginSnapshot,
resolveChatExtraAllowedDirs,
resolveEffectiveDesignSystemSelection,
resolveResearchCommandContract,
startServer,
} from '../src/server.js';
import { skillCwdAliasSegment } from '../src/cwd-aliases.js';
import { amrModelLoadingCache } from '../src/runtimes/amr-model-cache.js';
import { resetLiveModelsForTests } from '../src/runtimes/models.js';
import { getAgentDef } from '../src/agents.js';
import { readAppConfig, writeAppConfig } from '../src/app-config.js';
import { readMemoryConfig, writeMemoryConfig } from '../src/memory.js';
import {
ensureWorkspaceProject,
ensureWorkspaceResource,
getProject,
upsertMessage,
} from '../src/db.js';
import { teamResourceWorkspaceRoot } from '../src/collab/team-resource-materialization.js';
import { workspaceTeamDesignSystemBindingResourceId } from '../src/design-systems/workspace-team-binding.js';
const FAKE_VELA_FIXTURE = resolve(process.cwd(), 'tests', 'fixtures', 'fake-vela.mjs');
async function withFakeAgent<T>(
binName: string,
script: string,
run: () => Promise<T>,
): Promise<T> {
const dir = await fsp.mkdtemp(join(tmpdir(), 'od-chat-route-bin-'));
const oldPath = process.env.PATH;
try {
if (process.platform === 'win32') {
const runner = join(dir, `${binName}-test-runner.cjs`);
await fsp.writeFile(runner, script);
await fsp.writeFile(
join(dir, `${binName}.cmd`),
`@echo off\r\nnode "${runner}" %*\r\n`,
);
} else {
const bin = join(dir, binName);
await fsp.writeFile(bin, `#!/usr/bin/env node\n${script}`);
await fsp.chmod(bin, 0o755);
}
process.env.PATH = `${dir}${delimiter}${oldPath ?? ''}`;
return await run();
} finally {
process.env.PATH = oldPath;
killProcessesUsingPath(dir);
await fsp.rm(dir, { recursive: true, force: true });
}
}
async function withCodexExecJson<T>(run: () => Promise<T>): Promise<T> {
const previous = process.env.OD_CODEX_TRANSPORT;
process.env.OD_CODEX_TRANSPORT = 'exec-json';
try {
return await run();
} finally {
if (previous == null) delete process.env.OD_CODEX_TRANSPORT;
else process.env.OD_CODEX_TRANSPORT = previous;
}
}
function killProcessesUsingPath(pathFragment: string): void {
if (process.platform === 'win32') return;
let output = '';
try {
output = execFileSync('pgrep', ['-f', pathFragment], { encoding: 'utf8' });
} catch {
return;
}
for (const line of output.split('\n')) {
const pid = Number(line.trim());
if (!Number.isInteger(pid) || pid <= 0 || pid === process.pid) continue;
try {
process.kill(-pid, 'SIGKILL');
} catch {
try { process.kill(pid, 'SIGKILL'); } catch { /* already gone */ }
}
}
}
describe('/api/chat', () => {
let server: http.Server;
let baseUrl: string;
let originalMemoryConfig: Awaited<ReturnType<typeof readMemoryConfig>> | null = null;
const originalPath = process.env.PATH;
const originalAgentHome = process.env.OD_AGENT_HOME;
const tempDirs: string[] = [];
async function createPersonalWorkspaceBoundProjectFixture(label: string) {
if (!process.env.OD_DATA_DIR) {
throw new Error('OD_DATA_DIR is required for AMR Workspace scope tests');
}
const projectId = `proj-${randomUUID()}`;
const workspaceId = `personal-ws-${randomUUID()}`;
const workspaceMemberId = `personal-member-${randomUUID()}`;
const createProjectResponse = await fetch(`${baseUrl}/api/projects`, {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify({ id: projectId, name: label }),
});
expect(createProjectResponse.ok).toBe(true);
const sqlite = new Database(resolve(process.env.OD_DATA_DIR, 'app.sqlite'));
try {
ensureWorkspaceProject(sqlite as never, {
projectId,
workspaceId,
visibility: 'personal',
createdByWorkspaceMemberId: workspaceMemberId,
});
} finally {
sqlite.close();
}
return {
projectId,
headers: {
'x-od-workspace-id': workspaceId,
'x-od-workspace-member-id': workspaceMemberId,
'x-od-workspace-type': 'personal',
'x-od-workspace-role': 'owner',
},
};
}
async function runAmrModelRequest(model: string, extraEnv: Record<string, string> = {}): Promise<{
body: string;
invocations: string[];
responseOk: boolean;
}> {
if (!process.env.OD_DATA_DIR) {
throw new Error('OD_DATA_DIR is required for AMR model capability tests');
}
const previousRuntimeKey = process.env.VELA_RUNTIME_KEY;
const previousLinkUrl = process.env.VELA_LINK_URL;
const previousInvocationLog = process.env.FAKE_VELA_INVOCATION_LOG;
const previousLogSetModel = process.env.FAKE_VELA_LOG_SET_MODEL;
const previousLogPrompt = process.env.FAKE_VELA_LOG_PROMPT;
const invocationLog = join(tmpdir(), `od-amr-model-request-${randomUUID()}.jsonl`);
const previousExtraEnv = Object.fromEntries(Object.keys(extraEnv).map((key) => [key, process.env[key]]));
try {
Object.assign(process.env, extraEnv);
process.env.VELA_RUNTIME_KEY = `fake-runtime-key-${randomUUID()}`;
process.env.VELA_LINK_URL = 'https://amr-link.open-design.ai/v1';
process.env.FAKE_VELA_INVOCATION_LOG = invocationLog;
process.env.FAKE_VELA_LOG_SET_MODEL = '1';
process.env.FAKE_VELA_LOG_PROMPT = '1';
const workspaceFixture = await createPersonalWorkspaceBoundProjectFixture(
`AMR model request ${randomUUID()}`,
);
let body = '';
let responseOk = false;
await withFakeAgent(
'vela',
`
const { spawn } = require('node:child_process');
const fixture = ${JSON.stringify(FAKE_VELA_FIXTURE)};
const child = spawn(process.execPath, [fixture, ...process.argv.slice(2)], {
stdio: 'inherit',
env: process.env,
});
child.on('exit', (code, signal) => {
if (signal) process.kill(process.pid, signal);
process.exit(code ?? 0);
});
`,
async () => {
const response = await fetch(`${baseUrl}/api/chat`, {
method: 'POST',
headers: {
'Content-Type': 'application/json',
...workspaceFixture.headers,
},
body: JSON.stringify({
agentId: 'amr',
projectId: workspaceFixture.projectId,
message: 'Exercise the resolved AMR model.',
model,
}),
});
responseOk = response.ok;
body = await response.text();
},
);
const invocations = existsSync(invocationLog)
? readFileSync(invocationLog, 'utf8')
.trim()
.split('\n')
.filter(Boolean)
.map((line) => (JSON.parse(line) as { method: string }).method)
: [];
return { body, invocations, responseOk };
} finally {
rmSync(invocationLog, { force: true });
for (const [key, value] of Object.entries(previousExtraEnv)) {
if (value == null) delete process.env[key];
else process.env[key] = value;
}
if (previousRuntimeKey == null) delete process.env.VELA_RUNTIME_KEY;
else process.env.VELA_RUNTIME_KEY = previousRuntimeKey;
if (previousLinkUrl == null) delete process.env.VELA_LINK_URL;
else process.env.VELA_LINK_URL = previousLinkUrl;
if (previousInvocationLog == null) delete process.env.FAKE_VELA_INVOCATION_LOG;
else process.env.FAKE_VELA_INVOCATION_LOG = previousInvocationLog;
if (previousLogSetModel == null) delete process.env.FAKE_VELA_LOG_SET_MODEL;
else process.env.FAKE_VELA_LOG_SET_MODEL = previousLogSetModel;
if (previousLogPrompt == null) delete process.env.FAKE_VELA_LOG_PROMPT;
else process.env.FAKE_VELA_LOG_PROMPT = previousLogPrompt;
}
}
async function createPluginFixture(args: {
pluginId: string;
dirName: string;
localSkillPath?: string;
}): Promise<string> {
const root = await fsp.mkdtemp(join(tmpdir(), 'od-plugin-fixture-'));
tempDirs.push(root);
const fixtureDir = resolve(root, args.dirName);
const baseFixtureDir = resolve(
process.cwd(),
'tests',
'fixtures',
'plugin-fixtures',
'sample-plugin',
);
await fsp.cp(baseFixtureDir, fixtureDir, { recursive: true });
const manifestPath = resolve(fixtureDir, 'open-design.json');
const manifest = JSON.parse(await fsp.readFile(manifestPath, 'utf8')) as {
name: string;
title: string;
od?: { context?: { skills?: Array<{ ref?: string; path?: string }> } };
};
manifest.name = args.pluginId;
manifest.title = args.pluginId;
if (args.localSkillPath) {
manifest.od ??= {};
manifest.od.context ??= {};
manifest.od.context.skills = [{ path: args.localSkillPath }];
}
await fsp.writeFile(manifestPath, JSON.stringify(manifest, null, 2), 'utf8');
return fixtureDir;
}
beforeAll(async () => {
if (process.env.OD_DATA_DIR) {
originalMemoryConfig = await readMemoryConfig(process.env.OD_DATA_DIR);
await writeMemoryConfig(process.env.OD_DATA_DIR, {
enabled: false,
extraction: null,
});
}
const started = await startServer({
port: 0,
returnServer: true,
}) as {
url: string;
server: http.Server;
};
baseUrl = started.url;
server = started.server;
});
afterEach(() => {
if (originalPath == null) {
delete process.env.PATH;
} else {
process.env.PATH = originalPath;
}
if (originalAgentHome == null) {
delete process.env.OD_AGENT_HOME;
} else {
process.env.OD_AGENT_HOME = originalAgentHome;
}
});
afterAll(async () => {
for (const dir of tempDirs.splice(0)) {
rmSync(dir, { recursive: true, force: true });
}
if (server) {
await new Promise<void>((resolve) => server.close(() => resolve()));
}
if (process.env.OD_DATA_DIR && originalMemoryConfig) {
await writeMemoryConfig(process.env.OD_DATA_DIR, {
enabled: originalMemoryConfig.enabled,
extraction: originalMemoryConfig.extraction,
});
}
});
it('does not reference an out-of-scope response while starting a run', async () => {
process.env.PATH = '';
const emptyAgentHome = mkdtempSync(join(tmpdir(), 'od-empty-agent-home-'));
tempDirs.push(emptyAgentHome);
process.env.OD_AGENT_HOME = emptyAgentHome;
const response = await fetch(`${baseUrl}/api/chat`, {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify({
agentId: 'claude',
message: 'hello',
}),
});
const body = await response.text();
expect(response.ok).toBe(true);
expect(body).not.toContain('res is not defined');
expect(body).toContain('AGENT_UNAVAILABLE');
});
it.each([false, true])('keeps serving without a conversation row (project bound: %s)', async (projectBound) => {
const projectId = projectBound ? `unbound_chat_${randomUUID()}` : undefined;
if (projectId) {
const created = await fetch(`${baseUrl}/api/projects`, {
method: 'POST', headers: { 'Content-Type': 'application/json' },
body: JSON.stringify({ id: projectId, name: 'Unpersisted legacy chat' }),
});
expect(created.status).toBe(200);
}
const conversationId = `missing-conversation-${randomUUID()}`;
await withFakeAgent(
'opencode',
`
console.log(JSON.stringify({ type: 'step_start', sessionID: 'missing-conversation-session' }));
console.log(JSON.stringify({
type: 'text',
sessionID: 'missing-conversation-session',
part: { text: '<question-form id="discovery">{"questions":[{"id":"style","label":"Style?"}]}</question-form>' },
}));
console.log(JSON.stringify({ type: 'step_finish', part: { tokens: { input: 1, output: 1 } } }));
process.exit(0);
`,
async () => {
const response = await fetch(`${baseUrl}/api/chat`, {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify({
agentId: 'opencode',
conversationId,
...(projectId ? { projectId } : {}),
message: 'ask for clarification',
}),
});
const body = await response.text();
expect(response.ok).toBe(true);
expect(body).toContain('<question-form');
expect(body).toContain('"status":"succeeded"');
const healthResponse = await fetch(`${baseUrl}/api/health`);
expect(healthResponse.status).toBe(200);
},
);
});
it('marks json stream runs failed when an error frame exits with code 0', async () => {
const conversationId = `conv-${randomUUID()}`;
await withFakeAgent(
'opencode',
`
console.log(JSON.stringify({
type: 'error',
error: { message: 'model not found: fake-opencode-model' },
}));
process.exit(0);
`,
async () => {
const response = await fetch(`${baseUrl}/api/chat`, {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify({
agentId: 'opencode',
conversationId,
message: 'hello',
}),
});
const body = await response.text();
expect(response.ok).toBe(true);
expect(body).toContain('AGENT_EXECUTION_FAILED');
expect(body).toContain('model not found: fake-opencode-model');
expect(body).toContain('"status":"failed"');
expect(body).not.toContain('"status":"succeeded"');
const runsResponse = await fetch(
`${baseUrl}/api/runs?conversationId=${encodeURIComponent(conversationId)}`,
);
const runsBody = (await runsResponse.json()) as {
runs: Array<{ conversationId: string | null; status: string; exitCode: number | null }>;
};
expect(runsBody.runs).toHaveLength(1);
expect(runsBody.runs[0]).toMatchObject({
conversationId,
status: 'failed',
exitCode: 1,
});
},
);
});
it('marks OpenCode tool-only runs failed when no assistant output is produced', async () => {
const conversationId = `conv-${randomUUID()}`;
await withFakeAgent(
'opencode',
`
console.log(JSON.stringify({ type: 'step_start', sessionID: 'opencode-tool-only-session' }));
console.log(JSON.stringify({
type: 'tool_use',
sessionID: 'opencode-tool-only-session',
part: {
tool: 'Read',
callID: 'call-read-1',
state: {
status: 'completed',
input: JSON.stringify({ file: 'src/app.ts' }),
output: 'file contents',
},
},
}));
console.log(JSON.stringify({ type: 'step_finish', part: { tokens: { input: 1, output: 0 } } }));
process.exit(0);
`,
async () => {
const response = await fetch(`${baseUrl}/api/chat`, {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify({
agentId: 'opencode',
conversationId,
message: 'read the file and summarize it',
}),
});
const body = await response.text();
expect(response.ok).toBe(true);
expect(body).toContain('"type":"tool_use"');
expect(body).toContain('"type":"tool_result"');
expect(body).toContain('AGENT_EXECUTION_FAILED');
expect(body).toContain('Agent completed without producing any output');
expect(body).toContain('"status":"failed"');
expect(body).not.toContain('"status":"succeeded"');
const runsResponse = await fetch(
`${baseUrl}/api/runs?conversationId=${encodeURIComponent(conversationId)}`,
);
const runsBody = (await runsResponse.json()) as {
runs: Array<{ conversationId: string | null; status: string; exitCode: number | null }>;
};
expect(runsBody.runs).toHaveLength(1);
expect(runsBody.runs[0]).toMatchObject({
conversationId,
status: 'failed',
exitCode: 0,
});
},
);
});
/*
* 「问完就交棒」的一整条链路,从假 CLI 的字节一路到 run 的终态。
*
* 真机 run 441ff961-bd66-4c4a-91e7-812f1d489668(打包版 beta 0.21.1-beta.7):
* agent 先写下四条待办(1 条 in_progress + 3 条 pending),再发一个可渲染的
* `<question-form>` 交棒给用户,进程 exit 0、无 signal、无 error。
* 它被 stamp 成 endedWithUnfinishedWork,项目卡与 Pet 任务中心于是画成
* `incomplete`,聊天页脚说「已停止,仍有未完成任务」—— 而没有任何东西停过它。
*
* 这条用例走真实的 emitAgentEvent 收口(`captureRunWorkCompletenessSignals`
* 在那里把 text_delta 攒进 run.askUserScanText),所以它同时钉住了信号的采集
* 和 finish() 的判定;runs.test.ts 那几条只钉后者。
*/
it('does not stamp unfinished work on a turn that ended by asking the user', async () => {
const conversationId = `conv-${randomUUID()}`;
await withFakeAgent(
'opencode',
`
console.log(JSON.stringify({ type: 'step_start', sessionID: 'opencode-ask-user-session' }));
console.log(JSON.stringify({
type: 'tool_use',
sessionID: 'opencode-ask-user-session',
part: {
tool: 'todowrite',
callID: 'call-todo-1',
state: {
status: 'completed',
input: JSON.stringify({ todos: [
{ content: 'Collect the brand brief', status: 'in_progress' },
{ content: 'Decide the imagery strategy', status: 'pending' },
{ content: 'Fill inputs.json', status: 'pending' },
{ content: 'Render the landing page', status: 'pending' },
] }),
output: 'ok',
},
},
}));
console.log(JSON.stringify({ type: 'text', part: { text: '开始之前先确认几件事。\\n<question-form id="brand-brief" title="Brand brief">\\n{"questions":[{"id":"brand_name","label":"Brand name","type":"text"}]}\\n</question-form>' } }));
console.log(JSON.stringify({ type: 'step_finish', part: { tokens: { input: 1, output: 1 } } }));
process.exit(0);
`,
async () => {
const response = await fetch(`${baseUrl}/api/chat`, {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify({
agentId: 'opencode',
conversationId,
message: '帮我做个落地页。',
}),
});
const body = await response.text();
expect(response.ok).toBe(true);
expect(body).toContain('<question-form');
expect(body).toContain('"status":"succeeded"');
const runsResponse = await fetch(
`${baseUrl}/api/runs?conversationId=${encodeURIComponent(conversationId)}`,
);
const runsBody = (await runsResponse.json()) as {
runs: Array<{ status: string; endedWithUnfinishedWork: boolean }>;
};
expect(runsBody.runs).toHaveLength(1);
expect(runsBody.runs[0]).toMatchObject({
status: 'succeeded',
endedWithUnfinishedWork: false,
});
},
);
});
// 量法能看见缺陷:同一份待办、同一条链路,只把可渲染的表单换成被引用的裸标记,
// 这一轮就必须重新报「有未完成的活」。产物 HTML / 代码示例里出现这段文本的回合
// 不许因此被静音。
it('still stamps unfinished work when the form markup was only quoted', async () => {
const conversationId = `conv-${randomUUID()}`;
await withFakeAgent(
'opencode',
`
console.log(JSON.stringify({ type: 'step_start', sessionID: 'opencode-quoted-form-session' }));
console.log(JSON.stringify({
type: 'tool_use',
sessionID: 'opencode-quoted-form-session',
part: {
tool: 'todowrite',
callID: 'call-todo-1',
state: {
status: 'completed',
input: JSON.stringify({ todos: [
{ content: 'Collect the brand brief', status: 'in_progress' },
{ content: 'Render the landing page', status: 'pending' },
] }),
output: 'ok',
},
},
}));
console.log(JSON.stringify({ type: 'text', part: { text: '顺带演示一下 <question-form> 这个标记怎么写,我先接着做。' } }));
console.log(JSON.stringify({ type: 'step_finish', part: { tokens: { input: 1, output: 1 } } }));
process.exit(0);
`,
async () => {
const response = await fetch(`${baseUrl}/api/chat`, {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify({
agentId: 'opencode',
conversationId,
message: '帮我做个落地页。',
}),
});
expect((await response.text())).toContain('"status":"succeeded"');
const runsResponse = await fetch(
`${baseUrl}/api/runs?conversationId=${encodeURIComponent(conversationId)}`,
);
const runsBody = (await runsResponse.json()) as {
runs: Array<{ status: string; endedWithUnfinishedWork: boolean }>;
};
expect(runsBody.runs).toHaveLength(1);
expect(runsBody.runs[0]).toMatchObject({
status: 'succeeded',
endedWithUnfinishedWork: true,
});
},
);
});
it('passes OPENCODE_CONFIG_CONTENT external_directory rules for the managed project cwd', async () => {
if (!process.env.OD_DATA_DIR) {
throw new Error('OD_DATA_DIR is required for OpenCode cwd permission tests');
}
const projectId = `proj-${randomUUID()}`;
const markerDir = await fsp.mkdtemp(join(tmpdir(), 'od-opencode-config-'));
tempDirs.push(markerDir);
const envFile = join(markerDir, 'opencode-config-content.json');
const cwdFile = join(markerDir, 'cwd.txt');
const createProjectResponse = await fetch(`${baseUrl}/api/projects`, {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify({ id: projectId, name: 'OpenCode cwd permission fixture' }),
});
expect(createProjectResponse.ok).toBe(true);
await withFakeAgent(
'opencode',
`
const fs = require('node:fs');
process.stdin.resume();
process.stdin.on('end', () => {
fs.writeFileSync(${JSON.stringify(envFile)}, process.env.OPENCODE_CONFIG_CONTENT || '');
fs.writeFileSync(${JSON.stringify(cwdFile)}, process.cwd());
console.log(JSON.stringify({ type: 'step_start' }));
console.log(JSON.stringify({ type: 'text', part: { text: 'cwd-permission-ok' } }));
console.log(JSON.stringify({ type: 'step_finish', part: { tokens: { input: 1, output: 1 } } }));
process.exit(0);
});
`,
async () => {
const response = await fetch(`${baseUrl}/api/chat`, {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify({
agentId: 'opencode',
projectId,
message: 'hello',
}),
});
const body = await response.text();
expect(response.ok).toBe(true);
expect(body).toContain('cwd-permission-ok');
const effectiveCwd = (await fsp.readFile(cwdFile, 'utf8')).trim();
const raw = await fsp.readFile(envFile, 'utf8');
const parsed = JSON.parse(raw) as {
permission?: {
external_directory?: Record<string, string>;
};
};
const externalDirectory = parsed.permission?.external_directory ?? {};
const cwdAliases = new Set([effectiveCwd]);
if (effectiveCwd.startsWith('/private/var/')) {
cwdAliases.add(effectiveCwd.replace(/^\/private\/var\//, '/var/'));
}
const allowedCwd = [...cwdAliases].find(
(cwd) =>
externalDirectory[cwd] === 'allow' &&
externalDirectory[`${cwd}/*`] === 'allow' &&
externalDirectory[`${cwd}/**`] === 'allow',
);
expect(allowedCwd).toBeTruthy();
},
);
});
it('allowlists the active skill root that lives outside the repository skills dir', async () => {
if (!process.env.OD_DATA_DIR) {
throw new Error('OD_DATA_DIR is required for OpenCode skill permission tests');
}
// #8173: the skill preamble advertises the skill's absolute root as its
// fallback path, but the allowlist was built from the repository `skills/`
// tree alone. A skill installed under the daemon data dir — the same shape
// as the bundled plugin folder in the report — landed outside it, so
// OpenCode asked for `external_directory` approval on a file OD itself told
// the agent to open.
const skillId = `od-skill-root-${randomUUID().slice(0, 8)}`;
const userSkillDir = join(process.env.OD_DATA_DIR, 'skills', skillId);
mkdirSync(join(userSkillDir, 'references'), { recursive: true });
writeFileSync(
join(userSkillDir, 'SKILL.md'),
`---\nname: ${skillId}\ndescription: External directory allowlist fixture.\n---\n`
+ '# Fixture\n\nRead `references/layouts.md` before writing anything.\n',
);
writeFileSync(join(userSkillDir, 'references', 'layouts.md'), '# Layouts\n');
tempDirs.push(userSkillDir);
const projectId = `proj-${randomUUID()}`;
const markerDir = await fsp.mkdtemp(join(tmpdir(), 'od-opencode-skill-dir-'));
tempDirs.push(markerDir);
const envFile = join(markerDir, 'opencode-config-content.json');
const createProjectResponse = await fetch(`${baseUrl}/api/projects`, {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify({
id: projectId,
name: 'OpenCode skill root permission fixture',
skillId,
}),
});
expect(createProjectResponse.ok).toBe(true);
await withFakeAgent(
'opencode',
`
const fs = require('node:fs');
process.stdin.resume();
process.stdin.on('end', () => {
fs.writeFileSync(${JSON.stringify(envFile)}, process.env.OPENCODE_CONFIG_CONTENT || '');
console.log(JSON.stringify({ type: 'step_start' }));
console.log(JSON.stringify({ type: 'text', part: { text: 'skill-permission-ok' } }));
console.log(JSON.stringify({ type: 'step_finish', part: { tokens: { input: 1, output: 1 } } }));
process.exit(0);
});
`,
async () => {
const response = await fetch(`${baseUrl}/api/chat`, {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify({
agentId: 'opencode',
projectId,
skillId,
message: 'hello',
}),
});
expect(response.ok).toBe(true);
expect(await response.text()).toContain('skill-permission-ok');
const parsed = JSON.parse(readFileSync(envFile, 'utf8')) as {
permission?: { external_directory?: Record<string, string> };
};
const externalDirectory = parsed.permission?.external_directory ?? {};
const roots = new Set([userSkillDir, realpathSync(userSkillDir)]);
const allowedRoot = [...roots].find(
(root) =>
externalDirectory[root] === 'allow' &&
externalDirectory[`${root}/**`] === 'allow',
);
expect(allowedRoot).toBeTruthy();
},
);
});
it('passes BYOK provider config to the daemon-backed OpenCode runtime', async () => {
if (!process.env.OD_DATA_DIR) {
throw new Error('OD_DATA_DIR is required for BYOK OpenCode config tests');
}
const projectId = `proj-${randomUUID()}`;
const markerDir = await fsp.mkdtemp(join(tmpdir(), 'od-byok-opencode-config-'));
tempDirs.push(markerDir);
const envFile = join(markerDir, 'opencode-config-content.json');
const keyFile = join(markerDir, 'byok-key.txt');
const argsFile = join(markerDir, 'args.json');
const createProjectResponse = await fetch(`${baseUrl}/api/projects`, {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify({ id: projectId, name: 'BYOK OpenCode fixture' }),
});
expect(createProjectResponse.ok).toBe(true);
await withFakeAgent(
'opencode',
`
const fs = require('node:fs');
process.stdin.resume();
process.stdin.on('end', () => {
fs.writeFileSync(${JSON.stringify(envFile)}, process.env.OPENCODE_CONFIG_CONTENT || '');
fs.writeFileSync(${JSON.stringify(keyFile)}, process.env.OPEN_DESIGN_BYOK_API_KEY || '');
fs.writeFileSync(${JSON.stringify(argsFile)}, JSON.stringify(process.argv.slice(2)));
console.log(JSON.stringify({ type: 'step_start' }));
console.log(JSON.stringify({ type: 'text', part: { text: 'byok-opencode-ok' } }));
console.log(JSON.stringify({ type: 'step_finish', part: { tokens: { input: 1, output: 1 } } }));
process.exit(0);
});
`,
async () => {
const response = await fetch(`${baseUrl}/api/chat`, {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify({
agentId: 'byok-opencode',
projectId,
message: 'hello',
model: 'deepseek-v4-flash',
byokProvider: {
protocol: 'senseaudio',
apiKey: 'sk-test-byok',
baseUrl: 'https://api.senseaudio.cn',
model: 'deepseek-v4-flash',
requiresApiKey: true,
},
}),
});
const body = await response.text();
expect(response.ok).toBe(true);
expect(body).toContain('byok-opencode-ok');
expect(await fsp.readFile(keyFile, 'utf8')).toBe('sk-test-byok');
expect(JSON.parse(await fsp.readFile(argsFile, 'utf8'))).toEqual([
'run',
'--format',
'json',
'--dir',
expect.stringContaining(projectId),
'-m',
'open-design-byok/deepseek-v4-flash',
]);
const parsed = JSON.parse(await fsp.readFile(envFile, 'utf8')) as {
provider?: Record<string, {
npm?: string;
options?: Record<string, unknown>;
models?: Record<string, unknown>;
}>;
};
const provider = parsed.provider?.['open-design-byok'];
expect(provider).toMatchObject({
npm: '@ai-sdk/openai-compatible',
options: {
baseURL: 'https://api.senseaudio.cn',
apiKey: '{env:OPEN_DESIGN_BYOK_API_KEY}',
},
});
expect(provider?.models?.['deepseek-v4-flash']).toEqual({
name: 'deepseek-v4-flash',
modalities: { input: ['text', 'image'], output: ['text'] },
limit: {
context: 128_000,
output: 16_384,
},
});
expect(JSON.stringify(parsed)).not.toContain('sk-test-byok');
},
);
});
it('passes keyless BYOK provider config without auth fields to OpenCode', async () => {
if (!process.env.OD_DATA_DIR) {
throw new Error('OD_DATA_DIR is required for BYOK OpenCode config tests');
}
const projectId = `proj-${randomUUID()}`;
const markerDir = await fsp.mkdtemp(join(tmpdir(), 'od-byok-opencode-keyless-'));
tempDirs.push(markerDir);
const envFile = join(markerDir, 'opencode-config-content.json');
const keyFile = join(markerDir, 'byok-key.txt');
const argsFile = join(markerDir, 'args.json');
const createProjectResponse = await fetch(`${baseUrl}/api/projects`, {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify({ id: projectId, name: 'BYOK keyless OpenCode fixture' }),
});
expect(createProjectResponse.ok).toBe(true);
await withFakeAgent(
'opencode',
`
const fs = require('node:fs');
process.stdin.resume();
process.stdin.on('end', () => {
fs.writeFileSync(${JSON.stringify(envFile)}, process.env.OPENCODE_CONFIG_CONTENT || '');
fs.writeFileSync(${JSON.stringify(keyFile)}, process.env.OPEN_DESIGN_BYOK_API_KEY || '');
fs.writeFileSync(${JSON.stringify(argsFile)}, JSON.stringify(process.argv.slice(2)));
console.log(JSON.stringify({ type: 'step_start' }));
console.log(JSON.stringify({ type: 'text', part: { text: 'byok-opencode-keyless-ok' } }));
console.log(JSON.stringify({ type: 'step_finish', part: { tokens: { input: 1, output: 1 } } }));
process.exit(0);
});
`,
async () => {
const response = await fetch(`${baseUrl}/api/chat`, {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify({
agentId: 'byok-opencode',
projectId,
message: 'hello',
model: 'model',
byokProvider: {
protocol: 'openai',
apiKey: '',
baseUrl: 'http://127.0.0.1:8000/v1',
model: 'model',
requiresApiKey: false,
},
}),
});
const body = await response.text();
expect(response.ok).toBe(true);
expect(body).toContain('byok-opencode-keyless-ok');
expect(await fsp.readFile(keyFile, 'utf8')).toBe('');
expect(JSON.parse(await fsp.readFile(argsFile, 'utf8'))).toEqual([
'run',
'--format',
'json',
'--dir',
expect.stringContaining(projectId),
'-m',
'open-design-byok/model',
]);
const rawConfig = await fsp.readFile(envFile, 'utf8');
const parsed = JSON.parse(rawConfig) as {
provider?: Record<string, {
npm?: string;
options?: Record<string, unknown>;
models?: Record<string, unknown>;
}>;
};
const provider = parsed.provider?.['open-design-byok'];
expect(provider).toMatchObject({
npm: '@ai-sdk/openai-compatible',
options: {
baseURL: 'http://127.0.0.1:8000/v1',
},
});
expect(provider?.options).not.toHaveProperty('apiKey');
expect(rawConfig).not.toContain('OPEN_DESIGN_BYOK_API_KEY');
},
);
});
it('does not pass BYOK provider config to other local runtimes', async () => {
if (!process.env.OD_DATA_DIR) {
throw new Error('OD_DATA_DIR is required for BYOK OpenCode config tests');
}
const projectId = `proj-${randomUUID()}`;
const markerDir = await fsp.mkdtemp(join(tmpdir(), 'od-byok-opencode-isolation-'));
tempDirs.push(markerDir);
const envFile = join(markerDir, 'opencode-config-content.json');
const keyFile = join(markerDir, 'byok-key.txt');
const createProjectResponse = await fetch(`${baseUrl}/api/projects`, {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify({ id: projectId, name: 'BYOK isolation fixture' }),
});
expect(createProjectResponse.ok).toBe(true);
await withFakeAgent(
'opencode',
`
const fs = require('node:fs');
process.stdin.resume();
process.stdin.on('end', () => {
fs.writeFileSync(${JSON.stringify(envFile)}, process.env.OPENCODE_CONFIG_CONTENT || '');
fs.writeFileSync(${JSON.stringify(keyFile)}, process.env.OPEN_DESIGN_BYOK_API_KEY || '');
console.log(JSON.stringify({ type: 'step_start' }));
console.log(JSON.stringify({ type: 'text', part: { text: 'opencode-ok' } }));
console.log(JSON.stringify({ type: 'step_finish', part: { tokens: { input: 1, output: 1 } } }));
process.exit(0);
});
`,
async () => {
const response = await fetch(`${baseUrl}/api/chat`, {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify({
agentId: 'opencode',
projectId,
message: 'hello',
model: 'deepseek-v4-flash',
byokProvider: {
protocol: 'senseaudio',
apiKey: 'sk-test-byok',
baseUrl: 'https://api.senseaudio.cn',
},
}),
});
const body = await response.text();
expect(response.ok).toBe(true);
expect(body).toContain('opencode-ok');
expect(await fsp.readFile(keyFile, 'utf8')).toBe('');
const rawConfig = await fsp.readFile(envFile, 'utf8');
expect(rawConfig).not.toContain('open-design-byok');
expect(rawConfig).not.toContain('sk-test-byok');
},
);
});
it('strips inherited OpenCode server auth env before spawning the opencode CLI', async () => {
const inheritedPassword = process.env.OPENCODE_SERVER_PASSWORD;
process.env.OPENCODE_SERVER_PASSWORD = 'test-parent-server-password';
const markerDir = await fsp.mkdtemp(join(tmpdir(), 'od-opencode-env-'));
tempDirs.push(markerDir);
const envFile = join(markerDir, 'opencode-server-password.txt');
try {
await withFakeAgent(
'opencode',
`
const fs = require('node:fs');
process.stdin.resume();
process.stdin.on('end', () => {
fs.writeFileSync(${JSON.stringify(envFile)}, process.env.OPENCODE_SERVER_PASSWORD || '');
console.log(JSON.stringify({ type: 'step_start' }));
console.log(JSON.stringify({ type: 'text', part: { text: 'opencode-env-ok' } }));
console.log(JSON.stringify({ type: 'step_finish', part: { tokens: { input: 1, output: 1 } } }));
process.exit(0);
});
`,
async () => {
const response = await fetch(`${baseUrl}/api/chat`, {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify({
agentId: 'opencode',
message: 'hello',
}),
});
const body = await response.text();
expect(response.ok).toBe(true);
expect(body).toContain('opencode-env-ok');
expect(await fsp.readFile(envFile, 'utf8')).toBe('');
},
);
} finally {
if (inheritedPassword == null) {
delete process.env.OPENCODE_SERVER_PASSWORD;
} else {
process.env.OPENCODE_SERVER_PASSWORD = inheritedPassword;
}
}
});
it('reuses an existing assistant message row instead of creating a duplicate when assistantMessageId is supplied', async () => {
if (!process.env.OD_DATA_DIR) {
throw new Error('OD_DATA_DIR is required for assistant message reuse tests');
}
const projectId = `proj-${randomUUID()}`;
const assistantMessageId = `assistant-${randomUUID()}`;
const createProjectResponse = await fetch(`${baseUrl}/api/projects`, {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify({ id: projectId, name: 'Assistant row reuse fixture' }),
});
expect(createProjectResponse.ok).toBe(true);
const conversationsResponse = await fetch(`${baseUrl}/api/projects/${projectId}/conversations`);
expect(conversationsResponse.ok).toBe(true);
const conversationsBody = await conversationsResponse.json() as {
conversations: Array<{ id: string }>;
};
const conversationId = conversationsBody.conversations[0]?.id;
expect(conversationId).toBeTruthy();
const dbFile = resolve(process.env.OD_DATA_DIR, 'app.sqlite');
const sqlite = new Database(dbFile);
try {
upsertMessage(sqlite as never, conversationId!, {
id: assistantMessageId,
role: 'assistant',
content: '',
runStatus: 'failed',
startedAt: Date.now() - 1_000,
endedAt: Date.now() - 500,
});
} finally {
sqlite.close();
}
await withFakeAgent(
'opencode',
`
process.stdin.resume();
process.stdin.on('end', () => {
console.log(JSON.stringify({ type: 'step_start' }));
console.log(JSON.stringify({ type: 'text', part: { text: 'reused-assistant-row-ok' } }));
console.log(JSON.stringify({ type: 'step_finish', part: { tokens: { input: 1, output: 1 } } }));
process.exit(0);
});
`,
async () => {
const response = await fetch(`${baseUrl}/api/chat`, {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify({
agentId: 'opencode',
projectId,
conversationId,
assistantMessageId,
message: 'retry this turn',
}),
});
const body = await response.text();
expect(response.ok).toBe(true);
expect(body).toContain('reused-assistant-row-ok');
},
);
const verifyDb = new Database(dbFile, { readonly: true });
try {
const rows = verifyDb
.prepare(`SELECT id, content, run_id FROM messages WHERE conversation_id = ? AND role = 'assistant'`)
.all(conversationId) as Array<{ id: string; content: string; run_id: string | null }>;
expect(rows.filter((row) => row.id === assistantMessageId)).toHaveLength(1);
expect(rows.some((row) => row.id !== assistantMessageId && row.content.includes('reused-assistant-row-ok'))).toBe(false);
const reused = rows.find((row) => row.id === assistantMessageId);
expect(reused?.content).toContain('reused-assistant-row-ok');
} finally {
verifyDb.close();
}
});
it('does not leave a pinned assistant message queued when legacy chat fails before spawning', async () => {
if (!process.env.OD_DATA_DIR) {
throw new Error('OD_DATA_DIR is required for assistant message pin tests');
}
const projectId = `proj-${randomUUID()}`;
const assistantMessageId = `assistant-failed-${randomUUID()}`;
const createProjectResponse = await fetch(`${baseUrl}/api/projects`, {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify({ id: projectId, name: 'Failed assistant pin fixture' }),
});
expect(createProjectResponse.ok).toBe(true);
const conversationsResponse = await fetch(`${baseUrl}/api/projects/${projectId}/conversations`);
expect(conversationsResponse.ok).toBe(true);
const conversationsBody = await conversationsResponse.json() as {
conversations: Array<{ id: string }>;
};
const conversationId = conversationsBody.conversations[0]?.id;
expect(conversationId).toBeTruthy();
const response = await fetch(`${baseUrl}/api/chat`, {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify({
agentId: `missing-agent-${randomUUID()}`,
projectId,
conversationId,
assistantMessageId,
message: 'fail before spawn',
}),
});
const body = await response.text();
expect(response.ok).toBe(true);
expect(body).toContain('unknown agent');
const dbFile = resolve(process.env.OD_DATA_DIR, 'app.sqlite');
let lastStatus: string | null = null;
for (let attempt = 0; attempt < 100; attempt += 1) {
const sqlite = new Database(dbFile, { readonly: true });
try {
const row = sqlite
.prepare(`SELECT run_status FROM messages WHERE id = ?`)
.get(assistantMessageId) as { run_status: string | null } | undefined;
lastStatus = row?.run_status ?? null;
if (lastStatus && lastStatus !== 'queued' && lastStatus !== 'running') break;
} finally {
sqlite.close();
}
await new Promise((resolve) => setTimeout(resolve, 25));
}
expect(lastStatus).toBe('failed');
});
it('rewrites the OpenCode scanner overflow into a generic retry message', async () => {
const conversationId = `conv-${randomUUID()}`;
await withFakeAgent(
'opencode',
`
process.stderr.write('json-rpc id 4: opencode event stream: read opencode SSE: bufio.Scanner: token too long\\n');
process.exit(1);
`,
async () => {
const response = await fetch(`${baseUrl}/api/chat`, {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify({
agentId: 'opencode',
conversationId,
message: 'hello',
}),
});
const body = await response.text();
expect(response.ok).toBe(true);
expect(body).toContain('AGENT_EXECUTION_FAILED');
expect(body).toContain('The run failed due to an unknown upstream streaming error. Please retry.');
expect(body).toContain('event: stderr');
expect(body).toContain('"status":"failed"');
},
);
});
it('rejects a requested media-only model before AMR ACP launch', async () => {
const { body, invocations, responseOk } = await runAmrModelRequest('nano-banana-2');
expect(responseOk).toBe(true);
expect(invocations).toEqual([]);
expect(body).toContain('AMR_MODEL_UNAVAILABLE');
expect(body).toContain('model_not_chat_capable');
expect(body).toContain('"status":"failed"');
expect(body).not.toContain('Hello from fake vela.');
});
it('forwards an unknown custom-provider chat slug when the AMR catalog is stale', async () => {
const { body, invocations, responseOk } = await runAmrModelRequest(
'custom-provider/future-chat-2027',
);
expect(responseOk).toBe(true);
expect(invocations).toEqual([
'new',
'set_model:custom-provider/future-chat-2027',
'prompt',
]);
expect(body).not.toContain('AMR_MODEL_UNAVAILABLE');
expect(body).toContain('"type":"text_delta","delta":"Hello from fake "');
expect(body).toContain('"type":"text_delta","delta":"vela."');
expect(body).toContain('"status":"succeeded"');
});
// AMR 0.24.1, 10/5-10/7: 9 runs from 7 users failed with "session/set_model
// modelId is not available" because the saved model had left the AMR catalog.
it('switches to the AMR catalog default when vela rejects a delisted model', async () => {
const { body, invocations, responseOk } = await runAmrModelRequest('claude-opus-4.8', {
FAKE_VELA_SET_MODEL_UNAVAILABLE: 'claude-opus-4.8',
});
expect(responseOk).toBe(true);
expect(invocations[0]).toBe('new');
expect(invocations[1]).toBe('set_model_rejected:claude-opus-4.8');
expect(invocations[2]).toMatch(/^set_model:(deepseek-v4-flash|deepseek-v3\.2)$/);
expect(invocations[3]).toBe('prompt');
expect(body).toContain('"label":"model_fallback"');
expect(body).toContain('claude-opus-4.8 is no longer available in AMR.');
expect(body).not.toContain('AMR_MODEL_UNAVAILABLE');
expect(body).toContain('"status":"succeeded"');
});
it('survives transient AMR Link catalog failures without aborting the run', async () => {
// The run preflight resolves the AMR catalog through the shared
// AmrModelLoadingCache, which degrades to the offline `vela model preset`
// seed whenever the authoritative `vela model list` is momentarily
// unavailable (and refreshes the remote catalog in the background). So a
// transient catalog failure must NOT abort the run — the per-run path no
// longer blocks on a synchronous `model list` retry loop.
const previousRuntimeKey = process.env.VELA_RUNTIME_KEY;
const previousLinkUrl = process.env.VELA_LINK_URL;
const stateFile = join(tmpdir(), `od-amr-model-retry-${randomUUID()}.json`);
try {
// Unique key so the shared model cache key is unique per test run.
process.env.VELA_RUNTIME_KEY = `fake-runtime-key-${randomUUID()}`;
process.env.VELA_LINK_URL = 'https://amr-link.open-design.ai/v1';
const workspaceFixture =
await createPersonalWorkspaceBoundProjectFixture('Transient AMR catalog fixture');
await withFakeAgent(
'vela',
`
const { existsSync, readFileSync, writeFileSync } = require('node:fs');
const { spawn } = require('node:child_process');
const fixture = ${JSON.stringify(FAKE_VELA_FIXTURE)};
const stateFile = ${JSON.stringify(stateFile)};
const args = process.argv.slice(2);
if (args[0] === 'model' && args[1] === 'list') {
const state = existsSync(stateFile)
? JSON.parse(readFileSync(stateFile, 'utf8'))
: { attempts: 0 };
state.attempts += 1;
writeFileSync(stateFile, JSON.stringify(state), 'utf8');
if (state.attempts < 3) {
process.stderr.write('Get "https://amr-link.open-design.ai/v1/models": context deadline exceeded\\n');
process.exit(1);
}
}
const child = spawn(process.execPath, [fixture, ...args], {
stdio: 'inherit',
env: process.env,
});
child.on('exit', (code, signal) => {
if (signal) process.kill(process.pid, signal);
process.exit(code ?? 0);
});
`,
async () => {
const response = await fetch(`${baseUrl}/api/chat`, {
method: 'POST',
headers: {
'Content-Type': 'application/json',
...workspaceFixture.headers,
},
body: JSON.stringify({
agentId: 'amr',
projectId: workspaceFixture.projectId,
message: 'hello',
model: 'deepseek-v3.2',
}),
});
const body = await response.text();
expect(response.ok).toBe(true);
expect(body).toContain('"type":"text_delta","delta":"Hello from fake "');
expect(body).toContain('"type":"text_delta","delta":"vela."');
expect(body).not.toContain('model_catalog_unavailable');
expect(body).not.toContain('AMR_MODEL_UNAVAILABLE');
expect(body).not.toContain('AMR_WORKSPACE_SCOPE_REQUIRED');
// The catalog probe runs at least once (remote attempted, then the
// run proceeds from the preset seed). We no longer assert an exact
// synchronous retry count: the remote retry/backoff now happens in
// the cache's background refresh, not on the per-run hot path.
const attempts = JSON.parse(readFileSync(stateFile, 'utf8')) as { attempts: number };
expect(attempts.attempts).toBeGreaterThanOrEqual(1);
},
);
} finally {
rmSync(stateFile, { force: true });
if (previousRuntimeKey == null) delete process.env.VELA_RUNTIME_KEY;
else process.env.VELA_RUNTIME_KEY = previousRuntimeKey;
if (previousLinkUrl == null) delete process.env.VELA_LINK_URL;
else process.env.VELA_LINK_URL = previousLinkUrl;
}
});
it('proceeds with the AMR run via the cached/preset catalog when the live model list is unavailable', async () => {
// Red spec for the packaged-prerelease "AMR model the selected model is not
// available from Vela" report: the run preflight used to do a fresh,
// blocking `vela model list` (authoritative remote catalog) on EVERY run
// and fail-close the run whenever that single call timed out / errored —
// even though the user is logged in, the model picker already shows a
// model (seeded from the offline `vela model preset`), and the selected
// model is real. Under CorpLink/飞连 the remote call routinely exceeds the
// 10s timeout, so a logged-in user with a valid model could not run AMR at
// all. The fix reuses the shared AmrModelLoadingCache (cached remote when
// hot, otherwise the offline preset seed) instead of a per-run blocking
// remote probe, so a transient `model list` failure no longer kills the run.
const previousRuntimeKey = process.env.VELA_RUNTIME_KEY;
const previousLinkUrl = process.env.VELA_LINK_URL;
try {
// A unique runtime key both marks the user as logged-in AND makes the
// shared model cache key unique so this case never reuses another test's
// cached remote catalog.
process.env.VELA_RUNTIME_KEY = `fake-runtime-key-${randomUUID()}`;
process.env.VELA_LINK_URL = 'https://amr-link.open-design.ai/v1';
const workspaceFixture =
await createPersonalWorkspaceBoundProjectFixture('Cached AMR catalog fixture');
await withFakeAgent(
'vela',
`
const { spawn } = require('node:child_process');
const fixture = ${JSON.stringify(FAKE_VELA_FIXTURE)};
const args = process.argv.slice(2);
// Simulate a persistently unreachable authoritative catalog (gateway
// timeout / 飞连 congestion): every \`vela model list\` fails. \`model preset\`,
// \`login\`, and \`agent run\` still delegate to the fixture, mirroring the real
// CLI where the offline preset and the ACP run do not need the gateway.
if (args[0] === 'model' && args[1] === 'list') {
process.stderr.write('Get "https://amr-link.open-design.ai/v1/models": context deadline exceeded\\n');
process.exit(1);
}
const child = spawn(process.execPath, [fixture, ...args], {
stdio: 'inherit',
env: process.env,
});
child.on('exit', (code, signal) => {
if (signal) process.kill(process.pid, signal);
process.exit(code ?? 0);
});
`,
async () => {
const response = await fetch(`${baseUrl}/api/chat`, {
method: 'POST',
headers: {
'Content-Type': 'application/json',
...workspaceFixture.headers,
},
body: JSON.stringify({
agentId: 'amr',
projectId: workspaceFixture.projectId,
message: 'hello',
// Present in the preset seed (DEFAULT_MODEL_PRESET_JSON) but the
// live `model list` is unavailable, so only the preset path can
// surface it.
model: 'glm-5.1',
}),
});
const body = await response.text();
expect(response.ok).toBe(true);
// The run must NOT be fail-closed on the unavailable live catalog.
expect(body).not.toContain('AMR_MODEL_UNAVAILABLE');
expect(body).not.toContain('model_catalog_unavailable');
expect(body).not.toContain('is not available from Vela');
expect(body).not.toContain('AMR_WORKSPACE_SCOPE_REQUIRED');
// It must actually proceed into the ACP run and stream assistant text.
expect(body).toContain('"type":"text_delta","delta":"Hello from fake "');
expect(body).toContain('"type":"text_delta","delta":"vela."');
},
);
} finally {
if (previousRuntimeKey == null) delete process.env.VELA_RUNTIME_KEY;
else process.env.VELA_RUNTIME_KEY = previousRuntimeKey;
if (previousLinkUrl == null) delete process.env.VELA_LINK_URL;
else process.env.VELA_LINK_URL = previousLinkUrl;
}
});
it('persists exact AMR prompt budget context across new and resumed sessions', async () => {
const previousRuntimeKey = process.env.VELA_RUNTIME_KEY;
const previousLinkUrl = process.env.VELA_LINK_URL;
const previousPreset = process.env.FAKE_VELA_MODEL_PRESET_JSON;
const previousList = process.env.FAKE_VELA_MODEL_LIST_JSON;
const model = 'claude-observability-5';
try {
process.env.VELA_RUNTIME_KEY = `fake-runtime-key-${randomUUID()}`;
process.env.VELA_LINK_URL = 'https://amr-link.open-design.ai/v1';
process.env.FAKE_VELA_MODEL_PRESET_JSON = JSON.stringify({
source: 'preset',
data: [{
id: model,
enabled: true,
default: true,
metadata: { contextWindowTokens: 200_000 },
}],
});
process.env.FAKE_VELA_MODEL_LIST_JSON = JSON.stringify({
source: 'remote',
data: [{
id: model,
enabled: true,
default: true,
metadata: { contextWindowTokens: 200_000 },
}],
});
const workspaceFixture =
await createPersonalWorkspaceBoundProjectFixture('AMR prompt budget fixture');
const conversationsResponse = await fetch(
`${baseUrl}/api/projects/${workspaceFixture.projectId}/conversations`,
);
const conversationsBody = await conversationsResponse.json() as {
conversations: Array<{ id: string }>;
};
const conversationId = conversationsBody.conversations[0]?.id;
expect(conversationId).toBeTruthy();
await withFakeAgent(
'vela',
`
const { spawn } = require('node:child_process');
const fixture = ${JSON.stringify(FAKE_VELA_FIXTURE)};
const child = spawn(process.execPath, [fixture, ...process.argv.slice(2)], {
stdio: 'inherit',
env: process.env,
});
child.on('exit', (code, signal) => {
if (signal) process.kill(process.pid, signal);
process.exit(code ?? 0);
});
`,
async () => {
const runTurn = async (message: string) => {
const createResponse = await fetch(`${baseUrl}/api/runs`, {
method: 'POST',
headers: {
'Content-Type': 'application/json',
...workspaceFixture.headers,
},
body: JSON.stringify({
agentId: 'amr',
projectId: workspaceFixture.projectId,
conversationId,
model,
message,
currentPrompt: message,
}),
});
expect(createResponse.status).toBe(202);
const { runId } = await createResponse.json() as { runId: string };
const eventsResponse = await fetch(`${baseUrl}/api/runs/${runId}/events`);
const eventsBody = await readSseUntil(eventsResponse, 'event: final');
const statusBody = await waitForRunStatus(baseUrl, runId);
expect(statusBody.status).toBe('succeeded');
return eventsBody;
};
const first = await runTurn('first exact-frame turn');
expect(first).toContain('"name":"prompt_budget_v1"');
expect(first).toContain('"sessionMode":"new"');
expect(first).toContain('"contextWindowSource":"model_metadata"');
expect(first).toContain('"contextWindowTokens":200000');
expect(first).toContain('"priorSessionUsageSource":"unknown"');
const second = await runTurn('second resumed turn');
expect(second).toContain('"name":"prompt_budget_v1"');
expect(second).toContain('"sessionMode":"resume"');
expect(second).toContain('"contextWindowSource":"model_metadata"');
expect(second).toContain('"priorSessionUsageSource":"agent_session"');
expect(second).toContain('"priorSessionInputTokens":12');
},
);
} finally {
if (previousRuntimeKey == null) delete process.env.VELA_RUNTIME_KEY;
else process.env.VELA_RUNTIME_KEY = previousRuntimeKey;
if (previousLinkUrl == null) delete process.env.VELA_LINK_URL;
else process.env.VELA_LINK_URL = previousLinkUrl;
if (previousPreset == null) delete process.env.FAKE_VELA_MODEL_PRESET_JSON;
else process.env.FAKE_VELA_MODEL_PRESET_JSON = previousPreset;
if (previousList == null) delete process.env.FAKE_VELA_MODEL_LIST_JSON;
else process.env.FAKE_VELA_MODEL_LIST_JSON = previousList;
}
});
it('resolves an AMR default model from the caller catalog, not the shared preset seed', async () => {
// A free-tier caller: the bundled preset lists deepseek-v4-flash first, but
// the per-user catalog only allows deepseek-v4.1-flash. A run started before
// that catalog arrives must not turn `default` into a model vela rejects.
const saved = Object.fromEntries([
'VELA_RUNTIME_KEY', 'VELA_LINK_URL', 'FAKE_VELA_MODEL_PRESET_JSON', 'FAKE_VELA_MODEL_LIST_JSON',
'FAKE_VELA_MODEL_LIST_DELAY_MS', 'FAKE_VELA_ALLOWED_MODELS', 'FAKE_VELA_LOG_SET_MODEL_REQUEST', 'FAKE_VELA_INVOCATION_LOG',
].map((key) => [key, process.env[key]]));
const invocationLog = join(mkdtempSync(join(tmpdir(), 'od-amr-default-model-')), 'vela.log');
try {
amrModelLoadingCache.resetForTests();
resetLiveModelsForTests();
process.env.VELA_RUNTIME_KEY = `fake-runtime-key-${randomUUID()}`;
process.env.VELA_LINK_URL = 'https://amr-link.open-design.ai/v1';
process.env.FAKE_VELA_MODEL_PRESET_JSON = JSON.stringify({
source: 'preset',
data: [{ id: 'deepseek-v4-flash' }, { id: 'deepseek-v4-pro' }],
});
process.env.FAKE_VELA_MODEL_LIST_JSON = JSON.stringify({
source: 'remote',
data: [
{ id: 'deepseek-v4.1-flash', enabled: true, default: true },
{ id: 'deepseek-v4-flash', enabled: false },
{ id: 'deepseek-v4-pro', enabled: false },
],
});
process.env.FAKE_VELA_MODEL_LIST_DELAY_MS = '1500';
process.env.FAKE_VELA_ALLOWED_MODELS = 'deepseek-v4.1-flash';
process.env.FAKE_VELA_LOG_SET_MODEL_REQUEST = '1';
process.env.FAKE_VELA_INVOCATION_LOG = invocationLog;
const workspaceFixture =
await createPersonalWorkspaceBoundProjectFixture('AMR default model fixture');
await withFakeAgent(
'vela',
`
const { spawn } = require('node:child_process');
const fixture = ${JSON.stringify(FAKE_VELA_FIXTURE)};
const child = spawn(process.execPath, [fixture, ...process.argv.slice(2)], {
stdio: 'inherit',
env: process.env,
});
child.on('exit', (code, signal) => {
if (signal) process.kill(process.pid, signal);
process.exit(code ?? 0);
});
`,
async () => {
const createResponse = await fetch(`${baseUrl}/api/runs`, {
method: 'POST',
headers: { 'Content-Type': 'application/json', ...workspaceFixture.headers },
body: JSON.stringify({
agentId: 'amr',
projectId: workspaceFixture.projectId,
model: 'default',
message: 'hello',
currentPrompt: 'hello',
}),
});
expect(createResponse.status).toBe(202);
const { runId } = await createResponse.json() as { runId: string };
const statusBody = await waitForRunStatus(baseUrl, runId);
const requested = readFileSync(invocationLog, 'utf8')
.split('\n')
.filter(Boolean)
.map((line) => (JSON.parse(line) as { method: string }).method)
.filter((method) => method.startsWith('set_model_request:'));
expect(requested).toEqual(['set_model_request:deepseek-v4.1-flash']);
expect(statusBody.status).toBe('succeeded');
},
);
} finally {
for (const [key, value] of Object.entries(saved)) {
if (value == null) delete process.env[key];
else process.env[key] = value;
}
amrModelLoadingCache.resetForTests();
resetLiveModelsForTests();
}
});
it('keeps service tier overrides when /api/runs omits model but settings has one', async () => {
if (!process.env.OD_DATA_DIR) {
throw new Error('OD_DATA_DIR is required for service tier settings tests');
}
const argsPath = join(tmpdir(), `od-codex-args-${randomUUID()}.json`);
const previousConfig = await readAppConfig(process.env.OD_DATA_DIR);
try {
await writeAppConfig(process.env.OD_DATA_DIR, {
agentModels: { codex: { model: 'gpt-5.5' } },
});
await withCodexExecJson(() => withFakeAgent(
'codex',
`
const fs = require('node:fs');
const args = process.argv.slice(2);
if (args[0] === 'debug' && args[1] === 'models') {
console.log(JSON.stringify([{ id: 'gpt-5.5', name: 'gpt-5.5', service_tiers: [{ id: 'priority', label: 'Fast' }] }]));
process.exit(0);
}
if (args[0] === 'login' && args[1] === 'status') {
console.log('Logged in');
process.exit(0);
}
fs.writeFileSync(${JSON.stringify(argsPath)}, JSON.stringify(args));
console.log(JSON.stringify({ type: 'message', role: 'assistant', content: [{ type: 'output_text', text: 'done' }] }));
process.exit(0);
`,
async () => {
const createResponse = await fetch(`${baseUrl}/api/runs`, {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify({
agentId: 'codex',
message: 'hello',
serviceTier: 'priority',
}),
});
expect(createResponse.status).toBe(202);
const { runId } = await createResponse.json() as { runId: string };
await waitForRunStatus(baseUrl, runId);
const args = JSON.parse(readFileSync(argsPath, 'utf8')) as string[];
expect(args).toContain('--model');
expect(args).toContain('gpt-5.5');
expect(args).toContain('service_tier="priority"');
},
));
} finally {
rmSync(argsPath, { force: true });
await writeAppConfig(process.env.OD_DATA_DIR, {
agentModels: previousConfig.agentModels ?? null,
});
}
});
it('keeps service tier overrides when /api/runs omits model and settings has none', async () => {
if (!process.env.OD_DATA_DIR) {
throw new Error('OD_DATA_DIR is required for service tier settings tests');
}
const argsPath = join(tmpdir(), `od-codex-args-${randomUUID()}.json`);
const previousConfig = await readAppConfig(process.env.OD_DATA_DIR);
try {
await writeAppConfig(process.env.OD_DATA_DIR, { agentModels: null });
await withCodexExecJson(() => withFakeAgent(
'codex',
`
const fs = require('node:fs');
const args = process.argv.slice(2);
if (args[0] === 'debug' && args[1] === 'models') {
console.log(JSON.stringify([{ id: 'gpt-5.5', name: 'gpt-5.5', service_tiers: [{ id: 'priority', label: 'Fast' }] }]));
process.exit(0);
}
if (args[0] === 'login' && args[1] === 'status') {
console.log('Logged in');
process.exit(0);
}
fs.writeFileSync(${JSON.stringify(argsPath)}, JSON.stringify(args));
console.log(JSON.stringify({ type: 'message', role: 'assistant', content: [{ type: 'output_text', text: 'done' }] }));
process.exit(0);
`,
async () => {
await fetch(`${baseUrl}/api/agents`);
const createResponse = await fetch(`${baseUrl}/api/runs`, {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify({
agentId: 'codex',
message: 'hello',
serviceTier: 'priority',
}),
});
expect(createResponse.status).toBe(202);
const { runId } = await createResponse.json() as { runId: string };
await waitForRunStatus(baseUrl, runId);
const args = JSON.parse(readFileSync(argsPath, 'utf8')) as string[];
expect(args).toContain('--model');
expect(args).toContain('gpt-5.5');
expect(args).toContain('service_tier="priority"');
},
));
} finally {
rmSync(argsPath, { force: true });
await writeAppConfig(process.env.OD_DATA_DIR, {
agentModels: previousConfig.agentModels ?? null,
});
}
});
it('allows plugin authoring to succeed when the requested generated-plugin artifacts exist before close', async () => {
const projectId = `proj-plugin-authoring-success-${randomUUID()}`;
const createProjectResponse = await fetch(`${baseUrl}/api/projects`, {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify({
id: projectId,
name: 'Plugin authoring artifact success fixture',
skillId: null,
designSystemId: null,
}),
});
expect(createProjectResponse.status).toBe(200);
const conversationsResponse = await fetch(`${baseUrl}/api/projects/${projectId}/conversations`);
expect(conversationsResponse.status).toBe(200);
const conversationsBody = await conversationsResponse.json() as {
conversations: Array<{ id: string }>;
};
const conversationId = conversationsBody.conversations[0]?.id;
expect(conversationId).toBeTruthy();
await withFakeAgent(
'opencode',
`
const fs = require('node:fs');
const path = require('node:path');
process.stdin.resume();
process.stdin.on('end', () => {
const pluginDir = path.join(process.cwd(), 'generated-plugin');
fs.mkdirSync(pluginDir, { recursive: true });
fs.writeFileSync(path.join(pluginDir, 'open-design.json'), JSON.stringify({ name: 'generated-plugin' }, null, 2));
fs.writeFileSync(path.join(pluginDir, 'SKILL.md'), '# Generated plugin\\n');
console.log(JSON.stringify({ type: 'step_start' }));
console.log(JSON.stringify({ type: 'text', part: { text: '我来帮你创建一个通用的 OpenDesign 插件脚手架。先读取文档规范,再生成插件文件。' } }));
console.log(JSON.stringify({ type: 'step_finish', part: { tokens: { input: 1, output: 1 } } }));
process.exit(0);
});
`,
async () => {
const createResponse = await fetch(`${baseUrl}/api/runs`, {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify({
agentId: 'opencode',
projectId,
conversationId,
pluginId: 'od-plugin-authoring',
message: '请创建一个可刷新、可审计、由 API 驱动的 OpenDesign 插件脚手架。',
}),
});
expect(createResponse.status).toBe(202);
const { runId } = await createResponse.json() as { runId: string };
const eventsResponse = await fetch(`${baseUrl}/api/runs/${runId}/events`);
const eventsBody = await readSseUntil(eventsResponse, 'event: final');
const statusBody = await waitForRunStatus(baseUrl, runId);
expect(eventsBody).toContain('先读取文档规范,再生成插件文件');
expect(statusBody.status).toBe('succeeded');
const filesResponse = await fetch(`${baseUrl}/api/projects/${projectId}/files`);
expect(filesResponse.status).toBe(200);
const filesBody = await filesResponse.json() as { files: Array<{ name: string }> };
expect(filesBody.files.some((file) => file.name === 'generated-plugin/open-design.json')).toBe(true);
expect(filesBody.files.some((file) => file.name === 'generated-plugin/SKILL.md')).toBe(true);
},
);
});
it('does not report plugin authoring as succeeded when the agent only emits planning text without artifacts', async () => {
const projectId = `proj-plugin-authoring-${randomUUID()}`;
const createProjectResponse = await fetch(`${baseUrl}/api/projects`, {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify({
id: projectId,
name: 'Plugin authoring completion fixture',
skillId: null,
designSystemId: null,
}),
});
expect(createProjectResponse.status).toBe(200);
const conversationsResponse = await fetch(`${baseUrl}/api/projects/${projectId}/conversations`);
expect(conversationsResponse.status).toBe(200);
const conversationsBody = await conversationsResponse.json() as {
conversations: Array<{ id: string }>;
};
const conversationId = conversationsBody.conversations[0]?.id;
expect(conversationId).toBeTruthy();
await withFakeAgent(
'opencode',
`
process.stdin.resume();
process.stdin.on('end', () => {
console.log(JSON.stringify({ type: 'step_start' }));
console.log(JSON.stringify({ type: 'text', part: { text: '我来帮你创建一个通用的 OpenDesign 插件脚手架。先读取文档规范,再生成插件文件。' } }));
console.log(JSON.stringify({ type: 'step_finish', part: { tokens: { input: 1, output: 1 } } }));
process.exit(0);
});
`,
async () => {
const createResponse = await fetch(`${baseUrl}/api/runs`, {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify({
agentId: 'opencode',
projectId,
conversationId,
pluginId: 'od-plugin-authoring',
message: '请创建一个可刷新、可审计、由 API 驱动的 OpenDesign 插件脚手架。',
}),
});
expect(createResponse.status).toBe(202);
const {
runId,
pluginId,
appliedPluginSnapshotId,
} = await createResponse.json() as {
runId: string;
pluginId: string | null;
appliedPluginSnapshotId: string | null;
};
expect(pluginId).toBe('od-plugin-authoring');
expect(appliedPluginSnapshotId).toBeTruthy();
const eventsResponse = await fetch(`${baseUrl}/api/runs/${runId}/events`);
const eventsBody = await readSseUntil(eventsResponse, 'event: final');
const statusBody = await waitForRunStatus(baseUrl, runId);
expect(eventsBody).toContain('先读取文档规范,再生成插件文件');
expect(statusBody.status).not.toBe('succeeded');
const filesResponse = await fetch(`${baseUrl}/api/projects/${projectId}/files`);
expect(filesResponse.status).toBe(200);
const filesBody = await filesResponse.json() as { files: Array<{ name: string }> };
expect(filesBody.files.some((file) => file.name.startsWith('generated-plugin/'))).toBe(false);
},
);
});
it('does not fail plugin authoring when the turn-1 reply is a clarifying question-form awaiting the brief', async () => {
// The `od-plugin-authoring` plugin's turn-1 flow is to emit a
// `<question-form>` collecting the plugin brief, then STOP and wait for
// the user to answer — artifacts only land on the follow-up turn. The
// missing-artifacts guard must not treat that expected pause as a
// failure (regression: "Plugin authoring ended before generating the
// required generated-plugin artifacts.").
const projectId = `proj-plugin-authoring-question-${randomUUID()}`;
const createProjectResponse = await fetch(`${baseUrl}/api/projects`, {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify({
id: projectId,
name: 'Plugin authoring question-form fixture',
skillId: null,
designSystemId: null,
}),
});
expect(createProjectResponse.status).toBe(200);
const conversationsResponse = await fetch(`${baseUrl}/api/projects/${projectId}/conversations`);
expect(conversationsResponse.status).toBe(200);
const conversationsBody = await conversationsResponse.json() as {
conversations: Array<{ id: string }>;
};
const conversationId = conversationsBody.conversations[0]?.id;
expect(conversationId).toBeTruthy();
await withFakeAgent(
'opencode',
`
process.stdin.resume();
process.stdin.on('end', () => {
console.log(JSON.stringify({ type: 'step_start' }));
console.log(JSON.stringify({ type: 'text', part: { text: '先确认几个问题再开始搭建。\\n<question-form id="discovery" title="Plugin brief">\\n{"questions":[{"id":"purpose","label":"What should it do?","type":"text"}]}\\n</question-form>' } }));
console.log(JSON.stringify({ type: 'step_finish', part: { tokens: { input: 1, output: 1 } } }));
process.exit(0);
});
`,
async () => {
const createResponse = await fetch(`${baseUrl}/api/runs`, {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify({
agentId: 'opencode',
projectId,
conversationId,
pluginId: 'od-plugin-authoring',
message: '帮我做个插件。',
}),
});
expect(createResponse.status).toBe(202);
const { runId } = await createResponse.json() as { runId: string };
const eventsResponse = await fetch(`${baseUrl}/api/runs/${runId}/events`);
const eventsBody = await readSseUntil(eventsResponse, 'event: final');
const statusBody = await waitForRunStatus(baseUrl, runId);
expect(eventsBody).toContain('<question-form');
expect(eventsBody).not.toContain('ended before generating the required generated-plugin artifacts');
expect(statusBody.status).toBe('succeeded');
},
);
});
it('does not fail plugin authoring when the clarifying form uses the <ask-question> alias', async () => {
// `<ask-question>` is the alias the web form parser accepts alongside
// the canonical `<question-form>` (apps/web/src/artifacts/question-form.ts).
// Models sometimes drift to it; the UI still renders a valid brief form,
// so the daemon's missing-artifacts guard must recognize the alias too —
// otherwise the same "ended before generating…" regression returns for a
// supported clarification shape.
const projectId = `proj-plugin-authoring-alias-${randomUUID()}`;
const createProjectResponse = await fetch(`${baseUrl}/api/projects`, {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify({
id: projectId,
name: 'Plugin authoring ask-question alias fixture',
skillId: null,
designSystemId: null,
}),
});
expect(createProjectResponse.status).toBe(200);
const conversationsResponse = await fetch(`${baseUrl}/api/projects/${projectId}/conversations`);
expect(conversationsResponse.status).toBe(200);
const conversationsBody = await conversationsResponse.json() as {
conversations: Array<{ id: string }>;
};
const conversationId = conversationsBody.conversations[0]?.id;
expect(conversationId).toBeTruthy();
await withFakeAgent(
'opencode',
`
process.stdin.resume();
process.stdin.on('end', () => {
console.log(JSON.stringify({ type: 'step_start' }));
console.log(JSON.stringify({ type: 'text', part: { text: '先确认几个问题再开始搭建。\\n<ask-question id="discovery" title="Plugin brief">\\n{"questions":[{"id":"purpose","label":"What should it do?","type":"text"}]}\\n</ask-question>' } }));
console.log(JSON.stringify({ type: 'step_finish', part: { tokens: { input: 1, output: 1 } } }));
process.exit(0);
});
`,
async () => {
const createResponse = await fetch(`${baseUrl}/api/runs`, {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify({
agentId: 'opencode',
projectId,
conversationId,
pluginId: 'od-plugin-authoring',
message: '帮我做个插件。',
}),
});
expect(createResponse.status).toBe(202);
const { runId } = await createResponse.json() as { runId: string };
const eventsResponse = await fetch(`${baseUrl}/api/runs/${runId}/events`);
const eventsBody = await readSseUntil(eventsResponse, 'event: final');
const statusBody = await waitForRunStatus(baseUrl, runId);
expect(eventsBody).toContain('<ask-question');
expect(eventsBody).not.toContain('ended before generating the required generated-plugin artifacts');
expect(statusBody.status).toBe('succeeded');
},
);
});
it('still fails plugin authoring when a question-form tag wraps a non-renderable (non-JSON) body', async () => {
// The clarification carve-out must match the web parser's renderable-form
// contract (JSON body with a `questions` array), not just the opening
// tag. A `<question-form>` whose body is not valid form JSON renders as
// raw prose in the UI — no usable brief card — so suppressing the
// missing-artifacts failure for it would turn a hard failure into a false
// success. This pins that the guard stays gated on a renderable body.
const projectId = `proj-plugin-authoring-badform-${randomUUID()}`;
const createProjectResponse = await fetch(`${baseUrl}/api/projects`, {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify({
id: projectId,
name: 'Plugin authoring malformed-form fixture',
skillId: null,
designSystemId: null,
}),
});
expect(createProjectResponse.status).toBe(200);
const conversationsResponse = await fetch(`${baseUrl}/api/projects/${projectId}/conversations`);
expect(conversationsResponse.status).toBe(200);
const conversationsBody = await conversationsResponse.json() as {
conversations: Array<{ id: string }>;
};
const conversationId = conversationsBody.conversations[0]?.id;
expect(conversationId).toBeTruthy();
await withFakeAgent(
'opencode',
`
process.stdin.resume();
process.stdin.on('end', () => {
console.log(JSON.stringify({ type: 'step_start' }));
console.log(JSON.stringify({ type: 'text', part: { text: '先确认几个问题。\\n<question-form id="discovery">\\nWhat should it do? (free text)\\n</question-form>' } }));
console.log(JSON.stringify({ type: 'step_finish', part: { tokens: { input: 1, output: 1 } } }));
process.exit(0);
});
`,
async () => {
const createResponse = await fetch(`${baseUrl}/api/runs`, {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify({
agentId: 'opencode',
projectId,
conversationId,
pluginId: 'od-plugin-authoring',
message: '帮我做个插件。',
}),
});
expect(createResponse.status).toBe(202);
const { runId } = await createResponse.json() as { runId: string };
const eventsResponse = await fetch(`${baseUrl}/api/runs/${runId}/events`);
const eventsBody = await readSseUntil(eventsResponse, 'event: final');
const statusBody = await waitForRunStatus(baseUrl, runId);
expect(eventsBody).toContain('ended before generating the required generated-plugin artifacts');
expect(statusBody.status).not.toBe('succeeded');
},
);
});
it('does not fail plugin authoring when a valid form follows a Unicode preamble that expands under toLowerCase', async () => {
// The mirrored close-tag scan must stay in the original-string coordinate
// space. Some code points expand under toLowerCase ("İ" -> "i̇"), so
// lowercasing the whole buffer before indexing would desync the close-tag
// offset and corrupt the JSON body slice, failing a valid form. The
// preamble here contains "İ" before a well-formed form block.
const projectId = `proj-plugin-authoring-unicode-${randomUUID()}`;
const createProjectResponse = await fetch(`${baseUrl}/api/projects`, {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify({
id: projectId,
name: 'Plugin authoring unicode-preamble fixture',
skillId: null,
designSystemId: null,
}),
});
expect(createProjectResponse.status).toBe(200);
const conversationsResponse = await fetch(`${baseUrl}/api/projects/${projectId}/conversations`);
expect(conversationsResponse.status).toBe(200);
const conversationsBody = await conversationsResponse.json() as {
conversations: Array<{ id: string }>;
};
const conversationId = conversationsBody.conversations[0]?.id;
expect(conversationId).toBeTruthy();
await withFakeAgent(
'opencode',
`
process.stdin.resume();
process.stdin.on('end', () => {
console.log(JSON.stringify({ type: 'step_start' }));
console.log(JSON.stringify({ type: 'text', part: { text: 'İstanbul brief — 先确认几个问题。\\n<ask-question id="discovery" title="Plugin brief">\\n{"questions":[{"id":"purpose","label":"What should it do?","type":"text"}]}\\n</ask-question>' } }));
console.log(JSON.stringify({ type: 'step_finish', part: { tokens: { input: 1, output: 1 } } }));
process.exit(0);
});
`,
async () => {
const createResponse = await fetch(`${baseUrl}/api/runs`, {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify({
agentId: 'opencode',
projectId,
conversationId,
pluginId: 'od-plugin-authoring',
message: '帮我做个插件。',
}),
});
expect(createResponse.status).toBe(202);
const { runId } = await createResponse.json() as { runId: string };
const eventsResponse = await fetch(`${baseUrl}/api/runs/${runId}/events`);
const eventsBody = await readSseUntil(eventsResponse, 'event: final');
const statusBody = await waitForRunStatus(baseUrl, runId);
expect(eventsBody).not.toContain('ended before generating the required generated-plugin artifacts');
expect(statusBody.status).toBe('succeeded');
},
);
});
it('closes the # Instructions block with an explicit "do not echo" guard so models do not parrot the prompt back', async () => {
// claude-opus-4-7 (and a few other instruction-tuned models) start
// their reply by echoing the # Instructions block verbatim, which
// shows up to users as the system prompt leading the visible
// answer. server.ts:9934 closes every Instructions block with a
// trailing guard line; this test pins the literal so a future
// refactor cannot silently drop it.
await withFakeAgent(
'opencode',
`
let prompt = '';
process.stdin.setEncoding('utf8');
process.stdin.on('data', (chunk) => {
prompt += chunk;
});
process.stdin.on('end', () => {
const checks = [
prompt.includes('Do not quote, restate, or echo the # Instructions block above')
? 'has-echo-guard'
: 'missing-echo-guard',
];
console.log(JSON.stringify({ type: 'step_start' }));
console.log(JSON.stringify({ type: 'text', part: { text: checks.join('\\n') } }));
console.log(JSON.stringify({ type: 'step_finish', part: { tokens: { input: 1, output: 1 } } }));
process.exit(0);
});
`,
async () => {
const response = await fetch(`${baseUrl}/api/chat`, {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify({
agentId: 'opencode',
message: 'hello',
}),
});
const body = await response.text();
expect(response.ok).toBe(true);
expect(body).toContain('has-echo-guard');
expect(body).not.toContain('missing-echo-guard');
},
);
});
it('injects @-mention skillIds into the composed system prompt', async () => {
await withFakeAgent(
'opencode',
`
let prompt = '';
process.stdin.setEncoding('utf8');
process.stdin.on('data', (chunk) => {
prompt += chunk;
});
process.stdin.on('end', () => {
const checks = [
prompt.includes('## Composed skill — faq-page') ? 'has-composed-skill-header' : 'missing-composed-skill-header',
prompt.includes('# FAQ Page Skill') ? 'has-faq-skill-body' : 'missing-faq-skill-body',
prompt.includes('category filtering') ? 'has-faq-skill-content' : 'missing-faq-skill-content',
];
console.log(JSON.stringify({ type: 'step_start' }));
console.log(JSON.stringify({ type: 'text', part: { text: checks.join('\\n') } }));
console.log(JSON.stringify({ type: 'step_finish', part: { tokens: { input: 1, output: 1 } } }));
process.exit(0);
});
`,
async () => {
const response = await fetch(`${baseUrl}/api/chat`, {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify({
agentId: 'opencode',
message: 'build an faq page',
skillIds: ['faq-page'],
}),
});
const body = await response.text();
expect(response.ok).toBe(true);
expect(body).toContain('has-composed-skill-header');
expect(body).toContain('has-faq-skill-body');
expect(body).toContain('has-faq-skill-content');
expect(body).not.toContain('missing-composed-skill-header');
expect(body).not.toContain('missing-faq-skill-body');
expect(body).not.toContain('missing-faq-skill-content');
},
);
});
it('stages ad-hoc skill side files into the project cwd', async () => {
const projectId = `project-${randomUUID()}`;
const stagedRelativePath = `.od-skills/${skillCwdAliasSegment(resolve(process.cwd(), '..', '..', 'skills', 'release-notes-one-pager'))}/references/checklist.md`;
const expectedChecklist = await fsp.readFile(
resolve(process.cwd(), '..', '..', 'skills', 'release-notes-one-pager', 'references', 'checklist.md'),
'utf8',
);
const createProjectResponse = await fetch(`${baseUrl}/api/projects`, {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify({
id: projectId,
name: 'Ad hoc staged skill project',
}),
});
expect(createProjectResponse.ok).toBe(true);
const fakeAgentScript = `
const fs = require('node:fs');
const stagedChecklist = fs.readFileSync(${JSON.stringify(stagedRelativePath)}, 'utf8');
if (stagedChecklist !== ${JSON.stringify(expectedChecklist)}) {
console.error('staged-skill-side-files-mismatch');
process.exit(1);
}
process.stdin.resume();
process.stdin.on('end', () => {
console.log(JSON.stringify({ type: 'step_start' }));
console.log(JSON.stringify({ type: 'text', part: { text: 'staged-skill-side-files-before-spawn' } }));
console.log(JSON.stringify({ type: 'step_finish', part: { tokens: { input: 1, output: 1 } } }));
process.exit(0);
});
`;
await withFakeAgent(
'opencode',
fakeAgentScript,
async () => {
const response = await fetch(`${baseUrl}/api/chat`, {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify({
agentId: 'opencode',
projectId,
message: 'draft the release notes',
skillIds: ['release-notes-one-pager'],
}),
});
const body = await response.text();
expect(response.ok).toBe(true);
expect(body).toContain('staged-skill-side-files-before-spawn');
},
);
const stagedFileResponse = await fetch(
`${baseUrl}/api/projects/${projectId}/raw/${stagedRelativePath}`,
);
const stagedFileBody = await stagedFileResponse.text();
expect(stagedFileResponse.ok).toBe(true);
expect(stagedFileBody).toBe(expectedChecklist);
});
it('stages side files for every composed skill into the project cwd', async () => {
const projectId = `project-${randomUUID()}`;
const stagedPaths = [
`.od-skills/${skillCwdAliasSegment(resolve(process.cwd(), '..', '..', 'skills', 'release-notes-one-pager'))}/references/checklist.md`,
`.od-skills/${skillCwdAliasSegment(resolve(process.cwd(), '..', '..', 'skills', 'swiss-creative-mode-template'))}/references/checklist.md`,
] as const;
const expectedBodies = await Promise.all(
[
resolve(process.cwd(), '..', '..', 'skills', 'release-notes-one-pager', 'references', 'checklist.md'),
resolve(process.cwd(), '..', '..', 'skills', 'swiss-creative-mode-template', 'references', 'checklist.md'),
].map((file) => fsp.readFile(file, 'utf8')),
);
const createProjectResponse = await fetch(`${baseUrl}/api/projects`, {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify({
id: projectId,
name: 'Multi staged skill project',
}),
});
expect(createProjectResponse.ok).toBe(true);
const fakeAgentScript = `
const fs = require('node:fs');
const stagedBodies = [
fs.readFileSync(${JSON.stringify(stagedPaths[0])}, 'utf8'),
fs.readFileSync(${JSON.stringify(stagedPaths[1])}, 'utf8'),
];
const expectedBodies = ${JSON.stringify(expectedBodies)};
if (JSON.stringify(stagedBodies) !== JSON.stringify(expectedBodies)) {
console.error('multi-staged-skill-side-files-mismatch');
process.exit(1);
}
process.stdin.resume();
process.stdin.on('end', () => {
console.log(JSON.stringify({ type: 'step_start' }));
console.log(JSON.stringify({ type: 'text', part: { text: 'multi-staged-skill-side-files-before-spawn' } }));
console.log(JSON.stringify({ type: 'step_finish', part: { tokens: { input: 1, output: 1 } } }));
process.exit(0);
});
`;
await withFakeAgent(
'opencode',
fakeAgentScript,
async () => {
const response = await fetch(`${baseUrl}/api/chat`, {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify({
agentId: 'opencode',
projectId,
message: 'compose multiple skills',
skillIds: ['release-notes-one-pager', 'swiss-creative-mode-template'],
}),
});
const body = await response.text();
expect(response.ok).toBe(true);
expect(body).toContain('multi-staged-skill-side-files-before-spawn');
},
);
});
it('propagates the composed skill mode for ad-hoc-only deck skills', async () => {
await withFakeAgent(
'opencode',
`
let prompt = '';
process.stdin.setEncoding('utf8');
process.stdin.on('data', (chunk) => {
prompt += chunk;
});
process.stdin.on('end', () => {
const checks = [
prompt.includes('## Composed skill — open-design-landing-deck') ? 'has-deck-skill-header' : 'missing-deck-skill-header',
prompt.includes('# Slide deck — fixed framework (this is non-negotiable for deck mode)') ? 'has-deck-framework' : 'missing-deck-framework',
];
console.log(JSON.stringify({ type: 'step_start' }));
console.log(JSON.stringify({ type: 'text', part: { text: checks.join('\\n') } }));
console.log(JSON.stringify({ type: 'step_finish', part: { tokens: { input: 1, output: 1 } } }));
process.exit(0);
});
`,
async () => {
const response = await fetch(`${baseUrl}/api/chat`, {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify({
agentId: 'opencode',
message: 'build an editorial brand deck',
skillIds: ['open-design-landing-deck'],
}),
});
const body = await response.text();
expect(response.ok).toBe(true);
expect(body).toContain('has-deck-skill-header');
expect(body).toContain('has-deck-framework');
expect(body).not.toContain('missing-deck-skill-header');
expect(body).not.toContain('missing-deck-framework');
},
);
});
it('preserves a persisted media skill as the primary surface over a composed deck mention', async () => {
await withFakeAgent(
'opencode',
`
let prompt = '';
process.stdin.setEncoding('utf8');
process.stdin.on('data', (chunk) => {
prompt += chunk;
});
process.stdin.on('end', () => {
const checks = [
prompt.includes('# imagegen') ? 'has-base-image-skill-body' : 'missing-base-image-skill-body',
prompt.includes('## Composed skill — open-design-landing-deck') ? 'has-composed-deck-skill-header' : 'missing-composed-deck-skill-header',
prompt.includes('## Media generation contract (load-bearing — overrides softer wording above)') ? 'has-image-contract' : 'missing-image-contract',
prompt.includes('# Slide deck — fixed framework (this is non-negotiable for deck mode)') ? 'unexpected-deck-framework' : 'kept-deck-framework-out',
];
console.log(JSON.stringify({ type: 'step_start' }));
console.log(JSON.stringify({ type: 'text', part: { text: checks.join('\\n') } }));
console.log(JSON.stringify({ type: 'step_finish', part: { tokens: { input: 1, output: 1 } } }));
process.exit(0);
});
`,
async () => {
const response = await fetch(`${baseUrl}/api/chat`, {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify({
agentId: 'opencode',
message: 'generate an image while also referencing a deck template',
skillId: 'imagegen',
skillIds: ['open-design-landing-deck'],
}),
});
const body = await response.text();
expect(response.ok).toBe(true);
expect(body).toContain('has-base-image-skill-body');
expect(body).toContain('has-composed-deck-skill-header');
expect(body).toContain('has-image-contract');
expect(body).toContain('kept-deck-framework-out');
expect(body).not.toContain('missing-base-image-skill-body');
expect(body).not.toContain('missing-composed-deck-skill-header');
expect(body).not.toContain('missing-image-contract');
expect(body).not.toContain('unexpected-deck-framework');
},
);
});
it('honors mediaExecution on legacy chat requests', async () => {
const conversationId = `conv-${randomUUID()}`;
const response = await fetch(`${baseUrl}/api/chat`, {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify({
agentId: `missing-agent-${randomUUID()}`,
conversationId,
message: 'plan an image without using OD media',
skillId: 'imagegen',
mediaExecution: {
mode: 'disabled',
allowedSurfaces: ['image'],
},
}),
});
const body = await response.text();
expect(response.ok).toBe(true);
expect(body).toContain('unknown agent');
const runsResponse = await fetch(
`${baseUrl}/api/runs?conversationId=${encodeURIComponent(conversationId)}`,
);
const runsBody = await runsResponse.json() as {
runs: Array<{ mediaExecution?: { mode?: string; allowedSurfaces?: string[] } }>;
};
expect(runsBody.runs).toHaveLength(1);
expect(runsBody.runs[0]?.mediaExecution).toMatchObject({
mode: 'disabled',
allowedSurfaces: ['image'],
});
});
it('rejects invalid mediaExecution on legacy chat requests', async () => {
const conversationId = `conv-${randomUUID()}`;
const response = await fetch(`${baseUrl}/api/chat`, {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify({
agentId: 'opencode',
conversationId,
message: 'generate an image',
mediaExecution: { mode: 'provider-router' },
}),
});
const body = await response.text();
expect(response.status).toBe(400);
expect(body).toContain('mediaExecution.mode');
const runsResponse = await fetch(
`${baseUrl}/api/runs?conversationId=${encodeURIComponent(conversationId)}`,
);
const runsBody = await runsResponse.json() as { runs: unknown[] };
expect(runsBody.runs).toEqual([]);
});
it('propagates ad-hoc skill critique policy into the chat resolver', async () => {
if (!process.env.OD_DATA_DIR) {
throw new Error('OD_DATA_DIR is required for user skill critique-policy tests');
}
const skillId = `critique-opt-out-${randomUUID()}`;
const skillDir = resolve(process.env.OD_DATA_DIR, 'skills', skillId);
const originalCritiqueEnabled = process.env.OD_CRITIQUE_ENABLED;
await fsp.mkdir(skillDir, { recursive: true });
await fsp.writeFile(
resolve(skillDir, 'SKILL.md'),
`---
name: ${skillId}
description: Ad-hoc critique opt-out regression fixture.
od:
critique:
policy: opt-out
---
# Critique opt-out fixture
This skill should suppress critique when selected through skillIds.
`,
'utf8',
);
process.env.OD_CRITIQUE_ENABLED = 'true';
try {
await withFakeAgent(
'opencode',
`
let prompt = '';
process.stdin.setEncoding('utf8');
process.stdin.on('data', (chunk) => {
prompt += chunk;
});
process.stdin.on('end', () => {
const checks = [
prompt.includes('## Composed skill — ${skillId}') ? 'has-opt-out-skill-header' : 'missing-opt-out-skill-header',
prompt.includes('<CRITIQUE_RUN') ? 'unexpected-critique-panel' : 'critique-panel-disabled-by-skill-policy',
];
console.log(JSON.stringify({ type: 'step_start' }));
console.log(JSON.stringify({ type: 'text', part: { text: checks.join('\\n') } }));
console.log(JSON.stringify({ type: 'step_finish', part: { tokens: { input: 1, output: 1 } } }));
process.exit(0);
});
`,
async () => {
const response = await fetch(`${baseUrl}/api/chat`, {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify({
agentId: 'opencode',
designSystemId: 'default',
message: 'draft an opt-out skill artifact',
skillIds: [skillId],
}),
});
const body = await response.text();
expect(response.ok).toBe(true);
expect(body).toContain('has-opt-out-skill-header');
expect(body).toContain('critique-panel-disabled-by-skill-policy');
expect(body).not.toContain('missing-opt-out-skill-header');
expect(body).not.toContain('unexpected-critique-panel');
},
);
} finally {
if (originalCritiqueEnabled == null) {
delete process.env.OD_CRITIQUE_ENABLED;
} else {
process.env.OD_CRITIQUE_ENABLED = originalCritiqueEnabled;
}
await fsp.rm(skillDir, { recursive: true, force: true });
}
});
it('preserves plugin-local and composed @-mention skills in plugin-bound runs', async () => {
const pluginId = `plugin-local-${randomUUID()}`;
const pluginFixtureDir = await createPluginFixture({
pluginId,
dirName: `plugin-local-${randomUUID()}`,
localSkillPath: './SKILL.md',
});
const installResponse = await fetch(`${baseUrl}/api/plugins/install`, {
method: 'POST',
headers: { 'Content-Type': 'application/json', accept: 'text/event-stream' },
body: JSON.stringify({ source: pluginFixtureDir }),
});
const installBody = await installResponse.text();
expect(installResponse.status).toBe(200);
expect(installBody).toContain(`"id":"${pluginId}"`);
const projectId = `project-${randomUUID()}`;
const createProjectResponse = await fetch(`${baseUrl}/api/projects`, {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify({
id: projectId,
name: 'Plugin-bound skill composition project',
pluginId,
pluginInputs: { topic: 'agentic design' },
}),
});
const createProjectBody = await createProjectResponse.json() as {
appliedPluginSnapshotId?: string;
};
expect(createProjectResponse.ok).toBe(true);
expect(createProjectBody.appliedPluginSnapshotId).toBeTruthy();
await withFakeAgent(
'opencode',
`
let prompt = '';
process.stdin.setEncoding('utf8');
process.stdin.on('data', (chunk) => {
prompt += chunk;
});
process.stdin.on('end', () => {
const checks = [
prompt.includes('# Sample Plugin') ? 'has-plugin-skill-body' : 'missing-plugin-skill-body',
prompt.includes('## Composed skill — faq-page') ? 'has-composed-skill-header' : 'missing-composed-skill-header',
prompt.includes('# FAQ Page Skill') ? 'has-composed-skill-body' : 'missing-composed-skill-body',
];
console.log(JSON.stringify({ type: 'step_start' }));
console.log(JSON.stringify({ type: 'text', part: { text: checks.join('\\n') } }));
console.log(JSON.stringify({ type: 'step_finish', part: { tokens: { input: 1, output: 1 } } }));
process.exit(0);
});
`,
async () => {
const createRunResponse = await fetch(`${baseUrl}/api/runs`, {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify({
agentId: 'opencode',
projectId,
message: 'build a plugin-backed faq page',
appliedPluginSnapshotId: createProjectBody.appliedPluginSnapshotId,
skillIds: ['faq-page'],
}),
});
const createRunBody = await createRunResponse.json() as { runId: string };
expect(createRunResponse.status).toBe(202);
const eventsResponse = await fetch(`${baseUrl}/api/runs/${createRunBody.runId}/events`);
const body = await readSseUntil(eventsResponse, 'event: final');
expect(body).toContain('has-plugin-skill-body');
expect(body).toContain('has-composed-skill-header');
expect(body).toContain('has-composed-skill-body');
expect(body).not.toContain('missing-plugin-skill-body');
expect(body).not.toContain('missing-composed-skill-header');
expect(body).not.toContain('missing-composed-skill-body');
},
);
});
it('stages colliding plugin and composed skill dirs under distinct aliases', async () => {
if (!process.env.OD_DATA_DIR) {
throw new Error('OD_DATA_DIR is required for colliding skill-dir staging tests');
}
const pluginId = `plugin-collision-${randomUUID()}`;
const pluginFixtureDir = await createPluginFixture({
pluginId,
dirName: 'sample-plugin',
localSkillPath: './SKILL.md',
});
const installResponse = await fetch(`${baseUrl}/api/plugins/install`, {
method: 'POST',
headers: { 'Content-Type': 'application/json', accept: 'text/event-stream' },
body: JSON.stringify({ source: pluginFixtureDir }),
});
const installBody = await installResponse.text();
expect(installResponse.status).toBe(200);
expect(installBody).toContain(`"id":"${pluginId}"`);
const projectId = `project-${randomUUID()}`;
const userSkillDir = resolve(process.env.OD_DATA_DIR, 'skills', 'sample-plugin');
const userChecklist = 'user-skill-checklist';
const userAlias = skillCwdAliasSegment(userSkillDir);
await fsp.mkdir(resolve(userSkillDir, 'references'), { recursive: true });
await fsp.writeFile(
resolve(userSkillDir, 'SKILL.md'),
'# Sample-plugin side-file fixture\n\nRead references/checklist.md before drafting.',
'utf8',
);
await fsp.writeFile(resolve(userSkillDir, 'references', 'checklist.md'), userChecklist, 'utf8');
try {
const createProjectResponse = await fetch(`${baseUrl}/api/projects`, {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify({
id: projectId,
name: 'Colliding skill-dir project',
pluginId,
pluginInputs: { topic: 'agentic design' },
}),
});
const createProjectBody = await createProjectResponse.json() as {
appliedPluginSnapshotId?: string;
};
const installedPluginResponse = await fetch(`${baseUrl}/api/plugins/${pluginId}`);
const installedPluginBody = await installedPluginResponse.json() as { fsPath: string };
const pluginAlias = skillCwdAliasSegment(installedPluginBody.fsPath);
expect(createProjectResponse.ok).toBe(true);
expect(installedPluginResponse.ok).toBe(true);
expect(createProjectBody.appliedPluginSnapshotId).toBeTruthy();
expect(pluginAlias).not.toBe(userAlias);
await withFakeAgent(
'opencode',
`
const fs = require('node:fs');
const pluginSkill = fs.readFileSync(${JSON.stringify(`.od-skills/${pluginAlias}/SKILL.md`)}, 'utf8');
const userChecklist = fs.readFileSync(${JSON.stringify(`.od-skills/${userAlias}/references/checklist.md`)}, 'utf8');
if (!pluginSkill.includes('# Sample Plugin')) {
console.error('plugin-skill-stage-missing');
process.exit(1);
}
if (userChecklist !== ${JSON.stringify(userChecklist)}) {
console.error('colliding-skill-stage-mismatch');
process.exit(1);
}
process.stdin.resume();
process.stdin.on('end', () => {
console.log(JSON.stringify({ type: 'step_start' }));
console.log(JSON.stringify({ type: 'text', part: { text: 'colliding-skill-dirs-staged' } }));
console.log(JSON.stringify({ type: 'step_finish', part: { tokens: { input: 1, output: 1 } } }));
process.exit(0);
});
`,
async () => {
const createRunResponse = await fetch(`${baseUrl}/api/runs`, {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify({
agentId: 'opencode',
projectId,
message: 'use both plugin and user skill side files',
appliedPluginSnapshotId: createProjectBody.appliedPluginSnapshotId,
skillIds: ['sample-plugin'],
}),
});
const createRunBody = await createRunResponse.json() as { runId: string };
expect(createRunResponse.status).toBe(202);
const eventsResponse = await fetch(`${baseUrl}/api/runs/${createRunBody.runId}/events`);
const body = await readSseUntil(eventsResponse, 'event: final');
expect(body).toContain('colliding-skill-dirs-staged');
},
);
} finally {
await fsp.rm(userSkillDir, { recursive: true, force: true });
}
});
it('canonicalizes aliased skill ids before deduping composed skills', async () => {
await withFakeAgent(
'opencode',
`
let prompt = '';
process.stdin.setEncoding('utf8');
process.stdin.on('data', (chunk) => {
prompt += chunk;
});
process.stdin.on('end', () => {
const hasDuplicateComposedAlias = prompt.includes('## Composed skill — open-design-landing');
const checks = [
hasDuplicateComposedAlias ? 'duplicate-alias-composed-skill' : 'deduped-alias-composed-skill',
prompt.includes('# open-design-landing') ? 'has-base-alias-skill-body' : 'missing-base-alias-skill-body',
];
console.log(JSON.stringify({ type: 'step_start' }));
console.log(JSON.stringify({ type: 'text', part: { text: checks.join('\\n') } }));
console.log(JSON.stringify({ type: 'step_finish', part: { tokens: { input: 1, output: 1 } } }));
process.exit(0);
});
`,
async () => {
const response = await fetch(`${baseUrl}/api/chat`, {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify({
agentId: 'opencode',
message: 'build the OpenDesign landing page',
skillId: 'editorial-collage',
skillIds: ['open-design-landing'],
}),
});
const body = await response.text();
expect(response.ok).toBe(true);
expect(body).toContain('deduped-alias-composed-skill');
expect(body).toContain('has-base-alias-skill-body');
expect(body).not.toContain('duplicate-alias-composed-skill');
expect(body).not.toContain('missing-base-alias-skill-body');
},
);
});
it('classifies Cursor Agent authentication stderr as a typed run error', async () => {
await withFakeAgent(
'cursor-agent',
`
const args = process.argv.slice(2);
if (args[0] === '--version') {
console.log('2026.05.07-test');
process.exit(0);
}
if (args[0] === 'models') {
console.log('auto');
process.exit(0);
}
console.error("Authentication required. Please run 'agent login' first, or set CURSOR_API_KEY environment variable.");
process.exit(1);
`,
async () => {
const createResponse = await fetch(`${baseUrl}/api/runs`, {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify({
agentId: 'cursor-agent',
message: 'hello',
}),
});
expect(createResponse.status).toBe(202);
const { runId } = await createResponse.json() as { runId: string };
const eventsController = new AbortController();
const eventsResponse = await fetch(`${baseUrl}/api/runs/${runId}/events`, {
signal: eventsController.signal,
});
const eventsBody = await readSseUntil(eventsResponse, 'AGENT_AUTH_REQUIRED');
eventsController.abort();
const statusBody = await waitForRunStatus(baseUrl, runId);
expect(eventsBody).toContain('event: error');
expect(eventsBody).toContain('AGENT_AUTH_REQUIRED');
expect(eventsBody).toContain('cursor-agent login');
expect(eventsBody).toContain('cursor-agent status');
expect(statusBody.status).toBe('failed');
},
);
});
it('classifies Cursor Agent Not logged in stderr as a typed run error', async () => {
await withFakeAgent(
'cursor-agent',
`
const args = process.argv.slice(2);
if (args[0] === '--version') {
console.log('2026.05.07-test');
process.exit(0);
}
if (args[0] === 'models') {
console.log('auto');
process.exit(0);
}
console.error('Not logged in');
process.exit(1);
`,
async () => {
const createResponse = await fetch(`${baseUrl}/api/runs`, {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify({
agentId: 'cursor-agent',
message: 'hello',
}),
});
expect(createResponse.status).toBe(202);
const { runId } = await createResponse.json() as { runId: string };
const eventsController = new AbortController();
const eventsResponse = await fetch(`${baseUrl}/api/runs/${runId}/events`, {
signal: eventsController.signal,
});
const eventsBody = await readSseUntil(eventsResponse, 'AGENT_AUTH_REQUIRED');
eventsController.abort();
const statusBody = await waitForRunStatus(baseUrl, runId);
expect(eventsBody).toContain('event: error');
expect(eventsBody).toContain('AGENT_AUTH_REQUIRED');
expect(eventsBody).toContain('cursor-agent login');
expect(eventsBody).toContain('cursor-agent status');
expect(statusBody.status).toBe('failed');
},
);
});
it('classifies Cursor Agent stdout auth text as a typed run error', async () => {
await withFakeAgent(
'cursor-agent',
`
const args = process.argv.slice(2);
if (args[0] === '--version') {
console.log('2026.05.07-test');
process.exit(0);
}
if (args[0] === 'models') {
console.log('auto');
process.exit(0);
}
console.log('ConnectError: [unauthenticated]');
process.exit(1);
`,
async () => {
const createResponse = await fetch(`${baseUrl}/api/runs`, {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify({
agentId: 'cursor-agent',
message: 'hello',
}),
});
expect(createResponse.status).toBe(202);
const { runId } = await createResponse.json() as { runId: string };
const eventsController = new AbortController();
const eventsResponse = await fetch(`${baseUrl}/api/runs/${runId}/events`, {
signal: eventsController.signal,
});
const eventsBody = await readSseUntil(eventsResponse, 'AGENT_AUTH_REQUIRED');
eventsController.abort();
const statusBody = await waitForRunStatus(baseUrl, runId);
expect(eventsBody).toContain('event: error');
expect(eventsBody).toContain('AGENT_AUTH_REQUIRED');
expect(eventsBody).toContain('cursor-agent login');
expect(eventsBody).toContain('cursor-agent status');
expect(eventsBody).not.toContain('AGENT_EXECUTION_FAILED');
expect(statusBody.status).toBe('failed');
},
);
});
it('classifies Cursor Agent stdout error payloads as typed auth failures', async () => {
const cursorErrorLine = JSON.stringify({
type: 'error',
message: 'Error: [unauthenticated] Error',
});
await withFakeAgent(
'cursor-agent',
`
const args = process.argv.slice(2);
if (args[0] === '--version') {
console.log('2026.05.07-test');
process.exit(0);
}
if (args[0] === 'models') {
console.log('auto');
process.exit(0);
}
console.log(${JSON.stringify(cursorErrorLine)});
process.exit(1);
`,
async () => {
const createResponse = await fetch(`${baseUrl}/api/runs`, {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify({
agentId: 'cursor-agent',
message: 'hello',
}),
});
expect(createResponse.status).toBe(202);
const { runId } = await createResponse.json() as { runId: string };
const eventsController = new AbortController();
const eventsResponse = await fetch(`${baseUrl}/api/runs/${runId}/events`, {
signal: eventsController.signal,
});
const eventsBody = await readSseUntil(eventsResponse, 'AGENT_AUTH_REQUIRED');
eventsController.abort();
const statusBody = await waitForRunStatus(baseUrl, runId);
expect(eventsBody).toContain('event: error');
expect(eventsBody).toContain('AGENT_AUTH_REQUIRED');
expect(eventsBody).toContain('cursor-agent login');
expect(eventsBody).toContain('cursor-agent status');
expect(eventsBody).not.toContain('AGENT_EXECUTION_FAILED');
expect(statusBody.status).toBe('failed');
},
);
});
it('classifies DeepSeek TUI config guidance as typed auth failures', async () => {
await withFakeAgent(
'deepseek',
`
const args = process.argv.slice(2);
if (args[0] === '--version') {
console.log('deepseek 0.3.0-test');
process.exit(0);
}
console.error('KEY=<your-key> deepseek --api-key <your-key>');
console.error('api_key = "<your-key>" in ~/.deepseek/config.toml');
process.exit(1);
`,
async () => {
const deepseek = getAgentDef('deepseek');
expect(deepseek).toBeDefined();
const originalBudget = deepseek?.maxPromptArgBytes;
if (deepseek) deepseek.maxPromptArgBytes = 200_000;
try {
const createResponse = await fetch(`${baseUrl}/api/runs`, {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify({
agentId: 'deepseek',
message: 'hello',
}),
});
expect(createResponse.status).toBe(202);
const { runId } = await createResponse.json() as { runId: string };
const eventsController = new AbortController();
const eventsResponse = await fetch(`${baseUrl}/api/runs/${runId}/events`, {
signal: eventsController.signal,
});
const eventsBody = await readSseUntil(eventsResponse, 'AGENT_AUTH_REQUIRED');
eventsController.abort();
const statusBody = await waitForRunStatus(baseUrl, runId);
expect(eventsBody).toContain('event: error');
expect(eventsBody).toContain('AGENT_AUTH_REQUIRED');
expect(eventsBody).toContain('~/.deepseek/config.toml');
expect(eventsBody).toContain('DEEPSEEK_API_KEY');
expect(eventsBody).not.toContain('cursor-agent login');
expect(eventsBody).not.toContain('AGENT_EXECUTION_FAILED');
expect(statusBody.status).toBe('failed');
} finally {
if (deepseek) {
if (originalBudget === undefined) {
delete deepseek.maxPromptArgBytes;
} else {
deepseek.maxPromptArgBytes = originalBudget;
}
}
}
},
);
});
// OPEND-3495. The fake `agy` enforces the real CLI's prompt contract
// instead of echoing whatever it is handed:
// - `-p/--print <text>` ignores stdin entirely (agy 1.1.1 changelog: "no
// longer reading stdin when a prompt is provided via a flag"), so a
// daemon that puts a pointer in `-p` and the prompt on stdin gets a reply
// that never saw the prompt;
// - `--input-format stream-json` (requires `--output-format stream-json`)
// reads NDJSON `{"event":"user","message":{"content":…}}` messages from
// stdin and answers with agy's stream-json events (see
// tests/fixtures/antigravity-stream-json).
// The reply reports what actually arrived, so the test can only pass when
// the composed prompt reaches agy through the channel agy reads.
const FAKE_AGY_CONTRACT = `
const args = process.argv.slice(2);
if (args[0] === '--version') {
console.log('1.3.1');
process.exit(0);
}
if (args.includes('-p') || args.includes('--print') || args.includes('--prompt')) {
process.stdout.write('I do not see any instructions in your message.\\n');
process.exit(0);
}
const inputIndex = args.indexOf('--input-format');
const outputIndex = args.indexOf('--output-format');
if (inputIndex === -1 || args[inputIndex + 1] !== 'stream-json' || outputIndex === -1 || args[outputIndex + 1] !== 'stream-json') {
process.stderr.write('Error: no prompt provided\\n');
process.exit(2);
}
let stdin = '';
process.stdin.setEncoding('utf8');
process.stdin.on('data', (chunk) => { stdin += chunk; });
process.stdin.on('end', () => {
const emit = (event) => process.stdout.write(JSON.stringify(event) + '\\n');
const conversation_id = 'agy-fake-1';
emit({ event: 'init', conversation_id, init: { cwd: process.cwd(), tools: ['run_command'], permission_mode: 'request-review' } });
const messages = stdin.split('\\n').filter((line) => line.trim()).map((line) => JSON.parse(line));
if (messages.length !== 1 || messages[0].event !== 'user' || typeof messages[0].message?.content !== 'string') {
process.stderr.write('error: stream input "user" message has no content\\n');
emit({ event: 'result', result: { conversation_id, status: 'ERROR', response: '', error: 'stream input "user" message has no content', duration_seconds: 0, num_turns: 0 } });
process.exit(1);
}
const content = messages[0].message.content;
emit({ event: 'step_update', step_update: { conversation_id, step_index: 0, state: 'DONE', step_type: 'user_input' } });
const reply = 'RECEIVED ' + content.length + ' chars ending ' + JSON.stringify(content.slice(-40));
emit({ event: 'step_update', step_update: { conversation_id, step_index: 1, state: 'DONE', step_type: 'agent_response', text_delta: reply, duration_seconds: 0.5, usage: { input_tokens: 9, output_tokens: 3, thinking_tokens: 0, cache_read_tokens: 0, total_tokens: 12 } } });
emit({ event: 'result', result: { conversation_id, status: 'SUCCESS', response: reply, duration_seconds: 0.6, num_turns: 1, usage: { input_tokens: 9, output_tokens: 3, thinking_tokens: 0, cache_read_tokens: 0, total_tokens: 12 } } });
process.exit(0);
});
`;
function fakeAgyReplaying(name: string, exitCode: number): string {
const fixtures = fileURLToPath(new URL('./fixtures/antigravity-stream-json/', import.meta.url));
const stdout = readFileSync(join(fixtures, `${name}.stdout.ndjson`), 'utf8');
const stderr = readFileSync(join(fixtures, `${name}.stderr.txt`), 'utf8');
return `
const args = process.argv.slice(2);
if (args[0] === '--version') {
console.log('1.3.1');
process.exit(0);
}
process.stdin.resume();
process.stdin.on('end', () => {
process.stdout.write(${JSON.stringify(stdout)});
process.stderr.write(${JSON.stringify(stderr)});
process.exit(${exitCode});
});
`;
}
it('delivers the whole composed prompt to Antigravity through agy stream-json stdin', async () => {
const marker = 'OPEND_3495_END_OF_USER_REQUEST';
await withFakeAgent('agy', FAKE_AGY_CONTRACT, async () => {
const createResponse = await fetch(`${baseUrl}/api/runs`, {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify({
agentId: 'antigravity',
message: `${'Pad the composed prompt past the Windows command-line cap. '.repeat(700)}${marker}`,
}),
});
expect(createResponse.status).toBe(202);
const { runId } = await createResponse.json() as { runId: string };
const eventsController = new AbortController();
const eventsResponse = await fetch(`${baseUrl}/api/runs/${runId}/events`, {
signal: eventsController.signal,
});
const eventsBody = await readSseUntil(eventsResponse, 'event: final');
eventsController.abort();
const statusBody = await waitForRunStatus(baseUrl, runId);
expect(eventsBody).toContain('"type":"text_delta","delta":"RECEIVED ');
expect(eventsBody).toContain(marker);
expect(eventsBody).not.toContain('I do not see any instructions');
expect(eventsBody).not.toContain('event: stdout');
expect(eventsBody).toContain('"type":"usage"');
expect(statusBody.status).toBe('succeeded');
});
});
it('reports a signed-out Antigravity CLI as AGENT_AUTH_REQUIRED', async () => {
await withFakeAgent('agy', fakeAgyReplaying('signed-out', 1), async () => {
const createResponse = await fetch(`${baseUrl}/api/runs`, {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify({ agentId: 'antigravity', message: 'hello' }),
});
expect(createResponse.status).toBe(202);
const { runId } = await createResponse.json() as { runId: string };
const eventsController = new AbortController();
const eventsResponse = await fetch(`${baseUrl}/api/runs/${runId}/events`, {
signal: eventsController.signal,
});
const eventsBody = await readSseUntil(eventsResponse, 'event: final');
eventsController.abort();
const statusBody = await waitForRunStatus(baseUrl, runId);
expect(eventsBody).toContain('event: error');
expect(eventsBody).toContain('AGENT_AUTH_REQUIRED');
expect(eventsBody).toContain('open a terminal and run `agy` once');
expect(eventsBody).not.toContain('event: stdout');
expect(statusBody.status).toBe('failed');
});
});
it('reports an exhausted Antigravity quota as RATE_LIMITED', async () => {
await withFakeAgent('agy', fakeAgyReplaying('quota-exhausted', 3), async () => {
const createResponse = await fetch(`${baseUrl}/api/runs`, {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify({ agentId: 'antigravity', message: 'hello' }),
});
expect(createResponse.status).toBe(202);
const { runId } = await createResponse.json() as { runId: string };
const eventsController = new AbortController();
const eventsResponse = await fetch(`${baseUrl}/api/runs/${runId}/events`, {
signal: eventsController.signal,
});
const eventsBody = await readSseUntil(eventsResponse, 'event: final');
eventsController.abort();
const statusBody = await waitForRunStatus(baseUrl, runId);
expect(eventsBody).toContain('event: error');
expect(eventsBody).toContain('RATE_LIMITED');
expect(eventsBody).toContain('Individual quota reached');
expect(eventsBody).toContain('Switch Model picker');
expect(eventsBody).not.toContain('event: stdout');
expect(statusBody.status).toBe('failed');
});
});
it('fails plain-stream runs when stdout artifact persistence fails', async () => {
await withFakeAgent(
'qwen',
`
const args = process.argv.slice(2);
if (args[0] === '--version') {
console.log('1.107.0-test');
process.exit(0);
}
process.stdout.write('<artifact identifier="blocked" type="text/html" title="Blocked"><!doctype html><html><body>Name to confirm</body></html></artifact>\\n');
process.exit(0);
`,
async () => {
const projectId = `plain-artifact-fail-${randomUUID()}`;
const createProjectResponse = await fetch(`${baseUrl}/api/projects`, {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify({ id: projectId, name: 'Plain artifact failure' }),
});
expect(createProjectResponse.ok).toBe(true);
const createResponse = await fetch(`${baseUrl}/api/runs`, {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify({
agentId: 'qwen',
projectId,
message: 'emit blocked artifact',
}),
});
expect(createResponse.status).toBe(202);
const { runId } = await createResponse.json() as { runId: string };
const eventsController = new AbortController();
const eventsResponse = await fetch(`${baseUrl}/api/runs/${runId}/events`, {
signal: eventsController.signal,
});
const eventsBody = await readSseUntil(eventsResponse, 'event: final');
eventsController.abort();
const statusBody = await waitForRunStatus(baseUrl, runId);
expect(eventsBody).toContain('event: error');
expect(eventsBody).toContain('plain-stream artifact');
expect(statusBody.status).toBe('failed');
},
);
});
it('surfaces Qoder assistant error records through the SSE error channel', async () => {
const qoderErrorLine = JSON.stringify({
type: 'assistant',
message: { content: [] },
error: { message: 'Qoder authentication expired' },
});
await withFakeAgent(
'qodercli',
`console.log(${JSON.stringify(qoderErrorLine)});\nprocess.exit(0);\n`,
async () => {
const createResponse = await fetch(`${baseUrl}/api/runs`, {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify({
agentId: 'qoder',
message: 'hello',
}),
});
expect(createResponse.status).toBe(202);
const { runId } = await createResponse.json() as { runId: string };
const eventsController = new AbortController();
const eventsResponse = await fetch(`${baseUrl}/api/runs/${runId}/events`, {
signal: eventsController.signal,
});
const eventsBody = await readSseUntil(eventsResponse, 'event: error');
eventsController.abort();
const statusBody = await waitForRunStatus(baseUrl, runId);
expect(eventsBody).toContain('event: error');
expect(eventsBody).toContain('Qoder authentication expired');
expect(eventsBody).not.toContain('event: agent\\ndata: {"type":"error"');
expect(statusBody.status).toBe('failed');
},
);
});
it('marks reasoning-only stream runs failed when no assistant output is produced', async () => {
const reasoningLine = JSON.stringify({
type: 'assistant',
message: {
content: [
{
type: 'thinking',
thinking: 'I should inspect the project before answering.',
},
],
},
});
const resultLine = JSON.stringify({
type: 'result',
is_error: false,
usage: { input_tokens: 1, output_tokens: 0 },
});
await withFakeAgent(
'qodercli',
`
console.log(${JSON.stringify(reasoningLine)});
console.log(${JSON.stringify(resultLine)});
process.exit(0);
`,
async () => {
const createResponse = await fetch(`${baseUrl}/api/runs`, {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify({
agentId: 'qoder',
message: 'think but do not answer',
}),
});
expect(createResponse.status).toBe(202);
const { runId } = await createResponse.json() as { runId: string };
const eventsController = new AbortController();
const eventsResponse = await fetch(`${baseUrl}/api/runs/${runId}/events`, {
signal: eventsController.signal,
});
const eventsBody = await readSseUntil(
eventsResponse,
'Agent completed without producing any output',
);
eventsController.abort();
const statusBody = await waitForRunStatus(baseUrl, runId);
expect(eventsBody).toContain('"type":"thinking_delta"');
expect(eventsBody).toContain('AGENT_EXECUTION_FAILED');
expect(eventsBody).toContain('Agent completed without producing any output');
expect(eventsBody).not.toContain('"status":"succeeded"');
expect(statusBody.status).toBe('failed');
},
);
});
it('fails Qoder runs when the result reports is_error with exit code 0', async () => {
const qoderResultLine = JSON.stringify({
type: 'result',
subtype: 'error',
duration_ms: 17,
is_error: true,
stop_reason: 'tool_use_failed',
total_cost_usd: 0,
usage: {
input_tokens: 3,
output_tokens: 1,
},
});
await withFakeAgent(
'qodercli',
`console.log(${JSON.stringify(qoderResultLine)});\nprocess.exit(0);\n`,
async () => {
const createResponse = await fetch(`${baseUrl}/api/runs`, {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify({
agentId: 'qoder',
message: 'hello',
}),
});
expect(createResponse.status).toBe(202);
const { runId } = await createResponse.json() as { runId: string };
const eventsController = new AbortController();
const eventsResponse = await fetch(`${baseUrl}/api/runs/${runId}/events`, {
signal: eventsController.signal,
});
const eventsBody = await readSseUntil(eventsResponse, 'event: error');
eventsController.abort();
const statusBody = await waitForRunStatus(baseUrl, runId);
expect(eventsBody).toContain('event: agent');
expect(eventsBody).toContain('"type":"usage"');
expect(eventsBody).toContain('"isError":true');
expect(eventsBody).toContain('event: error');
expect(eventsBody).toContain('Qoder run failed: tool_use_failed');
expect(statusBody.status).toBe('failed');
},
);
});
it('fails stalled json-stream runs after the inactivity timeout elapses', async () => {
const previous = process.env.OD_CHAT_RUN_INACTIVITY_TIMEOUT_MS;
process.env.OD_CHAT_RUN_INACTIVITY_TIMEOUT_MS = '500';
try {
await withFakeAgent(
'opencode',
`
console.log(JSON.stringify({ type: 'step_start' }));
process.on('SIGTERM', () => process.exit(143));
setInterval(() => {}, 1000);
`,
async () => {
const createResponse = await fetch(`${baseUrl}/api/runs`, {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify({
agentId: 'opencode',
message: 'hello',
}),
});
expect(createResponse.status).toBe(202);
const { runId } = await createResponse.json() as { runId: string };
const eventsController = new AbortController();
const eventsResponse = await fetch(`${baseUrl}/api/runs/${runId}/events`, {
signal: eventsController.signal,
});
const eventsBody = await readSseUntil(eventsResponse, 'event: error');
eventsController.abort();
const statusBody = await waitForRunStatus(baseUrl, runId);
expect(eventsBody).toContain('event: error');
expect(eventsBody).toContain('Agent stalled without emitting any new output');
expect(eventsBody).toContain('Phase details: spawned agent opencode;');
expect(eventsBody).not.toContain('spawned agent binary');
expect(eventsBody).toMatch(/stdout arrived: (yes|no)/);
expect(statusBody.status).toBe('failed');
},
);
} finally {
if (previous == null) {
delete process.env.OD_CHAT_RUN_INACTIVITY_TIMEOUT_MS;
} else {
process.env.OD_CHAT_RUN_INACTIVITY_TIMEOUT_MS = previous;
}
}
});
it('keeps Claude stream runs alive while structured output is still flowing', async () => {
const previous = process.env.OD_CHAT_RUN_INACTIVITY_TIMEOUT_MS;
process.env.OD_CHAT_RUN_INACTIVITY_TIMEOUT_MS = '3000';
try {
await withFakeAgent(
'claude',
`
const lines = [
JSON.stringify({ type: 'stream_event', event: { type: 'message_start', message: { id: 'msg-1' }, ttft_ms: 10 } }),
JSON.stringify({ type: 'stream_event', event: { type: 'content_block_start', index: 0, content_block: { type: 'text' } } }),
JSON.stringify({ type: 'stream_event', event: { type: 'content_block_delta', index: 0, delta: { type: 'text_delta', text: 'hello ' } } }),
JSON.stringify({ type: 'stream_event', event: { type: 'content_block_delta', index: 0, delta: { type: 'text_delta', text: 'world' } } }),
JSON.stringify({ type: 'stream_event', event: { type: 'content_block_stop', index: 0 } }),
JSON.stringify({ type: 'result', usage: { input_tokens: 1, output_tokens: 2 }, duration_ms: 700, stop_reason: 'end_turn' }),
];
let index = 0;
console.log(lines[index++]);
const timer = setInterval(() => {
if (index >= lines.length) {
clearInterval(timer);
process.exit(0);
return;
}
console.log(lines[index++]);
}, 750);
`,
async () => {
const createResponse = await fetch(`${baseUrl}/api/runs`, {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify({
agentId: 'claude',
message: 'hello',
}),
});
expect(createResponse.status).toBe(202);
const { runId } = await createResponse.json() as { runId: string };
const statusBody = await waitForRunStatus(baseUrl, runId);
expect(statusBody.status).toBe('succeeded');
},
);
} finally {
if (previous == null) {
delete process.env.OD_CHAT_RUN_INACTIVITY_TIMEOUT_MS;
} else {
process.env.OD_CHAT_RUN_INACTIVITY_TIMEOUT_MS = previous;
}
}
});
it('surfaces Claude auth diagnostics through the SSE error channel', async () => {
await withFakeAgent(
'claude',
`
console.error(JSON.stringify({ apiKeySource: 'none', error_status: 401 }));
process.exit(1);
`,
async () => {
const createResponse = await fetch(`${baseUrl}/api/runs`, {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify({
agentId: 'claude',
message: 'hello',
}),
});
expect(createResponse.status).toBe(202);
const { runId } = await createResponse.json() as { runId: string };
const eventsController = new AbortController();
const eventsResponse = await fetch(`${baseUrl}/api/runs/${runId}/events`, {
signal: eventsController.signal,
});
const eventsBody = await readSseUntil(eventsResponse, 'event: error');
eventsController.abort();
const statusBody = await waitForRunStatus(baseUrl, runId);
expect(eventsBody).toContain('event: error');
expect(eventsBody).toContain('/login');
expect(eventsBody).toContain('CLAUDE_CONFIG_DIR');
expect(statusBody.status).toBe('failed');
},
);
});
it('prefers a terminal Claude prompt-length error over auth-shaped stderr (#6979)', async () => {
await withFakeAgent(
'claude',
`
console.error(JSON.stringify({ apiKeySource: 'none' }));
console.log(JSON.stringify({
type: 'result',
subtype: 'error_during_execution',
is_error: true,
result: 'Prompt is too long',
stop_reason: null,
}));
process.exit(1);
`,
async () => {
const createResponse = await fetch(`${baseUrl}/api/runs`, {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify({
agentId: 'claude',
message: 'hello',
}),
});
expect(createResponse.status).toBe(202);
const { runId } = await createResponse.json() as { runId: string };
const eventsController = new AbortController();
const eventsResponse = await fetch(`${baseUrl}/api/runs/${runId}/events`, {
signal: eventsController.signal,
});
const eventsBody = await readSseUntil(eventsResponse, 'event: error');
eventsController.abort();
await waitForRunStatus(baseUrl, runId);
const statusResponse = await fetch(`${baseUrl}/api/runs/${runId}`);
const statusBody = await statusResponse.json() as {
status: string;
failureCategory: string | null;
failureDetail: string | null;
};
expect(eventsBody).toContain('AGENT_PROMPT_TOO_LARGE');
expect(eventsBody).toContain('Prompt is too long');
expect(eventsBody).toContain('"retryable":false');
expect(eventsBody).not.toContain('could not authenticate');
expect(statusBody).toMatchObject({
status: 'failed',
failureCategory: 'prompt_too_large',
failureDetail: 'prompt_too_large',
});
},
);
});
it('does not treat prompt-length text in an assistant payload as the terminal cause (#6979)', async () => {
await withFakeAgent(
'claude',
`
console.log(JSON.stringify({
type: 'assistant',
parent_tool_use_id: null,
message: {
id: 'msg-prompt-text',
content: [{ type: 'text', text: 'The upstream phrase was: Prompt is too long.' }],
stop_reason: 'end_turn',
},
}));
console.log(JSON.stringify({
type: 'result',
subtype: 'error_during_execution',
is_error: true,
result: 'A different terminal failure',
stop_reason: null,
}));
process.exit(1);
`,
async () => {
const createResponse = await fetch(`${baseUrl}/api/runs`, {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify({
agentId: 'claude',
message: 'hello',
}),
});
expect(createResponse.status).toBe(202);
const { runId } = await createResponse.json() as { runId: string };
const eventsController = new AbortController();
const eventsResponse = await fetch(`${baseUrl}/api/runs/${runId}/events`, {
signal: eventsController.signal,
});
const eventsBody = await readSseUntil(eventsResponse, 'event: error');
eventsController.abort();
await waitForRunStatus(baseUrl, runId);
expect(eventsBody).toContain('AGENT_EXECUTION_FAILED');
expect(eventsBody).toContain('A different terminal failure');
expect(eventsBody).not.toContain('AGENT_PROMPT_TOO_LARGE');
},
);
});
it('caps oversized inactivity overrides so Node does not fire the timer immediately', async () => {
const previous = process.env.OD_CHAT_RUN_INACTIVITY_TIMEOUT_MS;
process.env.OD_CHAT_RUN_INACTIVITY_TIMEOUT_MS = '10000000000';
try {
await withFakeAgent(
'opencode',
`
setTimeout(() => {
console.log(JSON.stringify({ type: 'text', part: { text: 'done' } }));
process.exit(0);
}, 50);
`,
async () => {
const createResponse = await fetch(`${baseUrl}/api/runs`, {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify({
agentId: 'opencode',
message: 'hello',
}),
});
expect(createResponse.status).toBe(202);
const { runId } = await createResponse.json() as { runId: string };
const statusBody = await waitForRunStatus(baseUrl, runId);
expect(statusBody.status).toBe('succeeded');
},
);
} finally {
if (previous == null) {
delete process.env.OD_CHAT_RUN_INACTIVITY_TIMEOUT_MS;
} else {
process.env.OD_CHAT_RUN_INACTIVITY_TIMEOUT_MS = previous;
}
}
});
it('marks stalled runs failed even when the child ignores SIGTERM', async () => {
const previous = process.env.OD_CHAT_RUN_INACTIVITY_TIMEOUT_MS;
process.env.OD_CHAT_RUN_INACTIVITY_TIMEOUT_MS = '500';
try {
await withFakeAgent(
'opencode',
`
console.log(JSON.stringify({ type: 'step_start' }));
process.on('SIGTERM', () => {});
setInterval(() => {}, 1000);
`,
async () => {
const createResponse = await fetch(`${baseUrl}/api/runs`, {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify({
agentId: 'opencode',
message: 'hello',
}),
});
expect(createResponse.status).toBe(202);
const { runId } = await createResponse.json() as { runId: string };
const eventsController = new AbortController();
const eventsResponse = await fetch(`${baseUrl}/api/runs/${runId}/events`, {
signal: eventsController.signal,
});
const eventsBody = await readSseUntil(eventsResponse, 'event: error');
eventsController.abort();
const statusBody = await waitForRunStatus(baseUrl, runId);
expect(eventsBody).toContain('Agent stalled without emitting any new output');
expect(statusBody.status).toBe('failed');
},
);
} finally {
if (previous == null) {
delete process.env.OD_CHAT_RUN_INACTIVITY_TIMEOUT_MS;
} else {
process.env.OD_CHAT_RUN_INACTIVITY_TIMEOUT_MS = previous;
}
}
});
it('marks submitted discovery form answers as the active turn before the transcript', async () => {
const captureDir = mkdtempSync(join(tmpdir(), 'od-form-answer-prompt-'));
tempDirs.push(captureDir);
const capturePath = join(captureDir, 'prompt.txt');
const previousCapturePath = process.env.OD_CAPTURE_PROMPT_PATH;
process.env.OD_CAPTURE_PROMPT_PATH = capturePath;
try {
await withFakeAgent(
'opencode',
`
const fs = require('node:fs');
let input = '';
process.stdin.setEncoding('utf8');
process.stdin.on('data', (chunk) => { input += chunk; });
process.stdin.on('end', () => {
fs.writeFileSync(process.env.OD_CAPTURE_PROMPT_PATH, input, 'utf8');
console.log(JSON.stringify({ type: 'text', part: { text: 'building now' } }));
});
`,
async () => {
const formAnswers = [
'[form answers — discovery]',
'- output: Dashboard / tool UI',
'- brand: Pick a direction for me [value: pick_direction]',
].join('\n');
const transcript = [
'## user',
'Design a metrics dashboard.',
'',
'## assistant',
'<question-form id="discovery" title="Quick brief — 30 seconds"></question-form>',
'',
'## user',
formAnswers,
].join('\n');
const createResponse = await fetch(`${baseUrl}/api/runs`, {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify({
agentId: 'opencode',
message: transcript,
currentPrompt: formAnswers,
}),
});
expect(createResponse.status).toBe(202);
const { runId } = await createResponse.json() as { runId: string };
const statusBody = await waitForRunStatus(baseUrl, runId);
expect(statusBody.status).toBe('succeeded');
expect(existsSync(capturePath)).toBe(true);
const prompt = readFileSync(capturePath, 'utf8');
const transitionIdx = prompt.indexOf('## Latest user turn - form answers submitted');
const transcriptIdx = prompt.indexOf('## Full conversation transcript');
expect(transitionIdx).toBeGreaterThan(-1);
expect(transcriptIdx).toBeGreaterThan(transitionIdx);
expect(prompt).toContain(
'The user has answered the discovery form. Do not re-emit the answered form or repeat fields it already answered.',
);
expect(prompt).toContain(
'Apply the submitted answers and continue with RULE 2 / RULE 3 or the matching active workflow.',
);
expect(prompt).toContain(
'Only if a new, materially blocking requirement remains unresolved',
);
expect(prompt).toContain(formAnswers);
},
);
} finally {
if (previousCapturePath == null) {
delete process.env.OD_CAPTURE_PROMPT_PATH;
} else {
process.env.OD_CAPTURE_PROMPT_PATH = previousCapturePath;
}
}
});
it('latches intent signals on the conversation so a signal-free later turn keeps the deck framework', async () => {
// Red spec for specs/current/intent-signal-cache-hotfix.md §3 case 6 (R2).
// History is trimmed on agent switch (scopeHistoryToAgent) and
// non-transcript clients never resend prior turns, so a deck signal that
// fired on turn 1 must persist on the conversation row — recomputing it
// from the scanned text alone lets it flip OFF again and re-sends the
// ~17K stable block in both directions.
const MAYBE_DECK_HEADING = '## If this brief is a slide deck / keynote / presentation';
if (!process.env.OD_DATA_DIR) {
throw new Error('OD_DATA_DIR is required for intent-signal latch tests');
}
const projectId = `proj-${randomUUID()}`;
const createProjectResponse = await fetch(`${baseUrl}/api/projects`, {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify({ id: projectId, name: 'Intent signal latch fixture' }),
});
expect(createProjectResponse.ok).toBe(true);
const createConversationResponse = await fetch(
`${baseUrl}/api/projects/${projectId}/conversations`,
{
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify({}),
},
);
expect(createConversationResponse.ok).toBe(true);
const { conversation } = await createConversationResponse.json() as {
conversation: { id: string };
};
const conversationId = conversation.id;
const captureDir = mkdtempSync(join(tmpdir(), 'od-intent-latch-'));
tempDirs.push(captureDir);
const previousCapturePath = process.env.OD_CAPTURE_PROMPT_PATH;
try {
await withFakeAgent(
'opencode',
`
const fs = require('node:fs');
let input = '';
process.stdin.setEncoding('utf8');
process.stdin.on('data', (chunk) => { input += chunk; });
process.stdin.on('end', () => {
fs.writeFileSync(process.env.OD_CAPTURE_PROMPT_PATH, input, 'utf8');
console.log(JSON.stringify({ type: 'text', part: { text: 'ok' } }));
console.log(JSON.stringify({ type: 'step_finish', part: { tokens: { input: 1, output: 1 } } }));
});
`,
async () => {
const runTurn = async (
turn: { message: string; currentPrompt: string },
capturePath: string,
): Promise<string> => {
process.env.OD_CAPTURE_PROMPT_PATH = capturePath;
const response = await fetch(`${baseUrl}/api/runs`, {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify({
agentId: 'opencode',
projectId,
conversationId,
message: turn.message,
currentPrompt: turn.currentPrompt,
}),
});
expect(response.status).toBe(202);
const { runId } = await response.json() as { runId: string };
const statusBody = await waitForRunStatus(baseUrl, runId);
expect(statusBody.status).toBe('succeeded');
return readFileSync(capturePath, 'utf8');
};
// Turn 1 mentions a deck in the user's own words → framework present.
const deckBrief = '帮我做一份路演材料,先出内容大纲';
const turn1Prompt = await runTurn(
{ message: `## user\n${deckBrief}`, currentPrompt: deckBrief },
join(captureDir, 'turn1.txt'),
);
expect(turn1Prompt).toContain(MAYBE_DECK_HEADING);
// Turn 2 carries no deck vocabulary and a trimmed transcript
// (agent-switch trim / non-transcript client): the latched
// conversation signal must keep the framework present.
const followUp = '把主色调调亮一点';
const turn2Prompt = await runTurn(
{ message: `## user\n${followUp}`, currentPrompt: followUp },
join(captureDir, 'turn2.txt'),
);
expect(turn2Prompt).toContain(MAYBE_DECK_HEADING);
// The latch is persisted on the conversation row.
const dbFile = resolve(process.env.OD_DATA_DIR as string, 'app.sqlite');
const sqlite = new Database(dbFile, { readonly: true });
try {
const row = sqlite
.prepare(`SELECT intent_signals_json AS intentSignalsJson FROM conversations WHERE id = ?`)
.get(conversationId) as { intentSignalsJson: string | null } | undefined;
expect(row?.intentSignalsJson).toBeTruthy();
expect(JSON.parse(row?.intentSignalsJson ?? '{}')).toMatchObject({ deck: true });
} finally {
sqlite.close();
}
},
);
} finally {
if (previousCapturePath == null) {
delete process.env.OD_CAPTURE_PROMPT_PATH;
} else {
process.env.OD_CAPTURE_PROMPT_PATH = previousCapturePath;
}
}
});
it('uses a project design system in sandboxed chat runs without an explicit run designSystemId', async () => {
const projectId = `project-ds-${randomUUID()}`;
const projectResponse = await fetch(`${baseUrl}/api/projects`, {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify({
id: projectId,
name: 'Project DS fixture',
designSystemId: 'default',
skipDiscoveryBrief: true,
}),
});
expect(projectResponse.ok).toBe(true);
const conversationId = `conv-${randomUUID()}`;
await withFakeAgent(
'opencode',
`
let prompt = '';
process.stdin.setEncoding('utf8');
process.stdin.on('data', (chunk) => {
prompt += chunk;
});
process.stdin.on('end', () => {
const checks = [
prompt.includes('## Active design system') ? 'has-active-design-system' : 'missing-active-design-system',
prompt.includes('Treat the following DESIGN.md as authoritative') ? 'has-design-system-contract' : 'missing-design-system-contract',
];
console.log(JSON.stringify({ type: 'step_start' }));
console.log(JSON.stringify({ type: 'text', part: { text: checks.join('\\n') } }));
console.log(JSON.stringify({ type: 'step_finish', part: { tokens: { input: 1, output: 1 } } }));
process.exit(0);
});
`,
async () => {
const response = await fetch(`${baseUrl}/api/chat`, {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify({
agentId: 'opencode',
projectId,
conversationId,
message: 'draft a branded artifact',
}),
});
const body = await response.text();
expect(response.ok).toBe(true);
expect(body).toContain('has-active-design-system');
expect(body).toContain('has-design-system-contract');
expect(body).not.toContain('missing-active-design-system');
expect(body).not.toContain('missing-design-system-contract');
const runsResponse = await fetch(
`${baseUrl}/api/runs?conversationId=${encodeURIComponent(conversationId)}`,
);
const runsBody = await runsResponse.json() as {
runs: Array<{
designSystemId: string | null;
designSystemRequestedId: string | null;
designSystemSelectionSource: string | null;
designSystemDigest: string | null;
promptCache?: { hit: boolean; missReason: string | null };
}>;
};
expect(runsBody.runs).toHaveLength(1);
expect(runsBody.runs[0]).toMatchObject({
designSystemId: 'default',
designSystemRequestedId: 'default',
designSystemSelectionSource: 'project',
promptCache: { hit: false, missReason: 'new-session' },
});
expect(runsBody.runs[0]?.designSystemDigest).toMatch(/^[a-f0-9]{64}$/);
},
);
});
it('does not compose another member Personal design system from a persisted project id', async () => {
if (!process.env.OD_DATA_DIR) {
throw new Error('OD_DATA_DIR is required for Workspace design-system prompt tests');
}
const workspaceFixture =
await createPersonalWorkspaceBoundProjectFixture('Foreign Personal DS prompt fixture');
const workspaceId = workspaceFixture.headers['x-od-workspace-id'];
const foreignMemberId = `foreign-member-${randomUUID()}`;
const dirId = `foreign-prompt-${randomUUID()}`;
const designSystemId = `user:${dirId}`;
const secretMarker = `FOREIGN_PERSONAL_DS_${randomUUID()}`;
const designSystemDir = resolve(process.env.OD_DATA_DIR, 'design-systems', dirId);
await fsp.mkdir(designSystemDir, { recursive: true });
await fsp.writeFile(
resolve(designSystemDir, 'DESIGN.md'),
`# Foreign Personal design system\n\n${secretMarker}\n`,
'utf8',
);
await fsp.writeFile(
resolve(designSystemDir, 'metadata.json'),
`${JSON.stringify({
title: 'Foreign Personal design system',
status: 'published',
workspaceId,
}, null, 2)}\n`,
'utf8',
);
const sqlite = new Database(resolve(process.env.OD_DATA_DIR, 'app.sqlite'));
try {
ensureWorkspaceResource(
sqlite as never,
'design_system',
workspaceId,
designSystemId,
{
visibility: 'personal',
resourceState: 'active',
createdByWorkspaceMemberId: foreignMemberId,
updatedByWorkspaceMemberId: foreignMemberId,
},
);
sqlite.prepare('UPDATE projects SET design_system_id = ? WHERE id = ?')
.run(designSystemId, workspaceFixture.projectId);
} finally {
sqlite.close();
}
try {
await withFakeAgent(
'opencode',
`
let prompt = '';
process.stdin.setEncoding('utf8');
process.stdin.on('data', (chunk) => { prompt += chunk; });
process.stdin.on('end', () => {
const result = prompt.includes(${JSON.stringify(secretMarker)})
? 'foreign-personal-design-system-leaked'
: 'foreign-personal-design-system-blocked';
console.log(JSON.stringify({ type: 'step_start' }));
console.log(JSON.stringify({ type: 'text', part: { text: result } }));
console.log(JSON.stringify({ type: 'step_finish', part: { tokens: { input: 1, output: 1 } } }));
process.exit(0);
});
`,
async () => {
const response = await fetch(`${baseUrl}/api/chat`, {
method: 'POST',
headers: {
'Content-Type': 'application/json',
...workspaceFixture.headers,
},
body: JSON.stringify({
agentId: 'opencode',
projectId: workspaceFixture.projectId,
message: 'draft without reading another member private brand',
}),
});
const body = await response.text();
expect(response.ok).toBe(true);
expect(body).toContain('foreign-personal-design-system-blocked');
expect(body).not.toContain('foreign-personal-design-system-leaked');
},
);
} finally {
await fsp.rm(designSystemDir, { recursive: true, force: true });
}
});
it('composes the project creator Personal design system', async () => {
if (!process.env.OD_DATA_DIR) {
throw new Error('OD_DATA_DIR is required for Workspace design-system prompt tests');
}
const workspaceFixture =
await createPersonalWorkspaceBoundProjectFixture('Own Personal DS prompt fixture');
const workspaceId = workspaceFixture.headers['x-od-workspace-id'];
const workspaceMemberId = workspaceFixture.headers['x-od-workspace-member-id'];
const dirId = `own-personal-prompt-${randomUUID()}`;
const designSystemId = `user:${dirId}`;
const personalMarker = `OWN_PERSONAL_DS_${randomUUID()}`;
const designSystemDir = resolve(process.env.OD_DATA_DIR, 'design-systems', dirId);
await fsp.mkdir(designSystemDir, { recursive: true });
await fsp.writeFile(
resolve(designSystemDir, 'DESIGN.md'),
`# Own Personal design system\n\n${personalMarker}\n`,
'utf8',
);
await fsp.writeFile(
resolve(designSystemDir, 'metadata.json'),
`${JSON.stringify({
title: 'Own Personal design system',
status: 'published',
workspaceId,
}, null, 2)}\n`,
'utf8',
);
const sqlite = new Database(resolve(process.env.OD_DATA_DIR, 'app.sqlite'));
try {
ensureWorkspaceResource(
sqlite as never,
'design_system',
workspaceId,
designSystemId,
{
visibility: 'personal',
resourceState: 'active',
createdByWorkspaceMemberId: workspaceMemberId,
updatedByWorkspaceMemberId: workspaceMemberId,
},
);
sqlite.prepare('UPDATE projects SET design_system_id = ? WHERE id = ?')
.run(designSystemId, workspaceFixture.projectId);
} finally {
sqlite.close();
}
try {
await withFakeAgent(
'opencode',
`
let prompt = '';
process.stdin.setEncoding('utf8');
process.stdin.on('data', (chunk) => { prompt += chunk; });
process.stdin.on('end', () => {
const result = prompt.includes(${JSON.stringify(personalMarker)})
? 'own-personal-design-system-visible'
: 'own-personal-design-system-missing';
console.log(JSON.stringify({ type: 'step_start' }));
console.log(JSON.stringify({ type: 'text', part: { text: result } }));
console.log(JSON.stringify({ type: 'step_finish', part: { tokens: { input: 1, output: 1 } } }));
process.exit(0);
});
`,
async () => {
const response = await fetch(`${baseUrl}/api/chat`, {
method: 'POST',
headers: {
'Content-Type': 'application/json',
...workspaceFixture.headers,
},
body: JSON.stringify({
agentId: 'opencode',
projectId: workspaceFixture.projectId,
message: 'draft with my Personal brand',
}),
});
const body = await response.text();
expect(response.ok).toBe(true);
expect(body).toContain('own-personal-design-system-visible');
expect(body).not.toContain('own-personal-design-system-missing');
},
);
} finally {
await fsp.rm(designSystemDir, { recursive: true, force: true });
}
});
it('composes a Team design system without touching same-slug Personal or foreign projects', async () => {
if (!process.env.OD_DATA_DIR) {
throw new Error('OD_DATA_DIR is required for Workspace design-system prompt tests');
}
const projectId = `proj-${randomUUID()}`;
const workspaceId = `team-ws-${randomUUID()}`;
const workspaceMemberId = `team-member-${randomUUID()}`;
const personalBackingProjectId = `personal-ds-project-${randomUUID()}`;
const foreignProjectId = `foreign-project-${randomUUID()}`;
for (const [id, name] of [
[projectId, 'Team DS prompt fixture'],
[personalBackingProjectId, 'Personal DS backing project'],
[foreignProjectId, 'Foreign project'],
]) {
const createProjectResponse = await fetch(`${baseUrl}/api/projects`, {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify({ id, name }),
});
expect(createProjectResponse.ok).toBe(true);
}
const dirId = `team-prompt-${randomUUID()}`;
const designSystemId = `user:${dirId}`;
const teamMarker = `TEAM_DS_${randomUUID()}`;
const teamTokensMarker = `TEAM_TOKENS_${randomUUID()}`;
const globalMarker = `GLOBAL_DS_${randomUUID()}`;
const globalTokensMarker = `GLOBAL_TOKENS_${randomUUID()}`;
const foreignProjectMarker = `FOREIGN_PROJECT_DS_${randomUUID()}`;
const designSystemsRoot = resolve(process.env.OD_DATA_DIR, 'design-systems');
const designSystemDir = resolve(
teamResourceWorkspaceRoot(designSystemsRoot, workspaceId),
dirId,
);
const globalDesignSystemDir = resolve(designSystemsRoot, dirId);
await fsp.mkdir(designSystemDir, { recursive: true });
await fsp.mkdir(globalDesignSystemDir, { recursive: true });
await fsp.writeFile(
resolve(designSystemDir, 'DESIGN.md'),
`# Team design system\n\n${teamMarker}\n`,
'utf8',
);
await fsp.writeFile(
resolve(designSystemDir, 'tokens.css'),
`:root { --team-marker: ${teamTokensMarker}; }\n`,
'utf8',
);
await fsp.writeFile(
resolve(designSystemDir, 'metadata.json'),
`${JSON.stringify({
title: 'Team design system',
status: 'published',
teamSynced: true,
workspaceId,
projectId: foreignProjectId,
}, null, 2)}\n`,
'utf8',
);
await fsp.writeFile(
resolve(globalDesignSystemDir, 'DESIGN.md'),
`# Global design system\n\n${globalMarker}\n`,
'utf8',
);
await fsp.writeFile(
resolve(globalDesignSystemDir, 'tokens.css'),
`:root { --global-marker: ${globalTokensMarker}; }\n`,
'utf8',
);
await fsp.writeFile(
resolve(globalDesignSystemDir, 'metadata.json'),
`${JSON.stringify({
title: 'Global design system',
status: 'published',
projectId: personalBackingProjectId,
}, null, 2)}\n`,
'utf8',
);
const projectsRoot = resolve(process.env.OD_DATA_DIR, 'projects');
await Promise.all([
fsp.mkdir(resolve(projectsRoot, personalBackingProjectId), { recursive: true }),
fsp.mkdir(resolve(projectsRoot, foreignProjectId), { recursive: true }),
]);
await fsp.writeFile(
resolve(projectsRoot, personalBackingProjectId, 'DESIGN.md'),
'# Personal backing project\n\nMust remain untouched by a Team run.\n',
'utf8',
);
await fsp.writeFile(
resolve(projectsRoot, foreignProjectId, 'DESIGN.md'),
`# Foreign project\n\n${foreignProjectMarker}\n`,
'utf8',
);
const sqlite = new Database(resolve(process.env.OD_DATA_DIR, 'app.sqlite'));
let personalBackingProjectBefore: ReturnType<typeof getProject>;
let projectCountBefore = 0;
try {
ensureWorkspaceProject(sqlite as never, {
projectId,
workspaceId,
visibility: 'team',
createdByWorkspaceMemberId: workspaceMemberId,
});
ensureWorkspaceProject(sqlite as never, {
projectId: personalBackingProjectId,
workspaceId: `personal-ws-${randomUUID()}`,
visibility: 'personal',
createdByWorkspaceMemberId: `personal-member-${randomUUID()}`,
});
ensureWorkspaceProject(sqlite as never, {
projectId: foreignProjectId,
workspaceId: `foreign-ws-${randomUUID()}`,
visibility: 'personal',
createdByWorkspaceMemberId: `foreign-member-${randomUUID()}`,
});
ensureWorkspaceResource(
sqlite as never,
'design_system',
workspaceId,
workspaceTeamDesignSystemBindingResourceId(workspaceId, designSystemId),
{
visibility: 'team',
resourceState: 'active',
createdByWorkspaceMemberId: workspaceMemberId,
updatedByWorkspaceMemberId: workspaceMemberId,
},
);
sqlite.prepare('UPDATE projects SET design_system_id = ? WHERE id = ?')
.run(designSystemId, projectId);
personalBackingProjectBefore = getProject(
sqlite as never,
personalBackingProjectId,
);
projectCountBefore = (
sqlite.prepare('SELECT COUNT(*) AS count FROM projects').get() as { count: number }
).count;
} finally {
sqlite.close();
}
try {
await withFakeAgent(
'opencode',
`
let prompt = '';
process.stdin.setEncoding('utf8');
process.stdin.on('data', (chunk) => { prompt += chunk; });
process.stdin.on('end', () => {
const result = prompt.includes(${JSON.stringify(teamMarker)})
&& prompt.includes(${JSON.stringify(teamTokensMarker)})
&& !prompt.includes(${JSON.stringify(globalMarker)})
&& !prompt.includes(${JSON.stringify(globalTokensMarker)})
&& !prompt.includes(${JSON.stringify(foreignProjectMarker)})
? 'team-design-system-visible'
: 'team-design-system-missing';
console.log(JSON.stringify({ type: 'step_start' }));
console.log(JSON.stringify({ type: 'text', part: { text: result } }));
console.log(JSON.stringify({ type: 'step_finish', part: { tokens: { input: 1, output: 1 } } }));
process.exit(0);
});
`,
async () => {
const response = await fetch(`${baseUrl}/api/chat`, {
method: 'POST',
headers: {
'Content-Type': 'application/json',
'x-od-workspace-id': workspaceId,
'x-od-workspace-member-id': workspaceMemberId,
'x-od-workspace-type': 'team',
'x-od-workspace-role': 'owner',
},
body: JSON.stringify({
agentId: 'opencode',
projectId,
message: 'draft with the Team brand',
}),
});
const body = await response.text();
const verificationDb = new Database(
resolve(process.env.OD_DATA_DIR as string, 'app.sqlite'),
);
try {
expect.soft(
getProject(verificationDb as never, personalBackingProjectId),
).toEqual(personalBackingProjectBefore);
expect.soft(
(verificationDb.prepare('SELECT COUNT(*) AS count FROM projects').get() as {
count: number;
}).count,
).toBe(projectCountBefore);
} finally {
verificationDb.close();
}
expect.soft(response.ok).toBe(true);
expect.soft(body).toContain('team-design-system-visible');
expect.soft(body).not.toContain('team-design-system-missing');
},
);
} finally {
await fsp.rm(designSystemDir, { recursive: true, force: true });
await fsp.rm(globalDesignSystemDir, { recursive: true, force: true });
}
});
it('keeps requested design systems separate from missing injected design systems', async () => {
const missingDesignSystemId = `missing-ds-${randomUUID()}`;
const projectId = `project-missing-ds-${randomUUID()}`;
const projectResponse = await fetch(`${baseUrl}/api/projects`, {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify({
id: projectId,
name: 'Missing project DS fixture',
skipDiscoveryBrief: true,
}),
});
expect(projectResponse.ok).toBe(true);
const conversationId = `conv-${randomUUID()}`;
await withFakeAgent(
'opencode',
`
let prompt = '';
process.stdin.setEncoding('utf8');
process.stdin.on('data', (chunk) => {
prompt += chunk;
});
process.stdin.on('end', () => {
const checks = [
prompt.includes('## Active design system') ? 'has-active-design-system' : 'missing-active-design-system',
prompt.includes('Treat the following DESIGN.md as authoritative') ? 'has-design-system-contract' : 'missing-design-system-contract',
];
console.log(JSON.stringify({ type: 'step_start' }));
console.log(JSON.stringify({ type: 'text', part: { text: checks.join('\\n') } }));
console.log(JSON.stringify({ type: 'step_finish', part: { tokens: { input: 1, output: 1 } } }));
process.exit(0);
});
`,
async () => {
const response = await fetch(`${baseUrl}/api/chat`, {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify({
agentId: 'opencode',
projectId,
conversationId,
designSystemId: missingDesignSystemId,
message: 'draft a branded artifact',
}),
});
const body = await response.text();
expect(response.ok).toBe(true);
expect(body).toContain('missing-design-system-contract');
expect(body).not.toContain('has-design-system-contract');
const runsResponse = await fetch(
`${baseUrl}/api/runs?conversationId=${encodeURIComponent(conversationId)}`,
);
const runsBody = await runsResponse.json() as {
runs: Array<{
designSystemId: string | null;
designSystemRequestedId: string | null;
designSystemSelectionSource: string | null;
designSystemDigest: string | null;
}>;
};
expect(runsBody.runs).toHaveLength(1);
expect(runsBody.runs[0]).toMatchObject({
designSystemId: null,
designSystemRequestedId: missingDesignSystemId,
designSystemSelectionSource: 'none',
designSystemDigest: null,
});
},
);
});
});
describe('daemon run creation during shutdown', () => {
it('rejects new run creation while shutdown cleanup is still in flight', async () => {
const previousGrace = process.env.OD_CHAT_RUN_SHUTDOWN_GRACE_MS;
process.env.OD_CHAT_RUN_SHUTDOWN_GRACE_MS = '100';
const started = await startServer({ port: 0, returnServer: true }) as {
url: string;
server: http.Server;
shutdown: () => Promise<void>;
};
try {
await withFakeAgent(
'opencode',
`
process.on('SIGTERM', () => {});
setInterval(() => {}, 1000);
`,
async () => {
const activeResponse = await fetch(`${started.url}/api/runs`, {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify({ agentId: 'opencode', message: 'hello' }),
});
expect(activeResponse.status).toBe(202);
const { runId } = await activeResponse.json() as { runId: string };
await waitForRunStatus(started.url, runId, (status) => status === 'running');
const shutdownPromise = started.shutdown();
const runResponse = await fetch(`${started.url}/api/runs`, {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify({ agentId: 'opencode', message: 'late run' }),
});
const chatResponse = await fetch(`${started.url}/api/chat`, {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify({ agentId: 'opencode', message: 'late chat' }),
});
expect(runResponse.status).toBe(503);
expect(chatResponse.status).toBe(503);
await shutdownPromise;
},
);
} finally {
if (previousGrace == null) {
delete process.env.OD_CHAT_RUN_SHUTDOWN_GRACE_MS;
} else {
process.env.OD_CHAT_RUN_SHUTDOWN_GRACE_MS = previousGrace;
}
await new Promise<void>((resolve) => started.server.close(() => resolve()));
}
});
});
async function readSseUntil(response: Response, marker: string): Promise<string> {
const reader = response.body!.getReader();
const decoder = new TextDecoder();
let body = '';
for (let attempt = 0; attempt < 20; attempt += 1) {
const { done, value } = await reader.read();
if (done) return body;
body += decoder.decode(value, { stream: true });
if (body.includes(marker)) return body;
}
return body;
}
async function waitForRunStatus(
baseUrl: string,
runId: string,
done: (status: string) => boolean = (status) => status !== 'queued' && status !== 'running',
): Promise<{ status: string }> {
let lastStatus = 'unknown';
for (let attempt = 0; attempt < 500; attempt += 1) {
const statusResponse = await fetch(`${baseUrl}/api/runs/${runId}`);
const statusBody = await statusResponse.json() as { status: string };
lastStatus = statusBody.status;
if (done(statusBody.status)) return statusBody;
await new Promise((resolve) => setTimeout(resolve, 25));
}
throw new Error(`run did not reach expected status; last status: ${lastStatus}`);
}
describe('chat prompt helpers', () => {
it('appends a final prompt override after the client system prompt and removes earlier duplicates', () => {
const override = '## Final runtime policy\nUse the shared media dispatcher.';
const clientMediaContract =
'## Media generation contract\nclient contract wins unless a later override says otherwise';
const prompt = composeLiveInstructionPrompt({
daemonSystemPrompt: `daemon prompt\n${override}`,
runtimeToolPrompt: 'runtime tools',
clientSystemPrompt: clientMediaContract,
finalPromptOverride: override,
});
const clientIdx = prompt.indexOf(clientMediaContract);
const overrideIdx = prompt.indexOf('## Final runtime policy');
expect(clientIdx).toBeGreaterThan(-1);
expect(overrideIdx).toBeGreaterThan(clientIdx);
expect(prompt.match(/## Final runtime policy/g)).toHaveLength(1);
});
it('defaults enabled research without an explicit query to the current message', () => {
const prompt = resolveResearchCommandContract(
{ enabled: true },
'EV market 2025 trends',
);
expect(prompt).toContain('Canonical query for this run:');
expect(prompt).toContain('EV market 2025 trends');
expect(prompt).toContain('the first tool action must be the research command');
const explicit = resolveResearchCommandContract(
{ enabled: true, query: 'explicit canonical query' },
'legacy full transcript must not replace it',
);
expect(explicit).toContain('explicit canonical query');
expect(explicit).not.toContain('legacy full transcript must not replace it');
});
it('resolves design-system selection precedence for run prompt composition', () => {
expect(resolveEffectiveDesignSystemSelection({
requestDesignSystemId: 'request-ds',
pluginDesignSystemId: 'plugin-ds',
projectDesignSystemId: 'project-ds',
appDefaultDesignSystemId: 'default-ds',
})).toEqual({ id: 'request-ds', source: 'request' });
expect(resolveEffectiveDesignSystemSelection({
pluginDesignSystemId: 'plugin-ds',
projectDesignSystemId: 'project-ds',
appDefaultDesignSystemId: 'default-ds',
})).toEqual({ id: 'plugin-ds', source: 'plugin' });
expect(resolveEffectiveDesignSystemSelection({
projectDesignSystemId: 'project-ds',
appDefaultDesignSystemId: 'default-ds',
})).toEqual({ id: 'project-ds', source: 'project' });
expect(resolveEffectiveDesignSystemSelection({
requestDesignSystemId: null,
projectDesignSystemId: 'project-ds',
disabledDesignSystemIds: ['project-ds'],
allowAppDefault: false,
})).toEqual({ id: null, source: 'none' });
expect(resolveEffectiveDesignSystemSelection({
appDefaultDesignSystemId: 'default-ds',
})).toEqual({ id: 'default-ds', source: 'app-default' });
expect(resolveEffectiveDesignSystemSelection({
appDefaultDesignSystemId: 'default-ds',
allowAppDefault: false,
})).toEqual({ id: null, source: 'none' });
});
it('extracts the primary design-system id from a plugin snapshot', () => {
expect(designSystemIdFromPluginSnapshot({
resolvedContext: {
items: [
{ kind: 'skill', id: 'landing' },
{ kind: 'design-system', id: 'secondary' },
{ kind: 'design-system', id: 'primary', primary: true },
],
},
})).toBe('primary');
expect(designSystemIdFromPluginSnapshot({
resolvedContext: {
items: [
{ kind: 'design-system', id: 'fallback' },
],
},
})).toBe('fallback');
expect(designSystemIdFromPluginSnapshot({ resolvedContext: { items: [] } })).toBeNull();
});
it('describes stable prompt cache hits and miss reasons', () => {
expect(describeStablePromptCache({
isResuming: false,
storedStablePromptHash: null,
currentStableHash: 'hash-a',
})).toEqual({
stablePromptHash: 'hash-a',
hit: false,
missReason: 'new-session',
changedSections: null,
});
expect(describeStablePromptCache({
isResuming: true,
storedStablePromptHash: 'hash-a',
currentStableHash: 'hash-a',
})).toEqual({
stablePromptHash: 'hash-a',
hit: true,
missReason: null,
changedSections: null,
});
expect(describeStablePromptCache({
isResuming: true,
storedStablePromptHash: 'hash-a',
currentStableHash: 'hash-b',
})).toEqual({
stablePromptHash: 'hash-b',
hit: false,
missReason: 'stable-prompt-changed',
// No section map supplied: report no attribution rather than invent one.
changedSections: null,
});
});
it('names the drifted sections when a section map is supplied', () => {
expect(describeStablePromptCache({
isResuming: true,
storedStablePromptHash: 'hash-a',
currentStableHash: 'hash-b',
storedStableSections: { memory: 'm1', skill: 's1' },
currentStableSections: { memory: 'm2', skill: 's1' },
})).toEqual({
stablePromptHash: 'hash-b',
hit: false,
missReason: 'stable-prompt-changed',
changedSections: ['memory'],
});
});
it('reports unattributed drift when the prefix moved but no tracked section did', () => {
// The hash is the source of truth, so this is a real miss; an empty list
// would read as a drift with no cause instead of the coverage gap it is.
expect(describeStablePromptCache({
isResuming: true,
storedStablePromptHash: 'hash-a',
currentStableHash: 'hash-b',
storedStableSections: { memory: 'm1' },
currentStableSections: { memory: 'm1' },
})).toMatchObject({
missReason: 'stable-prompt-changed',
changedSections: ['unattributed'],
});
});
it('does not attribute sections for a missing stored hash', () => {
// A legacy/reseeded row has no baseline to diff, so every section would
// read as "changed" and drown the signal real drift carries.
expect(describeStablePromptCache({
isResuming: true,
storedStablePromptHash: null,
currentStableHash: 'hash-b',
storedStableSections: null,
currentStableSections: { memory: 'm1' },
})).toEqual({
stablePromptHash: 'hash-b',
hit: false,
missReason: 'missing-stored-hash',
changedSections: null,
});
});
it('does not grant media-specific extra directories to Codex', () => {
const dirs = resolveChatExtraAllowedDirs({
agentId: ' CoDeX ',
skillsDir: '/repo/skills',
designSystemsDir: '/repo/design-systems',
linkedDirs: ['/linked/reference'],
existsSync: () => true,
});
expect(dirs).toEqual([]);
});
it('keeps resource and linked dirs for non-Codex agents', () => {
const existingDirs = new Set([
'/repo/skills',
'/repo/design-systems',
'/linked/reference',
]);
const dirs = resolveChatExtraAllowedDirs({
agentId: 'claude',
skillsDir: '/repo/skills',
designSystemsDir: '/repo/design-systems',
linkedDirs: ['/linked/reference'],
existsSync: (dir: string) => existingDirs.has(dir),
});
expect(dirs).toEqual([
'/repo/skills',
'/repo/design-systems',
'/linked/reference',
]);
});
});