mirror of
https://github.com/QwenLM/qwen-code.git
synced 2026-11-07 06:19:43 -10:00
Connect Hosted question actions, structured answers, durable continuation, and the WebShell question form. Cover canonical response replay and pending-question recovery across Harness and Java context restarts. Co-authored-by: Qwen-Coder <[email protected]>
2454 lines
77 KiB
TypeScript
2454 lines
77 KiB
TypeScript
import { afterEach, beforeEach, describe, expect, it, vi } from 'vitest';
|
|
import { artifact } from './managed-tool-result.test-fixtures';
|
|
import { createJavaManagedAgentProvider } from './java-managed-agent-provider';
|
|
import { pendingQuestion } from './managed-question.test-fixtures';
|
|
|
|
function jsonResponse(value: unknown): Response {
|
|
return new Response(JSON.stringify(value), {
|
|
status: 200,
|
|
headers: { 'content-type': 'application/json' },
|
|
});
|
|
}
|
|
|
|
describe('createJavaManagedAgentProvider', () => {
|
|
it('submits and reads a compression operation through the product routes', async () => {
|
|
const operation = {
|
|
operationId: 'compression-1',
|
|
sessionId: 's1',
|
|
type: 'context_compression',
|
|
status: 'running',
|
|
expectedCompactionRevision: 0,
|
|
replayed: false,
|
|
};
|
|
const fetchImpl = vi
|
|
.fn<typeof fetch>()
|
|
.mockImplementation(async () => jsonResponse(operation));
|
|
const provider = createJavaManagedAgentProvider({
|
|
baseUrl: 'https://product.example',
|
|
productScope: 'tenant:actor',
|
|
fetch: fetchImpl,
|
|
});
|
|
const signal = new AbortController().signal;
|
|
await expect(
|
|
provider.contextCompression!.submit(
|
|
's1',
|
|
{ expectedCompactionRevision: 0 },
|
|
{
|
|
clientId: 'c',
|
|
idempotencyKey: 'original',
|
|
signal,
|
|
},
|
|
),
|
|
).resolves.toEqual(operation);
|
|
const [url, request] = fetchImpl.mock.calls[0];
|
|
expect(url).toBe(
|
|
'https://product.example/api/agent/web-shell/v1/sessions/compress',
|
|
);
|
|
expect(JSON.parse(String(request?.body))).toEqual({
|
|
sessionId: 's1',
|
|
expectedCompactionRevision: 0,
|
|
idempotencyKey: 'original',
|
|
requestId: expect.any(String),
|
|
});
|
|
await expect(
|
|
provider.contextCompression!.query('s1', 'compression-1', {
|
|
clientId: 'c',
|
|
signal,
|
|
}),
|
|
).resolves.toEqual(operation);
|
|
expect(fetchImpl.mock.calls[1][0]).toBe(
|
|
'https://product.example/api/agent/web-shell/v1/operations/query',
|
|
);
|
|
expect(JSON.parse(String(fetchImpl.mock.calls[1][1]?.body))).toEqual({
|
|
sessionId: 's1',
|
|
operationId: 'compression-1',
|
|
});
|
|
fetchImpl.mockResolvedValueOnce(
|
|
jsonResponse({ ...operation, operationId: 'other' }),
|
|
);
|
|
await expect(
|
|
provider.contextCompression!.query('s1', 'compression-1', {
|
|
clientId: 'c',
|
|
}),
|
|
).rejects.toThrow('identity mismatch');
|
|
fetchImpl.mockResolvedValueOnce(
|
|
jsonResponse({ ...operation, sessionId: 'other' }),
|
|
);
|
|
await expect(
|
|
provider.contextCompression!.submit(
|
|
's1',
|
|
{ expectedCompactionRevision: 0 },
|
|
{
|
|
clientId: 'c',
|
|
idempotencyKey: 'original',
|
|
},
|
|
),
|
|
).rejects.toThrow('identity mismatch');
|
|
});
|
|
|
|
it('projects compression revision and remote operation without allowing a Turn to race it', async () => {
|
|
const fetchImpl = vi.fn<typeof fetch>().mockResolvedValue(
|
|
jsonResponse({
|
|
sessionId: 's1',
|
|
status: 'active',
|
|
title: 'Session',
|
|
createdAt: 1,
|
|
updatedAt: 2,
|
|
workspace: { workspaceId: 'ws', cwdRelative: '.', contextRevision: 1 },
|
|
compactionRevision: 15,
|
|
contextCompressionOperationId: 'op',
|
|
capabilities: { workspaceTurns: true, contextCompression: true },
|
|
}),
|
|
);
|
|
const provider = createJavaManagedAgentProvider({
|
|
baseUrl: 'https://product.example',
|
|
productScope: 'tenant:actor',
|
|
fetch: fetchImpl,
|
|
});
|
|
const session = await provider.getSession('s1', { clientId: 'c' });
|
|
expect(session.compactionRevision).toBe(15);
|
|
expect(session.contextCompressionOperationId).toBe('op');
|
|
expect(session.capabilities.contextCompression).toBe(true);
|
|
expect(session.capabilities.canSend).toBe(false);
|
|
fetchImpl.mockResolvedValue(
|
|
jsonResponse({
|
|
sessionId: 's1',
|
|
status: 'active',
|
|
createdAt: 1,
|
|
updatedAt: 2,
|
|
}),
|
|
);
|
|
expect(
|
|
(await provider.getSession('s1', { clientId: 'c' })).compactionRevision,
|
|
).toBe(0);
|
|
expect(
|
|
createJavaManagedAgentProvider({ baseUrl: 'https://product.example' })
|
|
.contextCompression,
|
|
).toBeUndefined();
|
|
});
|
|
|
|
it.each([undefined, true, false])(
|
|
'keeps a recovery-blocked Turn parked with server cancel permission %s',
|
|
async (cancel) => {
|
|
const provider = createJavaManagedAgentProvider({
|
|
baseUrl: 'https://product.example',
|
|
fetch: vi.fn<typeof fetch>().mockResolvedValue(
|
|
jsonResponse({
|
|
sessionId: 's1',
|
|
title: 'Blocked',
|
|
status: 'active',
|
|
createdAt: 1,
|
|
updatedAt: 2,
|
|
...(cancel === undefined
|
|
? {}
|
|
: {
|
|
recovery: {
|
|
state: 'awaiting_evidence',
|
|
reasonCode: 'recovery_evidence_unavailable',
|
|
turnId: 't1',
|
|
nextTurnAllowed: false,
|
|
actions: {
|
|
recheck: false,
|
|
cancel,
|
|
continueInNewSession: true,
|
|
},
|
|
},
|
|
}),
|
|
activeTurn: {
|
|
turnId: 't1',
|
|
status: 'recovery_blocked',
|
|
submittedAt: 1,
|
|
errorCode: 'hosted_harness_recovery_blocked',
|
|
},
|
|
}),
|
|
),
|
|
});
|
|
const summary = await provider.getSession('s1', { clientId: 'client' });
|
|
expect(summary.phase).toBe('failed');
|
|
expect(summary.capabilities.canSend).toBe(false);
|
|
expect(summary.capabilities.canCancel).toBe(cancel !== false);
|
|
expect(summary.failure?.code).toBe('hosted_harness_recovery_blocked');
|
|
},
|
|
);
|
|
|
|
it.each([true, false, undefined])(
|
|
'uses the advertised rename capability %s independently of a running Turn',
|
|
async (rename) => {
|
|
const provider = createJavaManagedAgentProvider({
|
|
baseUrl: 'https://product.example',
|
|
fetch: vi.fn<typeof fetch>().mockResolvedValue(
|
|
jsonResponse({
|
|
sessionId: 's1',
|
|
title: 'Running',
|
|
status: 'active',
|
|
createdAt: 1,
|
|
updatedAt: 2,
|
|
activeTurn: { turnId: 't1', status: 'running', submittedAt: 1 },
|
|
capabilities: { rename },
|
|
}),
|
|
),
|
|
});
|
|
const summary = await provider.getSession('s1', { clientId: 'client' });
|
|
expect(summary.capabilities.canSend).toBe(false);
|
|
expect(summary.capabilities.canRename === true).toBe(rename === true);
|
|
},
|
|
);
|
|
|
|
it('returns the confirmed rename and rejects a different Session or missing title', async () => {
|
|
const fetchImpl = vi
|
|
.fn<typeof fetch>()
|
|
.mockResolvedValueOnce(
|
|
jsonResponse({ sessionId: 's1', title: 'Stored name', replayed: true }),
|
|
)
|
|
.mockResolvedValueOnce(jsonResponse({ sessionId: 's2', title: 'Other' }))
|
|
.mockResolvedValueOnce(jsonResponse({ sessionId: 's1' }));
|
|
const provider = createJavaManagedAgentProvider({
|
|
baseUrl: 'https://product.example',
|
|
fetch: fetchImpl,
|
|
});
|
|
const command = { clientId: 'c', idempotencyKey: 'original' };
|
|
expect(
|
|
await provider.renameSession!('s1', 'Requested name', command),
|
|
).toEqual({ displayName: 'Stored name' });
|
|
expect(JSON.parse(String(fetchImpl.mock.calls[0][1]?.body))).toEqual({
|
|
sessionId: 's1',
|
|
title: 'Requested name',
|
|
idempotencyKey: 'original',
|
|
});
|
|
await expect(
|
|
provider.renameSession!('s1', 'Requested name', command),
|
|
).rejects.toThrow('rename response is invalid');
|
|
await expect(
|
|
provider.renameSession!('s1', 'Requested name', command),
|
|
).rejects.toThrow('rename response is invalid');
|
|
});
|
|
|
|
it('reads tool details through the authenticated WebShell route and preserves empty output', async () => {
|
|
const details = {
|
|
input: { command: 'true' },
|
|
output: '',
|
|
truncated: false,
|
|
};
|
|
const fetchImpl = vi
|
|
.fn<typeof fetch>()
|
|
.mockResolvedValue(jsonResponse(details));
|
|
const abort = new AbortController();
|
|
const provider = createJavaManagedAgentProvider({
|
|
baseUrl: 'https://product.example',
|
|
productScope: 'tenant:actor',
|
|
getHeaders: () => ({
|
|
'X-Qwen-Tenant-Id': 'tenant',
|
|
'X-Qwen-Actor-Id': 'actor',
|
|
}),
|
|
fetch: fetchImpl,
|
|
});
|
|
expect(
|
|
await provider.toolDetails!.get('session-1', 'item-1', {
|
|
clientId: 'client',
|
|
signal: abort.signal,
|
|
}),
|
|
).toEqual(details);
|
|
expect(fetchImpl).toHaveBeenCalledTimes(1);
|
|
const [url, request] = fetchImpl.mock.calls[0];
|
|
expect(url).toBe(
|
|
'https://product.example/api/agent/web-shell/v1/tool-details/get',
|
|
);
|
|
expect(JSON.parse(String(request?.body))).toEqual({
|
|
sessionId: 'session-1',
|
|
itemId: 'item-1',
|
|
});
|
|
expect(request?.signal).toBe(abort.signal);
|
|
expect(new Headers(request?.headers).get('X-Qwen-Tenant-Id')).toBe(
|
|
'tenant',
|
|
);
|
|
expect(new Headers(request?.headers).get('X-Qwen-Actor-Id')).toBe('actor');
|
|
});
|
|
|
|
it('reads the selected Session Skill catalog with authenticated cancellable requests', async () => {
|
|
const catalog = {
|
|
skills: [{ name: 'review', description: 'Published review.' }],
|
|
};
|
|
const fetchImpl = vi
|
|
.fn<typeof fetch>()
|
|
.mockResolvedValue(jsonResponse(catalog));
|
|
const provider = createJavaManagedAgentProvider({
|
|
baseUrl: 'https://product.example',
|
|
fetch: fetchImpl,
|
|
getHeaders: () => ({ authorization: 'Bearer scoped' }),
|
|
});
|
|
const abort = new AbortController();
|
|
expect(
|
|
await provider.skills!.list('session-a', {
|
|
clientId: 'client',
|
|
signal: abort.signal,
|
|
}),
|
|
).toEqual(catalog);
|
|
const [url, init] = fetchImpl.mock.calls[0];
|
|
expect(url).toBe(
|
|
'https://product.example/api/agent/web-shell/v1/session-skills/query',
|
|
);
|
|
expect(init?.method).toBe('POST');
|
|
expect(init?.signal).toBe(abort.signal);
|
|
expect(init?.credentials).toBe('include');
|
|
expect(new Headers(init?.headers).get('authorization')).toBe(
|
|
'Bearer scoped',
|
|
);
|
|
expect(JSON.parse(String(init?.body))).toEqual({ sessionId: 'session-a' });
|
|
});
|
|
it('reads the selected Session MCP catalog with authenticated cancellable requests', async () => {
|
|
const catalog = {
|
|
object: 'agent.mcp_catalog',
|
|
session_id: 'session-a',
|
|
servers: [],
|
|
};
|
|
const fetchImpl = vi
|
|
.fn<typeof fetch>()
|
|
.mockResolvedValue(jsonResponse(catalog));
|
|
const provider = createJavaManagedAgentProvider({
|
|
baseUrl: 'https://product.example',
|
|
fetch: fetchImpl,
|
|
getHeaders: () => ({ authorization: 'Bearer scoped' }),
|
|
});
|
|
const abort = new AbortController();
|
|
expect(
|
|
await provider.mcp!.list('session-a', {
|
|
clientId: 'client',
|
|
signal: abort.signal,
|
|
}),
|
|
).toEqual(catalog);
|
|
const [url, init] = fetchImpl.mock.calls[0];
|
|
expect(url).toBe(
|
|
'https://product.example/api/agent/web-shell/v1/session-mcp/query',
|
|
);
|
|
expect(init?.method).toBe('POST');
|
|
expect(init?.signal).toBe(abort.signal);
|
|
expect(init?.credentials).toBe('include');
|
|
expect(new Headers(init?.headers).get('authorization')).toBe(
|
|
'Bearer scoped',
|
|
);
|
|
expect(JSON.parse(String(init?.body))).toEqual({ sessionId: 'session-a' });
|
|
});
|
|
|
|
it('preserves Workspace registration keys and validates directory and operation identities', async () => {
|
|
const operation = {
|
|
operationId: 'registration-a',
|
|
type: 'workspace_create',
|
|
status: 'running',
|
|
replayed: false,
|
|
};
|
|
const fetchImpl = vi
|
|
.fn<typeof fetch>()
|
|
.mockImplementation(async () => jsonResponse(operation));
|
|
const provider = createJavaManagedAgentProvider({
|
|
baseUrl: 'https://product.example',
|
|
productScope: 'tenant:actor',
|
|
enableWorkspaceBinding: true,
|
|
fetch: fetchImpl,
|
|
});
|
|
const registration = provider.workspaceBinding!.registration!;
|
|
expect(
|
|
await registration.create(
|
|
{ nodeId: 'mac', path: '/project', displayName: 'Project' },
|
|
{ clientId: 'client', idempotencyKey: 'original' },
|
|
),
|
|
).toEqual(operation);
|
|
expect(JSON.parse(String(fetchImpl.mock.calls[0][1]?.body))).toEqual({
|
|
nodeId: 'mac',
|
|
path: '/project',
|
|
displayName: 'Project',
|
|
idempotencyKey: 'original',
|
|
requestId: expect.any(String),
|
|
});
|
|
expect(
|
|
await registration.query('registration-a', { clientId: 'client' }),
|
|
).toEqual(operation);
|
|
fetchImpl.mockImplementation(async () =>
|
|
jsonResponse({ ...operation, operationId: 'wrong' }),
|
|
);
|
|
await expect(
|
|
registration.query('registration-a', { clientId: 'client' }),
|
|
).rejects.toThrow('identity mismatch');
|
|
fetchImpl.mockImplementation(async () =>
|
|
jsonResponse({ ...operation, type: 'cwd_change' }),
|
|
);
|
|
await expect(
|
|
registration.create(
|
|
{ nodeId: 'mac', path: '/project', displayName: 'Project' },
|
|
{ clientId: 'client', idempotencyKey: 'original' },
|
|
),
|
|
).rejects.toThrow('identity mismatch');
|
|
fetchImpl.mockImplementation(async () =>
|
|
jsonResponse({
|
|
nodeId: 'different',
|
|
canonicalPath: '/project',
|
|
platform: 'darwin',
|
|
}),
|
|
);
|
|
await expect(
|
|
registration.checkDirectory(
|
|
{ nodeId: 'mac', path: '/project' },
|
|
{ clientId: 'client' },
|
|
),
|
|
).rejects.toThrow('identity mismatch');
|
|
expect(
|
|
createJavaManagedAgentProvider({ baseUrl: 'https://product.example' })
|
|
.workspaceBinding,
|
|
).toBeUndefined();
|
|
});
|
|
|
|
it('uses the WebShell approval contract and preserves the exact request id and idempotency key', async () => {
|
|
const selection = {
|
|
approvalMode: 'default',
|
|
approvalRevision: 0,
|
|
availableApprovalModes: ['default', 'auto-edit', 'yolo'],
|
|
supported: true,
|
|
liveSupported: true,
|
|
};
|
|
const receipt = {
|
|
sessionId: 's',
|
|
approvalMode: 'yolo',
|
|
approvalRevision: 1,
|
|
replayed: true,
|
|
};
|
|
const fetchImpl = vi
|
|
.fn<typeof fetch>()
|
|
.mockResolvedValueOnce(
|
|
jsonResponse({ ...selection, privateRoute: 'private' }),
|
|
)
|
|
.mockResolvedValueOnce(
|
|
jsonResponse({ ...receipt, privateRoute: 'private' }),
|
|
);
|
|
const provider = createJavaManagedAgentProvider({
|
|
baseUrl: 'https://product.example',
|
|
productScope: 'tenant:actor',
|
|
fetch: fetchImpl,
|
|
});
|
|
expect(
|
|
await provider.approvalChange!.queryOptions('s', { clientId: 'client' }),
|
|
).toEqual(selection);
|
|
expect(
|
|
await provider.approvalChange!.submit(
|
|
's',
|
|
{ approvalMode: 'yolo', expectedRevision: 0 },
|
|
{
|
|
clientId: 'client',
|
|
idempotencyKey: 'original-key',
|
|
requestId: 'original-request',
|
|
},
|
|
),
|
|
).toEqual(receipt);
|
|
expect(fetchImpl.mock.calls.map(([url]) => String(url))).toEqual([
|
|
'https://product.example/api/agent/web-shell/v1/session-approval/query',
|
|
'https://product.example/api/agent/web-shell/v1/session-approval/change',
|
|
]);
|
|
expect(JSON.parse(String(fetchImpl.mock.calls[1][1]?.body))).toEqual({
|
|
sessionId: 's',
|
|
approvalMode: 'yolo',
|
|
expectedRevision: 0,
|
|
idempotencyKey: 'original-key',
|
|
requestId: 'original-request',
|
|
});
|
|
expect(
|
|
createJavaManagedAgentProvider({ baseUrl: 'https://product.example' })
|
|
.approvalChange,
|
|
).toBeUndefined();
|
|
});
|
|
|
|
it('preserves stored-agent effective and initial approval policy from Session summaries', async () => {
|
|
const agentId = `agent_${'a'.repeat(32)}`;
|
|
const fetchImpl = vi.fn<typeof fetch>().mockResolvedValue(
|
|
jsonResponse({
|
|
sessionId: 's',
|
|
agentId,
|
|
status: 'ACTIVE',
|
|
approvalMode: 'auto-edit',
|
|
initialApprovalMode: 'yolo',
|
|
approvalRevision: 2,
|
|
workspace: { workspaceId: 'workspace', cwdRelative: '.' },
|
|
capabilities: {
|
|
workspaceTurns: true,
|
|
approvalModeChange: true,
|
|
actions: true,
|
|
},
|
|
createdAt: 1,
|
|
updatedAt: 1,
|
|
lastSequence: 0,
|
|
}),
|
|
);
|
|
const provider = createJavaManagedAgentProvider({
|
|
baseUrl: 'https://product.example',
|
|
productScope: 'tenant:actor',
|
|
fetch: fetchImpl,
|
|
});
|
|
expect(
|
|
await provider.getSession('s', { clientId: 'client' }),
|
|
).toMatchObject({
|
|
agentId,
|
|
approvalMode: 'auto-edit',
|
|
initialApprovalMode: 'yolo',
|
|
approvalRevision: 2,
|
|
capabilities: { canSend: true, approvalModeChange: true, actions: true },
|
|
});
|
|
});
|
|
|
|
it.each([
|
|
{
|
|
approvalMode: ['yolo'],
|
|
approvalRevision: 0,
|
|
availableApprovalModes: ['yolo'],
|
|
supported: true,
|
|
},
|
|
{
|
|
approvalMode: 'yolo',
|
|
approvalRevision: -1,
|
|
availableApprovalModes: ['yolo'],
|
|
supported: true,
|
|
},
|
|
{
|
|
approvalMode: 'yolo',
|
|
approvalRevision: 0,
|
|
availableApprovalModes: ['default'],
|
|
supported: true,
|
|
},
|
|
{
|
|
approvalMode: 'yolo',
|
|
approvalRevision: 0,
|
|
availableApprovalModes: ['yolo', 'unknown'],
|
|
supported: true,
|
|
},
|
|
])('refuses invalid approval options (%j)', async (selection) => {
|
|
const provider = createJavaManagedAgentProvider({
|
|
baseUrl: 'https://product.example',
|
|
productScope: 'tenant:actor',
|
|
fetch: vi.fn<typeof fetch>().mockResolvedValue(jsonResponse(selection)),
|
|
});
|
|
await expect(
|
|
provider.approvalChange!.queryOptions('s', { clientId: 'client' }),
|
|
).rejects.toThrow('Invalid Session approval options');
|
|
});
|
|
|
|
it('round-trips Auto and Session rules through the existing approval contract', async () => {
|
|
const permissionRules = {
|
|
allow: ['Read'],
|
|
ask: ['Bash(git push*)'],
|
|
deny: ['Bash(rm *)'],
|
|
};
|
|
const selection = {
|
|
approvalMode: 'auto',
|
|
approvalRevision: 2,
|
|
availableApprovalModes: ['auto', 'default'],
|
|
supported: true,
|
|
permissionRulesSupported: true,
|
|
permissionRules,
|
|
};
|
|
const receipt = {
|
|
sessionId: 's',
|
|
approvalMode: 'auto',
|
|
approvalRevision: 3,
|
|
replayed: false,
|
|
permissionRules,
|
|
};
|
|
const fetchImpl = vi
|
|
.fn<typeof fetch>()
|
|
.mockResolvedValueOnce(jsonResponse(selection))
|
|
.mockResolvedValueOnce(jsonResponse(receipt));
|
|
const provider = createJavaManagedAgentProvider({
|
|
baseUrl: 'https://product.example',
|
|
productScope: 'tenant:actor',
|
|
fetch: fetchImpl,
|
|
});
|
|
expect(
|
|
await provider.approvalChange!.queryOptions('s', { clientId: 'c' }),
|
|
).toEqual(selection);
|
|
expect(
|
|
await provider.approvalChange!.submit(
|
|
's',
|
|
{ approvalMode: 'auto', expectedRevision: 2, permissionRules },
|
|
{ clientId: 'c', requestId: 'request', idempotencyKey: 'key' },
|
|
),
|
|
).toEqual(receipt);
|
|
expect(JSON.parse(String(fetchImpl.mock.calls[1][1]?.body))).toMatchObject({
|
|
permissionRules,
|
|
});
|
|
});
|
|
|
|
it('uses only server model choices and preserves the frozen change key and revision', async () => {
|
|
const fetchImpl = vi
|
|
.fn<typeof fetch>()
|
|
.mockResolvedValueOnce(
|
|
jsonResponse({
|
|
models: [
|
|
{ modelId: 'published', label: 'Published', route: 'private' },
|
|
],
|
|
modelSelection: { revision: 0, provider: 'private' },
|
|
supported: true,
|
|
credentials: 'private',
|
|
}),
|
|
)
|
|
.mockResolvedValueOnce(
|
|
jsonResponse({
|
|
sessionId: 's',
|
|
modelSelection: {
|
|
modelId: 'published',
|
|
revision: 1,
|
|
route: 'private',
|
|
},
|
|
replayed: true,
|
|
}),
|
|
);
|
|
const provider = createJavaManagedAgentProvider({
|
|
baseUrl: 'https://product.example',
|
|
productScope: 'tenant:actor',
|
|
fetch: fetchImpl,
|
|
});
|
|
expect(
|
|
await provider.modelChange!.queryOptions('s', { clientId: 'client' }),
|
|
).toEqual({
|
|
models: [{ modelId: 'published', label: 'Published' }],
|
|
modelSelection: { revision: 0 },
|
|
supported: true,
|
|
});
|
|
expect(
|
|
await provider.modelChange!.submit(
|
|
's',
|
|
{ modelId: 'published', expectedRevision: 0 },
|
|
{ clientId: 'client', idempotencyKey: 'original' },
|
|
),
|
|
).toEqual({
|
|
sessionId: 's',
|
|
modelSelection: { modelId: 'published', revision: 1 },
|
|
replayed: true,
|
|
});
|
|
expect(JSON.parse(String(fetchImpl.mock.calls[1][1]?.body))).toMatchObject({
|
|
sessionId: 's',
|
|
modelId: 'published',
|
|
expectedRevision: 0,
|
|
idempotencyKey: 'original',
|
|
});
|
|
expect(
|
|
createJavaManagedAgentProvider({ baseUrl: 'https://product.example' })
|
|
.modelChange,
|
|
).toBeUndefined();
|
|
});
|
|
|
|
it.each([
|
|
{
|
|
models: [
|
|
{ modelId: 'A', label: 'A' },
|
|
{ modelId: 'A', label: 'Duplicate' },
|
|
],
|
|
modelSelection: { revision: 0 },
|
|
supported: true,
|
|
},
|
|
{
|
|
models: [{ modelId: 'A', label: 5 }],
|
|
modelSelection: { revision: 0 },
|
|
supported: true,
|
|
},
|
|
{
|
|
models: [{ modelId: 'A\nroute', label: 'A' }],
|
|
modelSelection: { revision: 0 },
|
|
supported: true,
|
|
},
|
|
{ models: [], modelSelection: { revision: -1 }, supported: true },
|
|
{ models: [], modelSelection: { revision: 1 }, supported: true },
|
|
{ models: [], modelSelection: { revision: 0 }, supported: 'true' },
|
|
])('rejects malformed server model options (%j)', async (response) => {
|
|
const provider = createJavaManagedAgentProvider({
|
|
baseUrl: 'https://product.example',
|
|
productScope: 'tenant:actor',
|
|
fetch: vi.fn<typeof fetch>().mockResolvedValue(jsonResponse(response)),
|
|
});
|
|
await expect(
|
|
provider.modelChange!.queryOptions('s', { clientId: 'client' }),
|
|
).rejects.toThrow('Invalid Session model options');
|
|
});
|
|
|
|
it.each([
|
|
{
|
|
sessionId: 'other',
|
|
modelSelection: { modelId: 'B', revision: 2 },
|
|
replayed: false,
|
|
},
|
|
{
|
|
sessionId: 's',
|
|
modelSelection: { modelId: 'A', revision: 2 },
|
|
replayed: false,
|
|
},
|
|
{
|
|
sessionId: 's',
|
|
modelSelection: { modelId: 'B', revision: 1 },
|
|
replayed: false,
|
|
},
|
|
{
|
|
sessionId: 's',
|
|
modelSelection: { modelId: 'B', revision: 2.5 },
|
|
replayed: false,
|
|
},
|
|
])(
|
|
'rejects a change response that cannot confirm the original identity (%j)',
|
|
async (response) => {
|
|
const provider = createJavaManagedAgentProvider({
|
|
baseUrl: 'https://product.example',
|
|
productScope: 'tenant:actor',
|
|
fetch: vi.fn<typeof fetch>().mockResolvedValue(jsonResponse(response)),
|
|
});
|
|
await expect(
|
|
provider.modelChange!.submit(
|
|
's',
|
|
{ modelId: 'B', expectedRevision: 1 },
|
|
{ clientId: 'c', idempotencyKey: 'key' },
|
|
),
|
|
).rejects.toThrow('Model change identity mismatch');
|
|
},
|
|
);
|
|
async function readModelContext(value: unknown) {
|
|
const provider = createJavaManagedAgentProvider({
|
|
baseUrl: 'https://product.example',
|
|
fetch: vi.fn<typeof fetch>().mockResolvedValue(
|
|
jsonResponse({
|
|
sessionId: 's',
|
|
agentId: 'agent-a',
|
|
status: 'active',
|
|
createdAt: 1,
|
|
updatedAt: 1,
|
|
lastSequence: 0,
|
|
modelContext: value,
|
|
}),
|
|
),
|
|
});
|
|
return (await provider.getSession('s', { clientId: 'client' }))
|
|
.modelContext;
|
|
}
|
|
|
|
it('publishes only observed model-context fields including zero input tokens', async () => {
|
|
expect(
|
|
await readModelContext({
|
|
modelId: 'actual-pinned-model',
|
|
inputTokens: 0,
|
|
contextWindowSize: 1000,
|
|
observedAt: 12,
|
|
rawResource: 'private',
|
|
budget: { totalTokens: 999999 },
|
|
route: 'private',
|
|
}),
|
|
).toEqual({
|
|
modelId: 'actual-pinned-model',
|
|
inputTokens: 0,
|
|
contextWindowSize: 1000,
|
|
observedAt: 12,
|
|
});
|
|
});
|
|
|
|
it.each([
|
|
undefined,
|
|
null,
|
|
{},
|
|
{ modelId: 42, observedAt: 1 },
|
|
{ modelId: ' ', observedAt: 1 },
|
|
{ modelId: 'model', observedAt: -1 },
|
|
{ modelId: 'model', observedAt: 1.5 },
|
|
{ modelId: 'model', observedAt: Number.MAX_SAFE_INTEGER + 1 },
|
|
])('omits invalid model observations (%j)', async (value) => {
|
|
expect(await readModelContext(value)).toBeUndefined();
|
|
});
|
|
|
|
it.each([
|
|
{
|
|
inputTokens: -1,
|
|
contextWindowSize: 1000,
|
|
expected: { contextWindowSize: 1000 },
|
|
},
|
|
{ inputTokens: 10, contextWindowSize: 0, expected: { inputTokens: 10 } },
|
|
{
|
|
inputTokens: 1.5,
|
|
contextWindowSize: Number.MAX_SAFE_INTEGER + 1,
|
|
expected: {},
|
|
},
|
|
{ inputTokens: '10', contextWindowSize: '1000', expected: {} },
|
|
])(
|
|
'does not invent missing or invalid occupancy (%j)',
|
|
async ({ inputTokens, contextWindowSize, expected }) => {
|
|
expect(
|
|
await readModelContext({
|
|
modelId: 'model',
|
|
observedAt: 12,
|
|
inputTokens,
|
|
contextWindowSize,
|
|
}),
|
|
).toEqual({ modelId: 'model', observedAt: 12, ...expected });
|
|
},
|
|
);
|
|
|
|
it('reads tasks without identity scope but only exposes cancellation with a product scope', async () => {
|
|
const fetchImpl = vi
|
|
.fn<typeof fetch>()
|
|
.mockResolvedValue(jsonResponse({ data: [], hasMore: false }));
|
|
const provider = createJavaManagedAgentProvider({
|
|
baseUrl: 'https://product.example',
|
|
fetch: fetchImpl,
|
|
});
|
|
expect(provider.tasks).toBeDefined();
|
|
expect(provider.tasks!.cancel).toBeUndefined();
|
|
expect(
|
|
createJavaManagedAgentProvider({
|
|
baseUrl: 'https://product.example',
|
|
productScope: ' ',
|
|
}).tasks!.cancel,
|
|
).toBeUndefined();
|
|
const abort = new AbortController();
|
|
await provider.tasks!.list('s1', {
|
|
clientId: 'client',
|
|
cursor: 'opaque',
|
|
limit: 50,
|
|
signal: abort.signal,
|
|
});
|
|
expect(String(fetchImpl.mock.calls[0][0])).toContain('/tasks/query');
|
|
expect(JSON.parse(String(fetchImpl.mock.calls[0][1]?.body))).toEqual({
|
|
sessionId: 's1',
|
|
cursor: 'opaque',
|
|
limit: 50,
|
|
});
|
|
expect(fetchImpl.mock.calls[0][1]?.signal).toBe(abort.signal);
|
|
});
|
|
|
|
it('preserves the task cancel key and validates operation identity and type', async () => {
|
|
const operation = {
|
|
operationId: 'op1',
|
|
sessionId: 's1',
|
|
taskId: 'task1',
|
|
type: 'task_cancel',
|
|
status: 'pending',
|
|
replayed: false,
|
|
};
|
|
const fetchImpl = vi
|
|
.fn<typeof fetch>()
|
|
.mockImplementation(async () => jsonResponse(operation));
|
|
const provider = createJavaManagedAgentProvider({
|
|
baseUrl: 'https://product.example',
|
|
productScope: 'tenant:actor',
|
|
fetch: fetchImpl,
|
|
});
|
|
expect(
|
|
await provider.tasks!.cancel!.submit('s1', 'task1', {
|
|
clientId: 'client',
|
|
idempotencyKey: 'original',
|
|
}),
|
|
).toEqual(operation);
|
|
expect(JSON.parse(String(fetchImpl.mock.calls[0][1]?.body))).toEqual(
|
|
expect.objectContaining({
|
|
sessionId: 's1',
|
|
taskId: 'task1',
|
|
idempotencyKey: 'original',
|
|
}),
|
|
);
|
|
expect(
|
|
await provider.tasks!.cancel!.query('s1', 'task1', 'op1', {
|
|
clientId: 'client',
|
|
}),
|
|
).toEqual(operation);
|
|
fetchImpl.mockImplementation(async () =>
|
|
jsonResponse({ ...operation, taskId: 'different' }),
|
|
);
|
|
await expect(
|
|
provider.tasks!.cancel!.query('s1', 'task1', 'op1', {
|
|
clientId: 'client',
|
|
}),
|
|
).rejects.toThrow('identity mismatch');
|
|
fetchImpl.mockImplementation(async () =>
|
|
jsonResponse({ ...operation, type: 'cancel' }),
|
|
);
|
|
await expect(
|
|
provider.tasks!.cancel!.query('s1', 'task1', 'op1', {
|
|
clientId: 'client',
|
|
}),
|
|
).rejects.toThrow('identity mismatch');
|
|
});
|
|
it('keeps an empty bound session idle and disables execution', async () => {
|
|
const provider = createJavaManagedAgentProvider({
|
|
baseUrl: 'https://product.example',
|
|
productScope: 'tenant-a:actor-a',
|
|
enableWorkspaceBinding: true,
|
|
fetch: vi.fn<typeof fetch>().mockResolvedValue(
|
|
jsonResponse({
|
|
sessionId: 'empty-1',
|
|
agentId: 'agent-model-a',
|
|
status: 'ACTIVE',
|
|
createdAt: 1,
|
|
updatedAt: 1,
|
|
lastSequence: 0,
|
|
workspace: { workspaceId: 'ws-a', cwdRelative: 'services/api' },
|
|
}),
|
|
),
|
|
});
|
|
expect(
|
|
await provider.getSession('empty-1', { clientId: 'client' }),
|
|
).toEqual(
|
|
expect.objectContaining({
|
|
phase: 'created',
|
|
agentId: 'agent-model-a',
|
|
workspace: { workspaceId: 'ws-a', cwdRelative: 'services/api' },
|
|
capabilities: { canSend: false, canCancel: false },
|
|
}),
|
|
);
|
|
});
|
|
|
|
it.each([false, true])(
|
|
'allows the first message in an unbound empty session (opt-in: %s)',
|
|
async (enableWorkspaceBinding) => {
|
|
const provider = createJavaManagedAgentProvider({
|
|
baseUrl: 'https://product.example',
|
|
productScope: 'tenant-a:actor-a',
|
|
enableWorkspaceBinding,
|
|
fetch: vi.fn<typeof fetch>().mockResolvedValue(
|
|
jsonResponse({
|
|
sessionId: 'empty-1',
|
|
status: 'ACTIVE',
|
|
createdAt: 1,
|
|
updatedAt: 1,
|
|
lastSequence: 0,
|
|
}),
|
|
),
|
|
});
|
|
expect(
|
|
await provider.getSession('empty-1', { clientId: 'client' }),
|
|
).toEqual(
|
|
expect.objectContaining({
|
|
phase: 'created',
|
|
capabilities: { canSend: true, canCancel: false },
|
|
}),
|
|
);
|
|
},
|
|
);
|
|
|
|
it.each([true, false, undefined])(
|
|
'uses only the server Workspace creation capability (%s) and freezes the create command',
|
|
async (workspaceCreation) => {
|
|
const workspace = {
|
|
workspaceId: 'created-a',
|
|
displayName: 'A',
|
|
state: 'active',
|
|
canCreateSession: true,
|
|
};
|
|
const fetchImpl = vi.fn<typeof fetch>().mockImplementation(async (url) =>
|
|
jsonResponse(
|
|
String(url).endsWith('/workspaces/query')
|
|
? {
|
|
data: [],
|
|
defaultWorkspace: null,
|
|
hasMore: false,
|
|
capabilities: {
|
|
workspaceBinding: true,
|
|
workspaceContext: false,
|
|
workspaceCreation,
|
|
},
|
|
}
|
|
: workspace,
|
|
),
|
|
);
|
|
const provider = createJavaManagedAgentProvider({
|
|
baseUrl: 'https://product.example',
|
|
productScope: 'tenant-a:actor-a',
|
|
enableWorkspaceBinding: true,
|
|
fetch: fetchImpl,
|
|
});
|
|
expect(
|
|
(await provider.workspaceBinding!.list({ clientId: 'client' }))
|
|
.canCreateWorkspace,
|
|
).toBe(workspaceCreation === true);
|
|
const abort = new AbortController();
|
|
expect(
|
|
await provider.workspaceBinding!.createWorkspace!(
|
|
{ displayName: 'A' },
|
|
{
|
|
clientId: 'client',
|
|
idempotencyKey: 'saved-key',
|
|
signal: abort.signal,
|
|
},
|
|
),
|
|
).toEqual(workspace);
|
|
expect(String(fetchImpl.mock.calls[1][0])).toContain(
|
|
'/workspaces/create',
|
|
);
|
|
expect(JSON.parse(String(fetchImpl.mock.calls[1][1]?.body))).toEqual({
|
|
displayName: 'A',
|
|
idempotencyKey: 'saved-key',
|
|
});
|
|
expect(fetchImpl.mock.calls[1][1]?.signal).toBe(abort.signal);
|
|
},
|
|
);
|
|
|
|
it('creates a bound empty session without requiring turnId', async () => {
|
|
const fetchImpl = vi.fn<typeof fetch>().mockImplementation(async (input) =>
|
|
jsonResponse(
|
|
String(input).includes('/workspaces/query')
|
|
? {
|
|
data: [
|
|
{
|
|
workspaceId: 'ws-a',
|
|
displayName: 'A',
|
|
state: 'active',
|
|
canCreateSession: true,
|
|
},
|
|
],
|
|
defaultWorkspace: null,
|
|
hasMore: false,
|
|
nextCursor: null,
|
|
capabilities: {
|
|
workspaceBinding: true,
|
|
workspaceContext: false,
|
|
},
|
|
}
|
|
: { sessionId: 'empty-1', status: 'accepted', replayed: false },
|
|
),
|
|
);
|
|
const provider = createJavaManagedAgentProvider({
|
|
baseUrl: 'https://product.example',
|
|
productScope: 'tenant-a:actor-a',
|
|
agentId: 'agent-a',
|
|
enableWorkspaceBinding: true,
|
|
fetch: fetchImpl,
|
|
});
|
|
const listed = await provider.workspaceBinding!.list({
|
|
clientId: 'client',
|
|
});
|
|
expect(listed.supported).toBe(true);
|
|
expect(listed.canCreateWorkspace).toBe(false);
|
|
expect(listed.supportsApprovalModeSelection).toBe(false);
|
|
expect(listed.nextCursor).toBeUndefined();
|
|
expect(
|
|
await provider.workspaceBinding!.createEmpty(
|
|
{
|
|
agentId: 'agent-a',
|
|
workspaceId: 'ws-a',
|
|
cwdRelative: './docs',
|
|
},
|
|
{ clientId: 'client', idempotencyKey: 'key-a' },
|
|
),
|
|
).toEqual({ sessionId: 'empty-1' });
|
|
expect(JSON.parse(String(fetchImpl.mock.calls[1][1]?.body))).toEqual(
|
|
expect.objectContaining({
|
|
requestId: expect.stringMatching(/^managed_/),
|
|
agentId: 'agent-a',
|
|
input: [],
|
|
idempotencyKey: 'key-a',
|
|
workspace: { workspaceId: 'ws-a', cwdRelative: './docs' },
|
|
}),
|
|
);
|
|
expect(provider.storageKey).toContain('agent-a');
|
|
expect(() =>
|
|
createJavaManagedAgentProvider({
|
|
baseUrl: 'https://product.example',
|
|
enableWorkspaceBinding: true,
|
|
}),
|
|
).toThrow('productScope');
|
|
});
|
|
|
|
it('preserves model, worktree and extension pins on both creation paths', async () => {
|
|
const fetchImpl = vi
|
|
.fn<typeof fetch>()
|
|
.mockImplementation(async () =>
|
|
jsonResponse({ sessionId: 's', turnId: 't' }),
|
|
);
|
|
const provider = createJavaManagedAgentProvider({
|
|
baseUrl: 'https://product.example',
|
|
productScope: 'tenant:actor',
|
|
enableWorkspaceBinding: true,
|
|
fetch: fetchImpl,
|
|
});
|
|
const request = {
|
|
agentId: 'qwen-code',
|
|
modelId: 'model-b',
|
|
workspaceId: 'ws',
|
|
cwdRelative: '.',
|
|
approvalMode: 'yolo' as const,
|
|
worktree: { baseRef: 'main' },
|
|
extensionPins: [
|
|
{
|
|
extensionId: 'ext_00000000-0000-0000-0000-000000000001',
|
|
packageRevision: 1,
|
|
packageDigest: 'a'.repeat(64),
|
|
},
|
|
],
|
|
};
|
|
const command = { clientId: 'client', idempotencyKey: 'frozen-key' };
|
|
await provider.workspaceBinding!.createEmpty(request, command);
|
|
await provider.workspaceBinding!.createSession!(
|
|
{ ...request, text: 'First' },
|
|
command,
|
|
);
|
|
for (const [, init] of fetchImpl.mock.calls) {
|
|
expect(JSON.parse(String(init?.body))).toEqual(
|
|
expect.objectContaining({
|
|
agentId: 'qwen-code',
|
|
modelId: 'model-b',
|
|
approvalMode: 'yolo',
|
|
worktree: request.worktree,
|
|
extensionPins: request.extensionPins,
|
|
idempotencyKey: 'frozen-key',
|
|
}),
|
|
);
|
|
}
|
|
});
|
|
|
|
it('discovers policy selection, reads options and sends explicit policies for both bound creation paths', async () => {
|
|
const options = {
|
|
approvalMode: 'default',
|
|
availableApprovalModes: ['default', 'auto-edit'],
|
|
};
|
|
const fetchImpl = vi.fn<typeof fetch>().mockImplementation(async (url) =>
|
|
jsonResponse(
|
|
String(url).endsWith('/workspaces/query')
|
|
? {
|
|
data: [],
|
|
defaultWorkspace: null,
|
|
hasMore: false,
|
|
capabilities: {
|
|
workspaceBinding: true,
|
|
workspaceContext: false,
|
|
approvalModeSelection: true,
|
|
},
|
|
}
|
|
: String(url).endsWith('/session-options/query')
|
|
? options
|
|
: { sessionId: 's', turnId: 't' },
|
|
),
|
|
);
|
|
const provider = createJavaManagedAgentProvider({
|
|
baseUrl: 'https://product.example',
|
|
productScope: 'tenant:actor',
|
|
enableWorkspaceBinding: true,
|
|
fetch: fetchImpl,
|
|
});
|
|
expect(
|
|
(await provider.workspaceBinding!.list({ clientId: 'client' }))
|
|
.supportsApprovalModeSelection,
|
|
).toBe(true);
|
|
expect(
|
|
await provider.workspaceBinding!.getSessionOptions!('agent-b', {
|
|
clientId: 'client',
|
|
}),
|
|
).toEqual(options);
|
|
expect(JSON.parse(String(fetchImpl.mock.calls[1][1]?.body))).toEqual({
|
|
agentId: 'agent-b',
|
|
});
|
|
const command = { clientId: 'client', idempotencyKey: 'saved-key' };
|
|
const request = {
|
|
agentId: 'agent-b',
|
|
workspaceId: 'ws',
|
|
cwdRelative: '.',
|
|
approvalMode: 'auto-edit' as const,
|
|
};
|
|
await provider.workspaceBinding!.createEmpty(request, command);
|
|
await provider.workspaceBinding!.createSession!(
|
|
{ ...request, text: 'Write' },
|
|
command,
|
|
);
|
|
for (const index of [2, 3])
|
|
expect(
|
|
JSON.parse(String(fetchImpl.mock.calls[index][1]?.body)).approvalMode,
|
|
).toBe('auto-edit');
|
|
await provider.workspaceBinding!.createEmpty(
|
|
{ agentId: 'agent-b', workspaceId: 'ws', cwdRelative: '.' },
|
|
command,
|
|
);
|
|
expect(
|
|
JSON.parse(String(fetchImpl.mock.calls[4][1]?.body)),
|
|
).not.toHaveProperty('approvalMode');
|
|
});
|
|
|
|
it.each([
|
|
'default',
|
|
'auto-edit',
|
|
'yolo',
|
|
'auto',
|
|
'plan',
|
|
undefined,
|
|
{ name: 'default' },
|
|
])(
|
|
'only publishes supported read-back policy metadata (%j)',
|
|
async (approvalMode) => {
|
|
const provider = createJavaManagedAgentProvider({
|
|
baseUrl: 'https://product.example',
|
|
productScope: 'tenant:actor',
|
|
fetch: vi.fn<typeof fetch>().mockResolvedValue(
|
|
jsonResponse({
|
|
sessionId: 's',
|
|
agentId: 'qwen-code',
|
|
approvalMode,
|
|
status: 'ACTIVE',
|
|
createdAt: 1,
|
|
updatedAt: 1,
|
|
lastSequence: 0,
|
|
}),
|
|
),
|
|
});
|
|
const summary = await provider.getSession('s', { clientId: 'client' });
|
|
if (
|
|
['default', 'auto-edit', 'auto', 'yolo'].includes(String(approvalMode))
|
|
)
|
|
expect(summary.approvalMode).toBe(approvalMode);
|
|
else expect(summary).not.toHaveProperty('approvalMode');
|
|
},
|
|
);
|
|
|
|
it('creates a bound Session and initial Turn in one request with the chosen preset', async () => {
|
|
const fetchImpl = vi
|
|
.fn<typeof fetch>()
|
|
.mockResolvedValue(jsonResponse({ sessionId: 's', turnId: 't' }));
|
|
const provider = createJavaManagedAgentProvider({
|
|
baseUrl: 'https://product.example',
|
|
productScope: 'tenant:actor',
|
|
enableWorkspaceBinding: true,
|
|
fetch: fetchImpl,
|
|
});
|
|
const command = { clientId: 'client', idempotencyKey: 'frozen-key' };
|
|
await expect(
|
|
provider.workspaceBinding!.createSession!(
|
|
{
|
|
agentId: 'agent-b',
|
|
workspaceId: 'ws-b',
|
|
cwdRelative: 'src',
|
|
text: 'Read README',
|
|
},
|
|
command,
|
|
),
|
|
).resolves.toEqual({ sessionId: 's' });
|
|
const request = JSON.parse(fetchImpl.mock.calls[0][1]!.body as string);
|
|
expect(request).toEqual(
|
|
expect.objectContaining({
|
|
agentId: 'agent-b',
|
|
input: [{ type: 'input_text', text: 'Read README' }],
|
|
workspace: { workspaceId: 'ws-b', cwdRelative: 'src' },
|
|
metadata: { clientId: 'client' },
|
|
idempotencyKey: 'frozen-key',
|
|
}),
|
|
);
|
|
expect(fetchImpl).toHaveBeenCalledTimes(1);
|
|
});
|
|
|
|
it('rejects initial-input admission without a Turn identity', async () => {
|
|
const provider = createJavaManagedAgentProvider({
|
|
baseUrl: 'https://product.example',
|
|
productScope: 'tenant:actor',
|
|
enableWorkspaceBinding: true,
|
|
fetch: vi
|
|
.fn<typeof fetch>()
|
|
.mockResolvedValue(jsonResponse({ sessionId: 's' })),
|
|
});
|
|
await expect(
|
|
provider.workspaceBinding!.createSession!(
|
|
{ agentId: 'a', workspaceId: 'w', cwdRelative: '.', text: 'Read' },
|
|
{ clientId: 'c', idempotencyKey: 'k' },
|
|
),
|
|
).rejects.toThrow('admission identity');
|
|
});
|
|
|
|
it('keeps createEmpty empty when a host reuses a wider request object', async () => {
|
|
const fetchImpl = vi
|
|
.fn<typeof fetch>()
|
|
.mockResolvedValue(jsonResponse({ sessionId: 'empty' }));
|
|
const provider = createJavaManagedAgentProvider({
|
|
baseUrl: 'https://product.example',
|
|
productScope: 'tenant:actor',
|
|
enableWorkspaceBinding: true,
|
|
fetch: fetchImpl,
|
|
});
|
|
const request = {
|
|
agentId: 'a',
|
|
workspaceId: 'w',
|
|
cwdRelative: '.',
|
|
text: 'Extra host field',
|
|
};
|
|
await expect(
|
|
provider.workspaceBinding!.createEmpty(request, {
|
|
clientId: 'c',
|
|
idempotencyKey: 'k',
|
|
}),
|
|
).resolves.toEqual({ sessionId: 'empty' });
|
|
expect(
|
|
JSON.parse(fetchImpl.mock.calls[0][1]!.body as string).input,
|
|
).toEqual([]);
|
|
});
|
|
it('maps Java session, environment, and active turn state', async () => {
|
|
const fetchImpl = vi.fn<typeof fetch>().mockResolvedValue(
|
|
jsonResponse({
|
|
sessionId: 'session-1',
|
|
title: 'Managed task',
|
|
status: 'ACTIVE',
|
|
createdAt: 10,
|
|
updatedAt: 20,
|
|
activeTurn: {
|
|
turnId: 'turn-1',
|
|
sessionId: 'session-1',
|
|
status: 'IN_PROGRESS',
|
|
submittedAt: 11,
|
|
},
|
|
environment: {
|
|
environmentId: 'python',
|
|
state: 'starting',
|
|
},
|
|
lastSequence: 3,
|
|
}),
|
|
);
|
|
const provider = createJavaManagedAgentProvider({
|
|
baseUrl: 'https://product.example?token=not-stored',
|
|
fetch: fetchImpl,
|
|
});
|
|
|
|
const summary = await provider.getSession('session-1', {
|
|
clientId: 'client-1',
|
|
});
|
|
|
|
expect(summary).toEqual(
|
|
expect.objectContaining({
|
|
sessionId: 'session-1',
|
|
activeTurnId: 'turn-1',
|
|
phase: 'agent_running',
|
|
runtimeState: 'starting',
|
|
runtimeReady: false,
|
|
capabilities: { canSend: false, canCancel: true },
|
|
}),
|
|
);
|
|
expect(provider.storageKey).not.toContain('token');
|
|
expect(String(fetchImpl.mock.calls[0][0])).toBe(
|
|
'https://product.example/api/agent/web-shell/v1/sessions/get',
|
|
);
|
|
});
|
|
|
|
it.each([
|
|
['active', 'running', 'agent_running', false, true],
|
|
['active', 'cancelling', 'cancelling', false, false],
|
|
['archived', 'completed', 'completed', false, false],
|
|
['deleting', 'failed', 'failed', false, false],
|
|
// A terminal turn on an ACTIVE session enables the composer again; the
|
|
// only canSend: true row, the state a hardcoded false would delete.
|
|
['active', 'completed', 'completed', true, false],
|
|
] as const)(
|
|
'maps %s/%s to usable controls',
|
|
async (status, turnStatus, phase, canSend, canCancel) => {
|
|
const provider = createJavaManagedAgentProvider({
|
|
baseUrl: 'https://product.example',
|
|
fetch: vi.fn<typeof fetch>().mockResolvedValue(
|
|
jsonResponse({
|
|
sessionId: 'session-1',
|
|
status,
|
|
createdAt: 1,
|
|
updatedAt: 2,
|
|
activeTurn: {
|
|
turnId: 'turn-1',
|
|
status: turnStatus,
|
|
submittedAt: 1,
|
|
},
|
|
}),
|
|
),
|
|
});
|
|
expect(
|
|
await provider.getSession('session-1', { clientId: 'client-1' }),
|
|
).toEqual(
|
|
expect.objectContaining({
|
|
phase,
|
|
capabilities: { canSend, canCancel },
|
|
}),
|
|
);
|
|
},
|
|
);
|
|
|
|
it('maps the session list page, sends cursor and limit, and passes through nextCursor', async () => {
|
|
const fetchImpl = vi.fn<typeof fetch>().mockResolvedValue(
|
|
jsonResponse({
|
|
data: [
|
|
{
|
|
sessionId: 'list-1',
|
|
status: 'ACTIVE',
|
|
createdAt: 1,
|
|
updatedAt: 2,
|
|
lastSequence: 3,
|
|
},
|
|
],
|
|
hasMore: true,
|
|
nextCursor: 'cursor-2',
|
|
}),
|
|
);
|
|
const provider = createJavaManagedAgentProvider({
|
|
baseUrl: 'https://product.example',
|
|
fetch: fetchImpl,
|
|
});
|
|
|
|
const page = await provider.listSessions({
|
|
clientId: 'client-1',
|
|
cursor: 'cursor-1',
|
|
limit: 25,
|
|
});
|
|
|
|
expect(page.nextCursor).toBe('cursor-2');
|
|
expect(page.sessions).toHaveLength(1);
|
|
expect(page.sessions[0]).toEqual(
|
|
expect.objectContaining({
|
|
sessionId: 'list-1',
|
|
capabilities: expect.objectContaining({ canSend: true }),
|
|
}),
|
|
);
|
|
const [url, init] = fetchImpl.mock.calls[0]!;
|
|
expect(String(url)).toBe(
|
|
'https://product.example/api/agent/web-shell/v1/sessions/query',
|
|
);
|
|
expect(JSON.parse(String(init?.body))).toEqual(
|
|
expect.objectContaining({ cursor: 'cursor-1', limit: 25 }),
|
|
);
|
|
});
|
|
|
|
it('sends the paging cursor verbatim on transcript queries', async () => {
|
|
const fetchImpl = vi.fn<typeof fetch>().mockResolvedValue(
|
|
jsonResponse({
|
|
events: [],
|
|
items: [],
|
|
lastSequence: 7,
|
|
coveredSequence: 5,
|
|
hasMore: true,
|
|
olderCursor: 'older-1',
|
|
}),
|
|
);
|
|
const provider = createJavaManagedAgentProvider({
|
|
baseUrl: 'https://product.example',
|
|
fetch: fetchImpl,
|
|
});
|
|
|
|
const transcript = await provider.getTranscript('session-1', {
|
|
clientId: 'client-1',
|
|
before: 'before-1',
|
|
limit: 25,
|
|
});
|
|
|
|
expect(transcript.olderCursor).toBe('older-1');
|
|
expect(transcript.lastEventId).toBe(5);
|
|
expect(transcript.history).toBe('items');
|
|
expect(fetchImpl).toHaveBeenCalledTimes(1);
|
|
const [url, init] = fetchImpl.mock.calls[0]!;
|
|
expect(String(url)).toBe(
|
|
'https://product.example/api/agent/web-shell/v1/transcript/query',
|
|
);
|
|
expect(JSON.parse(String(init?.body))).toEqual(
|
|
expect.objectContaining({
|
|
sessionId: 'session-1',
|
|
cursor: 'before-1',
|
|
limit: 25,
|
|
}),
|
|
);
|
|
});
|
|
|
|
it.each([
|
|
['missing', undefined],
|
|
['null', null],
|
|
['negative', -1],
|
|
['fractional', 1.5],
|
|
])(
|
|
'rejects %s message coverage instead of resuming at the head',
|
|
async (_label, coverage) => {
|
|
const fetchImpl = vi.fn<typeof fetch>().mockResolvedValue(
|
|
jsonResponse({
|
|
events: [],
|
|
items: [],
|
|
lastSequence: 400,
|
|
coveredSequence: coverage,
|
|
hasMore: false,
|
|
}),
|
|
);
|
|
const provider = createJavaManagedAgentProvider({
|
|
baseUrl: 'https://product.example',
|
|
fetch: fetchImpl,
|
|
});
|
|
await expect(
|
|
provider.getTranscript('session-1', {
|
|
clientId: 'client-1',
|
|
limit: 100,
|
|
}),
|
|
).rejects.toThrow('Invalid message transcript coverage.');
|
|
expect(fetchImpl).toHaveBeenCalledTimes(1);
|
|
},
|
|
);
|
|
|
|
it('sends idempotent create, submit, and cancel commands only to Java', async () => {
|
|
const fetchImpl = vi
|
|
.fn<typeof fetch>()
|
|
.mockImplementation(async (input) => {
|
|
const url = String(input);
|
|
return jsonResponse({
|
|
sessionId: 'session-1',
|
|
turnId: url.includes('/sessions/create') ? 'turn-1' : 'turn-2',
|
|
status: 'accepted',
|
|
replayed: false,
|
|
...(!url.includes('/sessions/create')
|
|
? {
|
|
operationId: 'op-1',
|
|
admissionStage: 'java_durable',
|
|
deliveryState: 'pending',
|
|
}
|
|
: {}),
|
|
});
|
|
});
|
|
const provider = createJavaManagedAgentProvider({
|
|
baseUrl: 'https://product.example',
|
|
fetch: fetchImpl,
|
|
});
|
|
const options = { clientId: 'client-1', idempotencyKey: 'key-1' };
|
|
|
|
await provider.createSession({ text: 'hello' }, options);
|
|
await expect(
|
|
provider.submitPrompt('session-1', { text: 'next' }, options),
|
|
).resolves.toEqual({
|
|
sessionId: 'session-1',
|
|
turnId: 'turn-2',
|
|
operationId: 'op-1',
|
|
});
|
|
await expect(
|
|
provider.cancel('session-1', 'turn-2', options),
|
|
).resolves.toEqual({
|
|
sessionId: 'session-1',
|
|
turnId: 'turn-2',
|
|
operationId: 'op-1',
|
|
});
|
|
|
|
expect(fetchImpl.mock.calls.map(([url]) => String(url))).toEqual([
|
|
'https://product.example/api/agent/web-shell/v1/sessions/create',
|
|
'https://product.example/api/agent/web-shell/v1/turns/submit',
|
|
'https://product.example/api/agent/web-shell/v1/turns/cancel',
|
|
]);
|
|
const createBody = JSON.parse(String(fetchImpl.mock.calls[0][1]?.body));
|
|
expect(createBody).toEqual(
|
|
expect.objectContaining({
|
|
requestId: expect.stringMatching(/^managed_/),
|
|
idempotencyKey: 'key-1',
|
|
agentId: 'qwen-code',
|
|
input: [{ type: 'input_text', text: 'hello' }],
|
|
}),
|
|
);
|
|
expect(createBody).not.toHaveProperty('environmentId');
|
|
for (const [, init] of fetchImpl.mock.calls)
|
|
expect(JSON.parse(String(init?.body))).not.toHaveProperty(
|
|
'admissionProfile',
|
|
);
|
|
});
|
|
|
|
it.each(['submit', 'cancel'] as const)(
|
|
'rejects a %s response without its durable operation',
|
|
async (command) => {
|
|
const provider = createJavaManagedAgentProvider({
|
|
baseUrl: 'https://product.example',
|
|
fetch: vi.fn<typeof fetch>().mockResolvedValue(
|
|
jsonResponse({
|
|
sessionId: 'session-1',
|
|
turnId: 'turn-1',
|
|
status: 'accepted',
|
|
replayed: false,
|
|
}),
|
|
),
|
|
});
|
|
const options = { clientId: 'client-1', idempotencyKey: 'original-key' };
|
|
const result =
|
|
command === 'submit'
|
|
? provider.submitPrompt('session-1', { text: 'Continue' }, options)
|
|
: provider.cancel('session-1', 'turn-1', options);
|
|
await expect(result).rejects.toThrow(/identity/);
|
|
},
|
|
);
|
|
|
|
it('strips control characters from the derived create title', async () => {
|
|
const fetchImpl = vi.fn<typeof fetch>().mockImplementation(async () =>
|
|
jsonResponse({
|
|
sessionId: 'session-1',
|
|
turnId: 'turn-1',
|
|
status: 'accepted',
|
|
replayed: false,
|
|
}),
|
|
);
|
|
const provider = createJavaManagedAgentProvider({
|
|
baseUrl: 'https://product.example',
|
|
fetch: fetchImpl,
|
|
});
|
|
|
|
// Pasted terminal output carries ANSI escapes, NUL, BEL or DEL; the
|
|
// server refuses C0 control characters in a title with 400
|
|
// invalid_title, so the derivation strips them before posting.
|
|
await provider.createSession(
|
|
{ text: '\u001b[31mERROR\u001b[0m the build failed\u0007' },
|
|
{ clientId: 'client-1', idempotencyKey: 'key-1' },
|
|
);
|
|
|
|
const [url, init] = fetchImpl.mock.calls[0]!;
|
|
expect(String(url)).toBe(
|
|
'https://product.example/api/agent/web-shell/v1/sessions/create',
|
|
);
|
|
expect(JSON.parse(String(init?.body)).title).toBe(
|
|
'[31mERROR [0m the build failed',
|
|
);
|
|
});
|
|
|
|
it('preserves creation Skills and the prompt-derived title for a Workspace-bound create', async () => {
|
|
const skills = [
|
|
{
|
|
name: 'review',
|
|
description: 'Review the current change.',
|
|
body: 'Check the current change for regressions.',
|
|
userInvocable: true,
|
|
disableModelInvocation: false,
|
|
},
|
|
];
|
|
const fetchImpl = vi.fn<typeof fetch>().mockImplementation(async () =>
|
|
jsonResponse({
|
|
sessionId: 'session-1',
|
|
turnId: 'turn-1',
|
|
status: 'accepted',
|
|
replayed: false,
|
|
}),
|
|
);
|
|
const provider = createJavaManagedAgentProvider({
|
|
baseUrl: 'https://product.example',
|
|
fetch: fetchImpl,
|
|
productScope: 'tenant:actor',
|
|
enableWorkspaceBinding: true,
|
|
});
|
|
|
|
// The bound create omitted the title, so the list and header showed a
|
|
// Session-ID prefix while the standard shell shows a prompt-derived title.
|
|
await provider.workspaceBinding!.createSession!(
|
|
{
|
|
agentId: 'qwen-code',
|
|
workspaceId: 'ws-default',
|
|
cwdRelative: 'services/api',
|
|
text: '\u001b[31mERROR\u001b[0m the build failed',
|
|
skills,
|
|
},
|
|
{ clientId: 'client-1', idempotencyKey: 'key-1' },
|
|
);
|
|
|
|
const body = JSON.parse(String(fetchImpl.mock.calls[0]![1]?.body));
|
|
expect(body.title).toBe('[31mERROR [0m the build failed');
|
|
expect(body.skills).toEqual(skills);
|
|
expect(body.idempotencyKey).toBe('key-1');
|
|
expect(body.workspace).toEqual({
|
|
workspaceId: 'ws-default',
|
|
cwdRelative: 'services/api',
|
|
});
|
|
|
|
// The cap must slice by code point: a code-unit cut at 77 splits an astral
|
|
// character, sending a title that ends in a lone high surrogate, which no
|
|
// store column keeps, so the server would persist the tail as '?'.
|
|
await provider.workspaceBinding!.createSession!(
|
|
{
|
|
agentId: 'qwen-code',
|
|
workspaceId: 'ws-default',
|
|
cwdRelative: 'services/api',
|
|
text: `${'a'.repeat(76)}\u{1f600}${'b'.repeat(10)}`,
|
|
},
|
|
{ clientId: 'client-1', idempotencyKey: 'key-2' },
|
|
);
|
|
const capped = JSON.parse(String(fetchImpl.mock.calls[1]![1]?.body)).title;
|
|
expect(/[\uD800-\uDBFF](?![\uDC00-\uDFFF])/.test(capped)).toBe(false);
|
|
});
|
|
|
|
it('scopes the storageKey by product identity', () => {
|
|
const a = createJavaManagedAgentProvider({
|
|
baseUrl: 'https://product.example',
|
|
productScope: 'tenant:actor-a',
|
|
});
|
|
const b = createJavaManagedAgentProvider({
|
|
baseUrl: 'https://product.example',
|
|
productScope: 'tenant:actor-b',
|
|
});
|
|
expect(a.storageKey).not.toBe(b.storageKey);
|
|
expect(a.storageKey).toContain('tenant:actor-a');
|
|
});
|
|
|
|
it('uses lastEventId only as the Java public sequence cursor', async () => {
|
|
const fetchImpl = vi
|
|
.fn<typeof fetch>()
|
|
.mockResolvedValue(
|
|
new Response(
|
|
'id: 9\nevent: turn.completed\ndata: {"sequence":9,"eventId":"evt_9","sessionId":"session-1","turnId":"turn-1","type":"turn.completed","createdAt":9,"data":{},"terminal":true}\n\n',
|
|
{ status: 200 },
|
|
),
|
|
);
|
|
const provider = createJavaManagedAgentProvider({
|
|
baseUrl: 'https://product.example',
|
|
fetch: fetchImpl,
|
|
});
|
|
|
|
const events = [];
|
|
for await (const event of provider.subscribeEvents('session-1', {
|
|
clientId: 'client-1',
|
|
lastEventId: 8,
|
|
})) {
|
|
events.push(event);
|
|
}
|
|
|
|
expect(events).toEqual([
|
|
expect.objectContaining({ id: 9, type: 'completed' }),
|
|
]);
|
|
expect(JSON.parse(String(fetchImpl.mock.calls[0][1]?.body))).toEqual({
|
|
sessionId: 'session-1',
|
|
afterSequence: 8,
|
|
});
|
|
});
|
|
|
|
it('forwards stream establishment to the subscribe caller', async () => {
|
|
const provider = createJavaManagedAgentProvider({
|
|
baseUrl: 'https://product.example',
|
|
fetch: vi
|
|
.fn<typeof fetch>()
|
|
.mockResolvedValue(
|
|
new Response(
|
|
'id: 9\nevent: turn.completed\ndata: {"sequence":9,"eventId":"evt_9","sessionId":"session-1","turnId":"turn-1","type":"turn.completed","createdAt":9,"data":{},"terminal":true}\n\n',
|
|
{ status: 200 },
|
|
),
|
|
),
|
|
});
|
|
const onEstablished = vi.fn();
|
|
const events = [];
|
|
for await (const event of provider.subscribeEvents('session-1', {
|
|
clientId: 'client-1',
|
|
lastEventId: 8,
|
|
onEstablished,
|
|
})) {
|
|
events.push(event);
|
|
}
|
|
expect(onEstablished).toHaveBeenCalledOnce();
|
|
expect(events).toEqual([
|
|
expect.objectContaining({ id: 9, type: 'completed' }),
|
|
]);
|
|
});
|
|
|
|
it('forwards stream keep-alives to the subscribe caller', async () => {
|
|
const provider = createJavaManagedAgentProvider({
|
|
baseUrl: 'https://product.example',
|
|
fetch: vi
|
|
.fn<typeof fetch>()
|
|
.mockResolvedValue(
|
|
new Response(
|
|
': keepalive\r\n\r\nid: 9\nevent: turn.completed\ndata: {"sequence":9,"eventId":"evt_9","sessionId":"session-1","turnId":"turn-1","type":"turn.completed","createdAt":9,"data":{},"terminal":true}\n\n: keepalive\n\n',
|
|
{ status: 200 },
|
|
),
|
|
),
|
|
});
|
|
const onAlive = vi.fn();
|
|
const events = [];
|
|
for await (const event of provider.subscribeEvents('session-1', {
|
|
clientId: 'client-1',
|
|
lastEventId: 8,
|
|
onAlive,
|
|
})) {
|
|
events.push(event);
|
|
}
|
|
expect(onAlive).toHaveBeenCalledTimes(2);
|
|
expect(events).toEqual([
|
|
expect.objectContaining({ id: 9, type: 'completed' }),
|
|
]);
|
|
});
|
|
|
|
it('turns a resync frame into a stream gap and stops', async () => {
|
|
const provider = createJavaManagedAgentProvider({
|
|
baseUrl: 'https://product.example',
|
|
fetch: vi
|
|
.fn<typeof fetch>()
|
|
.mockResolvedValue(
|
|
new Response(
|
|
'event:agent.session.resync_required\ndata:{"type":"agent.session.resync_required","sessionId":"session-1","replayFloorSequence":40,"snapshotThroughSequence":42,"action":"reload_snapshot"}\n\nid: 43\nevent: turn.completed\ndata: {"sequence":43,"eventId":"evt_43","sessionId":"session-1","turnId":"turn-1","type":"turn.completed","createdAt":43,"data":{},"terminal":true}\n\n',
|
|
{ status: 200 },
|
|
),
|
|
),
|
|
});
|
|
|
|
const events = [];
|
|
for await (const event of provider.subscribeEvents('session-1', {
|
|
clientId: 'client-1',
|
|
lastEventId: 3,
|
|
})) {
|
|
events.push(event);
|
|
}
|
|
|
|
expect(events).toEqual([
|
|
expect.objectContaining({
|
|
id: 3,
|
|
type: 'stream_gap',
|
|
sessionId: 'session-1',
|
|
data: expect.objectContaining({ replayFloorSequence: 40 }),
|
|
}),
|
|
]);
|
|
});
|
|
|
|
it('projects a bounded transcript snapshot from canonical Java events', async () => {
|
|
const fetchImpl = vi.fn<typeof fetch>().mockResolvedValue(
|
|
jsonResponse({
|
|
items: [],
|
|
events: [
|
|
{
|
|
sequence: 1,
|
|
eventId: 'evt_1',
|
|
sessionId: 'session-1',
|
|
turnId: 'turn-1',
|
|
type: 'turn.accepted',
|
|
createdAt: 1,
|
|
data: { input: [{ type: 'text', text: 'hello' }] },
|
|
terminal: false,
|
|
},
|
|
{
|
|
sequence: 2,
|
|
eventId: 'evt_2',
|
|
sessionId: 'session-1',
|
|
turnId: 'turn-1',
|
|
type: 'item.output_text.delta',
|
|
createdAt: 2,
|
|
data: { text: 'world' },
|
|
terminal: false,
|
|
},
|
|
],
|
|
olderCursor: 'older-1',
|
|
hasMore: true,
|
|
lastSequence: 4,
|
|
coveredSequence: 4,
|
|
}),
|
|
);
|
|
const provider = createJavaManagedAgentProvider({
|
|
baseUrl: 'https://product.example',
|
|
fetch: fetchImpl,
|
|
});
|
|
|
|
const transcript = await provider.getTranscript('session-1', {
|
|
clientId: 'client-1',
|
|
limit: 100,
|
|
});
|
|
|
|
expect(transcript.events).toEqual([
|
|
expect.objectContaining({ id: 1, type: 'accepted' }),
|
|
expect.objectContaining({ id: 2, type: 'assistant_delta' }),
|
|
]);
|
|
expect(transcript.olderCursor).toBe('older-1');
|
|
expect(transcript.lastEventId).toBe(4);
|
|
});
|
|
|
|
it('hydrates history from durable items plus control events', async () => {
|
|
const fetchImpl = vi.fn<typeof fetch>().mockResolvedValue(
|
|
jsonResponse({
|
|
items: [
|
|
{
|
|
itemId: 'input-1',
|
|
sessionId: 'session-1',
|
|
turnId: 'turn-1',
|
|
type: 'message',
|
|
role: 'user',
|
|
status: 'completed',
|
|
content: [
|
|
{
|
|
partId: 'input-part-1',
|
|
type: 'input_text',
|
|
text: 'hello',
|
|
firstSequence: 1,
|
|
lastSequence: 1,
|
|
},
|
|
],
|
|
attributes: {},
|
|
firstSequence: 1,
|
|
lastSequence: 1,
|
|
createdAt: 1,
|
|
updatedAt: 1,
|
|
},
|
|
{
|
|
itemId: 'output-1',
|
|
sessionId: 'session-1',
|
|
turnId: 'turn-1',
|
|
type: 'message',
|
|
role: 'assistant',
|
|
status: 'completed',
|
|
content: [
|
|
{
|
|
partId: 'output-part-1',
|
|
type: 'output_text',
|
|
text: 'world',
|
|
firstSequence: 2,
|
|
lastSequence: 3,
|
|
},
|
|
],
|
|
attributes: {},
|
|
firstSequence: 2,
|
|
lastSequence: 4,
|
|
createdAt: 2,
|
|
updatedAt: 4,
|
|
},
|
|
],
|
|
events: [
|
|
{
|
|
sequence: 4,
|
|
eventId: 'evt_4',
|
|
sessionId: 'session-1',
|
|
turnId: 'turn-1',
|
|
type: 'turn.completed',
|
|
createdAt: 4,
|
|
data: {},
|
|
terminal: true,
|
|
},
|
|
],
|
|
coveredSequence: 4,
|
|
hasMore: false,
|
|
lastSequence: 4,
|
|
}),
|
|
);
|
|
const provider = createJavaManagedAgentProvider({
|
|
baseUrl: 'https://product.example',
|
|
fetch: fetchImpl,
|
|
});
|
|
|
|
const transcript = await provider.getTranscript('session-1', {
|
|
clientId: 'client-1',
|
|
});
|
|
|
|
expect(transcript.events).toEqual([
|
|
expect.objectContaining({
|
|
id: 1,
|
|
type: 'accepted',
|
|
assembledFromItem: true,
|
|
}),
|
|
expect.objectContaining({
|
|
id: 2,
|
|
type: 'assistant_delta',
|
|
data: { itemId: 'output-1', text: 'world' },
|
|
assembledFromItem: true,
|
|
}),
|
|
expect.objectContaining({ id: 4, type: 'completed' }),
|
|
]);
|
|
expect(transcript.olderCursor).toBeUndefined();
|
|
expect(transcript.lastEventId).toBe(4);
|
|
});
|
|
|
|
it.each([
|
|
undefined,
|
|
{ text: '{"input":', truncated: true, byteLength: 9000 },
|
|
])(
|
|
'lists pending Actions with optional preview %j, sends revisions, and rejects a failed replay',
|
|
async (inputPreview) => {
|
|
const permission = {
|
|
actionId: 'tool_approval_1',
|
|
sessionId: 'session-1',
|
|
kind: 'permission',
|
|
source: { type: 'tool_call' },
|
|
state: 'requested',
|
|
inputRevision: 1,
|
|
policyRevision: 'hosted-tool-approval/1',
|
|
createdAt: 1,
|
|
expiresAt: 600_001,
|
|
options: [
|
|
{ id: 'allow', label: 'Allow' },
|
|
{ id: 'deny', label: 'Deny' },
|
|
],
|
|
turnId: 'turn-1',
|
|
functionCallId: 'call-1',
|
|
toolName: 'write_file',
|
|
...(inputPreview ? { inputPreview } : {}),
|
|
};
|
|
const fetchImpl = vi
|
|
.fn<typeof fetch>()
|
|
.mockResolvedValueOnce(
|
|
jsonResponse({
|
|
data: [
|
|
permission,
|
|
{ ...permission, actionId: 'tool_approval_2', state: 'decided' },
|
|
{
|
|
...permission,
|
|
actionId: 'question_1',
|
|
kind: 'question',
|
|
state: 'decided',
|
|
questions: [],
|
|
},
|
|
],
|
|
hasMore: false,
|
|
}),
|
|
)
|
|
.mockResolvedValueOnce(
|
|
jsonResponse({ operationId: 'op-1', status: 'running' }),
|
|
)
|
|
.mockResolvedValueOnce(
|
|
jsonResponse({
|
|
operationId: 'op-1',
|
|
type: 'action_response',
|
|
status: 'completed',
|
|
}),
|
|
);
|
|
const provider = createJavaManagedAgentProvider({
|
|
baseUrl: 'https://product.example',
|
|
fetch: fetchImpl,
|
|
});
|
|
|
|
const pending = await provider.actions!.listPending('session-1', {
|
|
clientId: 'client-1',
|
|
});
|
|
expect(pending).toEqual([
|
|
{
|
|
actionId: 'tool_approval_1',
|
|
sessionId: 'session-1',
|
|
turnId: 'turn-1',
|
|
functionCallId: 'call-1',
|
|
toolName: 'write_file',
|
|
inputRevision: 1,
|
|
policyRevision: 'hosted-tool-approval/1',
|
|
expiresAt: 600_001,
|
|
...(inputPreview ? { inputPreview } : {}),
|
|
options: [
|
|
{ id: 'allow', label: 'Allow' },
|
|
{ id: 'deny', label: 'Deny' },
|
|
],
|
|
},
|
|
]);
|
|
expect(String(fetchImpl.mock.calls[0][0])).toBe(
|
|
'https://product.example/api/agent/web-shell/v1/actions/query',
|
|
);
|
|
|
|
await provider.actions!.respond(pending[0], 'deny', {
|
|
clientId: 'client-1',
|
|
idempotencyKey: 'tool_approval_1:deny',
|
|
});
|
|
expect(String(fetchImpl.mock.calls[1][0])).toBe(
|
|
'https://product.example/api/agent/web-shell/v1/actions/respond',
|
|
);
|
|
expect(JSON.parse(String(fetchImpl.mock.calls[1][1]?.body))).toEqual({
|
|
requestId: expect.any(String),
|
|
idempotencyKey: 'tool_approval_1:deny',
|
|
sessionId: 'session-1',
|
|
actionId: 'tool_approval_1',
|
|
response: {
|
|
kind: 'permission',
|
|
inputRevision: 1,
|
|
policyRevision: 'hosted-tool-approval/1',
|
|
optionId: 'deny',
|
|
},
|
|
});
|
|
// The accepted answer is followed on its own operation, not re-sent.
|
|
expect(String(fetchImpl.mock.calls[2][0])).toBe(
|
|
'https://product.example/api/agent/web-shell/v1/operations/query',
|
|
);
|
|
expect(JSON.parse(String(fetchImpl.mock.calls[2][1]?.body))).toEqual({
|
|
sessionId: 'session-1',
|
|
operationId: 'op-1',
|
|
});
|
|
|
|
fetchImpl.mockResolvedValueOnce(
|
|
new Response(
|
|
JSON.stringify({
|
|
operationId: 'op-1',
|
|
sessionId: 'session-1',
|
|
type: 'action_response',
|
|
status: 'failed',
|
|
admissionStage: 'java_durable',
|
|
deliveryState: 'blocked',
|
|
failureCode: 'action_delivery_failed',
|
|
replayed: true,
|
|
}),
|
|
{ status: 202, headers: { 'content-type': 'application/json' } },
|
|
),
|
|
);
|
|
await expect(
|
|
provider.actions!.respond(pending[0], 'deny', {
|
|
clientId: 'client-1',
|
|
idempotencyKey: 'tool_approval_1:deny',
|
|
}),
|
|
).rejects.toThrow('action_delivery_failed');
|
|
|
|
// The failure code rides the error so the hook can classify it (an ended
|
|
// Action vs a delivery failure worth retrying).
|
|
fetchImpl.mockResolvedValueOnce(
|
|
new Response(
|
|
JSON.stringify({
|
|
operationId: 'op-2',
|
|
sessionId: 'session-1',
|
|
type: 'action_response',
|
|
status: 'failed',
|
|
admissionStage: 'java_durable',
|
|
deliveryState: 'confirmed',
|
|
failureCode: 'action_response_delivery_failed',
|
|
replayed: true,
|
|
}),
|
|
{ status: 202, headers: { 'content-type': 'application/json' } },
|
|
),
|
|
);
|
|
await expect(
|
|
provider.actions!.respond(pending[0], 'deny', {
|
|
clientId: 'client-1',
|
|
idempotencyKey: 'tool_approval_1:deny',
|
|
}),
|
|
).rejects.toMatchObject({ code: 'action_response_delivery_failed' });
|
|
|
|
for (const status of ['cancelled', 'recovery_blocked'] as const) {
|
|
fetchImpl.mockResolvedValueOnce(
|
|
new Response(
|
|
JSON.stringify({
|
|
operationId: `op-${status}`,
|
|
sessionId: 'session-1',
|
|
type: 'action_response',
|
|
status,
|
|
admissionStage: 'java_durable',
|
|
deliveryState: 'blocked',
|
|
replayed: true,
|
|
}),
|
|
{ status: 202, headers: { 'content-type': 'application/json' } },
|
|
),
|
|
);
|
|
await expect(
|
|
provider.actions!.respond(pending[0], 'deny', {
|
|
clientId: 'client-1',
|
|
idempotencyKey: 'tool_approval_1:deny',
|
|
}),
|
|
).rejects.toThrow(`approval answer ${status}`);
|
|
}
|
|
},
|
|
);
|
|
|
|
it('lists a durable question and submits its structured answer through the existing operation', async () => {
|
|
const question = pendingQuestion();
|
|
const fetchImpl = vi
|
|
.fn<typeof fetch>()
|
|
.mockResolvedValueOnce(
|
|
jsonResponse({
|
|
data: [
|
|
{
|
|
...question,
|
|
source: { kind: 'tool_call', id: question.functionCallId },
|
|
state: 'requested',
|
|
createdAt: 1,
|
|
},
|
|
],
|
|
hasMore: false,
|
|
}),
|
|
)
|
|
.mockResolvedValueOnce(
|
|
jsonResponse({
|
|
operationId: 'question-response',
|
|
type: 'action_response',
|
|
status: 'completed',
|
|
}),
|
|
);
|
|
const provider = createJavaManagedAgentProvider({
|
|
baseUrl: 'https://product.example',
|
|
fetch: fetchImpl,
|
|
});
|
|
const pending = await provider.actions!.listPending(question.sessionId, {
|
|
clientId: 'client',
|
|
});
|
|
expect(pending).toEqual([question]);
|
|
const response = {
|
|
kind: 'question' as const,
|
|
answers: [
|
|
{ questionId: 'q1', optionIds: ['q1o2'] },
|
|
{ questionId: 'q2', optionIds: ['q2o1', 'q2o2'], text: 'custom' },
|
|
],
|
|
};
|
|
await provider.actions!.respond(question, response, {
|
|
clientId: 'client',
|
|
idempotencyKey: 'question:stable-digest',
|
|
});
|
|
expect(JSON.parse(String(fetchImpl.mock.calls[1][1]?.body))).toEqual({
|
|
requestId: expect.any(String),
|
|
sessionId: question.sessionId,
|
|
actionId: question.actionId,
|
|
idempotencyKey: 'question:stable-digest',
|
|
response: {
|
|
...response,
|
|
inputRevision: question.inputRevision,
|
|
policyRevision: question.policyRevision,
|
|
},
|
|
});
|
|
await expect(
|
|
provider.actions!.respond(question, 'allow', {
|
|
clientId: 'client',
|
|
idempotencyKey: 'invalid',
|
|
}),
|
|
).rejects.toThrow('kind does not match');
|
|
expect(fetchImpl).toHaveBeenCalledTimes(2);
|
|
});
|
|
|
|
describe('an accepted approval answer', () => {
|
|
const action = {
|
|
actionId: 'tool_approval_1',
|
|
sessionId: 'session-1',
|
|
turnId: 'turn-1',
|
|
functionCallId: 'call-1',
|
|
toolName: 'write_file',
|
|
inputRevision: 1,
|
|
policyRevision: 'hosted-tool-approval/1',
|
|
expiresAt: 600_001,
|
|
options: [
|
|
{ id: 'allow', label: 'Allow' },
|
|
{ id: 'deny', label: 'Deny' },
|
|
],
|
|
};
|
|
const operation = (status: string, failureCode?: string) =>
|
|
jsonResponse({
|
|
operationId: 'op-1',
|
|
sessionId: 'session-1',
|
|
type: 'action_response',
|
|
status,
|
|
admissionStage: 'java_durable',
|
|
deliveryState: status === 'running' ? 'leased' : 'confirmed',
|
|
...(failureCode ? { failureCode } : {}),
|
|
replayed: false,
|
|
});
|
|
const urls = (fetchImpl: ReturnType<typeof vi.fn<typeof fetch>>) =>
|
|
fetchImpl.mock.calls.map(([url]) =>
|
|
String(url).replace(
|
|
'https://product.example/api/agent/web-shell/v1',
|
|
'',
|
|
),
|
|
);
|
|
|
|
beforeEach(() => {
|
|
vi.useFakeTimers();
|
|
});
|
|
afterEach(() => {
|
|
vi.useRealTimers();
|
|
});
|
|
|
|
it('rejects when the operation it was accepted on later fails', async () => {
|
|
const fetchImpl = vi
|
|
.fn<typeof fetch>()
|
|
.mockResolvedValueOnce(operation('pending'))
|
|
.mockResolvedValueOnce(operation('running'))
|
|
.mockResolvedValueOnce(operation('failed', 'workspace_unavailable'));
|
|
const provider = createJavaManagedAgentProvider({
|
|
baseUrl: 'https://product.example',
|
|
fetch: fetchImpl,
|
|
});
|
|
|
|
// eslint-disable-next-line vitest/valid-expect -- awaited via `answer` below, after the fake timers advance (handler attached early so the rejection is not unhandled)
|
|
const answer = expect(
|
|
provider.actions!.respond(action, 'allow', {
|
|
clientId: 'client-1',
|
|
idempotencyKey: 'tool_approval_1:allow',
|
|
}),
|
|
).rejects.toThrow('approval answer failed (workspace_unavailable)');
|
|
await vi.advanceTimersByTimeAsync(30_000);
|
|
await answer;
|
|
expect(urls(fetchImpl)).toEqual([
|
|
'/actions/respond',
|
|
'/operations/query',
|
|
'/operations/query',
|
|
]);
|
|
});
|
|
|
|
it('reports an answer still unsettled after the polling budget as unconfirmed', async () => {
|
|
const fetchImpl = vi
|
|
.fn<typeof fetch>()
|
|
.mockImplementation(async () => operation('running'));
|
|
const provider = createJavaManagedAgentProvider({
|
|
baseUrl: 'https://product.example',
|
|
fetch: fetchImpl,
|
|
});
|
|
|
|
// eslint-disable-next-line vitest/valid-expect -- awaited via `answer` below, after the fake timers advance (handler attached early so the rejection is not unhandled)
|
|
const answer = expect(
|
|
provider.actions!.respond(action, 'allow', {
|
|
clientId: 'client-1',
|
|
idempotencyKey: 'tool_approval_1:allow',
|
|
}),
|
|
).rejects.toThrow('not confirmed yet (operation op-1 is running)');
|
|
await vi.advanceTimersByTimeAsync(60_000);
|
|
await answer;
|
|
const calls = urls(fetchImpl);
|
|
// One answer, then reads of that same operation only.
|
|
expect(calls[0]).toBe('/actions/respond');
|
|
expect(calls.slice(1).every((url) => url === '/operations/query')).toBe(
|
|
true,
|
|
);
|
|
expect(calls.length).toBeGreaterThan(2);
|
|
});
|
|
});
|
|
|
|
it('reports the actions capability only when the Session has it', async () => {
|
|
const session = {
|
|
sessionId: 'session-1',
|
|
status: 'ACTIVE',
|
|
createdAt: 1,
|
|
updatedAt: 1,
|
|
lastSequence: 0,
|
|
};
|
|
const provider = createJavaManagedAgentProvider({
|
|
baseUrl: 'https://product.example',
|
|
fetch: vi
|
|
.fn<typeof fetch>()
|
|
.mockResolvedValueOnce(
|
|
jsonResponse({
|
|
...session,
|
|
capabilities: { actions: true, tasks: false },
|
|
}),
|
|
)
|
|
.mockResolvedValueOnce(jsonResponse(session)),
|
|
});
|
|
expect(
|
|
(await provider.getSession('session-1', { clientId: 'c' })).capabilities,
|
|
).toEqual({ canSend: true, canCancel: false, actions: true });
|
|
expect(
|
|
(await provider.getSession('session-1', { clientId: 'c' })).capabilities,
|
|
).toEqual({ canSend: true, canCancel: false });
|
|
});
|
|
|
|
it('lets a bound Session send only when the service allows its caller', async () => {
|
|
const bound = {
|
|
sessionId: 'bound-1',
|
|
status: 'ACTIVE',
|
|
createdAt: 1,
|
|
updatedAt: 1,
|
|
lastSequence: 3,
|
|
workspace: { workspaceId: 'ws-a', cwdRelative: '.' },
|
|
};
|
|
const provider = createJavaManagedAgentProvider({
|
|
baseUrl: 'https://product.example',
|
|
fetch: vi
|
|
.fn<typeof fetch>()
|
|
.mockResolvedValueOnce(
|
|
jsonResponse({
|
|
...bound,
|
|
capabilities: { tasks: true, workspaceTurns: true },
|
|
}),
|
|
)
|
|
.mockResolvedValueOnce(
|
|
jsonResponse({
|
|
...bound,
|
|
capabilities: { tasks: true, workspaceTurns: false },
|
|
}),
|
|
),
|
|
});
|
|
|
|
expect(
|
|
(await provider.getSession('bound-1', { clientId: 'c' })).capabilities,
|
|
).toEqual({
|
|
canSend: true,
|
|
canCancel: false,
|
|
workspaceTurns: true,
|
|
tasks: true,
|
|
});
|
|
expect(
|
|
(await provider.getSession('bound-1', { clientId: 'c' })).capabilities,
|
|
).toEqual({ canSend: false, canCancel: false, tasks: true });
|
|
});
|
|
|
|
it.each([true, false])(
|
|
'offers cancel for a running bound Turn when workspaceTurns is %s',
|
|
async (workspaceTurns) => {
|
|
const provider = createJavaManagedAgentProvider({
|
|
baseUrl: 'https://product.example',
|
|
fetch: vi.fn<typeof fetch>().mockResolvedValue(
|
|
jsonResponse({
|
|
sessionId: 'bound-1',
|
|
status: 'ACTIVE',
|
|
createdAt: 1,
|
|
updatedAt: 2,
|
|
lastSequence: 5,
|
|
workspace: { workspaceId: 'ws-a', cwdRelative: '.' },
|
|
activeTurn: {
|
|
turnId: 'turn-2',
|
|
sessionId: 'bound-1',
|
|
status: 'RUNNING',
|
|
submittedAt: 2,
|
|
},
|
|
capabilities: { tasks: true, workspaceTurns },
|
|
}),
|
|
),
|
|
});
|
|
// The creator may still cancel after the Workspace stops admitting
|
|
// new work, so cancel does not follow workspaceTurns; the server
|
|
// refuses anyone else.
|
|
expect(
|
|
(await provider.getSession('bound-1', { clientId: 'c' })).capabilities,
|
|
).toEqual({
|
|
canSend: false,
|
|
canCancel: true,
|
|
tasks: true,
|
|
...(workspaceTurns ? { workspaceTurns: true } : {}),
|
|
});
|
|
},
|
|
);
|
|
|
|
it.each([true, false])(
|
|
'gates fresh Shell sending with foregroundShell=%s while retaining cancellation',
|
|
async (enabled) => {
|
|
const base = {
|
|
sessionId: 'shell-1',
|
|
status: 'ACTIVE',
|
|
createdAt: 1,
|
|
updatedAt: 2,
|
|
lastSequence: 5,
|
|
workspace: { workspaceId: 'ws-a', cwdRelative: '.' },
|
|
capabilities: {
|
|
tasks: true,
|
|
workspaceTurns: true,
|
|
foregroundShell: enabled,
|
|
},
|
|
};
|
|
const provider = createJavaManagedAgentProvider({
|
|
baseUrl: 'https://product.example',
|
|
fetch: vi
|
|
.fn<typeof fetch>()
|
|
.mockResolvedValueOnce(jsonResponse(base))
|
|
.mockResolvedValueOnce(
|
|
jsonResponse({
|
|
...base,
|
|
activeTurn: {
|
|
turnId: 'turn-1',
|
|
sessionId: 'shell-1',
|
|
status: 'RUNNING',
|
|
submittedAt: 2,
|
|
},
|
|
}),
|
|
),
|
|
});
|
|
expect(
|
|
(await provider.getSession('shell-1', { clientId: 'c' })).capabilities,
|
|
).toEqual({
|
|
canSend: enabled,
|
|
canCancel: false,
|
|
workspaceTurns: true,
|
|
tasks: true,
|
|
});
|
|
expect(
|
|
(await provider.getSession('shell-1', { clientId: 'c' })).capabilities,
|
|
).toEqual({
|
|
canSend: false,
|
|
canCancel: true,
|
|
workspaceTurns: true,
|
|
tasks: true,
|
|
});
|
|
},
|
|
);
|
|
|
|
it('passes download cancellation through the host sink to the content fetch', async () => {
|
|
const abort = new AbortController();
|
|
const fetchImpl = vi
|
|
.fn<typeof fetch>()
|
|
.mockImplementation(async (_url, init) => {
|
|
expect(init?.signal).toBe(abort.signal);
|
|
return new Response('hello', {
|
|
status: 200,
|
|
headers: { etag: `"${artifact.sha256}"`, 'content-length': '5' },
|
|
});
|
|
});
|
|
const saveArtifact = vi.fn(async (_artifact, options) => {
|
|
expect(options.signal).toBe(abort.signal);
|
|
const stream = await options.openStream();
|
|
expect(await stream.getReader().read()).toMatchObject({ done: false });
|
|
});
|
|
const provider = createJavaManagedAgentProvider({
|
|
baseUrl: 'https://product.example',
|
|
fetch: fetchImpl,
|
|
saveArtifact,
|
|
});
|
|
await provider.toolResults!.downloadArtifact(artifact, {
|
|
clientId: 'client',
|
|
signal: abort.signal,
|
|
});
|
|
expect(saveArtifact).toHaveBeenCalledWith(
|
|
artifact,
|
|
expect.objectContaining({
|
|
signal: abort.signal,
|
|
openStream: expect.any(Function),
|
|
}),
|
|
);
|
|
expect(fetchImpl).toHaveBeenCalledTimes(1);
|
|
});
|
|
});
|