@jskit-ai/assistant-core 0.2.1 → 0.2.2

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.
@@ -1,6 +1,12 @@
1
1
  import assert from "node:assert/strict";
2
2
  import test from "node:test";
3
+ import path from "node:path";
4
+ import { randomUUID } from "node:crypto";
3
5
  import { nativeConversationStoragePolicy } from "../src/server/conversation/nativeStorage.js";
6
+ import { createCodexAppServerProviderOwner } from "../src/server/conversation/codexProviderOwner.js";
7
+ import { createCodexConversationDriver } from "../src/server/conversation/providers/codexDriver.js";
8
+ import { createOpenCodeSharedRuntime, openCodeServerForDirectory } from "../src/server/conversation/openCodeRuntime.js";
9
+ import { createOpenCodeConversationDriver } from "../src/server/conversation/providers/opencodeDriver.js";
4
10
 
5
11
  test("native storage policy distinguishes locked deletion from host writer exclusion", () => {
6
12
  assert.equal(nativeConversationStoragePolicy("codex").requiresWriterExclusion, false);
@@ -23,3 +29,224 @@ test("native writer recognition handles executables and node entry points withou
23
29
  assert.ok(opencode.matchesProcess({ executable: "/usr/bin/bun", commandLine: "bun\0/tools/opencode\0serve\0" }));
24
30
  assert.equal(opencode.matchesProcess({ executable: "/tools/opencode-helper", commandLine: "node\0/notes/opencode.txt\0" }), false);
25
31
  });
32
+
33
+ async function eventually(check, message) {
34
+ const deadline = Date.now() + 2_000;
35
+ while (!check()) {
36
+ if (Date.now() > deadline) assert.fail(message);
37
+ await new Promise(resolve => setTimeout(resolve, 5));
38
+ }
39
+ }
40
+
41
+ // One shared native process with independent provider connections, as the
42
+ // real Codex app-server has per account and runtime scope.
43
+ function codexStorageFixture(t, { idleMs = 20, listModelsMs = 0 } = {}) {
44
+ // Owned runtimes and sharing are process-wide, so each fixture has its own.
45
+ const runtimeDir = `/tmp/jskit-codex-storage-runtime-${randomUUID()}`;
46
+ const runtime = { running: false, starts: 0, stops: 0, reads: [] };
47
+ const owner = createCodexAppServerProviderOwner({ transientIdleMs: idleMs, providerFactory: () => ({
48
+ isHelperProvider: () => false,
49
+ isAvailable: () => runtime.running,
50
+ async currentRuntimeInfo() { return { runtimeDir }; },
51
+ async listModels() {
52
+ runtime.reads.push("catalogue");
53
+ await new Promise(resolve => setTimeout(resolve, listModelsMs));
54
+ return { running: runtime.running, data: [{ id: "gpt" }] };
55
+ },
56
+ async readGoal() { runtime.reads.push("goal"); return { goal: null }; },
57
+ async readPlanUsage() { runtime.reads.push("usage"); return { status: "available", windows: [] }; },
58
+ async ensureAvailable() {
59
+ if (!runtime.running) { runtime.running = true; runtime.starts += 1; }
60
+ return { ok: true, runtime: { runtimeDir } };
61
+ },
62
+ async listNativeThreadsForCwd(cwd) { return [`thread-in-${path.basename(cwd)}`]; },
63
+ async stopRuntime() { runtime.running = false; runtime.stops += 1; return { stopped: true }; },
64
+ close() {}
65
+ }) });
66
+ t.after(() => owner.beginShutdown());
67
+ const context = sessionId => ({
68
+ sessionId, providerKey: `provider:${sessionId}`, providerOptions: { runtimeDir },
69
+ keyFields: key => ({ sessionId: key }), assertAdmission() {}, closing: false,
70
+ runOwner: { runtimeLifecycle: { rememberManagedSession() {}, managedThreadId: () => "" } },
71
+ async prepare() {
72
+ return { parameters: { runtimeDir }, owner: { sessionKey: sessionId }, resources: null,
73
+ controls: { available: false, subscribe: () => () => {} }, runInContext: operation => operation() };
74
+ }
75
+ });
76
+ const driver = createCodexConversationDriver({ host: {} });
77
+ const list = sessionId => driver.listConversationStorage({
78
+ sessionId, context: {},
79
+ binding: { engineId: "codex", modelProviderId: "openai", workdir: `/archived/${sessionId}` },
80
+ native: { providerOwner: owner, errorPrefix: "", preparation: {
81
+ async storage(id) { return { providerContext: context(id), toolHomeSource: "/home/person" }; }
82
+ } }
83
+ });
84
+ return { owner, runtime, context, list };
85
+ }
86
+
87
+ test("a native storage listing leaves no Codex runtime or provider running", async t => {
88
+ const { owner, runtime, list } = codexStorageFixture(t);
89
+ assert.deepEqual((await list("archived-1")).map(row => row.conversationId), ["thread-in-archived-1"]);
90
+ assert.equal(runtime.running, true, "the listing itself uses the shared process");
91
+ await eventually(() => !runtime.running, "an unused storage listing kept Codex running");
92
+ assert.equal(runtime.stops, 1);
93
+ assert.deepEqual([...owner.providers.keys()], []);
94
+ });
95
+
96
+ test("consecutive native storage reads share one Codex process and stop it after the last", async t => {
97
+ const { owner, runtime, list } = codexStorageFixture(t, { idleMs: 40 });
98
+ await list("archived-1");
99
+ await list("archived-2");
100
+ await list("archived-1");
101
+ assert.equal(runtime.starts, 1);
102
+ await eventually(() => runtime.stops === 1 && owner.providers.get("provider:archived-2") === undefined,
103
+ "the last storage read did not release Codex");
104
+ assert.equal(runtime.running, false);
105
+ assert.deepEqual([...owner.providers.keys()], []);
106
+ });
107
+
108
+ test("native storage reads never stop a Codex process a conversation still uses", async t => {
109
+ const { owner, runtime, context, list } = codexStorageFixture(t);
110
+ const conversation = await owner.ensureSession(context("open-session"));
111
+ await list("archived-1");
112
+ // A conversation that claims a provider a storage read created keeps it too.
113
+ await list("claimed-session");
114
+ await owner.ensureSession(context("claimed-session"));
115
+ await eventually(() => owner.providers.get("provider:archived-1") === undefined, "the storage provider was not released");
116
+ await new Promise(resolve => setTimeout(resolve, 60));
117
+ assert.equal(runtime.running, true);
118
+ assert.equal(runtime.stops, 0);
119
+ assert.equal(owner.providers.get("provider:open-session"), conversation);
120
+ assert.ok(owner.providers.get("provider:claimed-session"));
121
+ });
122
+
123
+ // One shared `opencode serve`, as OpenCode runs per runtime scope.
124
+ function openCodeStorageFixture({ idleMs = 20 } = {}) {
125
+ const owner = createOpenCodeSharedRuntime({ storageIdleMs: idleMs });
126
+ const server = { running: false, starts: 0, stops: 0 };
127
+ const connection = { modelProviderId: "provider", canonicalUrl: "https://provider.invalid", fingerprint: "account", endpointCode: "api" };
128
+ const create = async () => {
129
+ server.running = true;
130
+ server.starts += 1;
131
+ return {
132
+ client: {
133
+ async health() { return { healthy: true }; },
134
+ async listConversationsForDirectory(directory) { return [{ id: `conversation-in-${path.basename(directory)}` }]; },
135
+ forDirectory(directory) { return { ...this, directory }; }
136
+ },
137
+ async stop() {
138
+ server.running = false;
139
+ server.stops += 1;
140
+ return { exited: true };
141
+ }
142
+ };
143
+ };
144
+ const native = { owner, preparation: { storage: () => ({
145
+ process: async () => ({ create, options: {}, connections: [connection] }),
146
+ failure: error => { throw error; }
147
+ }) } };
148
+ const driver = createOpenCodeConversationDriver({ connections: { resolve() {} }, host: { workdir: "/tmp" } });
149
+ const list = archived => driver.listConversationStorage({ native, context: {},
150
+ binding: { engineId: "opencode", modelProviderId: "provider", workdir: `/work/${archived}` } });
151
+ const conversation = key => owner.acquire(key, async () => {
152
+ const shared = await owner.ensure(connection, async () => ({ server: await create(), connections: [connection] }));
153
+ return { key, workdir: `/work/${key}`, abortController: new AbortController(),
154
+ server: openCodeServerForDirectory(shared.server, `/work/${key}`) };
155
+ });
156
+ return { owner, server, list, conversation };
157
+ }
158
+
159
+ test("an OpenCode storage listing leaves no OpenCode server running", async () => {
160
+ const { server, list } = openCodeStorageFixture();
161
+ assert.deepEqual((await list("archived-1")).map(row => row.conversationId), ["conversation-in-archived-1"]);
162
+ assert.equal(server.running, true, "the listing itself uses the shared server");
163
+ await eventually(() => !server.running, "an unused storage listing kept opencode serve running");
164
+ assert.equal(server.stops, 1);
165
+ });
166
+
167
+ test("consecutive OpenCode storage reads share one server and stop it after the last", async () => {
168
+ const { server, list } = openCodeStorageFixture({ idleMs: 40 });
169
+ await list("archived-1");
170
+ await list("archived-2");
171
+ assert.equal(server.starts, 1);
172
+ await eventually(() => server.stops === 1, "the last storage read did not stop opencode serve");
173
+ assert.equal(server.running, false);
174
+ });
175
+
176
+ test("OpenCode storage reads never stop a server a conversation still uses", async () => {
177
+ const { owner, server, list, conversation } = openCodeStorageFixture();
178
+ const open = await conversation("open-session");
179
+ await list("archived-1");
180
+ await new Promise(resolve => setTimeout(resolve, 60));
181
+ assert.equal(server.running, true);
182
+ assert.equal(server.stops, 0);
183
+ await owner.release(open);
184
+ assert.equal(server.running, false);
185
+ assert.equal(server.starts, 1);
186
+ });
187
+
188
+ test("a catalogue read on a storage-started Codex process finishes before its release stops it", async t => {
189
+ const { owner, runtime, list } = codexStorageFixture(t, { listModelsMs: 60 });
190
+ await list("archived-1");
191
+ // The release timer fires during this read; the shared lifecycle holds it back.
192
+ const catalogue = await owner.readModelCatalog({
193
+ prepareProviderOptions: async () => ({ runtimeDir: owner.ownedRuntimes.values().next().value.runtimeDir }),
194
+ providerFactory: () => assert.fail("The running storage process should serve the catalogue.")
195
+ });
196
+ assert.equal(catalogue.running, true, "the release stopped Codex during the catalogue read");
197
+ await eventually(() => !runtime.running, "the storage process was not released after the catalogue read");
198
+ assert.equal(runtime.starts, 1);
199
+ assert.equal(runtime.stops, 1);
200
+ });
201
+
202
+ test("goal and plan-usage reads never use a provider a storage read borrowed", async t => {
203
+ const { owner, runtime, list } = codexStorageFixture(t, { idleMs: 200 });
204
+ await list("archived-1");
205
+ assert.deepEqual(await owner.readSessionGoal("archived-1", "thread-1"), { status: "unavailable", goal: null });
206
+ assert.deepEqual(await owner.readPlanUsage("archived-1"), { status: "unavailable", windows: [] });
207
+ assert.deepEqual(runtime.reads, []);
208
+ });
209
+
210
+ test("real use that reaches a storage-created provider directly keeps it", async t => {
211
+ const { owner, runtime, context, list } = codexStorageFixture(t);
212
+ for (const use of [
213
+ // Helper account checks read the provider without ensureSession.
214
+ id => owner.withLifecycle(() => owner.providerForSession(context(id))),
215
+ id => owner.acquireRuntime({ providerKey: `provider:${id}`, provider: owner.providers.get(`provider:${id}`),
216
+ providerOptions: context(id).providerOptions, operation: async () => {} }),
217
+ id => owner.createProvider({ providerKey: `provider:${id}`, providerOptions: context(id).providerOptions })
218
+ ]) {
219
+ const id = `archived-${randomUUID()}`;
220
+ await list(id);
221
+ const provider = owner.providers.get(`provider:${id}`);
222
+ await use(id);
223
+ await new Promise(resolve => setTimeout(resolve, 60));
224
+ assert.equal(owner.providers.get(`provider:${id}`), provider);
225
+ }
226
+ assert.equal(runtime.running, true);
227
+ assert.equal(runtime.stops, 0);
228
+ });
229
+
230
+ test("a storage read on a managed main thread's key leaves it to that thread", async t => {
231
+ const { owner, runtime, context, list } = codexStorageFixture(t);
232
+ const managed = context("archived-1");
233
+ managed.runOwner.runtimeLifecycle.managedThreadId = () => "main-thread";
234
+ await owner.withTransientSession(managed, async () => {});
235
+ await new Promise(resolve => setTimeout(resolve, 60));
236
+ assert.ok(owner.providers.get("provider:archived-1"));
237
+ assert.equal(runtime.stops, 0);
238
+ await list("archived-2");
239
+ await eventually(() => owner.providers.get("provider:archived-2") === undefined, "the borrowed provider was not released");
240
+ assert.equal(runtime.running, true, "a managed main thread still shares the process");
241
+ });
242
+
243
+ test("a conversation that starts after an OpenCode storage read keeps the server", async () => {
244
+ const { owner, server, list, conversation } = openCodeStorageFixture({ idleMs: 40 });
245
+ await list("archived-1");
246
+ const open = await conversation("open-session");
247
+ await new Promise(resolve => setTimeout(resolve, 80));
248
+ assert.equal(server.running, true, "the storage release stopped a server a conversation uses");
249
+ assert.equal(server.starts, 1);
250
+ await owner.release(open);
251
+ assert.equal(server.running, false);
252
+ });