takt/e2e/specs/opencode-conversation.e2e.ts
nrslib 67f6fc685c fix: opencodeの2ターン目ハングを修正し会話継続を実現
streamAbortController.signalをcreateOpencodeに渡していたため、
各callのfinallyでabortするとサーバーが停止し2ターン目がハングしていた。
signalをサーバー起動から除外し、sessionIdの引き継ぎを復元することで
複数ターンの会話継続を実現した。
2026-02-20 12:40:17 +09:00

100 lines
3.1 KiB
TypeScript
Raw Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

/**
* OpenCode real E2E conversation test.
*
* Tests the full stack with a real OpenCode server:
* OpenCodeProvider → callOpenCode → OpenCodeClient → createOpencode (real server)
*
* Skipped automatically if the opencode binary is not found.
* Run with: npm run test:e2e:opencode
*/
import { describe, it, expect, afterAll } from 'vitest';
import { execSync } from 'node:child_process';
import { resetSharedServer } from '../../src/infra/opencode/client.js';
import { OpenCodeProvider } from '../../src/infra/providers/opencode.js';
function isOpencodeAvailable(): boolean {
try {
execSync('which opencode', { stdio: 'ignore' });
return true;
} catch {
return false;
}
}
const MODEL = process.env.OPENCODE_E2E_MODEL ?? 'minimax/MiniMax-M2.5-highspeed';
const enabled = isOpencodeAvailable();
describe.skipIf(!enabled)('OpenCode real E2E conversation', () => {
afterAll(() => {
resetSharedServer();
});
it('should complete a two-turn conversation with sessionId inheritance', async () => {
const provider = new OpenCodeProvider();
const agent = provider.setup({
name: 'coder',
systemPrompt: 'You are a concise assistant. Keep all responses under 20 words.',
});
// 1ターン目
const result1 = await agent.call('Say only the word "apple".', {
cwd: process.cwd(),
model: MODEL,
});
expect(result1.status).toBe('done');
expect(result1.sessionId).toBeDefined();
// 2ターン目: sessionId を引き継いで送るconversationLoop と同じ)
const result2 = await agent.call('What fruit did I ask you about?', {
cwd: process.cwd(),
model: MODEL,
sessionId: result1.sessionId,
});
expect(result2.status).toBe('done');
// 同じセッションを再利用している
expect(result2.sessionId).toBe(result1.sessionId);
// 会話が引き継がれていれば "apple" に言及するはず
expect(result2.content.toLowerCase()).toContain('apple');
}, 120_000);
it('should complete a three-turn conversation without hanging', async () => {
const provider = new OpenCodeProvider();
const agent = provider.setup({
name: 'coder',
systemPrompt: 'You are a concise assistant. Keep all responses under 20 words.',
});
const results = [];
let prevSessionId: string | undefined;
const prompts = [
'Remember the number 42.',
'What number did I ask you to remember?',
'Double that number.',
];
for (const prompt of prompts) {
const result = await agent.call(prompt, {
cwd: process.cwd(),
model: MODEL,
sessionId: prevSessionId,
});
expect(result.status).toBe('done');
results.push(result);
prevSessionId = result.sessionId;
}
// すべてのターンが同じセッションを使っている
expect(results[1].sessionId).toBe(results[0].sessionId);
expect(results[2].sessionId).toBe(results[0].sessionId);
// 会話が引き継がれている
expect(results[1].content).toMatch(/42/);
expect(results[2].content).toMatch(/84/);
}, 180_000);
});