@coralai/sps-cli 0.48.1 → 0.49.1

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.
Files changed (147) hide show
  1. package/dist/console-assets/assets/{index-BR3iEolY.js → index-Co0H5e5_.js} +85 -85
  2. package/dist/console-assets/assets/{index-lsG7ZrzD.css → index-DNKOkgDj.css} +1 -1
  3. package/dist/console-assets/index.html +2 -2
  4. package/dist/console-server/routes/system.d.ts.map +1 -1
  5. package/dist/console-server/routes/system.js +30 -0
  6. package/dist/console-server/routes/system.js.map +1 -1
  7. package/package.json +4 -1
  8. package/dist/commands/cardMarkComplete.test.d.ts +0 -2
  9. package/dist/commands/cardMarkComplete.test.d.ts.map +0 -1
  10. package/dist/commands/cardMarkComplete.test.js +0 -142
  11. package/dist/commands/cardMarkComplete.test.js.map +0 -1
  12. package/dist/commands/cardMarkStarted.test.d.ts +0 -2
  13. package/dist/commands/cardMarkStarted.test.d.ts.map +0 -1
  14. package/dist/commands/cardMarkStarted.test.js +0 -98
  15. package/dist/commands/cardMarkStarted.test.js.map +0 -1
  16. package/dist/commands/projectInit.test.d.ts +0 -2
  17. package/dist/commands/projectInit.test.d.ts.map +0 -1
  18. package/dist/commands/projectInit.test.js +0 -126
  19. package/dist/commands/projectInit.test.js.map +0 -1
  20. package/dist/commands/tick-heartbeat.test.d.ts +0 -2
  21. package/dist/commands/tick-heartbeat.test.d.ts.map +0 -1
  22. package/dist/commands/tick-heartbeat.test.js +0 -89
  23. package/dist/commands/tick-heartbeat.test.js.map +0 -1
  24. package/dist/core/checklist.test.d.ts +0 -2
  25. package/dist/core/checklist.test.d.ts.map +0 -1
  26. package/dist/core/checklist.test.js +0 -74
  27. package/dist/core/checklist.test.js.map +0 -1
  28. package/dist/core/config.test.d.ts +0 -2
  29. package/dist/core/config.test.d.ts.map +0 -1
  30. package/dist/core/config.test.js +0 -352
  31. package/dist/core/config.test.js.map +0 -1
  32. package/dist/core/lock.test.d.ts +0 -2
  33. package/dist/core/lock.test.d.ts.map +0 -1
  34. package/dist/core/lock.test.js +0 -118
  35. package/dist/core/lock.test.js.map +0 -1
  36. package/dist/core/markerFile.test.d.ts +0 -2
  37. package/dist/core/markerFile.test.d.ts.map +0 -1
  38. package/dist/core/markerFile.test.js +0 -111
  39. package/dist/core/markerFile.test.js.map +0 -1
  40. package/dist/core/queue.test.d.ts +0 -2
  41. package/dist/core/queue.test.d.ts.map +0 -1
  42. package/dist/core/queue.test.js +0 -114
  43. package/dist/core/queue.test.js.map +0 -1
  44. package/dist/core/sessionCleanup.test.d.ts +0 -2
  45. package/dist/core/sessionCleanup.test.d.ts.map +0 -1
  46. package/dist/core/sessionCleanup.test.js +0 -158
  47. package/dist/core/sessionCleanup.test.js.map +0 -1
  48. package/dist/core/shellEnv.test.d.ts +0 -2
  49. package/dist/core/shellEnv.test.d.ts.map +0 -1
  50. package/dist/core/shellEnv.test.js +0 -116
  51. package/dist/core/shellEnv.test.js.map +0 -1
  52. package/dist/core/skillStore.test.d.ts +0 -2
  53. package/dist/core/skillStore.test.d.ts.map +0 -1
  54. package/dist/core/skillStore.test.js +0 -203
  55. package/dist/core/skillStore.test.js.map +0 -1
  56. package/dist/core/state.test.d.ts +0 -2
  57. package/dist/core/state.test.d.ts.map +0 -1
  58. package/dist/core/state.test.js +0 -336
  59. package/dist/core/state.test.js.map +0 -1
  60. package/dist/engines/CloseoutEngine.d.ts +0 -72
  61. package/dist/engines/CloseoutEngine.d.ts.map +0 -1
  62. package/dist/engines/CloseoutEngine.js +0 -648
  63. package/dist/engines/CloseoutEngine.js.map +0 -1
  64. package/dist/engines/EventHandler.test.d.ts +0 -2
  65. package/dist/engines/EventHandler.test.d.ts.map +0 -1
  66. package/dist/engines/EventHandler.test.js +0 -169
  67. package/dist/engines/EventHandler.test.js.map +0 -1
  68. package/dist/engines/ExecutionEngine.d.ts +0 -125
  69. package/dist/engines/ExecutionEngine.d.ts.map +0 -1
  70. package/dist/engines/ExecutionEngine.js +0 -766
  71. package/dist/engines/ExecutionEngine.js.map +0 -1
  72. package/dist/engines/MonitorEngine.test.d.ts +0 -2
  73. package/dist/engines/MonitorEngine.test.d.ts.map +0 -1
  74. package/dist/engines/MonitorEngine.test.js +0 -355
  75. package/dist/engines/MonitorEngine.test.js.map +0 -1
  76. package/dist/engines/engine-pipeline-adapter.test.d.ts +0 -17
  77. package/dist/engines/engine-pipeline-adapter.test.d.ts.map +0 -1
  78. package/dist/engines/engine-pipeline-adapter.test.js +0 -707
  79. package/dist/engines/engine-pipeline-adapter.test.js.map +0 -1
  80. package/dist/interfaces/HookProvider.d.ts +0 -9
  81. package/dist/interfaces/HookProvider.d.ts.map +0 -1
  82. package/dist/interfaces/HookProvider.js +0 -2
  83. package/dist/interfaces/HookProvider.js.map +0 -1
  84. package/dist/manager/completion-judge.test.d.ts +0 -17
  85. package/dist/manager/completion-judge.test.d.ts.map +0 -1
  86. package/dist/manager/completion-judge.test.js +0 -233
  87. package/dist/manager/completion-judge.test.js.map +0 -1
  88. package/dist/manager/integration-queue.d.ts +0 -71
  89. package/dist/manager/integration-queue.d.ts.map +0 -1
  90. package/dist/manager/integration-queue.js +0 -137
  91. package/dist/manager/integration-queue.js.map +0 -1
  92. package/dist/manager/integration-queue.test.d.ts +0 -17
  93. package/dist/manager/integration-queue.test.d.ts.map +0 -1
  94. package/dist/manager/integration-queue.test.js +0 -210
  95. package/dist/manager/integration-queue.test.js.map +0 -1
  96. package/dist/manager/pm-client.d.ts +0 -10
  97. package/dist/manager/pm-client.d.ts.map +0 -1
  98. package/dist/manager/pm-client.js +0 -260
  99. package/dist/manager/pm-client.js.map +0 -1
  100. package/dist/manager/resource-limiter.d.ts +0 -56
  101. package/dist/manager/resource-limiter.d.ts.map +0 -1
  102. package/dist/manager/resource-limiter.js +0 -116
  103. package/dist/manager/resource-limiter.js.map +0 -1
  104. package/dist/manager/resource-limiter.test.d.ts +0 -17
  105. package/dist/manager/resource-limiter.test.d.ts.map +0 -1
  106. package/dist/manager/resource-limiter.test.js +0 -118
  107. package/dist/manager/resource-limiter.test.js.map +0 -1
  108. package/dist/manager/supervisor.test.d.ts +0 -17
  109. package/dist/manager/supervisor.test.d.ts.map +0 -1
  110. package/dist/manager/supervisor.test.js +0 -216
  111. package/dist/manager/supervisor.test.js.map +0 -1
  112. package/dist/manager/worker-manager-impl.test.d.ts +0 -17
  113. package/dist/manager/worker-manager-impl.test.d.ts.map +0 -1
  114. package/dist/manager/worker-manager-impl.test.js +0 -446
  115. package/dist/manager/worker-manager-impl.test.js.map +0 -1
  116. package/dist/providers/PlaneTaskBackend.d.ts +0 -83
  117. package/dist/providers/PlaneTaskBackend.d.ts.map +0 -1
  118. package/dist/providers/PlaneTaskBackend.js +0 -461
  119. package/dist/providers/PlaneTaskBackend.js.map +0 -1
  120. package/dist/providers/TrelloTaskBackend.d.ts +0 -64
  121. package/dist/providers/TrelloTaskBackend.d.ts.map +0 -1
  122. package/dist/providers/TrelloTaskBackend.js +0 -298
  123. package/dist/providers/TrelloTaskBackend.js.map +0 -1
  124. package/dist/providers/adapters/acp-fs-handlers.test.d.ts +0 -2
  125. package/dist/providers/adapters/acp-fs-handlers.test.d.ts.map +0 -1
  126. package/dist/providers/adapters/acp-fs-handlers.test.js +0 -80
  127. package/dist/providers/adapters/acp-fs-handlers.test.js.map +0 -1
  128. package/dist/providers/adapters/acp-permissions.test.d.ts +0 -2
  129. package/dist/providers/adapters/acp-permissions.test.d.ts.map +0 -1
  130. package/dist/providers/adapters/acp-permissions.test.js +0 -103
  131. package/dist/providers/adapters/acp-permissions.test.js.map +0 -1
  132. package/dist/providers/adapters/acp-session-accumulator.test.d.ts +0 -2
  133. package/dist/providers/adapters/acp-session-accumulator.test.d.ts.map +0 -1
  134. package/dist/providers/adapters/acp-session-accumulator.test.js +0 -88
  135. package/dist/providers/adapters/acp-session-accumulator.test.js.map +0 -1
  136. package/dist/providers/adapters/acp-terminal-manager.test.d.ts +0 -2
  137. package/dist/providers/adapters/acp-terminal-manager.test.d.ts.map +0 -1
  138. package/dist/providers/adapters/acp-terminal-manager.test.js +0 -86
  139. package/dist/providers/adapters/acp-terminal-manager.test.js.map +0 -1
  140. package/dist/providers/outputParser.test.d.ts +0 -2
  141. package/dist/providers/outputParser.test.d.ts.map +0 -1
  142. package/dist/providers/outputParser.test.js +0 -185
  143. package/dist/providers/outputParser.test.js.map +0 -1
  144. package/dist/test-setup.d.ts +0 -2
  145. package/dist/test-setup.d.ts.map +0 -1
  146. package/dist/test-setup.js +0 -27
  147. package/dist/test-setup.js.map +0 -1
