pi-teams 0.8.7 → 0.9.2
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/extensions/index.ts +83 -5
- package/package.json +1 -1
- package/src/adapters/cmux-adapter.test.ts +309 -0
- package/src/adapters/cmux-adapter.ts +5 -1
- package/src/adapters/terminal-registry.ts +13 -9
- package/src/adapters/tmux-adapter.test.ts +231 -0
- package/src/adapters/tmux-adapter.ts +70 -6
- package/src/adapters/windows-adapter.test.ts +86 -155
- package/src/utils/paths.ts +4 -0
- package/src/utils/runtime.test.ts +171 -0
- package/src/utils/runtime.ts +168 -0
- package/src/utils/terminal-adapter.ts +2 -0
|
@@ -2,22 +2,44 @@
|
|
|
2
2
|
* Windows Adapter Tests
|
|
3
3
|
*/
|
|
4
4
|
|
|
5
|
-
import { describe, it, expect, beforeEach, vi } from "vitest";
|
|
5
|
+
import { describe, it, expect, beforeEach, vi, afterEach } from "vitest";
|
|
6
|
+
|
|
7
|
+
// Mock at module level - this is hoisted by Vitest
|
|
8
|
+
vi.mock("../utils/terminal-adapter", () => ({
|
|
9
|
+
execCommand: vi.fn(),
|
|
10
|
+
TerminalAdapter: class {},
|
|
11
|
+
}));
|
|
12
|
+
|
|
13
|
+
// Import after mock
|
|
14
|
+
import { execCommand } from "../utils/terminal-adapter";
|
|
6
15
|
import { WindowsAdapter } from "./windows-adapter";
|
|
7
16
|
|
|
8
|
-
|
|
17
|
+
const mockExecCommand = vi.mocked(execCommand);
|
|
18
|
+
|
|
9
19
|
const originalPlatform = process.platform;
|
|
10
20
|
|
|
11
21
|
describe("WindowsAdapter", () => {
|
|
12
22
|
let adapter: WindowsAdapter;
|
|
13
|
-
let mockExecCommand: ReturnType<typeof vi.fn>;
|
|
14
23
|
|
|
15
24
|
beforeEach(() => {
|
|
25
|
+
// Create a fresh adapter for each test
|
|
16
26
|
adapter = new WindowsAdapter();
|
|
17
27
|
vi.resetAllMocks();
|
|
18
28
|
vi.clearAllMocks();
|
|
19
29
|
|
|
20
|
-
//
|
|
30
|
+
// Reset environment
|
|
31
|
+
delete process.env.TMUX;
|
|
32
|
+
delete process.env.ZELLIJ;
|
|
33
|
+
delete process.env.WEZTERM_PANE;
|
|
34
|
+
|
|
35
|
+
Object.defineProperty(process, "platform", {
|
|
36
|
+
value: originalPlatform,
|
|
37
|
+
writable: true,
|
|
38
|
+
configurable: true,
|
|
39
|
+
});
|
|
40
|
+
});
|
|
41
|
+
|
|
42
|
+
afterEach(() => {
|
|
21
43
|
Object.defineProperty(process, "platform", {
|
|
22
44
|
value: originalPlatform,
|
|
23
45
|
writable: true,
|
|
@@ -34,22 +56,7 @@ describe("WindowsAdapter", () => {
|
|
|
34
56
|
describe("detect()", () => {
|
|
35
57
|
it("should detect when on Windows and wt is available", () => {
|
|
36
58
|
Object.defineProperty(process, "platform", { value: "win32" });
|
|
37
|
-
|
|
38
|
-
delete process.env.ZELLIJ;
|
|
39
|
-
delete process.env.WEZTERM_PANE;
|
|
40
|
-
|
|
41
|
-
// Mock successful wt --version
|
|
42
|
-
vi.mock("../utils/terminal-adapter", async () => {
|
|
43
|
-
const actual = await vi.importActual<typeof import("../utils/terminal-adapter")>("../utils/terminal-adapter");
|
|
44
|
-
return {
|
|
45
|
-
...actual,
|
|
46
|
-
execCommand: vi.fn().mockReturnValue({ stdout: "Windows Terminal", status: 0 }),
|
|
47
|
-
};
|
|
48
|
-
});
|
|
49
|
-
|
|
50
|
-
const { execCommand } = require("../utils/terminal-adapter");
|
|
51
|
-
// Create new adapter to use mocked execCommand
|
|
52
|
-
adapter = new WindowsAdapter();
|
|
59
|
+
mockExecCommand.mockReturnValue({ stdout: "Windows Terminal", status: 0 });
|
|
53
60
|
|
|
54
61
|
expect(adapter.detect()).toBe(true);
|
|
55
62
|
});
|
|
@@ -85,17 +92,12 @@ describe("WindowsAdapter", () => {
|
|
|
85
92
|
describe("spawn()", () => {
|
|
86
93
|
it("should spawn first pane on Windows", () => {
|
|
87
94
|
Object.defineProperty(process, "platform", { value: "win32" });
|
|
88
|
-
|
|
89
|
-
|
|
90
|
-
|
|
91
|
-
|
|
92
|
-
|
|
93
|
-
|
|
94
|
-
};
|
|
95
|
-
});
|
|
96
|
-
|
|
97
|
-
const { execCommand } = require("../utils/terminal-adapter");
|
|
98
|
-
adapter = new WindowsAdapter();
|
|
95
|
+
// Mock findWtBinary check
|
|
96
|
+
mockExecCommand.mockReturnValueOnce({ stdout: "wt", status: 0 });
|
|
97
|
+
// Mock getPanes - no existing panes
|
|
98
|
+
mockExecCommand.mockReturnValueOnce({ stdout: "[]", status: 0 });
|
|
99
|
+
// Mock actual spawn
|
|
100
|
+
mockExecCommand.mockReturnValueOnce({ stdout: "", status: 0 });
|
|
99
101
|
|
|
100
102
|
const paneId = adapter.spawn({
|
|
101
103
|
name: "test-agent",
|
|
@@ -104,210 +106,139 @@ describe("WindowsAdapter", () => {
|
|
|
104
106
|
env: { PI_TEAM_NAME: "team1", PI_AGENT_NAME: "agent1" },
|
|
105
107
|
});
|
|
106
108
|
|
|
109
|
+
// Returns synthetic ID: windows_<timestamp>_<name>
|
|
107
110
|
expect(paneId).toMatch(/^windows_\d+_test-agent$/);
|
|
108
|
-
expect(execCommand).toHaveBeenCalledWith(
|
|
109
|
-
"wt",
|
|
110
|
-
expect.arrayContaining([
|
|
111
|
-
"split-pane",
|
|
112
|
-
"--vertical",
|
|
113
|
-
"--size", "50%",
|
|
114
|
-
"--", "pwsh", "-NoExit", "-Command",
|
|
115
|
-
])
|
|
116
|
-
);
|
|
117
111
|
});
|
|
118
112
|
|
|
119
113
|
it("should spawn subsequent pane on Windows", () => {
|
|
120
114
|
Object.defineProperty(process, "platform", { value: "win32" });
|
|
121
|
-
|
|
122
|
-
|
|
123
|
-
|
|
124
|
-
|
|
125
|
-
|
|
126
|
-
|
|
127
|
-
.mockReturnValueOnce({ stdout: "pane-id", status: 0 }) // wt --version
|
|
128
|
-
.mockReturnValueOnce({ stdout: '[{"window":1,"pane":1},{"window":1,"pane":2}]', status: 0 }) // wt list
|
|
129
|
-
.mockReturnValue({ stdout: "pane-id", status: 0 }), // split-pane
|
|
130
|
-
};
|
|
131
|
-
});
|
|
132
|
-
|
|
133
|
-
const { execCommand } = require("../utils/terminal-adapter");
|
|
134
|
-
adapter = new WindowsAdapter();
|
|
115
|
+
// Mock findWtBinary check
|
|
116
|
+
mockExecCommand.mockReturnValueOnce({ stdout: "wt", status: 0 });
|
|
117
|
+
// Mock getPanes - existing panes
|
|
118
|
+
mockExecCommand.mockReturnValueOnce({ stdout: '[{"window":1}]', status: 0 });
|
|
119
|
+
// Mock actual spawn
|
|
120
|
+
mockExecCommand.mockReturnValueOnce({ stdout: "", status: 0 });
|
|
135
121
|
|
|
136
122
|
const paneId = adapter.spawn({
|
|
137
|
-
name: "test-agent",
|
|
123
|
+
name: "test-agent-2",
|
|
138
124
|
cwd: "/test/path",
|
|
139
125
|
command: "pi --model gpt-4",
|
|
140
126
|
env: { PI_TEAM_NAME: "team1", PI_AGENT_NAME: "agent1" },
|
|
141
127
|
});
|
|
142
128
|
|
|
143
|
-
|
|
144
|
-
expect(
|
|
145
|
-
"wt",
|
|
146
|
-
expect.arrayContaining(["split-pane", "--horizontal"])
|
|
147
|
-
);
|
|
129
|
+
// Returns synthetic ID: windows_<timestamp>_<name>
|
|
130
|
+
expect(paneId).toMatch(/^windows_\d+_test-agent-2$/);
|
|
148
131
|
});
|
|
149
132
|
|
|
150
133
|
it("should throw error when wt binary not found", () => {
|
|
151
134
|
Object.defineProperty(process, "platform", { value: "win32" });
|
|
152
|
-
|
|
153
|
-
|
|
154
|
-
|
|
155
|
-
|
|
156
|
-
|
|
157
|
-
|
|
158
|
-
|
|
159
|
-
|
|
160
|
-
|
|
161
|
-
|
|
162
|
-
|
|
163
|
-
|
|
164
|
-
expect(() => adapter.spawn({
|
|
165
|
-
name: "test-agent",
|
|
166
|
-
cwd: "/test/path",
|
|
167
|
-
command: "pi",
|
|
168
|
-
env: {},
|
|
169
|
-
})).toThrow("Windows Terminal (wt) CLI binary not found");
|
|
135
|
+
// Mock findWtBinary - not found
|
|
136
|
+
mockExecCommand.mockReturnValue({ stdout: "", status: 1 });
|
|
137
|
+
|
|
138
|
+
expect(() =>
|
|
139
|
+
adapter.spawn({
|
|
140
|
+
name: "test-agent",
|
|
141
|
+
cwd: "/test/path",
|
|
142
|
+
command: "pi",
|
|
143
|
+
env: {},
|
|
144
|
+
})
|
|
145
|
+
).toThrow();
|
|
170
146
|
});
|
|
171
147
|
});
|
|
172
148
|
|
|
173
149
|
describe("supportsWindows()", () => {
|
|
174
150
|
it("should return true when wt is available", () => {
|
|
175
151
|
Object.defineProperty(process, "platform", { value: "win32" });
|
|
176
|
-
|
|
177
|
-
vi.mock("../utils/terminal-adapter", async () => {
|
|
178
|
-
const actual = await vi.importActual<typeof import("../utils/terminal-adapter")>("../utils/terminal-adapter");
|
|
179
|
-
return {
|
|
180
|
-
...actual,
|
|
181
|
-
execCommand: vi.fn().mockReturnValue({ stdout: "version", status: 0 }),
|
|
182
|
-
};
|
|
183
|
-
});
|
|
184
|
-
|
|
185
|
-
const { execCommand } = require("../utils/terminal-adapter");
|
|
186
|
-
adapter = new WindowsAdapter();
|
|
152
|
+
mockExecCommand.mockReturnValue({ stdout: "wt found", status: 0 });
|
|
187
153
|
|
|
188
154
|
expect(adapter.supportsWindows()).toBe(true);
|
|
189
155
|
});
|
|
190
156
|
|
|
191
|
-
it("should return
|
|
157
|
+
it("should return true on Windows even if wt check fails (fallback)", () => {
|
|
192
158
|
Object.defineProperty(process, "platform", { value: "win32" });
|
|
159
|
+
// The Windows adapter has a fallback that assumes wt exists on Windows
|
|
160
|
+
mockExecCommand.mockReturnValue({ stdout: "", status: 1 });
|
|
193
161
|
|
|
194
|
-
|
|
195
|
-
|
|
196
|
-
return {
|
|
197
|
-
...actual,
|
|
198
|
-
execCommand: vi.fn().mockReturnValue({ stdout: "", status: 1 }),
|
|
199
|
-
};
|
|
200
|
-
});
|
|
201
|
-
|
|
202
|
-
const { execCommand } = require("../utils/terminal-adapter");
|
|
203
|
-
adapter = new WindowsAdapter();
|
|
204
|
-
|
|
205
|
-
expect(adapter.supportsWindows()).toBe(false);
|
|
162
|
+
// On Windows, the adapter falls back to assuming wt exists
|
|
163
|
+
expect(adapter.supportsWindows()).toBe(true);
|
|
206
164
|
});
|
|
207
165
|
});
|
|
208
166
|
|
|
209
167
|
describe("spawnWindow()", () => {
|
|
210
168
|
it("should spawn a new window", () => {
|
|
211
169
|
Object.defineProperty(process, "platform", { value: "win32" });
|
|
212
|
-
|
|
213
|
-
|
|
214
|
-
|
|
215
|
-
|
|
216
|
-
...actual,
|
|
217
|
-
execCommand: vi.fn().mockReturnValue({ stdout: "window-id", status: 0 }),
|
|
218
|
-
};
|
|
219
|
-
});
|
|
220
|
-
|
|
221
|
-
const { execCommand } = require("../utils/terminal-adapter");
|
|
222
|
-
adapter = new WindowsAdapter();
|
|
170
|
+
// Mock findWtBinary check
|
|
171
|
+
mockExecCommand.mockReturnValueOnce({ stdout: "wt", status: 0 });
|
|
172
|
+
// Mock actual spawn
|
|
173
|
+
mockExecCommand.mockReturnValueOnce({ stdout: "", status: 0 });
|
|
223
174
|
|
|
224
175
|
const windowId = adapter.spawnWindow({
|
|
225
|
-
name: "
|
|
226
|
-
cwd: "/test",
|
|
176
|
+
name: "team-lead",
|
|
177
|
+
cwd: "/test/path",
|
|
227
178
|
command: "pi",
|
|
228
|
-
env: {},
|
|
179
|
+
env: { PI_TEAM_NAME: "team1", PI_AGENT_NAME: "team-lead" },
|
|
229
180
|
teamName: "team1",
|
|
230
181
|
});
|
|
231
182
|
|
|
232
|
-
|
|
233
|
-
expect(
|
|
234
|
-
"wt",
|
|
235
|
-
expect.arrayContaining([
|
|
236
|
-
"new-window",
|
|
237
|
-
"--title", "team1: agent",
|
|
238
|
-
])
|
|
239
|
-
);
|
|
183
|
+
// Returns synthetic ID: windows_win_<timestamp>_<name>
|
|
184
|
+
expect(windowId).toMatch(/^windows_win_\d+_team-lead$/);
|
|
240
185
|
});
|
|
241
186
|
});
|
|
242
187
|
|
|
243
188
|
describe("kill()", () => {
|
|
244
189
|
it("should handle kill gracefully for windows pane", () => {
|
|
245
|
-
|
|
246
|
-
|
|
247
|
-
expect(true).toBe(true);
|
|
190
|
+
// Should not throw
|
|
191
|
+
adapter.kill("windows_123_test-pane");
|
|
248
192
|
});
|
|
249
193
|
|
|
250
194
|
it("should ignore non-windows pane IDs", () => {
|
|
251
|
-
|
|
252
|
-
|
|
253
|
-
|
|
195
|
+
// Should not throw for non-windows pane IDs
|
|
196
|
+
adapter.kill("tmux_pane-123");
|
|
197
|
+
adapter.kill("iterm_tab-456");
|
|
254
198
|
});
|
|
255
199
|
});
|
|
256
200
|
|
|
257
201
|
describe("killWindow()", () => {
|
|
258
202
|
it("should handle killWindow gracefully", () => {
|
|
259
|
-
|
|
260
|
-
|
|
261
|
-
expect(true).toBe(true);
|
|
203
|
+
// Should not throw
|
|
204
|
+
adapter.killWindow("windows_win_123_test-window");
|
|
262
205
|
});
|
|
263
206
|
});
|
|
264
207
|
|
|
265
208
|
describe("isAlive()", () => {
|
|
266
209
|
it("should return true for windows pane ID", () => {
|
|
267
|
-
expect(adapter.isAlive("
|
|
210
|
+
expect(adapter.isAlive("windows_123_test")).toBe(true);
|
|
268
211
|
});
|
|
269
212
|
|
|
270
213
|
it("should return false for non-windows pane ID", () => {
|
|
271
|
-
expect(adapter.isAlive("
|
|
214
|
+
expect(adapter.isAlive("tmux_pane-123")).toBe(false);
|
|
272
215
|
});
|
|
273
216
|
});
|
|
274
217
|
|
|
275
218
|
describe("isWindowAlive()", () => {
|
|
276
219
|
it("should return true for windows window ID", () => {
|
|
277
|
-
expect(adapter.isWindowAlive("
|
|
220
|
+
expect(adapter.isWindowAlive("windows_win_123_test")).toBe(true);
|
|
278
221
|
});
|
|
279
222
|
|
|
280
223
|
it("should return false for non-windows window ID", () => {
|
|
281
|
-
expect(adapter.isWindowAlive("
|
|
224
|
+
expect(adapter.isWindowAlive("iterm_window-123")).toBe(false);
|
|
282
225
|
});
|
|
283
226
|
});
|
|
284
227
|
|
|
285
228
|
describe("setTitle()", () => {
|
|
286
229
|
it("should set tab title gracefully", () => {
|
|
287
230
|
Object.defineProperty(process, "platform", { value: "win32" });
|
|
231
|
+
mockExecCommand.mockReturnValue({ stdout: "", status: 0 });
|
|
288
232
|
|
|
289
|
-
|
|
290
|
-
|
|
291
|
-
return {
|
|
292
|
-
...actual,
|
|
293
|
-
execCommand: vi.fn().mockReturnValue({ stdout: "", status: 0 }),
|
|
294
|
-
};
|
|
295
|
-
});
|
|
296
|
-
|
|
297
|
-
const { execCommand } = require("../utils/terminal-adapter");
|
|
298
|
-
adapter = new WindowsAdapter();
|
|
299
|
-
|
|
300
|
-
adapter.setTitle("Test Title");
|
|
301
|
-
expect(execCommand).toHaveBeenCalledWith("wt", ["set-tab-title", "Test Title"]);
|
|
233
|
+
// Should not throw
|
|
234
|
+
adapter.setTitle("windows_123_test", "New Title");
|
|
302
235
|
});
|
|
303
236
|
});
|
|
304
237
|
|
|
305
238
|
describe("setWindowTitle()", () => {
|
|
306
239
|
it("should gracefully handle setWindowTitle limitation", () => {
|
|
307
|
-
adapter
|
|
308
|
-
|
|
309
|
-
// Should silently do nothing without throwing
|
|
310
|
-
expect(true).toBe(true);
|
|
240
|
+
// Windows adapter doesn't support setWindowTitle, should not throw
|
|
241
|
+
adapter.setWindowTitle("windows_win_123_test", "New Title");
|
|
311
242
|
});
|
|
312
243
|
});
|
|
313
|
-
});
|
|
244
|
+
});
|
package/src/utils/paths.ts
CHANGED
|
@@ -32,6 +32,10 @@ export function inboxPath(teamName: string, agentName: string) {
|
|
|
32
32
|
return path.join(teamDir(teamName), "inboxes", `${sanitizeName(agentName)}.json`);
|
|
33
33
|
}
|
|
34
34
|
|
|
35
|
+
export function runtimeStatusPath(teamName: string, agentName: string) {
|
|
36
|
+
return path.join(teamDir(teamName), "runtime", `${sanitizeName(agentName)}.json`);
|
|
37
|
+
}
|
|
38
|
+
|
|
35
39
|
export function configPath(teamName: string) {
|
|
36
40
|
return path.join(teamDir(teamName), "config.json");
|
|
37
41
|
}
|
|
@@ -0,0 +1,171 @@
|
|
|
1
|
+
import { describe, it, expect, beforeEach, afterEach } from "vitest";
|
|
2
|
+
import fs from "node:fs";
|
|
3
|
+
import path from "node:path";
|
|
4
|
+
import {
|
|
5
|
+
writeRuntimeStatus,
|
|
6
|
+
readRuntimeStatus,
|
|
7
|
+
deleteRuntimeStatus,
|
|
8
|
+
cleanupStaleRuntimeFiles,
|
|
9
|
+
createRuntimeError,
|
|
10
|
+
HEARTBEAT_STALE_MS,
|
|
11
|
+
STARTUP_STALL_MS,
|
|
12
|
+
RUNTIME_STALE_MS,
|
|
13
|
+
} from "./runtime";
|
|
14
|
+
import { runtimeStatusPath, teamDir } from "./paths";
|
|
15
|
+
|
|
16
|
+
describe("runtime status", () => {
|
|
17
|
+
const teamName = `runtime-test-${Date.now()}`;
|
|
18
|
+
const agentName = "worker-1";
|
|
19
|
+
|
|
20
|
+
beforeEach(() => {
|
|
21
|
+
const dir = teamDir(teamName);
|
|
22
|
+
if (!fs.existsSync(dir)) fs.mkdirSync(dir, { recursive: true });
|
|
23
|
+
});
|
|
24
|
+
|
|
25
|
+
afterEach(() => {
|
|
26
|
+
const dir = teamDir(teamName);
|
|
27
|
+
if (fs.existsSync(dir)) fs.rmSync(dir, { recursive: true, force: true });
|
|
28
|
+
});
|
|
29
|
+
|
|
30
|
+
it("writes and reads status", async () => {
|
|
31
|
+
await writeRuntimeStatus(teamName, agentName, {
|
|
32
|
+
pid: 123,
|
|
33
|
+
startedAt: 1000,
|
|
34
|
+
ready: false,
|
|
35
|
+
});
|
|
36
|
+
|
|
37
|
+
const runtime = await readRuntimeStatus(teamName, agentName);
|
|
38
|
+
expect(runtime).not.toBeNull();
|
|
39
|
+
expect(runtime?.teamName).toBe(teamName);
|
|
40
|
+
expect(runtime?.agentName).toBe(agentName);
|
|
41
|
+
expect(runtime?.pid).toBe(123);
|
|
42
|
+
expect(runtime?.ready).toBe(false);
|
|
43
|
+
});
|
|
44
|
+
|
|
45
|
+
it("merges updates instead of overwriting status", async () => {
|
|
46
|
+
await writeRuntimeStatus(teamName, agentName, {
|
|
47
|
+
pid: 123,
|
|
48
|
+
startedAt: 1000,
|
|
49
|
+
ready: false,
|
|
50
|
+
});
|
|
51
|
+
|
|
52
|
+
await writeRuntimeStatus(teamName, agentName, {
|
|
53
|
+
lastHeartbeatAt: 2000,
|
|
54
|
+
ready: true,
|
|
55
|
+
});
|
|
56
|
+
|
|
57
|
+
const runtime = await readRuntimeStatus(teamName, agentName);
|
|
58
|
+
expect(runtime?.pid).toBe(123);
|
|
59
|
+
expect(runtime?.startedAt).toBe(1000);
|
|
60
|
+
expect(runtime?.lastHeartbeatAt).toBe(2000);
|
|
61
|
+
expect(runtime?.ready).toBe(true);
|
|
62
|
+
});
|
|
63
|
+
|
|
64
|
+
it("returns null when status does not exist", async () => {
|
|
65
|
+
const missing = await readRuntimeStatus(teamName, "missing-agent");
|
|
66
|
+
expect(missing).toBeNull();
|
|
67
|
+
});
|
|
68
|
+
|
|
69
|
+
it("stores status in team runtime directory", async () => {
|
|
70
|
+
await writeRuntimeStatus(teamName, agentName, { ready: true });
|
|
71
|
+
const p = runtimeStatusPath(teamName, agentName);
|
|
72
|
+
expect(path.basename(path.dirname(p))).toBe("runtime");
|
|
73
|
+
expect(fs.existsSync(p)).toBe(true);
|
|
74
|
+
});
|
|
75
|
+
|
|
76
|
+
describe("deleteRuntimeStatus", () => {
|
|
77
|
+
it("deletes existing runtime status", async () => {
|
|
78
|
+
await writeRuntimeStatus(teamName, agentName, { ready: true });
|
|
79
|
+
const deleted = await deleteRuntimeStatus(teamName, agentName);
|
|
80
|
+
expect(deleted).toBe(true);
|
|
81
|
+
|
|
82
|
+
const runtime = await readRuntimeStatus(teamName, agentName);
|
|
83
|
+
expect(runtime).toBeNull();
|
|
84
|
+
});
|
|
85
|
+
|
|
86
|
+
it("returns false when status does not exist", async () => {
|
|
87
|
+
const deleted = await deleteRuntimeStatus(teamName, "nonexistent");
|
|
88
|
+
expect(deleted).toBe(false);
|
|
89
|
+
});
|
|
90
|
+
});
|
|
91
|
+
|
|
92
|
+
describe("cleanupStaleRuntimeFiles", () => {
|
|
93
|
+
it("removes stale runtime files with old heartbeats", async () => {
|
|
94
|
+
const staleTime = Date.now() - RUNTIME_STALE_MS - 1000;
|
|
95
|
+
await writeRuntimeStatus(teamName, "stale-agent", {
|
|
96
|
+
startedAt: staleTime,
|
|
97
|
+
lastHeartbeatAt: staleTime,
|
|
98
|
+
ready: true,
|
|
99
|
+
});
|
|
100
|
+
|
|
101
|
+
const cleaned = await cleanupStaleRuntimeFiles(teamName);
|
|
102
|
+
expect(cleaned).toBe(1);
|
|
103
|
+
|
|
104
|
+
const runtime = await readRuntimeStatus(teamName, "stale-agent");
|
|
105
|
+
expect(runtime).toBeNull();
|
|
106
|
+
});
|
|
107
|
+
|
|
108
|
+
it("preserves runtime files with recent heartbeats", async () => {
|
|
109
|
+
await writeRuntimeStatus(teamName, agentName, {
|
|
110
|
+
startedAt: Date.now() - RUNTIME_STALE_MS - 1000,
|
|
111
|
+
lastHeartbeatAt: Date.now(),
|
|
112
|
+
ready: true,
|
|
113
|
+
});
|
|
114
|
+
|
|
115
|
+
const cleaned = await cleanupStaleRuntimeFiles(teamName);
|
|
116
|
+
expect(cleaned).toBe(0);
|
|
117
|
+
|
|
118
|
+
const runtime = await readRuntimeStatus(teamName, agentName);
|
|
119
|
+
expect(runtime).not.toBeNull();
|
|
120
|
+
});
|
|
121
|
+
|
|
122
|
+
it("removes corrupted files", async () => {
|
|
123
|
+
const runtimeDir = path.join(teamDir(teamName), "runtime");
|
|
124
|
+
if (!fs.existsSync(runtimeDir)) fs.mkdirSync(runtimeDir, { recursive: true });
|
|
125
|
+
fs.writeFileSync(path.join(runtimeDir, "corrupted.json"), "not valid json");
|
|
126
|
+
|
|
127
|
+
const cleaned = await cleanupStaleRuntimeFiles(teamName);
|
|
128
|
+
expect(cleaned).toBe(1);
|
|
129
|
+
});
|
|
130
|
+
|
|
131
|
+
it("returns 0 when no runtime directory exists", async () => {
|
|
132
|
+
const cleaned = await cleanupStaleRuntimeFiles("nonexistent-team");
|
|
133
|
+
expect(cleaned).toBe(0);
|
|
134
|
+
});
|
|
135
|
+
});
|
|
136
|
+
|
|
137
|
+
describe("createRuntimeError", () => {
|
|
138
|
+
it("creates structured error from Error object", () => {
|
|
139
|
+
const error = new Error("Test error");
|
|
140
|
+
const runtimeError = createRuntimeError(error);
|
|
141
|
+
expect(runtimeError.message).toBe("Test error");
|
|
142
|
+
expect(runtimeError.timestamp).toBeGreaterThan(0);
|
|
143
|
+
});
|
|
144
|
+
|
|
145
|
+
it("creates structured error from string", () => {
|
|
146
|
+
const runtimeError = createRuntimeError("String error");
|
|
147
|
+
expect(runtimeError.message).toBe("String error");
|
|
148
|
+
expect(runtimeError.timestamp).toBeGreaterThan(0);
|
|
149
|
+
});
|
|
150
|
+
|
|
151
|
+
it("creates structured error from unknown type", () => {
|
|
152
|
+
const runtimeError = createRuntimeError({ weird: "object" });
|
|
153
|
+
expect(runtimeError.message).toBe("[object Object]");
|
|
154
|
+
expect(runtimeError.timestamp).toBeGreaterThan(0);
|
|
155
|
+
});
|
|
156
|
+
});
|
|
157
|
+
|
|
158
|
+
describe("constants", () => {
|
|
159
|
+
it("exports HEARTBEAT_STALE_MS with correct value", () => {
|
|
160
|
+
expect(HEARTBEAT_STALE_MS).toBe(90000);
|
|
161
|
+
});
|
|
162
|
+
|
|
163
|
+
it("exports STARTUP_STALL_MS with correct value", () => {
|
|
164
|
+
expect(STARTUP_STALL_MS).toBe(60000);
|
|
165
|
+
});
|
|
166
|
+
|
|
167
|
+
it("exports RUNTIME_STALE_MS with correct value", () => {
|
|
168
|
+
expect(RUNTIME_STALE_MS).toBe(300000);
|
|
169
|
+
});
|
|
170
|
+
});
|
|
171
|
+
});
|