@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,187 @@
|
|
|
1
|
+
import { describe, test, expect, mock } from "bun:test";
|
|
2
|
+
import { PluginManager } from "../plugin-manager";
|
|
3
|
+
import type { AADPlugin, PluginContext, HookHandler } from "../plugin.types";
|
|
4
|
+
import { EventBus } from "../../../shared/events";
|
|
5
|
+
import { loadConfig } from "../../../shared/config";
|
|
6
|
+
import pino from "pino";
|
|
7
|
+
|
|
8
|
+
function createTestContext(): PluginContext {
|
|
9
|
+
return {
|
|
10
|
+
config: loadConfig({}),
|
|
11
|
+
eventBus: new EventBus(),
|
|
12
|
+
logger: pino({ level: "silent" }),
|
|
13
|
+
};
|
|
14
|
+
}
|
|
15
|
+
|
|
16
|
+
function createTestPlugin(overrides: Partial<AADPlugin> = {}): AADPlugin {
|
|
17
|
+
return {
|
|
18
|
+
name: "test-plugin",
|
|
19
|
+
version: "1.0.0",
|
|
20
|
+
activate: mock(() => {}),
|
|
21
|
+
deactivate: mock(() => {}),
|
|
22
|
+
...overrides,
|
|
23
|
+
};
|
|
24
|
+
}
|
|
25
|
+
|
|
26
|
+
describe("PluginManager", () => {
|
|
27
|
+
test("register activates plugin and adds to list", async () => {
|
|
28
|
+
const ctx = createTestContext();
|
|
29
|
+
const manager = new PluginManager(ctx);
|
|
30
|
+
const plugin = createTestPlugin();
|
|
31
|
+
|
|
32
|
+
await manager.register(plugin);
|
|
33
|
+
|
|
34
|
+
expect(plugin.activate).toHaveBeenCalledWith(ctx);
|
|
35
|
+
expect(manager.list()).toEqual([{ name: "test-plugin", version: "1.0.0" }]);
|
|
36
|
+
});
|
|
37
|
+
|
|
38
|
+
test("register skips duplicate plugin names", async () => {
|
|
39
|
+
const ctx = createTestContext();
|
|
40
|
+
const manager = new PluginManager(ctx);
|
|
41
|
+
const plugin1 = createTestPlugin();
|
|
42
|
+
const plugin2 = createTestPlugin({ activate: mock(() => {}) });
|
|
43
|
+
|
|
44
|
+
await manager.register(plugin1);
|
|
45
|
+
await manager.register(plugin2);
|
|
46
|
+
|
|
47
|
+
expect(plugin1.activate).toHaveBeenCalledTimes(1);
|
|
48
|
+
expect(plugin2.activate).not.toHaveBeenCalled();
|
|
49
|
+
expect(manager.list()).toHaveLength(1);
|
|
50
|
+
});
|
|
51
|
+
|
|
52
|
+
test("register throws on activation failure", async () => {
|
|
53
|
+
const ctx = createTestContext();
|
|
54
|
+
const manager = new PluginManager(ctx);
|
|
55
|
+
const plugin = createTestPlugin({
|
|
56
|
+
activate: () => { throw new Error("activation failed"); },
|
|
57
|
+
});
|
|
58
|
+
|
|
59
|
+
await expect(manager.register(plugin)).rejects.toThrow("activation failed");
|
|
60
|
+
expect(manager.list()).toHaveLength(0);
|
|
61
|
+
});
|
|
62
|
+
|
|
63
|
+
test("deactivateAll calls deactivate on all plugins", async () => {
|
|
64
|
+
const ctx = createTestContext();
|
|
65
|
+
const manager = new PluginManager(ctx);
|
|
66
|
+
const p1 = createTestPlugin({ name: "p1" });
|
|
67
|
+
const p2 = createTestPlugin({ name: "p2" });
|
|
68
|
+
|
|
69
|
+
await manager.register(p1);
|
|
70
|
+
await manager.register(p2);
|
|
71
|
+
await manager.deactivateAll();
|
|
72
|
+
|
|
73
|
+
expect(p1.deactivate).toHaveBeenCalledTimes(1);
|
|
74
|
+
expect(p2.deactivate).toHaveBeenCalledTimes(1);
|
|
75
|
+
expect(manager.list()).toHaveLength(0);
|
|
76
|
+
});
|
|
77
|
+
|
|
78
|
+
test("deactivateAll handles plugin without deactivate", async () => {
|
|
79
|
+
const ctx = createTestContext();
|
|
80
|
+
const manager = new PluginManager(ctx);
|
|
81
|
+
const plugin = createTestPlugin({ deactivate: undefined });
|
|
82
|
+
|
|
83
|
+
await manager.register(plugin);
|
|
84
|
+
await manager.deactivateAll(); // should not throw
|
|
85
|
+
expect(manager.list()).toHaveLength(0);
|
|
86
|
+
});
|
|
87
|
+
|
|
88
|
+
test("deactivateAll continues if one plugin deactivation fails", async () => {
|
|
89
|
+
const ctx = createTestContext();
|
|
90
|
+
const manager = new PluginManager(ctx);
|
|
91
|
+
const p1 = createTestPlugin({
|
|
92
|
+
name: "p1",
|
|
93
|
+
deactivate: () => { throw new Error("cleanup failed"); },
|
|
94
|
+
});
|
|
95
|
+
const p2 = createTestPlugin({ name: "p2" });
|
|
96
|
+
|
|
97
|
+
await manager.register(p1);
|
|
98
|
+
await manager.register(p2);
|
|
99
|
+
await manager.deactivateAll();
|
|
100
|
+
|
|
101
|
+
expect(p2.deactivate).toHaveBeenCalledTimes(1);
|
|
102
|
+
});
|
|
103
|
+
|
|
104
|
+
test("addHook and runHook execute in registration order (waterfall)", async () => {
|
|
105
|
+
const ctx = createTestContext();
|
|
106
|
+
const manager = new PluginManager(ctx);
|
|
107
|
+
|
|
108
|
+
manager.addHook("before:planning", ((data: unknown) => {
|
|
109
|
+
return { ...(data as Record<string, unknown>), step1: true };
|
|
110
|
+
}) as HookHandler);
|
|
111
|
+
manager.addHook("before:planning", ((data: unknown) => {
|
|
112
|
+
return { ...(data as Record<string, unknown>), step2: true };
|
|
113
|
+
}) as HookHandler);
|
|
114
|
+
|
|
115
|
+
const result = await manager.runHook("before:planning", { initial: true } as Record<string, unknown>);
|
|
116
|
+
|
|
117
|
+
expect(result).toEqual({ initial: true, step1: true, step2: true });
|
|
118
|
+
});
|
|
119
|
+
|
|
120
|
+
test("runHook returns original data if handler returns void", async () => {
|
|
121
|
+
const ctx = createTestContext();
|
|
122
|
+
const manager = new PluginManager(ctx);
|
|
123
|
+
|
|
124
|
+
manager.addHook("before:execution", (() => {
|
|
125
|
+
// side effect only, no return
|
|
126
|
+
}) as HookHandler);
|
|
127
|
+
|
|
128
|
+
const result = await manager.runHook("before:execution", { value: 42 });
|
|
129
|
+
expect(result).toEqual({ value: 42 });
|
|
130
|
+
});
|
|
131
|
+
|
|
132
|
+
test("runHook returns data unchanged when no hooks registered", async () => {
|
|
133
|
+
const ctx = createTestContext();
|
|
134
|
+
const manager = new PluginManager(ctx);
|
|
135
|
+
|
|
136
|
+
const data = { test: true };
|
|
137
|
+
const result = await manager.runHook("on:error", data);
|
|
138
|
+
expect(result).toBe(data);
|
|
139
|
+
});
|
|
140
|
+
|
|
141
|
+
test("runHook continues on handler error", async () => {
|
|
142
|
+
const ctx = createTestContext();
|
|
143
|
+
const manager = new PluginManager(ctx);
|
|
144
|
+
|
|
145
|
+
manager.addHook("before:planning", (() => {
|
|
146
|
+
throw new Error("hook error");
|
|
147
|
+
}) as HookHandler);
|
|
148
|
+
manager.addHook("before:planning", ((data: unknown) => {
|
|
149
|
+
return { ...(data as Record<string, unknown>), recovered: true };
|
|
150
|
+
}) as HookHandler);
|
|
151
|
+
|
|
152
|
+
const result = await manager.runHook("before:planning", { initial: true } as Record<string, unknown>);
|
|
153
|
+
expect(result).toEqual({ initial: true, recovered: true });
|
|
154
|
+
});
|
|
155
|
+
|
|
156
|
+
test("runHook supports async handlers", async () => {
|
|
157
|
+
const ctx = createTestContext();
|
|
158
|
+
const manager = new PluginManager(ctx);
|
|
159
|
+
|
|
160
|
+
manager.addHook("after:execution", (async (data: unknown) => {
|
|
161
|
+
await new Promise((r) => setTimeout(r, 1));
|
|
162
|
+
return { ...(data as Record<string, unknown>), async: true };
|
|
163
|
+
}) as HookHandler);
|
|
164
|
+
|
|
165
|
+
const result = await manager.runHook("after:execution", { sync: true } as Record<string, unknown>);
|
|
166
|
+
expect(result).toEqual({ sync: true, async: true });
|
|
167
|
+
});
|
|
168
|
+
|
|
169
|
+
test("list returns empty array when no plugins registered", () => {
|
|
170
|
+
const ctx = createTestContext();
|
|
171
|
+
const manager = new PluginManager(ctx);
|
|
172
|
+
expect(manager.list()).toEqual([]);
|
|
173
|
+
});
|
|
174
|
+
|
|
175
|
+
test("list returns all registered plugins", async () => {
|
|
176
|
+
const ctx = createTestContext();
|
|
177
|
+
const manager = new PluginManager(ctx);
|
|
178
|
+
|
|
179
|
+
await manager.register(createTestPlugin({ name: "alpha", version: "1.0.0" }));
|
|
180
|
+
await manager.register(createTestPlugin({ name: "beta", version: "2.0.0" }));
|
|
181
|
+
|
|
182
|
+
expect(manager.list()).toEqual([
|
|
183
|
+
{ name: "alpha", version: "1.0.0" },
|
|
184
|
+
{ name: "beta", version: "2.0.0" },
|
|
185
|
+
]);
|
|
186
|
+
});
|
|
187
|
+
});
|
|
@@ -0,0 +1,46 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Plugin Loader
|
|
3
|
+
* Dynamic loading and validation of AAD plugins
|
|
4
|
+
*/
|
|
5
|
+
|
|
6
|
+
import { z } from "zod";
|
|
7
|
+
import { resolve, isAbsolute } from "node:path";
|
|
8
|
+
import type { AADPlugin } from "./plugin.types";
|
|
9
|
+
|
|
10
|
+
const pluginSchema = z.object({
|
|
11
|
+
name: z.string().min(1),
|
|
12
|
+
version: z.string().min(1),
|
|
13
|
+
activate: z.function(),
|
|
14
|
+
deactivate: z.function().optional(),
|
|
15
|
+
});
|
|
16
|
+
|
|
17
|
+
/** Load a plugin from a file path or module name */
|
|
18
|
+
export async function loadPlugin(pathOrModule: string): Promise<AADPlugin> {
|
|
19
|
+
let modulePath: string;
|
|
20
|
+
|
|
21
|
+
if (isAbsolute(pathOrModule)) {
|
|
22
|
+
modulePath = pathOrModule;
|
|
23
|
+
} else if (pathOrModule.startsWith("./") || pathOrModule.startsWith("../")) {
|
|
24
|
+
modulePath = resolve(process.cwd(), pathOrModule);
|
|
25
|
+
} else {
|
|
26
|
+
// npm package name
|
|
27
|
+
modulePath = pathOrModule;
|
|
28
|
+
}
|
|
29
|
+
|
|
30
|
+
let mod: unknown;
|
|
31
|
+
try {
|
|
32
|
+
mod = await import(modulePath);
|
|
33
|
+
} catch (error) {
|
|
34
|
+
throw new Error(`Failed to load plugin from "${pathOrModule}": ${error instanceof Error ? error.message : String(error)}`);
|
|
35
|
+
}
|
|
36
|
+
|
|
37
|
+
// Support both default export and named 'plugin' export
|
|
38
|
+
const raw = (mod as Record<string, unknown>)?.default ?? (mod as Record<string, unknown>)?.plugin ?? mod;
|
|
39
|
+
|
|
40
|
+
const result = pluginSchema.safeParse(raw);
|
|
41
|
+
if (!result.success) {
|
|
42
|
+
throw new Error(`Invalid plugin from "${pathOrModule}": ${result.error.message}`);
|
|
43
|
+
}
|
|
44
|
+
|
|
45
|
+
return raw as AADPlugin;
|
|
46
|
+
}
|
|
@@ -0,0 +1,90 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* PluginManager
|
|
3
|
+
* Manages plugin lifecycle and hook execution
|
|
4
|
+
*/
|
|
5
|
+
|
|
6
|
+
import type { AADPlugin, PluginContext, HookPoint, HookHandler } from "./plugin.types";
|
|
7
|
+
|
|
8
|
+
export class PluginManager {
|
|
9
|
+
private plugins: Map<string, AADPlugin> = new Map();
|
|
10
|
+
private hooks: Map<HookPoint, HookHandler[]> = new Map();
|
|
11
|
+
|
|
12
|
+
constructor(private ctx: PluginContext) {}
|
|
13
|
+
|
|
14
|
+
/** Register a plugin */
|
|
15
|
+
async register(plugin: AADPlugin): Promise<void> {
|
|
16
|
+
if (this.plugins.has(plugin.name)) {
|
|
17
|
+
this.ctx.logger.warn({ plugin: plugin.name }, "Plugin already registered, skipping");
|
|
18
|
+
return;
|
|
19
|
+
}
|
|
20
|
+
|
|
21
|
+
try {
|
|
22
|
+
await plugin.activate(this.ctx);
|
|
23
|
+
this.plugins.set(plugin.name, plugin);
|
|
24
|
+
this.ctx.logger.info({ plugin: plugin.name, version: plugin.version }, "Plugin registered");
|
|
25
|
+
} catch (error) {
|
|
26
|
+
this.ctx.logger.error({ plugin: plugin.name, error }, "Plugin activation failed");
|
|
27
|
+
throw error;
|
|
28
|
+
}
|
|
29
|
+
}
|
|
30
|
+
|
|
31
|
+
/** Load plugins from config (file paths or package names) */
|
|
32
|
+
async loadFromConfig(pluginPaths: string[]): Promise<void> {
|
|
33
|
+
const { loadPlugin } = await import("./plugin-loader");
|
|
34
|
+
for (const pathOrModule of pluginPaths) {
|
|
35
|
+
try {
|
|
36
|
+
const plugin = await loadPlugin(pathOrModule);
|
|
37
|
+
await this.register(plugin);
|
|
38
|
+
} catch (error) {
|
|
39
|
+
this.ctx.logger.error({ pathOrModule, error }, "Failed to load plugin");
|
|
40
|
+
// Continue loading other plugins
|
|
41
|
+
}
|
|
42
|
+
}
|
|
43
|
+
}
|
|
44
|
+
|
|
45
|
+
/** Run all hooks for a given point (waterfall pattern) */
|
|
46
|
+
async runHook<T>(point: HookPoint, data: T): Promise<T> {
|
|
47
|
+
const handlers = this.hooks.get(point);
|
|
48
|
+
if (!handlers || handlers.length === 0) return data;
|
|
49
|
+
|
|
50
|
+
let result = data;
|
|
51
|
+
for (const handler of handlers) {
|
|
52
|
+
try {
|
|
53
|
+
const output = await handler(result);
|
|
54
|
+
if (output !== undefined && output !== null) {
|
|
55
|
+
result = output as T;
|
|
56
|
+
}
|
|
57
|
+
} catch (error) {
|
|
58
|
+
this.ctx.logger.error({ hookPoint: point, error }, "Hook handler error");
|
|
59
|
+
// Continue with other handlers
|
|
60
|
+
}
|
|
61
|
+
}
|
|
62
|
+
return result;
|
|
63
|
+
}
|
|
64
|
+
|
|
65
|
+
/** Register a hook handler */
|
|
66
|
+
addHook(point: HookPoint, handler: HookHandler): void {
|
|
67
|
+
const handlers = this.hooks.get(point) ?? [];
|
|
68
|
+
handlers.push(handler);
|
|
69
|
+
this.hooks.set(point, handlers);
|
|
70
|
+
}
|
|
71
|
+
|
|
72
|
+
/** Deactivate all plugins */
|
|
73
|
+
async deactivateAll(): Promise<void> {
|
|
74
|
+
for (const [name, plugin] of this.plugins) {
|
|
75
|
+
try {
|
|
76
|
+
await plugin.deactivate?.();
|
|
77
|
+
this.ctx.logger.debug({ plugin: name }, "Plugin deactivated");
|
|
78
|
+
} catch (error) {
|
|
79
|
+
this.ctx.logger.error({ plugin: name, error }, "Plugin deactivation failed");
|
|
80
|
+
}
|
|
81
|
+
}
|
|
82
|
+
this.plugins.clear();
|
|
83
|
+
this.hooks.clear();
|
|
84
|
+
}
|
|
85
|
+
|
|
86
|
+
/** List registered plugins */
|
|
87
|
+
list(): Array<{ name: string; version: string }> {
|
|
88
|
+
return Array.from(this.plugins.values()).map(({ name, version }) => ({ name, version }));
|
|
89
|
+
}
|
|
90
|
+
}
|
|
@@ -0,0 +1,37 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Plugin System Types
|
|
3
|
+
* Hook-based plugin interface for AAD lifecycle extensibility
|
|
4
|
+
*/
|
|
5
|
+
|
|
6
|
+
import type { EventBus } from "../../shared/events";
|
|
7
|
+
import type { Config } from "../../shared/config";
|
|
8
|
+
import type pino from "pino";
|
|
9
|
+
|
|
10
|
+
export interface PluginContext {
|
|
11
|
+
config: Config;
|
|
12
|
+
eventBus: EventBus;
|
|
13
|
+
logger: pino.Logger;
|
|
14
|
+
}
|
|
15
|
+
|
|
16
|
+
export interface AADPlugin {
|
|
17
|
+
name: string;
|
|
18
|
+
version: string;
|
|
19
|
+
|
|
20
|
+
/** Called when plugin is loaded. Register event listeners here. */
|
|
21
|
+
activate(ctx: PluginContext): Promise<void> | void;
|
|
22
|
+
|
|
23
|
+
/** Called on shutdown. Clean up resources. */
|
|
24
|
+
deactivate?(): Promise<void> | void;
|
|
25
|
+
}
|
|
26
|
+
|
|
27
|
+
/** Hook points for synchronous/async interceptors */
|
|
28
|
+
export type HookPoint =
|
|
29
|
+
| "before:planning"
|
|
30
|
+
| "after:planning"
|
|
31
|
+
| "before:execution"
|
|
32
|
+
| "after:execution"
|
|
33
|
+
| "before:merge"
|
|
34
|
+
| "after:merge"
|
|
35
|
+
| "on:error";
|
|
36
|
+
|
|
37
|
+
export type HookHandler<T = unknown> = (data: T) => Promise<T | void> | T | void;
|
|
@@ -0,0 +1,210 @@
|
|
|
1
|
+
import { describe, test, expect, beforeEach } from "bun:test";
|
|
2
|
+
import { createWorkerId, createTaskId } from "@aad/shared/types";
|
|
3
|
+
import { EventBus } from "@aad/shared/events";
|
|
4
|
+
import { ProcessManager } from "../process-manager";
|
|
5
|
+
import { pino } from "pino";
|
|
6
|
+
|
|
7
|
+
describe("ProcessManager", () => {
|
|
8
|
+
let processManager: ProcessManager;
|
|
9
|
+
let eventBus: EventBus;
|
|
10
|
+
|
|
11
|
+
beforeEach(() => {
|
|
12
|
+
eventBus = new EventBus();
|
|
13
|
+
const logger = pino({ level: "silent" });
|
|
14
|
+
|
|
15
|
+
processManager = new ProcessManager({
|
|
16
|
+
eventBus,
|
|
17
|
+
config: {
|
|
18
|
+
workers: { num: 4, max: 8 },
|
|
19
|
+
models: {},
|
|
20
|
+
timeouts: { claude: 30000, test: 60000, staleTask: 300000 },
|
|
21
|
+
retry: { maxRetries: 2 },
|
|
22
|
+
debug: false,
|
|
23
|
+
adaptiveEffort: false,
|
|
24
|
+
teams: { splitter: false, reviewer: false },
|
|
25
|
+
memorySync: false,
|
|
26
|
+
dashboard: { enabled: false, port: 7333, host: "localhost" },
|
|
27
|
+
},
|
|
28
|
+
logger,
|
|
29
|
+
});
|
|
30
|
+
});
|
|
31
|
+
|
|
32
|
+
describe("initializePool", () => {
|
|
33
|
+
test("initializes worker pool with specified number of workers", () => {
|
|
34
|
+
processManager.initializePool(3);
|
|
35
|
+
|
|
36
|
+
const allWorkers = processManager.getAllWorkers();
|
|
37
|
+
expect(allWorkers).toHaveLength(3);
|
|
38
|
+
|
|
39
|
+
const stats = processManager.getStats();
|
|
40
|
+
expect(stats.total).toBe(3);
|
|
41
|
+
expect(stats.idle).toBe(3);
|
|
42
|
+
});
|
|
43
|
+
|
|
44
|
+
test("emits worker:started events", () => {
|
|
45
|
+
const startedWorkers: string[] = [];
|
|
46
|
+
|
|
47
|
+
eventBus.on("worker:started", (event) => {
|
|
48
|
+
if (event.type === "worker:started") {
|
|
49
|
+
startedWorkers.push(event.workerId as string);
|
|
50
|
+
}
|
|
51
|
+
});
|
|
52
|
+
|
|
53
|
+
processManager.initializePool(2);
|
|
54
|
+
|
|
55
|
+
expect(startedWorkers).toHaveLength(2);
|
|
56
|
+
});
|
|
57
|
+
|
|
58
|
+
test("throws error if already initialized", () => {
|
|
59
|
+
processManager.initializePool(2);
|
|
60
|
+
|
|
61
|
+
expect(() => processManager.initializePool(2)).toThrow("already initialized");
|
|
62
|
+
});
|
|
63
|
+
});
|
|
64
|
+
|
|
65
|
+
describe("getIdleWorkers", () => {
|
|
66
|
+
test("returns all workers initially", () => {
|
|
67
|
+
processManager.initializePool(3);
|
|
68
|
+
|
|
69
|
+
const idleWorkers = processManager.getIdleWorkers();
|
|
70
|
+
expect(idleWorkers).toHaveLength(3);
|
|
71
|
+
});
|
|
72
|
+
|
|
73
|
+
test("returns only idle workers after assigning tasks", () => {
|
|
74
|
+
processManager.initializePool(3);
|
|
75
|
+
|
|
76
|
+
const allWorkers = processManager.getAllWorkers();
|
|
77
|
+
processManager.assignTask(allWorkers[0]!, createTaskId("task-1"));
|
|
78
|
+
|
|
79
|
+
const idleWorkers = processManager.getIdleWorkers();
|
|
80
|
+
expect(idleWorkers).toHaveLength(2);
|
|
81
|
+
});
|
|
82
|
+
});
|
|
83
|
+
|
|
84
|
+
describe("assignTask", () => {
|
|
85
|
+
test("assigns task to worker", () => {
|
|
86
|
+
processManager.initializePool(2);
|
|
87
|
+
|
|
88
|
+
const workerId = processManager.getIdleWorkers()[0]!;
|
|
89
|
+
const taskId = createTaskId("task-1");
|
|
90
|
+
|
|
91
|
+
processManager.assignTask(workerId, taskId);
|
|
92
|
+
|
|
93
|
+
const worker = processManager.getWorker(workerId);
|
|
94
|
+
expect(worker?.isBusy()).toBe(true);
|
|
95
|
+
expect(worker?.getCurrentTask()).toBe(taskId);
|
|
96
|
+
});
|
|
97
|
+
|
|
98
|
+
test("emits worker:busy event", () => {
|
|
99
|
+
processManager.initializePool(2);
|
|
100
|
+
|
|
101
|
+
let busyEventEmitted = false;
|
|
102
|
+
eventBus.on("worker:busy", () => {
|
|
103
|
+
busyEventEmitted = true;
|
|
104
|
+
});
|
|
105
|
+
|
|
106
|
+
const workerId = processManager.getIdleWorkers()[0]!;
|
|
107
|
+
processManager.assignTask(workerId, createTaskId("task-1"));
|
|
108
|
+
|
|
109
|
+
expect(busyEventEmitted).toBe(true);
|
|
110
|
+
});
|
|
111
|
+
|
|
112
|
+
test("throws error for non-existent worker", () => {
|
|
113
|
+
processManager.initializePool(2);
|
|
114
|
+
|
|
115
|
+
expect(() =>
|
|
116
|
+
processManager.assignTask(createWorkerId("worker-999"), createTaskId("task-1"))
|
|
117
|
+
).toThrow("Worker not found");
|
|
118
|
+
});
|
|
119
|
+
});
|
|
120
|
+
|
|
121
|
+
describe("completeTask", () => {
|
|
122
|
+
test("marks worker as idle", () => {
|
|
123
|
+
processManager.initializePool(2);
|
|
124
|
+
|
|
125
|
+
const workerId = processManager.getIdleWorkers()[0]!;
|
|
126
|
+
processManager.assignTask(workerId, createTaskId("task-1"));
|
|
127
|
+
processManager.completeTask(workerId);
|
|
128
|
+
|
|
129
|
+
const worker = processManager.getWorker(workerId);
|
|
130
|
+
expect(worker?.isIdle()).toBe(true);
|
|
131
|
+
expect(worker?.getCurrentTask()).toBeNull();
|
|
132
|
+
});
|
|
133
|
+
|
|
134
|
+
test("emits worker:idle event", () => {
|
|
135
|
+
processManager.initializePool(2);
|
|
136
|
+
|
|
137
|
+
let idleEventEmitted = false;
|
|
138
|
+
eventBus.on("worker:idle", () => {
|
|
139
|
+
idleEventEmitted = true;
|
|
140
|
+
});
|
|
141
|
+
|
|
142
|
+
const workerId = processManager.getIdleWorkers()[0]!;
|
|
143
|
+
processManager.assignTask(workerId, createTaskId("task-1"));
|
|
144
|
+
processManager.completeTask(workerId);
|
|
145
|
+
|
|
146
|
+
expect(idleEventEmitted).toBe(true);
|
|
147
|
+
});
|
|
148
|
+
});
|
|
149
|
+
|
|
150
|
+
describe("stopAll", () => {
|
|
151
|
+
test("stops all workers", () => {
|
|
152
|
+
processManager.initializePool(3);
|
|
153
|
+
|
|
154
|
+
processManager.stopAll();
|
|
155
|
+
|
|
156
|
+
const stats = processManager.getStats();
|
|
157
|
+
expect(stats.stopped).toBe(3);
|
|
158
|
+
expect(stats.idle).toBe(0);
|
|
159
|
+
});
|
|
160
|
+
|
|
161
|
+
test("emits worker:stopped events for all workers", () => {
|
|
162
|
+
processManager.initializePool(2);
|
|
163
|
+
|
|
164
|
+
const stoppedWorkers: string[] = [];
|
|
165
|
+
eventBus.on("worker:stopped", (event) => {
|
|
166
|
+
if (event.type === "worker:stopped") {
|
|
167
|
+
stoppedWorkers.push(event.workerId as string);
|
|
168
|
+
}
|
|
169
|
+
});
|
|
170
|
+
|
|
171
|
+
processManager.stopAll();
|
|
172
|
+
|
|
173
|
+
expect(stoppedWorkers).toHaveLength(2);
|
|
174
|
+
});
|
|
175
|
+
});
|
|
176
|
+
|
|
177
|
+
describe("stopWorker", () => {
|
|
178
|
+
test("stops specific worker", () => {
|
|
179
|
+
processManager.initializePool(3);
|
|
180
|
+
|
|
181
|
+
const workerId = processManager.getAllWorkers()[0]!;
|
|
182
|
+
processManager.stopWorker(workerId);
|
|
183
|
+
|
|
184
|
+
const worker = processManager.getWorker(workerId);
|
|
185
|
+
expect(worker?.isStopped()).toBe(true);
|
|
186
|
+
|
|
187
|
+
const stats = processManager.getStats();
|
|
188
|
+
expect(stats.stopped).toBe(1);
|
|
189
|
+
expect(stats.idle).toBe(2);
|
|
190
|
+
});
|
|
191
|
+
});
|
|
192
|
+
|
|
193
|
+
describe("getStats", () => {
|
|
194
|
+
test("returns accurate statistics", () => {
|
|
195
|
+
processManager.initializePool(4);
|
|
196
|
+
|
|
197
|
+
const workers = processManager.getAllWorkers();
|
|
198
|
+
processManager.assignTask(workers[0]!, createTaskId("task-1"));
|
|
199
|
+
processManager.assignTask(workers[1]!, createTaskId("task-2"));
|
|
200
|
+
processManager.stopWorker(workers[2]!);
|
|
201
|
+
|
|
202
|
+
const stats = processManager.getStats();
|
|
203
|
+
|
|
204
|
+
expect(stats.total).toBe(4);
|
|
205
|
+
expect(stats.idle).toBe(1);
|
|
206
|
+
expect(stats.busy).toBe(2);
|
|
207
|
+
expect(stats.stopped).toBe(1);
|
|
208
|
+
});
|
|
209
|
+
});
|
|
210
|
+
});
|
|
@@ -0,0 +1,89 @@
|
|
|
1
|
+
import { describe, test, expect } from "bun:test";
|
|
2
|
+
import { createWorkerId, createTaskId } from "@aad/shared/types";
|
|
3
|
+
import { WorkerStateMachine } from "../worker";
|
|
4
|
+
import { ProcessManagerError } from "@aad/shared/errors";
|
|
5
|
+
|
|
6
|
+
describe("WorkerStateMachine", () => {
|
|
7
|
+
test("initializes with idle status", () => {
|
|
8
|
+
const worker = new WorkerStateMachine(createWorkerId("worker-1"));
|
|
9
|
+
|
|
10
|
+
expect(worker.getStatus()).toBe("idle");
|
|
11
|
+
expect(worker.getCurrentTask()).toBeNull();
|
|
12
|
+
expect(worker.isIdle()).toBe(true);
|
|
13
|
+
});
|
|
14
|
+
|
|
15
|
+
test("transitions from idle to busy when task assigned", () => {
|
|
16
|
+
const worker = new WorkerStateMachine(createWorkerId("worker-1"));
|
|
17
|
+
const taskId = createTaskId("task-1");
|
|
18
|
+
|
|
19
|
+
worker.assignTask(taskId);
|
|
20
|
+
|
|
21
|
+
expect(worker.getStatus()).toBe("busy");
|
|
22
|
+
expect(worker.getCurrentTask()).toBe(taskId);
|
|
23
|
+
expect(worker.isBusy()).toBe(true);
|
|
24
|
+
});
|
|
25
|
+
|
|
26
|
+
test("transitions from busy to idle when task completed", () => {
|
|
27
|
+
const worker = new WorkerStateMachine(createWorkerId("worker-1"));
|
|
28
|
+
const taskId = createTaskId("task-1");
|
|
29
|
+
|
|
30
|
+
worker.assignTask(taskId);
|
|
31
|
+
worker.completeTask();
|
|
32
|
+
|
|
33
|
+
expect(worker.getStatus()).toBe("idle");
|
|
34
|
+
expect(worker.getCurrentTask()).toBeNull();
|
|
35
|
+
expect(worker.isIdle()).toBe(true);
|
|
36
|
+
});
|
|
37
|
+
|
|
38
|
+
test("throws error when assigning task to busy worker", () => {
|
|
39
|
+
const worker = new WorkerStateMachine(createWorkerId("worker-1"));
|
|
40
|
+
|
|
41
|
+
worker.assignTask(createTaskId("task-1"));
|
|
42
|
+
|
|
43
|
+
expect(() => worker.assignTask(createTaskId("task-2"))).toThrow(ProcessManagerError);
|
|
44
|
+
});
|
|
45
|
+
|
|
46
|
+
test("throws error when completing task on idle worker", () => {
|
|
47
|
+
const worker = new WorkerStateMachine(createWorkerId("worker-1"));
|
|
48
|
+
|
|
49
|
+
expect(() => worker.completeTask()).toThrow(ProcessManagerError);
|
|
50
|
+
});
|
|
51
|
+
|
|
52
|
+
test("stops worker from any state", () => {
|
|
53
|
+
const worker = new WorkerStateMachine(createWorkerId("worker-1"));
|
|
54
|
+
|
|
55
|
+
worker.stop();
|
|
56
|
+
expect(worker.getStatus()).toBe("stopped");
|
|
57
|
+
expect(worker.isStopped()).toBe(true);
|
|
58
|
+
});
|
|
59
|
+
|
|
60
|
+
test("stops worker from busy state", () => {
|
|
61
|
+
const worker = new WorkerStateMachine(createWorkerId("worker-1"));
|
|
62
|
+
|
|
63
|
+
worker.assignTask(createTaskId("task-1"));
|
|
64
|
+
worker.stop();
|
|
65
|
+
|
|
66
|
+
expect(worker.getStatus()).toBe("stopped");
|
|
67
|
+
expect(worker.getCurrentTask()).toBeNull();
|
|
68
|
+
});
|
|
69
|
+
|
|
70
|
+
test("sets and gets PID", () => {
|
|
71
|
+
const worker = new WorkerStateMachine(createWorkerId("worker-1"));
|
|
72
|
+
|
|
73
|
+
worker.setPid(12345);
|
|
74
|
+
|
|
75
|
+
const state = worker.getState();
|
|
76
|
+
expect(state.pid).toBe(12345);
|
|
77
|
+
});
|
|
78
|
+
|
|
79
|
+
test("getState returns immutable copy", () => {
|
|
80
|
+
const worker = new WorkerStateMachine(createWorkerId("worker-1"));
|
|
81
|
+
|
|
82
|
+
const state1 = worker.getState();
|
|
83
|
+
worker.assignTask(createTaskId("task-1"));
|
|
84
|
+
const state2 = worker.getState();
|
|
85
|
+
|
|
86
|
+
expect(state1.status).toBe("idle");
|
|
87
|
+
expect(state2.status).toBe("busy");
|
|
88
|
+
});
|
|
89
|
+
});
|