opencodeの終了判定が誤っていたので修正

This commit is contained in:
nrslib 2026-02-11 12:54:18 +09:00
parent ccca0949ae
commit 2a678f3a75
2 changed files with 55 additions and 45 deletions

View File

@ -31,45 +31,6 @@ class MockEventStream implements AsyncGenerator<unknown, void, unknown> {
} }
} }
class HangingAfterEventsStream implements AsyncGenerator<unknown, void, unknown> {
private index = 0;
private closed = false;
private pendingResolve: ((value: IteratorResult<unknown, void>) => void) | undefined;
readonly returnSpy = vi.fn(async () => {
this.closed = true;
this.pendingResolve?.({ done: true, value: undefined });
return { done: true as const, value: undefined };
});
constructor(private readonly events: unknown[]) {}
[Symbol.asyncIterator](): AsyncGenerator<unknown, void, unknown> {
return this;
}
async next(): Promise<IteratorResult<unknown, void>> {
if (this.closed) {
return { done: true, value: undefined };
}
if (this.index < this.events.length) {
const value = this.events[this.index];
this.index += 1;
return { done: false, value };
}
return new Promise<IteratorResult<unknown, void>>((resolve) => {
this.pendingResolve = resolve;
});
}
async return(): Promise<IteratorResult<unknown, void>> {
return this.returnSpy();
}
async throw(e?: unknown): Promise<IteratorResult<unknown, void>> {
throw e;
}
}
const { createOpencodeMock } = vi.hoisted(() => ({ const { createOpencodeMock } = vi.hoisted(() => ({
createOpencodeMock: vi.fn(), createOpencodeMock: vi.fn(),
})); }));
@ -188,9 +149,9 @@ describe('OpenCodeClient stream cleanup', () => {
); );
}); });
it('should complete without hanging when assistant message is completed', async () => { it('should continue after assistant message completed and finish on session.idle', async () => {
const { OpenCodeClient } = await import('../infra/opencode/client.js'); const { OpenCodeClient } = await import('../infra/opencode/client.js');
const stream = new HangingAfterEventsStream([ const stream = new MockEventStream([
{ {
type: 'message.part.updated', type: 'message.part.updated',
properties: { properties: {
@ -208,6 +169,17 @@ describe('OpenCodeClient stream cleanup', () => {
}, },
}, },
}, },
{
type: 'message.part.updated',
properties: {
part: { id: 'p-1', type: 'text', text: 'done more' },
delta: ' more',
},
},
{
type: 'session.idle',
properties: { sessionID: 'session-3' },
},
]); ]);
const promptAsync = vi.fn().mockResolvedValue(undefined); const promptAsync = vi.fn().mockResolvedValue(undefined);
@ -235,7 +207,7 @@ describe('OpenCodeClient stream cleanup', () => {
]); ]);
expect(result.status).toBe('done'); expect(result.status).toBe('done');
expect(result.content).toBe('done'); expect(result.content).toBe('done more');
expect(disposeInstance).toHaveBeenCalledWith( expect(disposeInstance).toHaveBeenCalledWith(
{ directory: '/tmp' }, { directory: '/tmp' },
expect.objectContaining({ signal: expect.any(AbortSignal) }), expect.objectContaining({ signal: expect.any(AbortSignal) }),

View File

@ -455,13 +455,51 @@ export class OpenCodeClient {
}; };
const info = messageProps.info; const info = messageProps.info;
const isCurrentAssistantMessage = info?.sessionID === sessionId && info.role === 'assistant'; const isCurrentAssistantMessage = info?.sessionID === sessionId && info.role === 'assistant';
const isCompleted = typeof info?.time?.completed === 'number'; if (isCurrentAssistantMessage) {
if (isCurrentAssistantMessage && isCompleted) { const streamError = extractOpenCodeErrorMessage(info?.error);
const streamError = extractOpenCodeErrorMessage(info.error);
if (streamError) { if (streamError) {
success = false; success = false;
failureMessage = streamError; failureMessage = streamError;
break;
} }
}
continue;
}
if (sseEvent.type === 'message.completed') {
const completedProps = sseEvent.properties as {
info?: {
sessionID?: string;
role?: 'assistant' | 'user';
error?: unknown;
};
};
const info = completedProps.info;
const isCurrentAssistantMessage = info?.sessionID === sessionId && info.role === 'assistant';
if (isCurrentAssistantMessage) {
const streamError = extractOpenCodeErrorMessage(info?.error);
if (streamError) {
success = false;
failureMessage = streamError;
break;
}
}
continue;
}
if (sseEvent.type === 'message.failed') {
const failedProps = sseEvent.properties as {
info?: {
sessionID?: string;
role?: 'assistant' | 'user';
error?: unknown;
};
};
const info = failedProps.info;
const isCurrentAssistantMessage = info?.sessionID === sessionId && info.role === 'assistant';
if (isCurrentAssistantMessage) {
success = false;
failureMessage = extractOpenCodeErrorMessage(info?.error) ?? 'OpenCode message failed';
break; break;
} }
continue; continue;