@opencode-trace/plugin 0.0.3 → 0.0.5
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/README.md +4 -11
- package/dist/__tests__/tracer.test.d.ts +2 -0
- package/dist/__tests__/tracer.test.d.ts.map +1 -0
- package/dist/__tests__/tracer.test.js +276 -0
- package/dist/__tests__/tracer.test.js.map +1 -0
- package/dist/integration.test.js +508 -11
- package/dist/integration.test.js.map +1 -1
- package/dist/plugin-instance.d.ts +33 -9
- package/dist/plugin-instance.d.ts.map +1 -1
- package/dist/plugin-instance.js +256 -65
- package/dist/plugin-instance.js.map +1 -1
- package/dist/plugin-instance.test.js +672 -25
- package/dist/plugin-instance.test.js.map +1 -1
- package/dist/trace.d.ts.map +1 -1
- package/dist/trace.js +212 -25
- package/dist/trace.js.map +1 -1
- package/dist/trace.test.js +741 -16
- package/dist/trace.test.js.map +1 -1
- package/dist/tracer.d.ts +20 -0
- package/dist/tracer.d.ts.map +1 -0
- package/dist/tracer.js +12 -0
- package/dist/tracer.js.map +1 -0
- package/dist/write-queue.d.ts +27 -2
- package/dist/write-queue.d.ts.map +1 -1
- package/dist/write-queue.js +99 -14
- package/dist/write-queue.js.map +1 -1
- package/dist/write-queue.test.js +373 -6
- package/dist/write-queue.test.js.map +1 -1
- package/package.json +11 -4
- package/dist/state-queue.d.ts +0 -14
- package/dist/state-queue.d.ts.map +0 -1
- package/dist/state-queue.js +0 -44
- package/dist/state-queue.js.map +0 -1
- package/dist/state-queue.test.d.ts +0 -2
- package/dist/state-queue.test.d.ts.map +0 -1
- package/dist/state-queue.test.js +0 -99
- package/dist/state-queue.test.js.map +0 -1
|
@@ -1,8 +1,9 @@
|
|
|
1
|
-
import { describe, test, expect, beforeEach, afterEach } from "vitest";
|
|
1
|
+
import { describe, test, expect, beforeEach, afterEach, vi } from "vitest";
|
|
2
2
|
import { TracePlugin } from "./plugin-instance.js";
|
|
3
|
-
import { mkdtempSync, rmSync, readFileSync, existsSync } from "node:fs";
|
|
3
|
+
import { mkdtempSync, rmSync, readFileSync, existsSync, writeFileSync, readdirSync, statSync, } from "node:fs";
|
|
4
4
|
import { tmpdir, homedir } from "node:os";
|
|
5
5
|
import { join } from "node:path";
|
|
6
|
+
import { logger } from "@opencode-trace/core";
|
|
6
7
|
async function waitForFile(filePath, timeoutMs = 5000) {
|
|
7
8
|
const startTime = Date.now();
|
|
8
9
|
while (true) {
|
|
@@ -14,13 +15,12 @@ async function waitForFile(filePath, timeoutMs = 5000) {
|
|
|
14
15
|
return;
|
|
15
16
|
}
|
|
16
17
|
}
|
|
17
|
-
catch {
|
|
18
|
-
}
|
|
18
|
+
catch { }
|
|
19
19
|
}
|
|
20
20
|
if (Date.now() - startTime > timeoutMs) {
|
|
21
21
|
throw new Error(`Timeout waiting for valid file ${filePath} after ${timeoutMs}ms`);
|
|
22
22
|
}
|
|
23
|
-
await new Promise(r => setTimeout(r, 10));
|
|
23
|
+
await new Promise((r) => setTimeout(r, 10));
|
|
24
24
|
}
|
|
25
25
|
}
|
|
26
26
|
describe("TracePlugin", () => {
|
|
@@ -28,16 +28,15 @@ describe("TracePlugin", () => {
|
|
|
28
28
|
let plugin;
|
|
29
29
|
beforeEach(() => {
|
|
30
30
|
tempDir = mkdtempSync(join(tmpdir(), "plugin-test-"));
|
|
31
|
-
plugin = new TracePlugin(tempDir);
|
|
31
|
+
plugin = new TracePlugin({ globalDir: tempDir, localDir: tempDir });
|
|
32
32
|
});
|
|
33
33
|
afterEach(() => {
|
|
34
34
|
plugin.uninstallInterceptor();
|
|
35
35
|
rmSync(tempDir, { recursive: true, force: true });
|
|
36
36
|
});
|
|
37
|
-
test("constructor initializes write
|
|
37
|
+
test("constructor initializes write queue", () => {
|
|
38
38
|
expect(plugin).toBeDefined();
|
|
39
39
|
expect(plugin["writeQueue"]).toBeDefined();
|
|
40
|
-
expect(plugin["stateQueue"]).toBeDefined();
|
|
41
40
|
});
|
|
42
41
|
test("installInterceptor installs traced fetch", () => {
|
|
43
42
|
const originalFetch = globalThis.fetch;
|
|
@@ -78,7 +77,7 @@ describe("TracePlugin", () => {
|
|
|
78
77
|
const mockFetch = async () => {
|
|
79
78
|
return new Response(JSON.stringify({ result: "ok" }), {
|
|
80
79
|
status: 200,
|
|
81
|
-
headers: { "content-type": "application/json" }
|
|
80
|
+
headers: { "content-type": "application/json" },
|
|
82
81
|
});
|
|
83
82
|
};
|
|
84
83
|
globalThis.fetch = mockFetch;
|
|
@@ -88,9 +87,9 @@ describe("TracePlugin", () => {
|
|
|
88
87
|
method: "POST",
|
|
89
88
|
headers: {
|
|
90
89
|
"x-opencode-session": sessionId,
|
|
91
|
-
"content-type": "application/json"
|
|
90
|
+
"content-type": "application/json",
|
|
92
91
|
},
|
|
93
|
-
body: JSON.stringify({ test: true })
|
|
92
|
+
body: JSON.stringify({ test: true }),
|
|
94
93
|
});
|
|
95
94
|
const response = await plugin.tracedFetch(request);
|
|
96
95
|
const filePath = join(tempDir, sessionId, "1.json");
|
|
@@ -107,14 +106,14 @@ describe("TracePlugin", () => {
|
|
|
107
106
|
Connection to 192.168.1.100:8080 failed
|
|
108
107
|
Server running on 127.0.0.1:3000`;
|
|
109
108
|
const sanitized = sanitizeStackTrace(stack);
|
|
110
|
-
expect(sanitized).toContain(
|
|
111
|
-
expect(sanitized).toContain(
|
|
112
|
-
expect(sanitized).toContain(
|
|
109
|
+
expect(sanitized).toContain("[HOME]");
|
|
110
|
+
expect(sanitized).toContain("[IP]");
|
|
111
|
+
expect(sanitized).toContain(":[PORT]");
|
|
113
112
|
expect(sanitized).not.toContain(userHome);
|
|
114
|
-
expect(sanitized).not.toContain(
|
|
115
|
-
expect(sanitized).not.toContain(
|
|
116
|
-
expect(sanitized).not.toContain(
|
|
117
|
-
expect(sanitized).not.toContain(
|
|
113
|
+
expect(sanitized).not.toContain("192.168.1.100");
|
|
114
|
+
expect(sanitized).not.toContain("127.0.0.1");
|
|
115
|
+
expect(sanitized).not.toContain(":8080");
|
|
116
|
+
expect(sanitized).not.toContain(":3000");
|
|
118
117
|
});
|
|
119
118
|
test("sanitizeStackTrace redacts ports in Windows paths", () => {
|
|
120
119
|
const sanitizeStackTrace = plugin["sanitizeStackTrace"];
|
|
@@ -123,13 +122,661 @@ Server running on 127.0.0.1:3000`;
|
|
|
123
122
|
Connection to 10.0.0.1:8080 failed
|
|
124
123
|
Listening on 0.0.0.0:3000`;
|
|
125
124
|
const sanitized = sanitizeStackTrace(windowsStack);
|
|
126
|
-
expect(sanitized).toContain(
|
|
127
|
-
expect(sanitized).toContain(
|
|
128
|
-
expect(sanitized).toContain(
|
|
129
|
-
expect(sanitized).not.toContain(
|
|
130
|
-
expect(sanitized).not.toContain(
|
|
131
|
-
expect(sanitized).not.toContain(
|
|
132
|
-
expect(sanitized).not.toContain(
|
|
125
|
+
expect(sanitized).toContain("[HOME]");
|
|
126
|
+
expect(sanitized).toContain("[IP]");
|
|
127
|
+
expect(sanitized).toContain(":[PORT]");
|
|
128
|
+
expect(sanitized).not.toContain("10.0.0.1");
|
|
129
|
+
expect(sanitized).not.toContain("0.0.0.0");
|
|
130
|
+
expect(sanitized).not.toContain(":8080");
|
|
131
|
+
expect(sanitized).not.toContain(":3000");
|
|
132
|
+
});
|
|
133
|
+
});
|
|
134
|
+
describe("TracePlugin - constructor & init", () => {
|
|
135
|
+
let globalDir;
|
|
136
|
+
let localDir;
|
|
137
|
+
beforeEach(() => {
|
|
138
|
+
globalDir = mkdtempSync(join(tmpdir(), "plugin-global-"));
|
|
139
|
+
localDir = mkdtempSync(join(tmpdir(), "plugin-local-"));
|
|
140
|
+
});
|
|
141
|
+
afterEach(() => {
|
|
142
|
+
rmSync(globalDir, { recursive: true, force: true });
|
|
143
|
+
rmSync(localDir, { recursive: true, force: true });
|
|
144
|
+
});
|
|
145
|
+
test("throws TypeError when localDir is missing", () => {
|
|
146
|
+
expect(() => new TracePlugin({ globalDir })).toThrow(TypeError);
|
|
147
|
+
expect(() => new TracePlugin({})).toThrow(/localDir is required/);
|
|
148
|
+
});
|
|
149
|
+
test("defaults globalDir to ~/.opencode-trace when omitted", () => {
|
|
150
|
+
const p = new TracePlugin({ localDir });
|
|
151
|
+
const status = p.getScopeStatus();
|
|
152
|
+
expect(status.globalDir).toBe(join(homedir(), ".opencode-trace"));
|
|
153
|
+
expect(status.localDir).toBe(localDir);
|
|
154
|
+
});
|
|
155
|
+
test("initStateManager creates both config managers and config files", async () => {
|
|
156
|
+
const p = new TracePlugin({ globalDir, localDir });
|
|
157
|
+
expect(p.getStateManager()).toBeNull();
|
|
158
|
+
expect(p.getGlobalConfigManager()).toBeNull();
|
|
159
|
+
expect(p.getLocalConfigManager()).toBeNull();
|
|
160
|
+
await p.initStateManager();
|
|
161
|
+
expect(p.getStateManager()).not.toBeNull();
|
|
162
|
+
expect(p.getGlobalConfigManager()).not.toBeNull();
|
|
163
|
+
expect(p.getLocalConfigManager()).not.toBeNull();
|
|
164
|
+
expect(p.getStateManager()).toBe(p.getGlobalConfigManager());
|
|
165
|
+
expect(existsSync(join(globalDir, "config.json"))).toBe(true);
|
|
166
|
+
expect(existsSync(join(localDir, "config.json"))).toBe(true);
|
|
167
|
+
});
|
|
168
|
+
});
|
|
169
|
+
describe("TracePlugin - shouldRecord (scope resolution)", () => {
|
|
170
|
+
let globalDir;
|
|
171
|
+
let localDir;
|
|
172
|
+
let plugin;
|
|
173
|
+
beforeEach(async () => {
|
|
174
|
+
globalDir = mkdtempSync(join(tmpdir(), "plugin-sr-g-"));
|
|
175
|
+
localDir = mkdtempSync(join(tmpdir(), "plugin-sr-l-"));
|
|
176
|
+
plugin = new TracePlugin({ globalDir, localDir });
|
|
177
|
+
await plugin.initStateManager();
|
|
178
|
+
});
|
|
179
|
+
afterEach(() => {
|
|
180
|
+
plugin.uninstallInterceptor();
|
|
181
|
+
rmSync(globalDir, { recursive: true, force: true });
|
|
182
|
+
rmSync(localDir, { recursive: true, force: true });
|
|
183
|
+
});
|
|
184
|
+
test("returns true when state managers not initialized (defensive default)", () => {
|
|
185
|
+
const fresh = new TracePlugin({ globalDir, localDir });
|
|
186
|
+
expect(fresh.shouldRecord("s1")).toBe(true);
|
|
187
|
+
expect(fresh.shouldRecord()).toBe(true);
|
|
188
|
+
});
|
|
189
|
+
test("global enabled → true regardless of local/session", () => {
|
|
190
|
+
const gcm = plugin.getGlobalConfigManager();
|
|
191
|
+
const lcm = plugin.getLocalConfigManager();
|
|
192
|
+
gcm.setGlobalState("global_trace_enabled", "true");
|
|
193
|
+
lcm.setGlobalState("global_trace_enabled", "false");
|
|
194
|
+
gcm.setSessionEnabled("s1", false);
|
|
195
|
+
expect(plugin.shouldRecord("s1")).toBe(true);
|
|
196
|
+
expect(plugin.shouldRecord()).toBe(true);
|
|
197
|
+
});
|
|
198
|
+
test("global disabled + local enabled → true", () => {
|
|
199
|
+
const gcm = plugin.getGlobalConfigManager();
|
|
200
|
+
const lcm = plugin.getLocalConfigManager();
|
|
201
|
+
gcm.setGlobalState("global_trace_enabled", "false");
|
|
202
|
+
lcm.setGlobalState("global_trace_enabled", "true");
|
|
203
|
+
gcm.setSessionEnabled("s1", false);
|
|
204
|
+
expect(plugin.shouldRecord("s1")).toBe(true);
|
|
205
|
+
expect(plugin.shouldRecord()).toBe(true);
|
|
206
|
+
});
|
|
207
|
+
test("global disabled + local disabled + session enabled → true", () => {
|
|
208
|
+
const gcm = plugin.getGlobalConfigManager();
|
|
209
|
+
const lcm = plugin.getLocalConfigManager();
|
|
210
|
+
gcm.setGlobalState("global_trace_enabled", "false");
|
|
211
|
+
lcm.setGlobalState("global_trace_enabled", "false");
|
|
212
|
+
gcm.setSessionEnabled("s1", true);
|
|
213
|
+
expect(plugin.shouldRecord("s1")).toBe(true);
|
|
214
|
+
});
|
|
215
|
+
test("global disabled + local disabled + session disabled → false", () => {
|
|
216
|
+
const gcm = plugin.getGlobalConfigManager();
|
|
217
|
+
const lcm = plugin.getLocalConfigManager();
|
|
218
|
+
gcm.setGlobalState("global_trace_enabled", "false");
|
|
219
|
+
lcm.setGlobalState("global_trace_enabled", "false");
|
|
220
|
+
gcm.setSessionEnabled("s1", false);
|
|
221
|
+
expect(plugin.shouldRecord("s1")).toBe(false);
|
|
222
|
+
});
|
|
223
|
+
test("global disabled + local disabled + no sessionId → false", () => {
|
|
224
|
+
const gcm = plugin.getGlobalConfigManager();
|
|
225
|
+
const lcm = plugin.getLocalConfigManager();
|
|
226
|
+
gcm.setGlobalState("global_trace_enabled", "false");
|
|
227
|
+
lcm.setGlobalState("global_trace_enabled", "false");
|
|
228
|
+
expect(plugin.shouldRecord(undefined)).toBe(false);
|
|
229
|
+
expect(plugin.shouldRecord()).toBe(false);
|
|
230
|
+
});
|
|
231
|
+
test("global disabled + local disabled + unknown session → true (default trace_enabled=true)", () => {
|
|
232
|
+
const gcm = plugin.getGlobalConfigManager();
|
|
233
|
+
const lcm = plugin.getLocalConfigManager();
|
|
234
|
+
gcm.setGlobalState("global_trace_enabled", "false");
|
|
235
|
+
lcm.setGlobalState("global_trace_enabled", "false");
|
|
236
|
+
expect(plugin.shouldRecord("never-seen")).toBe(true);
|
|
237
|
+
});
|
|
238
|
+
});
|
|
239
|
+
describe("TracePlugin - resolveTraceDir (storage preference)", () => {
|
|
240
|
+
let globalDir;
|
|
241
|
+
let localDir;
|
|
242
|
+
let plugin;
|
|
243
|
+
beforeEach(async () => {
|
|
244
|
+
globalDir = mkdtempSync(join(tmpdir(), "plugin-rd-g-"));
|
|
245
|
+
localDir = mkdtempSync(join(tmpdir(), "plugin-rd-l-"));
|
|
246
|
+
plugin = new TracePlugin({ globalDir, localDir });
|
|
247
|
+
await plugin.initStateManager();
|
|
248
|
+
});
|
|
249
|
+
afterEach(() => {
|
|
250
|
+
plugin.uninstallInterceptor();
|
|
251
|
+
rmSync(globalDir, { recursive: true, force: true });
|
|
252
|
+
rmSync(localDir, { recursive: true, force: true });
|
|
253
|
+
});
|
|
254
|
+
test("returns globalDir when managers not initialized", () => {
|
|
255
|
+
const fresh = new TracePlugin({ globalDir, localDir });
|
|
256
|
+
expect(fresh.resolveTraceDir("s1")).toBe(globalDir);
|
|
257
|
+
expect(fresh.resolveTraceDir()).toBe(globalDir);
|
|
258
|
+
});
|
|
259
|
+
test("no session pref + global pref 'global' → globalDir", () => {
|
|
260
|
+
plugin.getGlobalConfigManager().setStoragePreference("global");
|
|
261
|
+
expect(plugin.resolveTraceDir("s1")).toBe(globalDir);
|
|
262
|
+
expect(plugin.resolveTraceDir()).toBe(globalDir);
|
|
263
|
+
});
|
|
264
|
+
test("no session pref + global pref 'local' → localDir", () => {
|
|
265
|
+
plugin.getGlobalConfigManager().setStoragePreference("local");
|
|
266
|
+
expect(plugin.resolveTraceDir("s1")).toBe(localDir);
|
|
267
|
+
expect(plugin.resolveTraceDir()).toBe(localDir);
|
|
268
|
+
});
|
|
269
|
+
test("session pref 'global' overrides global pref 'local' → globalDir", () => {
|
|
270
|
+
const gcm = plugin.getGlobalConfigManager();
|
|
271
|
+
gcm.setStoragePreference("local");
|
|
272
|
+
gcm.setSessionStoragePreference("s1", "global");
|
|
273
|
+
expect(plugin.resolveTraceDir("s1")).toBe(globalDir);
|
|
274
|
+
expect(plugin.resolveTraceDir("other")).toBe(localDir);
|
|
275
|
+
});
|
|
276
|
+
test("session pref 'local' overrides global pref 'global' → localDir", () => {
|
|
277
|
+
const gcm = plugin.getGlobalConfigManager();
|
|
278
|
+
gcm.setStoragePreference("global");
|
|
279
|
+
gcm.setSessionStoragePreference("s1", "local");
|
|
280
|
+
expect(plugin.resolveTraceDir("s1")).toBe(localDir);
|
|
281
|
+
expect(plugin.resolveTraceDir("other")).toBe(globalDir);
|
|
282
|
+
});
|
|
283
|
+
});
|
|
284
|
+
describe("TracePlugin - getScopeStatus", () => {
|
|
285
|
+
let globalDir;
|
|
286
|
+
let localDir;
|
|
287
|
+
beforeEach(() => {
|
|
288
|
+
globalDir = mkdtempSync(join(tmpdir(), "plugin-ss-g-"));
|
|
289
|
+
localDir = mkdtempSync(join(tmpdir(), "plugin-ss-l-"));
|
|
290
|
+
});
|
|
291
|
+
afterEach(() => {
|
|
292
|
+
rmSync(globalDir, { recursive: true, force: true });
|
|
293
|
+
rmSync(localDir, { recursive: true, force: true });
|
|
294
|
+
});
|
|
295
|
+
test("returns defaults when state manager not initialized", () => {
|
|
296
|
+
const p = new TracePlugin({ globalDir, localDir });
|
|
297
|
+
const status = p.getScopeStatus("s1");
|
|
298
|
+
expect(status.globalEnabled).toBe(false);
|
|
299
|
+
expect(status.localEnabled).toBe(false);
|
|
300
|
+
expect(status.sessionEnabled).toBeNull();
|
|
301
|
+
expect(status.effectiveEnabled).toBe(true);
|
|
302
|
+
expect(status.storageLocation).toBe("global");
|
|
303
|
+
expect(status.globalDir).toBe(globalDir);
|
|
304
|
+
expect(status.localDir).toBe(localDir);
|
|
305
|
+
});
|
|
306
|
+
test("reports sessionEnabled=null when no sessionId provided", async () => {
|
|
307
|
+
const p = new TracePlugin({ globalDir, localDir });
|
|
308
|
+
await p.initStateManager();
|
|
309
|
+
const status = p.getScopeStatus();
|
|
310
|
+
expect(status.sessionEnabled).toBeNull();
|
|
311
|
+
});
|
|
312
|
+
test("reports full status with all scopes engaged", async () => {
|
|
313
|
+
const p = new TracePlugin({ globalDir, localDir });
|
|
314
|
+
await p.initStateManager();
|
|
315
|
+
const gcm = p.getGlobalConfigManager();
|
|
316
|
+
const lcm = p.getLocalConfigManager();
|
|
317
|
+
gcm.setGlobalState("global_trace_enabled", "true");
|
|
318
|
+
lcm.setGlobalState("global_trace_enabled", "false");
|
|
319
|
+
gcm.setStoragePreference("local");
|
|
320
|
+
gcm.setSessionEnabled("s1", true);
|
|
321
|
+
const status = p.getScopeStatus("s1");
|
|
322
|
+
expect(status.globalEnabled).toBe(true);
|
|
323
|
+
expect(status.localEnabled).toBe(false);
|
|
324
|
+
expect(status.sessionEnabled).toBe(true);
|
|
325
|
+
expect(status.effectiveEnabled).toBe(true);
|
|
326
|
+
expect(status.storageLocation).toBe("local");
|
|
327
|
+
});
|
|
328
|
+
test("reports storageLocation='global' when resolveTraceDir picks global", async () => {
|
|
329
|
+
const p = new TracePlugin({ globalDir, localDir });
|
|
330
|
+
await p.initStateManager();
|
|
331
|
+
p.getGlobalConfigManager().setStoragePreference("global");
|
|
332
|
+
const status = p.getScopeStatus("s1");
|
|
333
|
+
expect(status.storageLocation).toBe("global");
|
|
334
|
+
});
|
|
335
|
+
});
|
|
336
|
+
describe("TracePlugin - session metadata operations via ConfigManager", () => {
|
|
337
|
+
let globalDir;
|
|
338
|
+
let localDir;
|
|
339
|
+
let plugin;
|
|
340
|
+
beforeEach(async () => {
|
|
341
|
+
globalDir = mkdtempSync(join(tmpdir(), "plugin-meta-g-"));
|
|
342
|
+
localDir = mkdtempSync(join(tmpdir(), "plugin-meta-l-"));
|
|
343
|
+
plugin = new TracePlugin({ globalDir, localDir });
|
|
344
|
+
await plugin.initStateManager();
|
|
345
|
+
});
|
|
346
|
+
afterEach(() => {
|
|
347
|
+
plugin.uninstallInterceptor();
|
|
348
|
+
rmSync(globalDir, { recursive: true, force: true });
|
|
349
|
+
rmSync(localDir, { recursive: true, force: true });
|
|
350
|
+
});
|
|
351
|
+
test("setSessionEnabled / getSessionEnabled write to session metadata file", () => {
|
|
352
|
+
const gcm = plugin.getGlobalConfigManager();
|
|
353
|
+
const sessionId = "meta-session";
|
|
354
|
+
expect(gcm.getSessionEnabled(sessionId)).toBe(true);
|
|
355
|
+
gcm.setSessionEnabled(sessionId, false);
|
|
356
|
+
expect(gcm.getSessionEnabled(sessionId)).toBe(false);
|
|
357
|
+
const metaPath = join(globalDir, sessionId, "metadata.json");
|
|
358
|
+
const meta = JSON.parse(readFileSync(metaPath, "utf-8"));
|
|
359
|
+
expect(meta.trace_enabled).toBe(false);
|
|
360
|
+
gcm.setSessionEnabled(sessionId, true);
|
|
361
|
+
expect(gcm.getSessionEnabled(sessionId)).toBe(true);
|
|
362
|
+
const meta2 = JSON.parse(readFileSync(metaPath, "utf-8"));
|
|
363
|
+
expect(meta2.trace_enabled).toBe(true);
|
|
364
|
+
});
|
|
365
|
+
test("setSessionStoragePreference / getSessionStoragePreference round-trip", () => {
|
|
366
|
+
const gcm = plugin.getGlobalConfigManager();
|
|
367
|
+
const sessionId = "storage-session";
|
|
368
|
+
expect(gcm.getSessionStoragePreference(sessionId)).toBeNull();
|
|
369
|
+
gcm.setSessionStoragePreference(sessionId, "local");
|
|
370
|
+
expect(gcm.getSessionStoragePreference(sessionId)).toBe("local");
|
|
371
|
+
gcm.setSessionStoragePreference(sessionId, "global");
|
|
372
|
+
expect(gcm.getSessionStoragePreference(sessionId)).toBe("global");
|
|
373
|
+
const metaPath = join(globalDir, sessionId, "metadata.json");
|
|
374
|
+
const meta = JSON.parse(readFileSync(metaPath, "utf-8"));
|
|
375
|
+
expect(meta.storage_preference).toBe("global");
|
|
376
|
+
});
|
|
377
|
+
test("setStoragePreference / getStoragePreference write to global config", () => {
|
|
378
|
+
const gcm = plugin.getGlobalConfigManager();
|
|
379
|
+
expect(gcm.getStoragePreference()).toBe("global");
|
|
380
|
+
gcm.setStoragePreference("local");
|
|
381
|
+
expect(gcm.getStoragePreference()).toBe("local");
|
|
382
|
+
const config = JSON.parse(readFileSync(join(globalDir, "config.json"), "utf-8"));
|
|
383
|
+
expect(config.storage_preference).toBe("local");
|
|
384
|
+
});
|
|
385
|
+
test("addSubSession links parent to child and dedups duplicates", () => {
|
|
386
|
+
const gcm = plugin.getGlobalConfigManager();
|
|
387
|
+
gcm.startSession("parent-session");
|
|
388
|
+
gcm.startSession("child-1");
|
|
389
|
+
gcm.startSession("child-2");
|
|
390
|
+
gcm.addSubSession("parent-session", "child-1");
|
|
391
|
+
gcm.addSubSession("parent-session", "child-2");
|
|
392
|
+
gcm.addSubSession("parent-session", "child-1");
|
|
393
|
+
const metaPath = join(globalDir, "parent-session", "metadata.json");
|
|
394
|
+
const meta = JSON.parse(readFileSync(metaPath, "utf-8"));
|
|
395
|
+
expect(meta.subSessions).toEqual(["child-1", "child-2"]);
|
|
396
|
+
});
|
|
397
|
+
test("updateSessionMetadata sets parentID on child for sub-session linking", () => {
|
|
398
|
+
const gcm = plugin.getGlobalConfigManager();
|
|
399
|
+
gcm.startSession("parent-id");
|
|
400
|
+
gcm.startSession("child-id");
|
|
401
|
+
gcm.updateSessionMetadata("child-id", { parentID: "parent-id" });
|
|
402
|
+
const metaPath = join(globalDir, "child-id", "metadata.json");
|
|
403
|
+
const meta = JSON.parse(readFileSync(metaPath, "utf-8"));
|
|
404
|
+
expect(meta.parentID).toBe("parent-id");
|
|
405
|
+
});
|
|
406
|
+
});
|
|
407
|
+
describe("TracePlugin - config corruption recovery", () => {
|
|
408
|
+
test("initStateManager falls back to defaults when config.json is invalid JSON", async () => {
|
|
409
|
+
const globalDir = mkdtempSync(join(tmpdir(), "plugin-corrupt-g-"));
|
|
410
|
+
const localDir = mkdtempSync(join(tmpdir(), "plugin-corrupt-l-"));
|
|
411
|
+
try {
|
|
412
|
+
writeFileSync(join(globalDir, "config.json"), "{ this is not valid json", "utf-8");
|
|
413
|
+
writeFileSync(join(localDir, "config.json"), "{{{", "utf-8");
|
|
414
|
+
const errorSpy = vi.spyOn(logger, "error").mockImplementation(((..._args) => logger));
|
|
415
|
+
const p = new TracePlugin({ globalDir, localDir });
|
|
416
|
+
await p.initStateManager();
|
|
417
|
+
const gcm = p.getGlobalConfigManager();
|
|
418
|
+
expect(gcm.getGlobalState("global_trace_enabled")).toBe("false");
|
|
419
|
+
expect(gcm.getGlobalState("plugin_enabled")).toBe("true");
|
|
420
|
+
expect(gcm.getStoragePreference()).toBe("global");
|
|
421
|
+
expect(errorSpy).toHaveBeenCalled();
|
|
422
|
+
const calls = errorSpy.mock.calls.map((c) => String(c[0]));
|
|
423
|
+
expect(calls.some((m) => /config\.json/i.test(m))).toBe(true);
|
|
424
|
+
errorSpy.mockRestore();
|
|
425
|
+
}
|
|
426
|
+
finally {
|
|
427
|
+
rmSync(globalDir, { recursive: true, force: true });
|
|
428
|
+
rmSync(localDir, { recursive: true, force: true });
|
|
429
|
+
}
|
|
430
|
+
});
|
|
431
|
+
});
|
|
432
|
+
describe("TracePlugin - tracedFetch edge cases", () => {
|
|
433
|
+
let tempDir;
|
|
434
|
+
let plugin;
|
|
435
|
+
let savedFetch;
|
|
436
|
+
beforeEach(() => {
|
|
437
|
+
tempDir = mkdtempSync(join(tmpdir(), "plugin-edge-"));
|
|
438
|
+
plugin = new TracePlugin({ globalDir: tempDir, localDir: tempDir });
|
|
439
|
+
savedFetch = globalThis.fetch;
|
|
440
|
+
});
|
|
441
|
+
afterEach(async () => {
|
|
442
|
+
plugin.uninstallInterceptor();
|
|
443
|
+
globalThis.fetch = savedFetch;
|
|
444
|
+
await plugin.flush();
|
|
445
|
+
rmSync(tempDir, { recursive: true, force: true });
|
|
446
|
+
});
|
|
447
|
+
test("delegates without recording when no session header is present", async () => {
|
|
448
|
+
let invoked = false;
|
|
449
|
+
globalThis.fetch = async () => {
|
|
450
|
+
invoked = true;
|
|
451
|
+
return new Response(JSON.stringify({ ok: true }), { status: 200 });
|
|
452
|
+
};
|
|
453
|
+
plugin.installInterceptor();
|
|
454
|
+
const res = await plugin.tracedFetch("https://example.com");
|
|
455
|
+
expect(invoked).toBe(true);
|
|
456
|
+
expect(res.status).toBe(200);
|
|
457
|
+
await plugin.flush();
|
|
458
|
+
const sessionDirs = readdirSync(tempDir).filter((e) => {
|
|
459
|
+
try {
|
|
460
|
+
return statSync(join(tempDir, e)).isDirectory();
|
|
461
|
+
}
|
|
462
|
+
catch {
|
|
463
|
+
return false;
|
|
464
|
+
}
|
|
465
|
+
});
|
|
466
|
+
expect(sessionDirs.length).toBe(0);
|
|
467
|
+
});
|
|
468
|
+
test("delegates without recording when shouldRecord returns false", async () => {
|
|
469
|
+
await plugin.initStateManager();
|
|
470
|
+
const gcm = plugin.getGlobalConfigManager();
|
|
471
|
+
gcm.setGlobalState("global_trace_enabled", "false");
|
|
472
|
+
gcm.setSessionEnabled("blocked", false);
|
|
473
|
+
let invoked = false;
|
|
474
|
+
globalThis.fetch = async () => {
|
|
475
|
+
invoked = true;
|
|
476
|
+
return new Response("ok", { status: 200 });
|
|
477
|
+
};
|
|
478
|
+
plugin.installInterceptor();
|
|
479
|
+
const req = new Request("https://example.com", {
|
|
480
|
+
headers: { "x-opencode-session": "blocked" },
|
|
481
|
+
});
|
|
482
|
+
const res = await plugin.tracedFetch(req);
|
|
483
|
+
expect(invoked).toBe(true);
|
|
484
|
+
expect(res.status).toBe(200);
|
|
485
|
+
await plugin.flush();
|
|
486
|
+
const sessionDir = join(tempDir, "blocked");
|
|
487
|
+
if (existsSync(sessionDir)) {
|
|
488
|
+
const recordFiles = readdirSync(sessionDir).filter((f) => /^\d+\.json$/.test(f));
|
|
489
|
+
expect(recordFiles.length).toBe(0);
|
|
490
|
+
}
|
|
491
|
+
});
|
|
492
|
+
test("records error and rethrows when delegate fetch throws Error", async () => {
|
|
493
|
+
const fetchError = new Error("Network down");
|
|
494
|
+
globalThis.fetch = async () => {
|
|
495
|
+
throw fetchError;
|
|
496
|
+
};
|
|
497
|
+
plugin.installInterceptor();
|
|
498
|
+
const sessionId = "err-session";
|
|
499
|
+
const req = new Request("https://example.com", {
|
|
500
|
+
method: "POST",
|
|
501
|
+
headers: {
|
|
502
|
+
"x-opencode-session": sessionId,
|
|
503
|
+
"content-type": "application/json",
|
|
504
|
+
},
|
|
505
|
+
body: JSON.stringify({ q: 1 }),
|
|
506
|
+
});
|
|
507
|
+
await expect(plugin.tracedFetch(req)).rejects.toThrow("Network down");
|
|
508
|
+
const filePath = join(tempDir, sessionId, "1.json");
|
|
509
|
+
await waitForFile(filePath, 5000);
|
|
510
|
+
const content = JSON.parse(readFileSync(filePath, "utf-8"));
|
|
511
|
+
expect(content.response).toBeNull();
|
|
512
|
+
expect(content.error).toBeTruthy();
|
|
513
|
+
expect(content.error.message).toBe("Network down");
|
|
514
|
+
});
|
|
515
|
+
test("records error and rethrows when delegate throws non-Error value", async () => {
|
|
516
|
+
globalThis.fetch = async () => {
|
|
517
|
+
throw "boom-string";
|
|
518
|
+
};
|
|
519
|
+
plugin.installInterceptor();
|
|
520
|
+
const sessionId = "str-err-session";
|
|
521
|
+
const req = new Request("https://example.com", {
|
|
522
|
+
method: "POST",
|
|
523
|
+
headers: { "x-opencode-session": sessionId },
|
|
524
|
+
body: "{}",
|
|
525
|
+
});
|
|
526
|
+
await expect(plugin.tracedFetch(req)).rejects.toBe("boom-string");
|
|
527
|
+
const filePath = join(tempDir, sessionId, "1.json");
|
|
528
|
+
await waitForFile(filePath, 5000);
|
|
529
|
+
const content = JSON.parse(readFileSync(filePath, "utf-8"));
|
|
530
|
+
expect(content.error).toBeTruthy();
|
|
531
|
+
expect(content.error.message).toBe("boom-string");
|
|
532
|
+
expect(content.error.stack).toBeUndefined();
|
|
533
|
+
});
|
|
534
|
+
test("wraps streaming response and captures latency metadata", async () => {
|
|
535
|
+
globalThis.fetch = async () => {
|
|
536
|
+
const encoder = new TextEncoder();
|
|
537
|
+
const stream = new ReadableStream({
|
|
538
|
+
start(controller) {
|
|
539
|
+
controller.enqueue(encoder.encode("data: chunk1\n\n"));
|
|
540
|
+
controller.enqueue(encoder.encode("data: chunk2\n\n"));
|
|
541
|
+
controller.close();
|
|
542
|
+
},
|
|
543
|
+
});
|
|
544
|
+
return new Response(stream, {
|
|
545
|
+
status: 200,
|
|
546
|
+
headers: { "content-type": "text/event-stream" },
|
|
547
|
+
});
|
|
548
|
+
};
|
|
549
|
+
plugin.installInterceptor();
|
|
550
|
+
const sessionId = "stream-session";
|
|
551
|
+
const req = new Request("https://example.com", {
|
|
552
|
+
method: "POST",
|
|
553
|
+
headers: {
|
|
554
|
+
"x-opencode-session": sessionId,
|
|
555
|
+
"content-type": "application/json",
|
|
556
|
+
},
|
|
557
|
+
body: JSON.stringify({ stream: true, model: "test" }),
|
|
558
|
+
});
|
|
559
|
+
const res = await plugin.tracedFetch(req);
|
|
560
|
+
expect(res.status).toBe(200);
|
|
561
|
+
await res.text();
|
|
562
|
+
const filePath = join(tempDir, sessionId, "1.json");
|
|
563
|
+
await waitForFile(filePath, 5000);
|
|
564
|
+
const content = JSON.parse(readFileSync(filePath, "utf-8"));
|
|
565
|
+
expect(content.requestSentAt).toBeTypeOf("number");
|
|
566
|
+
expect(content.firstTokenAt).toBeTypeOf("number");
|
|
567
|
+
expect(content.lastTokenAt).toBeTypeOf("number");
|
|
568
|
+
expect(content.firstTokenAt).toBeGreaterThanOrEqual(content.requestSentAt);
|
|
569
|
+
expect(content.lastTokenAt).toBeGreaterThanOrEqual(content.firstTokenAt);
|
|
570
|
+
});
|
|
571
|
+
test("captures request body when not valid JSON (raw string)", async () => {
|
|
572
|
+
globalThis.fetch = async () => new Response("plain", {
|
|
573
|
+
status: 200,
|
|
574
|
+
headers: { "content-type": "text/plain" },
|
|
575
|
+
});
|
|
576
|
+
plugin.installInterceptor();
|
|
577
|
+
const sessionId = "raw-session";
|
|
578
|
+
const req = new Request("https://example.com", {
|
|
579
|
+
method: "POST",
|
|
580
|
+
headers: {
|
|
581
|
+
"x-opencode-session": sessionId,
|
|
582
|
+
"content-type": "text/plain",
|
|
583
|
+
},
|
|
584
|
+
body: "hello world",
|
|
585
|
+
});
|
|
586
|
+
const res = await plugin.tracedFetch(req);
|
|
587
|
+
expect(res.status).toBe(200);
|
|
588
|
+
const filePath = join(tempDir, sessionId, "1.json");
|
|
589
|
+
await waitForFile(filePath, 5000);
|
|
590
|
+
const content = JSON.parse(readFileSync(filePath, "utf-8"));
|
|
591
|
+
expect(content.request.body).toBe("hello world");
|
|
592
|
+
});
|
|
593
|
+
test("classifyPurpose returns '' for body with non-empty tools array", () => {
|
|
594
|
+
const classify = plugin["classifyPurpose"].bind(plugin);
|
|
595
|
+
expect(classify({ tools: [{ name: "tool1" }] })).toBe("");
|
|
596
|
+
expect(classify({ tools: [] })).toBe("[meta]");
|
|
597
|
+
expect(classify({})).toBe("[meta]");
|
|
598
|
+
expect(classify(null)).toBe("[meta]");
|
|
599
|
+
expect(classify("text")).toBe("[meta]");
|
|
600
|
+
expect(classify([1, 2, 3])).toBe("[meta]");
|
|
601
|
+
});
|
|
602
|
+
});
|
|
603
|
+
describe("TracePlugin - buildTimelineEntry provider & token extraction", () => {
|
|
604
|
+
let tempDir;
|
|
605
|
+
let plugin;
|
|
606
|
+
let savedFetch;
|
|
607
|
+
beforeEach(() => {
|
|
608
|
+
tempDir = mkdtempSync(join(tmpdir(), "plugin-tl-"));
|
|
609
|
+
plugin = new TracePlugin({ globalDir: tempDir, localDir: tempDir });
|
|
610
|
+
savedFetch = globalThis.fetch;
|
|
611
|
+
});
|
|
612
|
+
afterEach(async () => {
|
|
613
|
+
plugin.uninstallInterceptor();
|
|
614
|
+
globalThis.fetch = savedFetch;
|
|
615
|
+
await plugin.flush();
|
|
616
|
+
rmSync(tempDir, { recursive: true, force: true });
|
|
617
|
+
});
|
|
618
|
+
async function waitForNdjsonLine(path, timeoutMs = 5000) {
|
|
619
|
+
const start = Date.now();
|
|
620
|
+
while (Date.now() - start < timeoutMs) {
|
|
621
|
+
if (existsSync(path)) {
|
|
622
|
+
const raw = readFileSync(path, "utf-8");
|
|
623
|
+
const firstLine = raw.split("\n").find((l) => l.trim());
|
|
624
|
+
if (firstLine) {
|
|
625
|
+
try {
|
|
626
|
+
return JSON.parse(firstLine);
|
|
627
|
+
}
|
|
628
|
+
catch { }
|
|
629
|
+
}
|
|
630
|
+
}
|
|
631
|
+
await new Promise((r) => setTimeout(r, 10));
|
|
632
|
+
}
|
|
633
|
+
throw new Error(`Timeout waiting for ndjson line in ${path}`);
|
|
634
|
+
}
|
|
635
|
+
test("extracts openai provider with prompt/completion tokens", async () => {
|
|
636
|
+
globalThis.fetch = async () => new Response(JSON.stringify({
|
|
637
|
+
model: "gpt-4",
|
|
638
|
+
usage: { prompt_tokens: 120, completion_tokens: 60 },
|
|
639
|
+
}), { status: 200, headers: { "content-type": "application/json" } });
|
|
640
|
+
plugin.installInterceptor();
|
|
641
|
+
const req = new Request("https://api.openai.com/v1/chat/completions", {
|
|
642
|
+
method: "POST",
|
|
643
|
+
headers: { "x-opencode-session": "oai", "content-type": "application/json" },
|
|
644
|
+
body: "{}",
|
|
645
|
+
});
|
|
646
|
+
await plugin.tracedFetch(req);
|
|
647
|
+
const entry = await waitForNdjsonLine(join(tempDir, "oai", "timeline.ndjson"));
|
|
648
|
+
expect(entry.provider).toBe("openai");
|
|
649
|
+
expect(entry.model).toBe("gpt-4");
|
|
650
|
+
expect(entry.inputTokens).toBe(120);
|
|
651
|
+
expect(entry.outputTokens).toBe(60);
|
|
652
|
+
});
|
|
653
|
+
test("extracts anthropic provider with input/output tokens", async () => {
|
|
654
|
+
globalThis.fetch = async () => new Response(JSON.stringify({
|
|
655
|
+
model: "claude-3-opus",
|
|
656
|
+
usage: { input_tokens: 200, output_tokens: 80 },
|
|
657
|
+
}), { status: 200, headers: { "content-type": "application/json" } });
|
|
658
|
+
plugin.installInterceptor();
|
|
659
|
+
const req = new Request("https://api.anthropic.com/v1/messages", {
|
|
660
|
+
method: "POST",
|
|
661
|
+
headers: { "x-opencode-session": "ant", "content-type": "application/json" },
|
|
662
|
+
body: "{}",
|
|
663
|
+
});
|
|
664
|
+
await plugin.tracedFetch(req);
|
|
665
|
+
const entry = await waitForNdjsonLine(join(tempDir, "ant", "timeline.ndjson"));
|
|
666
|
+
expect(entry.provider).toBe("anthropic");
|
|
667
|
+
expect(entry.model).toBe("claude-3-opus");
|
|
668
|
+
expect(entry.inputTokens).toBe(200);
|
|
669
|
+
expect(entry.outputTokens).toBe(80);
|
|
670
|
+
});
|
|
671
|
+
test("provider is null for unknown URL, tokens null when usage absent", async () => {
|
|
672
|
+
globalThis.fetch = async () => new Response(JSON.stringify({ ok: true }), {
|
|
673
|
+
status: 200,
|
|
674
|
+
headers: { "content-type": "application/json" },
|
|
675
|
+
});
|
|
676
|
+
plugin.installInterceptor();
|
|
677
|
+
const req = new Request("https://example.com/api", {
|
|
678
|
+
method: "POST",
|
|
679
|
+
headers: { "x-opencode-session": "unk", "content-type": "application/json" },
|
|
680
|
+
body: "{}",
|
|
681
|
+
});
|
|
682
|
+
await plugin.tracedFetch(req);
|
|
683
|
+
const entry = await waitForNdjsonLine(join(tempDir, "unk", "timeline.ndjson"));
|
|
684
|
+
expect(entry.provider).toBeNull();
|
|
685
|
+
expect(entry.model).toBeNull();
|
|
686
|
+
expect(entry.inputTokens).toBeNull();
|
|
687
|
+
expect(entry.outputTokens).toBeNull();
|
|
688
|
+
});
|
|
689
|
+
});
|
|
690
|
+
describe("TracePlugin - flush, wrap, getInterceptor, getSessionId", () => {
|
|
691
|
+
let tempDir;
|
|
692
|
+
let plugin;
|
|
693
|
+
let savedFetch;
|
|
694
|
+
beforeEach(() => {
|
|
695
|
+
tempDir = mkdtempSync(join(tmpdir(), "plugin-misc-"));
|
|
696
|
+
plugin = new TracePlugin({ globalDir: tempDir, localDir: tempDir });
|
|
697
|
+
savedFetch = globalThis.fetch;
|
|
698
|
+
});
|
|
699
|
+
afterEach(async () => {
|
|
700
|
+
plugin.uninstallInterceptor();
|
|
701
|
+
globalThis.fetch = savedFetch;
|
|
702
|
+
await plugin.flush();
|
|
703
|
+
rmSync(tempDir, { recursive: true, force: true });
|
|
704
|
+
});
|
|
705
|
+
test("flush awaits underlying writeQueue.flush()", async () => {
|
|
706
|
+
const spy = vi
|
|
707
|
+
.spyOn(plugin["writeQueue"], "flush")
|
|
708
|
+
.mockResolvedValue(undefined);
|
|
709
|
+
await plugin.flush();
|
|
710
|
+
expect(spy).toHaveBeenCalled();
|
|
711
|
+
spy.mockRestore();
|
|
712
|
+
});
|
|
713
|
+
test("wrap returns a function that uses the provided fetch as origFetch", async () => {
|
|
714
|
+
let providedCalled = 0;
|
|
715
|
+
const provided = (async () => {
|
|
716
|
+
providedCalled++;
|
|
717
|
+
return new Response("from-provided", { status: 201 });
|
|
718
|
+
});
|
|
719
|
+
const wrapped = plugin.wrap(provided);
|
|
720
|
+
const res = await wrapped("https://example.com");
|
|
721
|
+
expect(providedCalled).toBe(1);
|
|
722
|
+
expect(res.status).toBe(201);
|
|
723
|
+
});
|
|
724
|
+
test("getInterceptor returns a function that uses origFetch captured at construction", async () => {
|
|
725
|
+
let origCalled = 0;
|
|
726
|
+
const fakeOrig = (async () => {
|
|
727
|
+
origCalled++;
|
|
728
|
+
return new Response("from-orig", { status: 202 });
|
|
729
|
+
});
|
|
730
|
+
globalThis.fetch = fakeOrig;
|
|
731
|
+
const p = new TracePlugin({ globalDir: tempDir, localDir: tempDir });
|
|
732
|
+
globalThis.fetch = savedFetch;
|
|
733
|
+
const interceptor = p.getInterceptor();
|
|
734
|
+
const res = await interceptor("https://example.com");
|
|
735
|
+
expect(origCalled).toBe(1);
|
|
736
|
+
expect(res.status).toBe(202);
|
|
737
|
+
});
|
|
738
|
+
test("getSessionId reads from x-session-affinity header", async () => {
|
|
739
|
+
globalThis.fetch = async () => new Response("ok", { status: 200, headers: { "content-type": "text/plain" } });
|
|
740
|
+
plugin.installInterceptor();
|
|
741
|
+
const req = new Request("https://example.com", {
|
|
742
|
+
method: "POST",
|
|
743
|
+
headers: { "x-session-affinity": "affinity-session" },
|
|
744
|
+
body: "{}",
|
|
745
|
+
});
|
|
746
|
+
await plugin.tracedFetch(req);
|
|
747
|
+
const filePath = join(tempDir, "affinity-session", "1.json");
|
|
748
|
+
await waitForFile(filePath, 5000);
|
|
749
|
+
expect(existsSync(filePath)).toBe(true);
|
|
750
|
+
});
|
|
751
|
+
test("getSessionId reads from session_id header", async () => {
|
|
752
|
+
globalThis.fetch = async () => new Response("ok", { status: 200, headers: { "content-type": "text/plain" } });
|
|
753
|
+
plugin.installInterceptor();
|
|
754
|
+
const req = new Request("https://example.com", {
|
|
755
|
+
method: "POST",
|
|
756
|
+
headers: { session_id: "fallback-session" },
|
|
757
|
+
body: "{}",
|
|
758
|
+
});
|
|
759
|
+
await plugin.tracedFetch(req);
|
|
760
|
+
const filePath = join(tempDir, "fallback-session", "1.json");
|
|
761
|
+
await waitForFile(filePath, 5000);
|
|
762
|
+
expect(existsSync(filePath)).toBe(true);
|
|
763
|
+
});
|
|
764
|
+
test("sequence numbers increment per session", async () => {
|
|
765
|
+
globalThis.fetch = async () => new Response("ok", { status: 200, headers: { "content-type": "text/plain" } });
|
|
766
|
+
plugin.installInterceptor();
|
|
767
|
+
const sessionId = "seq-session";
|
|
768
|
+
for (let i = 0; i < 3; i++) {
|
|
769
|
+
const req = new Request(`https://example.com/${i}`, {
|
|
770
|
+
method: "POST",
|
|
771
|
+
headers: { "x-opencode-session": sessionId },
|
|
772
|
+
body: "{}",
|
|
773
|
+
});
|
|
774
|
+
await plugin.tracedFetch(req);
|
|
775
|
+
}
|
|
776
|
+
await waitForFile(join(tempDir, sessionId, "3.json"), 5000);
|
|
777
|
+
expect(existsSync(join(tempDir, sessionId, "1.json"))).toBe(true);
|
|
778
|
+
expect(existsSync(join(tempDir, sessionId, "2.json"))).toBe(true);
|
|
779
|
+
expect(existsSync(join(tempDir, sessionId, "3.json"))).toBe(true);
|
|
133
780
|
});
|
|
134
781
|
});
|
|
135
782
|
//# sourceMappingURL=plugin-instance.test.js.map
|