@ebowwa/coder 0.7.64 → 0.7.66

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 (101) hide show
  1. package/dist/index.js +36233 -32
  2. package/dist/interfaces/ui/terminal/cli/index.js +34318 -158
  3. package/dist/interfaces/ui/terminal/native/README.md +53 -0
  4. package/dist/interfaces/ui/terminal/native/claude_code_native.darwin-x64.node +0 -0
  5. package/dist/interfaces/ui/terminal/native/claude_code_native.dylib +0 -0
  6. package/dist/interfaces/ui/terminal/native/index.d.ts +0 -0
  7. package/dist/interfaces/ui/terminal/native/index.darwin-arm64.node +0 -0
  8. package/dist/interfaces/ui/terminal/native/index.js +43 -0
  9. package/dist/interfaces/ui/terminal/native/index.node +0 -0
  10. package/dist/interfaces/ui/terminal/native/package.json +34 -0
  11. package/dist/native/README.md +53 -0
  12. package/dist/native/claude_code_native.darwin-x64.node +0 -0
  13. package/dist/native/claude_code_native.dylib +0 -0
  14. package/dist/native/index.d.ts +0 -480
  15. package/dist/native/index.darwin-arm64.node +0 -0
  16. package/dist/native/index.js +43 -1625
  17. package/dist/native/index.node +0 -0
  18. package/dist/native/package.json +34 -0
  19. package/native/index.darwin-arm64.node +0 -0
  20. package/native/index.js +33 -19
  21. package/package.json +3 -2
  22. package/packages/src/core/agent-loop/__tests__/compaction.test.ts +17 -14
  23. package/packages/src/core/agent-loop/compaction.ts +6 -2
  24. package/packages/src/core/agent-loop/index.ts +2 -0
  25. package/packages/src/core/agent-loop/loop-state.ts +1 -1
  26. package/packages/src/core/agent-loop/turn-executor.ts +4 -0
  27. package/packages/src/core/agent-loop/types.ts +4 -0
  28. package/packages/src/core/api-client-impl.ts +377 -176
  29. package/packages/src/core/cognitive-security/hooks.ts +2 -1
  30. package/packages/src/core/config/todo +7 -0
  31. package/packages/src/core/context/__tests__/integration.test.ts +334 -0
  32. package/packages/src/core/context/compaction.ts +170 -0
  33. package/packages/src/core/context/constants.ts +58 -0
  34. package/packages/src/core/context/extraction.ts +85 -0
  35. package/packages/src/core/context/index.ts +66 -0
  36. package/packages/src/core/context/summarization.ts +251 -0
  37. package/packages/src/core/context/token-estimation.ts +98 -0
  38. package/packages/src/core/context/types.ts +59 -0
  39. package/packages/src/core/models.ts +81 -4
  40. package/packages/src/core/normalizers/todo +5 -1
  41. package/packages/src/core/providers/README.md +230 -0
  42. package/packages/src/core/providers/__tests__/providers.test.ts +135 -0
  43. package/packages/src/core/providers/index.ts +419 -0
  44. package/packages/src/core/providers/types.ts +132 -0
  45. package/packages/src/core/retry.ts +10 -0
  46. package/packages/src/ecosystem/tools/index.ts +174 -0
  47. package/packages/src/index.ts +23 -2
  48. package/packages/src/interfaces/ui/index.ts +17 -20
  49. package/packages/src/interfaces/ui/spinner.ts +2 -2
  50. package/packages/src/interfaces/ui/terminal/bridge/index.ts +370 -0
  51. package/packages/src/interfaces/ui/terminal/bridge/ipc.ts +829 -0
  52. package/packages/src/interfaces/ui/terminal/bridge/screen-export.ts +968 -0
  53. package/packages/src/interfaces/ui/terminal/bridge/types.ts +226 -0
  54. package/packages/src/interfaces/ui/terminal/bridge/useBridge.ts +210 -0
  55. package/packages/src/interfaces/ui/terminal/cli/bootstrap.ts +132 -0
  56. package/packages/src/interfaces/ui/terminal/cli/index.ts +200 -13
  57. package/packages/src/interfaces/ui/terminal/cli/interactive/index.ts +110 -0
  58. package/packages/src/interfaces/ui/terminal/cli/interactive/input-handler.ts +402 -0
  59. package/packages/src/interfaces/ui/terminal/cli/interactive/interactive-runner.ts +820 -0
  60. package/packages/src/interfaces/ui/terminal/cli/interactive/message-store.ts +299 -0
  61. package/packages/src/interfaces/ui/terminal/cli/interactive/types.ts +274 -0
  62. package/packages/src/interfaces/ui/terminal/shared/index.ts +13 -0
  63. package/packages/src/interfaces/ui/terminal/shared/query.ts +9 -3
  64. package/packages/src/interfaces/ui/terminal/shared/setup.ts +5 -1
  65. package/packages/src/interfaces/ui/terminal/shared/spinner-frames.ts +73 -0
  66. package/packages/src/interfaces/ui/terminal/shared/status-line.ts +10 -2
  67. package/packages/src/native/index.ts +404 -27
  68. package/packages/src/native/tui_v2_types.ts +39 -0
  69. package/packages/src/teammates/coordination.test.ts +279 -0
  70. package/packages/src/teammates/coordination.ts +646 -0
  71. package/packages/src/teammates/index.ts +95 -25
  72. package/packages/src/teammates/integration.test.ts +272 -0
  73. package/packages/src/teammates/runner.test.ts +235 -0
  74. package/packages/src/teammates/runner.ts +750 -0
  75. package/packages/src/teammates/schemas.ts +673 -0
  76. package/packages/src/types/index.ts +1 -0
  77. package/packages/src/core/context-compaction.ts +0 -578
  78. package/packages/src/interfaces/ui/Screenshot 2026-03-02 at 9.23.10/342/200/257PM.png +0 -0
  79. package/packages/src/interfaces/ui/Screenshot 2026-03-03 at 10.55.11/342/200/257AM.png +0 -0
  80. package/packages/src/interfaces/ui/terminal/tui/HelpPanel.tsx +0 -262
  81. package/packages/src/interfaces/ui/terminal/tui/InputContext.tsx +0 -232
  82. package/packages/src/interfaces/ui/terminal/tui/InputField.tsx +0 -62
  83. package/packages/src/interfaces/ui/terminal/tui/InteractiveTUI.tsx +0 -537
  84. package/packages/src/interfaces/ui/terminal/tui/MessageArea.tsx +0 -107
  85. package/packages/src/interfaces/ui/terminal/tui/MessageStore.tsx +0 -240
  86. package/packages/src/interfaces/ui/terminal/tui/StatusBar.tsx +0 -54
  87. package/packages/src/interfaces/ui/terminal/tui/commands.ts +0 -438
  88. package/packages/src/interfaces/ui/terminal/tui/components/InteractiveElements.tsx +0 -584
  89. package/packages/src/interfaces/ui/terminal/tui/components/MultilineInput.tsx +0 -614
  90. package/packages/src/interfaces/ui/terminal/tui/components/PaneManager.tsx +0 -333
  91. package/packages/src/interfaces/ui/terminal/tui/components/Sidebar.tsx +0 -604
  92. package/packages/src/interfaces/ui/terminal/tui/components/index.ts +0 -118
  93. package/packages/src/interfaces/ui/terminal/tui/console.ts +0 -49
  94. package/packages/src/interfaces/ui/terminal/tui/index.ts +0 -90
  95. package/packages/src/interfaces/ui/terminal/tui/run.tsx +0 -42
  96. package/packages/src/interfaces/ui/terminal/tui/spinner.ts +0 -69
  97. package/packages/src/interfaces/ui/terminal/tui/tui-app.tsx +0 -390
  98. package/packages/src/interfaces/ui/terminal/tui/tui-footer.ts +0 -422
  99. package/packages/src/interfaces/ui/terminal/tui/types.ts +0 -186
  100. package/packages/src/interfaces/ui/terminal/tui/useInputHandler.ts +0 -104
  101. package/packages/src/interfaces/ui/terminal/tui/useNativeInput.ts +0 -239
