@dotdrelle/wiki-manager 0.12.0 → 0.12.7
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/package.json +2 -2
- package/src/activity/activityAggregator.js +34 -3
- package/src/activity/activityAggregator.test.js +32 -0
- package/src/agent/graph.js +307 -29
- package/src/agent/graph.test.js +404 -1
- package/src/cli/wiki-manager.js +24 -1
- package/src/commands/slash.js +74 -1
- package/src/commands/slash.test.js +36 -0
- package/src/contracts/schemas.test.js +2 -2
- package/src/core/activity.js +4 -0
- package/src/core/activity.test.js +9 -1
- package/src/core/agentEvents.js +30 -0
- package/src/core/agentEvents.test.js +42 -1
- package/src/core/buildInfo.js +58 -0
- package/src/core/buildInfo.json +4 -0
- package/src/core/buildInfo.test.js +14 -0
- package/src/core/mcp.js +50 -3
- package/src/core/mcp.test.js +94 -1
- package/src/core/runtimeLog.js +6 -1
- package/src/core/runtimeLog.test.js +3 -1
- package/src/runtime/client.js +22 -0
- package/src/runtime/controlMessages.js +43 -0
- package/src/runtime/controlMessages.test.js +21 -0
- package/src/runtime/donna-contract.test.js +3 -3
- package/src/runtime/recoveryManager.js +54 -0
- package/src/runtime/recoveryManager.test.js +73 -0
- package/src/runtime/runner.js +49 -13
- package/src/runtime/runner.test.js +196 -0
- package/src/runtime/server.js +73 -13
- package/src/runtime/server.test.js +284 -67
- package/src/runtime/store.js +48 -3
- package/src/runtime/store.test.js +56 -24
- package/src/runtime/supervisor.js +77 -1
- package/src/shell/RightPane.tsx +124 -31
- package/src/shell/SetupWizard.tsx +13 -1
- package/src/shell/repl.js +81 -11
- package/src/shell/repl.test.js +115 -2
- package/src/shell/tui.tsx +4 -1
- package/src/shell/useAgent.ts +37 -4
- package/src/shell/useSession.ts +51 -10
|
@@ -224,15 +224,18 @@ test('runtime server accepts only one active run', async (t) => {
|
|
|
224
224
|
fetch(url, {
|
|
225
225
|
method: 'POST',
|
|
226
226
|
headers: { 'Content-Type': 'application/json' },
|
|
227
|
-
|
|
227
|
+
// Messages during an active run are now CLASSIFIED (observe answers
|
|
228
|
+
// immediately, cancel aborts…) — enqueueing a future run is the
|
|
229
|
+
// explicit intent.
|
|
230
|
+
body: JSON.stringify({ input: 'second', intent: 'enqueue' }),
|
|
228
231
|
}),
|
|
229
232
|
]);
|
|
230
233
|
|
|
231
234
|
assert.deepEqual([first.status, second.status], [202, 202]);
|
|
232
235
|
const bodies = [await first.json(), await second.json()];
|
|
233
|
-
const acceptedRun = bodies.find((body) => body.
|
|
234
|
-
const queuedRun = bodies.find((body) => body.
|
|
235
|
-
assert.equal(queuedRun.
|
|
236
|
+
const acceptedRun = bodies.find((body) => body.runId && body.accepted);
|
|
237
|
+
const queuedRun = bodies.find((body) => body.kind === 'enqueue_run');
|
|
238
|
+
assert.equal(queuedRun.accepted, true);
|
|
236
239
|
assert.equal(acceptedRun.accepted, true);
|
|
237
240
|
assert.match(acceptedRun.runId, /^[0-9a-f-]{36}$/);
|
|
238
241
|
assert.equal(runCount, 1);
|
|
@@ -271,7 +274,7 @@ test('runtime server returns the accepted run id and passes it to the runner', a
|
|
|
271
274
|
const response = await fetch(`http://127.0.0.1:${handle.port}/run`, {
|
|
272
275
|
method: 'POST',
|
|
273
276
|
headers: { 'Content-Type': 'application/json' },
|
|
274
|
-
body: JSON.stringify({ input: 'build', workspace: '
|
|
277
|
+
body: JSON.stringify({ input: 'build', workspace: 'acme', evaluate: false, replans: 1 }),
|
|
275
278
|
});
|
|
276
279
|
assert.equal(response.status, 202);
|
|
277
280
|
const body = await response.json();
|
|
@@ -279,7 +282,7 @@ test('runtime server returns the accepted run id and passes it to the runner', a
|
|
|
279
282
|
assert.match(body.runId, /^[0-9a-f-]{36}$/);
|
|
280
283
|
await new Promise((resolve) => setImmediate(resolve));
|
|
281
284
|
assert.equal(receivedBody.runId, body.runId);
|
|
282
|
-
assert.equal(receivedBody.workspace, '
|
|
285
|
+
assert.equal(receivedBody.workspace, 'acme');
|
|
283
286
|
assert.equal(receivedBody.evaluate, false);
|
|
284
287
|
assert.equal(receivedBody.replans, 1);
|
|
285
288
|
} finally {
|
|
@@ -287,11 +290,163 @@ test('runtime server returns the accepted run id and passes it to the runner', a
|
|
|
287
290
|
}
|
|
288
291
|
});
|
|
289
292
|
|
|
293
|
+
test('runtime server kill aborts active run and interrupts workspace work', async (t) => {
|
|
294
|
+
let abortSeen = false;
|
|
295
|
+
let cancelCalled = false;
|
|
296
|
+
let interruptArgs = null;
|
|
297
|
+
let taskCancelArgs = null;
|
|
298
|
+
let handle;
|
|
299
|
+
try {
|
|
300
|
+
handle = await startRuntimeServer({
|
|
301
|
+
host: '127.0.0.1',
|
|
302
|
+
port: 0,
|
|
303
|
+
store: {
|
|
304
|
+
dbPath: ':memory:',
|
|
305
|
+
getState: () => ({ status: 'running' }),
|
|
306
|
+
listEvents: () => [],
|
|
307
|
+
interruptRuns: (args) => {
|
|
308
|
+
interruptArgs = args;
|
|
309
|
+
return 1;
|
|
310
|
+
},
|
|
311
|
+
cancelActiveTasksForInterruptedRuns: (args) => {
|
|
312
|
+
taskCancelArgs = args;
|
|
313
|
+
return 3;
|
|
314
|
+
},
|
|
315
|
+
},
|
|
316
|
+
session: { controlQueue: [{ id: 'control-1', workspace: 'docs', status: 'queued' }] },
|
|
317
|
+
run: async (_context, _body, { signal }) => {
|
|
318
|
+
signal.addEventListener('abort', () => { abortSeen = true; }, { once: true });
|
|
319
|
+
await new Promise(() => {});
|
|
320
|
+
},
|
|
321
|
+
cancel: async () => {
|
|
322
|
+
cancelCalled = true;
|
|
323
|
+
},
|
|
324
|
+
});
|
|
325
|
+
} catch (err) {
|
|
326
|
+
if (err?.code === 'EPERM') {
|
|
327
|
+
t.skip('network listen is not permitted in this sandbox');
|
|
328
|
+
return;
|
|
329
|
+
}
|
|
330
|
+
throw err;
|
|
331
|
+
}
|
|
332
|
+
|
|
333
|
+
try {
|
|
334
|
+
const base = `http://127.0.0.1:${handle.port}`;
|
|
335
|
+
const runResponse = await fetch(`${base}/run`, {
|
|
336
|
+
method: 'POST',
|
|
337
|
+
headers: { 'Content-Type': 'application/json' },
|
|
338
|
+
body: JSON.stringify({ input: 'build', workspace: 'docs' }),
|
|
339
|
+
});
|
|
340
|
+
assert.equal(runResponse.status, 202);
|
|
341
|
+
|
|
342
|
+
const killResponse = await fetch(`${base}/kill?workspace=docs`, { method: 'POST' });
|
|
343
|
+
assert.equal(killResponse.status, 202);
|
|
344
|
+
assert.deepEqual(await killResponse.json(), {
|
|
345
|
+
killed: true,
|
|
346
|
+
workspace: 'docs',
|
|
347
|
+
runId: null,
|
|
348
|
+
runs: 1,
|
|
349
|
+
tasks: 3,
|
|
350
|
+
queued: 1,
|
|
351
|
+
});
|
|
352
|
+
assert.equal(abortSeen, true);
|
|
353
|
+
assert.equal(cancelCalled, true);
|
|
354
|
+
assert.equal(interruptArgs.workspace, 'docs');
|
|
355
|
+
assert.equal(interruptArgs.runId, null);
|
|
356
|
+
assert.deepEqual(taskCancelArgs, { workspace: 'docs', runId: null });
|
|
357
|
+
} finally {
|
|
358
|
+
await handle.close();
|
|
359
|
+
}
|
|
360
|
+
});
|
|
361
|
+
|
|
362
|
+
test('runtime server kill succeeds without an active run', async (t) => {
|
|
363
|
+
let handle;
|
|
364
|
+
try {
|
|
365
|
+
handle = await startRuntimeServer({
|
|
366
|
+
host: '127.0.0.1',
|
|
367
|
+
port: 0,
|
|
368
|
+
store: {
|
|
369
|
+
dbPath: ':memory:',
|
|
370
|
+
getState: () => ({ status: 'idle' }),
|
|
371
|
+
listEvents: () => [],
|
|
372
|
+
interruptRuns: () => 0,
|
|
373
|
+
cancelActiveTasksForInterruptedRuns: () => 0,
|
|
374
|
+
},
|
|
375
|
+
session: {},
|
|
376
|
+
run: async () => {},
|
|
377
|
+
});
|
|
378
|
+
} catch (err) {
|
|
379
|
+
if (err?.code === 'EPERM') {
|
|
380
|
+
t.skip('network listen is not permitted in this sandbox');
|
|
381
|
+
return;
|
|
382
|
+
}
|
|
383
|
+
throw err;
|
|
384
|
+
}
|
|
385
|
+
|
|
386
|
+
try {
|
|
387
|
+
const response = await fetch(`http://127.0.0.1:${handle.port}/kill?workspace=docs`, { method: 'POST' });
|
|
388
|
+
assert.equal(response.status, 202);
|
|
389
|
+
assert.deepEqual(await response.json(), {
|
|
390
|
+
killed: true,
|
|
391
|
+
workspace: 'docs',
|
|
392
|
+
runId: null,
|
|
393
|
+
runs: 0,
|
|
394
|
+
tasks: 0,
|
|
395
|
+
queued: 0,
|
|
396
|
+
});
|
|
397
|
+
} finally {
|
|
398
|
+
await handle.close();
|
|
399
|
+
}
|
|
400
|
+
});
|
|
401
|
+
|
|
402
|
+
test('runtime server kill can target a specific run id', async (t) => {
|
|
403
|
+
let interruptArgs = null;
|
|
404
|
+
let taskCancelArgs = null;
|
|
405
|
+
let handle;
|
|
406
|
+
try {
|
|
407
|
+
handle = await startRuntimeServer({
|
|
408
|
+
host: '127.0.0.1',
|
|
409
|
+
port: 0,
|
|
410
|
+
store: {
|
|
411
|
+
dbPath: ':memory:',
|
|
412
|
+
getState: () => ({ status: 'idle' }),
|
|
413
|
+
listEvents: () => [],
|
|
414
|
+
interruptRuns: (args) => {
|
|
415
|
+
interruptArgs = args;
|
|
416
|
+
return 1;
|
|
417
|
+
},
|
|
418
|
+
cancelActiveTasksForInterruptedRuns: (args) => {
|
|
419
|
+
taskCancelArgs = args;
|
|
420
|
+
return 2;
|
|
421
|
+
},
|
|
422
|
+
},
|
|
423
|
+
session: {},
|
|
424
|
+
run: async () => {},
|
|
425
|
+
});
|
|
426
|
+
} catch (err) {
|
|
427
|
+
if (err?.code === 'EPERM') {
|
|
428
|
+
t.skip('network listen is not permitted in this sandbox');
|
|
429
|
+
return;
|
|
430
|
+
}
|
|
431
|
+
throw err;
|
|
432
|
+
}
|
|
433
|
+
|
|
434
|
+
try {
|
|
435
|
+
const response = await fetch(`http://127.0.0.1:${handle.port}/kill?workspace=docs&runId=run-1`, { method: 'POST' });
|
|
436
|
+
assert.equal(response.status, 202);
|
|
437
|
+
assert.equal((await response.json()).runId, 'run-1');
|
|
438
|
+
assert.equal(interruptArgs.runId, 'run-1');
|
|
439
|
+
assert.deepEqual(taskCancelArgs, { workspace: 'docs', runId: 'run-1' });
|
|
440
|
+
} finally {
|
|
441
|
+
await handle.close();
|
|
442
|
+
}
|
|
443
|
+
});
|
|
444
|
+
|
|
290
445
|
test('runtime server state exposes active run identity while running', async (t) => {
|
|
291
446
|
let releaseRun;
|
|
292
447
|
const context = {
|
|
293
|
-
workspace: '
|
|
294
|
-
session: { workspace: '
|
|
448
|
+
workspace: 'acme',
|
|
449
|
+
session: { workspace: 'acme' },
|
|
295
450
|
running: false,
|
|
296
451
|
currentAbortController: null,
|
|
297
452
|
currentRunId: null,
|
|
@@ -321,18 +476,18 @@ test('runtime server state exposes active run identity while running', async (t)
|
|
|
321
476
|
}
|
|
322
477
|
|
|
323
478
|
try {
|
|
324
|
-
const runResponse = await fetch(`http://127.0.0.1:${handle.port}/run?workspace=
|
|
479
|
+
const runResponse = await fetch(`http://127.0.0.1:${handle.port}/run?workspace=acme`, {
|
|
325
480
|
method: 'POST',
|
|
326
481
|
headers: { 'Content-Type': 'application/json' },
|
|
327
482
|
body: JSON.stringify({ input: 'build' }),
|
|
328
483
|
});
|
|
329
484
|
acceptedRun = await runResponse.json();
|
|
330
|
-
const stateResponse = await fetch(`http://127.0.0.1:${handle.port}/state?workspace=
|
|
485
|
+
const stateResponse = await fetch(`http://127.0.0.1:${handle.port}/state?workspace=acme`);
|
|
331
486
|
const state = await stateResponse.json();
|
|
332
487
|
assert.equal(state.status, 'running');
|
|
333
488
|
assert.equal(state.running, true);
|
|
334
489
|
assert.equal(state.runId, acceptedRun.runId);
|
|
335
|
-
assert.equal(state.workspace, '
|
|
490
|
+
assert.equal(state.workspace, 'acme');
|
|
336
491
|
} finally {
|
|
337
492
|
releaseRun?.();
|
|
338
493
|
await handle.close();
|
|
@@ -381,8 +536,8 @@ test('runtime server isolates active runs by workspace', async (t) => {
|
|
|
381
536
|
|
|
382
537
|
try {
|
|
383
538
|
const url = `http://127.0.0.1:${handle.port}/run`;
|
|
384
|
-
const [
|
|
385
|
-
fetch(`${url}?workspace=
|
|
539
|
+
const [acme, docs] = await Promise.all([
|
|
540
|
+
fetch(`${url}?workspace=acme`, {
|
|
386
541
|
method: 'POST',
|
|
387
542
|
headers: { 'Content-Type': 'application/json' },
|
|
388
543
|
body: JSON.stringify({ input: 'first' }),
|
|
@@ -393,18 +548,18 @@ test('runtime server isolates active runs by workspace', async (t) => {
|
|
|
393
548
|
body: JSON.stringify({ input: 'second' }),
|
|
394
549
|
}),
|
|
395
550
|
]);
|
|
396
|
-
assert.deepEqual([
|
|
551
|
+
assert.deepEqual([acme.status, docs.status], [202, 202]);
|
|
397
552
|
|
|
398
|
-
const queued = await fetch(`${url}?workspace=
|
|
553
|
+
const queued = await fetch(`${url}?workspace=acme`, {
|
|
399
554
|
method: 'POST',
|
|
400
555
|
headers: { 'Content-Type': 'application/json' },
|
|
401
|
-
body: JSON.stringify({ input: 'third' }),
|
|
556
|
+
body: JSON.stringify({ input: 'third', intent: 'enqueue' }),
|
|
402
557
|
});
|
|
403
558
|
assert.equal(queued.status, 202);
|
|
404
|
-
assert.equal((await queued.json()).
|
|
405
|
-
assert.deepEqual(runWorkspaces.sort(), ['
|
|
559
|
+
assert.equal((await queued.json()).kind, 'enqueue_run');
|
|
560
|
+
assert.deepEqual(runWorkspaces.sort(), ['acme', 'docs']);
|
|
406
561
|
} finally {
|
|
407
|
-
releases.get('
|
|
562
|
+
releases.get('acme')?.();
|
|
408
563
|
releases.get('docs')?.();
|
|
409
564
|
await handle.close();
|
|
410
565
|
}
|
|
@@ -446,10 +601,10 @@ test('runtime server filters state and events by workspace', async (t) => {
|
|
|
446
601
|
}
|
|
447
602
|
|
|
448
603
|
try {
|
|
449
|
-
const state = await fetch(`http://127.0.0.1:${handle.port}/state?workspace=
|
|
604
|
+
const state = await fetch(`http://127.0.0.1:${handle.port}/state?workspace=acme`);
|
|
450
605
|
assert.equal(state.status, 200);
|
|
451
|
-
assert.equal((await state.json()).workspace, '
|
|
452
|
-
assert.equal(stateWorkspace, '
|
|
606
|
+
assert.equal((await state.json()).workspace, 'acme');
|
|
607
|
+
assert.equal(stateWorkspace, 'acme');
|
|
453
608
|
|
|
454
609
|
const events = await fetch(`http://127.0.0.1:${handle.port}/events?workspace=docs`);
|
|
455
610
|
assert.equal(events.status, 200);
|
|
@@ -533,15 +688,15 @@ test('runtime server exposes manual resume endpoint', async (t) => {
|
|
|
533
688
|
}
|
|
534
689
|
|
|
535
690
|
try {
|
|
536
|
-
const response = await fetch(`http://127.0.0.1:${handle.port}/resume?workspace=
|
|
691
|
+
const response = await fetch(`http://127.0.0.1:${handle.port}/resume?workspace=acme`, {
|
|
537
692
|
method: 'POST',
|
|
538
693
|
});
|
|
539
694
|
assert.equal(response.status, 202);
|
|
540
|
-
assert.equal(resumedWorkspace, '
|
|
695
|
+
assert.equal(resumedWorkspace, 'acme');
|
|
541
696
|
assert.deepEqual(await response.json(), {
|
|
542
697
|
resumed: 1,
|
|
543
698
|
interrupted: 0,
|
|
544
|
-
workspaces: [{ workspace: '
|
|
699
|
+
workspaces: [{ workspace: 'acme', resumed: true }],
|
|
545
700
|
});
|
|
546
701
|
} finally {
|
|
547
702
|
await handle.close();
|
|
@@ -575,13 +730,13 @@ test('runtime server exposes approval endpoint', async (t) => {
|
|
|
575
730
|
}
|
|
576
731
|
|
|
577
732
|
try {
|
|
578
|
-
const response = await fetch(`http://127.0.0.1:${handle.port}/approve?workspace=
|
|
733
|
+
const response = await fetch(`http://127.0.0.1:${handle.port}/approve?workspace=acme&runId=run-1&itemId=item-1`, {
|
|
579
734
|
method: 'POST',
|
|
580
735
|
});
|
|
581
736
|
assert.equal(response.status, 202);
|
|
582
737
|
assert.deepEqual(approved, {
|
|
583
|
-
workspace: '
|
|
584
|
-
workspaceId: '
|
|
738
|
+
workspace: 'acme',
|
|
739
|
+
workspaceId: 'acme',
|
|
585
740
|
runId: 'run-1',
|
|
586
741
|
itemId: 'item-1',
|
|
587
742
|
approvalId: null,
|
|
@@ -599,8 +754,8 @@ test('runtime server exposes approval endpoint', async (t) => {
|
|
|
599
754
|
|
|
600
755
|
test('runtime server exposes control status and explanation', async (t) => {
|
|
601
756
|
const session = {
|
|
602
|
-
workspace: '
|
|
603
|
-
controlQueue: [{ id: 'control-1', workspace: '
|
|
757
|
+
workspace: 'acme',
|
|
758
|
+
controlQueue: [{ id: 'control-1', workspace: 'acme', status: 'queued', input: 'later' }],
|
|
604
759
|
};
|
|
605
760
|
let handle;
|
|
606
761
|
try {
|
|
@@ -620,7 +775,7 @@ test('runtime server exposes control status and explanation', async (t) => {
|
|
|
620
775
|
listEvents: () => [],
|
|
621
776
|
},
|
|
622
777
|
getContext: async () => ({
|
|
623
|
-
workspace: '
|
|
778
|
+
workspace: 'acme',
|
|
624
779
|
session,
|
|
625
780
|
running: false,
|
|
626
781
|
currentAbortController: null,
|
|
@@ -636,14 +791,14 @@ test('runtime server exposes control status and explanation', async (t) => {
|
|
|
636
791
|
}
|
|
637
792
|
|
|
638
793
|
try {
|
|
639
|
-
const status = await fetch(`http://127.0.0.1:${handle.port}/control?workspace=
|
|
794
|
+
const status = await fetch(`http://127.0.0.1:${handle.port}/control?workspace=acme`);
|
|
640
795
|
assert.equal(status.status, 200);
|
|
641
796
|
const statusBody = await status.json();
|
|
642
797
|
assert.equal(statusBody.status, 'idle');
|
|
643
798
|
assert.equal(statusBody.running, false);
|
|
644
799
|
assert.equal(statusBody.controlQueue[0].id, 'control-1');
|
|
645
800
|
|
|
646
|
-
const explain = await fetch(`http://127.0.0.1:${handle.port}/control?workspace=
|
|
801
|
+
const explain = await fetch(`http://127.0.0.1:${handle.port}/control?workspace=acme`, {
|
|
647
802
|
method: 'POST',
|
|
648
803
|
headers: { 'Content-Type': 'application/json' },
|
|
649
804
|
body: JSON.stringify({ action: 'explain' }),
|
|
@@ -657,7 +812,7 @@ test('runtime server exposes control status and explanation', async (t) => {
|
|
|
657
812
|
|
|
658
813
|
test('runtime server control enqueue emits events but does not patch an active plan or start a run', async (t) => {
|
|
659
814
|
const session = {
|
|
660
|
-
workspace: '
|
|
815
|
+
workspace: 'acme',
|
|
661
816
|
controlQueue: [],
|
|
662
817
|
};
|
|
663
818
|
const events = [];
|
|
@@ -681,7 +836,7 @@ test('runtime server control enqueue emits events but does not patch an active p
|
|
|
681
836
|
listEvents: () => [],
|
|
682
837
|
},
|
|
683
838
|
getContext: async () => ({
|
|
684
|
-
workspace: '
|
|
839
|
+
workspace: 'acme',
|
|
685
840
|
session,
|
|
686
841
|
running: true,
|
|
687
842
|
currentAbortController: new AbortController(),
|
|
@@ -697,7 +852,7 @@ test('runtime server control enqueue emits events but does not patch an active p
|
|
|
697
852
|
}
|
|
698
853
|
|
|
699
854
|
try {
|
|
700
|
-
const response = await fetch(`http://127.0.0.1:${handle.port}/control?workspace=
|
|
855
|
+
const response = await fetch(`http://127.0.0.1:${handle.port}/control?workspace=acme`, {
|
|
701
856
|
method: 'POST',
|
|
702
857
|
headers: { 'Content-Type': 'application/json' },
|
|
703
858
|
body: JSON.stringify({ action: 'enqueue', input: 'run this after current work' }),
|
|
@@ -718,7 +873,7 @@ test('runtime server control enqueue emits events but does not patch an active p
|
|
|
718
873
|
|
|
719
874
|
test('runtime server control message observes an active run without enqueueing', async (t) => {
|
|
720
875
|
const session = {
|
|
721
|
-
workspace: '
|
|
876
|
+
workspace: 'acme',
|
|
722
877
|
controlQueue: [],
|
|
723
878
|
};
|
|
724
879
|
let runCount = 0;
|
|
@@ -739,7 +894,7 @@ test('runtime server control message observes an active run without enqueueing',
|
|
|
739
894
|
listEvents: () => [],
|
|
740
895
|
},
|
|
741
896
|
getContext: async () => ({
|
|
742
|
-
workspace: '
|
|
897
|
+
workspace: 'acme',
|
|
743
898
|
session,
|
|
744
899
|
running: true,
|
|
745
900
|
currentAbortController: new AbortController(),
|
|
@@ -755,7 +910,7 @@ test('runtime server control message observes an active run without enqueueing',
|
|
|
755
910
|
}
|
|
756
911
|
|
|
757
912
|
try {
|
|
758
|
-
const response = await fetch(`http://127.0.0.1:${handle.port}/control?workspace=
|
|
913
|
+
const response = await fetch(`http://127.0.0.1:${handle.port}/control?workspace=acme`, {
|
|
759
914
|
method: 'POST',
|
|
760
915
|
headers: { 'Content-Type': 'application/json' },
|
|
761
916
|
body: JSON.stringify({ action: 'message', input: 'Où en est le build ?' }),
|
|
@@ -773,7 +928,7 @@ test('runtime server control message observes an active run without enqueueing',
|
|
|
773
928
|
|
|
774
929
|
test('runtime server control message handles approve and cancel intents during an active run', async (t) => {
|
|
775
930
|
const session = {
|
|
776
|
-
workspace: '
|
|
931
|
+
workspace: 'acme',
|
|
777
932
|
controlQueue: [],
|
|
778
933
|
};
|
|
779
934
|
const abortController = new AbortController();
|
|
@@ -795,7 +950,7 @@ test('runtime server control message handles approve and cancel intents during a
|
|
|
795
950
|
listEvents: () => [],
|
|
796
951
|
},
|
|
797
952
|
getContext: async () => ({
|
|
798
|
-
workspace: '
|
|
953
|
+
workspace: 'acme',
|
|
799
954
|
session,
|
|
800
955
|
running: true,
|
|
801
956
|
currentAbortController: abortController,
|
|
@@ -812,7 +967,7 @@ test('runtime server control message handles approve and cancel intents during a
|
|
|
812
967
|
}
|
|
813
968
|
|
|
814
969
|
try {
|
|
815
|
-
const approveResponse = await fetch(`http://127.0.0.1:${handle.port}/control?workspace=
|
|
970
|
+
const approveResponse = await fetch(`http://127.0.0.1:${handle.port}/control?workspace=acme`, {
|
|
816
971
|
method: 'POST',
|
|
817
972
|
headers: { 'Content-Type': 'application/json' },
|
|
818
973
|
body: JSON.stringify({ action: 'message', input: 'valide tout' }),
|
|
@@ -820,7 +975,7 @@ test('runtime server control message handles approve and cancel intents during a
|
|
|
820
975
|
assert.equal(approveResponse.status, 200);
|
|
821
976
|
assert.equal((await approveResponse.json()).kind, 'approve');
|
|
822
977
|
|
|
823
|
-
const cancelResponse = await fetch(`http://127.0.0.1:${handle.port}/control?workspace=
|
|
978
|
+
const cancelResponse = await fetch(`http://127.0.0.1:${handle.port}/control?workspace=acme`, {
|
|
824
979
|
method: 'POST',
|
|
825
980
|
headers: { 'Content-Type': 'application/json' },
|
|
826
981
|
body: JSON.stringify({ action: 'message', input: 'annule le run' }),
|
|
@@ -837,18 +992,18 @@ test('runtime server control message handles approve and cancel intents during a
|
|
|
837
992
|
|
|
838
993
|
test('runtime server control message records active plan mutation as a proposal', async (t) => {
|
|
839
994
|
const session = {
|
|
840
|
-
workspace: '
|
|
995
|
+
workspace: 'acme',
|
|
841
996
|
controlQueue: [],
|
|
842
997
|
};
|
|
843
998
|
dispatchAgentEvent(session, createAgentEvent('run_started', {
|
|
844
999
|
origin: 'runtime',
|
|
845
1000
|
runId: 'run-mutate',
|
|
846
|
-
workspace: '
|
|
1001
|
+
workspace: 'acme',
|
|
847
1002
|
}));
|
|
848
1003
|
dispatchAgentEvent(session, createAgentEvent('plan_set', {
|
|
849
1004
|
origin: 'tool',
|
|
850
1005
|
runId: 'run-mutate',
|
|
851
|
-
workspace: '
|
|
1006
|
+
workspace: 'acme',
|
|
852
1007
|
payload: {
|
|
853
1008
|
steps: [{ step: 1, id: 'generate', description: 'Generate', status: 'running' }],
|
|
854
1009
|
},
|
|
@@ -865,13 +1020,13 @@ test('runtime server control message records active plan mutation as a proposal'
|
|
|
865
1020
|
...session.agentProjection,
|
|
866
1021
|
status: session.agentProjection?.status ?? 'running',
|
|
867
1022
|
queue: [],
|
|
868
|
-
runs: [{ id: 'run-mutate', workspace: '
|
|
1023
|
+
runs: [{ id: 'run-mutate', workspace: 'acme', status: 'running' }],
|
|
869
1024
|
runId: 'run-mutate',
|
|
870
1025
|
}),
|
|
871
1026
|
listEvents: () => [],
|
|
872
1027
|
},
|
|
873
1028
|
getContext: async () => ({
|
|
874
|
-
workspace: '
|
|
1029
|
+
workspace: 'acme',
|
|
875
1030
|
session,
|
|
876
1031
|
running: true,
|
|
877
1032
|
currentAbortController: new AbortController(),
|
|
@@ -888,7 +1043,7 @@ test('runtime server control message records active plan mutation as a proposal'
|
|
|
888
1043
|
}
|
|
889
1044
|
|
|
890
1045
|
try {
|
|
891
|
-
const response = await fetch(`http://127.0.0.1:${handle.port}/control?workspace=
|
|
1046
|
+
const response = await fetch(`http://127.0.0.1:${handle.port}/control?workspace=acme`, {
|
|
892
1047
|
method: 'POST',
|
|
893
1048
|
headers: { 'Content-Type': 'application/json' },
|
|
894
1049
|
body: JSON.stringify({ action: 'message', input: 'Ajoute un envoi après chaque génération' }),
|
|
@@ -901,7 +1056,7 @@ test('runtime server control message records active plan mutation as a proposal'
|
|
|
901
1056
|
assert.equal(session.agentProjection.planPatches[0].status, 'proposed');
|
|
902
1057
|
assert.ok(session.agentEvents.some((event) => event.type === 'control_message_received'));
|
|
903
1058
|
assert.ok(session.agentEvents.some((event) => event.type === 'plan_patch_proposed'));
|
|
904
|
-
const approveResponse = await fetch(`http://127.0.0.1:${handle.port}/control?workspace=
|
|
1059
|
+
const approveResponse = await fetch(`http://127.0.0.1:${handle.port}/control?workspace=acme`, {
|
|
905
1060
|
method: 'POST',
|
|
906
1061
|
headers: { 'Content-Type': 'application/json' },
|
|
907
1062
|
body: JSON.stringify({ action: 'approve_patch', patchId: body.proposal.id }),
|
|
@@ -924,7 +1079,7 @@ test('runtime server control message records active plan mutation as a proposal'
|
|
|
924
1079
|
|
|
925
1080
|
test('runtime server control message reports ambiguity without starting a run', async (t) => {
|
|
926
1081
|
const session = {
|
|
927
|
-
workspace: '
|
|
1082
|
+
workspace: 'acme',
|
|
928
1083
|
controlQueue: [],
|
|
929
1084
|
};
|
|
930
1085
|
let runCount = 0;
|
|
@@ -945,7 +1100,7 @@ test('runtime server control message reports ambiguity without starting a run',
|
|
|
945
1100
|
listEvents: () => [],
|
|
946
1101
|
},
|
|
947
1102
|
getContext: async () => ({
|
|
948
|
-
workspace: '
|
|
1103
|
+
workspace: 'acme',
|
|
949
1104
|
session,
|
|
950
1105
|
running: true,
|
|
951
1106
|
currentAbortController: new AbortController(),
|
|
@@ -961,7 +1116,7 @@ test('runtime server control message reports ambiguity without starting a run',
|
|
|
961
1116
|
}
|
|
962
1117
|
|
|
963
1118
|
try {
|
|
964
|
-
const response = await fetch(`http://127.0.0.1:${handle.port}/control?workspace=
|
|
1119
|
+
const response = await fetch(`http://127.0.0.1:${handle.port}/control?workspace=acme`, {
|
|
965
1120
|
method: 'POST',
|
|
966
1121
|
headers: { 'Content-Type': 'application/json' },
|
|
967
1122
|
body: JSON.stringify({ action: 'message', input: 'Lance aussi la publication' }),
|
|
@@ -979,7 +1134,7 @@ test('runtime server control message reports ambiguity without starting a run',
|
|
|
979
1134
|
|
|
980
1135
|
test('runtime server drains queued control requests when idle', async (t) => {
|
|
981
1136
|
const session = {
|
|
982
|
-
workspace: '
|
|
1137
|
+
workspace: 'acme',
|
|
983
1138
|
controlQueue: [],
|
|
984
1139
|
};
|
|
985
1140
|
const events = [];
|
|
@@ -1003,7 +1158,7 @@ test('runtime server drains queued control requests when idle', async (t) => {
|
|
|
1003
1158
|
listEvents: () => [],
|
|
1004
1159
|
},
|
|
1005
1160
|
getContext: async () => ({
|
|
1006
|
-
workspace: '
|
|
1161
|
+
workspace: 'acme',
|
|
1007
1162
|
session,
|
|
1008
1163
|
running: false,
|
|
1009
1164
|
currentAbortController: null,
|
|
@@ -1021,7 +1176,7 @@ test('runtime server drains queued control requests when idle', async (t) => {
|
|
|
1021
1176
|
}
|
|
1022
1177
|
|
|
1023
1178
|
try {
|
|
1024
|
-
const response = await fetch(`http://127.0.0.1:${handle.port}/control?workspace=
|
|
1179
|
+
const response = await fetch(`http://127.0.0.1:${handle.port}/control?workspace=acme`, {
|
|
1025
1180
|
method: 'POST',
|
|
1026
1181
|
headers: { 'Content-Type': 'application/json' },
|
|
1027
1182
|
body: JSON.stringify({ action: 'enqueue', input: 'run from control queue' }),
|
|
@@ -1029,7 +1184,7 @@ test('runtime server drains queued control requests when idle', async (t) => {
|
|
|
1029
1184
|
assert.equal(response.status, 202);
|
|
1030
1185
|
await new Promise((resolve) => setImmediate(resolve));
|
|
1031
1186
|
assert.equal(receivedBody.input, 'run from control queue');
|
|
1032
|
-
assert.equal(receivedBody.workspace, '
|
|
1187
|
+
assert.equal(receivedBody.workspace, 'acme');
|
|
1033
1188
|
assert.match(receivedBody.runId, /^[0-9a-f-]{36}$/);
|
|
1034
1189
|
assert.equal(session.controlQueue[0].status, 'running');
|
|
1035
1190
|
assert.equal(session.controlQueue[0].runId, receivedBody.runId);
|
|
@@ -1041,13 +1196,13 @@ test('runtime server drains queued control requests when idle', async (t) => {
|
|
|
1041
1196
|
|
|
1042
1197
|
test('runtime server handle drains a pre-existing hydrated control request', async (t) => {
|
|
1043
1198
|
const session = {
|
|
1044
|
-
workspace: '
|
|
1045
|
-
controlQueue: [{ id: 'control-existing', workspace: '
|
|
1199
|
+
workspace: 'acme',
|
|
1200
|
+
controlQueue: [{ id: 'control-existing', workspace: 'acme', status: 'queued', input: 'resume queued control' }],
|
|
1046
1201
|
};
|
|
1047
1202
|
const events = [];
|
|
1048
1203
|
session._onAgentEvent = (event) => events.push(event);
|
|
1049
1204
|
const context = {
|
|
1050
|
-
workspace: '
|
|
1205
|
+
workspace: 'acme',
|
|
1051
1206
|
session,
|
|
1052
1207
|
running: false,
|
|
1053
1208
|
currentAbortController: null,
|
|
@@ -1089,8 +1244,8 @@ test('runtime server handle drains a pre-existing hydrated control request', asy
|
|
|
1089
1244
|
|
|
1090
1245
|
test('runtime server exposes config profile list and switch endpoints', async (t) => {
|
|
1091
1246
|
const context = {
|
|
1092
|
-
workspace: '
|
|
1093
|
-
session: { workspace: '
|
|
1247
|
+
workspace: 'acme',
|
|
1248
|
+
session: { workspace: 'acme', wikirc: { profile: 'default' } },
|
|
1094
1249
|
running: false,
|
|
1095
1250
|
currentAbortController: null,
|
|
1096
1251
|
};
|
|
@@ -1123,11 +1278,11 @@ test('runtime server exposes config profile list and switch endpoints', async (t
|
|
|
1123
1278
|
}
|
|
1124
1279
|
|
|
1125
1280
|
try {
|
|
1126
|
-
const profiles = await fetch(`http://127.0.0.1:${handle.port}/config/profiles?workspace=
|
|
1281
|
+
const profiles = await fetch(`http://127.0.0.1:${handle.port}/config/profiles?workspace=acme`);
|
|
1127
1282
|
assert.equal(profiles.status, 200);
|
|
1128
1283
|
assert.deepEqual(await profiles.json(), { profiles: ['default', 'vpn'], active: 'default' });
|
|
1129
1284
|
|
|
1130
|
-
const use = await fetch(`http://127.0.0.1:${handle.port}/config/use?workspace=
|
|
1285
|
+
const use = await fetch(`http://127.0.0.1:${handle.port}/config/use?workspace=acme`, {
|
|
1131
1286
|
method: 'POST',
|
|
1132
1287
|
headers: { 'Content-Type': 'application/json' },
|
|
1133
1288
|
body: JSON.stringify({ profile: 'vpn' }),
|
|
@@ -1152,8 +1307,8 @@ test('runtime server rejects config switching while a run is active', async (t)
|
|
|
1152
1307
|
listEvents: () => [],
|
|
1153
1308
|
},
|
|
1154
1309
|
getContext: async () => ({
|
|
1155
|
-
workspace: '
|
|
1156
|
-
session: { workspace: '
|
|
1310
|
+
workspace: 'acme',
|
|
1311
|
+
session: { workspace: 'acme' },
|
|
1157
1312
|
running: true,
|
|
1158
1313
|
currentAbortController: null,
|
|
1159
1314
|
}),
|
|
@@ -1169,7 +1324,7 @@ test('runtime server rejects config switching while a run is active', async (t)
|
|
|
1169
1324
|
}
|
|
1170
1325
|
|
|
1171
1326
|
try {
|
|
1172
|
-
const response = await fetch(`http://127.0.0.1:${handle.port}/config/use?workspace=
|
|
1327
|
+
const response = await fetch(`http://127.0.0.1:${handle.port}/config/use?workspace=acme`, {
|
|
1173
1328
|
method: 'POST',
|
|
1174
1329
|
headers: { 'Content-Type': 'application/json' },
|
|
1175
1330
|
body: JSON.stringify({ profile: 'vpn' }),
|
|
@@ -1179,3 +1334,65 @@ test('runtime server rejects config switching while a run is active', async (t)
|
|
|
1179
1334
|
await handle.close();
|
|
1180
1335
|
}
|
|
1181
1336
|
});
|
|
1337
|
+
|
|
1338
|
+
test('runtime server answers control messages posted to /run during an active run', async (t) => {
|
|
1339
|
+
let releaseRun;
|
|
1340
|
+
let handle;
|
|
1341
|
+
try {
|
|
1342
|
+
handle = await startRuntimeServer({
|
|
1343
|
+
host: '127.0.0.1',
|
|
1344
|
+
port: 0,
|
|
1345
|
+
store: {
|
|
1346
|
+
dbPath: ':memory:',
|
|
1347
|
+
getState: () => ({ status: 'running' }),
|
|
1348
|
+
listEvents: () => [],
|
|
1349
|
+
},
|
|
1350
|
+
session: {},
|
|
1351
|
+
run: async () => {
|
|
1352
|
+
await new Promise((resolve) => { releaseRun = resolve; });
|
|
1353
|
+
},
|
|
1354
|
+
});
|
|
1355
|
+
} catch (err) {
|
|
1356
|
+
if (err?.code === 'EPERM') {
|
|
1357
|
+
t.skip('network listen is not permitted in this sandbox');
|
|
1358
|
+
return;
|
|
1359
|
+
}
|
|
1360
|
+
throw err;
|
|
1361
|
+
}
|
|
1362
|
+
|
|
1363
|
+
try {
|
|
1364
|
+
const url = `http://127.0.0.1:${handle.port}/run`;
|
|
1365
|
+
const started = await fetch(url, {
|
|
1366
|
+
method: 'POST',
|
|
1367
|
+
headers: { 'Content-Type': 'application/json' },
|
|
1368
|
+
body: JSON.stringify({ input: 'lance le pipeline' }),
|
|
1369
|
+
});
|
|
1370
|
+
assert.equal(started.status, 202);
|
|
1371
|
+
|
|
1372
|
+
// A status question must be answered NOW, not parked until the run ends
|
|
1373
|
+
// (regression: serve UI got no result until the job finished/stopped).
|
|
1374
|
+
const observe = await fetch(url, {
|
|
1375
|
+
method: 'POST',
|
|
1376
|
+
headers: { 'Content-Type': 'application/json' },
|
|
1377
|
+
body: JSON.stringify({ input: 'où en est le run ?' }),
|
|
1378
|
+
});
|
|
1379
|
+
assert.equal(observe.status, 200);
|
|
1380
|
+
const observeBody = await observe.json();
|
|
1381
|
+
assert.equal(observeBody.kind, 'observe');
|
|
1382
|
+
assert.ok(String(observeBody.explanation ?? '').length > 0, 'observe must carry an explanation');
|
|
1383
|
+
|
|
1384
|
+
// A cancel must abort the active run instead of being enqueued.
|
|
1385
|
+
const cancel = await fetch(url, {
|
|
1386
|
+
method: 'POST',
|
|
1387
|
+
headers: { 'Content-Type': 'application/json' },
|
|
1388
|
+
body: JSON.stringify({ input: 'annule le run' }),
|
|
1389
|
+
});
|
|
1390
|
+
assert.equal(cancel.status, 200);
|
|
1391
|
+
const cancelBody = await cancel.json();
|
|
1392
|
+
assert.equal(cancelBody.kind, 'cancel');
|
|
1393
|
+
assert.equal(cancelBody.accepted, true);
|
|
1394
|
+
} finally {
|
|
1395
|
+
releaseRun?.();
|
|
1396
|
+
await handle.close();
|
|
1397
|
+
}
|
|
1398
|
+
});
|