@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.
- package/dist/index.js +36233 -32
- package/dist/interfaces/ui/terminal/cli/index.js +34318 -158
- package/dist/interfaces/ui/terminal/native/README.md +53 -0
- package/dist/interfaces/ui/terminal/native/claude_code_native.darwin-x64.node +0 -0
- package/dist/interfaces/ui/terminal/native/claude_code_native.dylib +0 -0
- package/dist/interfaces/ui/terminal/native/index.d.ts +0 -0
- package/dist/interfaces/ui/terminal/native/index.darwin-arm64.node +0 -0
- package/dist/interfaces/ui/terminal/native/index.js +43 -0
- package/dist/interfaces/ui/terminal/native/index.node +0 -0
- package/dist/interfaces/ui/terminal/native/package.json +34 -0
- package/dist/native/README.md +53 -0
- package/dist/native/claude_code_native.darwin-x64.node +0 -0
- package/dist/native/claude_code_native.dylib +0 -0
- package/dist/native/index.d.ts +0 -480
- package/dist/native/index.darwin-arm64.node +0 -0
- package/dist/native/index.js +43 -1625
- package/dist/native/index.node +0 -0
- package/dist/native/package.json +34 -0
- package/native/index.darwin-arm64.node +0 -0
- package/native/index.js +33 -19
- package/package.json +3 -2
- package/packages/src/core/agent-loop/__tests__/compaction.test.ts +17 -14
- package/packages/src/core/agent-loop/compaction.ts +6 -2
- package/packages/src/core/agent-loop/index.ts +2 -0
- package/packages/src/core/agent-loop/loop-state.ts +1 -1
- package/packages/src/core/agent-loop/turn-executor.ts +4 -0
- package/packages/src/core/agent-loop/types.ts +4 -0
- package/packages/src/core/api-client-impl.ts +377 -176
- package/packages/src/core/cognitive-security/hooks.ts +2 -1
- package/packages/src/core/config/todo +7 -0
- package/packages/src/core/context/__tests__/integration.test.ts +334 -0
- package/packages/src/core/context/compaction.ts +170 -0
- package/packages/src/core/context/constants.ts +58 -0
- package/packages/src/core/context/extraction.ts +85 -0
- package/packages/src/core/context/index.ts +66 -0
- package/packages/src/core/context/summarization.ts +251 -0
- package/packages/src/core/context/token-estimation.ts +98 -0
- package/packages/src/core/context/types.ts +59 -0
- package/packages/src/core/models.ts +81 -4
- package/packages/src/core/normalizers/todo +5 -1
- package/packages/src/core/providers/README.md +230 -0
- package/packages/src/core/providers/__tests__/providers.test.ts +135 -0
- package/packages/src/core/providers/index.ts +419 -0
- package/packages/src/core/providers/types.ts +132 -0
- package/packages/src/core/retry.ts +10 -0
- package/packages/src/ecosystem/tools/index.ts +174 -0
- package/packages/src/index.ts +23 -2
- package/packages/src/interfaces/ui/index.ts +17 -20
- package/packages/src/interfaces/ui/spinner.ts +2 -2
- package/packages/src/interfaces/ui/terminal/bridge/index.ts +370 -0
- package/packages/src/interfaces/ui/terminal/bridge/ipc.ts +829 -0
- package/packages/src/interfaces/ui/terminal/bridge/screen-export.ts +968 -0
- package/packages/src/interfaces/ui/terminal/bridge/types.ts +226 -0
- package/packages/src/interfaces/ui/terminal/bridge/useBridge.ts +210 -0
- package/packages/src/interfaces/ui/terminal/cli/bootstrap.ts +132 -0
- package/packages/src/interfaces/ui/terminal/cli/index.ts +200 -13
- package/packages/src/interfaces/ui/terminal/cli/interactive/index.ts +110 -0
- package/packages/src/interfaces/ui/terminal/cli/interactive/input-handler.ts +402 -0
- package/packages/src/interfaces/ui/terminal/cli/interactive/interactive-runner.ts +820 -0
- package/packages/src/interfaces/ui/terminal/cli/interactive/message-store.ts +299 -0
- package/packages/src/interfaces/ui/terminal/cli/interactive/types.ts +274 -0
- package/packages/src/interfaces/ui/terminal/shared/index.ts +13 -0
- package/packages/src/interfaces/ui/terminal/shared/query.ts +9 -3
- package/packages/src/interfaces/ui/terminal/shared/setup.ts +5 -1
- package/packages/src/interfaces/ui/terminal/shared/spinner-frames.ts +73 -0
- package/packages/src/interfaces/ui/terminal/shared/status-line.ts +10 -2
- package/packages/src/native/index.ts +404 -27
- package/packages/src/native/tui_v2_types.ts +39 -0
- package/packages/src/teammates/coordination.test.ts +279 -0
- package/packages/src/teammates/coordination.ts +646 -0
- package/packages/src/teammates/index.ts +95 -25
- package/packages/src/teammates/integration.test.ts +272 -0
- package/packages/src/teammates/runner.test.ts +235 -0
- package/packages/src/teammates/runner.ts +750 -0
- package/packages/src/teammates/schemas.ts +673 -0
- package/packages/src/types/index.ts +1 -0
- package/packages/src/core/context-compaction.ts +0 -578
- package/packages/src/interfaces/ui/Screenshot 2026-03-02 at 9.23.10/342/200/257PM.png +0 -0
- package/packages/src/interfaces/ui/Screenshot 2026-03-03 at 10.55.11/342/200/257AM.png +0 -0
- package/packages/src/interfaces/ui/terminal/tui/HelpPanel.tsx +0 -262
- package/packages/src/interfaces/ui/terminal/tui/InputContext.tsx +0 -232
- package/packages/src/interfaces/ui/terminal/tui/InputField.tsx +0 -62
- package/packages/src/interfaces/ui/terminal/tui/InteractiveTUI.tsx +0 -537
- package/packages/src/interfaces/ui/terminal/tui/MessageArea.tsx +0 -107
- package/packages/src/interfaces/ui/terminal/tui/MessageStore.tsx +0 -240
- package/packages/src/interfaces/ui/terminal/tui/StatusBar.tsx +0 -54
- package/packages/src/interfaces/ui/terminal/tui/commands.ts +0 -438
- package/packages/src/interfaces/ui/terminal/tui/components/InteractiveElements.tsx +0 -584
- package/packages/src/interfaces/ui/terminal/tui/components/MultilineInput.tsx +0 -614
- package/packages/src/interfaces/ui/terminal/tui/components/PaneManager.tsx +0 -333
- package/packages/src/interfaces/ui/terminal/tui/components/Sidebar.tsx +0 -604
- package/packages/src/interfaces/ui/terminal/tui/components/index.ts +0 -118
- package/packages/src/interfaces/ui/terminal/tui/console.ts +0 -49
- package/packages/src/interfaces/ui/terminal/tui/index.ts +0 -90
- package/packages/src/interfaces/ui/terminal/tui/run.tsx +0 -42
- package/packages/src/interfaces/ui/terminal/tui/spinner.ts +0 -69
- package/packages/src/interfaces/ui/terminal/tui/tui-app.tsx +0 -390
- package/packages/src/interfaces/ui/terminal/tui/tui-footer.ts +0 -422
- package/packages/src/interfaces/ui/terminal/tui/types.ts +0 -186
- package/packages/src/interfaces/ui/terminal/tui/useInputHandler.ts +0 -104
- 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
|
-
|
|
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
|
-
|
|
385
|
-
|
|
386
|
-
|
|
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
|
-
//
|
|
840
|
-
|
|
841
|
-
|
|
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
|
-
|
|
846
|
-
|
|
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
|
-
|
|
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
|
+
});
|