@flrande/browserctl 0.4.0-dev.15.1 → 0.5.0-dev.19.1

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.
Files changed (52) hide show
  1. package/apps/browserctl/src/commands/act.test.ts +71 -0
  2. package/apps/browserctl/src/commands/act.ts +45 -1
  3. package/apps/browserctl/src/commands/command-wrappers.test.ts +302 -0
  4. package/apps/browserctl/src/commands/console-list.test.ts +102 -0
  5. package/apps/browserctl/src/commands/console-list.ts +89 -1
  6. package/apps/browserctl/src/commands/har-export.test.ts +112 -0
  7. package/apps/browserctl/src/commands/har-export.ts +120 -0
  8. package/apps/browserctl/src/commands/memory-delete.ts +20 -0
  9. package/apps/browserctl/src/commands/memory-inspect.ts +20 -0
  10. package/apps/browserctl/src/commands/memory-list.ts +90 -0
  11. package/apps/browserctl/src/commands/memory-mode-set.ts +29 -0
  12. package/apps/browserctl/src/commands/memory-purge.ts +16 -0
  13. package/apps/browserctl/src/commands/memory-resolve.ts +56 -0
  14. package/apps/browserctl/src/commands/memory-status.ts +16 -0
  15. package/apps/browserctl/src/commands/memory-ttl-set.ts +28 -0
  16. package/apps/browserctl/src/commands/memory-upsert.ts +142 -0
  17. package/apps/browserctl/src/commands/network-list.test.ts +110 -0
  18. package/apps/browserctl/src/commands/network-list.ts +112 -0
  19. package/apps/browserctl/src/commands/session-drop.test.ts +36 -0
  20. package/apps/browserctl/src/commands/session-drop.ts +16 -0
  21. package/apps/browserctl/src/commands/session-list.test.ts +81 -0
  22. package/apps/browserctl/src/commands/session-list.ts +70 -0
  23. package/apps/browserctl/src/commands/trace-get.test.ts +61 -0
  24. package/apps/browserctl/src/commands/trace-get.ts +62 -0
  25. package/apps/browserctl/src/commands/wait-element.test.ts +80 -0
  26. package/apps/browserctl/src/commands/wait-element.ts +76 -0
  27. package/apps/browserctl/src/commands/wait-text.test.ts +110 -0
  28. package/apps/browserctl/src/commands/wait-text.ts +93 -0
  29. package/apps/browserctl/src/commands/wait-url.test.ts +80 -0
  30. package/apps/browserctl/src/commands/wait-url.ts +76 -0
  31. package/apps/browserctl/src/main.dispatch.test.ts +206 -1
  32. package/apps/browserctl/src/main.test.ts +30 -0
  33. package/apps/browserctl/src/main.ts +246 -4
  34. package/apps/browserd/src/container.ts +1603 -48
  35. package/apps/browserd/src/main.test.ts +538 -1
  36. package/apps/browserd/src/tool-matrix.test.ts +492 -3
  37. package/package.json +5 -1
  38. package/packages/core/src/driver.ts +1 -1
  39. package/packages/core/src/index.ts +1 -0
  40. package/packages/core/src/navigation-memory.test.ts +259 -0
  41. package/packages/core/src/navigation-memory.ts +360 -0
  42. package/packages/core/src/session-store.test.ts +33 -0
  43. package/packages/core/src/session-store.ts +111 -6
  44. package/packages/driver-chrome-relay/src/chrome-relay-driver.test.ts +112 -2
  45. package/packages/driver-chrome-relay/src/chrome-relay-driver.ts +233 -10
  46. package/packages/driver-managed/src/managed-driver.test.ts +124 -0
  47. package/packages/driver-managed/src/managed-driver.ts +233 -17
  48. package/packages/driver-managed/src/managed-local-driver.test.ts +104 -2
  49. package/packages/driver-managed/src/managed-local-driver.ts +232 -10
  50. package/packages/driver-remote-cdp/src/remote-cdp-driver.test.ts +112 -2
  51. package/packages/driver-remote-cdp/src/remote-cdp-driver.ts +232 -10
  52. package/packages/transport-mcp-stdio/src/tool-map.ts +18 -1
