@aion0/forge 0.4.16 → 0.5.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/README.md +1 -1
- package/RELEASE_NOTES.md +170 -14
- package/app/api/agents/route.ts +17 -0
- package/app/api/delivery/[id]/route.ts +62 -0
- package/app/api/delivery/route.ts +40 -0
- package/app/api/mobile-chat/route.ts +13 -7
- package/app/api/monitor/route.ts +10 -6
- package/app/api/pipelines/[id]/route.ts +16 -3
- package/app/api/tasks/route.ts +2 -1
- package/app/api/workspace/[id]/agents/route.ts +35 -0
- package/app/api/workspace/[id]/memory/route.ts +23 -0
- package/app/api/workspace/[id]/smith/route.ts +22 -0
- package/app/api/workspace/[id]/stream/route.ts +28 -0
- package/app/api/workspace/route.ts +100 -0
- package/app/global-error.tsx +10 -4
- package/app/icon.ico +0 -0
- package/app/layout.tsx +2 -2
- package/app/login/LoginForm.tsx +96 -0
- package/app/login/page.tsx +7 -98
- package/app/page.tsx +2 -2
- package/bin/forge-server.mjs +13 -1
- package/check-forge-status.sh +9 -0
- package/components/ConversationEditor.tsx +411 -0
- package/components/ConversationGraphView.tsx +347 -0
- package/components/ConversationTerminalView.tsx +303 -0
- package/components/Dashboard.tsx +36 -39
- package/components/DashboardWrapper.tsx +9 -0
- package/components/DeliveryFlowEditor.tsx +491 -0
- package/components/DeliveryList.tsx +230 -0
- package/components/DeliveryWorkspace.tsx +589 -0
- package/components/DocTerminal.tsx +10 -2
- package/components/DocsViewer.tsx +10 -2
- package/components/HelpTerminal.tsx +11 -6
- package/components/InlinePipelineView.tsx +111 -0
- package/components/MobileView.tsx +20 -0
- package/components/MonitorPanel.tsx +9 -4
- package/components/NewTaskModal.tsx +32 -0
- package/components/PipelineEditor.tsx +49 -6
- package/components/PipelineView.tsx +482 -64
- package/components/ProjectDetail.tsx +314 -56
- package/components/ProjectManager.tsx +49 -4
- package/components/SessionView.tsx +27 -13
- package/components/SettingsModal.tsx +790 -124
- package/components/SkillsPanel.tsx +31 -8
- package/components/TaskBoard.tsx +3 -0
- package/components/WebTerminal.tsx +257 -43
- package/components/WorkspaceTree.tsx +221 -0
- package/components/WorkspaceView.tsx +2224 -0
- package/install.sh +2 -2
- package/lib/agents/claude-adapter.ts +104 -0
- package/lib/agents/generic-adapter.ts +64 -0
- package/lib/agents/index.ts +242 -0
- package/lib/agents/types.ts +70 -0
- package/lib/artifacts.ts +106 -0
- package/lib/delivery.ts +787 -0
- package/lib/forge-skills/forge-inbox.md +37 -0
- package/lib/forge-skills/forge-send.md +40 -0
- package/lib/forge-skills/forge-status.md +32 -0
- package/lib/forge-skills/forge-workspace-sync.md +37 -0
- package/lib/help-docs/00-overview.md +7 -1
- package/lib/help-docs/01-settings.md +159 -2
- package/lib/help-docs/05-pipelines.md +89 -0
- package/lib/help-docs/07-projects.md +35 -1
- package/lib/help-docs/11-workspace.md +204 -0
- package/lib/help-docs/CLAUDE.md +5 -2
- package/lib/init.ts +60 -10
- package/lib/pipeline.ts +537 -1
- package/lib/settings.ts +115 -22
- package/lib/skills.ts +249 -372
- package/lib/task-manager.ts +113 -33
- package/lib/telegram-bot.ts +33 -1
- package/lib/workspace/__tests__/state-machine.test.ts +388 -0
- package/lib/workspace/__tests__/workspace.test.ts +311 -0
- package/lib/workspace/agent-bus.ts +416 -0
- package/lib/workspace/agent-worker.ts +667 -0
- package/lib/workspace/backends/api-backend.ts +262 -0
- package/lib/workspace/backends/cli-backend.ts +479 -0
- package/lib/workspace/index.ts +82 -0
- package/lib/workspace/manager.ts +136 -0
- package/lib/workspace/orchestrator.ts +1804 -0
- package/lib/workspace/persistence.ts +310 -0
- package/lib/workspace/presets.ts +170 -0
- package/lib/workspace/skill-installer.ts +188 -0
- package/lib/workspace/smith-memory.ts +498 -0
- package/lib/workspace/types.ts +231 -0
- package/lib/workspace/watch-manager.ts +288 -0
- package/lib/workspace-standalone.ts +790 -0
- package/middleware.ts +1 -0
- package/package.json +4 -1
- package/src/config/index.ts +12 -1
- package/src/core/db/database.ts +1 -0
- package/start.sh +7 -0
|
@@ -0,0 +1,388 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* State Machine Tests — verify smith/task status transitions and message flow.
|
|
3
|
+
*
|
|
4
|
+
* Usage: npx tsx lib/workspace/__tests__/state-machine.test.ts
|
|
5
|
+
*/
|
|
6
|
+
|
|
7
|
+
import assert from 'node:assert';
|
|
8
|
+
import { WorkspaceOrchestrator } from '../orchestrator';
|
|
9
|
+
import { AgentBus } from '../agent-bus';
|
|
10
|
+
import type { WorkspaceAgentConfig, AgentState, BusMessage, WorkerEvent } from '../types';
|
|
11
|
+
|
|
12
|
+
const TEST_WS = 'test-sm-' + Date.now();
|
|
13
|
+
const TEST_PATH = '/tmp/test-sm';
|
|
14
|
+
|
|
15
|
+
// ─── Helpers ─────────────────────────────────────────────
|
|
16
|
+
|
|
17
|
+
function createOrch(): WorkspaceOrchestrator {
|
|
18
|
+
return new WorkspaceOrchestrator(TEST_WS, TEST_PATH, 'test');
|
|
19
|
+
}
|
|
20
|
+
|
|
21
|
+
function addInput(orch: WorkspaceOrchestrator, id = 'input-1'): void {
|
|
22
|
+
orch.addAgent({
|
|
23
|
+
id, label: 'Requirements', icon: '📝', type: 'input',
|
|
24
|
+
content: '', entries: [], role: '', backend: 'cli',
|
|
25
|
+
dependsOn: [], outputs: [], steps: [],
|
|
26
|
+
});
|
|
27
|
+
}
|
|
28
|
+
|
|
29
|
+
function addAgent(orch: WorkspaceOrchestrator, id: string, label: string, dependsOn: string[]): void {
|
|
30
|
+
orch.addAgent({
|
|
31
|
+
id, label, icon: '🤖', role: 'test', backend: 'cli',
|
|
32
|
+
dependsOn, outputs: [], workDir: id, // unique workDir to avoid conflicts
|
|
33
|
+
steps: [{ id: 's1', label: 'Step 1', prompt: 'do something' }],
|
|
34
|
+
});
|
|
35
|
+
}
|
|
36
|
+
|
|
37
|
+
function getState(orch: WorkspaceOrchestrator, id: string): AgentState {
|
|
38
|
+
return orch.getAllAgentStates()[id];
|
|
39
|
+
}
|
|
40
|
+
|
|
41
|
+
function collectEvents(orch: WorkspaceOrchestrator): any[] {
|
|
42
|
+
const events: any[] = [];
|
|
43
|
+
orch.on('event', (e: any) => events.push(e));
|
|
44
|
+
return events;
|
|
45
|
+
}
|
|
46
|
+
|
|
47
|
+
let passed = 0;
|
|
48
|
+
let failed = 0;
|
|
49
|
+
let testNum = 0;
|
|
50
|
+
|
|
51
|
+
function ok(condition: boolean, msg: string) {
|
|
52
|
+
if (condition) {
|
|
53
|
+
console.log(` ✅ ${msg}`);
|
|
54
|
+
passed++;
|
|
55
|
+
} else {
|
|
56
|
+
console.log(` ❌ ${msg}`);
|
|
57
|
+
failed++;
|
|
58
|
+
}
|
|
59
|
+
}
|
|
60
|
+
|
|
61
|
+
function test(name: string, fn: () => void | Promise<void>) {
|
|
62
|
+
testNum++;
|
|
63
|
+
console.log(`\n📋 Test ${testNum}: ${name}`);
|
|
64
|
+
try {
|
|
65
|
+
const result = fn();
|
|
66
|
+
if (result instanceof Promise) {
|
|
67
|
+
return result.catch(e => {
|
|
68
|
+
console.log(` 💥 Crashed: ${e.message}`);
|
|
69
|
+
failed++;
|
|
70
|
+
});
|
|
71
|
+
}
|
|
72
|
+
} catch (e: any) {
|
|
73
|
+
console.log(` 💥 Crashed: ${e.message}`);
|
|
74
|
+
failed++;
|
|
75
|
+
}
|
|
76
|
+
}
|
|
77
|
+
|
|
78
|
+
// ─── Tests ───────────────────────────────────────────────
|
|
79
|
+
|
|
80
|
+
async function runAll() {
|
|
81
|
+
console.log('🧪 State Machine Tests\n');
|
|
82
|
+
|
|
83
|
+
// Test 1: Initial state
|
|
84
|
+
await test('Initial agent state', () => {
|
|
85
|
+
const orch = createOrch();
|
|
86
|
+
addAgent(orch, 'a1', 'Agent1', []);
|
|
87
|
+
const s = getState(orch, 'a1');
|
|
88
|
+
ok(s.smithStatus === 'down', `smithStatus = down (got ${s.smithStatus})`);
|
|
89
|
+
ok(s.taskStatus === 'idle', `taskStatus = idle (got ${s.taskStatus})`);
|
|
90
|
+
ok(s.mode === 'auto', `mode = auto (got ${s.mode})`);
|
|
91
|
+
});
|
|
92
|
+
|
|
93
|
+
// Test 2: startDaemon sets all smiths to active
|
|
94
|
+
await test('startDaemon sets smithStatus=active', async () => {
|
|
95
|
+
const orch = createOrch();
|
|
96
|
+
addInput(orch);
|
|
97
|
+
addAgent(orch, 'pm', 'PM', ['input-1']);
|
|
98
|
+
addAgent(orch, 'eng', 'Engineer', ['pm']);
|
|
99
|
+
|
|
100
|
+
ok(getState(orch, 'pm').smithStatus === 'down', 'PM starts down');
|
|
101
|
+
ok(getState(orch, 'eng').smithStatus === 'down', 'Engineer starts down');
|
|
102
|
+
|
|
103
|
+
await orch.startDaemon();
|
|
104
|
+
|
|
105
|
+
ok(getState(orch, 'pm').smithStatus === 'active', 'PM active after startDaemon');
|
|
106
|
+
ok(getState(orch, 'eng').smithStatus === 'active', 'Engineer active after startDaemon');
|
|
107
|
+
ok(orch.isDaemonActive(), 'daemonActive = true');
|
|
108
|
+
|
|
109
|
+
orch.stopDaemon();
|
|
110
|
+
ok(getState(orch, 'pm').smithStatus === 'down', 'PM down after stopDaemon');
|
|
111
|
+
ok(getState(orch, 'eng').smithStatus === 'down', 'Engineer down after stopDaemon');
|
|
112
|
+
ok(!orch.isDaemonActive(), 'daemonActive = false');
|
|
113
|
+
});
|
|
114
|
+
|
|
115
|
+
// Test 3: stopDaemon preserves taskStatus
|
|
116
|
+
await test('stopDaemon preserves taskStatus', async () => {
|
|
117
|
+
const orch = createOrch();
|
|
118
|
+
addAgent(orch, 'a1', 'Agent1', []);
|
|
119
|
+
|
|
120
|
+
// Manually set to done to simulate completed agent
|
|
121
|
+
const states = orch.getAllAgentStates();
|
|
122
|
+
(states['a1'] as any).taskStatus = 'done';
|
|
123
|
+
// Hack: directly modify internal state for testing
|
|
124
|
+
(orch as any).agents.get('a1').state.taskStatus = 'done';
|
|
125
|
+
|
|
126
|
+
await orch.startDaemon();
|
|
127
|
+
ok(getState(orch, 'a1').taskStatus === 'done', 'taskStatus stays done after startDaemon');
|
|
128
|
+
|
|
129
|
+
orch.stopDaemon();
|
|
130
|
+
ok(getState(orch, 'a1').taskStatus === 'done', 'taskStatus stays done after stopDaemon');
|
|
131
|
+
});
|
|
132
|
+
|
|
133
|
+
// Test 4: loadSnapshot resets smith to down and pending messages to failed
|
|
134
|
+
await test('loadSnapshot resets state correctly', () => {
|
|
135
|
+
const orch = createOrch();
|
|
136
|
+
|
|
137
|
+
const busLog: BusMessage[] = [
|
|
138
|
+
{ id: 'm1', from: 'a', to: 'b', type: 'notify', payload: { action: 'test' }, timestamp: Date.now(), status: 'pending' },
|
|
139
|
+
{ id: 'm2', from: 'a', to: 'b', type: 'notify', payload: { action: 'test2' }, timestamp: Date.now(), status: 'done' },
|
|
140
|
+
{ id: 'm3', from: 'a', to: 'b', type: 'notify', payload: { action: 'test3' }, timestamp: Date.now(), status: 'pending' },
|
|
141
|
+
];
|
|
142
|
+
|
|
143
|
+
orch.loadSnapshot({
|
|
144
|
+
agents: [{
|
|
145
|
+
id: 'a1', label: 'Agent1', icon: '🤖', role: '', backend: 'cli',
|
|
146
|
+
dependsOn: [], outputs: [], workDir: 'a1',
|
|
147
|
+
steps: [{ id: 's1', label: 'Step1', prompt: 'test' }],
|
|
148
|
+
}],
|
|
149
|
+
agentStates: {
|
|
150
|
+
'a1': {
|
|
151
|
+
smithStatus: 'active', mode: 'auto', taskStatus: 'running',
|
|
152
|
+
history: [], artifacts: [],
|
|
153
|
+
} as AgentState,
|
|
154
|
+
},
|
|
155
|
+
busLog,
|
|
156
|
+
});
|
|
157
|
+
|
|
158
|
+
const s = getState(orch, 'a1');
|
|
159
|
+
ok(s.smithStatus === 'down', 'smithStatus reset to down after load');
|
|
160
|
+
ok(s.taskStatus === 'failed', 'running taskStatus becomes failed after load');
|
|
161
|
+
|
|
162
|
+
// Check bus messages
|
|
163
|
+
const log = orch.getBusLog();
|
|
164
|
+
const m1 = log.find(m => m.id === 'm1');
|
|
165
|
+
const m2 = log.find(m => m.id === 'm2');
|
|
166
|
+
const m3 = log.find(m => m.id === 'm3');
|
|
167
|
+
ok(m1?.status === 'failed', 'pending message m1 marked failed');
|
|
168
|
+
ok(m2?.status === 'done', 'acked message m2 unchanged');
|
|
169
|
+
ok(m3?.status === 'failed', 'pending message m3 marked failed');
|
|
170
|
+
});
|
|
171
|
+
|
|
172
|
+
// Test 5: completeInput sends input_updated messages (no daemon to avoid CLI spawn)
|
|
173
|
+
await test('completeInput sends bus messages to downstream', () => {
|
|
174
|
+
const orch = createOrch();
|
|
175
|
+
addInput(orch);
|
|
176
|
+
addAgent(orch, 'pm', 'PM', ['input-1']);
|
|
177
|
+
|
|
178
|
+
// Don't start daemon — just test that completeInput sends bus messages
|
|
179
|
+
const busLog = orch.getBusLog();
|
|
180
|
+
const before = busLog.length;
|
|
181
|
+
|
|
182
|
+
orch.completeInput('input-1', 'Build a todo app');
|
|
183
|
+
|
|
184
|
+
const after = busLog.length;
|
|
185
|
+
ok(after > before, `New bus messages sent (${before} → ${after})`);
|
|
186
|
+
|
|
187
|
+
const inputMsgs = busLog.filter(m => m.payload.action === 'input_updated');
|
|
188
|
+
ok(inputMsgs.length > 0, 'input_updated message found');
|
|
189
|
+
ok(inputMsgs[0].from === 'input-1', 'from = input-1');
|
|
190
|
+
ok(inputMsgs[0].to === 'pm', 'to = pm');
|
|
191
|
+
// Smith is down so message stays pending (not acked)
|
|
192
|
+
ok(inputMsgs[0].status === 'pending', `message pending (smith down) — got ${inputMsgs[0].status}`);
|
|
193
|
+
});
|
|
194
|
+
|
|
195
|
+
// Test 6: broadcastCompletion sends upstream_complete (no daemon)
|
|
196
|
+
await test('broadcastCompletion sends upstream_complete', () => {
|
|
197
|
+
const orch = createOrch();
|
|
198
|
+
addInput(orch);
|
|
199
|
+
addAgent(orch, 'pm', 'PM', ['input-1']);
|
|
200
|
+
addAgent(orch, 'eng', 'Engineer', ['pm']);
|
|
201
|
+
|
|
202
|
+
// Set PM to done with artifacts
|
|
203
|
+
(orch as any).agents.get('pm').state.taskStatus = 'done';
|
|
204
|
+
(orch as any).agents.get('pm').state.artifacts = [{ type: 'file', path: 'docs/prd.md' }];
|
|
205
|
+
(orch as any).agents.get('pm').state.history = [
|
|
206
|
+
{ type: 'result', subtype: 'final_summary', content: 'PRD completed', timestamp: new Date().toISOString() }
|
|
207
|
+
];
|
|
208
|
+
|
|
209
|
+
(orch as any).broadcastCompletion('pm');
|
|
210
|
+
|
|
211
|
+
const busLog = orch.getBusLog();
|
|
212
|
+
const upstreamMsgs = busLog.filter(m => m.payload.action === 'upstream_complete' && m.from === 'pm');
|
|
213
|
+
ok(upstreamMsgs.length > 0, 'upstream_complete message sent');
|
|
214
|
+
ok(upstreamMsgs[0].to === 'eng', 'sent to Engineer');
|
|
215
|
+
ok(upstreamMsgs[0].payload.files?.includes('docs/prd.md') === true, 'includes file path');
|
|
216
|
+
});
|
|
217
|
+
|
|
218
|
+
// Test 7: Bus message ACK flow
|
|
219
|
+
await test('Bus message ACK: pending → acked on success, pending → failed on error', () => {
|
|
220
|
+
const bus = new AgentBus();
|
|
221
|
+
bus.setAgentStatus('a', 'alive');
|
|
222
|
+
bus.setAgentStatus('b', 'alive');
|
|
223
|
+
|
|
224
|
+
const msg = bus.send('a', 'b', 'notify', { action: 'test', content: 'hello' });
|
|
225
|
+
ok(msg.status === 'pending', 'starts as pending');
|
|
226
|
+
|
|
227
|
+
// Simulate ack
|
|
228
|
+
msg.status = 'done';
|
|
229
|
+
ok(msg.status === 'done', 'acked after processing');
|
|
230
|
+
|
|
231
|
+
// Test failed path
|
|
232
|
+
const msg2 = bus.send('a', 'b', 'notify', { action: 'test2', content: 'world' });
|
|
233
|
+
msg2.status = 'failed';
|
|
234
|
+
ok(msg2.status === 'failed', 'failed on error');
|
|
235
|
+
|
|
236
|
+
// Test retry
|
|
237
|
+
const retried = bus.retryMessage(msg2.id);
|
|
238
|
+
ok(retried !== null, 'retryMessage returns message');
|
|
239
|
+
ok(retried!.status === 'pending', 'retried message back to pending');
|
|
240
|
+
});
|
|
241
|
+
|
|
242
|
+
// Test 8: markAllPendingAsFailed
|
|
243
|
+
await test('markAllPendingAsFailed on restart', () => {
|
|
244
|
+
const bus = new AgentBus();
|
|
245
|
+
bus.setAgentStatus('a', 'alive');
|
|
246
|
+
bus.setAgentStatus('b', 'alive');
|
|
247
|
+
|
|
248
|
+
const m1 = bus.send('a', 'b', 'notify', { action: 'test1' });
|
|
249
|
+
const m2 = bus.send('a', 'b', 'notify', { action: 'test2' });
|
|
250
|
+
m1.status = 'done'; // already processed
|
|
251
|
+
// m2 stays pending
|
|
252
|
+
|
|
253
|
+
bus.markAllPendingAsFailed();
|
|
254
|
+
|
|
255
|
+
ok(m1.status === 'done', 'acked message unchanged');
|
|
256
|
+
ok(m2.status === 'failed', 'pending message marked failed');
|
|
257
|
+
});
|
|
258
|
+
|
|
259
|
+
// Test 9: Manual run (force) dep check logic — test validateCanRun vs force dep check
|
|
260
|
+
await test('Force run checks smith active, normal run checks taskStatus done', () => {
|
|
261
|
+
const orch = createOrch();
|
|
262
|
+
addInput(orch);
|
|
263
|
+
addAgent(orch, 'pm', 'PM', ['input-1']);
|
|
264
|
+
addAgent(orch, 'eng', 'Engineer', ['pm']);
|
|
265
|
+
|
|
266
|
+
// Normal validateCanRun: PM idle → should fail
|
|
267
|
+
let error1 = '';
|
|
268
|
+
try { orch.validateCanRun('eng'); } catch (e: any) { error1 = e.message; }
|
|
269
|
+
ok(error1.includes('not completed'), `Normal: rejects when PM idle`);
|
|
270
|
+
|
|
271
|
+
// PM done → should pass
|
|
272
|
+
(orch as any).agents.get('pm').state.taskStatus = 'done';
|
|
273
|
+
let error2 = '';
|
|
274
|
+
try { orch.validateCanRun('eng'); } catch (e: any) { error2 = e.message; }
|
|
275
|
+
ok(!error2, `Normal: passes when PM done`);
|
|
276
|
+
|
|
277
|
+
// Force dep check: PM smith down → should fail
|
|
278
|
+
(orch as any).agents.get('pm').state.taskStatus = 'idle';
|
|
279
|
+
(orch as any).agents.get('pm').state.smithStatus = 'down';
|
|
280
|
+
// Simulate the force dep check from runAgentDaemon
|
|
281
|
+
const config = (orch as any).agents.get('eng').config;
|
|
282
|
+
let forceError = '';
|
|
283
|
+
for (const depId of config.dependsOn) {
|
|
284
|
+
const dep = (orch as any).agents.get(depId);
|
|
285
|
+
if (dep && dep.config.type !== 'input' && dep.state.smithStatus !== 'active') {
|
|
286
|
+
forceError = `${dep.config.label} smith not active`;
|
|
287
|
+
}
|
|
288
|
+
}
|
|
289
|
+
ok(forceError.includes('not active'), `Force: rejects when PM smith down`);
|
|
290
|
+
|
|
291
|
+
// PM smith active → should pass
|
|
292
|
+
(orch as any).agents.get('pm').state.smithStatus = 'active';
|
|
293
|
+
forceError = '';
|
|
294
|
+
for (const depId of config.dependsOn) {
|
|
295
|
+
const dep = (orch as any).agents.get(depId);
|
|
296
|
+
if (dep && dep.config.type !== 'input' && dep.state.smithStatus !== 'active') {
|
|
297
|
+
forceError = `${dep.config.label} smith not active`;
|
|
298
|
+
}
|
|
299
|
+
}
|
|
300
|
+
ok(!forceError, `Force: passes when PM smith active`);
|
|
301
|
+
});
|
|
302
|
+
|
|
303
|
+
// Test 10: Auto trigger requires taskStatus=done (check via validateCanRun)
|
|
304
|
+
await test('Auto trigger requires dep taskStatus=done', () => {
|
|
305
|
+
const orch = createOrch();
|
|
306
|
+
addInput(orch);
|
|
307
|
+
addAgent(orch, 'pm', 'PM', ['input-1']);
|
|
308
|
+
addAgent(orch, 'eng', 'Engineer', ['pm']);
|
|
309
|
+
|
|
310
|
+
// PM is idle — validateCanRun for Engineer should fail
|
|
311
|
+
let error = '';
|
|
312
|
+
try {
|
|
313
|
+
orch.validateCanRun('eng');
|
|
314
|
+
} catch (e: any) {
|
|
315
|
+
error = e.message;
|
|
316
|
+
}
|
|
317
|
+
ok(error.includes('not completed'), `validateCanRun rejects when PM idle: ${error}`);
|
|
318
|
+
|
|
319
|
+
// Set PM to done — should pass
|
|
320
|
+
(orch as any).agents.get('pm').state.taskStatus = 'done';
|
|
321
|
+
let error2 = '';
|
|
322
|
+
try {
|
|
323
|
+
orch.validateCanRun('eng');
|
|
324
|
+
} catch (e: any) {
|
|
325
|
+
error2 = e.message;
|
|
326
|
+
}
|
|
327
|
+
ok(!error2, `validateCanRun passes when PM done (got: ${error2 || 'no error'})`);
|
|
328
|
+
});
|
|
329
|
+
|
|
330
|
+
// Test 11: taskStatus transitions - never goes from done to idle
|
|
331
|
+
await test('taskStatus never goes from done→idle', async () => {
|
|
332
|
+
const orch = createOrch();
|
|
333
|
+
addAgent(orch, 'a1', 'Agent1', []);
|
|
334
|
+
|
|
335
|
+
// Set to done
|
|
336
|
+
(orch as any).agents.get('a1').state.taskStatus = 'done';
|
|
337
|
+
(orch as any).agents.get('a1').state.smithStatus = 'active';
|
|
338
|
+
|
|
339
|
+
// Stop daemon should NOT change taskStatus
|
|
340
|
+
await orch.startDaemon();
|
|
341
|
+
orch.stopDaemon();
|
|
342
|
+
|
|
343
|
+
ok(getState(orch, 'a1').taskStatus === 'done',
|
|
344
|
+
`taskStatus stays done after stop (got ${getState(orch, 'a1').taskStatus})`);
|
|
345
|
+
});
|
|
346
|
+
|
|
347
|
+
// Test 12: startDaemon doesn't re-send input messages
|
|
348
|
+
await test('startDaemon does NOT broadcast existing input', async () => {
|
|
349
|
+
const orch = createOrch();
|
|
350
|
+
addInput(orch);
|
|
351
|
+
addAgent(orch, 'pm', 'PM', ['input-1']);
|
|
352
|
+
|
|
353
|
+
// Complete input first
|
|
354
|
+
orch.completeInput('input-1', 'test content');
|
|
355
|
+
|
|
356
|
+
const busLogBefore = orch.getBusLog().length;
|
|
357
|
+
|
|
358
|
+
await orch.startDaemon();
|
|
359
|
+
|
|
360
|
+
const busLogAfter = orch.getBusLog().length;
|
|
361
|
+
const newMsgs = orch.getBusLog().slice(busLogBefore);
|
|
362
|
+
const inputMsgs = newMsgs.filter(m => m.payload.action === 'input_updated');
|
|
363
|
+
|
|
364
|
+
ok(inputMsgs.length === 0, `No input_updated sent by startDaemon (got ${inputMsgs.length})`);
|
|
365
|
+
|
|
366
|
+
orch.stopDaemon();
|
|
367
|
+
});
|
|
368
|
+
|
|
369
|
+
// ─── Summary ──────────────────────────────────────────
|
|
370
|
+
|
|
371
|
+
console.log('\n' + '═'.repeat(40));
|
|
372
|
+
console.log(`Results: ${passed} passed, ${failed} failed`);
|
|
373
|
+
console.log('═'.repeat(40));
|
|
374
|
+
|
|
375
|
+
console.log('');
|
|
376
|
+
if (failed > 0) {
|
|
377
|
+
process.exit(1);
|
|
378
|
+
} else {
|
|
379
|
+
process.exit(0);
|
|
380
|
+
}
|
|
381
|
+
}
|
|
382
|
+
|
|
383
|
+
// Force exit after 8s in case of dangling timers/workers
|
|
384
|
+
setTimeout(() => {
|
|
385
|
+
console.log('\n(Force exit due to dangling timers)');
|
|
386
|
+
process.exit(passed > 0 && failed === 0 ? 0 : 1);
|
|
387
|
+
}, 8000);
|
|
388
|
+
runAll();
|