@herdctl/core 0.0.1 → 0.0.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/dist/config/__tests__/agent.test.js +31 -13
- package/dist/config/__tests__/agent.test.js.map +1 -1
- package/dist/config/__tests__/merge.test.js +9 -2
- package/dist/config/__tests__/merge.test.js.map +1 -1
- package/dist/config/__tests__/schema.test.js +350 -1
- package/dist/config/__tests__/schema.test.js.map +1 -1
- package/dist/config/index.d.ts +1 -1
- package/dist/config/index.d.ts.map +1 -1
- package/dist/config/index.js +3 -1
- package/dist/config/index.js.map +1 -1
- package/dist/config/schema.d.ts +828 -24
- package/dist/config/schema.d.ts.map +1 -1
- package/dist/config/schema.js +118 -6
- package/dist/config/schema.js.map +1 -1
- package/dist/fleet-manager/__tests__/coverage.test.js +11 -332
- package/dist/fleet-manager/__tests__/coverage.test.js.map +1 -1
- package/dist/fleet-manager/__tests__/errors.test.js +1 -49
- package/dist/fleet-manager/__tests__/errors.test.js.map +1 -1
- package/dist/fleet-manager/__tests__/integration.test.js +109 -0
- package/dist/fleet-manager/__tests__/integration.test.js.map +1 -1
- package/dist/fleet-manager/__tests__/reload.test.js +1 -1
- package/dist/fleet-manager/__tests__/reload.test.js.map +1 -1
- package/dist/fleet-manager/config-reload.d.ts +164 -0
- package/dist/fleet-manager/config-reload.d.ts.map +1 -0
- package/dist/fleet-manager/config-reload.js +445 -0
- package/dist/fleet-manager/config-reload.js.map +1 -0
- package/dist/fleet-manager/context.d.ts +76 -0
- package/dist/fleet-manager/context.d.ts.map +1 -0
- package/dist/fleet-manager/context.js +11 -0
- package/dist/fleet-manager/context.js.map +1 -0
- package/dist/fleet-manager/errors.d.ts +0 -25
- package/dist/fleet-manager/errors.d.ts.map +1 -1
- package/dist/fleet-manager/errors.js +0 -38
- package/dist/fleet-manager/errors.js.map +1 -1
- package/dist/fleet-manager/event-emitters.d.ts +123 -0
- package/dist/fleet-manager/event-emitters.d.ts.map +1 -0
- package/dist/fleet-manager/event-emitters.js +136 -0
- package/dist/fleet-manager/event-emitters.js.map +1 -0
- package/dist/fleet-manager/event-types.d.ts +0 -15
- package/dist/fleet-manager/event-types.d.ts.map +1 -1
- package/dist/fleet-manager/fleet-manager.d.ts +40 -653
- package/dist/fleet-manager/fleet-manager.d.ts.map +1 -1
- package/dist/fleet-manager/fleet-manager.js +95 -1720
- package/dist/fleet-manager/fleet-manager.js.map +1 -1
- package/dist/fleet-manager/index.d.ts +13 -2
- package/dist/fleet-manager/index.d.ts.map +1 -1
- package/dist/fleet-manager/index.js +19 -6
- package/dist/fleet-manager/index.js.map +1 -1
- package/dist/fleet-manager/job-control.d.ts +64 -0
- package/dist/fleet-manager/job-control.d.ts.map +1 -0
- package/dist/fleet-manager/job-control.js +296 -0
- package/dist/fleet-manager/job-control.js.map +1 -0
- package/dist/fleet-manager/log-streaming.d.ts +171 -0
- package/dist/fleet-manager/log-streaming.d.ts.map +1 -0
- package/dist/fleet-manager/log-streaming.js +503 -0
- package/dist/fleet-manager/log-streaming.js.map +1 -0
- package/dist/fleet-manager/schedule-executor.d.ts +63 -0
- package/dist/fleet-manager/schedule-executor.d.ts.map +1 -0
- package/dist/fleet-manager/schedule-executor.js +209 -0
- package/dist/fleet-manager/schedule-executor.js.map +1 -0
- package/dist/fleet-manager/schedule-management.d.ts +71 -0
- package/dist/fleet-manager/schedule-management.d.ts.map +1 -0
- package/dist/fleet-manager/schedule-management.js +171 -0
- package/dist/fleet-manager/schedule-management.js.map +1 -0
- package/dist/fleet-manager/status-queries.d.ts +105 -0
- package/dist/fleet-manager/status-queries.d.ts.map +1 -0
- package/dist/fleet-manager/status-queries.js +247 -0
- package/dist/fleet-manager/status-queries.js.map +1 -0
- package/dist/fleet-manager/types.d.ts +0 -39
- package/dist/fleet-manager/types.d.ts.map +1 -1
- package/dist/runner/__tests__/job-executor.test.js +206 -1
- package/dist/runner/__tests__/job-executor.test.js.map +1 -1
- package/dist/runner/job-executor.d.ts +9 -0
- package/dist/runner/job-executor.d.ts.map +1 -1
- package/dist/runner/job-executor.js +78 -4
- package/dist/runner/job-executor.js.map +1 -1
- package/dist/runner/types.d.ts +2 -0
- package/dist/runner/types.d.ts.map +1 -1
- package/dist/scheduler/__tests__/cron.test.d.ts +2 -0
- package/dist/scheduler/__tests__/cron.test.d.ts.map +1 -0
- package/dist/scheduler/__tests__/cron.test.js +867 -0
- package/dist/scheduler/__tests__/cron.test.js.map +1 -0
- package/dist/scheduler/__tests__/scheduler.test.js +164 -5
- package/dist/scheduler/__tests__/scheduler.test.js.map +1 -1
- package/dist/scheduler/cron.d.ts +126 -0
- package/dist/scheduler/cron.d.ts.map +1 -0
- package/dist/scheduler/cron.js +390 -0
- package/dist/scheduler/cron.js.map +1 -0
- package/dist/scheduler/errors.d.ts +81 -1
- package/dist/scheduler/errors.d.ts.map +1 -1
- package/dist/scheduler/errors.js +81 -6
- package/dist/scheduler/errors.js.map +1 -1
- package/dist/scheduler/index.d.ts +1 -0
- package/dist/scheduler/index.d.ts.map +1 -1
- package/dist/scheduler/index.js +2 -0
- package/dist/scheduler/index.js.map +1 -1
- package/dist/scheduler/schedule-runner.d.ts +2 -2
- package/dist/scheduler/schedule-runner.d.ts.map +1 -1
- package/dist/scheduler/schedule-runner.js +20 -8
- package/dist/scheduler/schedule-runner.js.map +1 -1
- package/dist/scheduler/scheduler.d.ts +4 -4
- package/dist/scheduler/scheduler.d.ts.map +1 -1
- package/dist/scheduler/scheduler.js +86 -20
- package/dist/scheduler/scheduler.js.map +1 -1
- package/dist/scheduler/types.d.ts +1 -1
- package/dist/scheduler/types.d.ts.map +1 -1
- package/dist/state/schemas/job-metadata.d.ts +2 -2
- package/package.json +33 -8
- package/.turbo/turbo-build.log +0 -4
- package/.turbo/turbo-test.log +0 -219
- package/.turbo/turbo-typecheck.log +0 -4
- package/coverage/base.css +0 -224
- package/coverage/block-navigation.js +0 -87
- package/coverage/coverage-final.json +0 -51
- package/coverage/favicon.png +0 -0
- package/coverage/index.html +0 -251
- package/coverage/prettify.css +0 -1
- package/coverage/prettify.js +0 -2
- package/coverage/sort-arrow-sprite.png +0 -0
- package/coverage/sorter.js +0 -210
- package/coverage/src/config/index.html +0 -191
- package/coverage/src/config/index.ts.html +0 -442
- package/coverage/src/config/interpolate.ts.html +0 -652
- package/coverage/src/config/loader.ts.html +0 -1501
- package/coverage/src/config/merge.ts.html +0 -823
- package/coverage/src/config/parser.ts.html +0 -1213
- package/coverage/src/config/schema.ts.html +0 -1123
- package/coverage/src/fleet-manager/errors.ts.html +0 -2326
- package/coverage/src/fleet-manager/event-types.ts.html +0 -1219
- package/coverage/src/fleet-manager/fleet-manager.ts.html +0 -7030
- package/coverage/src/fleet-manager/index.html +0 -206
- package/coverage/src/fleet-manager/index.ts.html +0 -469
- package/coverage/src/fleet-manager/job-manager.ts.html +0 -2074
- package/coverage/src/fleet-manager/job-queue.ts.html +0 -2479
- package/coverage/src/fleet-manager/types.ts.html +0 -2602
- package/coverage/src/index.html +0 -116
- package/coverage/src/index.ts.html +0 -181
- package/coverage/src/runner/errors.ts.html +0 -1006
- package/coverage/src/runner/index.html +0 -191
- package/coverage/src/runner/index.ts.html +0 -256
- package/coverage/src/runner/job-executor.ts.html +0 -1429
- package/coverage/src/runner/message-processor.ts.html +0 -1150
- package/coverage/src/runner/sdk-adapter.ts.html +0 -658
- package/coverage/src/runner/types.ts.html +0 -559
- package/coverage/src/scheduler/errors.ts.html +0 -388
- package/coverage/src/scheduler/index.html +0 -206
- package/coverage/src/scheduler/index.ts.html +0 -244
- package/coverage/src/scheduler/interval.ts.html +0 -652
- package/coverage/src/scheduler/schedule-runner.ts.html +0 -1411
- package/coverage/src/scheduler/schedule-state.ts.html +0 -718
- package/coverage/src/scheduler/scheduler.ts.html +0 -1795
- package/coverage/src/scheduler/types.ts.html +0 -733
- package/coverage/src/state/directory.ts.html +0 -736
- package/coverage/src/state/errors.ts.html +0 -376
- package/coverage/src/state/fleet-state.ts.html +0 -937
- package/coverage/src/state/index.html +0 -221
- package/coverage/src/state/index.ts.html +0 -322
- package/coverage/src/state/job-metadata.ts.html +0 -1420
- package/coverage/src/state/job-output.ts.html +0 -1033
- package/coverage/src/state/schemas/fleet-state.ts.html +0 -445
- package/coverage/src/state/schemas/index.html +0 -176
- package/coverage/src/state/schemas/index.ts.html +0 -286
- package/coverage/src/state/schemas/job-metadata.ts.html +0 -628
- package/coverage/src/state/schemas/job-output.ts.html +0 -616
- package/coverage/src/state/schemas/session-info.ts.html +0 -361
- package/coverage/src/state/session.ts.html +0 -844
- package/coverage/src/state/types.ts.html +0 -262
- package/coverage/src/state/utils/atomic.ts.html +0 -748
- package/coverage/src/state/utils/index.html +0 -146
- package/coverage/src/state/utils/index.ts.html +0 -103
- package/coverage/src/state/utils/reads.ts.html +0 -1621
- package/coverage/src/work-sources/adapters/github.ts.html +0 -3583
- package/coverage/src/work-sources/adapters/index.html +0 -131
- package/coverage/src/work-sources/adapters/index.ts.html +0 -277
- package/coverage/src/work-sources/errors.ts.html +0 -298
- package/coverage/src/work-sources/index.html +0 -176
- package/coverage/src/work-sources/index.ts.html +0 -529
- package/coverage/src/work-sources/manager.ts.html +0 -1324
- package/coverage/src/work-sources/registry.ts.html +0 -619
- package/coverage/src/work-sources/types.ts.html +0 -568
- package/dist/fleet-manager/__tests__/event-helpers.test.d.ts +0 -7
- package/dist/fleet-manager/__tests__/event-helpers.test.d.ts.map +0 -1
- package/dist/fleet-manager/__tests__/event-helpers.test.js +0 -368
- package/dist/fleet-manager/__tests__/event-helpers.test.js.map +0 -1
- package/src/config/__tests__/agent.test.ts +0 -864
- package/src/config/__tests__/interpolate.test.ts +0 -644
- package/src/config/__tests__/loader.test.ts +0 -784
- package/src/config/__tests__/merge.test.ts +0 -751
- package/src/config/__tests__/parser.test.ts +0 -533
- package/src/config/__tests__/schema.test.ts +0 -873
- package/src/config/index.ts +0 -119
- package/src/config/interpolate.ts +0 -189
- package/src/config/loader.ts +0 -472
- package/src/config/merge.ts +0 -246
- package/src/config/parser.ts +0 -376
- package/src/config/schema.ts +0 -346
- package/src/fleet-manager/__tests__/coverage.test.ts +0 -2869
- package/src/fleet-manager/__tests__/errors.test.ts +0 -660
- package/src/fleet-manager/__tests__/event-helpers.test.ts +0 -448
- package/src/fleet-manager/__tests__/integration.test.ts +0 -1209
- package/src/fleet-manager/__tests__/job-control.test.ts +0 -283
- package/src/fleet-manager/__tests__/job-manager.test.ts +0 -869
- package/src/fleet-manager/__tests__/job-queue.test.ts +0 -401
- package/src/fleet-manager/__tests__/reload.test.ts +0 -751
- package/src/fleet-manager/__tests__/status-queries.test.ts +0 -595
- package/src/fleet-manager/__tests__/trigger.test.ts +0 -601
- package/src/fleet-manager/errors.ts +0 -747
- package/src/fleet-manager/event-types.ts +0 -378
- package/src/fleet-manager/fleet-manager.ts +0 -2315
- package/src/fleet-manager/index.ts +0 -128
- package/src/fleet-manager/job-manager.ts +0 -663
- package/src/fleet-manager/job-queue.ts +0 -798
- package/src/fleet-manager/types.ts +0 -839
- package/src/index.ts +0 -32
- package/src/runner/__tests__/errors.test.ts +0 -382
- package/src/runner/__tests__/job-executor.test.ts +0 -1708
- package/src/runner/__tests__/message-processor.test.ts +0 -960
- package/src/runner/__tests__/sdk-adapter.test.ts +0 -626
- package/src/runner/errors.ts +0 -307
- package/src/runner/index.ts +0 -57
- package/src/runner/job-executor.ts +0 -448
- package/src/runner/message-processor.ts +0 -355
- package/src/runner/sdk-adapter.ts +0 -191
- package/src/runner/types.ts +0 -158
- package/src/scheduler/__tests__/errors.test.ts +0 -159
- package/src/scheduler/__tests__/interval.test.ts +0 -515
- package/src/scheduler/__tests__/schedule-runner.test.ts +0 -798
- package/src/scheduler/__tests__/schedule-state.test.ts +0 -671
- package/src/scheduler/__tests__/scheduler.test.ts +0 -1280
- package/src/scheduler/errors.ts +0 -101
- package/src/scheduler/index.ts +0 -53
- package/src/scheduler/interval.ts +0 -189
- package/src/scheduler/schedule-runner.ts +0 -442
- package/src/scheduler/schedule-state.ts +0 -211
- package/src/scheduler/scheduler.ts +0 -570
- package/src/scheduler/types.ts +0 -216
- package/src/state/__tests__/directory.test.ts +0 -595
- package/src/state/__tests__/fleet-state.test.ts +0 -868
- package/src/state/__tests__/job-metadata-schema.test.ts +0 -414
- package/src/state/__tests__/job-metadata.test.ts +0 -831
- package/src/state/__tests__/job-output.test.ts +0 -856
- package/src/state/__tests__/session-schema.test.ts +0 -378
- package/src/state/__tests__/session.test.ts +0 -604
- package/src/state/directory.ts +0 -217
- package/src/state/errors.ts +0 -97
- package/src/state/fleet-state.ts +0 -284
- package/src/state/index.ts +0 -79
- package/src/state/job-metadata.ts +0 -445
- package/src/state/job-output.ts +0 -316
- package/src/state/schemas/__tests__/job-output.test.ts +0 -338
- package/src/state/schemas/fleet-state.ts +0 -120
- package/src/state/schemas/index.ts +0 -67
- package/src/state/schemas/job-metadata.ts +0 -181
- package/src/state/schemas/job-output.ts +0 -177
- package/src/state/schemas/session-info.ts +0 -92
- package/src/state/session.ts +0 -253
- package/src/state/types.ts +0 -59
- package/src/state/utils/__tests__/atomic.test.ts +0 -723
- package/src/state/utils/__tests__/reads.test.ts +0 -1071
- package/src/state/utils/atomic.ts +0 -221
- package/src/state/utils/index.ts +0 -6
- package/src/state/utils/reads.ts +0 -512
- package/src/work-sources/__tests__/github.test.ts +0 -1800
- package/src/work-sources/__tests__/manager.test.ts +0 -529
- package/src/work-sources/__tests__/registry.test.ts +0 -477
- package/src/work-sources/__tests__/types.test.ts +0 -479
- package/src/work-sources/adapters/github.ts +0 -1166
- package/src/work-sources/adapters/index.ts +0 -64
- package/src/work-sources/errors.ts +0 -71
- package/src/work-sources/index.ts +0 -148
- package/src/work-sources/manager.ts +0 -413
- package/src/work-sources/registry.ts +0 -178
- package/src/work-sources/types.ts +0 -161
- package/tsconfig.json +0 -9
- package/vitest.config.ts +0 -19
|
@@ -1,868 +0,0 @@
|
|
|
1
|
-
import { describe, it, expect, beforeEach, afterEach, vi } from "vitest";
|
|
2
|
-
import { mkdir, rm, realpath, writeFile, readFile } from "node:fs/promises";
|
|
3
|
-
import { join } from "node:path";
|
|
4
|
-
import { tmpdir } from "node:os";
|
|
5
|
-
import {
|
|
6
|
-
readFleetState,
|
|
7
|
-
writeFleetState,
|
|
8
|
-
updateAgentState,
|
|
9
|
-
initializeFleetState,
|
|
10
|
-
removeAgentState,
|
|
11
|
-
type StateLogger,
|
|
12
|
-
} from "../fleet-state.js";
|
|
13
|
-
import {
|
|
14
|
-
createInitialFleetState,
|
|
15
|
-
type FleetState,
|
|
16
|
-
type AgentState,
|
|
17
|
-
} from "../schemas/fleet-state.js";
|
|
18
|
-
import { StateFileError } from "../errors.js";
|
|
19
|
-
|
|
20
|
-
// Helper to create a temp directory
|
|
21
|
-
async function createTempDir(): Promise<string> {
|
|
22
|
-
const baseDir = join(
|
|
23
|
-
tmpdir(),
|
|
24
|
-
`herdctl-fleet-state-test-${Date.now()}-${Math.random().toString(36).slice(2)}`
|
|
25
|
-
);
|
|
26
|
-
await mkdir(baseDir, { recursive: true });
|
|
27
|
-
// Resolve to real path to handle macOS /var -> /private/var symlink
|
|
28
|
-
return await realpath(baseDir);
|
|
29
|
-
}
|
|
30
|
-
|
|
31
|
-
// Helper to create a mock logger
|
|
32
|
-
function createMockLogger(): StateLogger & { warnings: string[] } {
|
|
33
|
-
const warnings: string[] = [];
|
|
34
|
-
return {
|
|
35
|
-
warnings,
|
|
36
|
-
warn: (message: string) => warnings.push(message),
|
|
37
|
-
};
|
|
38
|
-
}
|
|
39
|
-
|
|
40
|
-
describe("readFleetState", () => {
|
|
41
|
-
let tempDir: string;
|
|
42
|
-
|
|
43
|
-
beforeEach(async () => {
|
|
44
|
-
tempDir = await createTempDir();
|
|
45
|
-
});
|
|
46
|
-
|
|
47
|
-
afterEach(async () => {
|
|
48
|
-
await rm(tempDir, { recursive: true, force: true });
|
|
49
|
-
});
|
|
50
|
-
|
|
51
|
-
describe("valid state files", () => {
|
|
52
|
-
it("reads and validates state.yaml with full fleet state", async () => {
|
|
53
|
-
const stateFile = join(tempDir, "state.yaml");
|
|
54
|
-
const stateContent = `
|
|
55
|
-
fleet:
|
|
56
|
-
started_at: "2024-01-15T10:30:00Z"
|
|
57
|
-
agents:
|
|
58
|
-
my-agent:
|
|
59
|
-
status: running
|
|
60
|
-
current_job: job-123
|
|
61
|
-
last_job: job-122
|
|
62
|
-
next_schedule: hourly
|
|
63
|
-
next_trigger_at: "2024-01-15T11:00:00Z"
|
|
64
|
-
container_id: abc123
|
|
65
|
-
other-agent:
|
|
66
|
-
status: idle
|
|
67
|
-
`;
|
|
68
|
-
await writeFile(stateFile, stateContent, "utf-8");
|
|
69
|
-
|
|
70
|
-
const state = await readFleetState(stateFile);
|
|
71
|
-
|
|
72
|
-
expect(state.fleet.started_at).toBe("2024-01-15T10:30:00Z");
|
|
73
|
-
expect(state.agents["my-agent"]).toEqual({
|
|
74
|
-
status: "running",
|
|
75
|
-
current_job: "job-123",
|
|
76
|
-
last_job: "job-122",
|
|
77
|
-
next_schedule: "hourly",
|
|
78
|
-
next_trigger_at: "2024-01-15T11:00:00Z",
|
|
79
|
-
container_id: "abc123",
|
|
80
|
-
});
|
|
81
|
-
expect(state.agents["other-agent"]).toEqual({
|
|
82
|
-
status: "idle",
|
|
83
|
-
});
|
|
84
|
-
});
|
|
85
|
-
|
|
86
|
-
it("reads state with agent error status", async () => {
|
|
87
|
-
const stateFile = join(tempDir, "state.yaml");
|
|
88
|
-
const stateContent = `
|
|
89
|
-
fleet:
|
|
90
|
-
started_at: "2024-01-15T10:30:00Z"
|
|
91
|
-
agents:
|
|
92
|
-
failed-agent:
|
|
93
|
-
status: error
|
|
94
|
-
last_job: job-100
|
|
95
|
-
error_message: "Container exited with code 1"
|
|
96
|
-
`;
|
|
97
|
-
await writeFile(stateFile, stateContent, "utf-8");
|
|
98
|
-
|
|
99
|
-
const state = await readFleetState(stateFile);
|
|
100
|
-
|
|
101
|
-
expect(state.agents["failed-agent"]).toEqual({
|
|
102
|
-
status: "error",
|
|
103
|
-
last_job: "job-100",
|
|
104
|
-
error_message: "Container exited with code 1",
|
|
105
|
-
});
|
|
106
|
-
});
|
|
107
|
-
|
|
108
|
-
it("applies default values for missing optional fields", async () => {
|
|
109
|
-
const stateFile = join(tempDir, "state.yaml");
|
|
110
|
-
const stateContent = `
|
|
111
|
-
agents:
|
|
112
|
-
minimal-agent:
|
|
113
|
-
status: idle
|
|
114
|
-
`;
|
|
115
|
-
await writeFile(stateFile, stateContent, "utf-8");
|
|
116
|
-
|
|
117
|
-
const state = await readFleetState(stateFile);
|
|
118
|
-
|
|
119
|
-
expect(state.fleet).toEqual({});
|
|
120
|
-
expect(state.agents["minimal-agent"]).toEqual({
|
|
121
|
-
status: "idle",
|
|
122
|
-
});
|
|
123
|
-
});
|
|
124
|
-
|
|
125
|
-
it("handles empty agents map", async () => {
|
|
126
|
-
const stateFile = join(tempDir, "state.yaml");
|
|
127
|
-
const stateContent = `
|
|
128
|
-
fleet:
|
|
129
|
-
started_at: "2024-01-15T10:30:00Z"
|
|
130
|
-
agents: {}
|
|
131
|
-
`;
|
|
132
|
-
await writeFile(stateFile, stateContent, "utf-8");
|
|
133
|
-
|
|
134
|
-
const state = await readFleetState(stateFile);
|
|
135
|
-
|
|
136
|
-
expect(state.fleet.started_at).toBe("2024-01-15T10:30:00Z");
|
|
137
|
-
expect(state.agents).toEqual({});
|
|
138
|
-
});
|
|
139
|
-
});
|
|
140
|
-
|
|
141
|
-
describe("missing file handling", () => {
|
|
142
|
-
it("returns default empty state when file does not exist", async () => {
|
|
143
|
-
const stateFile = join(tempDir, "nonexistent.yaml");
|
|
144
|
-
|
|
145
|
-
const state = await readFleetState(stateFile);
|
|
146
|
-
|
|
147
|
-
expect(state).toEqual(createInitialFleetState());
|
|
148
|
-
expect(state.fleet).toEqual({});
|
|
149
|
-
expect(state.agents).toEqual({});
|
|
150
|
-
});
|
|
151
|
-
|
|
152
|
-
it("does not log warning for missing file", async () => {
|
|
153
|
-
const stateFile = join(tempDir, "nonexistent.yaml");
|
|
154
|
-
const logger = createMockLogger();
|
|
155
|
-
|
|
156
|
-
await readFleetState(stateFile, { logger });
|
|
157
|
-
|
|
158
|
-
expect(logger.warnings).toHaveLength(0);
|
|
159
|
-
});
|
|
160
|
-
});
|
|
161
|
-
|
|
162
|
-
describe("empty file handling", () => {
|
|
163
|
-
it("returns default state for empty file", async () => {
|
|
164
|
-
const stateFile = join(tempDir, "empty.yaml");
|
|
165
|
-
await writeFile(stateFile, "", "utf-8");
|
|
166
|
-
|
|
167
|
-
const state = await readFleetState(stateFile);
|
|
168
|
-
|
|
169
|
-
expect(state).toEqual(createInitialFleetState());
|
|
170
|
-
});
|
|
171
|
-
|
|
172
|
-
it("returns default state for file with only whitespace", async () => {
|
|
173
|
-
const stateFile = join(tempDir, "whitespace.yaml");
|
|
174
|
-
await writeFile(stateFile, " \n \n ", "utf-8");
|
|
175
|
-
|
|
176
|
-
const state = await readFleetState(stateFile);
|
|
177
|
-
|
|
178
|
-
expect(state).toEqual(createInitialFleetState());
|
|
179
|
-
});
|
|
180
|
-
|
|
181
|
-
it("returns default state for file with only comments", async () => {
|
|
182
|
-
const stateFile = join(tempDir, "comments.yaml");
|
|
183
|
-
await writeFile(stateFile, "# This is a comment\n", "utf-8");
|
|
184
|
-
|
|
185
|
-
const state = await readFleetState(stateFile);
|
|
186
|
-
|
|
187
|
-
expect(state).toEqual(createInitialFleetState());
|
|
188
|
-
});
|
|
189
|
-
});
|
|
190
|
-
|
|
191
|
-
describe("corrupted file handling", () => {
|
|
192
|
-
it("returns default state and logs warning for invalid YAML syntax", async () => {
|
|
193
|
-
const stateFile = join(tempDir, "invalid-syntax.yaml");
|
|
194
|
-
await writeFile(stateFile, "fleet: [unclosed", "utf-8");
|
|
195
|
-
const logger = createMockLogger();
|
|
196
|
-
|
|
197
|
-
const state = await readFleetState(stateFile, { logger });
|
|
198
|
-
|
|
199
|
-
expect(state).toEqual(createInitialFleetState());
|
|
200
|
-
expect(logger.warnings).toHaveLength(1);
|
|
201
|
-
// YAML parse errors come through as read errors
|
|
202
|
-
expect(logger.warnings[0]).toContain("Using default state");
|
|
203
|
-
});
|
|
204
|
-
|
|
205
|
-
it("returns default state and logs warning for invalid status enum", async () => {
|
|
206
|
-
const stateFile = join(tempDir, "invalid-status.yaml");
|
|
207
|
-
const stateContent = `
|
|
208
|
-
agents:
|
|
209
|
-
bad-agent:
|
|
210
|
-
status: invalid_status
|
|
211
|
-
`;
|
|
212
|
-
await writeFile(stateFile, stateContent, "utf-8");
|
|
213
|
-
const logger = createMockLogger();
|
|
214
|
-
|
|
215
|
-
const state = await readFleetState(stateFile, { logger });
|
|
216
|
-
|
|
217
|
-
expect(state).toEqual(createInitialFleetState());
|
|
218
|
-
expect(logger.warnings).toHaveLength(1);
|
|
219
|
-
expect(logger.warnings[0]).toContain("Corrupted state file");
|
|
220
|
-
});
|
|
221
|
-
|
|
222
|
-
it("returns default state and logs warning for wrong type structure", async () => {
|
|
223
|
-
const stateFile = join(tempDir, "wrong-type.yaml");
|
|
224
|
-
const stateContent = `
|
|
225
|
-
agents: "not an object"
|
|
226
|
-
`;
|
|
227
|
-
await writeFile(stateFile, stateContent, "utf-8");
|
|
228
|
-
const logger = createMockLogger();
|
|
229
|
-
|
|
230
|
-
const state = await readFleetState(stateFile, { logger });
|
|
231
|
-
|
|
232
|
-
expect(state).toEqual(createInitialFleetState());
|
|
233
|
-
expect(logger.warnings).toHaveLength(1);
|
|
234
|
-
});
|
|
235
|
-
|
|
236
|
-
it("uses default console.warn when no logger provided", async () => {
|
|
237
|
-
const stateFile = join(tempDir, "invalid.yaml");
|
|
238
|
-
await writeFile(stateFile, "fleet: [unclosed", "utf-8");
|
|
239
|
-
const consoleSpy = vi.spyOn(console, "warn").mockImplementation(() => {});
|
|
240
|
-
|
|
241
|
-
const state = await readFleetState(stateFile);
|
|
242
|
-
|
|
243
|
-
expect(state).toEqual(createInitialFleetState());
|
|
244
|
-
expect(consoleSpy).toHaveBeenCalled();
|
|
245
|
-
consoleSpy.mockRestore();
|
|
246
|
-
});
|
|
247
|
-
});
|
|
248
|
-
|
|
249
|
-
describe("FleetState type validation", () => {
|
|
250
|
-
it("validates all AgentStatus enum values", async () => {
|
|
251
|
-
const stateFile = join(tempDir, "all-statuses.yaml");
|
|
252
|
-
const stateContent = `
|
|
253
|
-
agents:
|
|
254
|
-
idle-agent:
|
|
255
|
-
status: idle
|
|
256
|
-
running-agent:
|
|
257
|
-
status: running
|
|
258
|
-
error-agent:
|
|
259
|
-
status: error
|
|
260
|
-
`;
|
|
261
|
-
await writeFile(stateFile, stateContent, "utf-8");
|
|
262
|
-
|
|
263
|
-
const state = await readFleetState(stateFile);
|
|
264
|
-
|
|
265
|
-
expect(state.agents["idle-agent"].status).toBe("idle");
|
|
266
|
-
expect(state.agents["running-agent"].status).toBe("running");
|
|
267
|
-
expect(state.agents["error-agent"].status).toBe("error");
|
|
268
|
-
});
|
|
269
|
-
|
|
270
|
-
it("allows nullable fields to be null", async () => {
|
|
271
|
-
const stateFile = join(tempDir, "nullable.yaml");
|
|
272
|
-
const stateContent = `
|
|
273
|
-
agents:
|
|
274
|
-
agent-with-nulls:
|
|
275
|
-
status: idle
|
|
276
|
-
current_job: null
|
|
277
|
-
last_job: null
|
|
278
|
-
next_schedule: null
|
|
279
|
-
next_trigger_at: null
|
|
280
|
-
container_id: null
|
|
281
|
-
error_message: null
|
|
282
|
-
`;
|
|
283
|
-
await writeFile(stateFile, stateContent, "utf-8");
|
|
284
|
-
|
|
285
|
-
const state = await readFleetState(stateFile);
|
|
286
|
-
|
|
287
|
-
expect(state.agents["agent-with-nulls"].current_job).toBeNull();
|
|
288
|
-
expect(state.agents["agent-with-nulls"].last_job).toBeNull();
|
|
289
|
-
expect(state.agents["agent-with-nulls"].error_message).toBeNull();
|
|
290
|
-
});
|
|
291
|
-
});
|
|
292
|
-
});
|
|
293
|
-
|
|
294
|
-
describe("writeFleetState", () => {
|
|
295
|
-
let tempDir: string;
|
|
296
|
-
|
|
297
|
-
beforeEach(async () => {
|
|
298
|
-
tempDir = await createTempDir();
|
|
299
|
-
});
|
|
300
|
-
|
|
301
|
-
afterEach(async () => {
|
|
302
|
-
await rm(tempDir, { recursive: true, force: true });
|
|
303
|
-
});
|
|
304
|
-
|
|
305
|
-
describe("successful writes", () => {
|
|
306
|
-
it("writes valid fleet state to file", async () => {
|
|
307
|
-
const stateFile = join(tempDir, "state.yaml");
|
|
308
|
-
const state: FleetState = {
|
|
309
|
-
fleet: {
|
|
310
|
-
started_at: "2024-01-15T10:30:00Z",
|
|
311
|
-
},
|
|
312
|
-
agents: {
|
|
313
|
-
"my-agent": {
|
|
314
|
-
status: "running",
|
|
315
|
-
current_job: "job-123",
|
|
316
|
-
},
|
|
317
|
-
},
|
|
318
|
-
};
|
|
319
|
-
|
|
320
|
-
await writeFleetState(stateFile, state);
|
|
321
|
-
|
|
322
|
-
const content = await readFile(stateFile, "utf-8");
|
|
323
|
-
expect(content).toContain("started_at:");
|
|
324
|
-
expect(content).toContain("my-agent:");
|
|
325
|
-
expect(content).toContain("status: running");
|
|
326
|
-
expect(content).toContain("current_job: job-123");
|
|
327
|
-
});
|
|
328
|
-
|
|
329
|
-
it("writes empty state correctly", async () => {
|
|
330
|
-
const stateFile = join(tempDir, "empty-state.yaml");
|
|
331
|
-
const state = createInitialFleetState();
|
|
332
|
-
|
|
333
|
-
await writeFleetState(stateFile, state);
|
|
334
|
-
|
|
335
|
-
const readState = await readFleetState(stateFile);
|
|
336
|
-
expect(readState).toEqual(state);
|
|
337
|
-
});
|
|
338
|
-
|
|
339
|
-
it("overwrites existing file", async () => {
|
|
340
|
-
const stateFile = join(tempDir, "overwrite.yaml");
|
|
341
|
-
await writeFile(stateFile, "old: content\n", "utf-8");
|
|
342
|
-
const state: FleetState = {
|
|
343
|
-
fleet: { started_at: "2024-01-15T10:30:00Z" },
|
|
344
|
-
agents: {},
|
|
345
|
-
};
|
|
346
|
-
|
|
347
|
-
await writeFleetState(stateFile, state);
|
|
348
|
-
|
|
349
|
-
const content = await readFile(stateFile, "utf-8");
|
|
350
|
-
expect(content).not.toContain("old:");
|
|
351
|
-
expect(content).toContain("started_at:");
|
|
352
|
-
});
|
|
353
|
-
|
|
354
|
-
it("preserves custom indent option", async () => {
|
|
355
|
-
const stateFile = join(tempDir, "custom-indent.yaml");
|
|
356
|
-
const state: FleetState = {
|
|
357
|
-
fleet: {},
|
|
358
|
-
agents: {
|
|
359
|
-
agent: { status: "idle" },
|
|
360
|
-
},
|
|
361
|
-
};
|
|
362
|
-
|
|
363
|
-
await writeFleetState(stateFile, state, { indent: 4 });
|
|
364
|
-
|
|
365
|
-
const content = await readFile(stateFile, "utf-8");
|
|
366
|
-
// Check for 4-space indentation
|
|
367
|
-
expect(content).toMatch(/^\s{4}agent:/m);
|
|
368
|
-
});
|
|
369
|
-
|
|
370
|
-
it("round-trips complex state correctly", async () => {
|
|
371
|
-
const stateFile = join(tempDir, "roundtrip.yaml");
|
|
372
|
-
const originalState: FleetState = {
|
|
373
|
-
fleet: {
|
|
374
|
-
started_at: "2024-01-15T10:30:00Z",
|
|
375
|
-
},
|
|
376
|
-
agents: {
|
|
377
|
-
"agent-1": {
|
|
378
|
-
status: "running",
|
|
379
|
-
current_job: "job-123",
|
|
380
|
-
last_job: "job-122",
|
|
381
|
-
next_schedule: "hourly",
|
|
382
|
-
next_trigger_at: "2024-01-15T11:00:00Z",
|
|
383
|
-
container_id: "container-abc",
|
|
384
|
-
},
|
|
385
|
-
"agent-2": {
|
|
386
|
-
status: "error",
|
|
387
|
-
last_job: "job-456",
|
|
388
|
-
error_message: "Out of memory",
|
|
389
|
-
},
|
|
390
|
-
"agent-3": {
|
|
391
|
-
status: "idle",
|
|
392
|
-
},
|
|
393
|
-
},
|
|
394
|
-
};
|
|
395
|
-
|
|
396
|
-
await writeFleetState(stateFile, originalState);
|
|
397
|
-
const readState = await readFleetState(stateFile);
|
|
398
|
-
|
|
399
|
-
expect(readState.fleet.started_at).toBe(originalState.fleet.started_at);
|
|
400
|
-
expect(readState.agents["agent-1"]).toEqual(originalState.agents["agent-1"]);
|
|
401
|
-
expect(readState.agents["agent-2"]).toEqual(originalState.agents["agent-2"]);
|
|
402
|
-
expect(readState.agents["agent-3"]).toEqual(originalState.agents["agent-3"]);
|
|
403
|
-
});
|
|
404
|
-
});
|
|
405
|
-
|
|
406
|
-
describe("atomic write behavior", () => {
|
|
407
|
-
it("writes atomically (no partial writes)", async () => {
|
|
408
|
-
const stateFile = join(tempDir, "atomic.yaml");
|
|
409
|
-
const state: FleetState = {
|
|
410
|
-
fleet: { started_at: "2024-01-15T10:30:00Z" },
|
|
411
|
-
agents: {
|
|
412
|
-
agent: {
|
|
413
|
-
status: "running",
|
|
414
|
-
current_job: "job-1",
|
|
415
|
-
},
|
|
416
|
-
},
|
|
417
|
-
};
|
|
418
|
-
|
|
419
|
-
await writeFleetState(stateFile, state);
|
|
420
|
-
|
|
421
|
-
// File should be complete and valid
|
|
422
|
-
const readState = await readFleetState(stateFile);
|
|
423
|
-
expect(readState.fleet.started_at).toBe("2024-01-15T10:30:00Z");
|
|
424
|
-
});
|
|
425
|
-
|
|
426
|
-
it("does not leave temp files on success", async () => {
|
|
427
|
-
const stateFile = join(tempDir, "no-temp.yaml");
|
|
428
|
-
const state = createInitialFleetState();
|
|
429
|
-
|
|
430
|
-
await writeFleetState(stateFile, state);
|
|
431
|
-
|
|
432
|
-
// Check no temp files exist
|
|
433
|
-
const { readdir } = await import("node:fs/promises");
|
|
434
|
-
const files = await readdir(tempDir);
|
|
435
|
-
const tempFiles = files.filter((f) => f.includes(".tmp."));
|
|
436
|
-
expect(tempFiles).toHaveLength(0);
|
|
437
|
-
});
|
|
438
|
-
});
|
|
439
|
-
|
|
440
|
-
describe("error handling", () => {
|
|
441
|
-
it("throws StateFileError when write fails", async () => {
|
|
442
|
-
// Try to write to a non-existent directory
|
|
443
|
-
const stateFile = join(tempDir, "nonexistent-dir", "state.yaml");
|
|
444
|
-
const state = createInitialFleetState();
|
|
445
|
-
|
|
446
|
-
await expect(writeFleetState(stateFile, state)).rejects.toThrow(StateFileError);
|
|
447
|
-
});
|
|
448
|
-
|
|
449
|
-
it("validates state before writing", async () => {
|
|
450
|
-
const stateFile = join(tempDir, "validate.yaml");
|
|
451
|
-
const invalidState = {
|
|
452
|
-
fleet: {},
|
|
453
|
-
agents: {
|
|
454
|
-
agent: {
|
|
455
|
-
status: "invalid_status", // Invalid status
|
|
456
|
-
},
|
|
457
|
-
},
|
|
458
|
-
} as unknown as FleetState;
|
|
459
|
-
|
|
460
|
-
await expect(writeFleetState(stateFile, invalidState)).rejects.toThrow();
|
|
461
|
-
});
|
|
462
|
-
});
|
|
463
|
-
});
|
|
464
|
-
|
|
465
|
-
describe("updateAgentState", () => {
|
|
466
|
-
let tempDir: string;
|
|
467
|
-
|
|
468
|
-
beforeEach(async () => {
|
|
469
|
-
tempDir = await createTempDir();
|
|
470
|
-
});
|
|
471
|
-
|
|
472
|
-
afterEach(async () => {
|
|
473
|
-
await rm(tempDir, { recursive: true, force: true });
|
|
474
|
-
});
|
|
475
|
-
|
|
476
|
-
describe("updating existing agents", () => {
|
|
477
|
-
it("updates single field of existing agent", async () => {
|
|
478
|
-
const stateFile = join(tempDir, "state.yaml");
|
|
479
|
-
const initialState: FleetState = {
|
|
480
|
-
fleet: { started_at: "2024-01-15T10:30:00Z" },
|
|
481
|
-
agents: {
|
|
482
|
-
"my-agent": {
|
|
483
|
-
status: "idle",
|
|
484
|
-
last_job: "job-100",
|
|
485
|
-
},
|
|
486
|
-
},
|
|
487
|
-
};
|
|
488
|
-
await writeFleetState(stateFile, initialState);
|
|
489
|
-
|
|
490
|
-
const updatedState = await updateAgentState(stateFile, "my-agent", {
|
|
491
|
-
status: "running",
|
|
492
|
-
});
|
|
493
|
-
|
|
494
|
-
expect(updatedState.agents["my-agent"].status).toBe("running");
|
|
495
|
-
expect(updatedState.agents["my-agent"].last_job).toBe("job-100");
|
|
496
|
-
});
|
|
497
|
-
|
|
498
|
-
it("updates multiple fields of existing agent", async () => {
|
|
499
|
-
const stateFile = join(tempDir, "state.yaml");
|
|
500
|
-
const initialState: FleetState = {
|
|
501
|
-
fleet: {},
|
|
502
|
-
agents: {
|
|
503
|
-
"my-agent": {
|
|
504
|
-
status: "idle",
|
|
505
|
-
},
|
|
506
|
-
},
|
|
507
|
-
};
|
|
508
|
-
await writeFleetState(stateFile, initialState);
|
|
509
|
-
|
|
510
|
-
const updatedState = await updateAgentState(stateFile, "my-agent", {
|
|
511
|
-
status: "running",
|
|
512
|
-
current_job: "job-200",
|
|
513
|
-
container_id: "container-xyz",
|
|
514
|
-
});
|
|
515
|
-
|
|
516
|
-
expect(updatedState.agents["my-agent"]).toEqual({
|
|
517
|
-
status: "running",
|
|
518
|
-
current_job: "job-200",
|
|
519
|
-
container_id: "container-xyz",
|
|
520
|
-
});
|
|
521
|
-
});
|
|
522
|
-
|
|
523
|
-
it("can set fields to null", async () => {
|
|
524
|
-
const stateFile = join(tempDir, "state.yaml");
|
|
525
|
-
const initialState: FleetState = {
|
|
526
|
-
fleet: {},
|
|
527
|
-
agents: {
|
|
528
|
-
"my-agent": {
|
|
529
|
-
status: "error",
|
|
530
|
-
error_message: "Some error",
|
|
531
|
-
current_job: "job-100",
|
|
532
|
-
},
|
|
533
|
-
},
|
|
534
|
-
};
|
|
535
|
-
await writeFleetState(stateFile, initialState);
|
|
536
|
-
|
|
537
|
-
const updatedState = await updateAgentState(stateFile, "my-agent", {
|
|
538
|
-
status: "idle",
|
|
539
|
-
error_message: null,
|
|
540
|
-
current_job: null,
|
|
541
|
-
});
|
|
542
|
-
|
|
543
|
-
expect(updatedState.agents["my-agent"].status).toBe("idle");
|
|
544
|
-
expect(updatedState.agents["my-agent"].error_message).toBeNull();
|
|
545
|
-
expect(updatedState.agents["my-agent"].current_job).toBeNull();
|
|
546
|
-
});
|
|
547
|
-
|
|
548
|
-
it("preserves other agents when updating one", async () => {
|
|
549
|
-
const stateFile = join(tempDir, "state.yaml");
|
|
550
|
-
const initialState: FleetState = {
|
|
551
|
-
fleet: {},
|
|
552
|
-
agents: {
|
|
553
|
-
"agent-1": { status: "idle" },
|
|
554
|
-
"agent-2": { status: "running", current_job: "job-100" },
|
|
555
|
-
},
|
|
556
|
-
};
|
|
557
|
-
await writeFleetState(stateFile, initialState);
|
|
558
|
-
|
|
559
|
-
const updatedState = await updateAgentState(stateFile, "agent-1", {
|
|
560
|
-
status: "running",
|
|
561
|
-
});
|
|
562
|
-
|
|
563
|
-
expect(updatedState.agents["agent-1"].status).toBe("running");
|
|
564
|
-
expect(updatedState.agents["agent-2"]).toEqual({
|
|
565
|
-
status: "running",
|
|
566
|
-
current_job: "job-100",
|
|
567
|
-
});
|
|
568
|
-
});
|
|
569
|
-
|
|
570
|
-
it("preserves fleet metadata when updating agent", async () => {
|
|
571
|
-
const stateFile = join(tempDir, "state.yaml");
|
|
572
|
-
const initialState: FleetState = {
|
|
573
|
-
fleet: { started_at: "2024-01-15T10:30:00Z" },
|
|
574
|
-
agents: {
|
|
575
|
-
"my-agent": { status: "idle" },
|
|
576
|
-
},
|
|
577
|
-
};
|
|
578
|
-
await writeFleetState(stateFile, initialState);
|
|
579
|
-
|
|
580
|
-
const updatedState = await updateAgentState(stateFile, "my-agent", {
|
|
581
|
-
status: "running",
|
|
582
|
-
});
|
|
583
|
-
|
|
584
|
-
expect(updatedState.fleet.started_at).toBe("2024-01-15T10:30:00Z");
|
|
585
|
-
});
|
|
586
|
-
});
|
|
587
|
-
|
|
588
|
-
describe("creating new agents", () => {
|
|
589
|
-
it("creates new agent if it does not exist", async () => {
|
|
590
|
-
const stateFile = join(tempDir, "state.yaml");
|
|
591
|
-
const initialState: FleetState = {
|
|
592
|
-
fleet: {},
|
|
593
|
-
agents: {},
|
|
594
|
-
};
|
|
595
|
-
await writeFleetState(stateFile, initialState);
|
|
596
|
-
|
|
597
|
-
const updatedState = await updateAgentState(stateFile, "new-agent", {
|
|
598
|
-
status: "running",
|
|
599
|
-
current_job: "job-1",
|
|
600
|
-
});
|
|
601
|
-
|
|
602
|
-
expect(updatedState.agents["new-agent"]).toEqual({
|
|
603
|
-
status: "running",
|
|
604
|
-
current_job: "job-1",
|
|
605
|
-
});
|
|
606
|
-
});
|
|
607
|
-
|
|
608
|
-
it("creates new agent with default status if not provided", async () => {
|
|
609
|
-
const stateFile = join(tempDir, "state.yaml");
|
|
610
|
-
const initialState: FleetState = {
|
|
611
|
-
fleet: {},
|
|
612
|
-
agents: {},
|
|
613
|
-
};
|
|
614
|
-
await writeFleetState(stateFile, initialState);
|
|
615
|
-
|
|
616
|
-
const updatedState = await updateAgentState(stateFile, "new-agent", {
|
|
617
|
-
last_job: "job-1",
|
|
618
|
-
});
|
|
619
|
-
|
|
620
|
-
expect(updatedState.agents["new-agent"].status).toBe("idle");
|
|
621
|
-
expect(updatedState.agents["new-agent"].last_job).toBe("job-1");
|
|
622
|
-
});
|
|
623
|
-
|
|
624
|
-
it("creates agent in file that does not exist", async () => {
|
|
625
|
-
const stateFile = join(tempDir, "new-state.yaml");
|
|
626
|
-
|
|
627
|
-
const updatedState = await updateAgentState(stateFile, "new-agent", {
|
|
628
|
-
status: "running",
|
|
629
|
-
});
|
|
630
|
-
|
|
631
|
-
expect(updatedState.agents["new-agent"].status).toBe("running");
|
|
632
|
-
|
|
633
|
-
// Verify it was persisted
|
|
634
|
-
const readState = await readFleetState(stateFile);
|
|
635
|
-
expect(readState.agents["new-agent"].status).toBe("running");
|
|
636
|
-
});
|
|
637
|
-
});
|
|
638
|
-
|
|
639
|
-
describe("file operations", () => {
|
|
640
|
-
it("writes changes back to file", async () => {
|
|
641
|
-
const stateFile = join(tempDir, "state.yaml");
|
|
642
|
-
const initialState: FleetState = {
|
|
643
|
-
fleet: {},
|
|
644
|
-
agents: {
|
|
645
|
-
"my-agent": { status: "idle" },
|
|
646
|
-
},
|
|
647
|
-
};
|
|
648
|
-
await writeFleetState(stateFile, initialState);
|
|
649
|
-
|
|
650
|
-
await updateAgentState(stateFile, "my-agent", { status: "running" });
|
|
651
|
-
|
|
652
|
-
// Read from file directly to verify persistence
|
|
653
|
-
const persistedState = await readFleetState(stateFile);
|
|
654
|
-
expect(persistedState.agents["my-agent"].status).toBe("running");
|
|
655
|
-
});
|
|
656
|
-
|
|
657
|
-
it("handles corrupted file by starting fresh", async () => {
|
|
658
|
-
const stateFile = join(tempDir, "corrupted.yaml");
|
|
659
|
-
await writeFile(stateFile, "invalid: [yaml", "utf-8");
|
|
660
|
-
const logger = createMockLogger();
|
|
661
|
-
|
|
662
|
-
const updatedState = await updateAgentState(
|
|
663
|
-
stateFile,
|
|
664
|
-
"new-agent",
|
|
665
|
-
{ status: "running" },
|
|
666
|
-
{ logger }
|
|
667
|
-
);
|
|
668
|
-
|
|
669
|
-
expect(updatedState.agents["new-agent"].status).toBe("running");
|
|
670
|
-
expect(logger.warnings).toHaveLength(1);
|
|
671
|
-
});
|
|
672
|
-
});
|
|
673
|
-
});
|
|
674
|
-
|
|
675
|
-
describe("initializeFleetState", () => {
|
|
676
|
-
let tempDir: string;
|
|
677
|
-
|
|
678
|
-
beforeEach(async () => {
|
|
679
|
-
tempDir = await createTempDir();
|
|
680
|
-
});
|
|
681
|
-
|
|
682
|
-
afterEach(async () => {
|
|
683
|
-
await rm(tempDir, { recursive: true, force: true });
|
|
684
|
-
});
|
|
685
|
-
|
|
686
|
-
it("sets started_at if not already set", async () => {
|
|
687
|
-
const stateFile = join(tempDir, "state.yaml");
|
|
688
|
-
|
|
689
|
-
const state = await initializeFleetState(stateFile);
|
|
690
|
-
|
|
691
|
-
expect(state.fleet.started_at).toBeDefined();
|
|
692
|
-
expect(new Date(state.fleet.started_at!).getTime()).toBeGreaterThan(0);
|
|
693
|
-
});
|
|
694
|
-
|
|
695
|
-
it("does not overwrite existing started_at", async () => {
|
|
696
|
-
const stateFile = join(tempDir, "state.yaml");
|
|
697
|
-
const originalTimestamp = "2024-01-01T00:00:00Z";
|
|
698
|
-
const initialState: FleetState = {
|
|
699
|
-
fleet: { started_at: originalTimestamp },
|
|
700
|
-
agents: {},
|
|
701
|
-
};
|
|
702
|
-
await writeFleetState(stateFile, initialState);
|
|
703
|
-
|
|
704
|
-
const state = await initializeFleetState(stateFile);
|
|
705
|
-
|
|
706
|
-
expect(state.fleet.started_at).toBe(originalTimestamp);
|
|
707
|
-
});
|
|
708
|
-
|
|
709
|
-
it("creates file if it does not exist", async () => {
|
|
710
|
-
const stateFile = join(tempDir, "new-state.yaml");
|
|
711
|
-
|
|
712
|
-
const state = await initializeFleetState(stateFile);
|
|
713
|
-
|
|
714
|
-
expect(state.fleet.started_at).toBeDefined();
|
|
715
|
-
|
|
716
|
-
// Verify file was created
|
|
717
|
-
const persistedState = await readFleetState(stateFile);
|
|
718
|
-
expect(persistedState.fleet.started_at).toBe(state.fleet.started_at);
|
|
719
|
-
});
|
|
720
|
-
|
|
721
|
-
it("preserves existing agents", async () => {
|
|
722
|
-
const stateFile = join(tempDir, "state.yaml");
|
|
723
|
-
const initialState: FleetState = {
|
|
724
|
-
fleet: {},
|
|
725
|
-
agents: {
|
|
726
|
-
"existing-agent": { status: "idle" },
|
|
727
|
-
},
|
|
728
|
-
};
|
|
729
|
-
await writeFleetState(stateFile, initialState);
|
|
730
|
-
|
|
731
|
-
const state = await initializeFleetState(stateFile);
|
|
732
|
-
|
|
733
|
-
expect(state.agents["existing-agent"]).toEqual({ status: "idle" });
|
|
734
|
-
expect(state.fleet.started_at).toBeDefined();
|
|
735
|
-
});
|
|
736
|
-
});
|
|
737
|
-
|
|
738
|
-
describe("removeAgentState", () => {
|
|
739
|
-
let tempDir: string;
|
|
740
|
-
|
|
741
|
-
beforeEach(async () => {
|
|
742
|
-
tempDir = await createTempDir();
|
|
743
|
-
});
|
|
744
|
-
|
|
745
|
-
afterEach(async () => {
|
|
746
|
-
await rm(tempDir, { recursive: true, force: true });
|
|
747
|
-
});
|
|
748
|
-
|
|
749
|
-
it("removes specified agent from state", async () => {
|
|
750
|
-
const stateFile = join(tempDir, "state.yaml");
|
|
751
|
-
const initialState: FleetState = {
|
|
752
|
-
fleet: {},
|
|
753
|
-
agents: {
|
|
754
|
-
"agent-1": { status: "idle" },
|
|
755
|
-
"agent-2": { status: "running" },
|
|
756
|
-
},
|
|
757
|
-
};
|
|
758
|
-
await writeFleetState(stateFile, initialState);
|
|
759
|
-
|
|
760
|
-
const updatedState = await removeAgentState(stateFile, "agent-1");
|
|
761
|
-
|
|
762
|
-
expect(updatedState.agents["agent-1"]).toBeUndefined();
|
|
763
|
-
expect(updatedState.agents["agent-2"]).toEqual({ status: "running" });
|
|
764
|
-
});
|
|
765
|
-
|
|
766
|
-
it("persists removal to file", async () => {
|
|
767
|
-
const stateFile = join(tempDir, "state.yaml");
|
|
768
|
-
const initialState: FleetState = {
|
|
769
|
-
fleet: {},
|
|
770
|
-
agents: {
|
|
771
|
-
"agent-to-remove": { status: "idle" },
|
|
772
|
-
},
|
|
773
|
-
};
|
|
774
|
-
await writeFleetState(stateFile, initialState);
|
|
775
|
-
|
|
776
|
-
await removeAgentState(stateFile, "agent-to-remove");
|
|
777
|
-
|
|
778
|
-
const persistedState = await readFleetState(stateFile);
|
|
779
|
-
expect(persistedState.agents["agent-to-remove"]).toBeUndefined();
|
|
780
|
-
});
|
|
781
|
-
|
|
782
|
-
it("handles removal of non-existent agent gracefully", async () => {
|
|
783
|
-
const stateFile = join(tempDir, "state.yaml");
|
|
784
|
-
const initialState: FleetState = {
|
|
785
|
-
fleet: {},
|
|
786
|
-
agents: {
|
|
787
|
-
"existing-agent": { status: "idle" },
|
|
788
|
-
},
|
|
789
|
-
};
|
|
790
|
-
await writeFleetState(stateFile, initialState);
|
|
791
|
-
|
|
792
|
-
const updatedState = await removeAgentState(stateFile, "non-existent");
|
|
793
|
-
|
|
794
|
-
expect(updatedState.agents["existing-agent"]).toEqual({ status: "idle" });
|
|
795
|
-
expect(Object.keys(updatedState.agents)).toHaveLength(1);
|
|
796
|
-
});
|
|
797
|
-
|
|
798
|
-
it("preserves fleet metadata when removing agent", async () => {
|
|
799
|
-
const stateFile = join(tempDir, "state.yaml");
|
|
800
|
-
const initialState: FleetState = {
|
|
801
|
-
fleet: { started_at: "2024-01-15T10:30:00Z" },
|
|
802
|
-
agents: {
|
|
803
|
-
"agent-to-remove": { status: "idle" },
|
|
804
|
-
},
|
|
805
|
-
};
|
|
806
|
-
await writeFleetState(stateFile, initialState);
|
|
807
|
-
|
|
808
|
-
const updatedState = await removeAgentState(stateFile, "agent-to-remove");
|
|
809
|
-
|
|
810
|
-
expect(updatedState.fleet.started_at).toBe("2024-01-15T10:30:00Z");
|
|
811
|
-
});
|
|
812
|
-
});
|
|
813
|
-
|
|
814
|
-
describe("concurrent operations", () => {
|
|
815
|
-
let tempDir: string;
|
|
816
|
-
|
|
817
|
-
beforeEach(async () => {
|
|
818
|
-
tempDir = await createTempDir();
|
|
819
|
-
});
|
|
820
|
-
|
|
821
|
-
afterEach(async () => {
|
|
822
|
-
await rm(tempDir, { recursive: true, force: true });
|
|
823
|
-
});
|
|
824
|
-
|
|
825
|
-
it("handles multiple concurrent reads", async () => {
|
|
826
|
-
const stateFile = join(tempDir, "state.yaml");
|
|
827
|
-
const state: FleetState = {
|
|
828
|
-
fleet: { started_at: "2024-01-15T10:30:00Z" },
|
|
829
|
-
agents: {
|
|
830
|
-
agent: { status: "running" },
|
|
831
|
-
},
|
|
832
|
-
};
|
|
833
|
-
await writeFleetState(stateFile, state);
|
|
834
|
-
|
|
835
|
-
const reads = [];
|
|
836
|
-
for (let i = 0; i < 50; i++) {
|
|
837
|
-
reads.push(readFleetState(stateFile));
|
|
838
|
-
}
|
|
839
|
-
|
|
840
|
-
const results = await Promise.all(reads);
|
|
841
|
-
|
|
842
|
-
for (const result of results) {
|
|
843
|
-
expect(result.fleet.started_at).toBe("2024-01-15T10:30:00Z");
|
|
844
|
-
expect(result.agents.agent.status).toBe("running");
|
|
845
|
-
}
|
|
846
|
-
});
|
|
847
|
-
|
|
848
|
-
it("handles sequential updates correctly", async () => {
|
|
849
|
-
const stateFile = join(tempDir, "state.yaml");
|
|
850
|
-
await writeFleetState(stateFile, createInitialFleetState());
|
|
851
|
-
|
|
852
|
-
// Sequential updates (not concurrent to avoid race conditions)
|
|
853
|
-
for (let i = 0; i < 10; i++) {
|
|
854
|
-
await updateAgentState(stateFile, `agent-${i}`, {
|
|
855
|
-
status: "running",
|
|
856
|
-
current_job: `job-${i}`,
|
|
857
|
-
});
|
|
858
|
-
}
|
|
859
|
-
|
|
860
|
-
const finalState = await readFleetState(stateFile);
|
|
861
|
-
|
|
862
|
-
expect(Object.keys(finalState.agents)).toHaveLength(10);
|
|
863
|
-
for (let i = 0; i < 10; i++) {
|
|
864
|
-
expect(finalState.agents[`agent-${i}`].status).toBe("running");
|
|
865
|
-
expect(finalState.agents[`agent-${i}`].current_job).toBe(`job-${i}`);
|
|
866
|
-
}
|
|
867
|
-
});
|
|
868
|
-
});
|