opencode-pilot 0.18.2 → 0.19.0

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.
@@ -0,0 +1,347 @@
1
+ /**
2
+ * Integration tests for session and sandbox reuse
3
+ *
4
+ * These tests verify the actual API interactions work correctly.
5
+ * They use a mock server that simulates OpenCode's behavior.
6
+ */
7
+ import { describe, it, beforeEach, afterEach } from "node:test";
8
+ import assert from "node:assert";
9
+ import { createServer } from "node:http";
10
+
11
+ import {
12
+ listSessions,
13
+ findReusableSession,
14
+ isSessionArchived,
15
+ sendMessageToSession,
16
+ executeAction,
17
+ } from "../../service/actions.js";
18
+
19
+ import {
20
+ listWorktrees,
21
+ resolveWorktreeDirectory,
22
+ } from "../../service/worktree.js";
23
+
24
+ /**
25
+ * Create a mock OpenCode server for testing
26
+ */
27
+ function createMockServer(handlers = {}) {
28
+ const server = createServer((req, res) => {
29
+ const url = new URL(req.url, `http://${req.headers.host}`);
30
+ const path = url.pathname;
31
+ const method = req.method;
32
+ const directory = url.searchParams.get("directory");
33
+
34
+ // Collect request body
35
+ let body = "";
36
+ req.on("data", (chunk) => (body += chunk));
37
+ req.on("end", () => {
38
+ const request = {
39
+ method,
40
+ path,
41
+ directory,
42
+ query: Object.fromEntries(url.searchParams),
43
+ body: body ? JSON.parse(body) : null,
44
+ };
45
+
46
+ // Find matching handler
47
+ const handlerKey = `${method} ${path}`;
48
+ const handler = handlers[handlerKey] || handlers.default;
49
+
50
+ if (handler) {
51
+ const result = handler(request);
52
+ res.writeHead(result.status || 200, { "Content-Type": "application/json" });
53
+ res.end(JSON.stringify(result.body));
54
+ } else {
55
+ res.writeHead(404, { "Content-Type": "application/json" });
56
+ res.end(JSON.stringify({ error: "Not found" }));
57
+ }
58
+ });
59
+ });
60
+
61
+ return new Promise((resolve) => {
62
+ server.listen(0, "127.0.0.1", () => {
63
+ const { port } = server.address();
64
+ resolve({
65
+ url: `http://127.0.0.1:${port}`,
66
+ server,
67
+ close: () => new Promise((r) => server.close(r)),
68
+ });
69
+ });
70
+ });
71
+ }
72
+
73
+ describe("integration: session reuse", () => {
74
+ let mockServer;
75
+
76
+ afterEach(async () => {
77
+ if (mockServer) {
78
+ await mockServer.close();
79
+ mockServer = null;
80
+ }
81
+ });
82
+
83
+ it("listSessions passes directory parameter to server", async () => {
84
+ let receivedDirectory = null;
85
+
86
+ mockServer = await createMockServer({
87
+ "GET /session": (req) => {
88
+ receivedDirectory = req.directory;
89
+ return {
90
+ body: [
91
+ { id: "ses_1", directory: "/path/to/project", time: { created: 1000, updated: 2000 } },
92
+ ],
93
+ };
94
+ },
95
+ });
96
+
97
+ const sessions = await listSessions(mockServer.url, { directory: "/path/to/project" });
98
+
99
+ assert.strictEqual(sessions.length, 1);
100
+ assert.strictEqual(receivedDirectory, "/path/to/project", "Server should receive directory parameter");
101
+ });
102
+
103
+ it("findReusableSession filters out archived sessions", async () => {
104
+ mockServer = await createMockServer({
105
+ "GET /session": () => ({
106
+ body: [
107
+ { id: "ses_archived", directory: "/proj", time: { created: 1000, updated: 3000, archived: 4000 } },
108
+ { id: "ses_active", directory: "/proj", time: { created: 2000, updated: 2500 } },
109
+ ],
110
+ }),
111
+ "GET /session/status": () => ({ body: {} }),
112
+ });
113
+
114
+ const session = await findReusableSession(mockServer.url, "/proj");
115
+
116
+ assert.ok(session, "Should find a session");
117
+ assert.strictEqual(session.id, "ses_active", "Should return the active session, not archived");
118
+ });
119
+
120
+ it("findReusableSession prefers idle sessions over busy", async () => {
121
+ mockServer = await createMockServer({
122
+ "GET /session": () => ({
123
+ body: [
124
+ { id: "ses_busy", directory: "/proj", time: { created: 1000, updated: 3000 } },
125
+ { id: "ses_idle", directory: "/proj", time: { created: 2000, updated: 2000 } },
126
+ ],
127
+ }),
128
+ "GET /session/status": () => ({
129
+ body: { ses_busy: { type: "busy" } },
130
+ }),
131
+ });
132
+
133
+ const session = await findReusableSession(mockServer.url, "/proj");
134
+
135
+ assert.strictEqual(session.id, "ses_idle", "Should prefer idle session even with older update time");
136
+ });
137
+
138
+ it("sendMessageToSession updates title and posts message", async () => {
139
+ let titleUpdated = false;
140
+ let messagePosted = false;
141
+ let postedBody = null;
142
+
143
+ mockServer = await createMockServer({
144
+ "PATCH /session/ses_123": (req) => {
145
+ titleUpdated = req.body?.title === "New Title";
146
+ return { body: {} };
147
+ },
148
+ "POST /session/ses_123/message": (req) => {
149
+ messagePosted = true;
150
+ postedBody = req.body;
151
+ return { body: { success: true } };
152
+ },
153
+ });
154
+
155
+ const result = await sendMessageToSession(
156
+ mockServer.url,
157
+ "ses_123",
158
+ "/proj",
159
+ "Hello world",
160
+ { title: "New Title", agent: "plan" }
161
+ );
162
+
163
+ assert.ok(result.success);
164
+ assert.strictEqual(result.reused, true);
165
+ assert.ok(titleUpdated, "Should update session title");
166
+ assert.ok(messagePosted, "Should post message");
167
+ assert.strictEqual(postedBody.parts[0].text, "Hello world");
168
+ assert.strictEqual(postedBody.agent, "plan");
169
+ });
170
+
171
+ it("executeAction reuses existing session when available", async () => {
172
+ let sessionCreated = false;
173
+ let messageSessionId = null;
174
+
175
+ mockServer = await createMockServer({
176
+ "GET /session": () => ({
177
+ body: [{ id: "ses_existing", directory: "/proj", time: { created: 1000, updated: 2000 } }],
178
+ }),
179
+ "GET /session/status": () => ({ body: {} }),
180
+ "POST /session": () => {
181
+ sessionCreated = true;
182
+ return { body: { id: "ses_new" } };
183
+ },
184
+ "PATCH /session/ses_existing": () => ({ body: {} }),
185
+ "POST /session/ses_existing/message": (req) => {
186
+ messageSessionId = "ses_existing";
187
+ return { body: { success: true } };
188
+ },
189
+ "GET /project/current": () => ({
190
+ body: { id: "proj_1", worktree: "/proj", time: { created: 1000, updated: 2000 }, sandboxes: [] },
191
+ }),
192
+ });
193
+
194
+ const result = await executeAction(
195
+ { number: 123, title: "Test issue" },
196
+ { path: "/proj", prompt: "default" },
197
+ { discoverServer: async () => mockServer.url }
198
+ );
199
+
200
+ assert.ok(result.success);
201
+ assert.strictEqual(result.sessionReused, true, "Should indicate session was reused");
202
+ assert.strictEqual(sessionCreated, false, "Should NOT create new session");
203
+ assert.strictEqual(messageSessionId, "ses_existing", "Should post to existing session");
204
+ });
205
+
206
+ it("executeAction creates new session when all existing are archived", async () => {
207
+ let sessionCreated = false;
208
+
209
+ mockServer = await createMockServer({
210
+ "GET /session": () => ({
211
+ body: [{ id: "ses_archived", directory: "/proj", time: { created: 1000, archived: 2000 } }],
212
+ }),
213
+ "GET /session/status": () => ({ body: {} }),
214
+ "POST /session": () => {
215
+ sessionCreated = true;
216
+ return { body: { id: "ses_new" } };
217
+ },
218
+ "PATCH /session/ses_new": () => ({ body: {} }),
219
+ "POST /session/ses_new/message": () => ({ body: { success: true } }),
220
+ "GET /project/current": () => ({
221
+ body: { id: "proj_1", worktree: "/proj", time: { created: 1000, updated: 2000 }, sandboxes: [] },
222
+ }),
223
+ });
224
+
225
+ const result = await executeAction(
226
+ { number: 456, title: "Test" },
227
+ { path: "/proj", prompt: "default" },
228
+ { discoverServer: async () => mockServer.url }
229
+ );
230
+
231
+ assert.ok(result.success);
232
+ assert.strictEqual(result.sessionReused, undefined, "Should NOT indicate session was reused");
233
+ assert.ok(sessionCreated, "Should create new session when existing is archived");
234
+ });
235
+ });
236
+
237
+ describe("integration: sandbox reuse", () => {
238
+ let mockServer;
239
+
240
+ afterEach(async () => {
241
+ if (mockServer) {
242
+ await mockServer.close();
243
+ mockServer = null;
244
+ }
245
+ });
246
+
247
+ it("listWorktrees passes directory parameter to server", async () => {
248
+ let receivedDirectory = null;
249
+
250
+ mockServer = await createMockServer({
251
+ "GET /experimental/worktree": (req) => {
252
+ receivedDirectory = req.directory;
253
+ return { body: ["/worktree/branch-1"] };
254
+ },
255
+ });
256
+
257
+ const worktrees = await listWorktrees(mockServer.url, { directory: "/path/to/project" });
258
+
259
+ assert.strictEqual(worktrees.length, 1);
260
+ assert.strictEqual(receivedDirectory, "/path/to/project", "Server should receive directory parameter");
261
+ });
262
+
263
+ it("resolveWorktreeDirectory reuses existing sandbox when name matches", async () => {
264
+ let postCalled = false;
265
+ let listDirectory = null;
266
+
267
+ mockServer = await createMockServer({
268
+ "GET /experimental/worktree": (req) => {
269
+ listDirectory = req.directory;
270
+ return {
271
+ body: [
272
+ "/worktree/other-branch",
273
+ "/worktree/my-feature",
274
+ ],
275
+ };
276
+ },
277
+ "POST /experimental/worktree": () => {
278
+ postCalled = true;
279
+ return { body: { name: "my-feature", directory: "/worktree/my-feature-new" } };
280
+ },
281
+ });
282
+
283
+ const result = await resolveWorktreeDirectory(
284
+ mockServer.url,
285
+ "/path/to/project",
286
+ { worktree: "new", worktreeName: "my-feature" }
287
+ );
288
+
289
+ assert.strictEqual(result.directory, "/worktree/my-feature");
290
+ assert.strictEqual(result.worktreeReused, true);
291
+ assert.strictEqual(postCalled, false, "Should NOT create new worktree");
292
+ assert.strictEqual(listDirectory, "/path/to/project", "Should pass directory to list worktrees");
293
+ });
294
+
295
+ it("resolveWorktreeDirectory creates new sandbox when name doesn't match", async () => {
296
+ let postCalled = false;
297
+ let postDirectory = null;
298
+
299
+ mockServer = await createMockServer({
300
+ "GET /experimental/worktree": () => ({
301
+ body: ["/worktree/other-branch"],
302
+ }),
303
+ "POST /experimental/worktree": (req) => {
304
+ postCalled = true;
305
+ postDirectory = req.directory;
306
+ return {
307
+ body: {
308
+ name: "new-feature",
309
+ branch: "opencode/new-feature",
310
+ directory: "/worktree/new-feature",
311
+ },
312
+ };
313
+ },
314
+ });
315
+
316
+ const result = await resolveWorktreeDirectory(
317
+ mockServer.url,
318
+ "/path/to/project",
319
+ { worktree: "new", worktreeName: "new-feature" }
320
+ );
321
+
322
+ assert.strictEqual(result.directory, "/worktree/new-feature");
323
+ assert.strictEqual(result.worktreeCreated, true);
324
+ assert.ok(postCalled, "Should create new worktree");
325
+ assert.strictEqual(postDirectory, "/path/to/project", "Should pass directory to create worktree");
326
+ });
327
+
328
+ it("resolveWorktreeDirectory passes directory when looking up named worktree", async () => {
329
+ let listDirectory = null;
330
+
331
+ mockServer = await createMockServer({
332
+ "GET /experimental/worktree": (req) => {
333
+ listDirectory = req.directory;
334
+ return { body: ["/worktree/my-branch"] };
335
+ },
336
+ });
337
+
338
+ const result = await resolveWorktreeDirectory(
339
+ mockServer.url,
340
+ "/path/to/project",
341
+ { worktree: "my-branch" }
342
+ );
343
+
344
+ assert.strictEqual(result.directory, "/worktree/my-branch");
345
+ assert.strictEqual(listDirectory, "/path/to/project", "Should pass directory when looking up named worktree");
346
+ });
347
+ });
@@ -567,9 +567,9 @@ describe('actions.js', () => {
567
567
  // Mock server discovery
568
568
  const mockDiscoverServer = async () => 'http://localhost:4096';
569
569
 
570
- // Mock worktree list lookup
570
+ // Mock worktree list lookup - now includes directory param
571
571
  const mockFetch = async (url) => {
572
- if (url === 'http://localhost:4096/experimental/worktree') {
572
+ if (url.includes('/experimental/worktree')) {
573
573
  return {
574
574
  ok: true,
575
575
  json: async () => [
@@ -904,4 +904,297 @@ describe('actions.js', () => {
904
904
  assert.ok(result.warning.includes('Failed to send message'), 'Warning should mention message failure');
905
905
  });
906
906
  });
907
+
908
+ describe('session reuse', () => {
909
+ test('isSessionArchived returns true when time.archived is set', async () => {
910
+ const { isSessionArchived } = await import('../../service/actions.js');
911
+
912
+ // Archived session (time.archived is a timestamp)
913
+ const archivedSession = { id: 'ses_1', time: { created: 1000, updated: 2000, archived: 3000 } };
914
+ assert.strictEqual(isSessionArchived(archivedSession), true);
915
+
916
+ // Active session (no time.archived)
917
+ const activeSession = { id: 'ses_2', time: { created: 1000, updated: 2000 } };
918
+ assert.strictEqual(isSessionArchived(activeSession), false);
919
+
920
+ // Handle edge cases
921
+ assert.strictEqual(isSessionArchived(null), false);
922
+ assert.strictEqual(isSessionArchived({}), false);
923
+ assert.strictEqual(isSessionArchived({ time: {} }), false);
924
+ });
925
+
926
+ test('selectBestSession prefers idle sessions', async () => {
927
+ const { selectBestSession } = await import('../../service/actions.js');
928
+
929
+ const sessions = [
930
+ { id: 'ses_busy', time: { updated: 3000 } },
931
+ { id: 'ses_idle', time: { updated: 2000 } },
932
+ { id: 'ses_retry', time: { updated: 1000 } },
933
+ ];
934
+
935
+ const statuses = {
936
+ 'ses_busy': { type: 'busy' },
937
+ 'ses_retry': { type: 'retry', attempt: 1, message: 'error', next: 5000 },
938
+ // ses_idle not in statuses = idle
939
+ };
940
+
941
+ const best = selectBestSession(sessions, statuses);
942
+ assert.strictEqual(best.id, 'ses_idle', 'Should prefer idle session even with older updated time');
943
+ });
944
+
945
+ test('selectBestSession falls back to most recently updated when all busy', async () => {
946
+ const { selectBestSession } = await import('../../service/actions.js');
947
+
948
+ const sessions = [
949
+ { id: 'ses_1', time: { updated: 1000 } },
950
+ { id: 'ses_2', time: { updated: 3000 } }, // most recent
951
+ { id: 'ses_3', time: { updated: 2000 } },
952
+ ];
953
+
954
+ const statuses = {
955
+ 'ses_1': { type: 'busy' },
956
+ 'ses_2': { type: 'busy' },
957
+ 'ses_3': { type: 'busy' },
958
+ };
959
+
960
+ const best = selectBestSession(sessions, statuses);
961
+ assert.strictEqual(best.id, 'ses_2', 'Should select most recently updated when all busy');
962
+ });
963
+
964
+ test('selectBestSession returns null for empty array', async () => {
965
+ const { selectBestSession } = await import('../../service/actions.js');
966
+
967
+ assert.strictEqual(selectBestSession([], {}), null);
968
+ assert.strictEqual(selectBestSession(null, {}), null);
969
+ });
970
+
971
+ test('listSessions fetches sessions filtered by directory', async () => {
972
+ const { listSessions } = await import('../../service/actions.js');
973
+
974
+ let calledUrl = null;
975
+ const mockFetch = async (url) => {
976
+ calledUrl = url;
977
+ return {
978
+ ok: true,
979
+ json: async () => [
980
+ { id: 'ses_1', directory: '/path/to/project', time: { created: 1000 } },
981
+ ],
982
+ };
983
+ };
984
+
985
+ const sessions = await listSessions('http://localhost:4096', {
986
+ directory: '/path/to/project',
987
+ fetch: mockFetch
988
+ });
989
+
990
+ assert.ok(calledUrl.includes('directory='), 'Should include directory param');
991
+ assert.ok(calledUrl.includes('roots=true'), 'Should only get root sessions');
992
+ assert.strictEqual(sessions.length, 1);
993
+ });
994
+
995
+ test('findReusableSession filters out archived sessions', async () => {
996
+ const { findReusableSession } = await import('../../service/actions.js');
997
+
998
+ const mockFetch = async (url) => {
999
+ if (url.includes('/session/status')) {
1000
+ return { ok: true, json: async () => ({}) };
1001
+ }
1002
+ // GET /session
1003
+ return {
1004
+ ok: true,
1005
+ json: async () => [
1006
+ { id: 'ses_archived', directory: '/path', time: { created: 1000, updated: 3000, archived: 4000 } },
1007
+ { id: 'ses_active', directory: '/path', time: { created: 2000, updated: 2500 } },
1008
+ ],
1009
+ };
1010
+ };
1011
+
1012
+ const session = await findReusableSession('http://localhost:4096', '/path', { fetch: mockFetch });
1013
+
1014
+ assert.ok(session, 'Should find a session');
1015
+ assert.strictEqual(session.id, 'ses_active', 'Should return the active session, not archived');
1016
+ });
1017
+
1018
+ test('findReusableSession returns null when all sessions are archived', async () => {
1019
+ const { findReusableSession } = await import('../../service/actions.js');
1020
+
1021
+ const mockFetch = async (url) => {
1022
+ if (url.includes('/session/status')) {
1023
+ return { ok: true, json: async () => ({}) };
1024
+ }
1025
+ return {
1026
+ ok: true,
1027
+ json: async () => [
1028
+ { id: 'ses_1', directory: '/path', time: { created: 1000, archived: 2000 } },
1029
+ { id: 'ses_2', directory: '/path', time: { created: 1500, archived: 2500 } },
1030
+ ],
1031
+ };
1032
+ };
1033
+
1034
+ const session = await findReusableSession('http://localhost:4096', '/path', { fetch: mockFetch });
1035
+
1036
+ assert.strictEqual(session, null, 'Should return null when all sessions are archived');
1037
+ });
1038
+
1039
+ test('executeAction reuses existing session instead of creating new', async () => {
1040
+ const { executeAction } = await import('../../service/actions.js');
1041
+
1042
+ const item = { number: 123, title: 'Fix bug' };
1043
+ const config = {
1044
+ path: tempDir,
1045
+ prompt: 'default',
1046
+ };
1047
+
1048
+ let sessionCreated = false;
1049
+ let messagePosted = false;
1050
+ let messageSessionId = null;
1051
+
1052
+ const mockFetch = async (url, opts) => {
1053
+ // GET /session - return existing active session
1054
+ if (url.includes('/session') && !url.includes('/message') && !url.includes('/status') && (!opts || opts.method !== 'POST' && opts.method !== 'PATCH')) {
1055
+ return {
1056
+ ok: true,
1057
+ json: async () => [
1058
+ { id: 'ses_existing', directory: tempDir, time: { created: 1000, updated: 2000 } },
1059
+ ],
1060
+ };
1061
+ }
1062
+ // GET /session/status
1063
+ if (url.includes('/session/status')) {
1064
+ return { ok: true, json: async () => ({}) }; // session is idle
1065
+ }
1066
+ // POST /session (create) - should NOT be called
1067
+ if (url.endsWith('/session') && opts?.method === 'POST') {
1068
+ sessionCreated = true;
1069
+ return { ok: true, json: async () => ({ id: 'ses_new' }) };
1070
+ }
1071
+ // PATCH /session/:id (update title)
1072
+ if (opts?.method === 'PATCH') {
1073
+ return { ok: true, json: async () => ({}) };
1074
+ }
1075
+ // POST /session/:id/message
1076
+ if (url.includes('/message') && opts?.method === 'POST') {
1077
+ messagePosted = true;
1078
+ messageSessionId = url.match(/session\/([^/]+)\/message/)?.[1];
1079
+ return { ok: true, json: async () => ({ success: true }) };
1080
+ }
1081
+ return { ok: false, text: async () => 'Not found' };
1082
+ };
1083
+
1084
+ const mockDiscoverServer = async () => 'http://localhost:4096';
1085
+
1086
+ const result = await executeAction(item, config, {
1087
+ discoverServer: mockDiscoverServer,
1088
+ fetch: mockFetch
1089
+ });
1090
+
1091
+ assert.ok(result.success, 'Should succeed');
1092
+ assert.strictEqual(result.sessionId, 'ses_existing', 'Should use existing session ID');
1093
+ assert.strictEqual(result.sessionReused, true, 'Should indicate session was reused');
1094
+ assert.strictEqual(sessionCreated, false, 'Should NOT create a new session');
1095
+ assert.strictEqual(messagePosted, true, 'Should post message to existing session');
1096
+ assert.strictEqual(messageSessionId, 'ses_existing', 'Should post to the existing session');
1097
+ });
1098
+
1099
+ test('executeAction creates new session when existing is archived', async () => {
1100
+ const { executeAction } = await import('../../service/actions.js');
1101
+
1102
+ const item = { number: 456, title: 'New feature' };
1103
+ const config = {
1104
+ path: tempDir,
1105
+ prompt: 'default',
1106
+ };
1107
+
1108
+ let sessionCreated = false;
1109
+
1110
+ const mockFetch = async (url, opts) => {
1111
+ // GET /session - return only archived session
1112
+ if (url.includes('/session') && !url.includes('/message') && !url.includes('/status') && (!opts || opts.method !== 'POST' && opts.method !== 'PATCH')) {
1113
+ return {
1114
+ ok: true,
1115
+ json: async () => [
1116
+ { id: 'ses_archived', directory: tempDir, time: { created: 1000, updated: 2000, archived: 3000 } },
1117
+ ],
1118
+ };
1119
+ }
1120
+ // GET /session/status
1121
+ if (url.includes('/session/status')) {
1122
+ return { ok: true, json: async () => ({}) };
1123
+ }
1124
+ // POST /session (create) - should be called since archived session can't be reused
1125
+ if (url.includes('/session') && !url.includes('/message') && opts?.method === 'POST') {
1126
+ sessionCreated = true;
1127
+ return { ok: true, json: async () => ({ id: 'ses_new' }) };
1128
+ }
1129
+ // PATCH /session/:id
1130
+ if (opts?.method === 'PATCH') {
1131
+ return { ok: true, json: async () => ({}) };
1132
+ }
1133
+ // POST /session/:id/message
1134
+ if (url.includes('/message') && opts?.method === 'POST') {
1135
+ return { ok: true, json: async () => ({ success: true }) };
1136
+ }
1137
+ return { ok: false, text: async () => 'Not found' };
1138
+ };
1139
+
1140
+ const mockDiscoverServer = async () => 'http://localhost:4096';
1141
+
1142
+ const result = await executeAction(item, config, {
1143
+ discoverServer: mockDiscoverServer,
1144
+ fetch: mockFetch
1145
+ });
1146
+
1147
+ assert.ok(result.success, 'Should succeed');
1148
+ assert.strictEqual(result.sessionId, 'ses_new', 'Should create new session');
1149
+ assert.strictEqual(result.sessionReused, undefined, 'Should NOT indicate session was reused');
1150
+ assert.strictEqual(sessionCreated, true, 'Should create a new session when existing is archived');
1151
+ });
1152
+
1153
+ test('executeAction skips session reuse when reuse_active_session is false', async () => {
1154
+ const { executeAction } = await import('../../service/actions.js');
1155
+
1156
+ const item = { number: 789, title: 'Forced new' };
1157
+ const config = {
1158
+ path: tempDir,
1159
+ prompt: 'default',
1160
+ reuse_active_session: false, // disable reuse
1161
+ };
1162
+
1163
+ let sessionListCalled = false;
1164
+ let sessionCreated = false;
1165
+
1166
+ const mockFetch = async (url, opts) => {
1167
+ // GET /session - should NOT be called
1168
+ if (url.includes('/session') && !url.includes('/message') && !url.includes('/status') && (!opts || opts.method !== 'POST' && opts.method !== 'PATCH')) {
1169
+ sessionListCalled = true;
1170
+ return { ok: true, json: async () => [] };
1171
+ }
1172
+ // POST /session
1173
+ if (url.includes('/session') && !url.includes('/message') && opts?.method === 'POST') {
1174
+ sessionCreated = true;
1175
+ return { ok: true, json: async () => ({ id: 'ses_forced_new' }) };
1176
+ }
1177
+ // PATCH
1178
+ if (opts?.method === 'PATCH') {
1179
+ return { ok: true, json: async () => ({}) };
1180
+ }
1181
+ // POST message
1182
+ if (url.includes('/message') && opts?.method === 'POST') {
1183
+ return { ok: true, json: async () => ({ success: true }) };
1184
+ }
1185
+ return { ok: false, text: async () => 'Not found' };
1186
+ };
1187
+
1188
+ const mockDiscoverServer = async () => 'http://localhost:4096';
1189
+
1190
+ const result = await executeAction(item, config, {
1191
+ discoverServer: mockDiscoverServer,
1192
+ fetch: mockFetch
1193
+ });
1194
+
1195
+ assert.ok(result.success, 'Should succeed');
1196
+ assert.strictEqual(sessionListCalled, false, 'Should NOT list sessions when reuse disabled');
1197
+ assert.strictEqual(sessionCreated, true, 'Should create new session directly');
1198
+ });
1199
+ });
907
1200
  });