@agentrq/acp-gateway 0.2.4 → 0.2.6

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,7 +1,17 @@
1
- import { describe, it, expect, vi, beforeEach } from "vitest";
1
+ import { describe, it, expect, vi, beforeEach, afterEach } from "vitest";
2
+ import { EventEmitter } from "node:events";
2
3
  import * as acp from "@agentclientprotocol/sdk";
3
- import { createAcpSessionSwitcher, checkForNextTask, mapMcpServers, TaskQueue, getOrCreateSession, activeSessions, authConfig, createSessionWithAuth, isInteractiveTerminal, openAgentConnection, parseGatewayArgs, assertAgentRunnable, helpText, isRunnable, printHelp, resolveAgentCommand, runListAgents, pickHumanApprovalMode, enforceHumanApprovalMode, runAuthCommand, } from "../index.js";
4
+ import { createAcpSessionSwitcher, checkForNextTask, mapMcpServers, TaskQueue, getOrCreateSession, activeSessions, authConfig, createSessionWithAuth, isInteractiveTerminal, openAgentConnection, parseGatewayArgs, assertAgentRunnable, helpText, isRunnable, printHelp, resolveAgentCommand, runListAgents, pickHumanApprovalMode, enforceHumanApprovalMode, handleAgentModeChange, runAgentCommand, findActiveSession, handleTaskCancellation, cancelledTaskSeq, markTaskCancelled, isTaskCancelled, nextTaskSeq, } from "../index.js";
4
5
  import { AUTH_REQUIRED_CODE } from "../auth.js";
6
+ /**
7
+ * A stand-in for the workspace bridge.
8
+ *
9
+ * The ACP client registers a verdict listener on it as soon as it is built, so
10
+ * a bare object is not enough — the real bridge is an EventEmitter.
11
+ */
12
+ function fakeBridge() {
13
+ return new EventEmitter();
14
+ }
5
15
  // Real emitters so the agent-process lifecycle handlers can be exercised.
6
16
  const { spawnedAgents } = vi.hoisted(() => ({ spawnedAgents: [] }));
7
17
  vi.mock("node:child_process", async () => {
@@ -66,6 +76,44 @@ describe("index", () => {
66
76
  headers: []
67
77
  }]);
68
78
  });
