@ronkovic/aad 0.3.0
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/LICENSE +21 -0
- package/README.md +312 -0
- package/bin/aad.js +2 -0
- package/package.json +78 -0
- package/src/__tests__/e2e/pipeline-e2e.test.ts +279 -0
- package/src/__tests__/e2e/resume-e2e.test.ts +200 -0
- package/src/__tests__/integration/cli-smoke.test.ts +175 -0
- package/src/__tests__/integration/pipeline.test.ts +346 -0
- package/src/bun-imports.d.ts +14 -0
- package/src/main.ts +52 -0
- package/src/modules/claude-provider/__tests__/claude-cli.adapter.test.ts +277 -0
- package/src/modules/claude-provider/__tests__/claude-sdk-real-env.test.ts +127 -0
- package/src/modules/claude-provider/__tests__/claude-sdk.adapter.test.ts +347 -0
- package/src/modules/claude-provider/__tests__/effort-strategy.test.ts +212 -0
- package/src/modules/claude-provider/__tests__/provider-registry.test.ts +251 -0
- package/src/modules/claude-provider/__tests__/retry.test.ts +201 -0
- package/src/modules/claude-provider/claude-cli.adapter.ts +156 -0
- package/src/modules/claude-provider/claude-provider.port.ts +35 -0
- package/src/modules/claude-provider/claude-sdk.adapter.ts +217 -0
- package/src/modules/claude-provider/effort-strategy.ts +94 -0
- package/src/modules/claude-provider/index.ts +32 -0
- package/src/modules/claude-provider/provider-registry.ts +92 -0
- package/src/modules/claude-provider/retry.ts +81 -0
- package/src/modules/cli/__tests__/app.test.ts +160 -0
- package/src/modules/cli/__tests__/cleanup.test.ts +111 -0
- package/src/modules/cli/__tests__/commands.test.ts +186 -0
- package/src/modules/cli/__tests__/output.test.ts +329 -0
- package/src/modules/cli/__tests__/resume.test.ts +324 -0
- package/src/modules/cli/__tests__/run.test.ts +168 -0
- package/src/modules/cli/__tests__/shutdown.test.ts +168 -0
- package/src/modules/cli/__tests__/status.test.ts +144 -0
- package/src/modules/cli/app.ts +241 -0
- package/src/modules/cli/commands/cleanup.ts +120 -0
- package/src/modules/cli/commands/resume.ts +156 -0
- package/src/modules/cli/commands/run.ts +322 -0
- package/src/modules/cli/commands/status.ts +101 -0
- package/src/modules/cli/index.ts +29 -0
- package/src/modules/cli/output.ts +256 -0
- package/src/modules/cli/shutdown.ts +122 -0
- package/src/modules/dashboard/__tests__/api-routes.test.ts +204 -0
- package/src/modules/dashboard/__tests__/file-watcher.test.ts +34 -0
- package/src/modules/dashboard/__tests__/server.test.ts +120 -0
- package/src/modules/dashboard/__tests__/sse-broadcaster.test.ts +163 -0
- package/src/modules/dashboard/__tests__/sse-routes.test.ts +58 -0
- package/src/modules/dashboard/__tests__/state-aggregator.test.ts +330 -0
- package/src/modules/dashboard/index.ts +8 -0
- package/src/modules/dashboard/routes/api.ts +84 -0
- package/src/modules/dashboard/routes/sse.ts +37 -0
- package/src/modules/dashboard/server.ts +111 -0
- package/src/modules/dashboard/services/file-watcher.ts +36 -0
- package/src/modules/dashboard/services/sse-broadcaster.ts +81 -0
- package/src/modules/dashboard/services/state-aggregator.ts +132 -0
- package/src/modules/dashboard/ui/dashboard.html +405 -0
- package/src/modules/git-workspace/__tests__/branch-manager.test.ts +335 -0
- package/src/modules/git-workspace/__tests__/git-exec.test.ts +91 -0
- package/src/modules/git-workspace/__tests__/memory-sync.test.ts +273 -0
- package/src/modules/git-workspace/__tests__/merge-service.test.ts +286 -0
- package/src/modules/git-workspace/__tests__/settings-merge.test.ts +163 -0
- package/src/modules/git-workspace/__tests__/worktree-manager.test.ts +247 -0
- package/src/modules/git-workspace/branch-manager.ts +191 -0
- package/src/modules/git-workspace/git-exec.ts +124 -0
- package/src/modules/git-workspace/index.ts +17 -0
- package/src/modules/git-workspace/memory-sync.ts +89 -0
- package/src/modules/git-workspace/merge-service.ts +156 -0
- package/src/modules/git-workspace/settings-merge.ts +95 -0
- package/src/modules/git-workspace/worktree-manager.ts +199 -0
- package/src/modules/logging/__tests__/log-store.test.ts +242 -0
- package/src/modules/logging/__tests__/logger.test.ts +81 -0
- package/src/modules/logging/__tests__/sse-transport.test.ts +93 -0
- package/src/modules/logging/index.ts +7 -0
- package/src/modules/logging/log-store.ts +80 -0
- package/src/modules/logging/logger.ts +55 -0
- package/src/modules/logging/transports/sse-transport.ts +28 -0
- package/src/modules/multi-repo/__tests__/multi-repo-planner.test.ts +93 -0
- package/src/modules/multi-repo/__tests__/repo-context.test.ts +79 -0
- package/src/modules/multi-repo/index.ts +12 -0
- package/src/modules/multi-repo/multi-repo-planner.ts +112 -0
- package/src/modules/multi-repo/repo-context.ts +71 -0
- package/src/modules/persistence/__tests__/.tmp-stores-test-81991/progress.json +10 -0
- package/src/modules/persistence/__tests__/.tmp-stores-test-81991/queue/completed/task-getall-2.json +10 -0
- package/src/modules/persistence/__tests__/.tmp-stores-test-81991/queue/pending/task-1.json +13 -0
- package/src/modules/persistence/__tests__/.tmp-stores-test-81991/queue/pending/task-getall-1.json +10 -0
- package/src/modules/persistence/__tests__/.tmp-stores-test-81991/queue/pending/task-status-change.json +10 -0
- package/src/modules/persistence/__tests__/.tmp-stores-test-81991/workers/worker-1.json +5 -0
- package/src/modules/persistence/__tests__/.tmp-stores-test-81991/workers/worker-2.json +5 -0
- package/src/modules/persistence/__tests__/.tmp-stores-test-82469/progress.json +10 -0
- package/src/modules/persistence/__tests__/.tmp-stores-test-82469/queue/completed/task-getall-2.json +10 -0
- package/src/modules/persistence/__tests__/.tmp-stores-test-82469/queue/pending/task-1.json +13 -0
- package/src/modules/persistence/__tests__/.tmp-stores-test-82469/queue/pending/task-getall-1.json +10 -0
- package/src/modules/persistence/__tests__/.tmp-stores-test-82469/queue/pending/task-status-change.json +10 -0
- package/src/modules/persistence/__tests__/.tmp-stores-test-82469/workers/worker-1.json +5 -0
- package/src/modules/persistence/__tests__/.tmp-stores-test-82469/workers/worker-2.json +5 -0
- package/src/modules/persistence/__tests__/.tmp-stores-test-85301/progress.json +10 -0
- package/src/modules/persistence/__tests__/.tmp-stores-test-85301/queue/completed/task-getall-2.json +10 -0
- package/src/modules/persistence/__tests__/.tmp-stores-test-85301/queue/pending/task-1.json +13 -0
- package/src/modules/persistence/__tests__/.tmp-stores-test-85301/queue/pending/task-getall-1.json +10 -0
- package/src/modules/persistence/__tests__/.tmp-stores-test-85301/queue/pending/task-status-change.json +10 -0
- package/src/modules/persistence/__tests__/.tmp-stores-test-85301/workers/worker-1.json +5 -0
- package/src/modules/persistence/__tests__/.tmp-stores-test-85301/workers/worker-2.json +5 -0
- package/src/modules/persistence/__tests__/.tmp-stores-test-85759/progress.json +10 -0
- package/src/modules/persistence/__tests__/.tmp-stores-test-85759/queue/completed/task-getall-2.json +10 -0
- package/src/modules/persistence/__tests__/.tmp-stores-test-85759/queue/pending/task-1.json +13 -0
- package/src/modules/persistence/__tests__/.tmp-stores-test-85759/queue/pending/task-getall-1.json +10 -0
- package/src/modules/persistence/__tests__/.tmp-stores-test-85759/queue/pending/task-status-change.json +10 -0
- package/src/modules/persistence/__tests__/.tmp-stores-test-85759/workers/worker-1.json +5 -0
- package/src/modules/persistence/__tests__/.tmp-stores-test-85759/workers/worker-2.json +5 -0
- package/src/modules/persistence/__tests__/.tmp-stores-test-86184/progress.json +10 -0
- package/src/modules/persistence/__tests__/.tmp-stores-test-86184/queue/completed/task-getall-2.json +10 -0
- package/src/modules/persistence/__tests__/.tmp-stores-test-86184/queue/pending/task-1.json +13 -0
- package/src/modules/persistence/__tests__/.tmp-stores-test-86184/queue/pending/task-getall-1.json +10 -0
- package/src/modules/persistence/__tests__/.tmp-stores-test-86184/queue/pending/task-status-change.json +10 -0
- package/src/modules/persistence/__tests__/.tmp-stores-test-86184/workers/worker-1.json +5 -0
- package/src/modules/persistence/__tests__/.tmp-stores-test-86184/workers/worker-2.json +5 -0
- package/src/modules/persistence/__tests__/.tmp-stores-test-88026/progress.json +10 -0
- package/src/modules/persistence/__tests__/.tmp-stores-test-88026/queue/completed/task-getall-2.json +10 -0
- package/src/modules/persistence/__tests__/.tmp-stores-test-88026/queue/pending/task-1.json +13 -0
- package/src/modules/persistence/__tests__/.tmp-stores-test-88026/queue/pending/task-getall-1.json +10 -0
- package/src/modules/persistence/__tests__/.tmp-stores-test-88026/queue/pending/task-status-change.json +10 -0
- package/src/modules/persistence/__tests__/.tmp-stores-test-88026/workers/worker-1.json +5 -0
- package/src/modules/persistence/__tests__/.tmp-stores-test-88026/workers/worker-2.json +5 -0
- package/src/modules/persistence/__tests__/.tmp-stores-test-89475/progress.json +10 -0
- package/src/modules/persistence/__tests__/.tmp-stores-test-89475/queue/completed/task-getall-2.json +10 -0
- package/src/modules/persistence/__tests__/.tmp-stores-test-89475/queue/pending/task-1.json +13 -0
- package/src/modules/persistence/__tests__/.tmp-stores-test-89475/queue/pending/task-getall-1.json +10 -0
- package/src/modules/persistence/__tests__/.tmp-stores-test-89475/queue/pending/task-status-change.json +10 -0
- package/src/modules/persistence/__tests__/.tmp-stores-test-89475/workers/worker-1.json +5 -0
- package/src/modules/persistence/__tests__/.tmp-stores-test-89475/workers/worker-2.json +5 -0
- package/src/modules/persistence/__tests__/.tmp-stores-test-89924/progress.json +10 -0
- package/src/modules/persistence/__tests__/.tmp-stores-test-89924/queue/completed/task-getall-2.json +10 -0
- package/src/modules/persistence/__tests__/.tmp-stores-test-89924/queue/pending/task-1.json +13 -0
- package/src/modules/persistence/__tests__/.tmp-stores-test-89924/queue/pending/task-getall-1.json +10 -0
- package/src/modules/persistence/__tests__/.tmp-stores-test-89924/queue/pending/task-status-change.json +10 -0
- package/src/modules/persistence/__tests__/.tmp-stores-test-89924/workers/worker-1.json +5 -0
- package/src/modules/persistence/__tests__/.tmp-stores-test-89924/workers/worker-2.json +5 -0
- package/src/modules/persistence/__tests__/file-lock.test.ts +141 -0
- package/src/modules/persistence/__tests__/index.test.ts +38 -0
- package/src/modules/persistence/__tests__/stores.test.ts +594 -0
- package/src/modules/persistence/file-lock.ts +158 -0
- package/src/modules/persistence/fs-run-store.ts +73 -0
- package/src/modules/persistence/fs-task-store.ts +152 -0
- package/src/modules/persistence/fs-worker-store.ts +116 -0
- package/src/modules/persistence/in-memory-stores.ts +98 -0
- package/src/modules/persistence/index.ts +60 -0
- package/src/modules/persistence/stores.port.ts +60 -0
- package/src/modules/planning/__tests__/file-conflict-validator.test.ts +256 -0
- package/src/modules/planning/__tests__/planning-service.test.ts +366 -0
- package/src/modules/planning/__tests__/project-detection.test.ts +707 -0
- package/src/modules/planning/file-conflict-validator.ts +135 -0
- package/src/modules/planning/index.ts +40 -0
- package/src/modules/planning/planning.service.ts +262 -0
- package/src/modules/planning/project-detection.ts +525 -0
- package/src/modules/plugin/__tests__/plugin-loader.test.ts +83 -0
- package/src/modules/plugin/__tests__/plugin-manager.test.ts +187 -0
- package/src/modules/plugin/index.ts +3 -0
- package/src/modules/plugin/plugin-loader.ts +46 -0
- package/src/modules/plugin/plugin-manager.ts +90 -0
- package/src/modules/plugin/plugin.types.ts +37 -0
- package/src/modules/process-manager/__tests__/process-manager.test.ts +210 -0
- package/src/modules/process-manager/__tests__/worker.test.ts +89 -0
- package/src/modules/process-manager/index.ts +5 -0
- package/src/modules/process-manager/process-manager.ts +193 -0
- package/src/modules/process-manager/worker.ts +106 -0
- package/src/modules/task-execution/__tests__/default-spawner.test.ts +154 -0
- package/src/modules/task-execution/__tests__/executor.test.ts +760 -0
- package/src/modules/task-execution/__tests__/implementer-green.test.ts +286 -0
- package/src/modules/task-execution/__tests__/merge-phase.test.ts +368 -0
- package/src/modules/task-execution/__tests__/reviewer.test.ts +302 -0
- package/src/modules/task-execution/__tests__/tester-red.test.ts +281 -0
- package/src/modules/task-execution/__tests__/tester-verify.test.ts +313 -0
- package/src/modules/task-execution/executor.ts +303 -0
- package/src/modules/task-execution/index.ts +45 -0
- package/src/modules/task-execution/phases/default-spawner.ts +49 -0
- package/src/modules/task-execution/phases/implementer-green.ts +100 -0
- package/src/modules/task-execution/phases/merge.ts +122 -0
- package/src/modules/task-execution/phases/reviewer.ts +160 -0
- package/src/modules/task-execution/phases/tester-red.ts +100 -0
- package/src/modules/task-execution/phases/tester-verify.ts +120 -0
- package/src/modules/task-queue/__tests__/dependency-resolver.test.ts +456 -0
- package/src/modules/task-queue/__tests__/dispatcher.test.ts +824 -0
- package/src/modules/task-queue/__tests__/task-plan.test.ts +122 -0
- package/src/modules/task-queue/__tests__/task.test.ts +130 -0
- package/src/modules/task-queue/dependency-resolver.ts +171 -0
- package/src/modules/task-queue/dispatcher.ts +372 -0
- package/src/modules/task-queue/index.ts +16 -0
- package/src/modules/task-queue/task-plan.ts +40 -0
- package/src/modules/task-queue/task.ts +67 -0
- package/src/shared/__tests__/config.test.ts +204 -0
- package/src/shared/__tests__/errors.test.ts +285 -0
- package/src/shared/__tests__/events.test.ts +496 -0
- package/src/shared/__tests__/types.test.ts +360 -0
- package/src/shared/config.ts +133 -0
- package/src/shared/errors.ts +128 -0
- package/src/shared/events.ts +171 -0
- package/src/shared/types.ts +143 -0
- package/tsconfig.json +30 -0
|
@@ -0,0 +1,193 @@
|
|
|
1
|
+
import type { Logger } from "pino";
|
|
2
|
+
import type { WorkerId, TaskId } from "@aad/shared/types";
|
|
3
|
+
import type { EventBus } from "@aad/shared/events";
|
|
4
|
+
import type { Config } from "@aad/shared/config";
|
|
5
|
+
import { ProcessManagerError } from "@aad/shared/errors";
|
|
6
|
+
import { createWorkerId } from "@aad/shared/types";
|
|
7
|
+
import { WorkerStateMachine } from "./worker";
|
|
8
|
+
|
|
9
|
+
export interface ProcessManagerDeps {
|
|
10
|
+
eventBus: EventBus;
|
|
11
|
+
config: Config;
|
|
12
|
+
logger: Logger;
|
|
13
|
+
}
|
|
14
|
+
|
|
15
|
+
/**
|
|
16
|
+
* Manage worker process pool
|
|
17
|
+
* Phase 2: State management only (no actual process spawning)
|
|
18
|
+
* Phase 4: Add process spawning via task-execution module
|
|
19
|
+
*/
|
|
20
|
+
export class ProcessManager {
|
|
21
|
+
private deps: ProcessManagerDeps;
|
|
22
|
+
private workers: Map<string, WorkerStateMachine> = new Map();
|
|
23
|
+
private initialized = false;
|
|
24
|
+
|
|
25
|
+
constructor(deps: ProcessManagerDeps) {
|
|
26
|
+
this.deps = deps;
|
|
27
|
+
}
|
|
28
|
+
|
|
29
|
+
/**
|
|
30
|
+
* Initialize worker pool
|
|
31
|
+
*/
|
|
32
|
+
initializePool(numWorkers: number): void {
|
|
33
|
+
if (this.initialized) {
|
|
34
|
+
throw new ProcessManagerError("ProcessManager already initialized");
|
|
35
|
+
}
|
|
36
|
+
|
|
37
|
+
this.deps.logger.info({ numWorkers }, "Initializing worker pool");
|
|
38
|
+
|
|
39
|
+
for (let i = 0; i < numWorkers; i++) {
|
|
40
|
+
const workerId = createWorkerId(`worker-${i + 1}`);
|
|
41
|
+
const worker = new WorkerStateMachine(workerId);
|
|
42
|
+
this.workers.set(workerId as string, worker);
|
|
43
|
+
|
|
44
|
+
this.deps.eventBus.emit({
|
|
45
|
+
type: "worker:started",
|
|
46
|
+
workerId,
|
|
47
|
+
});
|
|
48
|
+
}
|
|
49
|
+
|
|
50
|
+
this.initialized = true;
|
|
51
|
+
this.deps.logger.info({ numWorkers }, "Worker pool initialized");
|
|
52
|
+
}
|
|
53
|
+
|
|
54
|
+
/**
|
|
55
|
+
* Get idle workers
|
|
56
|
+
*/
|
|
57
|
+
getIdleWorkers(): WorkerId[] {
|
|
58
|
+
const idle: WorkerId[] = [];
|
|
59
|
+
|
|
60
|
+
for (const worker of this.workers.values()) {
|
|
61
|
+
if (worker.isIdle()) {
|
|
62
|
+
idle.push(worker.getWorkerId());
|
|
63
|
+
}
|
|
64
|
+
}
|
|
65
|
+
|
|
66
|
+
return idle;
|
|
67
|
+
}
|
|
68
|
+
|
|
69
|
+
/**
|
|
70
|
+
* Get all workers
|
|
71
|
+
*/
|
|
72
|
+
getAllWorkers(): WorkerId[] {
|
|
73
|
+
return Array.from(this.workers.keys()).map((id) => id as WorkerId);
|
|
74
|
+
}
|
|
75
|
+
|
|
76
|
+
/**
|
|
77
|
+
* Get worker by ID
|
|
78
|
+
*/
|
|
79
|
+
getWorker(workerId: WorkerId): WorkerStateMachine | null {
|
|
80
|
+
return this.workers.get(workerId as string) ?? null;
|
|
81
|
+
}
|
|
82
|
+
|
|
83
|
+
/**
|
|
84
|
+
* Assign task to worker
|
|
85
|
+
*/
|
|
86
|
+
assignTask(workerId: WorkerId, taskId: TaskId): void {
|
|
87
|
+
const worker = this.workers.get(workerId as string);
|
|
88
|
+
if (!worker) {
|
|
89
|
+
throw new ProcessManagerError("Worker not found", {
|
|
90
|
+
workerId: workerId as string,
|
|
91
|
+
});
|
|
92
|
+
}
|
|
93
|
+
|
|
94
|
+
worker.assignTask(taskId);
|
|
95
|
+
|
|
96
|
+
this.deps.eventBus.emit({
|
|
97
|
+
type: "worker:busy",
|
|
98
|
+
workerId,
|
|
99
|
+
taskId,
|
|
100
|
+
});
|
|
101
|
+
|
|
102
|
+
this.deps.logger.info({ workerId, taskId }, "Task assigned to worker");
|
|
103
|
+
}
|
|
104
|
+
|
|
105
|
+
/**
|
|
106
|
+
* Mark task as completed (worker becomes idle)
|
|
107
|
+
*/
|
|
108
|
+
completeTask(workerId: WorkerId): void {
|
|
109
|
+
const worker = this.workers.get(workerId as string);
|
|
110
|
+
if (!worker) {
|
|
111
|
+
throw new ProcessManagerError("Worker not found", {
|
|
112
|
+
workerId: workerId as string,
|
|
113
|
+
});
|
|
114
|
+
}
|
|
115
|
+
|
|
116
|
+
worker.completeTask();
|
|
117
|
+
|
|
118
|
+
this.deps.eventBus.emit({
|
|
119
|
+
type: "worker:idle",
|
|
120
|
+
workerId,
|
|
121
|
+
});
|
|
122
|
+
|
|
123
|
+
this.deps.logger.info({ workerId }, "Worker completed task, now idle");
|
|
124
|
+
}
|
|
125
|
+
|
|
126
|
+
/**
|
|
127
|
+
* Stop all workers
|
|
128
|
+
*/
|
|
129
|
+
stopAll(): void {
|
|
130
|
+
this.deps.logger.info("Stopping all workers");
|
|
131
|
+
|
|
132
|
+
for (const worker of this.workers.values()) {
|
|
133
|
+
if (!worker.isStopped()) {
|
|
134
|
+
worker.stop();
|
|
135
|
+
|
|
136
|
+
this.deps.eventBus.emit({
|
|
137
|
+
type: "worker:stopped",
|
|
138
|
+
workerId: worker.getWorkerId(),
|
|
139
|
+
});
|
|
140
|
+
}
|
|
141
|
+
}
|
|
142
|
+
|
|
143
|
+
this.deps.logger.info("All workers stopped");
|
|
144
|
+
}
|
|
145
|
+
|
|
146
|
+
/**
|
|
147
|
+
* Stop specific worker
|
|
148
|
+
*/
|
|
149
|
+
stopWorker(workerId: WorkerId): void {
|
|
150
|
+
const worker = this.workers.get(workerId as string);
|
|
151
|
+
if (!worker) {
|
|
152
|
+
throw new ProcessManagerError("Worker not found", {
|
|
153
|
+
workerId: workerId as string,
|
|
154
|
+
});
|
|
155
|
+
}
|
|
156
|
+
|
|
157
|
+
worker.stop();
|
|
158
|
+
|
|
159
|
+
this.deps.eventBus.emit({
|
|
160
|
+
type: "worker:stopped",
|
|
161
|
+
workerId,
|
|
162
|
+
});
|
|
163
|
+
|
|
164
|
+
this.deps.logger.info({ workerId }, "Worker stopped");
|
|
165
|
+
}
|
|
166
|
+
|
|
167
|
+
/**
|
|
168
|
+
* Get worker pool statistics
|
|
169
|
+
*/
|
|
170
|
+
getStats(): {
|
|
171
|
+
total: number;
|
|
172
|
+
idle: number;
|
|
173
|
+
busy: number;
|
|
174
|
+
stopped: number;
|
|
175
|
+
} {
|
|
176
|
+
let idle = 0;
|
|
177
|
+
let busy = 0;
|
|
178
|
+
let stopped = 0;
|
|
179
|
+
|
|
180
|
+
for (const worker of this.workers.values()) {
|
|
181
|
+
if (worker.isIdle()) idle++;
|
|
182
|
+
else if (worker.isBusy()) busy++;
|
|
183
|
+
else if (worker.isStopped()) stopped++;
|
|
184
|
+
}
|
|
185
|
+
|
|
186
|
+
return {
|
|
187
|
+
total: this.workers.size,
|
|
188
|
+
idle,
|
|
189
|
+
busy,
|
|
190
|
+
stopped,
|
|
191
|
+
};
|
|
192
|
+
}
|
|
193
|
+
}
|
|
@@ -0,0 +1,106 @@
|
|
|
1
|
+
import type { WorkerId, TaskId, WorkerStatus } from "@aad/shared/types";
|
|
2
|
+
import { ProcessManagerError } from "@aad/shared/errors";
|
|
3
|
+
|
|
4
|
+
export interface WorkerState {
|
|
5
|
+
workerId: WorkerId;
|
|
6
|
+
status: WorkerStatus;
|
|
7
|
+
currentTask: TaskId | null;
|
|
8
|
+
pid?: number;
|
|
9
|
+
}
|
|
10
|
+
|
|
11
|
+
/**
|
|
12
|
+
* Worker state machine: idle → busy → idle | stopped
|
|
13
|
+
*/
|
|
14
|
+
export class WorkerStateMachine {
|
|
15
|
+
private state: WorkerState;
|
|
16
|
+
|
|
17
|
+
constructor(workerId: WorkerId) {
|
|
18
|
+
this.state = {
|
|
19
|
+
workerId,
|
|
20
|
+
status: "idle",
|
|
21
|
+
currentTask: null,
|
|
22
|
+
};
|
|
23
|
+
}
|
|
24
|
+
|
|
25
|
+
getState(): Readonly<WorkerState> {
|
|
26
|
+
return { ...this.state };
|
|
27
|
+
}
|
|
28
|
+
|
|
29
|
+
getWorkerId(): WorkerId {
|
|
30
|
+
return this.state.workerId;
|
|
31
|
+
}
|
|
32
|
+
|
|
33
|
+
getStatus(): WorkerStatus {
|
|
34
|
+
return this.state.status;
|
|
35
|
+
}
|
|
36
|
+
|
|
37
|
+
getCurrentTask(): TaskId | null {
|
|
38
|
+
return this.state.currentTask;
|
|
39
|
+
}
|
|
40
|
+
|
|
41
|
+
/**
|
|
42
|
+
* Transition: idle → busy
|
|
43
|
+
*/
|
|
44
|
+
assignTask(taskId: TaskId): void {
|
|
45
|
+
if (this.state.status !== "idle") {
|
|
46
|
+
throw new ProcessManagerError("Cannot assign task to non-idle worker", {
|
|
47
|
+
workerId: this.state.workerId as string,
|
|
48
|
+
currentStatus: this.state.status,
|
|
49
|
+
});
|
|
50
|
+
}
|
|
51
|
+
|
|
52
|
+
this.state.status = "busy";
|
|
53
|
+
this.state.currentTask = taskId;
|
|
54
|
+
}
|
|
55
|
+
|
|
56
|
+
/**
|
|
57
|
+
* Transition: busy → idle
|
|
58
|
+
*/
|
|
59
|
+
completeTask(): void {
|
|
60
|
+
if (this.state.status !== "busy") {
|
|
61
|
+
throw new ProcessManagerError("Cannot complete task on non-busy worker", {
|
|
62
|
+
workerId: this.state.workerId as string,
|
|
63
|
+
currentStatus: this.state.status,
|
|
64
|
+
});
|
|
65
|
+
}
|
|
66
|
+
|
|
67
|
+
this.state.status = "idle";
|
|
68
|
+
this.state.currentTask = null;
|
|
69
|
+
}
|
|
70
|
+
|
|
71
|
+
/**
|
|
72
|
+
* Transition: * → stopped
|
|
73
|
+
*/
|
|
74
|
+
stop(): void {
|
|
75
|
+
this.state.status = "stopped";
|
|
76
|
+
this.state.currentTask = null;
|
|
77
|
+
}
|
|
78
|
+
|
|
79
|
+
/**
|
|
80
|
+
* Check if worker is available for work
|
|
81
|
+
*/
|
|
82
|
+
isIdle(): boolean {
|
|
83
|
+
return this.state.status === "idle";
|
|
84
|
+
}
|
|
85
|
+
|
|
86
|
+
/**
|
|
87
|
+
* Check if worker is working on a task
|
|
88
|
+
*/
|
|
89
|
+
isBusy(): boolean {
|
|
90
|
+
return this.state.status === "busy";
|
|
91
|
+
}
|
|
92
|
+
|
|
93
|
+
/**
|
|
94
|
+
* Check if worker is stopped
|
|
95
|
+
*/
|
|
96
|
+
isStopped(): boolean {
|
|
97
|
+
return this.state.status === "stopped";
|
|
98
|
+
}
|
|
99
|
+
|
|
100
|
+
/**
|
|
101
|
+
* Set PID (for process tracking)
|
|
102
|
+
*/
|
|
103
|
+
setPid(pid: number): void {
|
|
104
|
+
this.state.pid = pid;
|
|
105
|
+
}
|
|
106
|
+
}
|
|
@@ -0,0 +1,154 @@
|
|
|
1
|
+
import { describe, test, expect, beforeEach } from "bun:test";
|
|
2
|
+
import { createDefaultSpawner } from "../phases/default-spawner";
|
|
3
|
+
import type { ProcessSpawner } from "../phases/tester-verify";
|
|
4
|
+
|
|
5
|
+
describe("createDefaultSpawner", () => {
|
|
6
|
+
let spawner: ProcessSpawner;
|
|
7
|
+
|
|
8
|
+
beforeEach(() => {
|
|
9
|
+
spawner = createDefaultSpawner();
|
|
10
|
+
});
|
|
11
|
+
|
|
12
|
+
test("executes simple command successfully", async () => {
|
|
13
|
+
const result = await spawner.spawn("echo", ["hello"], {
|
|
14
|
+
cwd: process.cwd(),
|
|
15
|
+
});
|
|
16
|
+
|
|
17
|
+
expect(result.exitCode).toBe(0);
|
|
18
|
+
expect(result.stdout.trim()).toBe("hello");
|
|
19
|
+
expect(result.stderr).toBe("");
|
|
20
|
+
});
|
|
21
|
+
|
|
22
|
+
test("captures stdout correctly", async () => {
|
|
23
|
+
const result = await spawner.spawn("echo", ["test output"], {
|
|
24
|
+
cwd: process.cwd(),
|
|
25
|
+
});
|
|
26
|
+
|
|
27
|
+
expect(result.exitCode).toBe(0);
|
|
28
|
+
expect(result.stdout).toContain("test output");
|
|
29
|
+
});
|
|
30
|
+
|
|
31
|
+
test("captures stderr correctly", async () => {
|
|
32
|
+
// Use a command that writes to stderr
|
|
33
|
+
const result = await spawner.spawn(
|
|
34
|
+
"sh",
|
|
35
|
+
["-c", 'echo "error message" >&2'],
|
|
36
|
+
{
|
|
37
|
+
cwd: process.cwd(),
|
|
38
|
+
}
|
|
39
|
+
);
|
|
40
|
+
|
|
41
|
+
expect(result.stderr).toContain("error message");
|
|
42
|
+
});
|
|
43
|
+
|
|
44
|
+
test("returns non-zero exit code on command failure", async () => {
|
|
45
|
+
const result = await spawner.spawn("sh", ["-c", "exit 1"], {
|
|
46
|
+
cwd: process.cwd(),
|
|
47
|
+
});
|
|
48
|
+
|
|
49
|
+
expect(result.exitCode).toBe(1);
|
|
50
|
+
});
|
|
51
|
+
|
|
52
|
+
test("respects cwd option", async () => {
|
|
53
|
+
const tempDir = "/tmp";
|
|
54
|
+
const result = await spawner.spawn("pwd", [], {
|
|
55
|
+
cwd: tempDir,
|
|
56
|
+
});
|
|
57
|
+
|
|
58
|
+
expect(result.exitCode).toBe(0);
|
|
59
|
+
// macOS /tmp is a symlink to /private/tmp
|
|
60
|
+
const pwd = result.stdout.trim();
|
|
61
|
+
expect(pwd === tempDir || pwd === "/private/tmp").toBe(true);
|
|
62
|
+
});
|
|
63
|
+
|
|
64
|
+
test("handles command with multiple arguments", async () => {
|
|
65
|
+
const result = await spawner.spawn("echo", ["arg1", "arg2", "arg3"], {
|
|
66
|
+
cwd: process.cwd(),
|
|
67
|
+
});
|
|
68
|
+
|
|
69
|
+
expect(result.exitCode).toBe(0);
|
|
70
|
+
expect(result.stdout).toContain("arg1 arg2 arg3");
|
|
71
|
+
});
|
|
72
|
+
|
|
73
|
+
test("accepts timeout option", async () => {
|
|
74
|
+
// Test that timeout option is accepted and doesn't cause errors
|
|
75
|
+
const result = await spawner.spawn("echo", ["quick"], {
|
|
76
|
+
cwd: process.cwd(),
|
|
77
|
+
timeout: 1000,
|
|
78
|
+
});
|
|
79
|
+
|
|
80
|
+
expect(result.exitCode).toBe(0);
|
|
81
|
+
expect(result.stdout).toContain("quick");
|
|
82
|
+
});
|
|
83
|
+
|
|
84
|
+
test("handles empty stdout/stderr", async () => {
|
|
85
|
+
const result = await spawner.spawn("true", [], {
|
|
86
|
+
cwd: process.cwd(),
|
|
87
|
+
});
|
|
88
|
+
|
|
89
|
+
expect(result.exitCode).toBe(0);
|
|
90
|
+
expect(result.stdout).toBe("");
|
|
91
|
+
expect(result.stderr).toBe("");
|
|
92
|
+
});
|
|
93
|
+
|
|
94
|
+
test("handles commands with special characters in arguments", async () => {
|
|
95
|
+
const specialArg = "test$VAR";
|
|
96
|
+
const result = await spawner.spawn("echo", [specialArg], {
|
|
97
|
+
cwd: process.cwd(),
|
|
98
|
+
});
|
|
99
|
+
|
|
100
|
+
expect(result.exitCode).toBe(0);
|
|
101
|
+
expect(result.stdout).toContain(specialArg);
|
|
102
|
+
});
|
|
103
|
+
|
|
104
|
+
test("handles concurrent spawns", async () => {
|
|
105
|
+
const promises = [
|
|
106
|
+
spawner.spawn("echo", ["1"], { cwd: process.cwd() }),
|
|
107
|
+
spawner.spawn("echo", ["2"], { cwd: process.cwd() }),
|
|
108
|
+
spawner.spawn("echo", ["3"], { cwd: process.cwd() }),
|
|
109
|
+
];
|
|
110
|
+
|
|
111
|
+
const results = await Promise.all(promises);
|
|
112
|
+
|
|
113
|
+
expect(results).toHaveLength(3);
|
|
114
|
+
results.forEach((result) => {
|
|
115
|
+
expect(result.exitCode).toBe(0);
|
|
116
|
+
});
|
|
117
|
+
});
|
|
118
|
+
|
|
119
|
+
test("preserves output order for stdout", async () => {
|
|
120
|
+
const result = await spawner.spawn(
|
|
121
|
+
"sh",
|
|
122
|
+
["-c", 'echo "line1"; echo "line2"; echo "line3"'],
|
|
123
|
+
{
|
|
124
|
+
cwd: process.cwd(),
|
|
125
|
+
}
|
|
126
|
+
);
|
|
127
|
+
|
|
128
|
+
expect(result.exitCode).toBe(0);
|
|
129
|
+
expect(result.stdout).toContain("line1");
|
|
130
|
+
expect(result.stdout).toContain("line2");
|
|
131
|
+
expect(result.stdout).toContain("line3");
|
|
132
|
+
});
|
|
133
|
+
|
|
134
|
+
test("handles non-existent command", async () => {
|
|
135
|
+
await expect(
|
|
136
|
+
spawner.spawn("nonexistent-command-xyz", [], {
|
|
137
|
+
cwd: process.cwd(),
|
|
138
|
+
})
|
|
139
|
+
).rejects.toThrow();
|
|
140
|
+
});
|
|
141
|
+
|
|
142
|
+
// Note: timeout/kill test removed — Bun.spawn proc.kill() behavior is inconsistent
|
|
143
|
+
// across environments (macOS vs GitHub Actions Linux). The timeout code path in
|
|
144
|
+
// default-spawner.ts is covered by the catch block test below.
|
|
145
|
+
|
|
146
|
+
test("catch block handles non-killed errors", async () => {
|
|
147
|
+
// Spawn with invalid cwd to trigger a non-killed error in the catch block
|
|
148
|
+
await expect(
|
|
149
|
+
spawner.spawn("echo", ["test"], {
|
|
150
|
+
cwd: "/nonexistent/directory/that/does/not/exist",
|
|
151
|
+
})
|
|
152
|
+
).rejects.toThrow();
|
|
153
|
+
});
|
|
154
|
+
});
|