[Init] Initial commit - NetMesh terminal manager
Some checks failed
build-packages / resolve bundled mosh-client (push) Has been cancelled
build-packages / resolve bundled et-client (push) Has been cancelled
build-packages / build-macos (push) Has been cancelled
build-packages / build-windows (push) Has been cancelled
build-packages / build-linux-x64 (push) Has been cancelled
build-packages / build-linux-arm64 (push) Has been cancelled
build-packages / release (push) Has been cancelled
build-packages / update Nix release metadata (push) Has been cancelled
build-packages / bump homebrew tap (push) Has been cancelled
test / lint-and-test (push) Has been cancelled
AI automation / Route event (push) Has been cancelled
AI automation / Hand reopened issue to maintainers (push) Has been cancelled
AI automation / Clean source issue state (push) Has been cancelled
AI automation / Reconcile handoffs (push) Has been cancelled
AI automation / Classify issue (push) Has been cancelled
AI automation / Claude Code smoke (push) Has been cancelled
AI automation / Review issue follow-up (push) Has been cancelled
AI automation / Publish issue follow-up (push) Has been cancelled
AI automation / Implement with Claude Code (push) Has been cancelled
AI automation / Publish implement PR (push) Has been cancelled
AI automation / Continue queued issue comments (push) Has been cancelled
AI automation / Codex review loop (push) Has been cancelled
AI automation / Publish Codex fix (push) Has been cancelled
AI automation / Clear Codex dispatch marker (push) Has been cancelled
AI automation / Own PR re-request Codex (push) Has been cancelled
AI automation / External PR re-request Codex (push) Has been cancelled
AI automation / Poll Codex reaction / retry (push) Has been cancelled
build-et-binaries / build-linux-x64 (push) Has been cancelled
build-et-binaries / build-linux-arm64 (push) Has been cancelled
build-et-binaries / build-macos-universal (push) Has been cancelled
build-et-binaries / build-windows-x64 (push) Has been cancelled
build-et-binaries / release (push) Has been cancelled
Some checks failed
build-packages / resolve bundled mosh-client (push) Has been cancelled
build-packages / resolve bundled et-client (push) Has been cancelled
build-packages / build-macos (push) Has been cancelled
build-packages / build-windows (push) Has been cancelled
build-packages / build-linux-x64 (push) Has been cancelled
build-packages / build-linux-arm64 (push) Has been cancelled
build-packages / release (push) Has been cancelled
build-packages / update Nix release metadata (push) Has been cancelled
build-packages / bump homebrew tap (push) Has been cancelled
test / lint-and-test (push) Has been cancelled
AI automation / Route event (push) Has been cancelled
AI automation / Hand reopened issue to maintainers (push) Has been cancelled
AI automation / Clean source issue state (push) Has been cancelled
AI automation / Reconcile handoffs (push) Has been cancelled
AI automation / Classify issue (push) Has been cancelled
AI automation / Claude Code smoke (push) Has been cancelled
AI automation / Review issue follow-up (push) Has been cancelled
AI automation / Publish issue follow-up (push) Has been cancelled
AI automation / Implement with Claude Code (push) Has been cancelled
AI automation / Publish implement PR (push) Has been cancelled
AI automation / Continue queued issue comments (push) Has been cancelled
AI automation / Codex review loop (push) Has been cancelled
AI automation / Publish Codex fix (push) Has been cancelled
AI automation / Clear Codex dispatch marker (push) Has been cancelled
AI automation / Own PR re-request Codex (push) Has been cancelled
AI automation / External PR re-request Codex (push) Has been cancelled
AI automation / Poll Codex reaction / retry (push) Has been cancelled
build-et-binaries / build-linux-x64 (push) Has been cancelled
build-et-binaries / build-linux-arm64 (push) Has been cancelled
build-et-binaries / build-macos-universal (push) Has been cancelled
build-et-binaries / build-windows-x64 (push) Has been cancelled
build-et-binaries / release (push) Has been cancelled
This commit is contained in:
583
infrastructure/ai/sdkAgentAdapter.test.ts
Normal file
583
infrastructure/ai/sdkAgentAdapter.test.ts
Normal file
@@ -0,0 +1,583 @@
|
||||
import test from 'node:test';
|
||||
import assert from 'node:assert/strict';
|
||||
|
||||
import { formatSdkAgentErrorForDisplay, runSdkAgentTurn, steerSdkAgentTurn } from './sdkAgentAdapter';
|
||||
import type { SdkAgentCallbacks } from './sdkAgentAdapter';
|
||||
import type { ExternalAgentConfig } from './types';
|
||||
|
||||
function createCallbacks(errors: string[]): SdkAgentCallbacks {
|
||||
return {
|
||||
onTextDelta: () => {},
|
||||
onThinkingDelta: () => {},
|
||||
onThinkingDone: () => {},
|
||||
onToolCall: () => {},
|
||||
onToolResult: () => {},
|
||||
onError: (error) => errors.push(error),
|
||||
onDone: () => {},
|
||||
};
|
||||
}
|
||||
|
||||
const sdkConfig: ExternalAgentConfig = {
|
||||
id: 'agent',
|
||||
name: 'Agent',
|
||||
command: 'agent',
|
||||
enabled: true,
|
||||
sdkBackend: 'codex',
|
||||
};
|
||||
|
||||
test('steerSdkAgentTurn forwards the active request and preserves typed outcomes', async () => {
|
||||
const calls: unknown[][] = [];
|
||||
const result = await steerSdkAgentTurn({
|
||||
aiSdkAgentSteer: async (...args: unknown[]) => {
|
||||
calls.push(args);
|
||||
return { status: 'not-steerable', turnKind: 'review' };
|
||||
},
|
||||
}, 'request-1', 'chat-1', 'change direction', [{
|
||||
base64Data: 'aGVsbG8=',
|
||||
mediaType: 'image/png',
|
||||
filename: 'image.png',
|
||||
}], 'user-1');
|
||||
|
||||
assert.deepEqual(result, { status: 'not-steerable', turnKind: 'review' });
|
||||
assert.deepEqual(calls[0], [
|
||||
'request-1',
|
||||
'chat-1',
|
||||
'change direction',
|
||||
[{ base64Data: 'aGVsbG8=', mediaType: 'image/png', filename: 'image.png' }],
|
||||
'user-1',
|
||||
]);
|
||||
assert.deepEqual(await steerSdkAgentTurn({}, 'request-1', 'chat-1', 'text', undefined, 'user-1'), {
|
||||
status: 'unsupported',
|
||||
});
|
||||
});
|
||||
|
||||
test('formatSdkAgentErrorForDisplay preserves nested SDK agent error messages', () => {
|
||||
assert.equal(
|
||||
formatSdkAgentErrorForDisplay({
|
||||
error: {
|
||||
code: 'invalid_model',
|
||||
message: 'Model is not available',
|
||||
},
|
||||
}),
|
||||
'Model is not available',
|
||||
);
|
||||
});
|
||||
|
||||
test('formatSdkAgentErrorForDisplay stringifies unknown objects instead of [object Object]', () => {
|
||||
assert.equal(
|
||||
formatSdkAgentErrorForDisplay({ status: 502, detail: 'Proxy failed' }),
|
||||
'{"status":502,"detail":"Proxy failed"}',
|
||||
);
|
||||
});
|
||||
|
||||
test('formatSdkAgentErrorForDisplay handles circular errors', () => {
|
||||
const error: Record<string, unknown> = { status: 500 };
|
||||
error.self = error;
|
||||
|
||||
assert.equal(
|
||||
formatSdkAgentErrorForDisplay(error),
|
||||
'{"status":500,"self":"[Circular]"}',
|
||||
);
|
||||
});
|
||||
|
||||
test('runSdkAgentTurn formats structured startup errors', async () => {
|
||||
const errors: string[] = [];
|
||||
const bridge: Record<string, (...args: unknown[]) => unknown> = {
|
||||
aiSdkAgentStream: async () => ({
|
||||
ok: false,
|
||||
error: {
|
||||
error: {
|
||||
code: 'invalid_model',
|
||||
message: 'Model is not available',
|
||||
},
|
||||
},
|
||||
}),
|
||||
aiSdkAgentCancel: async () => ({ ok: true }),
|
||||
onAiSdkAgentEvent: () => () => {},
|
||||
onAiSdkAgentDone: () => () => {},
|
||||
onAiSdkAgentError: () => () => {},
|
||||
};
|
||||
|
||||
await runSdkAgentTurn(
|
||||
bridge,
|
||||
'request-1',
|
||||
'chat-1',
|
||||
sdkConfig,
|
||||
'hello',
|
||||
createCallbacks(errors),
|
||||
);
|
||||
|
||||
assert.deepEqual(errors, ['Model is not available']);
|
||||
});
|
||||
|
||||
test('runSdkAgentTurn forwards configured SDK agent environment', async () => {
|
||||
let streamArgs: unknown[] = [];
|
||||
let done: (() => void) | null = null;
|
||||
const bridge: Record<string, (...args: unknown[]) => unknown> = {
|
||||
aiSdkAgentStream: async (...args: unknown[]) => {
|
||||
streamArgs = args;
|
||||
queueMicrotask(() => done?.());
|
||||
return { ok: true };
|
||||
},
|
||||
aiSdkAgentCancel: async () => ({ ok: true }),
|
||||
onAiSdkAgentEvent: () => () => {},
|
||||
onAiSdkAgentDone: (_requestId: unknown, cb: unknown) => {
|
||||
done = cb as () => void;
|
||||
return () => {};
|
||||
},
|
||||
onAiSdkAgentError: () => () => {},
|
||||
};
|
||||
|
||||
await runSdkAgentTurn(
|
||||
bridge,
|
||||
'request-env',
|
||||
'chat-env',
|
||||
{
|
||||
...sdkConfig,
|
||||
env: { CLAUDE_CODE_EXECUTABLE: '/opt/homebrew/bin/claude' },
|
||||
},
|
||||
'hello',
|
||||
createCallbacks([]),
|
||||
);
|
||||
|
||||
assert.deepEqual(streamArgs[13], {
|
||||
CLAUDE_CODE_EXECUTABLE: '/opt/homebrew/bin/claude',
|
||||
});
|
||||
assert.equal(streamArgs[2], 'codex');
|
||||
});
|
||||
|
||||
test('runSdkAgentTurn forwards the configured agent command path', async () => {
|
||||
let streamArgs: unknown[] = [];
|
||||
let done: (() => void) | null = null;
|
||||
const bridge: Record<string, (...args: unknown[]) => unknown> = {
|
||||
aiSdkAgentStream: async (...args: unknown[]) => {
|
||||
streamArgs = args;
|
||||
queueMicrotask(() => done?.());
|
||||
return { ok: true };
|
||||
},
|
||||
aiSdkAgentCancel: async () => ({ ok: true }),
|
||||
onAiSdkAgentEvent: () => () => {},
|
||||
onAiSdkAgentDone: (_requestId: unknown, cb: unknown) => {
|
||||
done = cb as () => void;
|
||||
return () => {};
|
||||
},
|
||||
onAiSdkAgentError: () => () => {},
|
||||
};
|
||||
|
||||
await runSdkAgentTurn(
|
||||
bridge,
|
||||
'request-command',
|
||||
'chat-command',
|
||||
{
|
||||
...sdkConfig,
|
||||
command: '/opt/homebrew/bin/codex',
|
||||
commandSource: 'manual',
|
||||
},
|
||||
'hello',
|
||||
createCallbacks([]),
|
||||
);
|
||||
|
||||
assert.equal(streamArgs[14], '/opt/homebrew/bin/codex');
|
||||
});
|
||||
|
||||
test('runSdkAgentTurn does not forward auto-detected command paths', async () => {
|
||||
let streamArgs: unknown[] = [];
|
||||
let done: (() => void) | null = null;
|
||||
const bridge: Record<string, (...args: unknown[]) => unknown> = {
|
||||
aiSdkAgentStream: async (...args: unknown[]) => {
|
||||
streamArgs = args;
|
||||
queueMicrotask(() => done?.());
|
||||
return { ok: true };
|
||||
},
|
||||
aiSdkAgentCancel: async () => ({ ok: true }),
|
||||
onAiSdkAgentEvent: () => () => {},
|
||||
onAiSdkAgentDone: (_requestId: unknown, cb: unknown) => {
|
||||
done = cb as () => void;
|
||||
return () => {};
|
||||
},
|
||||
onAiSdkAgentError: () => () => {},
|
||||
};
|
||||
|
||||
await runSdkAgentTurn(
|
||||
bridge,
|
||||
'request-auto-command',
|
||||
'chat-auto-command',
|
||||
{
|
||||
...sdkConfig,
|
||||
command: '/opt/homebrew/bin/codex',
|
||||
commandSource: 'auto',
|
||||
},
|
||||
'hello',
|
||||
createCallbacks([]),
|
||||
);
|
||||
|
||||
assert.equal(streamArgs[14], undefined);
|
||||
});
|
||||
|
||||
test('runSdkAgentTurn stores SDK session ids with backend and path metadata', async () => {
|
||||
const sessionIds: string[] = [];
|
||||
let onEvent: ((event: unknown) => void) | null = null;
|
||||
let done: (() => void) | null = null;
|
||||
const bridge: Record<string, (...args: unknown[]) => unknown> = {
|
||||
aiSdkAgentStream: async () => {
|
||||
queueMicrotask(() => {
|
||||
onEvent?.({
|
||||
type: 'session-id',
|
||||
sessionId: 'thread-1',
|
||||
sdkBackend: 'codex',
|
||||
binPath: '/opt/homebrew/bin/codex',
|
||||
});
|
||||
done?.();
|
||||
});
|
||||
return { ok: true };
|
||||
},
|
||||
aiSdkAgentCancel: async () => ({ ok: true }),
|
||||
onAiSdkAgentEvent: (_requestId: unknown, cb: unknown) => {
|
||||
onEvent = cb as (event: unknown) => void;
|
||||
return () => {};
|
||||
},
|
||||
onAiSdkAgentDone: (_requestId: unknown, cb: unknown) => {
|
||||
done = cb as () => void;
|
||||
return () => {};
|
||||
},
|
||||
onAiSdkAgentError: () => () => {},
|
||||
};
|
||||
|
||||
await runSdkAgentTurn(
|
||||
bridge,
|
||||
'request-session-metadata',
|
||||
'chat-session-metadata',
|
||||
sdkConfig,
|
||||
'hello',
|
||||
{
|
||||
...createCallbacks([]),
|
||||
onSessionId: (sessionId) => sessionIds.push(sessionId),
|
||||
},
|
||||
);
|
||||
|
||||
assert.equal(sessionIds.length, 1);
|
||||
assert.match(sessionIds[0], /^netcatty-sdk-session:/);
|
||||
const payload = JSON.parse(decodeURIComponent(sessionIds[0].replace(/^netcatty-sdk-session:/, '')));
|
||||
assert.deepEqual(payload, {
|
||||
v: 1,
|
||||
id: 'thread-1',
|
||||
backend: 'codex',
|
||||
binPath: '/opt/homebrew/bin/codex',
|
||||
runtime: 'sdk',
|
||||
});
|
||||
});
|
||||
|
||||
test('runSdkAgentTurn forwards Cursor API key as agent environment', async () => {
|
||||
let streamArgs: unknown[] = [];
|
||||
let done: (() => void) | null = null;
|
||||
const bridge: Record<string, (...args: unknown[]) => unknown> = {
|
||||
aiSdkAgentStream: async (...args: unknown[]) => {
|
||||
streamArgs = args;
|
||||
queueMicrotask(() => done?.());
|
||||
return { ok: true };
|
||||
},
|
||||
aiSdkAgentCancel: async () => ({ ok: true }),
|
||||
onAiSdkAgentEvent: () => () => {},
|
||||
onAiSdkAgentDone: (_requestId: unknown, cb: unknown) => {
|
||||
done = cb as () => void;
|
||||
return () => {};
|
||||
},
|
||||
onAiSdkAgentError: () => () => {},
|
||||
};
|
||||
|
||||
await runSdkAgentTurn(
|
||||
bridge,
|
||||
'request-cursor-key',
|
||||
'chat-cursor-key',
|
||||
{
|
||||
id: 'cursor',
|
||||
name: 'Cursor',
|
||||
command: 'cursor',
|
||||
enabled: true,
|
||||
sdkBackend: 'cursor',
|
||||
apiKey: 'cur-test-key',
|
||||
},
|
||||
'hello',
|
||||
createCallbacks([]),
|
||||
);
|
||||
|
||||
assert.deepEqual(streamArgs[13], {
|
||||
NETCATTY_CURSOR_AUTH_MODE: 'api-key',
|
||||
CURSOR_API_KEY: 'cur-test-key',
|
||||
});
|
||||
assert.equal(streamArgs[2], 'cursor');
|
||||
});
|
||||
|
||||
test('runSdkAgentTurn in cursor cli-login mode does not inject CURSOR_API_KEY', async () => {
|
||||
let streamArgs: unknown[] = [];
|
||||
let done: (() => void) | null = null;
|
||||
const bridge: Record<string, (...args: unknown[]) => unknown> = {
|
||||
aiSdkAgentStream: async (...args: unknown[]) => {
|
||||
streamArgs = args;
|
||||
queueMicrotask(() => done?.());
|
||||
return { ok: true };
|
||||
},
|
||||
aiSdkAgentCancel: async () => ({ ok: true }),
|
||||
onAiSdkAgentEvent: () => () => {},
|
||||
onAiSdkAgentDone: (_requestId: unknown, cb: unknown) => {
|
||||
done = cb as () => void;
|
||||
return () => {};
|
||||
},
|
||||
onAiSdkAgentError: () => () => {},
|
||||
};
|
||||
|
||||
await runSdkAgentTurn(
|
||||
bridge,
|
||||
'request-cursor-cli',
|
||||
'chat-cursor-cli',
|
||||
{
|
||||
id: 'cursor',
|
||||
name: 'Cursor',
|
||||
command: '/Users/me/.local/bin/agent',
|
||||
enabled: true,
|
||||
sdkBackend: 'cursor',
|
||||
cursorAuthMode: 'cli-login',
|
||||
apiKey: 'should-not-inject',
|
||||
},
|
||||
'hello',
|
||||
createCallbacks([]),
|
||||
);
|
||||
|
||||
assert.deepEqual(streamArgs[13], {
|
||||
NETCATTY_CURSOR_AUTH_MODE: 'cli-login',
|
||||
NETCATTY_CURSOR_CLI_BIN: '/Users/me/.local/bin/agent',
|
||||
});
|
||||
});
|
||||
|
||||
|
||||
test('runSdkAgentTurn formats structured async error events', async () => {
|
||||
const errors: string[] = [];
|
||||
let onError: ((error: unknown) => void) | null = null;
|
||||
const bridge: Record<string, (...args: unknown[]) => unknown> = {
|
||||
aiSdkAgentStream: async () => {
|
||||
queueMicrotask(() => {
|
||||
onError?.({
|
||||
data: {
|
||||
error: {
|
||||
message: 'Proxy failed',
|
||||
},
|
||||
},
|
||||
});
|
||||
});
|
||||
return { ok: true };
|
||||
},
|
||||
aiSdkAgentCancel: async () => ({ ok: true }),
|
||||
onAiSdkAgentEvent: () => () => {},
|
||||
onAiSdkAgentDone: () => () => {},
|
||||
onAiSdkAgentError: (_requestId: unknown, cb: unknown) => {
|
||||
onError = cb as (error: unknown) => void;
|
||||
return () => {};
|
||||
},
|
||||
};
|
||||
|
||||
await runSdkAgentTurn(
|
||||
bridge,
|
||||
'request-2',
|
||||
'chat-1',
|
||||
sdkConfig,
|
||||
'hello',
|
||||
createCallbacks(errors),
|
||||
);
|
||||
|
||||
assert.deepEqual(errors, ['Proxy failed']);
|
||||
});
|
||||
|
||||
test('runSdkAgentTurn formats structured stream error events', async () => {
|
||||
const errors: string[] = [];
|
||||
let onEvent: ((event: unknown) => void) | null = null;
|
||||
const bridge: Record<string, (...args: unknown[]) => unknown> = {
|
||||
aiSdkAgentStream: async () => {
|
||||
queueMicrotask(() => {
|
||||
onEvent?.({
|
||||
type: 'error',
|
||||
error: {
|
||||
error: {
|
||||
message: 'Stream failed',
|
||||
},
|
||||
},
|
||||
});
|
||||
});
|
||||
return { ok: true };
|
||||
},
|
||||
aiSdkAgentCancel: async () => ({ ok: true }),
|
||||
onAiSdkAgentEvent: (_requestId: unknown, cb: unknown) => {
|
||||
onEvent = cb as (event: unknown) => void;
|
||||
return () => {};
|
||||
},
|
||||
onAiSdkAgentDone: () => () => {},
|
||||
onAiSdkAgentError: () => () => {},
|
||||
};
|
||||
|
||||
await runSdkAgentTurn(
|
||||
bridge,
|
||||
'request-3',
|
||||
'chat-1',
|
||||
sdkConfig,
|
||||
'hello',
|
||||
createCallbacks(errors),
|
||||
);
|
||||
|
||||
assert.deepEqual(errors, ['Stream failed']);
|
||||
});
|
||||
|
||||
test('runSdkAgentTurn forwards Codex activities and usage without treating warnings as fatal', async () => {
|
||||
const activities: unknown[] = [];
|
||||
const usages: unknown[] = [];
|
||||
const errors: string[] = [];
|
||||
let onEvent: ((event: unknown) => void) | null = null;
|
||||
let done: (() => void) | null = null;
|
||||
const bridge: Record<string, (...args: unknown[]) => unknown> = {
|
||||
aiSdkAgentStream: async () => {
|
||||
queueMicrotask(() => {
|
||||
onEvent?.({
|
||||
type: 'file-change',
|
||||
itemId: 'patch-1',
|
||||
status: 'completed',
|
||||
changes: [{ path: 'src/app.ts', kind: 'update' }],
|
||||
});
|
||||
onEvent?.({
|
||||
type: 'web-search',
|
||||
itemId: 'search-1',
|
||||
query: 'Codex SDK events',
|
||||
status: 'completed',
|
||||
});
|
||||
onEvent?.({
|
||||
type: 'plan-update',
|
||||
itemId: 'plan-1',
|
||||
items: [{ text: 'Map events', completed: true }],
|
||||
status: 'completed',
|
||||
});
|
||||
onEvent?.({ type: 'warning', itemId: 'warning-1', message: 'Search result unavailable' });
|
||||
onEvent?.({
|
||||
type: 'usage',
|
||||
inputTokens: 100,
|
||||
cachedInputTokens: 40,
|
||||
outputTokens: 25,
|
||||
reasoningTokens: 10,
|
||||
totalTokens: 125,
|
||||
});
|
||||
done?.();
|
||||
});
|
||||
return { ok: true };
|
||||
},
|
||||
aiSdkAgentCancel: async () => ({ ok: true }),
|
||||
onAiSdkAgentEvent: (_requestId: unknown, cb: unknown) => {
|
||||
onEvent = cb as (event: unknown) => void;
|
||||
return () => {};
|
||||
},
|
||||
onAiSdkAgentDone: (_requestId: unknown, cb: unknown) => {
|
||||
done = cb as () => void;
|
||||
return () => {};
|
||||
},
|
||||
onAiSdkAgentError: () => () => {},
|
||||
};
|
||||
|
||||
await runSdkAgentTurn(
|
||||
bridge,
|
||||
'request-events',
|
||||
'chat-events',
|
||||
sdkConfig,
|
||||
'hello',
|
||||
{
|
||||
...createCallbacks(errors),
|
||||
onFileChange: (activity) => activities.push(activity),
|
||||
onWebSearch: (activity) => activities.push(activity),
|
||||
onPlanUpdate: (activity) => activities.push(activity),
|
||||
onWarning: (activity) => activities.push(activity),
|
||||
onUsage: (usage) => usages.push(usage),
|
||||
},
|
||||
);
|
||||
|
||||
assert.deepEqual((activities as Array<{ type: string }>).map((activity) => activity.type), [
|
||||
'file_change',
|
||||
'web_search',
|
||||
'plan_update',
|
||||
'warning',
|
||||
]);
|
||||
assert.deepEqual(usages, [{
|
||||
inputTokens: 100,
|
||||
cachedInputTokens: 40,
|
||||
outputTokens: 25,
|
||||
reasoningTokens: 10,
|
||||
totalTokens: 125,
|
||||
estimated: false,
|
||||
}]);
|
||||
assert.deepEqual(errors, []);
|
||||
});
|
||||
|
||||
test('runSdkAgentTurn forwards CodeBuddy options, hooks, and elicitation events', async () => {
|
||||
const errors: string[] = [];
|
||||
const hooks: unknown[] = [];
|
||||
const elicitations: unknown[] = [];
|
||||
let streamArgs: unknown[] = [];
|
||||
let onEvent: ((event: unknown) => void) | null = null;
|
||||
let done: (() => void) | null = null;
|
||||
const bridge: Record<string, (...args: unknown[]) => unknown> = {
|
||||
aiSdkAgentStream: async (...args: unknown[]) => {
|
||||
streamArgs = args;
|
||||
queueMicrotask(() => {
|
||||
onEvent?.({ type: 'hook', hookEvent: 'PreToolUse', toolName: 'Bash' });
|
||||
onEvent?.({
|
||||
type: 'elicitation-create',
|
||||
elicitationId: 'el-1',
|
||||
request: { message: 'Confirm?' },
|
||||
});
|
||||
onEvent?.({
|
||||
type: 'elicitation-complete',
|
||||
notification: { elicitationId: 'el-1' },
|
||||
});
|
||||
done?.();
|
||||
});
|
||||
return { ok: true };
|
||||
},
|
||||
aiSdkAgentCancel: async () => ({ ok: true }),
|
||||
onAiSdkAgentEvent: (_requestId: unknown, cb: unknown) => {
|
||||
onEvent = cb as (event: unknown) => void;
|
||||
return () => {};
|
||||
},
|
||||
onAiSdkAgentDone: (_requestId: unknown, cb: unknown) => {
|
||||
done = cb as () => void;
|
||||
return () => {};
|
||||
},
|
||||
onAiSdkAgentError: () => () => {},
|
||||
};
|
||||
const codebuddyConfig: ExternalAgentConfig = {
|
||||
id: 'codebuddy',
|
||||
name: 'CodeBuddy',
|
||||
command: 'codebuddy',
|
||||
enabled: true,
|
||||
sdkBackend: 'codebuddy',
|
||||
codebuddyOptions: { effort: 'high' },
|
||||
};
|
||||
|
||||
await runSdkAgentTurn(
|
||||
bridge,
|
||||
'request-codebuddy',
|
||||
'chat-codebuddy',
|
||||
codebuddyConfig,
|
||||
'hello',
|
||||
{
|
||||
...createCallbacks(errors),
|
||||
onHook: (hookEvent, payload) => hooks.push({ hookEvent, payload }),
|
||||
onElicitationCreate: (elicitationId, request) => {
|
||||
elicitations.push({ elicitationId, request });
|
||||
},
|
||||
onElicitationComplete: (notification) => {
|
||||
elicitations.push({ notification });
|
||||
},
|
||||
},
|
||||
);
|
||||
|
||||
assert.deepEqual(streamArgs[17], { effort: 'high' });
|
||||
assert.equal((hooks[0] as { hookEvent: string }).hookEvent, 'PreToolUse');
|
||||
assert.deepEqual(elicitations, [
|
||||
{ elicitationId: 'el-1', request: { message: 'Confirm?' } },
|
||||
{ notification: { elicitationId: 'el-1' } },
|
||||
]);
|
||||
assert.deepEqual(errors, []);
|
||||
});
|
||||
Reference in New Issue
Block a user