fix(chat): pin existing-session sends to captured target (#2424)
* fix(chat): pin sends to captured session * fix(chat): handle runtime cancellation consistently
This commit is contained in:
@@ -6,6 +6,7 @@ type ConfigResponse = { data: Record<string, unknown> };
|
||||
|
||||
const configResolvers: Array<(response: ConfigResponse) => void> = [];
|
||||
let configCalls = 0;
|
||||
let runtimeKey = 'test-runtime';
|
||||
const promptAsyncCalls: unknown[][] = [];
|
||||
const promptAsyncResults: Array<unknown> = [];
|
||||
|
||||
@@ -44,7 +45,7 @@ mock.module('@/lib/runtime-url', () => ({
|
||||
|
||||
mock.module('@/lib/runtime-switch', () => ({
|
||||
getRuntimeApiBaseUrl: mock(() => ''),
|
||||
getRuntimeKey: mock(() => 'test-runtime'),
|
||||
getRuntimeKey: mock(() => runtimeKey),
|
||||
}));
|
||||
|
||||
mock.module('@/lib/runtime-fetch', () => ({
|
||||
@@ -60,6 +61,7 @@ mock.module('@/lib/startupTrace', () => ({
|
||||
const { opencodeClient } = await import(`./client?cache-test=${Date.now()}`);
|
||||
|
||||
beforeEach(() => {
|
||||
runtimeKey = 'test-runtime';
|
||||
promptAsyncCalls.length = 0;
|
||||
promptAsyncResults.length = 0;
|
||||
});
|
||||
@@ -160,4 +162,34 @@ describe('opencodeClient prompt retry behavior', () => {
|
||||
expect(promptAsyncCalls.length).toBe(1);
|
||||
expect(error instanceof Error ? error.message : String(error)).toContain('Failed to send message (503)');
|
||||
});
|
||||
|
||||
test('does not dispatch after the runtime changes while preparing attachments', async () => {
|
||||
runtimeKey = 'runtime-a';
|
||||
const pending = opencodeClient.sendMessage({
|
||||
id: 'ses_runtime_race',
|
||||
providerID: 'runtime-race-provider',
|
||||
modelID: 'model-a',
|
||||
text: 'hello',
|
||||
runtimeKey: 'runtime-a',
|
||||
files: [{
|
||||
type: 'file',
|
||||
mime: 'text/markdown',
|
||||
filename: 'notes.md',
|
||||
url: 'data:text/markdown,hello',
|
||||
}],
|
||||
});
|
||||
|
||||
runtimeKey = 'runtime-b';
|
||||
|
||||
let error: unknown = null;
|
||||
try {
|
||||
await pending;
|
||||
} catch (caught) {
|
||||
error = caught;
|
||||
}
|
||||
|
||||
expect(error).toBeInstanceOf(Error);
|
||||
expect(error instanceof Error ? error.message : String(error)).toContain('runtime changed');
|
||||
expect(promptAsyncCalls).toHaveLength(0);
|
||||
});
|
||||
});
|
||||
|
||||
@@ -269,6 +269,12 @@ class OpencodeService {
|
||||
this.client = createRuntimeOpencodeClient({ baseUrl: this.baseUrl });
|
||||
}
|
||||
|
||||
private assertRuntimeUnchanged(runtimeKey?: string): void {
|
||||
if (runtimeKey && runtimeKey !== getRuntimeKey()) {
|
||||
throw new Error('Message was not sent because the runtime changed.');
|
||||
}
|
||||
}
|
||||
|
||||
getBaseUrl(): string {
|
||||
return this.baseUrl;
|
||||
}
|
||||
@@ -744,6 +750,7 @@ class OpencodeService {
|
||||
}
|
||||
|
||||
async sendMessage(params: {
|
||||
runtimeKey?: string;
|
||||
id: string;
|
||||
providerID: string;
|
||||
modelID: string;
|
||||
@@ -769,6 +776,8 @@ class OpencodeService {
|
||||
};
|
||||
directory?: string | null;
|
||||
}): Promise<string> {
|
||||
this.assertRuntimeUnchanged(params.runtimeKey);
|
||||
|
||||
// Use the optimistic/client-generated ID as the real user message ID so SSE
|
||||
// can reconcile the echoed server message in-place.
|
||||
const messageId = params.messageId ?? ascendingId("msg");
|
||||
@@ -852,6 +861,7 @@ class OpencodeService {
|
||||
}
|
||||
|
||||
assertProviderCircuitClosed(params.providerID);
|
||||
this.assertRuntimeUnchanged(params.runtimeKey);
|
||||
|
||||
let response: Response;
|
||||
|
||||
@@ -918,6 +928,7 @@ class OpencodeService {
|
||||
}
|
||||
|
||||
async sendCommand(params: {
|
||||
runtimeKey?: string;
|
||||
id: string;
|
||||
providerID: string;
|
||||
modelID: string;
|
||||
@@ -929,6 +940,8 @@ class OpencodeService {
|
||||
messageId?: string;
|
||||
directory?: string | null;
|
||||
}): Promise<string> {
|
||||
this.assertRuntimeUnchanged(params.runtimeKey);
|
||||
|
||||
const tempMessageId = params.messageId ?? ascendingId("msg");
|
||||
|
||||
const parts: FilePartInput[] = [];
|
||||
@@ -939,6 +952,7 @@ class OpencodeService {
|
||||
}
|
||||
|
||||
const requestDirectory = this.normalizeCandidatePath(params.directory ?? null) ?? this.currentDirectory;
|
||||
this.assertRuntimeUnchanged(params.runtimeKey);
|
||||
|
||||
const response = await this.client.session.command({
|
||||
sessionID: params.id,
|
||||
@@ -968,6 +982,7 @@ class OpencodeService {
|
||||
}
|
||||
|
||||
async shellSession(params: {
|
||||
runtimeKey?: string;
|
||||
sessionId: string;
|
||||
command: string;
|
||||
agent: string;
|
||||
@@ -975,6 +990,7 @@ class OpencodeService {
|
||||
messageId?: string;
|
||||
directory?: string | null;
|
||||
}): Promise<{ info: Message; parts: Part[] }> {
|
||||
this.assertRuntimeUnchanged(params.runtimeKey);
|
||||
const requestDirectory = this.normalizeCandidatePath(params.directory ?? null) ?? this.currentDirectory;
|
||||
const response = await this.client.session.shell({
|
||||
sessionID: params.sessionId,
|
||||
|
||||
Reference in New Issue
Block a user