import { test } from 'node:test';
import assert from 'node:assert/strict';
Object.defineProperty(globalThis, 'location', {
value: new URL('http://localhost/?token=test-token'),
configurable: true,
});
test('openWorkspaceFile scopes the host opener to the owning session', async () => {
const calls: Array<{ url: string; init?: RequestInit }> = [];
const originalFetch = globalThis.fetch;
globalThis.fetch = (async (url: RequestInfo | URL, init?: RequestInit) => {
calls.push({ url: String(url), init });
return new Response('{"success":true}', { status: 200 });
}) as typeof fetch;
try {
const { openWorkspaceFile } = await import('./api.ts');
await openWorkspaceFile('reports/result.md', 'session-1');
assert.equal(calls[0].url, '/fs/open');
assert.deepEqual(JSON.parse(String(calls[0].init?.body)), {
path: 'reports/result.md',
session_id: 'session-1',
});
} finally {
globalThis.fetch = originalFetch;
}
});
test('searchFiles URL-encodes the dir and the raw @ token, returns matches', async () => {
const calls: Array<{ url: string; init?: RequestInit }> = [];
const originalFetch = globalThis.fetch;
globalThis.fetch = (async (url: RequestInfo | URL, init?: RequestInit) => {
calls.push({ url: String(url), init });
return new Response(
JSON.stringify({
path: '/work/erp',
matches: [
{ path: 'src/main/java/ApplyStockController.java', is_dir: false },
{ path: 'src/apply/', is_dir: true },
],
}),
{ status: 200 },
);
}) as typeof fetch;
try {
const { searchFiles } = await import('./api.ts');
const r = await searchFiles('/work/erp', 'src/apply stock');
assert.equal(
calls[0].url,
'/fs/search?path=%2Fwork%2Ferp&q=src%2Fapply%20stock',
);
assert.equal(r.matches.length, 2);
assert.deepEqual(r.matches[1], { path: 'src/apply/', is_dir: true });
} finally {
globalThis.fetch = originalFetch;
}
});
test('postLiveMessage does not send approval_mode because live mode is global', async () => {
const calls: Array<{ url: string; init?: RequestInit }> = [];
const originalFetch = globalThis.fetch;
globalThis.fetch = (async (url: RequestInfo | URL, init?: RequestInit) => {
calls.push({ url: String(url), init });
return new Response(
'{"accepted":true,"disposition":"steered","generation":3,"turn_id":7}',
{ status: 200 },
);
}) as typeof fetch;
try {
const { postLiveMessage } = await import('./api.ts');
const receipt = await postLiveMessage('hello', undefined, undefined, 'session-1', 'input-1');
assert.equal(calls.length, 1);
assert.equal(calls[0].url, '/live/message');
const body = JSON.parse(String(calls[0].init?.body));
assert.deepEqual(body, {
message: 'hello',
session_id: 'session-1',
client_input_id: 'input-1',
});
assert.deepEqual(receipt, { disposition: 'steered', generation: 3, turn_id: 7 });
} finally {
globalThis.fetch = originalFetch;
}
});
test('postLiveMessage accepts the legacy accepted-only receipt without retrying', async () => {
const originalFetch = globalThis.fetch;
globalThis.fetch = (async () => new Response('{"accepted":true}', { status: 200 })) as typeof fetch;
try {
const { postLiveMessage } = await import('./api.ts');
assert.deepEqual(await postLiveMessage('hello'), {
disposition: 'started',
generation: 0,
turn_id: 0,
});
} finally {
globalThis.fetch = originalFetch;
}
});
test('postLiveMessage exposes the provider actually used for a busy-turn steer', async () => {
const originalFetch = globalThis.fetch;
globalThis.fetch = (async () => new Response(JSON.stringify({
accepted: true,
disposition: 'steered',
generation: 4,
turn_id: 9,
provider: 'provider-active',
provider_change_applied: false,
}), { status: 200 })) as typeof fetch;
try {
const { postLiveMessage } = await import('./api.ts');
assert.deepEqual(await postLiveMessage('continue', undefined, 'provider-requested'), {
disposition: 'steered',
generation: 4,
turn_id: 9,
provider: 'provider-active',
providerChangeApplied: false,
});
} finally {
globalThis.fetch = originalFetch;
}
});
test('postLiveProvider scopes the runtime switch to the active session', async () => {
const calls: Array<{ url: string; init?: RequestInit }> = [];
const originalFetch = globalThis.fetch;
globalThis.fetch = (async (url: RequestInfo | URL, init?: RequestInit) => {
calls.push({ url: String(url), init });
return new Response('{"ok":true}', { status: 200 });
}) as typeof fetch;
try {
const { postLiveProvider } = await import('./api.ts');
await postLiveProvider('provider-b', 'session-1');
assert.equal(calls.length, 1);
assert.equal(calls[0].url, '/live/provider');
assert.deepEqual(JSON.parse(String(calls[0].init?.body)), {
provider: 'provider-b',
session_id: 'session-1',
});
} finally {
globalThis.fetch = originalFetch;
}
});
test('live session switch returns a structured active-turn rejection', async () => {
const originalFetch = globalThis.fetch;
globalThis.fetch = (async () => new Response(
JSON.stringify({ ok: false, active_turn: true, error: 'runtime is busy' }),
{ status: 200, headers: { 'Content-Type': 'application/json' } },
)) as typeof fetch;
try {
const { postLiveSwitchSession } = await import('./api.ts');
assert.deepEqual(await postLiveSwitchSession('session-2'), {
ok: false,
activeTurn: true,
error: 'runtime is busy',
});
} finally {
globalThis.fetch = originalFetch;
}
});
test('live mode still rejects protocol-level failures', async () => {
const originalFetch = globalThis.fetch;
globalThis.fetch = (async () => new Response(
JSON.stringify({ ok: false, error: 'runtime is busy' }),
{ status: 200, headers: { 'Content-Type': 'application/json' } },
)) as typeof fetch;
try {
const { postLiveMode } = await import('./api.ts');
await assert.rejects(
() => postLiveMode('plan'),
/rejected the mode switch/,
);
} finally {
globalThis.fetch = originalFetch;
}
});
test('postLiveUserInput rejects an answer the runtime did not accept', async () => {
const originalFetch = globalThis.fetch;
globalThis.fetch = (async () => new Response(
JSON.stringify({ accepted: false }),
{ status: 200, headers: { 'Content-Type': 'application/json' } },
)) as typeof fetch;
try {
const { postLiveUserInput } = await import('./api.ts');
await assert.rejects(
() => postLiveUserInput({
request_id: 42,
declined: false,
selected: ['继续'],
text: null,
}),
/did not accept/i,
);
} finally {
globalThis.fetch = originalFetch;
}
});
test('postLivePolicyInterventionResolution correlates the typed recovery action', async () => {
const calls: Array<{ url: string; init?: RequestInit }> = [];
const originalFetch = globalThis.fetch;
globalThis.fetch = (async (url: RequestInfo | URL, init?: RequestInit) => {
calls.push({ url: String(url), init });
return new Response('{"accepted":true}', { status: 200 });
}) as typeof fetch;
try {
const { postLivePolicyInterventionResolution } = await import('./api.ts');
await postLivePolicyInterventionResolution(42, 'skip_step');
assert.equal(calls[0].url, '/live/policy-intervention');
assert.deepEqual(JSON.parse(String(calls[0].init?.body)), {
intervention_id: 42,
action: 'skip_step',
});
} finally {
globalThis.fetch = originalFetch;
}
});
test('postChatUserInput correlates the answer by session and native request id', async () => {
const calls: Array<{ url: string; init?: RequestInit }> = [];
const originalFetch = globalThis.fetch;
globalThis.fetch = (async (url: RequestInfo | URL, init?: RequestInit) => {
calls.push({ url: String(url), init });
return new Response('{"accepted":true}', { status: 200 });
}) as typeof fetch;
try {
const { postChatUserInput } = await import('./api.ts');
await postChatUserInput('session-1', {
request_id: 42,
declined: false,
selected: ['继续'],
text: null,
});
assert.equal(calls[0].url, '/chat/user-input');
assert.deepEqual(JSON.parse(String(calls[0].init?.body)), {
session_id: 'session-1',
request_id: 42,
declined: false,
selected: ['继续'],
text: null,
});
} finally {
globalThis.fetch = originalFetch;
}
});
test('scoped live user input answers stay on the live endpoint even with a session id', async () => {
const calls: string[] = [];
const originalFetch = globalThis.fetch;
globalThis.fetch = (async (url: RequestInfo | URL) => {
calls.push(String(url));
return new Response('{"accepted":true}', { status: 200 });
}) as typeof fetch;
try {
const { postUserInputAnswer } = await import('./api.ts');
await postUserInputAnswer({
type: 'user_input_request',
request_id: 42,
session_id: 'session-live',
response_transport: 'live',
header: 'Confirm',
question: 'Continue?',
mode: 'text',
options: [],
}, {
request_id: 42,
declined: true,
selected: [],
text: null,
});
assert.deepEqual(calls, ['/live/user-input']);
} finally {
globalThis.fetch = originalFetch;
}
});
test('chat user input answers use the session-correlated chat endpoint', async () => {
const calls: string[] = [];
const originalFetch = globalThis.fetch;
globalThis.fetch = (async (url: RequestInfo | URL) => {
calls.push(String(url));
return new Response('{"accepted":true}', { status: 200 });
}) as typeof fetch;
try {
const { postUserInputAnswer } = await import('./api.ts');
await postUserInputAnswer({
type: 'user_input_request',
request_id: 43,
session_id: 'session-chat',
response_transport: 'chat',
header: 'Confirm',
question: 'Continue?',
mode: 'text',
options: [],
}, {
request_id: 43,
declined: false,
selected: [],
text: 'continue',
});
assert.deepEqual(calls, ['/chat/user-input']);
} finally {
globalThis.fetch = originalFetch;
}
});
test('chat user input without a session id fails closed before fetch', async () => {
let fetchCalled = false;
const originalFetch = globalThis.fetch;
globalThis.fetch = (async () => {
fetchCalled = true;
return new Response('{"accepted":true}', { status: 200 });
}) as typeof fetch;
try {
const { postUserInputAnswer } = await import('./api.ts');
await assert.rejects(
() => postUserInputAnswer({
type: 'user_input_request',
request_id: 44,
response_transport: 'chat',
header: 'Confirm',
question: 'Continue?',
mode: 'text',
options: [],
}, {
request_id: 44,
declined: true,
selected: [],
text: null,
}),
/missing session_id/,
);
assert.equal(fetchCalled, false);
} finally {
globalThis.fetch = originalFetch;
}
});
test('a one-question questions payload still uses the batch response protocol', async () => {
const { isUserInputBatch } = await import('./api.ts');
assert.equal(isUserInputBatch({
type: 'user_input_request',
request_id: 42,
header: '',
question: '',
mode: 'single',
options: [],
questions: [{
header: 'Pick',
question: 'Red or blue?',
mode: 'single',
options: [{ label: 'Red' }, { label: 'Blue' }],
}],
}), true);
});
test('collection APIs reject server error payloads instead of returning non-arrays', async () => {
const originalFetch = globalThis.fetch;
globalThis.fetch = (async () => new Response(
JSON.stringify('session metadata is missing'),
{ status: 500, headers: { 'Content-Type': 'application/json' } },
)) as typeof fetch;
try {
const { getModels, getProjects } = await import('./api.ts');
await assert.rejects(() => getModels(), /list models failed: 500/);
await assert.rejects(() => getProjects(), /list projects failed: 500/);
} finally {
globalThis.fetch = originalFetch;
}
});
test('deleteSession surfaces the daemon conflict reason', async () => {
const originalFetch = globalThis.fetch;
globalThis.fetch = (async () => new Response(
JSON.stringify({
success: false,
error: 'This session is active. Switch to or create another session, then try again.',
code: 'SESSION_IN_USE',
retryable: false,
}),
{ status: 409, headers: { 'Content-Type': 'application/json' } },
)) as typeof fetch;
try {
const { deleteSession, DeleteSessionError } = await import('./api.ts');
const error = await deleteSession('0123456789abcdef', 's1').catch((cause) => cause);
assert.ok(error instanceof DeleteSessionError);
assert.equal(error.code, 'SESSION_IN_USE');
assert.match(error.message, /session is active/i);
} finally {
globalThis.fetch = originalFetch;
}
});
test('streamChat rejects a clean EOF without an authoritative terminal', async () => {
const originalFetch = globalThis.fetch;
globalThis.fetch = (async () => new Response(
'data: {"type":"text","content":"partial"}\n\n',
{ status: 200, headers: { 'Content-Type': 'text/event-stream' } },
)) as typeof fetch;
try {
const { streamChat } = await import('./api.ts');
const events: unknown[] = [];
await assert.rejects(
() => streamChat({ message: 'hello' }, (event) => events.push(event)),
/ended before an authoritative terminal/i,
);
assert.deepEqual(events, [{ type: 'text', content: 'partial' }]);
} finally {
globalThis.fetch = originalFetch;
}
});
test('streamChat accepts done, stopped, and error as authoritative terminals', async () => {
const originalFetch = globalThis.fetch;
const { streamChat } = await import('./api.ts');
try {
for (const terminal of [
{ type: 'done', tokens: null, tool_calls: null, session_id: 'session-1' },
{ type: 'stopped' },
{ type: 'error', message: 'provider failed' },
]) {
globalThis.fetch = (async () => new Response(
`data: ${JSON.stringify(terminal)}\n\n`,
{ status: 200, headers: { 'Content-Type': 'text/event-stream' } },
)) as typeof fetch;
await streamChat({ message: 'hello' }, () => {});
}
} finally {
globalThis.fetch = originalFetch;
}
});
test('cancelDetachedChat aborts the local stream and uses the existing stop protocol', async () => {
const calls: Array<{ url: string; init?: RequestInit }> = [];
const originalFetch = globalThis.fetch;
globalThis.fetch = (async (url: RequestInfo | URL, init?: RequestInit) => {
calls.push({ url: String(url), init });
return new Response(null, { status: 200 });
}) as typeof fetch;
try {
const { cancelDetachedChat } = await import('./api.ts');
const controller = new AbortController();
await cancelDetachedChat('request-1', controller);
assert.equal(controller.signal.aborted, true);
assert.equal(calls.length, 1);
assert.equal(calls[0].url, '/chat/stop');
assert.deepEqual(JSON.parse(String(calls[0].init?.body)), {
session_id: 'request-1',
});
} finally {
globalThis.fetch = originalFetch;
}
});
test('getActiveChatSessions reads the authoritative detached chat registry', async () => {
const originalFetch = globalThis.fetch;
globalThis.fetch = (async (url: RequestInfo | URL) => {
assert.equal(String(url), '/chat/active');
return new Response(JSON.stringify(['session-1', 'session-2']), {
status: 200,
headers: { 'Content-Type': 'application/json' },
});
}) as typeof fetch;
try {
const { getActiveChatSessions } = await import('./api.ts');
assert.deepEqual(await getActiveChatSessions(), ['session-1', 'session-2']);
} finally {
globalThis.fetch = originalFetch;
}
});