@@ -1,707 +0,0 @@
1
- /**
2
- * @module engine-pipeline-adapter.test
3
- * @description 集成测试:验证所有引擎通过 ProjectPipelineAdapter 使用可配置状态名
4
- *
5
- * @author eddy
6
- * @organization wykj
7
- * @ownership wykj/eddy
8
- *
9
- * @created 2026-04-02
10
- * @updated 2026-04-03
11
- *
12
- * @role test
13
- * @layer engine
14
- * @boundedContext pipeline-configuration
15
- */
16
- import { cpSync, mkdirSync, mkdtempSync, rmSync, } from 'node:fs';
17
- import { tmpdir } from 'node:os';
18
- import { join } from 'node:path';
19
- import { afterEach, beforeEach, describe, expect, it, vi } from 'vitest';
20
- import { ProjectPipelineAdapter } from '../core/projectPipelineAdapter.js';
21
- import { RuntimeStore } from '../core/runtimeStore.js';
22
- import { createIdleWorkerSlot, writeState } from '../core/state.js';
23
- import { SPSEventHandler } from './EventHandler.js';
24
- import { MonitorEngine } from './MonitorEngine.js';
25
- import { SchedulerEngine } from './SchedulerEngine.js';
26
- import { StageEngine } from './StageEngine.js';
27
- // ─── Custom state names (must match __fixtures__/custom-pipeline.yaml) ──
28
- const CUSTOM = {
29
- planning: 'Planned',
30
- backlog: 'Queue',
31
- ready: 'Ready',
32
- active: 'Working',
33
- review: 'Review',
34
- done: 'Shipped',
35
- };
36
- // ─── Helpers ────────────────────────────────────────────────────────
37
- function makeTempDir() {
38
- return mkdtempSync(join(tmpdir(), 'sps-engine-adapter-test-'));
39
- }
40
- function makeConfig(overrides = {}) {
41
- return {
42
- PROJECT_NAME: 'test-project',
43
- PROJECT_DIR: '/tmp/test-project',
44
- GITLAB_PROJECT: 'test/project',
45
- GITLAB_PROJECT_ID: '1',
46
- GITLAB_MERGE_BRANCH: 'main',
47
- PM_TOOL: 'markdown',
48
- MR_MODE: 'create',
49
- WORKER_TRANSPORT: 'acp-sdk',
50
- MAX_CONCURRENT_WORKERS: 2,
51
- WORKER_RESTART_LIMIT: 3,
52
- MAX_ACTIONS_PER_TICK: 5,
53
- INPROGRESS_TIMEOUT_HOURS: 4,
54
- MONITOR_AUTO_QA: true,
55
- CONFLICT_DEFAULT: 'parallel',
56
- TICK_LOCK_TIMEOUT_MINUTES: 10,
57
- WORKER_LAUNCH_TIMEOUT_S: 60,
58
- WORKER_IDLE_TIMEOUT_M: 30,
59
- WORKER_ACK_TIMEOUT_S: 60,
60
- WORKER_ACK_MAX_RETRIES: 1,
61
- raw: {},
62
- ...overrides,
63
- };
64
- }
65
- function makeDefaultState(maxWorkers) {
66
- const workers = {};
67
- for (let i = 0; i < maxWorkers; i++) {
68
- workers[`worker-${i}`] = createIdleWorkerSlot();
69
- }
70
- return {
71
- version: 1,
72
- generation: 0,
73
- updatedAt: new Date().toISOString(),
74
- updatedBy: 'test',
75
- workers,
76
- activeCards: {},
77
- leases: {},
78
- worktreeEvidence: {},
79
- worktreeCleanup: [],
80
- sessions: {},
81
- integrationQueues: {},
82
- pendingPMActions: [],
83
- };
84
- }
85
- function makeCard(seq, state, overrides = {}) {
86
- return {
87
- id: `card-${seq}`,
88
- seq,
89
- title: `Test card ${seq}`,
90
- desc: `Description for card ${seq}`,
91
- state,
92
- labels: [],
93
- meta: {},
94
- ...overrides,
95
- };
96
- }
97
- function makeCtx(tempDir, config) {
98
- const stateFile = join(tempDir, 'state.json');
99
- const logsDir = join(tempDir, 'logs');
100
- mkdirSync(logsDir, { recursive: true });
101
- return {
102
- projectName: 'test-project',
103
- config,
104
- paths: {
105
- repoDir: tempDir,
106
- stateFile,
107
- lockFile: join(tempDir, 'tick.lock'),
108
- logsDir,
109
- pipelineOrderFile: join(tempDir, 'pipeline_order.json'),
110
- },
111
- pmTool: config.PM_TOOL,
112
- maxWorkers: config.MAX_CONCURRENT_WORKERS,
113
- mrMode: config.MR_MODE,
114
- mergeBranch: config.GITLAB_MERGE_BRANCH,
115
- validate: () => ({ ok: true, errors: [] }),
116
- reload: () => { },
117
- };
118
- }
119
- function makeTaskBackend() {
120
- return {
121
- listAll: vi.fn().mockResolvedValue([]),
122
- listByState: vi.fn().mockResolvedValue([]),
123
- getBySeq: vi.fn().mockResolvedValue(null),
124
- move: vi.fn().mockResolvedValue(undefined),
125
- addLabel: vi.fn().mockResolvedValue(undefined),
126
- removeLabel: vi.fn().mockResolvedValue(undefined),
127
- claim: vi.fn().mockResolvedValue(undefined),
128
- releaseClaim: vi.fn().mockResolvedValue(undefined),
129
- comment: vi.fn().mockResolvedValue(undefined),
130
- create: vi.fn().mockResolvedValue(null),
131
- checklistCreate: vi.fn().mockResolvedValue(undefined),
132
- checklistList: vi.fn().mockResolvedValue([]),
133
- metaRead: vi.fn().mockResolvedValue({}),
134
- metaWrite: vi.fn().mockResolvedValue(undefined),
135
- bootstrap: vi.fn().mockResolvedValue(undefined),
136
- };
137
- }
138
- function makeRepoBackend() {
139
- return {
140
- ensureCleanBase: vi.fn().mockResolvedValue(undefined),
141
- ensureBranch: vi.fn().mockResolvedValue(undefined),
142
- ensureWorktree: vi.fn().mockResolvedValue(undefined),
143
- commit: vi.fn().mockResolvedValue(undefined),
144
- push: vi.fn().mockResolvedValue(undefined),
145
- createOrUpdateMr: vi.fn().mockResolvedValue({ url: '', iid: 1 }),
146
- getMrStatus: vi.fn().mockResolvedValue({ exists: false, state: 'none', merged: false }),
147
- mergeMr: vi.fn().mockResolvedValue({ merged: true }),
148
- detectMerged: vi.fn().mockResolvedValue(false),
149
- rebase: vi.fn().mockResolvedValue({ success: true, conflictFiles: [] }),
150
- removeWorktree: vi.fn().mockResolvedValue(undefined),
151
- };
152
- }
153
- function makeWorkerManager() {
154
- const response = {
155
- accepted: true,
156
- slot: 'worker-0',
157
- workerId: 'test-project:worker-0:1',
158
- pid: 99999,
159
- sessionId: 'test-session',
160
- };
161
- return {
162
- run: vi.fn().mockResolvedValue(response),
163
- cancel: vi.fn().mockResolvedValue(undefined),
164
- inspect: vi.fn().mockReturnValue([]),
165
- onEvent: vi.fn(),
166
- cleanup: vi.fn(),
167
- };
168
- }
169
- function makeSupervisor() {
170
- return {
171
- spawn: vi.fn(),
172
- kill: vi.fn().mockResolvedValue(undefined),
173
- remove: vi.fn(),
174
- get: vi.fn().mockReturnValue(null),
175
- list: vi.fn().mockReturnValue([]),
176
- };
177
- }
178
- function makeWorkerManagerStub() {
179
- return {
180
- run: vi.fn().mockResolvedValue({ accepted: true, slot: 'worker-1', workerId: 'w', pid: 1 }),
181
- resume: vi.fn().mockResolvedValue({ accepted: true, slot: 'worker-1', workerId: 'w', pid: 1 }),
182
- cancel: vi.fn().mockResolvedValue(undefined),
183
- sendInput: vi.fn().mockResolvedValue(undefined),
184
- confirm: vi.fn().mockResolvedValue(undefined),
185
- inspect: vi.fn().mockReturnValue([]),
186
- onEvent: vi.fn(),
187
- recover: vi.fn().mockResolvedValue({ scanned: 0, alive: 0, completed: 0, failed: 0, released: 0, queueRebuilt: 0 }),
188
- cleanup: vi.fn(),
189
- };
190
- }
191
- // ─── Setup pipeline adapter from YAML fixture ──────────────────────
192
- function setupAdapterWithCustomYaml(tempDir, config) {
193
- // Copy the custom YAML to the temp project dir
194
- const pipelinesDir = join(tempDir, '.sps', 'pipelines');
195
- mkdirSync(pipelinesDir, { recursive: true });
196
- cpSync(join(__dirname, '__fixtures__', 'custom-pipeline.yaml'), join(pipelinesDir, 'custom-pipeline.yaml'));
197
- return new ProjectPipelineAdapter(config, tempDir);
198
- }
199
- // ─── Tests ──────────────────────────────────────────────────────────
200
- describe('ProjectPipelineAdapter YAML loading', () => {
201
- let tempDir;
202
- beforeEach(() => { tempDir = makeTempDir(); });
203
- afterEach(() => { rmSync(tempDir, { recursive: true, force: true }); });
204
- it('loads custom state names from YAML', () => {
205
- const config = makeConfig({ PROJECT_DIR: tempDir });
206
- const adapter = setupAdapterWithCustomYaml(tempDir, config);
207
- expect(adapter.states.planning).toBe(CUSTOM.planning);
208
- expect(adapter.states.backlog).toBe(CUSTOM.backlog);
209
- expect(adapter.states.ready).toBe(CUSTOM.ready);
210
- expect(adapter.states.active).toBe(CUSTOM.active);
211
- expect(adapter.states.review).toBe(CUSTOM.review);
212
- expect(adapter.states.done).toBe(CUSTOM.done);
213
- });
214
- it('loads custom stages from YAML', () => {
215
- const config = makeConfig({ PROJECT_DIR: tempDir });
216
- const adapter = setupAdapterWithCustomYaml(tempDir, config);
217
- expect(adapter.stages).toHaveLength(2);
218
- expect(adapter.stages[0].name).toBe('develop');
219
- expect(adapter.stages[0].triggerState).toBe('Ready');
220
- expect(adapter.stages[0].activeState).toBe('Working');
221
- expect(adapter.stages[0].onCompleteState).toBe('Review');
222
- expect(adapter.stages[1].name).toBe('integrate');
223
- expect(adapter.stages[1].triggerState).toBe('Review');
224
- expect(adapter.stages[1].onCompleteState).toBe('Shipped');
225
- });
226
- it('activeStates uses custom names', () => {
227
- const config = makeConfig({ PROJECT_DIR: tempDir });
228
- const adapter = setupAdapterWithCustomYaml(tempDir, config);
229
- expect(adapter.activeStates).toContain('Planned');
230
- expect(adapter.activeStates).toContain('Queue');
231
- expect(adapter.activeStates).toContain('Ready');
232
- expect(adapter.activeStates).toContain('Working');
233
- expect(adapter.activeStates).toContain('Review');
234
- expect(adapter.activeStates).not.toContain('Done');
235
- expect(adapter.activeStates).not.toContain('Shipped');
236
- });
237
- it('derivePmState maps lease phases to custom states', () => {
238
- const config = makeConfig({ PROJECT_DIR: tempDir });
239
- const adapter = setupAdapterWithCustomYaml(tempDir, config);
240
- expect(adapter.derivePmState('queued')).toBe('Ready');
241
- expect(adapter.derivePmState('coding')).toBe('Working');
242
- expect(adapter.derivePmState('merging')).toBe('Review');
243
- expect(adapter.derivePmState('resolving_conflict')).toBe('Review');
244
- expect(adapter.derivePmState('closing')).toBe('Review');
245
- });
246
- });
247
- describe('SchedulerEngine uses adapter states', () => {
248
- let tempDir;
249
- let taskBackend;
250
- let adapter;
251
- beforeEach(() => {
252
- tempDir = makeTempDir();
253
- taskBackend = makeTaskBackend();
254
- const config = makeConfig({ PROJECT_DIR: tempDir });
255
- adapter = setupAdapterWithCustomYaml(tempDir, config);
256
- });
257
- afterEach(() => { rmSync(tempDir, { recursive: true, force: true }); });
258
- it('lists cards by custom planning state', async () => {
259
- const config = makeConfig({ PROJECT_DIR: tempDir });
260
- const ctx = makeCtx(tempDir, config);
261
- const state = makeDefaultState(2);
262
- writeState(ctx.paths.stateFile, state, 'test');
263
- // Return a card with AI-PIPELINE label
264
- const card = makeCard('1', CUSTOM.planning, { labels: ['AI-PIPELINE'] });
265
- taskBackend.listByState.mockResolvedValue([card]);
266
- const engine = new SchedulerEngine(ctx, taskBackend, adapter);
267
- await engine.tick({ dryRun: true });
268
- // Should call listByState with custom 'Planned' state (not 'Planning')
269
- expect(taskBackend.listByState).toHaveBeenCalledWith('Planned');
270
- });
271
- it('moves cards to custom backlog state', async () => {
272
- const config = makeConfig({ PROJECT_DIR: tempDir });
273
- const ctx = makeCtx(tempDir, config);
274
- const state = makeDefaultState(2);
275
- writeState(ctx.paths.stateFile, state, 'test');
276
- const card = makeCard('1', CUSTOM.planning, { labels: ['AI-PIPELINE'] });
277
- taskBackend.listByState.mockResolvedValue([card]);
278
- const engine = new SchedulerEngine(ctx, taskBackend, adapter);
279
- const _result = await engine.tick();
280
- // Should move to custom 'Queue' state (not 'Backlog')
281
- expect(taskBackend.move).toHaveBeenCalledWith('1', 'Queue');
282
- });
283
- });
284
- describe('StageEngine (first stage) uses adapter states', () => {
285
- let tempDir;
286
- let taskBackend;
287
- let repoBackend;
288
- let workerManager;
289
- let adapter;
290
- beforeEach(() => {
291
- tempDir = makeTempDir();
292
- taskBackend = makeTaskBackend();
293
- repoBackend = makeRepoBackend();
294
- workerManager = makeWorkerManager();
295
- const config = makeConfig({ PROJECT_DIR: tempDir });
296
- adapter = setupAdapterWithCustomYaml(tempDir, config);
297
- });
298
- afterEach(() => { rmSync(tempDir, { recursive: true, force: true }); });
299
- it('lists backlog cards by custom state', async () => {
300
- const config = makeConfig({ PROJECT_DIR: tempDir });
301
- const ctx = makeCtx(tempDir, config);
302
- const state = makeDefaultState(2);
303
- writeState(ctx.paths.stateFile, state, 'test');
304
- const firstStage = adapter.stages[0];
305
- const engine = new StageEngine(ctx, firstStage, 0, adapter.stages.length, taskBackend, repoBackend, workerManager, adapter);
306
- await engine.tick({ dryRun: true });
307
- // Should query custom states: 'Working' (active), 'Queue' (backlog), 'Ready' (ready)
308
- const calls = taskBackend.listByState.mock.calls.map(c => c[0]);
309
- expect(calls).toContain('Working'); // listRuntimeAwareActiveCards
310
- expect(calls).toContain('Queue'); // backlog cards
311
- expect(calls).toContain('Ready'); // ready cards (at least 2 calls)
312
- });
313
- it('prepares card: moves to custom ready state', async () => {
314
- const config = makeConfig({ PROJECT_DIR: tempDir });
315
- const ctx = makeCtx(tempDir, config);
316
- const state = makeDefaultState(2);
317
- writeState(ctx.paths.stateFile, state, 'test');
318
- // Return a backlog card, empty for other states
319
- const backlogCard = makeCard('1', CUSTOM.backlog);
320
- taskBackend.listByState.mockImplementation((s) => {
321
- if (s === CUSTOM.backlog)
322
- return Promise.resolve([backlogCard]);
323
- return Promise.resolve([]);
324
- });
325
- const firstStage = adapter.stages[0];
326
- const engine = new StageEngine(ctx, firstStage, 0, adapter.stages.length, taskBackend, repoBackend, workerManager, adapter);
327
- await engine.tick();
328
- // prepare phase should move Backlog → Ready (custom states)
329
- expect(taskBackend.move).toHaveBeenCalledWith('1', 'Ready');
330
- });
331
- it('launches card: moves to custom active state', async () => {
332
- const config = makeConfig({ PROJECT_DIR: tempDir });
333
- const ctx = makeCtx(tempDir, config);
334
- const state = makeDefaultState(2);
335
- writeState(ctx.paths.stateFile, state, 'test');
336
- // Return a Todo/Ready card ready to launch
337
- const readyCard = makeCard('1', CUSTOM.ready);
338
- taskBackend.listByState.mockImplementation((s) => {
339
- if (s === CUSTOM.ready)
340
- return Promise.resolve([readyCard]);
341
- return Promise.resolve([]);
342
- });
343
- const firstStage = adapter.stages[0];
344
- const engine = new StageEngine(ctx, firstStage, 0, adapter.stages.length, taskBackend, repoBackend, workerManager, adapter);
345
- await engine.tick();
346
- // launch phase should move Ready → Working (custom states)
347
- expect(taskBackend.move).toHaveBeenCalledWith('1', 'Working');
348
- });
349
- });
350
- describe('StageEngine (last stage) uses adapter states', () => {
351
- let tempDir;
352
- let taskBackend;
353
- let repoBackend;
354
- let workerManager;
355
- let adapter;
356
- beforeEach(() => {
357
- tempDir = makeTempDir();
358
- taskBackend = makeTaskBackend();
359
- repoBackend = makeRepoBackend();
360
- workerManager = makeWorkerManager();
361
- const config = makeConfig({ PROJECT_DIR: tempDir });
362
- adapter = setupAdapterWithCustomYaml(tempDir, config);
363
- });
364
- afterEach(() => { rmSync(tempDir, { recursive: true, force: true }); });
365
- it('lists cards by custom review state', async () => {
366
- const config = makeConfig({ PROJECT_DIR: tempDir });
367
- const ctx = makeCtx(tempDir, config);
368
- const state = makeDefaultState(2);
369
- writeState(ctx.paths.stateFile, state, 'test');
370
- const lastIdx = adapter.stages.length - 1;
371
- const lastStage = adapter.stages[lastIdx];
372
- const engine = new StageEngine(ctx, lastStage, lastIdx, adapter.stages.length, taskBackend, repoBackend, workerManager, adapter);
373
- await engine.tick();
374
- // Should call listByState with custom 'Review' (not 'QA')
375
- expect(taskBackend.listByState).toHaveBeenCalledWith('Review');
376
- });
377
- });
378
- describe('MonitorEngine uses adapter states', () => {
379
- let tempDir;
380
- let taskBackend;
381
- let repoBackend;
382
- let supervisor;
383
- let adapter;
384
- beforeEach(() => {
385
- tempDir = makeTempDir();
386
- taskBackend = makeTaskBackend();
387
- repoBackend = makeRepoBackend();
388
- supervisor = makeSupervisor();
389
- const config = makeConfig({ PROJECT_DIR: tempDir });
390
- adapter = setupAdapterWithCustomYaml(tempDir, config);
391
- });
392
- afterEach(() => { rmSync(tempDir, { recursive: true, force: true }); });
393
- it('lists inprogress cards by custom active state', async () => {
394
- const config = makeConfig({ PROJECT_DIR: tempDir });
395
- const ctx = makeCtx(tempDir, config);
396
- const state = makeDefaultState(2);
397
- writeState(ctx.paths.stateFile, state, 'test');
398
- const engine = new MonitorEngine(ctx, taskBackend, repoBackend, undefined, supervisor, adapter, makeWorkerManagerStub());
399
- await engine.tick();
400
- // Should call listByState with custom 'Working' (not 'Inprogress')
401
- const calls = taskBackend.listByState.mock.calls.map(c => c[0]);
402
- expect(calls).toContain('Working');
403
- });
404
- it('checkBlockedCards iterates custom state names', async () => {
405
- const config = makeConfig({ PROJECT_DIR: tempDir });
406
- const ctx = makeCtx(tempDir, config);
407
- const state = makeDefaultState(2);
408
- writeState(ctx.paths.stateFile, state, 'test');
409
- const engine = new MonitorEngine(ctx, taskBackend, repoBackend, undefined, supervisor, adapter, makeWorkerManagerStub());
410
- await engine.tick();
411
- // checkBlockedCards should iterate Queue/Ready/Working/Review (not Backlog/Todo/Inprogress/QA)
412
- const calls = taskBackend.listByState.mock.calls.map(c => c[0]);
413
- expect(calls).toContain('Queue');
414
- expect(calls).toContain('Ready');
415
- expect(calls).toContain('Review');
416
- });
417
- it('auto-retry moves to custom ready state', async () => {
418
- const config = makeConfig({ PROJECT_DIR: tempDir, MONITOR_AUTO_QA: true });
419
- const ctx = makeCtx(tempDir, config);
420
- // Set up state with an active card that has a stale slot
421
- const state = makeDefaultState(2);
422
- state.workers['worker-0'] = {
423
- ...createIdleWorkerSlot(),
424
- status: 'active',
425
- seq: 1,
426
- branch: 'feature/1-test',
427
- worktree: '/tmp/wt-1',
428
- claimedAt: new Date(Date.now() - 600_000).toISOString(),
429
- lastHeartbeat: null,
430
- mode: 'acp-sdk',
431
- transport: 'acp-sdk',
432
- outputFile: '/tmp/non-existent-output.jsonl',
433
- };
434
- state.activeCards['1'] = {
435
- seq: 1,
436
- state: 'Working',
437
- worker: 'worker-0',
438
- mrUrl: null,
439
- conflictDomains: [],
440
- startedAt: new Date(Date.now() - 600_000).toISOString(),
441
- retryCount: 0,
442
- };
443
- state.leases['1'] = {
444
- seq: 1,
445
- pmStateObserved: 'Working',
446
- phase: 'coding',
447
- slot: 'worker-0',
448
- branch: 'feature/1-test',
449
- worktree: '/tmp/wt-1',
450
- sessionId: null,
451
- runId: null,
452
- claimedAt: new Date(Date.now() - 600_000).toISOString(),
453
- retryCount: 0,
454
- lastTransitionAt: new Date(Date.now() - 600_000).toISOString(),
455
- };
456
- writeState(ctx.paths.stateFile, state, 'test');
457
- // Return the card as being in 'Working' state
458
- const card = makeCard('1', CUSTOM.active);
459
- taskBackend.listByState.mockImplementation((s) => {
460
- if (s === CUSTOM.active)
461
- return Promise.resolve([card]);
462
- return Promise.resolve([]);
463
- });
464
- // getMrStatus returns no MR (so it's a genuine stale runtime)
465
- repoBackend.getMrStatus.mockResolvedValue({
466
- exists: false,
467
- state: 'none',
468
- merged: false,
469
- });
470
- const engine = new MonitorEngine(ctx, taskBackend, repoBackend, undefined, supervisor, adapter, makeWorkerManagerStub());
471
- await engine.tick();
472
- // With MONITOR_AUTO_QA, stale runtime should be moved to Review (custom, not QA)
473
- const moveCalls = taskBackend.move.mock.calls;
474
- if (moveCalls.length > 0) {
475
- // Should use 'Review' (custom) not 'QA' (default)
476
- const targets = moveCalls.map(c => c[1]);
477
- expect(targets.every(t => t !== 'QA')).toBe(true);
478
- // Check for either Review (auto-qa) or Ready (auto-retry)
479
- expect(targets.some(t => t === 'Review' || t === 'Ready')).toBe(true);
480
- }
481
- });
482
- });
483
- describe('SPSEventHandler uses adapter states', () => {
484
- let tempDir;
485
- let taskBackend;
486
- let adapter;
487
- beforeEach(() => {
488
- tempDir = makeTempDir();
489
- taskBackend = makeTaskBackend();
490
- const config = makeConfig({ PROJECT_DIR: tempDir });
491
- adapter = setupAdapterWithCustomYaml(tempDir, config);
492
- });
493
- afterEach(() => { rmSync(tempDir, { recursive: true, force: true }); });
494
- it('onCompleted moves to custom done state for integration', async () => {
495
- const config = makeConfig({ PROJECT_DIR: tempDir });
496
- const ctx = makeCtx(tempDir, config);
497
- const state = makeDefaultState(2);
498
- state.leases['1'] = {
499
- seq: 1,
500
- pmStateObserved: 'Review',
501
- phase: 'merging',
502
- slot: 'worker-0',
503
- branch: 'feature/1-test',
504
- worktree: '/tmp/wt-1',
505
- sessionId: null,
506
- runId: null,
507
- claimedAt: new Date().toISOString(),
508
- retryCount: 0,
509
- lastTransitionAt: new Date().toISOString(),
510
- };
511
- writeState(ctx.paths.stateFile, state, 'test');
512
- // Mock card with COMPLETED-integrate label — signals Claude declared completion
513
- taskBackend.getBySeq = vi.fn().mockResolvedValue(makeCard('1', 'Review', { labels: ['COMPLETED-integrate'] }));
514
- const runtimeStore = new RuntimeStore({
515
- paths: { stateFile: ctx.paths.stateFile },
516
- maxWorkers: config.MAX_CONCURRENT_WORKERS,
517
- });
518
- const handler = new SPSEventHandler({
519
- taskBackend,
520
- runtimeStore,
521
- project: 'test-project',
522
- pipelineAdapter: adapter,
523
- });
524
- // Simulate a completed integration event
525
- handler.handle({
526
- type: 'run.completed',
527
- taskId: '1',
528
- cardId: '1',
529
- workerId: 'test-project:worker-0:1',
530
- timestamp: new Date().toISOString(),
531
- phase: 'integration',
532
- slot: 'worker-0',
533
- project: 'test-project',
534
- state: 'completed',
535
- exitCode: 0,
536
- completionResult: { status: 'completed', reason: 'already_merged' },
537
- });
538
- // Allow async handlers to complete
539
- await new Promise(r => setTimeout(r, 100));
540
- // Should move to 'Shipped' (custom done), not 'Done'
541
- expect(taskBackend.move).toHaveBeenCalledWith('1', 'Shipped');
542
- });
543
- it('onCompleted moves to custom review state for development', async () => {
544
- const config = makeConfig({ PROJECT_DIR: tempDir });
545
- const ctx = makeCtx(tempDir, config);
546
- const state = makeDefaultState(2);
547
- state.leases['1'] = {
548
- seq: 1,
549
- pmStateObserved: 'Working',
550
- phase: 'coding',
551
- slot: 'worker-0',
552
- branch: 'feature/1-test',
553
- worktree: '/tmp/wt-1',
554
- sessionId: null,
555
- runId: null,
556
- claimedAt: new Date().toISOString(),
557
- retryCount: 0,
558
- lastTransitionAt: new Date().toISOString(),
559
- };
560
- writeState(ctx.paths.stateFile, state, 'test');
561
- // Mock card with COMPLETED-develop label
562
- taskBackend.getBySeq = vi.fn().mockResolvedValue(makeCard('1', 'Working', { labels: ['COMPLETED-develop'] }));
563
- const runtimeStore = new RuntimeStore({
564
- paths: { stateFile: ctx.paths.stateFile },
565
- maxWorkers: config.MAX_CONCURRENT_WORKERS,
566
- });
567
- const handler = new SPSEventHandler({
568
- taskBackend,
569
- runtimeStore,
570
- project: 'test-project',
571
- pipelineAdapter: adapter,
572
- });
573
- // Simulate a completed development event
574
- handler.handle({
575
- type: 'run.completed',
576
- taskId: '1',
577
- cardId: '1',
578
- workerId: 'test-project:worker-0:1',
579
- timestamp: new Date().toISOString(),
580
- phase: 'development',
581
- slot: 'worker-0',
582
- project: 'test-project',
583
- state: 'completed',
584
- exitCode: 0,
585
- completionResult: { status: 'completed', reason: 'branch_pushed' },
586
- });
587
- // Allow async handlers to complete
588
- await new Promise(r => setTimeout(r, 100));
589
- // Should move to 'Review' (custom review), not 'QA'
590
- expect(taskBackend.move).toHaveBeenCalledWith('1', 'Review');
591
- });
592
- it('releaseSlot sets custom review state in pmStateObserved', async () => {
593
- const config = makeConfig({ PROJECT_DIR: tempDir });
594
- const ctx = makeCtx(tempDir, config);
595
- const state = makeDefaultState(2);
596
- state.workers['worker-0'] = {
597
- ...createIdleWorkerSlot(),
598
- status: 'active',
599
- seq: 1,
600
- branch: 'feature/1-test',
601
- worktree: '/tmp/wt-1',
602
- claimedAt: new Date().toISOString(),
603
- lastHeartbeat: null,
604
- };
605
- state.activeCards['1'] = {
606
- seq: 1,
607
- state: 'Working',
608
- worker: 'worker-0',
609
- mrUrl: null,
610
- conflictDomains: [],
611
- startedAt: new Date().toISOString(),
612
- };
613
- state.leases['1'] = {
614
- seq: 1,
615
- pmStateObserved: 'Working',
616
- phase: 'coding',
617
- slot: 'worker-0',
618
- branch: 'feature/1-test',
619
- worktree: '/tmp/wt-1',
620
- sessionId: null,
621
- runId: null,
622
- claimedAt: new Date().toISOString(),
623
- retryCount: 0,
624
- lastTransitionAt: new Date().toISOString(),
625
- };
626
- writeState(ctx.paths.stateFile, state, 'test');
627
- const runtimeStore = new RuntimeStore({
628
- paths: { stateFile: ctx.paths.stateFile },
629
- maxWorkers: config.MAX_CONCURRENT_WORKERS,
630
- });
631
- const handler = new SPSEventHandler({
632
- taskBackend,
633
- runtimeStore,
634
- project: 'test-project',
635
- pipelineAdapter: adapter,
636
- });
637
- // Simulate development completion
638
- handler.handle({
639
- type: 'run.completed',
640
- taskId: '1',
641
- cardId: '1',
642
- workerId: 'test-project:worker-0:1',
643
- timestamp: new Date().toISOString(),
644
- phase: 'development',
645
- slot: 'worker-0',
646
- project: 'test-project',
647
- state: 'completed',
648
- exitCode: 0,
649
- completionResult: { status: 'completed', reason: 'branch_pushed' },
650
- });
651
- await new Promise(r => setTimeout(r, 100));
652
- // Verify runtime state has custom review state
653
- const freshState = runtimeStore.readState();
654
- if (freshState.leases['1']) {
655
- expect(freshState.leases['1'].pmStateObserved).toBe('Review');
656
- }
657
- });
658
- });
659
- describe('Full pipeline flow with custom states (dry-run)', () => {
660
- let tempDir;
661
- let taskBackend;
662
- let repoBackend;
663
- let workerManager;
664
- let adapter;
665
- beforeEach(() => {
666
- tempDir = makeTempDir();
667
- taskBackend = makeTaskBackend();
668
- repoBackend = makeRepoBackend();
669
- workerManager = makeWorkerManager();
670
- const config = makeConfig({ PROJECT_DIR: tempDir });
671
- adapter = setupAdapterWithCustomYaml(tempDir, config);
672
- });
673
- afterEach(() => { rmSync(tempDir, { recursive: true, force: true }); });
674
- it('never uses default state names when custom YAML is loaded', async () => {
675
- const config = makeConfig({ PROJECT_DIR: tempDir });
676
- const ctx = makeCtx(tempDir, config);
677
- const state = makeDefaultState(2);
678
- writeState(ctx.paths.stateFile, state, 'test');
679
- const DEFAULTS = ['Planning', 'Backlog', 'Todo', 'Inprogress', 'QA', 'Done'];
680
- // Run all engines in sequence
681
- const scheduler = new SchedulerEngine(ctx, taskBackend, adapter);
682
- await scheduler.tick();
683
- // Run all stage engines
684
- for (let i = 0; i < adapter.stages.length; i++) {
685
- const stageEngine = new StageEngine(ctx, adapter.stages[i], i, adapter.stages.length, taskBackend, repoBackend, workerManager, adapter);
686
- await stageEngine.tick();
687
- }
688
- const supervisor = makeSupervisor();
689
- const monitor = new MonitorEngine(ctx, taskBackend, repoBackend, undefined, supervisor, adapter, makeWorkerManagerStub());
690
- await monitor.tick();
691
- // Collect all calls to listByState and move
692
- const listCalls = taskBackend.listByState.mock.calls.map(c => c[0]);
693
- const moveCalls = taskBackend.move.mock.calls.map(c => c[1]);
694
- const allStateCalls = [...listCalls, ...moveCalls];
695
- // No call should use any default state name
696
- for (const call of allStateCalls) {
697
- expect(DEFAULTS).not.toContain(call);
698
- }
699
- // Should use custom state names instead
700
- expect(listCalls).toContain('Planned'); // Scheduler
701
- expect(listCalls).toContain('Queue'); // StageEngine[0] (backlog)
702
- expect(listCalls).toContain('Ready'); // StageEngine[0] (ready)
703
- expect(listCalls).toContain('Working'); // StageEngine[0] (active) + Monitor
704
- expect(listCalls).toContain('Review'); // StageEngine[1] (trigger) + Monitor
705
- });
706
- });
707
- //# sourceMappingURL=engine-pipeline-adapter.test.js.map