@@ -111,6 +111,14 @@ describe("browserd container", () => {
111
111
  expect(config.downloadRoot).toBeUndefined();
112
112
  expect(config.authToken).toBeUndefined();
113
113
  expect(config.authScopes).toEqual(["read", "act", "upload", "download"]);
114
+ expect(config.sessionTtlMs).toBe(30 * 60 * 1_000);
115
+ expect(config.sessionCleanupIntervalMs).toBe(60 * 1_000);
116
+ expect(config.domainAllowlistMode).toBe("off");
117
+ expect(config.domainAllowlist).toEqual([]);
118
+ expect(config.sessionMaxTotal).toBe(200);
119
+ expect(config.sessionMaxPerTenant).toBe(50);
120
+ expect(config.sessionRequireTenantPrefix).toBe(false);
121
+ expect(config.tenantAllowlist).toEqual([]);
114
122
  });
115
123
 
116
124
  it("registers managed-local, managed, chrome-relay, and remote-cdp drivers by default", () => {
@@ -129,7 +137,15 @@ describe("browserd container", () => {
129
137
  BROWSERD_UPLOAD_ROOT: "C:\\safe\\uploads",
130
138
  BROWSERD_DOWNLOAD_ROOT: "C:\\safe\\downloads",
131
139
  BROWSERD_AUTH_TOKEN: "test-token",
132
- BROWSERD_AUTH_SCOPES: " read, download "
140
+ BROWSERD_AUTH_SCOPES: " read, download ",
141
+ BROWSERD_SESSION_TTL_MS: "120000",
142
+ BROWSERD_SESSION_CLEANUP_INTERVAL_MS: "5000",
143
+ BROWSERD_DOMAIN_ALLOWLIST_MODE: "enforce",
144
+ BROWSERD_DOMAIN_ALLOWLIST: "example.com,*.corp.local",
145
+ BROWSERD_SESSION_MAX_TOTAL: "2",
146
+ BROWSERD_SESSION_MAX_PER_TENANT: "1",
147
+ BROWSERD_SESSION_REQUIRE_TENANT_PREFIX: "true",
148
+ BROWSERD_TENANT_ALLOWLIST: "finance,ops"
133
149
  });
134
150
 
135
151
  expect(config.remoteCdpUrl).toBe("http://127.0.0.1:9333/devtools/browser/override");
@@ -140,6 +156,37 @@ describe("browserd container", () => {
140
156
  expect(config.downloadRoot).toBe("C:\\safe\\downloads");
141
157
  expect(config.authToken).toBe("test-token");
142
158
  expect(config.authScopes).toEqual(["read", "download"]);
159
+ expect(config.sessionTtlMs).toBe(120000);
160
+ expect(config.sessionCleanupIntervalMs).toBe(5000);
161
+ expect(config.domainAllowlistMode).toBe("enforce");
162
+ expect(config.domainAllowlist).toEqual(["example.com", "*.corp.local"]);
163
+ expect(config.sessionMaxTotal).toBe(2);
164
+ expect(config.sessionMaxPerTenant).toBe(1);
165
+ expect(config.sessionRequireTenantPrefix).toBe(true);
166
+ expect(config.tenantAllowlist).toEqual(["finance", "ops"]);
167
+ });
168
+
169
+ it("uses memory defaults in loadBrowserdConfig", () => {
170
+ const config = loadBrowserdConfig({});
171
+
172
+ expect(config.memoryEnabled).toBe(true);
173
+ expect(config.memoryMode).toBe("ask");
174
+ expect(config.memoryTtlDays).toBe(30);
175
+ expect(config.memoryPath).toBe(".browserctl-runtime/navigation-memory.json");
176
+ });
177
+
178
+ it("uses memory env overrides in loadBrowserdConfig", () => {
179
+ const config = loadBrowserdConfig({
180
+ BROWSERD_MEMORY_ENABLED: "false",
181
+ BROWSERD_MEMORY_MODE: "auto",
182
+ BROWSERD_MEMORY_TTL_DAYS: "7",
183
+ BROWSERD_MEMORY_PATH: ".browserctl-runtime/memory-custom.json"
184
+ });
185
+
186
+ expect(config.memoryEnabled).toBe(false);
187
+ expect(config.memoryMode).toBe("auto");
188
+ expect(config.memoryTtlDays).toBe(7);
189
+ expect(config.memoryPath).toBe(".browserctl-runtime/memory-custom.json");
143
190
  });
144
191
 
145
192
  it("parses chrome relay extension mode env values", () => {
@@ -677,6 +724,87 @@ describe("browserd bootstrap", () => {
677
724
  output.end();
678
725
  });
679
726
 
727
+ it("supports POSIX-style upload/download allowlist roots", async () => {
728
+ const input = new PassThrough();
729
+ const output = new PassThrough();
730
+ const runtime = bootstrapBrowserd({
731
+ env: createTestEnv({
732
+ BROWSERD_DEFAULT_DRIVER: "managed",
733
+ BROWSERD_UPLOAD_ROOT: "/allowed/uploads",
734
+ BROWSERD_DOWNLOAD_ROOT: "/allowed/downloads"
735
+ }),
736
+ input,
737
+ output,
738
+ stdioProtocol: "legacy"
739
+ });
740
+
741
+ const openResponse = await sendToolRequest(input, output, {
742
+ id: "request-posix-open",
743
+ name: "browser.tab.open",
744
+ traceId: "trace:posix:open",
745
+ arguments: {
746
+ sessionId: "session:posix-root",
747
+ url: "https://example.com/posix"
748
+ }
749
+ });
750
+
751
+ const targetId = (openResponse.data as { targetId: string }).targetId;
752
+
753
+ const uploadResponse = await sendToolRequest(input, output, {
754
+ id: "request-posix-upload",
755
+ name: "browser.upload.arm",
756
+ traceId: "trace:posix:upload",
757
+ arguments: {
758
+ sessionId: "session:posix-root",
759
+ targetId,
760
+ files: ["reports/q1.csv"]
761
+ }
762
+ });
763
+
764
+ expect(uploadResponse.ok).toBe(true);
765
+ expect(uploadResponse.data).toMatchObject({
766
+ files: ["/allowed/uploads/reports/q1.csv"]
767
+ });
768
+
769
+ const downloadResponse = await sendToolRequest(input, output, {
770
+ id: "request-posix-download",
771
+ name: "browser.download.wait",
772
+ traceId: "trace:posix:download",
773
+ arguments: {
774
+ sessionId: "session:posix-root",
775
+ targetId,
776
+ path: "reports/out.bin"
777
+ }
778
+ });
779
+
780
+ expect(downloadResponse.ok).toBe(true);
781
+ expect(downloadResponse.data).toMatchObject({
782
+ download: {
783
+ path: "/allowed/downloads/reports/out.bin"
784
+ }
785
+ });
786
+
787
+ const traversalResponse = await sendToolRequest(input, output, {
788
+ id: "request-posix-download-traversal",
789
+ name: "browser.download.wait",
790
+ traceId: "trace:posix:download:traversal",
791
+ arguments: {
792
+ sessionId: "session:posix-root",
793
+ targetId,
794
+ path: "../escape.bin"
795
+ }
796
+ });
797
+
798
+ expect(traversalResponse.ok).toBe(false);
799
+ expect(traversalResponse.error).toMatchObject({
800
+ code: "E_PERMISSION"
801
+ });
802
+
803
+ runtime.close();
804
+ input.end();
805
+ output.end();
806
+ });
807
+
680
808
  it("rejects download-wait when download root is not configured", async () => {
681
809
  const input = new PassThrough();
682
810
  const output = new PassThrough();
@@ -811,6 +939,66 @@ describe("browserd bootstrap", () => {
811
939
  entries: []
812
940
  });
813
941
 
942
+ const consoleFilteredResponse = await sendToolRequest(input, output, {
943
+ id: "request-console-filtered",
944
+ name: "browser.console.list",
945
+ traceId: "trace:tools:console:filtered",
946
+ arguments: {
947
+ sessionId: "session:tools",
948
+ targetId,
949
+ type: "error",
950
+ contains: "timeout",
951
+ since: "2026-01-01T00:00:00.000Z",
952
+ limit: 10
953
+ }
954
+ });
955
+
956
+ expect(consoleFilteredResponse.ok).toBe(true);
957
+ expect(consoleFilteredResponse.data).toEqual({
958
+ driver: "managed",
959
+ targetId,
960
+ entries: []
961
+ });
962
+
963
+ const networkListResponse = await sendToolRequest(input, output, {
964
+ id: "request-network-list",
965
+ name: "browser.network.list",
966
+ traceId: "trace:tools:network:list",
967
+ arguments: {
968
+ sessionId: "session:tools",
969
+ targetId,
970
+ limit: 20
971
+ }
972
+ });
973
+
974
+ expect(networkListResponse.ok).toBe(true);
975
+ expect(networkListResponse.data).toEqual({
976
+ driver: "managed",
977
+ targetId,
978
+ requests: []
979
+ });
980
+
981
+ const harExportResponse = await sendToolRequest(input, output, {
982
+ id: "request-network-har-export",
983
+ name: "browser.network.harExport",
984
+ traceId: "trace:tools:network:har",
985
+ arguments: {
986
+ sessionId: "session:tools",
987
+ targetId
988
+ }
989
+ });
990
+
991
+ expect(harExportResponse.ok).toBe(true);
992
+ expect(harExportResponse.data).toMatchObject({
993
+ driver: "managed",
994
+ targetId,
995
+ har: {
996
+ log: {
997
+ entries: []
998
+ }
999
+ }
1000
+ });
1001
+
814
1002
  const networkResponse = await sendToolRequest(input, output, {
815
1003
  id: "request-network",
816
1004
  name: "browser.network.responseBody",
@@ -831,6 +1019,29 @@ describe("browserd bootstrap", () => {
831
1019
  encoding: "utf8"
832
1020
  });
833
1021
 
1022
+ const traceResponse = await sendToolRequest(input, output, {
1023
+ id: "request-trace-get",
1024
+ name: "browser.trace.get",
1025
+ traceId: "trace:tools:trace",
1026
+ arguments: {
1027
+ sessionId: "session:tools",
1028
+ limit: 50
1029
+ }
1030
+ });
1031
+
1032
+ expect(traceResponse.ok).toBe(true);
1033
+ expect(traceResponse.data).toMatchObject({
1034
+ sessionId: "session:tools"
1035
+ });
1036
+ const tracePayload = traceResponse.data as {
1037
+ steps?: Array<{ tool?: string }>;
1038
+ keyResponses?: Array<{ kind?: string }>;
1039
+ };
1040
+ expect(Array.isArray(tracePayload.steps)).toBe(true);
1041
+ expect(tracePayload.steps?.some((step) => step.tool === "browser.console.list")).toBe(true);
1042
+ expect(Array.isArray(tracePayload.keyResponses)).toBe(true);
1043
+ expect(tracePayload.keyResponses?.some((event) => event.kind === "network.list")).toBe(true);
1044
+
834
1045
  const screenshotResponse = await sendToolRequest(input, output, {
835
1046
  id: "request-screenshot",
836
1047
  name: "browser.screenshot",
@@ -851,6 +1062,332 @@ describe("browserd bootstrap", () => {
851
1062
  output.end();
852
1063
  });
853
1064
 
1065
+ it("enforces domain allowlist for tab.open", async () => {
1066
+ const input = new PassThrough();
1067
+ const output = new PassThrough();
1068
+ const runtime = bootstrapBrowserd({
1069
+ env: createTestEnv({
1070
+ BROWSERD_DEFAULT_DRIVER: "managed",
1071
+ BROWSERD_DOMAIN_ALLOWLIST_MODE: "enforce",
1072
+ BROWSERD_DOMAIN_ALLOWLIST: "example.com,*.corp.local"
1073
+ }),
1074
+ input,
1075
+ output,
1076
+ stdioProtocol: "legacy"
1077
+ });
1078
+
1079
+ const allowed = await sendToolRequest(input, output, {
1080
+ id: "request-domain-allowed",
1081
+ name: "browser.tab.open",
1082
+ traceId: "trace:domain:allowed",
1083
+ arguments: {
1084
+ sessionId: "finance:domain-allowed",
1085
+ url: "https://example.com/home"
1086
+ }
1087
+ });
1088
+
1089
+ expect(allowed.ok).toBe(true);
1090
+
1091
+ const blocked = await sendToolRequest(input, output, {
1092
+ id: "request-domain-blocked",
1093
+ name: "browser.tab.open",
1094
+ traceId: "trace:domain:blocked",
1095
+ arguments: {
1096
+ sessionId: "finance:domain-blocked",
1097
+ url: "https://evil.test/home"
1098
+ }
1099
+ });
1100
+
1101
+ expect(blocked.ok).toBe(false);
1102
+ expect(blocked.error).toMatchObject({
1103
+ code: "E_PERMISSION"
1104
+ });
1105
+
1106
+ runtime.close();
1107
+ input.end();
1108
+ output.end();
1109
+ });
1110
+
1111
+ it("enforces domain allowlist for act navigate/goto", async () => {
1112
+ const input = new PassThrough();
1113
+ const output = new PassThrough();
1114
+ const runtime = bootstrapBrowserd({
1115
+ env: createTestEnv({
1116
+ BROWSERD_DEFAULT_DRIVER: "managed",
1117
+ BROWSERD_DOMAIN_ALLOWLIST_MODE: "enforce",
1118
+ BROWSERD_DOMAIN_ALLOWLIST: "example.com"
1119
+ }),
1120
+ input,
1121
+ output,
1122
+ stdioProtocol: "legacy"
1123
+ });
1124
+
1125
+ const openResponse = await sendToolRequest(input, output, {
1126
+ id: "request-domain-act-open",
1127
+ name: "browser.tab.open",
1128
+ traceId: "trace:domain:act:open",
1129
+ arguments: {
1130
+ sessionId: "finance:domain-act",
1131
+ url: "https://example.com"
1132
+ }
1133
+ });
1134
+ const targetId = (openResponse.data as { targetId: string }).targetId;
1135
+
1136
+ const actResponse = await sendToolRequest(input, output, {
1137
+ id: "request-domain-act-blocked",
1138
+ name: "browser.act",
1139
+ traceId: "trace:domain:act:blocked",
1140
+ arguments: {
1141
+ sessionId: "finance:domain-act",
1142
+ targetId,
1143
+ action: {
1144
+ type: "navigate",
1145
+ payload: {
1146
+ url: "https://unauthorized.example.net"
1147
+ }
1148
+ }
1149
+ }
1150
+ });
1151
+
1152
+ expect(actResponse.ok).toBe(false);
1153
+ expect(actResponse.error).toMatchObject({
1154
+ code: "E_PERMISSION"
1155
+ });
1156
+
1157
+ runtime.close();
1158
+ input.end();
1159
+ output.end();
1160
+ });
1161
+
1162
+ it("enforces max total session limit", async () => {
1163
+ const input = new PassThrough();
1164
+ const output = new PassThrough();
1165
+ const runtime = bootstrapBrowserd({
1166
+ env: createTestEnv({
1167
+ BROWSERD_DEFAULT_DRIVER: "managed",
1168
+ BROWSERD_SESSION_MAX_TOTAL: "1"
1169
+ }),
1170
+ input,
1171
+ output,
1172
+ stdioProtocol: "legacy"
1173
+ });
1174
+
1175
+ const first = await sendToolRequest(input, output, {
1176
+ id: "request-session-limit-first",
1177
+ name: "browser.status",
1178
+ traceId: "trace:session-limit:first",
1179
+ arguments: {
1180
+ sessionId: "team-a:first"
1181
+ }
1182
+ });
1183
+ expect(first.ok).toBe(true);
1184
+
1185
+ const second = await sendToolRequest(input, output, {
1186
+ id: "request-session-limit-second",
1187
+ name: "browser.status",
1188
+ traceId: "trace:session-limit:second",
1189
+ arguments: {
1190
+ sessionId: "team-b:second"
1191
+ }
1192
+ });
1193
+ expect(second.ok).toBe(false);
1194
+ expect(second.error).toMatchObject({
1195
+ code: "E_CONFLICT"
1196
+ });
1197
+
1198
+ runtime.close();
1199
+ input.end();
1200
+ output.end();
1201
+ });
1202
+
1203
+ it("enforces tenant prefix, allowlist, and per-tenant session limit", async () => {
1204
+ const input = new PassThrough();
1205
+ const output = new PassThrough();
1206
+ const runtime = bootstrapBrowserd({
1207
+ env: createTestEnv({
1208
+ BROWSERD_DEFAULT_DRIVER: "managed",
1209
+ BROWSERD_SESSION_REQUIRE_TENANT_PREFIX: "true",
1210
+ BROWSERD_TENANT_ALLOWLIST: "finance,ops",
1211
+ BROWSERD_SESSION_MAX_TOTAL: "10",
1212
+ BROWSERD_SESSION_MAX_PER_TENANT: "1"
1213
+ }),
1214
+ input,
1215
+ output,
1216
+ stdioProtocol: "legacy"
1217
+ });
1218
+
1219
+ const missingTenant = await sendToolRequest(input, output, {
1220
+ id: "request-tenant-missing",
1221
+ name: "browser.status",
1222
+ traceId: "trace:tenant:missing",
1223
+ arguments: {
1224
+ sessionId: "missingTenant"
1225
+ }
1226
+ });
1227
+ expect(missingTenant.ok).toBe(false);
1228
+ expect(missingTenant.error).toMatchObject({
1229
+ code: "E_PERMISSION"
1230
+ });
1231
+
1232
+ const disallowedTenant = await sendToolRequest(input, output, {
1233
+ id: "request-tenant-disallowed",
1234
+ name: "browser.status",
1235
+ traceId: "trace:tenant:disallowed",
1236
+ arguments: {
1237
+ sessionId: "personal:task-1"
1238
+ }
1239
+ });
1240
+ expect(disallowedTenant.ok).toBe(false);
1241
+ expect(disallowedTenant.error).toMatchObject({
1242
+ code: "E_PERMISSION"
1243
+ });
1244
+
1245
+ const financeFirst = await sendToolRequest(input, output, {
1246
+ id: "request-tenant-finance-first",
1247
+ name: "browser.status",
1248
+ traceId: "trace:tenant:finance:first",
1249
+ arguments: {
1250
+ sessionId: "finance:task-1"
1251
+ }
1252
+ });
1253
+ expect(financeFirst.ok).toBe(true);
1254
+
1255
+ const financeSecond = await sendToolRequest(input, output, {
1256
+ id: "request-tenant-finance-second",
1257
+ name: "browser.status",
1258
+ traceId: "trace:tenant:finance:second",
1259
+ arguments: {
1260
+ sessionId: "finance:task-2"
1261
+ }
1262
+ });
1263
+ expect(financeSecond.ok).toBe(false);
1264
+ expect(financeSecond.error).toMatchObject({
1265
+ code: "E_CONFLICT"
1266
+ });
1267
+
1268
+ const opsFirst = await sendToolRequest(input, output, {
1269
+ id: "request-tenant-ops-first",
1270
+ name: "browser.status",
1271
+ traceId: "trace:tenant:ops:first",
1272
+ arguments: {
1273
+ sessionId: "ops:task-1"
1274
+ }
1275
+ });
1276
+ expect(opsFirst.ok).toBe(true);
1277
+
1278
+ runtime.close();
1279
+ input.end();
1280
+ output.end();
1281
+ });
1282
+
1283
+ it("supports session-list and session-drop governance tools", async () => {
1284
+ const input = new PassThrough();
1285
+ const output = new PassThrough();
1286
+ const runtime = bootstrapBrowserd({
1287
+ env: createTestEnv({
1288
+ BROWSERD_DEFAULT_DRIVER: "managed"
1289
+ }),
1290
+ input,
1291
+ output,
1292
+ stdioProtocol: "legacy"
1293
+ });
1294
+
1295
+ const financeStatus = await sendToolRequest(input, output, {
1296
+ id: "request-session-governance-finance",
1297
+ name: "browser.status",
1298
+ traceId: "trace:session-governance:finance",
1299
+ arguments: {
1300
+ sessionId: "finance:task-1"
1301
+ }
1302
+ });
1303
+ expect(financeStatus.ok).toBe(true);
1304
+
1305
+ const opsStatus = await sendToolRequest(input, output, {
1306
+ id: "request-session-governance-ops",
1307
+ name: "browser.status",
1308
+ traceId: "trace:session-governance:ops",
1309
+ arguments: {
1310
+ sessionId: "ops:task-1"
1311
+ }
1312
+ });
1313
+ expect(opsStatus.ok).toBe(true);
1314
+
1315
+ const listBeforeDrop = await sendToolRequest(input, output, {
1316
+ id: "request-session-list-before",
1317
+ name: "browser.session.list",
1318
+ traceId: "trace:session-governance:list:before",
1319
+ arguments: {
1320
+ sessionId: "finance:task-1",
1321
+ limit: 20
1322
+ }
1323
+ });
1324
+ expect(listBeforeDrop.ok).toBe(true);
1325
+ const listPayloadBeforeDrop = listBeforeDrop.data as { sessions: Array<{ sessionId: string }> };
1326
+ expect(Array.isArray(listPayloadBeforeDrop.sessions)).toBe(true);
1327
+ expect(listPayloadBeforeDrop.sessions.some((entry) => entry.sessionId === "ops:task-1")).toBe(true);
1328
+
1329
+ const dropResponse = await sendToolRequest(input, output, {
1330
+ id: "request-session-drop",
1331
+ name: "browser.session.drop",
1332
+ traceId: "trace:session-governance:drop",
1333
+ arguments: {
1334
+ sessionId: "finance:task-1",
1335
+ sessionIdToDelete: "ops:task-1"
1336
+ }
1337
+ });
1338
+ expect(dropResponse.ok).toBe(false);
1339
+ expect(dropResponse.error).toMatchObject({
1340
+ code: "E_PERMISSION"
1341
+ });
1342
+
1343
+ const listAfterDrop = await sendToolRequest(input, output, {
1344
+ id: "request-session-list-after",
1345
+ name: "browser.session.list",
1346
+ traceId: "trace:session-governance:list:after",
1347
+ arguments: {
1348
+ sessionId: "finance:task-1",
1349
+ limit: 20
1350
+ }
1351
+ });
1352
+ expect(listAfterDrop.ok).toBe(true);
1353
+ const listPayloadAfterDrop = listAfterDrop.data as { sessions: Array<{ sessionId: string }> };
1354
+ expect(listPayloadAfterDrop.sessions.some((entry) => entry.sessionId === "ops:task-1")).toBe(true);
1355
+
1356
+ const selfDropResponse = await sendToolRequest(input, output, {
1357
+ id: "request-session-drop-self",
1358
+ name: "browser.session.drop",
1359
+ traceId: "trace:session-governance:drop:self",
1360
+ arguments: {
1361
+ sessionId: "finance:task-1",
1362
+ sessionIdToDelete: "finance:task-1"
1363
+ }
1364
+ });
1365
+ expect(selfDropResponse.ok).toBe(true);
1366
+ expect(selfDropResponse.data).toMatchObject({
1367
+ sessionIdToDelete: "finance:task-1",
1368
+ dropped: true
1369
+ });
1370
+
1371
+ const listAfterSelfDrop = await sendToolRequest(input, output, {
1372
+ id: "request-session-list-after-self-drop",
1373
+ name: "browser.session.list",
1374
+ traceId: "trace:session-governance:list:after:self",
1375
+ arguments: {
1376
+ sessionId: "ops:task-1",
1377
+ limit: 20
1378
+ }
1379
+ });
1380
+ expect(listAfterSelfDrop.ok).toBe(true);
1381
+ const listPayloadAfterSelfDrop = listAfterSelfDrop.data as {
1382
+ sessions: Array<{ sessionId: string }>;
1383
+ };
1384
+ expect(listPayloadAfterSelfDrop.sessions.some((entry) => entry.sessionId === "finance:task-1")).toBe(false);
1385
+
1386
+ runtime.close();
1387
+ input.end();
1388
+ output.end();
1389
+ });
1390
+
854
1391
  it("preserves id and trace/session metadata when queue-level error handling runs", async () => {
855
1392
  const input = new PassThrough();
856
1393
  const output = new PassThrough();