@jskit-ai/assistant-core 0.2.15 → 0.2.17
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/examples/conversation/package.json +5 -5
- package/package.json +5 -5
- package/src/server/conversation/claudeNativeTurn.js +9 -1
- package/src/server/conversation/claudeSessionCleanup.js +9 -1
- package/src/server/conversation/claudeTurn.js +1 -0
- package/src/server/conversation/codexConversations.js +65 -6
- package/src/server/conversation/codexRunJournal.js +33 -1
- package/src/server/conversation/codexRuntimeLifecycle.js +20 -1
- package/src/server/conversation/codexTurn.js +41 -4
- package/src/server/conversation/codexTurnEndings.js +9 -6
- package/src/server/conversation/nativeConversationLifecycle.js +22 -0
- package/src/server/conversation/openCodeProcessOwner.js +23 -6
- package/src/server/conversation/openCodeRuntime.js +1 -0
- package/src/server/conversation/openCodeSessionTurns.js +10 -3
- package/src/server/conversation/openCodeTemporaryConversations.js +10 -3
- package/src/server/conversation/openCodeTurn.js +15 -3
- package/src/server/conversation/openCodeTurnMonitor.js +11 -2
- package/src/server/conversation/providers/claudeDriver.js +5 -0
- package/src/server/conversation/providers/codexDriver.js +5 -0
- package/src/server/conversation/providers/opencodeDriver.js +5 -0
- package/src/server/conversation/runtime.js +1 -0
- package/test/claudeLifecycle.test.js +28 -0
- package/test/codexConversationsCheckpoint.test.js +2 -0
- package/test/codexLifecycleIsolation.test.js +2 -0
- package/test/codexShutdownStop.test.js +168 -0
- package/test/codexTemporaryProviderRelease.test.js +2 -0
- package/test/codexTurn.test.js +57 -0
- package/test/conversationSession.test.js +29 -0
- package/test/openCodeLifecycle.test.js +338 -0
|
@@ -0,0 +1,338 @@
|
|
|
1
|
+
import assert from "node:assert/strict";
|
|
2
|
+
import test from "node:test";
|
|
3
|
+
import { createOpenCodeSharedRuntime } from "../src/server/conversation/openCodeRuntime.js";
|
|
4
|
+
import { observeOpenCodeTurnCompletion, openCodeMessageError } from "../src/server/conversation/openCodeTurn.js";
|
|
5
|
+
|
|
6
|
+
const SESSION_ID = "session-a";
|
|
7
|
+
const KEY = `project\0${SESSION_ID}`;
|
|
8
|
+
const THREAD_ID = "ses_native";
|
|
9
|
+
const INPUT_ID = "msg_input";
|
|
10
|
+
const CONNECTION = { modelProviderId: "provider", canonicalUrl: "https://provider.invalid", fingerprint: "account", endpointCode: "api" };
|
|
11
|
+
const SELECTION = { agentId: "build", modelId: "model", modelProviderId: "provider", variantId: "" };
|
|
12
|
+
const ABORTED = "The operation was aborted.";
|
|
13
|
+
const PROVIDER_FAILURE = "The provider refused this request.";
|
|
14
|
+
|
|
15
|
+
// OpenCode's native history outlives each server that runs it. The turn's reply
|
|
16
|
+
// is still being written until an interrupt ends it as aborted, its provider
|
|
17
|
+
// fails it, or it completes on its own.
|
|
18
|
+
function nativeOpenCode() {
|
|
19
|
+
const rows = [
|
|
20
|
+
{ id: INPUT_ID, type: "user", time: { created: 1 } },
|
|
21
|
+
{ id: "msg_reply", type: "assistant", text: "", time: { created: 2 } }
|
|
22
|
+
];
|
|
23
|
+
let busy = true;
|
|
24
|
+
const calls = { interrupts: [], starts: 0, stops: 0 };
|
|
25
|
+
function create() {
|
|
26
|
+
calls.starts += 1;
|
|
27
|
+
const client = {
|
|
28
|
+
async health() { return { healthy: true }; },
|
|
29
|
+
async prepareDirectory() {},
|
|
30
|
+
async readSession(id) { return { id }; },
|
|
31
|
+
async switchModel() {},
|
|
32
|
+
async switchAgent() {},
|
|
33
|
+
async messages() { return rows; },
|
|
34
|
+
async sessionStatus() { return { type: busy ? "busy" : "idle" }; },
|
|
35
|
+
async interrupt(id) {
|
|
36
|
+
calls.interrupts.push(id);
|
|
37
|
+
if (busy) rows[1] = { ...rows[1], error: { name: "MessageAbortedError", data: { message: ABORTED } }, time: { created: 2, completed: 3 } };
|
|
38
|
+
busy = false;
|
|
39
|
+
return true;
|
|
40
|
+
},
|
|
41
|
+
forDirectory() { return client; }
|
|
42
|
+
};
|
|
43
|
+
return { client, async stop() { calls.stops += 1; return { exited: true }; } };
|
|
44
|
+
}
|
|
45
|
+
function fail() {
|
|
46
|
+
rows[1] = { ...rows[1], error: { name: "APIError", data: { message: PROVIDER_FAILURE } }, time: { created: 2, completed: 3 } };
|
|
47
|
+
busy = false;
|
|
48
|
+
}
|
|
49
|
+
function complete() {
|
|
50
|
+
rows[1] = { ...rows[1], text: "Done.", finish: "stop", time: { created: 2, completed: 3 } };
|
|
51
|
+
busy = false;
|
|
52
|
+
}
|
|
53
|
+
return { calls, create, fail, complete };
|
|
54
|
+
}
|
|
55
|
+
|
|
56
|
+
// One server with Vibe64's run rules: the session record keeps the OpenCode run
|
|
57
|
+
// with the shutdown's mark, and a turn's end first runs the host's checkpoint, a
|
|
58
|
+
// Git snapshot there.
|
|
59
|
+
function server(native, saved, { turnEndCheckpoint = async () => {} } = {}) {
|
|
60
|
+
const runtime = createOpenCodeSharedRuntime({});
|
|
61
|
+
const calls = { checkpoints: [], writes: [] };
|
|
62
|
+
const active = Promise.withResolvers();
|
|
63
|
+
async function writeRun(turn, state, error = "") {
|
|
64
|
+
const running = state === "starting" || state === "active";
|
|
65
|
+
if (!running && turn.id) {
|
|
66
|
+
calls.checkpoints.push(`${turn.id}:${state}`);
|
|
67
|
+
await turnEndCheckpoint();
|
|
68
|
+
}
|
|
69
|
+
calls.writes.push(state);
|
|
70
|
+
saved.run = { active: running, state, error, observationError: turn.observationError || "",
|
|
71
|
+
stoppedByShutdown: turn.stoppedByShutdown === true, threadId: turn.threadId, turnId: turn.id, startedAt: turn.startedAt };
|
|
72
|
+
if (state === "active") active.resolve();
|
|
73
|
+
}
|
|
74
|
+
const projection = {
|
|
75
|
+
messageId: (id, role) => `${id}:${role}`, readError: openCodeMessageError, reasoning: async () => {},
|
|
76
|
+
store: {
|
|
77
|
+
async writeConversationAssistantMessage(_sessionId, message) { return { assistant: message }; },
|
|
78
|
+
updateConversationStream() {}, completeConversationStreamMessage() {}
|
|
79
|
+
},
|
|
80
|
+
publishTurn: async () => {}, publishStream: async () => {}
|
|
81
|
+
};
|
|
82
|
+
const monitor = {
|
|
83
|
+
prepare() { return { fields: {} }; },
|
|
84
|
+
create() {
|
|
85
|
+
return {
|
|
86
|
+
observe: () => ({ readFailure: () => null, close: async () => {} }),
|
|
87
|
+
finalResponse: { readError: openCodeMessageError },
|
|
88
|
+
writeRun,
|
|
89
|
+
projectMessages: (messages, options) => runtime.writeConversationProjection(SESSION_ID, messages, options, projection),
|
|
90
|
+
completeResult: async (_turn, { failure }) => failure,
|
|
91
|
+
onRetired() {}
|
|
92
|
+
};
|
|
93
|
+
},
|
|
94
|
+
onError() {}
|
|
95
|
+
};
|
|
96
|
+
const failure = error => { throw error; };
|
|
97
|
+
const release = {
|
|
98
|
+
ownsTarget: () => true, async beforeRelease() {}, async onRemoved() {}, async closeTerminals() {}, async afterRelease() {}, failure
|
|
99
|
+
};
|
|
100
|
+
const processPreparation = {
|
|
101
|
+
key: KEY, connection: CONNECTION, failure, async onFailure() {},
|
|
102
|
+
async configure() {
|
|
103
|
+
return {
|
|
104
|
+
target: { sessionId: SESSION_ID, upstreamSessionId: THREAD_ID, workdir: "/work/a", projectContextRoot: "/work",
|
|
105
|
+
connection: CONNECTION, selection: SELECTION, helperModelId: "" },
|
|
106
|
+
async prepareServer() { return { create: async () => native.create(), options: {}, connections: [CONNECTION] }; }
|
|
107
|
+
};
|
|
108
|
+
}
|
|
109
|
+
};
|
|
110
|
+
const session = { selection: SELECTION, model: { id: SELECTION.modelId, providerID: SELECTION.modelProviderId },
|
|
111
|
+
identity: { async write() {}, async publish() {} } };
|
|
112
|
+
return {
|
|
113
|
+
runtime, calls, release,
|
|
114
|
+
shutdown: () => runtime.invalidateRuntimes({ reason: "server-shutdown" }, { release }),
|
|
115
|
+
// The server's shutdown begins: nothing stops yet.
|
|
116
|
+
begin: () => runtime.beginShutdown(),
|
|
117
|
+
async running() {
|
|
118
|
+
const target = await runtime.acquirePrepared(processPreparation);
|
|
119
|
+
void runtime.beginMessageMonitor(KEY, target, { id: INPUT_ID, eventStartedAt: 1, startedAt: "2026-11-05T00:00:00.000Z" }, {}, monitor);
|
|
120
|
+
await active.promise;
|
|
121
|
+
return target;
|
|
122
|
+
},
|
|
123
|
+
stop: () => runtime.interruptPreparedTurn({ key: KEY, threadId: THREAD_ID, writeRun, failure }),
|
|
124
|
+
// A temporary conversation's turn, observed as the application's detached chats are.
|
|
125
|
+
async temporary(conversationId) {
|
|
126
|
+
const target = await runtime.acquirePrepared(processPreparation);
|
|
127
|
+
const closes = [];
|
|
128
|
+
const entry = { active: true, target, conversationId, inputMessageId: INPUT_ID, abortController: new AbortController() };
|
|
129
|
+
runtime.temporaryConversations.set(`${KEY}\0${conversationId}`, entry);
|
|
130
|
+
entry.completion = observeOpenCodeTurnCompletion(runtime, { ...target, upstreamSessionId: conversationId }, conversationId, entry, {
|
|
131
|
+
events: { async close() {} },
|
|
132
|
+
signal: AbortSignal.any([target.abortController.signal, entry.abortController.signal]),
|
|
133
|
+
afterClose: closed => { closes.push(closed); }
|
|
134
|
+
});
|
|
135
|
+
return { entry, closes };
|
|
136
|
+
},
|
|
137
|
+
// A later read of that conversation, as the application reads a saved chat.
|
|
138
|
+
async readTemporary(conversationId, stoppedByShutdown) {
|
|
139
|
+
const target = await runtime.acquirePrepared(processPreparation);
|
|
140
|
+
return runtime.readPersistentConversation(target, conversationId, null, {
|
|
141
|
+
readError: openCodeMessageError, messageId: (id, part) => `${id}:${part}`, inputMessageId: "", stoppedByShutdown
|
|
142
|
+
});
|
|
143
|
+
},
|
|
144
|
+
// Opening the chat: its readiness recovers a run the previous server left.
|
|
145
|
+
ready: () => runtime.ensurePreparedSessionReadiness({
|
|
146
|
+
key: KEY, selection: SELECTION, workdir: "/work/a", changed: () => new Error("The session changed."),
|
|
147
|
+
connection: async () => CONNECTION, refresh: async () => ({ key: KEY, workdir: "/work/a", selection: SELECTION }),
|
|
148
|
+
prepare: async operation => ({
|
|
149
|
+
value: await operation({ key: KEY, configuration: () => ({ process: async () => processPreparation, session }) })
|
|
150
|
+
}),
|
|
151
|
+
observation: {
|
|
152
|
+
get run() { return saved.run; },
|
|
153
|
+
async write(operation) { await operation(saved.run, apply => apply(writeRun)); }
|
|
154
|
+
}
|
|
155
|
+
}),
|
|
156
|
+
async recover() {
|
|
157
|
+
const result = await runtime.reconcileSessions([{ sessionId: SESSION_ID }], {}, {
|
|
158
|
+
session: async () => ({
|
|
159
|
+
key: KEY, activeRun: saved.run?.active ? saved.run : undefined, failure,
|
|
160
|
+
process: async () => processPreparation, session: () => session,
|
|
161
|
+
observation: { writeRun, monitor, fallbackTurnId: "msg_fallback" }
|
|
162
|
+
})
|
|
163
|
+
});
|
|
164
|
+
await runtime.monitors.get(KEY);
|
|
165
|
+
return result;
|
|
166
|
+
}
|
|
167
|
+
};
|
|
168
|
+
}
|
|
169
|
+
|
|
170
|
+
function runOf(saved) {
|
|
171
|
+
const { active, state, error, stoppedByShutdown, turnId } = saved.run;
|
|
172
|
+
return { active, state, error, stoppedByShutdown, turnId };
|
|
173
|
+
}
|
|
174
|
+
|
|
175
|
+
// Shutdown with a host checkpoint that can take longer than the server has to stop.
|
|
176
|
+
async function shutdownRunning(native, saved) {
|
|
177
|
+
const checkpoint = Promise.withResolvers();
|
|
178
|
+
const first = server(native, saved, { turnEndCheckpoint: () => checkpoint.promise });
|
|
179
|
+
await first.running();
|
|
180
|
+
let timer;
|
|
181
|
+
const result = await Promise.race([
|
|
182
|
+
first.shutdown(),
|
|
183
|
+
new Promise((_, reject) => { timer = setTimeout(() => reject(new Error("Shutdown waited for the turn's checkpoint")), 1_000); })
|
|
184
|
+
]).finally(() => {
|
|
185
|
+
clearTimeout(timer);
|
|
186
|
+
checkpoint.resolve();
|
|
187
|
+
});
|
|
188
|
+
assert.equal(result.ok, true, JSON.stringify(result));
|
|
189
|
+
assert.deepEqual(native.calls.interrupts, [THREAD_ID], "shutdown stopped the native turn");
|
|
190
|
+
assert.equal(native.calls.stops, 1, "shutdown proved the OpenCode process exit");
|
|
191
|
+
assert.deepEqual(first.calls.checkpoints, [], "shutdown ran no checkpoint");
|
|
192
|
+
assert.deepEqual(first.calls.writes, ["active", "active"], "shutdown only marked the saved run");
|
|
193
|
+
assert.deepEqual(runOf(saved), { active: true, state: "active", error: "", stoppedByShutdown: true, turnId: INPUT_ID });
|
|
194
|
+
}
|
|
195
|
+
|
|
196
|
+
test("a goal's recovery on the next server records an OpenCode turn stopped by shutdown as interrupted, with one checkpoint", { timeout: 5_000 }, async t => {
|
|
197
|
+
const native = nativeOpenCode();
|
|
198
|
+
const saved = { run: null };
|
|
199
|
+
await shutdownRunning(native, saved);
|
|
200
|
+
const next = server(native, saved);
|
|
201
|
+
t.after(() => next.shutdown());
|
|
202
|
+
const recovered = await next.recover();
|
|
203
|
+
assert.deepEqual(recovered.results, [{ ok: true, resumed: false, sessionId: SESSION_ID }]);
|
|
204
|
+
assert.deepEqual(next.calls.checkpoints, [`${INPUT_ID}:interrupted`]);
|
|
205
|
+
assert.deepEqual(runOf(saved), { active: false, state: "interrupted", error: "", stoppedByShutdown: true, turnId: INPUT_ID });
|
|
206
|
+
});
|
|
207
|
+
|
|
208
|
+
test("opening the chat on the next server records an OpenCode turn stopped by shutdown as interrupted, with one checkpoint", { timeout: 5_000 }, async t => {
|
|
209
|
+
const native = nativeOpenCode();
|
|
210
|
+
const saved = { run: null };
|
|
211
|
+
await shutdownRunning(native, saved);
|
|
212
|
+
const next = server(native, saved);
|
|
213
|
+
t.after(() => next.shutdown());
|
|
214
|
+
assert.equal((await next.ready()).ok, true);
|
|
215
|
+
assert.deepEqual(next.calls.checkpoints, [`${INPUT_ID}:interrupted`]);
|
|
216
|
+
assert.deepEqual(runOf(saved), { active: false, state: "interrupted", error: "", stoppedByShutdown: true, turnId: INPUT_ID });
|
|
217
|
+
assert.equal((await next.ready()).ok, true);
|
|
218
|
+
assert.deepEqual(next.calls.checkpoints, [`${INPUT_ID}:interrupted`], "a second opening changes nothing");
|
|
219
|
+
});
|
|
220
|
+
|
|
221
|
+
test("a genuine OpenCode failure still records failed, and a native abort without the shutdown's mark is no interruption", { timeout: 5_000 }, async t => {
|
|
222
|
+
const failing = nativeOpenCode();
|
|
223
|
+
const failed = { run: null };
|
|
224
|
+
const running = server(failing, failed);
|
|
225
|
+
t.after(() => running.shutdown());
|
|
226
|
+
await running.running();
|
|
227
|
+
failing.fail();
|
|
228
|
+
await running.runtime.monitors.get(KEY);
|
|
229
|
+
assert.deepEqual(running.calls.checkpoints, [`${INPUT_ID}:failed`]);
|
|
230
|
+
assert.equal(failed.run.state, "failed");
|
|
231
|
+
assert.match(failed.run.error, /provider refused/u);
|
|
232
|
+
|
|
233
|
+
// A run left active without the mark (a crash, say) whose native turn was aborted.
|
|
234
|
+
const native = nativeOpenCode();
|
|
235
|
+
const saved = { run: { active: true, state: "active", error: "", observationError: "", stoppedByShutdown: false,
|
|
236
|
+
threadId: THREAD_ID, turnId: INPUT_ID, startedAt: "2026-11-05T00:00:00.000Z" } };
|
|
237
|
+
await native.create().client.interrupt(THREAD_ID);
|
|
238
|
+
const next = server(native, saved);
|
|
239
|
+
t.after(() => next.shutdown());
|
|
240
|
+
assert.deepEqual((await next.recover()).results, [{ ok: true, resumed: true, sessionId: SESSION_ID }]);
|
|
241
|
+
assert.deepEqual(next.calls.checkpoints, [`${INPUT_ID}:failed`]);
|
|
242
|
+
assert.deepEqual(runOf(saved), { active: false, state: "failed", error: ABORTED, stoppedByShutdown: false, turnId: INPUT_ID });
|
|
243
|
+
});
|
|
244
|
+
|
|
245
|
+
test("OpenCode Stop and session close still end a running turn through the host's checkpoint", async t => {
|
|
246
|
+
const stopped = { run: null };
|
|
247
|
+
const stopping = server(nativeOpenCode(), stopped);
|
|
248
|
+
t.after(() => stopping.shutdown());
|
|
249
|
+
await stopping.running();
|
|
250
|
+
assert.equal((await stopping.stop()).interrupted, true);
|
|
251
|
+
assert.deepEqual(stopping.calls.checkpoints, [`${INPUT_ID}:interrupted`]);
|
|
252
|
+
assert.deepEqual({ active: stopped.run.active, state: stopped.run.state }, { active: false, state: "interrupted" });
|
|
253
|
+
|
|
254
|
+
const closed = { run: null };
|
|
255
|
+
const closing = server(nativeOpenCode(), closed);
|
|
256
|
+
t.after(() => closing.shutdown());
|
|
257
|
+
await closing.running();
|
|
258
|
+
assert.equal((await closing.runtime.closeSession(SESSION_ID, {}, closing.release)).ok, true);
|
|
259
|
+
assert.deepEqual(closing.calls.checkpoints, [`${INPUT_ID}:cancelled`]);
|
|
260
|
+
assert.deepEqual({ active: closed.run.active, state: closed.run.state }, { active: false, state: "cancelled" });
|
|
261
|
+
});
|
|
262
|
+
|
|
263
|
+
test("OpenCode shutdown ends a temporary conversation's turn as interrupted, and only the shutdown's mark reads it so on the next server", { timeout: 5_000 }, async t => {
|
|
264
|
+
const native = nativeOpenCode();
|
|
265
|
+
const first = server(native, { run: null });
|
|
266
|
+
const { entry, closes } = await first.temporary("ses_temp");
|
|
267
|
+
assert.equal((await first.shutdown()).ok, true);
|
|
268
|
+
assert.deepEqual(await entry.completion, { interrupted: true });
|
|
269
|
+
assert.deepEqual(native.calls.interrupts, ["ses_temp"], "shutdown stopped the native turn");
|
|
270
|
+
assert.deepEqual(closes, [{ stopped: true, outcome: "interrupted", stoppedByShutdown: true }],
|
|
271
|
+
"the application is told to leave the turn's checkpoint to the next server");
|
|
272
|
+
|
|
273
|
+
const next = server(native, { run: null });
|
|
274
|
+
t.after(() => next.shutdown());
|
|
275
|
+
const ended = [];
|
|
276
|
+
const read = mark => next.readTemporary("ses_temp", mark).then(({ status, error, runId }) => ({ status, error, runId }));
|
|
277
|
+
assert.deepEqual(await read({ runId: INPUT_ID, async ended() { ended.push(INPUT_ID); } }),
|
|
278
|
+
{ status: "interrupted", error: "", runId: INPUT_ID });
|
|
279
|
+
assert.deepEqual(ended, [INPUT_ID], "recording the stopped run lets the application make its checkpoint");
|
|
280
|
+
assert.deepEqual(await read(undefined), { status: "failed", error: ABORTED, runId: INPUT_ID });
|
|
281
|
+
assert.deepEqual(await read({ runId: "msg_other", async ended() { ended.push("msg_other"); } }),
|
|
282
|
+
{ status: "failed", error: ABORTED, runId: INPUT_ID });
|
|
283
|
+
assert.deepEqual(ended, [INPUT_ID]);
|
|
284
|
+
|
|
285
|
+
// Outside shutdown a session close still ends the turn now, through the application's checkpoint.
|
|
286
|
+
const closing = server(nativeOpenCode(), { run: null });
|
|
287
|
+
t.after(() => closing.shutdown());
|
|
288
|
+
const closed = await closing.temporary("ses_closed");
|
|
289
|
+
assert.equal((await closing.runtime.closeSession(SESSION_ID, {}, closing.release)).ok, true);
|
|
290
|
+
await closed.entry.completion.catch(() => null);
|
|
291
|
+
assert.deepEqual(closed.closes, [{ stopped: true, outcome: "failed", stoppedByShutdown: false }]);
|
|
292
|
+
});
|
|
293
|
+
|
|
294
|
+
test("once OpenCode's shutdown has begun, a main turn that ends on its own is left to the next server, which records it with one checkpoint", { timeout: 5_000 }, async t => {
|
|
295
|
+
const native = nativeOpenCode();
|
|
296
|
+
const saved = { run: null };
|
|
297
|
+
const first = server(native, saved);
|
|
298
|
+
t.after(() => first.shutdown());
|
|
299
|
+
await first.running();
|
|
300
|
+
first.begin();
|
|
301
|
+
assert.deepEqual({ interrupts: native.calls.interrupts, stops: native.calls.stops }, { interrupts: [], stops: 0 },
|
|
302
|
+
"beginning the shutdown stopped nothing");
|
|
303
|
+
native.complete();
|
|
304
|
+
await first.runtime.monitors.get(KEY);
|
|
305
|
+
assert.deepEqual(first.calls.checkpoints, [], "the turn's end made no checkpoint");
|
|
306
|
+
assert.deepEqual(runOf(saved), { active: true, state: "active", error: "", stoppedByShutdown: true, turnId: INPUT_ID });
|
|
307
|
+
assert.equal((await first.shutdown()).ok, true);
|
|
308
|
+
assert.equal(native.calls.stops, 1, "the full stop still proved the OpenCode process exit");
|
|
309
|
+
assert.deepEqual(first.calls.checkpoints, []);
|
|
310
|
+
|
|
311
|
+
const next = server(native, saved);
|
|
312
|
+
t.after(() => next.shutdown());
|
|
313
|
+
assert.equal((await next.ready()).ok, true);
|
|
314
|
+
assert.equal((await next.ready()).ok, true);
|
|
315
|
+
assert.deepEqual(next.calls.checkpoints, [`${INPUT_ID}:interrupted`], "the next server made the checkpoint once");
|
|
316
|
+
assert.deepEqual(runOf(saved), { active: false, state: "interrupted", error: "", stoppedByShutdown: true, turnId: INPUT_ID });
|
|
317
|
+
});
|
|
318
|
+
|
|
319
|
+
test("once OpenCode's shutdown has begun, a temporary conversation's turn that ends on its own leaves its checkpoint to the next server", { timeout: 5_000 }, async t => {
|
|
320
|
+
const native = nativeOpenCode();
|
|
321
|
+
const first = server(native, { run: null });
|
|
322
|
+
t.after(() => first.shutdown());
|
|
323
|
+
const { entry, closes } = await first.temporary("ses_temp");
|
|
324
|
+
first.begin();
|
|
325
|
+
native.complete();
|
|
326
|
+
assert.deepEqual(await entry.completion, { interrupted: true });
|
|
327
|
+
assert.deepEqual(native.calls.interrupts, [], "nothing interrupted the native turn");
|
|
328
|
+
assert.deepEqual(closes, [{ stopped: true, outcome: "interrupted", stoppedByShutdown: true }],
|
|
329
|
+
"the application is told to leave the turn's checkpoint to the next server");
|
|
330
|
+
assert.equal((await first.shutdown()).ok, true);
|
|
331
|
+
|
|
332
|
+
const next = server(native, { run: null });
|
|
333
|
+
t.after(() => next.shutdown());
|
|
334
|
+
const ended = [];
|
|
335
|
+
const read = await next.readTemporary("ses_temp", { runId: INPUT_ID, async ended() { ended.push(INPUT_ID); } });
|
|
336
|
+
assert.deepEqual({ status: read.status, error: read.error }, { status: "interrupted", error: "" });
|
|
337
|
+
assert.deepEqual(ended, [INPUT_ID], "recording the run lets the application make its checkpoint");
|
|
338
|
+
});
|