@@ -11,6 +11,18 @@ import type { Teammate, Team, TeammateMessage, TeammateStatus } from "../types/i
11
11
  import { spawn } from "child_process";
12
12
  import { mkdirSync, rmSync, existsSync, readFileSync, readdirSync, renameSync, writeFileSync, statSync } from "fs";
13
13
  import { join, basename } from "path";
14
+ import {
15
+ parseTeam,
16
+ parseTeammate,
17
+ parseTeammateMessage,
18
+ parseStoredMessage,
19
+ safeParseTeam,
20
+ safeParseTeammate,
21
+ safeParseTeammateMessage,
22
+ safeParseStoredMessage,
23
+ ValidationError,
24
+ sanitizeForFilePath,
25
+ } from "./schemas.js";
14
26
 
15
27
  // ============================================
16
28
  // FILE-BASED INBOX TYPES
@@ -48,8 +60,24 @@ export class TeammateManager {
48
60
  // INBOX PATH HELPERS
49
61
  // ============================================
50
62
 
63
+ /**
64
+ * Get safe directory name for a team (prevents ENAMETOOLONG)
65
+ */
66
+ private getSafeTeamDir(teamName: string): string {
67
+ return sanitizeForFilePath(teamName);
68
+ }
69
+
70
+ /**
71
+ * Get safe directory name for a teammate (prevents ENAMETOOLONG)
72
+ */
73
+ private getSafeTeammateDir(teammateId: string): string {
74
+ return sanitizeForFilePath(teammateId);
75
+ }
76
+
51
77
  private getInboxPath(teamName: string, teammateId: string): string {
52
- return join(this.storagePath, teamName, "inboxes", teammateId);
78
+ const safeTeamName = this.getSafeTeamDir(teamName);
79
+ const safeTeammateId = this.getSafeTeammateDir(teammateId);
80
+ return join(this.storagePath, safeTeamName, "inboxes", safeTeammateId);
53
81
  }
54
82
 
55
83
  private getPendingPath(teamName: string, teammateId: string): string {
@@ -135,8 +163,8 @@ export class TeammateManager {
135
163
  }
136
164
  this.teams.delete(name);
137
165
 
138
- // Delete team directory from disk
139
- const teamDir = join(this.storagePath, name);
166
+ // Delete team directory from disk (use safe path)
167
+ const teamDir = join(this.storagePath, this.getSafeTeamDir(name));
140
168
  try {
141
169
  rmSync(teamDir, { recursive: true, force: true });
142
170
  } catch (err) {
@@ -363,6 +391,7 @@ export class TeammateManager {
363
391
 
364
392
  /**
365
393
  * Read all pending messages from a teammate's inbox
394
+ * Validates messages using Zod schema, skipping malformed ones
366
395
  */
367
396
  private readPendingMessages(teamName: string, teammateId: string): StoredMessage[] {
368
397
  const pendingPath = this.getPendingPath(teamName, teammateId);
@@ -381,14 +410,31 @@ export class TeammateManager {
381
410
  try {
382
411
  const msgPath = join(pendingPath, file);
383
412
  const content = readFileSync(msgPath, 'utf-8');
384
- const msg = JSON.parse(content) as StoredMessage;
385
- messages.push(msg);
386
- } catch {
413
+
414
+ // Parse JSON first, then validate
415
+ let parsed: unknown;
416
+ try {
417
+ parsed = JSON.parse(content);
418
+ } catch {
419
+ continue; // Skip non-JSON files
420
+ }
421
+
422
+ const result = safeParseStoredMessage(parsed);
423
+
424
+ if (result.success && result.data) {
425
+ messages.push(result.data);
426
+ } else {
427
+ // Log validation error but skip this message
428
+ console.error(`Failed to parse message from ${msgPath}:`, result.error);
429
+ }
430
+ } catch (err) {
387
431
  // Skip malformed messages
432
+ console.error(`Error reading message file:`, err);
388
433
  }
389
434
  }
390
- } catch {
435
+ } catch (err) {
391
436
  // Directory read error
437
+ console.error(`Error reading pending messages:`, err);
392
438
  }
393
439
 
394
440
  return messages;
@@ -784,7 +830,7 @@ export class TeammateManager {
784
830
  * Persist a team configuration to disk
785
831
  */
786
832
  private async persistTeam(team: Team): Promise<void> {
787
- const teamDir = join(this.storagePath, team.name);
833
+ const teamDir = join(this.storagePath, this.getSafeTeamDir(team.name));
788
834
  const configPath = join(teamDir, "config.json");
789
835
 
790
836
  // Ensure directory exists
@@ -813,6 +859,7 @@ export class TeammateManager {
813
859
  /**
814
860
  * Load all teams from disk at startup
815
861
  * Uses synchronous operations for constructor compatibility
862
+ * Validates all data using Zod schemas
816
863
  */
817
864
  loadTeams(): void {
818
865
  // Use Bun's glob to find team configs
@@ -834,27 +881,26 @@ export class TeammateManager {
834
881
  // Read file synchronously using readFileSync
835
882
  // Bun.file().text() is async, so we use fs.readFileSync for sync operation
836
883
  const text = readFileSync(filePath, "utf-8");
837
- const config = JSON.parse(text);
838
884
 
839
- // Validate required fields - skip if missing teammates or name
840
- if (!config.name || !config.teammates || !Array.isArray(config.teammates)) {
841
- // Skip configs that don't match our expected structure
885
+ // Parse JSON first, then validate with Zod schema
886
+ let parsed: unknown;
887
+ try {
888
+ parsed = JSON.parse(text);
889
+ } catch (parseError) {
890
+ console.error(`Failed to parse JSON from ${filePath}:`, parseError);
842
891
  continue;
843
892
  }
844
893
 
845
- const team: Team = {
846
- name: config.name,
847
- description: config.description || "",
848
- teammates: config.teammates,
849
- taskListId: config.taskListId || "",
850
- status: config.status || "active",
851
- coordination: config.coordination || {
852
- dependencyOrder: [],
853
- communicationProtocol: "broadcast",
854
- taskAssignmentStrategy: "manual",
855
- },
856
- };
894
+ // Validate using Zod schema
895
+ const result = safeParseTeam(parsed);
857
896
 
897
+ if (!result.success || !result.data) {
898
+ // Log validation error but skip this config
899
+ console.error(`Failed to load team config from ${filePath}:`, result.error);
900
+ continue;
901
+ }
902
+
903
+ const team = result.data;
858
904
  this.teams.set(team.name, team);
859
905
 
860
906
  // Index teammates
@@ -863,12 +909,13 @@ export class TeammateManager {
863
909
  }
864
910
  } catch (error) {
865
911
  // Silently skip malformed configs
912
+ console.error(`Error reading team config:`, error);
866
913
  }
867
914
  }
868
915
  } catch (error) {
869
916
  // Storage path may not exist yet - that's okay
870
917
  if ((error as NodeJS.ErrnoException).code !== "ENOENT") {
871
- // Ignore permission errors too
918
+ console.error("Error loading teams:", error);
872
919
  }
873
920
  }
874
921
  }
@@ -980,3 +1027,26 @@ export function createTeammate(
980
1027
 
981
1028
  // Export types
982
1029
  export type { StoredMessage };
1030
+
1031
+ // Re-export runner module
1032
+ export {
1033
+ TeammateModeRunner,
1034
+ getTeammateRunner,
1035
+ setTeammateRunner,
1036
+ isTeammateModeActive,
1037
+ type TeammateModeConfig,
1038
+ type TeammateModeState,
1039
+ } from "./runner.js";
1040
+
1041
+ // Re-export coordination module
1042
+ export {
1043
+ CoordinationManager,
1044
+ createCoordinationMessage,
1045
+ parseCoordinationMessage,
1046
+ type CoordinationEvent,
1047
+ type CoordinationEventType,
1048
+ type CoordinationCallback,
1049
+ type CoordinationConfig,
1050
+ type ProgressReport,
1051
+ type FileClaim,
1052
+ } from "./coordination.js";
@@ -0,0 +1,272 @@
1
+ /**
2
+ * Integration test for teammate coordination
3
+ * Tests: long horizon, cohesion, assignment completion
4
+ */
5
+
6
+ import { describe, it, expect, beforeEach, afterEach } from "bun:test";
7
+ import { TeammateManager } from "./index.js";
8
+ import { TeammateModeRunner, setTeammateRunner } from "./runner.js";
9
+ import * as fs from "fs";
10
+ import * as path from "path";
11
+ import * as os from "os";
12
+
13
+ const TEAMS_DIR = path.join(os.homedir(), ".claude", "teams");
14
+ const TEST_TEAM = `integration-test-${Date.now()}`;
15
+
16
+ function cleanupTestTeam() {
17
+ const teamPath = path.join(TEAMS_DIR, TEST_TEAM);
18
+ if (fs.existsSync(teamPath)) {
19
+ fs.rmSync(teamPath, { recursive: true, force: true });
20
+ }
21
+ }
22
+
23
+ describe("Teammate Integration Tests", () => {
24
+ let manager: TeammateManager;
25
+
26
+ beforeEach(() => {
27
+ cleanupTestTeam();
28
+ manager = new TeammateManager();
29
+ });
30
+
31
+ afterEach(() => {
32
+ cleanupTestTeam();
33
+ setTeammateRunner(null);
34
+ });
35
+
36
+ describe("Long Horizon - Extended Operation", () => {
37
+ it("should maintain state over multiple operations", async () => {
38
+ // Create team
39
+ const team = manager.createTeam({
40
+ name: TEST_TEAM,
41
+ description: "Long horizon test team",
42
+ teammates: [],
43
+ taskListId: `${TEST_TEAM}-tasks`,
44
+ coordination: {
45
+ dependencyOrder: [],
46
+ communicationProtocol: "broadcast",
47
+ taskAssignmentStrategy: "manual",
48
+ },
49
+ });
50
+
51
+ // Create teammate
52
+ const runner = new TeammateModeRunner({
53
+ teamName: TEST_TEAM,
54
+ agentName: "long-horizon-agent",
55
+ agentColor: "green",
56
+ workingDirectory: process.cwd(),
57
+ pollInterval: 100,
58
+ });
59
+
60
+ await runner.start();
61
+ setTeammateRunner(runner);
62
+
63
+ // Simulate long horizon: multiple status updates
64
+ expect(runner.getStatus()).toBe("in_progress");
65
+
66
+ runner.reportActivity();
67
+ expect(runner.getStatus()).toBe("in_progress");
68
+
69
+ // Complete a task
70
+ runner.reportTaskComplete("task-1", "First task");
71
+ expect(runner.getStatus()).toBe("completed");
72
+
73
+ // Request new task (resets to idle)
74
+ runner.requestTask();
75
+ expect(runner.getStatus()).toBe("idle");
76
+
77
+ // Report activity again (should flip to in_progress)
78
+ runner.reportActivity();
79
+ expect(runner.getStatus()).toBe("in_progress");
80
+
81
+ // Fail a task
82
+ runner.reportTaskFailed("task-2", "Second task", "Test failure");
83
+ expect(runner.getStatus()).toBe("failed");
84
+
85
+ await runner.stop();
86
+ expect(runner.getStatus()).toBe("idle");
87
+ });
88
+
89
+ it("should persist team state across operations", async () => {
90
+ const runner = new TeammateModeRunner({
91
+ teamName: TEST_TEAM,
92
+ agentName: "persist-agent",
93
+ workingDirectory: process.cwd(),
94
+ pollInterval: 100,
95
+ });
96
+
97
+ await runner.start();
98
+
99
+ // Verify teammate was added to team
100
+ const teammate = runner.getTeammate();
101
+ expect(teammate).not.toBe(null);
102
+ expect(teammate?.name).toBe("persist-agent");
103
+
104
+ // Stop and restart
105
+ await runner.stop();
106
+
107
+ // Create new runner with same team
108
+ const runner2 = new TeammateModeRunner({
109
+ teamName: TEST_TEAM,
110
+ agentName: "persist-agent-2",
111
+ workingDirectory: process.cwd(),
112
+ pollInterval: 100,
113
+ });
114
+
115
+ await runner2.start();
116
+
117
+ // Team should have both teammates now
118
+ const members = runner2.getTeamMembers();
119
+ expect(members.length).toBe(2);
120
+
121
+ await runner2.stop();
122
+ });
123
+ });
124
+
125
+ describe("Cohesion - Multi-Agent Coordination", () => {
126
+ it("should support multiple teammates in same team", async () => {
127
+ // Create first teammate
128
+ const runner1 = new TeammateModeRunner({
129
+ teamName: TEST_TEAM,
130
+ agentName: "coordinator",
131
+ agentColor: "blue",
132
+ workingDirectory: process.cwd(),
133
+ pollInterval: 100,
134
+ });
135
+
136
+ await runner1.start();
137
+
138
+ // Wait for persistence to complete
139
+ await new Promise(resolve => setTimeout(resolve, 50));
140
+
141
+ // Create second teammate (will load team from disk)
142
+ const runner2 = new TeammateModeRunner({
143
+ teamName: TEST_TEAM,
144
+ agentName: "worker",
145
+ agentColor: "orange",
146
+ workingDirectory: process.cwd(),
147
+ pollInterval: 100,
148
+ });
149
+
150
+ await runner2.start();
151
+
152
+ // Wait for persistence
153
+ await new Promise(resolve => setTimeout(resolve, 50));
154
+
155
+ // Both should see each other
156
+ const members1 = runner1.getTeamMembers();
157
+ const members2 = runner2.getTeamMembers();
158
+
159
+ // runner1 sees itself (in memory) + runner2 (loaded from disk)
160
+ expect(members1.length).toBeGreaterThanOrEqual(1);
161
+ // runner2 sees itself (in memory) + runner1 (loaded from disk)
162
+ expect(members2.length).toBeGreaterThanOrEqual(1);
163
+
164
+ // Verify coordinator and worker are both in the team
165
+ const allNames = new Set([...members1, ...members2].map(m => m.name));
166
+ expect(allNames.has("coordinator")).toBe(true);
167
+ expect(allNames.has("worker")).toBe(true);
168
+
169
+ await runner1.stop();
170
+ await runner2.stop();
171
+ });
172
+
173
+ it("should support broadcast messages", async () => {
174
+ const runner1 = new TeammateModeRunner({
175
+ teamName: TEST_TEAM,
176
+ agentName: "broadcaster",
177
+ workingDirectory: process.cwd(),
178
+ pollInterval: 100,
179
+ });
180
+
181
+ await runner1.start();
182
+
183
+ // Broadcast should not throw
184
+ runner1.broadcast("Attention all teammates!");
185
+
186
+ await runner1.stop();
187
+ });
188
+ });
189
+
190
+ describe("Assignment Completion", () => {
191
+ it("should track task completion workflow", async () => {
192
+ const runner = new TeammateModeRunner({
193
+ teamName: TEST_TEAM,
194
+ agentName: "task-worker",
195
+ workingDirectory: process.cwd(),
196
+ pollInterval: 100,
197
+ });
198
+
199
+ await runner.start();
200
+
201
+ // Initial status
202
+ expect(runner.getStatus()).toBe("in_progress");
203
+
204
+ // Complete task
205
+ runner.reportTaskComplete("assignment-1", "Complete integration tests");
206
+ expect(runner.getStatus()).toBe("completed");
207
+
208
+ // Request new assignment
209
+ runner.requestTask();
210
+ expect(runner.getStatus()).toBe("idle");
211
+
212
+ await runner.stop();
213
+ });
214
+
215
+ it("should track task failure and recovery", async () => {
216
+ const runner = new TeammateModeRunner({
217
+ teamName: TEST_TEAM,
218
+ agentName: "failing-worker",
219
+ workingDirectory: process.cwd(),
220
+ pollInterval: 100,
221
+ });
222
+
223
+ await runner.start();
224
+
225
+ // Fail task
226
+ runner.reportTaskFailed("assignment-2", "Risky task", "Dependencies not met");
227
+ expect(runner.getStatus()).toBe("failed");
228
+
229
+ // Recover by requesting new task
230
+ runner.requestTask();
231
+ expect(runner.getStatus()).toBe("idle");
232
+
233
+ await runner.stop();
234
+ });
235
+ });
236
+
237
+ describe("Inbox Management", () => {
238
+ it("should track inbox stats", async () => {
239
+ const runner = new TeammateModeRunner({
240
+ teamName: TEST_TEAM,
241
+ agentName: "inbox-worker",
242
+ workingDirectory: process.cwd(),
243
+ pollInterval: 100,
244
+ });
245
+
246
+ await runner.start();
247
+
248
+ const stats = runner.getInboxStats();
249
+ expect(stats).toHaveProperty("pending");
250
+ expect(stats).toHaveProperty("processed");
251
+
252
+ await runner.stop();
253
+ });
254
+
255
+ it("should check for pending messages", async () => {
256
+ const runner = new TeammateModeRunner({
257
+ teamName: TEST_TEAM,
258
+ agentName: "message-checker",
259
+ workingDirectory: process.cwd(),
260
+ pollInterval: 100,
261
+ });
262
+
263
+ await runner.start();
264
+
265
+ expect(runner.hasPendingMessages()).toBe(false);
266
+ expect(runner.getPendingMessages()).toEqual([]);
267
+ expect(runner.peekPendingMessages()).toEqual([]);
268
+
269
+ await runner.stop();
270
+ });
271
+ });
272
+ });
@@ -0,0 +1,235 @@
1
+ /**
2
+ * Tests for TeammateModeRunner
3
+ */
4
+
5
+ import { describe, it, expect, beforeEach, afterEach, vi } from "bun:test";
6
+ import { TeammateModeRunner, getTeammateRunner, setTeammateRunner, isTeammateModeActive } from "./runner.js";
7
+ import type { TeammateModeConfig } from "./runner.js";
8
+
9
+ // Mock TeammateManager
10
+ vi.mock("./index.js", () => ({
11
+ TeammateManager: vi.fn().mockImplementation(() => ({
12
+ getTeam: vi.fn(() => null),
13
+ createTeam: vi.fn((config) => ({ ...config, teammates: [] })),
14
+ getTeammate: vi.fn(() => null),
15
+ addTeammate: vi.fn(),
16
+ updateTeammateStatus: vi.fn(),
17
+ persistAllTeams: vi.fn(),
18
+ getMessages: vi.fn(() => []),
19
+ sendDirect: vi.fn(),
20
+ broadcast: vi.fn(),
21
+ injectUserMessageToTeammate: vi.fn(),
22
+ getInboxStats: vi.fn(() => ({ pending: 0, processed: 0 })),
23
+ waitForTeammatesToBecomeIdle: vi.fn(() => ({ success: true, timedOut: false, statuses: {} })),
24
+ })),
25
+ generateTeammateId: vi.fn(() => "test-teammate-id"),
26
+ }));
27
+
28
+ describe("TeammateModeRunner", () => {
29
+ let runner: TeammateModeRunner;
30
+ let config: TeammateModeConfig;
31
+
32
+ beforeEach(() => {
33
+ config = {
34
+ teamName: "test-team",
35
+ workingDirectory: "/tmp/test",
36
+ pollInterval: 100, // Fast polling for tests
37
+ };
38
+ runner = new TeammateModeRunner(config);
39
+ vi.useFakeTimers();
40
+ });
41
+
42
+ afterEach(async () => {
43
+ if (runner.isActive()) {
44
+ await runner.stop();
45
+ }
46
+ vi.useRealTimers();
47
+ vi.clearAllMocks();
48
+ });
49
+
50
+ describe("constructor", () => {
51
+ it("should initialize with correct default state", () => {
52
+ expect(runner.isActive()).toBe(false);
53
+ expect(runner.getStatus()).toBe("pending");
54
+ expect(runner.getTeammate()).toBe(null);
55
+ expect(runner.getTeam()).toBe(null);
56
+ });
57
+ });
58
+
59
+ describe("start", () => {
60
+ it("should start teammate mode successfully", async () => {
61
+ const teammate = await runner.start();
62
+
63
+ expect(runner.isActive()).toBe(true);
64
+ expect(runner.getStatus()).toBe("in_progress");
65
+ expect(teammate.teamName).toBe("test-team");
66
+ // Status comes from local state, not the returned teammate object
67
+ expect(runner.getStatus()).toBe("in_progress");
68
+ });
69
+
70
+ it("should throw if already active", async () => {
71
+ await runner.start();
72
+
73
+ await expect(runner.start()).rejects.toThrow("Teammate mode already active");
74
+ });
75
+
76
+ it("should use provided agent config", async () => {
77
+ const customConfig: TeammateModeConfig = {
78
+ ...config,
79
+ // Don't provide agentId - that requires an existing teammate
80
+ agentName: "Custom Agent",
81
+ agentColor: "red",
82
+ prompt: "Test prompt",
83
+ };
84
+ const customRunner = new TeammateModeRunner(customConfig);
85
+
86
+ const teammate = await customRunner.start();
87
+
88
+ expect(teammate.name).toBe("Custom Agent");
89
+ expect(teammate.color).toBe("red");
90
+ expect(teammate.prompt).toBe("Test prompt");
91
+
92
+ await customRunner.stop();
93
+ });
94
+ });
95
+
96
+ describe("stop", () => {
97
+ it("should stop teammate mode", async () => {
98
+ await runner.start();
99
+ await runner.stop();
100
+
101
+ expect(runner.isActive()).toBe(false);
102
+ expect(runner.getStatus()).toBe("idle");
103
+ });
104
+
105
+ it("should be idempotent", async () => {
106
+ await runner.start();
107
+ await runner.stop();
108
+ await runner.stop(); // Should not throw
109
+
110
+ expect(runner.isActive()).toBe(false);
111
+ });
112
+ });
113
+
114
+ describe("message polling", () => {
115
+ it("should have pending messages methods", async () => {
116
+ await runner.start();
117
+
118
+ expect(runner.hasPendingMessages()).toBe(false);
119
+ expect(runner.getPendingMessages()).toEqual([]);
120
+ expect(runner.peekPendingMessages()).toEqual([]);
121
+ });
122
+ });
123
+
124
+ describe("idle detection", () => {
125
+ it("should start with non-idle status", async () => {
126
+ await runner.start();
127
+
128
+ expect(runner.getStatus()).toBe("in_progress");
129
+ });
130
+
131
+ it("should report activity", async () => {
132
+ await runner.start();
133
+
134
+ runner.reportActivity();
135
+
136
+ expect(runner.getStatus()).toBe("in_progress");
137
+ });
138
+ });
139
+
140
+ describe("messaging", () => {
141
+ it("should throw when sending without active teammate", async () => {
142
+ expect(() => runner.sendDirectMessage("target", "hello")).toThrow("Teammate mode not active");
143
+ expect(() => runner.broadcast("hello")).toThrow("Teammate mode not active");
144
+ expect(() => runner.injectUserMessage("hello")).toThrow("Teammate mode not active");
145
+ });
146
+ });
147
+
148
+ describe("status & info", () => {
149
+ it("should return teammate after start", async () => {
150
+ await runner.start();
151
+
152
+ const teammate = runner.getTeammate();
153
+ expect(teammate).not.toBe(null);
154
+ expect(teammate?.teamName).toBe("test-team");
155
+ });
156
+
157
+ it("should return team after start", async () => {
158
+ await runner.start();
159
+
160
+ const team = runner.getTeam();
161
+ expect(team).not.toBe(null);
162
+ expect(team?.name).toBe("test-team");
163
+ });
164
+
165
+ it("should return inbox stats", async () => {
166
+ await runner.start();
167
+
168
+ const stats = runner.getInboxStats();
169
+ expect(stats).toEqual({ pending: 0, processed: 0 });
170
+ });
171
+
172
+ it("should return team members", async () => {
173
+ await runner.start();
174
+
175
+ const members = runner.getTeamMembers();
176
+ expect(Array.isArray(members)).toBe(true);
177
+ });
178
+ });
179
+
180
+ describe("task integration", () => {
181
+ it("should report task complete", async () => {
182
+ await runner.start();
183
+ expect(runner.getStatus()).toBe("in_progress");
184
+
185
+ // reportTaskComplete calls updateStatus("completed")
186
+ runner.reportTaskComplete("task-123", "Test Task");
187
+
188
+ // Verify status changed to completed
189
+ const status = runner.getStatus();
190
+ expect(status).toBe("completed");
191
+ });
192
+
193
+ it("should report task failed", async () => {
194
+ await runner.start();
195
+ expect(runner.getStatus()).toBe("in_progress");
196
+
197
+ runner.reportTaskFailed("task-123", "Test Task", "Something went wrong");
198
+
199
+ const status = runner.getStatus();
200
+ expect(status).toBe("failed");
201
+ });
202
+
203
+ it("should request task", async () => {
204
+ await runner.start();
205
+
206
+ runner.requestTask();
207
+
208
+ expect(runner.getStatus()).toBe("idle");
209
+ });
210
+ });
211
+ });
212
+
213
+ describe("Global runner functions", () => {
214
+ afterEach(() => {
215
+ setTeammateRunner(null);
216
+ });
217
+
218
+ it("should return null when no global runner set", () => {
219
+ expect(getTeammateRunner()).toBe(null);
220
+ expect(isTeammateModeActive()).toBe(false);
221
+ });
222
+
223
+ it("should set and get global runner", () => {
224
+ const config: TeammateModeConfig = {
225
+ teamName: "global-test",
226
+ workingDirectory: "/tmp/test",
227
+ };
228
+ const runner = new TeammateModeRunner(config);
229
+
230
+ setTeammateRunner(runner);
231
+
232
+ expect(getTeammateRunner()).toBe(runner);
233
+ expect(isTeammateModeActive()).toBe(false); // Not started yet
234
+ });
235
+ });