79
+ it("should map an sse server as its own transport", () => {
80
+ const configs = [{
81
+ type: "sse",
82
+ name: "events",
83
+ url: "http://localhost:8000/sse",
84
+ }];
85
+ expect(mapMcpServers(configs, { mcpCapabilities: { sse: true } })).toEqual([{
86
+ type: "sse",
87
+ name: "events",
88
+ url: "http://localhost:8000/sse",
89
+ headers: [],
90
+ }]);
91
+ });
92
+ it("should leave out a transport the agent says it does not support", () => {
93
+ const errorSpy = vi.spyOn(console, "error").mockImplementation(() => { });
94
+ const configs = [
95
+ { type: "http", name: "remote", url: "http://localhost:8000" },
96
+ { type: "stdio", name: "local", command: "npx" },
97
+ ];
98
+ // An agent may refuse the whole session/new rather than skip one entry.
99
+ const result = mapMcpServers(configs, {
100
+ mcpCapabilities: { http: false, sse: false },
101
+ });
102
+ expect(result.map((s) => s.name)).toEqual(["local"]);
103
+ expect(errorSpy.mock.calls.flat().join("\n")).toContain('Not passing MCP server "remote"');
104
+ errorSpy.mockRestore();
105
+ });
106
+ it("should still hand over a transport the agent never mentions", () => {
107
+ const errorSpy = vi.spyOn(console, "error").mockImplementation(() => { });
108
+ const configs = [
109
+ { type: "http", name: "remote", url: "http://localhost:8000" },
110
+ ];
111
+ // A terse agent is likelier than one that genuinely cannot reach HTTP,
112
+ // and dropping this would take the workspace's own server away from it.
113
+ expect(mapMcpServers(configs, {}).map((s) => s.name)).toEqual(["remote"]);
114
+ expect(mapMcpServers(configs).map((s) => s.name)).toEqual(["remote"]);
115
+ errorSpy.mockRestore();
116
+ });
69
117
  it("should correctly map stdio servers", () => {
70
118
  const configs = [{
71
119
  type: "stdio",
@@ -328,7 +376,7 @@ describe("index", () => {
328
376
  activeSessions.clear();
329
377
  });
330
378
  it("should spawn a new session when not cached", async () => {
331
- const mockBridge = {};
379
+ const mockBridge = fakeBridge();
332
380
  const configs = [];
333
381
  const agentrqConfig = { env: {} };
334
382
  const session = await getOrCreateSession("T-New", ["node", "agent.js"], configs, agentrqConfig, mockBridge);
@@ -337,7 +385,7 @@ describe("index", () => {
337
385
  expect(activeSessions.has("T-New")).toBe(true);
338
386
  });
339
387
  it("should return cached session when already created", async () => {
340
- const mockBridge = {};
388
+ const mockBridge = fakeBridge();
341
389
  const configs = [];
342
390
  const agentrqConfig = { env: {} };
343
391
  const session1 = await getOrCreateSession("T-Cache", ["node", "agent.js"], configs, agentrqConfig, mockBridge);
@@ -345,7 +393,7 @@ describe("index", () => {
345
393
  expect(session1).toBe(session2);
346
394
  });
347
395
  it("should declare elicitation support when initializing the ACP connection", async () => {
348
- const mockBridge = {};
396
+ const mockBridge = fakeBridge();
349
397
  const configs = [];
350
398
  const agentrqConfig = { env: {} };
351
399
  const session = await getOrCreateSession("T-Elicit", ["node", "agent.js"], configs, agentrqConfig, mockBridge);
@@ -356,7 +404,7 @@ describe("index", () => {
356
404
  }));
357
405
  });
358
406
  it("should declare whether it can host a terminal login", async () => {
359
- const mockBridge = {};
407
+ const mockBridge = fakeBridge();
360
408
  const session = await getOrCreateSession("T-Auth-Cap", ["node", "agent.js"], [], { env: {} }, mockBridge);
361
409
  expect(session.connection.initialize).toHaveBeenCalledWith(expect.objectContaining({
362
410
  clientCapabilities: expect.objectContaining({
@@ -381,13 +429,13 @@ describe("index", () => {
381
429
  prompt: vi.fn(),
382
430
  };
383
431
  });
384
- const session = await getOrCreateSession("T-Login", ["node", "agent.js"], [], { env: {} }, {});
432
+ const session = await getOrCreateSession("T-Login", ["node", "agent.js"], [], { env: {} }, fakeBridge());
385
433
  expect(authenticate).toHaveBeenCalledWith({ methodId: "agent-login" });
386
434
  expect(newSession).toHaveBeenCalledTimes(2);
387
435
  expect(session.sessionId).toBe("authed-sess");
388
436
  });
389
437
  it("should drop the cached session when the agent process dies", async () => {
390
- const mockBridge = {};
438
+ const mockBridge = fakeBridge();
391
439
  await getOrCreateSession("T-Exit", ["node", "agent.js"], [], { env: {} }, mockBridge);
392
440
  expect(activeSessions.has("T-Exit")).toBe(true);
393
441
  spawnedAgents[spawnedAgents.length - 1].emit("exit", 1, null);
@@ -411,7 +459,7 @@ describe("index", () => {
411
459
  const onExit = vi.fn();
412
460
  const agent = await openAgentConnection({
413
461
  acpCmdArgs: ["node", "agent.js"],
414
- mcpBridge: {},
462
+ mcpBridge: fakeBridge(),
415
463
  label: "login",
416
464
  onExit,
417
465
  });
@@ -450,11 +498,22 @@ describe("index", () => {
450
498
  it("should default to bridging tasks with a concurrency of 2", () => {
451
499
  expect(parseGatewayArgs([])).toEqual({
452
500
  maxConcurrency: 2,
501
+ permissionTimeoutMs: 30 * 60_000,
453
502
  command: "run",
454
503
  allowUnverifiedAgent: false,
455
504
  rest: [],
456
505
  });
457
506
  });
507
+ it("should take the permission timeout in minutes", () => {
508
+ expect(parseGatewayArgs(["--permission-timeout", "5"]).permissionTimeoutMs).toBe(300_000);
509
+ // Zero is a deliberate "wait indefinitely", so it must not be ignored.
510
+ expect(parseGatewayArgs(["--permission-timeout", "0"]).permissionTimeoutMs).toBe(0);
511
+ });
512
+ it("should keep the default when the timeout makes no sense", () => {
513
+ expect(parseGatewayArgs(["--permission-timeout"]).permissionTimeoutMs).toBe(30 * 60_000);
514
+ expect(parseGatewayArgs(["--permission-timeout", "soon"]).permissionTimeoutMs).toBe(30 * 60_000);
515
+ expect(parseGatewayArgs(["--permission-timeout", "-5"]).permissionTimeoutMs).toBe(30 * 60_000);
516
+ });
458
517
  it("should accept both spellings of the concurrency flag", () => {
459
518
  expect(parseGatewayArgs(["--max-concurrency", "4"]).maxConcurrency).toBe(4);
460
519
  expect(parseGatewayArgs(["--maxConcurrency", "8"]).maxConcurrency).toBe(8);
@@ -577,6 +636,7 @@ describe("index", () => {
577
636
  describe("resolveAgentCommand", () => {
578
637
  const options = (overrides = {}) => ({
579
638
  maxConcurrency: 2,
639
+ permissionTimeoutMs: 30 * 60_000,
580
640
  command: "run",
581
641
  allowUnverifiedAgent: false,
582
642
  rest: [],
@@ -621,7 +681,7 @@ describe("index", () => {
621
681
  await expect(resolveAgentCommand(options({ agentId: "nope" }), [], fetchImpl)).rejects.toThrow(/No agent "nope" in the ACP registry/);
622
682
  });
623
683
  });
624
- describe("runAuthCommand", () => {
684
+ describe("runAgentCommand", () => {
625
685
  const agentrqConfig = { env: {} };
626
686
  let logSpy;
627
687
  let errorSpy;
@@ -645,22 +705,40 @@ describe("index", () => {
645
705
  authMethods: [{ id: "agent-login", name: "Agent login" }],
646
706
  agentCapabilities: { auth: { logout: {} } },
647
707
  });
648
- await runAuthCommand("list-auth-methods", ["gemini", "--acp"], agentrqConfig, {});
708
+ await runAgentCommand("list-auth-methods", ["gemini", "--acp"], agentrqConfig, fakeBridge());
649
709
  const printed = logSpy.mock.calls.flat().join("\n");
650
710
  expect(printed).toContain("Agent login (agent-login)");
651
711
  expect(printed).toContain("--logout");
652
712
  expect(spawnedAgents[0].kill).toHaveBeenCalled();
653
713
  });
714
+ it("should print what the agent says it supports", async () => {
715
+ mockConnection({}, {
716
+ protocolVersion: 1,
717
+ agentCapabilities: {
718
+ loadSession: true,
719
+ sessionCapabilities: { resume: {} },
720
+ mcpCapabilities: { http: true },
721
+ },
722
+ });
723
+ await runAgentCommand("agent-info", ["gemini", "--acp"], agentrqConfig, fakeBridge());
724
+ const printed = String(logSpy.mock.calls.at(-1)[0]);
725
+ expect(printed).toContain("gemini --acp");
726
+ expect(printed).toContain("ACP protocol version 1");
727
+ expect(printed).toContain("session/resume yes");
728
+ expect(printed).toContain("session/close no");
729
+ expect(printed).toContain("http yes");
730
+ expect(spawnedAgents[0].kill).toHaveBeenCalled();
731
+ });
654
732
  it("should report agents that advertise no login", async () => {
655
733
  mockConnection({});
656
- await runAuthCommand("list-auth-methods", ["gemini", "--acp"], agentrqConfig, {});
734
+ await runAgentCommand("list-auth-methods", ["gemini", "--acp"], agentrqConfig, fakeBridge());
657
735
  expect(logSpy.mock.calls.flat().join("\n")).toContain("no authentication methods");
658
736
  });
659
737
  it("should log out", async () => {
660
738
  const connection = mockConnection({ logout: vi.fn().mockResolvedValue({}) }, {
661
739
  agentCapabilities: { auth: { logout: {} } },
662
740
  });
663
- await runAuthCommand("logout", ["gemini", "--acp"], agentrqConfig, {});
741
+ await runAgentCommand("logout", ["gemini", "--acp"], agentrqConfig, fakeBridge());
664
742
  expect(connection.logout).toHaveBeenCalledWith({});
665
743
  });
666
744
  it("should log in with the method the user named", async () => {
@@ -670,14 +748,14 @@ describe("index", () => {
670
748
  { id: "oauth", name: "OAuth" },
671
749
  ],
672
750
  });
673
- await runAuthCommand("login", ["gemini", "--acp"], agentrqConfig, {}, "oauth");
751
+ await runAgentCommand("login", ["gemini", "--acp"], agentrqConfig, fakeBridge(), "oauth");
674
752
  expect(connection.authenticate).toHaveBeenCalledWith({ methodId: "oauth" });
675
753
  });
676
754
  it("should still shut the agent down when the login fails", async () => {
677
755
  mockConnection({ authenticate: vi.fn() }, {
678
756
  authMethods: [{ id: "agent-login", name: "Agent login" }],
679
757
  });
680
- await expect(runAuthCommand("login", ["gemini", "--acp"], agentrqConfig, {}, "missing")).rejects.toThrow(/Unknown authentication method/);
758
+ await expect(runAgentCommand("login", ["gemini", "--acp"], agentrqConfig, fakeBridge(), "missing")).rejects.toThrow(/Unknown authentication method/);
681
759
  expect(spawnedAgents[0].kill).toHaveBeenCalled();
682
760
  });
683
761
  });
@@ -690,6 +768,7 @@ describe("index", () => {
690
768
  expect(new Set(documented)).toEqual(new Set([
691
769
  "--agent",
692
770
  "--list-agents",
771
+ "--agent-info",
693
772
  "--allow-unverified-agent",
694
773
  "--registry-url",
695
774
  "--list-auth-methods",
@@ -697,6 +776,7 @@ describe("index", () => {
697
776
  "--logout",
698
777
  "--auth-method",
699
778
  "--max-concurrency",
779
+ "--permission-timeout",
700
780
  "--help",
701
781
  ]));
702
782
  });
@@ -875,5 +955,193 @@ describe("index", () => {
875
955
  expect(consoleSpy).toHaveBeenCalledWith(expect.stringContaining("Failed to set session mode"), expect.any(Error));
876
956
  });
877
957
  });
958
+ describe("task cancellation handling", () => {
959
+ let errorSpy;
960
+ beforeEach(() => {
961
+ activeSessions.clear();
962
+ errorSpy = vi.spyOn(console, "error").mockImplementation(() => { });
963
+ });
964
+ afterEach(() => {
965
+ activeSessions.clear();
966
+ errorSpy.mockRestore();
967
+ });
968
+ describe("findActiveSession", () => {
969
+ it("should find session by direct task key", () => {
970
+ const mockSession = { sessionId: "sess-1", acpClient: { cancelTurn: vi.fn() } };
971
+ activeSessions.set("task-1", mockSession);
972
+ expect(findActiveSession("task-1")).toBe(mockSession);
973
+ });
974
+ it("should find session by sessionId match", () => {
975
+ const mockSession = { sessionId: "sess-custom", acpClient: { cancelTurn: vi.fn() } };
976
+ activeSessions.set("key-other", mockSession);
977
+ expect(findActiveSession("sess-custom")).toBe(mockSession);
978
+ });
979
+ it("should return the single session if no taskId is provided", () => {
980
+ const mockSession = { sessionId: "sess-only", acpClient: { cancelTurn: vi.fn() } };
981
+ activeSessions.set("default", mockSession);
982
+ expect(findActiveSession(undefined)).toBe(mockSession);
983
+ });
984
+ it("should fallback to default keyed session when taskId is given but only 1 session exists", () => {
985
+ const mockSession = { sessionId: "s1", acpClient: { cancelTurn: vi.fn() } };
986
+ activeSessions.set("default", mockSession);
987
+ expect(findActiveSession("task-new-id")).toBe(mockSession);
988
+ });
989
+ it("should not fall back to a session that belongs to a different task", () => {
990
+ // Task A is running; a cancel for queued task B must not abort it.
991
+ const sessionA = { sessionId: "sess-a", acpClient: { cancelTurn: vi.fn() } };
992
+ activeSessions.set("task-a", sessionA);
993
+ expect(findActiveSession("task-b")).toBeUndefined();
994
+ });
995
+ it("should return undefined if taskId is not found when multiple sessions exist", () => {
996
+ activeSessions.set("task-1", { sessionId: "s1" });
997
+ activeSessions.set("task-2", { sessionId: "s2" });
998
+ expect(findActiveSession("task-nonexistent")).toBeUndefined();
999
+ expect(findActiveSession(undefined)).toBeUndefined();
1000
+ });
1001
+ });
1002
+ describe("handleTaskCancellation", () => {
1003
+ it("should cancel turn on the matched active session", async () => {
1004
+ const cancelTurn = vi.fn().mockResolvedValue(undefined);
1005
+ const mockSession = {
1006
+ sessionId: "sess-100",
1007
+ acpClient: { cancelTurn },
1008
+ };
1009
+ activeSessions.set("task-100", mockSession);
1010
+ await handleTaskCancellation("task-100", "cancelled by user");
1011
+ expect(cancelTurn).toHaveBeenCalledWith("sess-100");
1012
+ expect(errorSpy).toHaveBeenCalledWith(expect.stringContaining("Cancelling session sess-100 for task task-100 (cancelled by user)"));
1013
+ });
1014
+ it("should skip a task cancelled while it was still being fetched", async () => {
1015
+ cancelledTaskSeq.clear();
1016
+ const promptMock = vi.fn();
1017
+ const switcher = { ensureForTask: vi.fn().mockResolvedValue("s-q1") };
1018
+ const acpClient = { flushReply: vi.fn(), reportStopReason: vi.fn() };
1019
+ const connection = { prompt: promptMock };
1020
+ const bridge = {
1021
+ // The cancel lands while the poll is in flight — the task has no
1022
+ // session yet, so nothing but this flag can stop it.
1023
+ callTool: vi.fn().mockImplementation(async () => {
1024
+ await handleTaskCancellation("task-queued-1");
1025
+ return { content: [{ type: "text", text: "task task-queued-1: do work" }] };
1026
+ }),
1027
+ };
1028
+ await checkForNextTask(bridge, connection, switcher, acpClient);
1029
+ expect(promptMock).not.toHaveBeenCalled();
1030
+ });
1031
+ it("should not swallow work queued after the cancellation", async () => {
1032
+ // agentrq reuses a chat's id as the task id, so the id cancelled a
1033
+ // moment ago is the same id the user's next message arrives under.
1034
+ cancelledTaskSeq.clear();
1035
+ await handleTaskCancellation("task-queued-2");
1036
+ const promptMock = vi.fn().mockResolvedValue({ stopReason: "end_turn" });
1037
+ const switcher = { ensureForTask: vi.fn().mockResolvedValue("s-q2") };
1038
+ const acpClient = { flushReply: vi.fn(), reportStopReason: vi.fn() };
1039
+ const connection = { prompt: promptMock };
1040
+ const bridge = {
1041
+ callTool: vi.fn().mockResolvedValue({
1042
+ content: [{ type: "text", text: "task task-queued-2: fresh work" }],
1043
+ }),
1044
+ };
1045
+ await checkForNextTask(bridge, connection, switcher, acpClient);
1046
+ expect(promptMock).toHaveBeenCalled();
1047
+ });
1048
+ it("should forget the oldest cancellations rather than grow forever", async () => {
1049
+ cancelledTaskSeq.clear();
1050
+ for (let i = 0; i < 250; i++)
1051
+ markTaskCancelled(`task-${i}`);
1052
+ expect(cancelledTaskSeq.size).toBe(200);
1053
+ expect(cancelledTaskSeq.has("task-0")).toBe(false);
1054
+ expect(cancelledTaskSeq.has("task-249")).toBe(true);
1055
+ });
1056
+ it("should only stop the notification that was queued before the cancel", async () => {
1057
+ cancelledTaskSeq.clear();
1058
+ const older = nextTaskSeq();
1059
+ await handleTaskCancellation("task-two-turns");
1060
+ const newer = nextTaskSeq();
1061
+ expect(isTaskCancelled("task-two-turns", older)).toBe(true);
1062
+ expect(isTaskCancelled("task-two-turns", newer)).toBe(false);
1063
+ });
1064
+ it("should log when task to cancel is not found", async () => {
1065
+ await handleTaskCancellation("task-missing");
1066
+ expect(errorSpy).toHaveBeenCalledWith(expect.stringContaining("Received cancellation for task task-missing, but no active session found"));
1067
+ });
1068
+ it("should cancel the single session when taskId is omitted and only 1 session exists", async () => {
1069
+ const cancel1 = vi.fn().mockResolvedValue(undefined);
1070
+ activeSessions.set("t1", { sessionId: "s1", acpClient: { cancelTurn: cancel1 } });
1071
+ await handleTaskCancellation(undefined, "single shutdown");
1072
+ expect(cancel1).toHaveBeenCalledWith("s1");
1073
+ expect(errorSpy).toHaveBeenCalledWith(expect.stringContaining("Received cancellation with no taskId (single shutdown). Cancelling active session s1..."));
1074
+ });
1075
+ it("should fail closed and not cancel anything when taskId is omitted and multiple sessions exist", async () => {
1076
+ const cancel1 = vi.fn().mockResolvedValue(undefined);
1077
+ const cancel2 = vi.fn().mockResolvedValue(undefined);
1078
+ activeSessions.set("t1", { sessionId: "s1", acpClient: { cancelTurn: cancel1 } });
1079
+ activeSessions.set("t2", { sessionId: "s2", acpClient: { cancelTurn: cancel2 } });
1080
+ await handleTaskCancellation(undefined, "multiple active");
1081
+ expect(cancel1).not.toHaveBeenCalled();
1082
+ expect(cancel2).not.toHaveBeenCalled();
1083
+ expect(errorSpy).toHaveBeenCalledWith(expect.stringContaining("but 2 sessions are active (skipping to avoid aborting unrelated tasks)"));
1084
+ });
1085
+ });
1086
+ });
1087
+ describe("handleAgentModeChange", () => {
1088
+ const modes = {
1089
+ currentModeId: "ask",
1090
+ availableModes: [
1091
+ { id: "ask", name: "Ask first" },
1092
+ { id: "auto", name: "Auto approve" },
1093
+ ],
1094
+ };
1095
+ let errorSpy;
1096
+ beforeEach(() => {
1097
+ errorSpy = vi.spyOn(console, "error").mockImplementation(() => { });
1098
+ });
1099
+ afterEach(() => errorSpy.mockRestore());
1100
+ it("should leave a mode that still asks the human alone", async () => {
1101
+ const connection = { setSessionMode: vi.fn() };
1102
+ await handleAgentModeChange(connection, "sess-ok", "ask", modes);
1103
+ expect(connection.setSessionMode).not.toHaveBeenCalled();
1104
+ });
1105
+ it("should put the session back when the agent starts approving for us", async () => {
1106
+ const connection = { setSessionMode: vi.fn().mockResolvedValue({}) };
1107
+ await handleAgentModeChange(connection, "sess-drift", "auto", modes);
1108
+ expect(connection.setSessionMode).toHaveBeenCalledWith({
1109
+ sessionId: "sess-drift",
1110
+ modeId: "ask",
1111
+ });
1112
+ });
1113
+ it("should treat a mode the agent never advertised as untrusted", async () => {
1114
+ const connection = { setSessionMode: vi.fn().mockResolvedValue({}) };
1115
+ await handleAgentModeChange(connection, "sess-unknown", "something-new", modes);
1116
+ expect(connection.setSessionMode).toHaveBeenCalledWith({
1117
+ sessionId: "sess-unknown",
1118
+ modeId: "ask",
1119
+ });
1120
+ });
1121
+ it("should stop fighting an agent that keeps switching back", async () => {
1122
+ const connection = { setSessionMode: vi.fn().mockResolvedValue({}) };
1123
+ for (let i = 0; i < 5; i++) {
1124
+ await handleAgentModeChange(connection, "sess-stubborn", "auto", modes);
1125
+ }
1126
+ // An unbounded fight would be an endless stream of setSessionMode calls.
1127
+ expect(connection.setSessionMode).toHaveBeenCalledTimes(3);
1128
+ expect(errorSpy.mock.calls.flat().join("\n")).toContain("Giving up after 3 attempts");
1129
+ });
1130
+ it("should start counting again once the session is back in a safe mode", async () => {
1131
+ const connection = { setSessionMode: vi.fn().mockResolvedValue({}) };
1132
+ await handleAgentModeChange(connection, "sess-recovered", "auto", modes);
1133
+ await handleAgentModeChange(connection, "sess-recovered", "ask", modes);
1134
+ for (let i = 0; i < 4; i++) {
1135
+ await handleAgentModeChange(connection, "sess-recovered", "auto", modes);
1136
+ }
1137
+ expect(connection.setSessionMode).toHaveBeenCalledTimes(4);
1138
+ });
1139
+ it("should do nothing for an agent that offers no modes at all", async () => {
1140
+ const connection = { setSessionMode: vi.fn() };
1141
+ await handleAgentModeChange(connection, "sess-modeless", "whatever", undefined);
1142
+ await handleAgentModeChange(connection, "sess-modeless", "whatever", { availableModes: [] });
1143
+ expect(connection.setSessionMode).not.toHaveBeenCalled();
1144
+ });
1145
+ });
878
1146
  });
879
1147
  //# sourceMappingURL=index.test.js.map