feat(worker): ClaudeCodeAdapter runs claude -p stream-json in the per-user box
This commit is contained in:
@@ -0,0 +1,110 @@
|
||||
import { describe, expect, test, vi } from 'vitest';
|
||||
|
||||
import type { NormalizedAgentEvent } from '../../src/agent-events';
|
||||
import type { ExecStreamFn } from '../../src/runtime/agent-runtime';
|
||||
import type { AdapterWorkspace, Claim } from '../../src/runtime/provider';
|
||||
|
||||
const loadAdapter = async () => {
|
||||
process.env.SPOON_WORKER_TOKEN = 'test-worker-token';
|
||||
process.env.GITHUB_APP_ID = '123';
|
||||
process.env.GITHUB_APP_PRIVATE_KEY =
|
||||
'-----BEGIN PRIVATE KEY-----\\ntest\\n-----END PRIVATE KEY-----';
|
||||
return await import('../../src/runtime/claude-adapter');
|
||||
};
|
||||
|
||||
const claim = {
|
||||
job: {
|
||||
_id: 'job-1',
|
||||
prompt: 'hi',
|
||||
baseBranch: 'main',
|
||||
workBranch: 'spoon/x',
|
||||
forkOwner: 'o',
|
||||
forkRepo: 'r',
|
||||
upstreamOwner: 'u',
|
||||
upstreamRepo: 'r',
|
||||
},
|
||||
spoon: { name: 'Spoon' },
|
||||
openai: { model: 'claude-sonnet-4', reasoningEffort: 'medium' },
|
||||
aiProviderProfile: {
|
||||
id: 'p',
|
||||
name: 'Anthropic',
|
||||
provider: 'anthropic',
|
||||
authType: 'api_key',
|
||||
secret: 'sk-ant-test',
|
||||
model: 'claude-sonnet-4',
|
||||
reasoningEffort: 'medium',
|
||||
},
|
||||
github: {},
|
||||
secrets: [],
|
||||
} as unknown as Claim;
|
||||
|
||||
const makeWorkspace = (): AdapterWorkspace => ({
|
||||
claim,
|
||||
workdir: '/tmp/spoon-claude-adapter-test-missing',
|
||||
homeDir: '/tmp/spoon-claude-adapter-test-missing',
|
||||
username: 'tester',
|
||||
containerHome: '/home/tester',
|
||||
containerRepo: '/home/tester/Code/spoon',
|
||||
repoDir: '/tmp/spoon-claude-adapter-test-missing/Code/spoon',
|
||||
boxName: 'spoon-box-tester',
|
||||
redact: (value: string) => value,
|
||||
runtime: 'claude',
|
||||
});
|
||||
|
||||
describe('ClaudeCodeAdapter', () => {
|
||||
test('streams normalized events and marks the turn via buildMarkedCommand', async () => {
|
||||
const { createClaudeAdapter } = await loadAdapter();
|
||||
const captured: unknown[] = [];
|
||||
const execStream = vi.fn(async (args) => {
|
||||
captured.push(args.command);
|
||||
await args.onStdoutLine?.(
|
||||
JSON.stringify({
|
||||
type: 'system',
|
||||
subtype: 'init',
|
||||
session_id: 'abc',
|
||||
}),
|
||||
);
|
||||
await args.onStdoutLine?.(
|
||||
JSON.stringify({
|
||||
type: 'assistant',
|
||||
message: { content: [{ type: 'text', text: 'hi there' }] },
|
||||
}),
|
||||
);
|
||||
await args.onStdoutLine?.(
|
||||
JSON.stringify({
|
||||
type: 'result',
|
||||
subtype: 'success',
|
||||
result: 'final text',
|
||||
session_id: 'abc',
|
||||
}),
|
||||
);
|
||||
return { exitCode: 0, output: '' };
|
||||
}) as unknown as ExecStreamFn;
|
||||
|
||||
const adapter = createClaudeAdapter({ execStream });
|
||||
const events: NormalizedAgentEvent[] = [];
|
||||
const onEvent = (event: NormalizedAgentEvent): Promise<void> => {
|
||||
events.push(event);
|
||||
return Promise.resolve();
|
||||
};
|
||||
|
||||
const result = await adapter.runTurn(makeWorkspace(), 'do it', onEvent);
|
||||
|
||||
expect(events).toContainEqual({ kind: 'session', sessionId: 'abc' });
|
||||
expect(events).toContainEqual({
|
||||
kind: 'assistant_delta',
|
||||
content: 'hi there',
|
||||
});
|
||||
expect(result.finalMessage).toBe('final text');
|
||||
expect(result.sessionId).toBe('abc');
|
||||
expect(result.error).toBeUndefined();
|
||||
|
||||
// The turn ran through buildMarkedCommand (setsid bash session) carrying the
|
||||
// `claude -p` argv in the script.
|
||||
const command = captured[0] as string[];
|
||||
expect(command[0]).toBe('setsid');
|
||||
expect(command[1]).toBe('bash');
|
||||
expect(command[3]).toContain("'claude' '-p'");
|
||||
expect(command[3]).toContain("'--output-format' 'stream-json'");
|
||||
});
|
||||
});
|
||||
Reference in New Issue
Block a user