@north-light/crouter 0.3.229 → 0.3.230
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.
|
@@ -250,7 +250,7 @@ describe('canvas inbox watcher — hold + idle delivery', () => {
|
|
|
250
250
|
pi.fire('agent_start', { type: 'agent_start' }, { isIdle: () => false });
|
|
251
251
|
await wait(TICK_MS + 100);
|
|
252
252
|
appendInbox(meta.node_id, { from: 'child-x', tier: 'normal', kind: 'update', label: 'handoff at yield' });
|
|
253
|
-
await
|
|
253
|
+
await waitForFirstInjection(pi);
|
|
254
254
|
assert.equal(pi.injected.length, 1, 'the active conversation received the handoff before yield');
|
|
255
255
|
// Yield discards that conversation before it can act on the queued handoff.
|
|
256
256
|
// Its settle must leave the entry durable for the fresh revive's kickoff.
|
|
@@ -267,7 +267,7 @@ describe('canvas inbox watcher — hold + idle delivery', () => {
|
|
|
267
267
|
// No agent_start fired → watcher treats the node as idle.
|
|
268
268
|
await wait(TICK_MS + 100);
|
|
269
269
|
appendInbox('node-idle', { from: 'child-3', tier: 'normal', kind: 'final', label: 'done while idle' });
|
|
270
|
-
await
|
|
270
|
+
await waitForFirstInjection(pi);
|
|
271
271
|
assert.equal(pi.injected.length, 1);
|
|
272
272
|
assert.equal(pi.injected[0].deliverAs, undefined, 'idle → sendUserMessage triggers a turn, no deliverAs');
|
|
273
273
|
});
|
|
@@ -295,7 +295,7 @@ describe('canvas inbox watcher — hold + idle delivery', () => {
|
|
|
295
295
|
assert.equal(pi.injected.length, 0, 'a paused profile delivers nothing to its live broker');
|
|
296
296
|
assert.equal(readCursor('node-paused-profile'), undefined, 'the held entry stays unconsumed');
|
|
297
297
|
resumeProfile(profile.profileId);
|
|
298
|
-
await
|
|
298
|
+
await waitForFirstInjection(pi);
|
|
299
299
|
assert.equal(pi.injected.length, 1, 'resume delivers the entry that waited');
|
|
300
300
|
assert.match(pi.injected[0].content, /sent while paused/);
|
|
301
301
|
}
|
|
@@ -348,7 +348,7 @@ describe('canvas inbox watcher — action-gated startup (session_start arm)', ()
|
|
|
348
348
|
// now must pick up the already-buffered pre-bind entry.
|
|
349
349
|
actionsReady = true;
|
|
350
350
|
pi.fire('session_start', { type: 'session_start' }, undefined);
|
|
351
|
-
await
|
|
351
|
+
await waitForFirstInjection(pi);
|
|
352
352
|
assert.equal(sendAttempts, 1, 'exactly one action attempt once session_start arms the watcher');
|
|
353
353
|
assert.equal(pi.injected.length, 1, 'exactly one handoff once actions are bound');
|
|
354
354
|
assert.equal(readCursor('node-action-gate'), undefined, 'still uncommitted until agent_settled');
|
|
@@ -404,7 +404,7 @@ describe('canvas inbox watcher — durable cursor (at-least-once)', () => {
|
|
|
404
404
|
pi1.fire('agent_start', { type: 'agent_start' }, { isIdle: () => false });
|
|
405
405
|
await wait(TICK_MS + 100);
|
|
406
406
|
appendInbox('node-cursor-crash', { from: 'child-1', tier: 'normal', kind: 'update', label: 'still working' });
|
|
407
|
-
await
|
|
407
|
+
await waitForFirstInjection(pi1);
|
|
408
408
|
assert.equal(pi1.injected.length, 1, 'delivered mid-turn as a followUp');
|
|
409
409
|
assert.equal(pi1.injected[0].deliverAs, 'followUp');
|
|
410
410
|
// The durability boundary: no settle has fired, so the on-disk cursor must
|
|
@@ -420,7 +420,7 @@ describe('canvas inbox watcher — durable cursor (at-least-once)', () => {
|
|
|
420
420
|
// modeling crash-then-revive rather than a clean stop.
|
|
421
421
|
const pi2 = makeFakePi();
|
|
422
422
|
disposers.push(registerWatcher(pi2));
|
|
423
|
-
await
|
|
423
|
+
await waitForFirstInjection(pi2);
|
|
424
424
|
assert.equal(pi2.injected.length, 1, 'a fresh watcher re-reads and re-delivers the same uncommitted entry');
|
|
425
425
|
assert.match(pi2.injected[0].content, /still working/);
|
|
426
426
|
});
|
|
@@ -431,7 +431,7 @@ describe('canvas inbox watcher — durable cursor (at-least-once)', () => {
|
|
|
431
431
|
pi1.fire('agent_start', { type: 'agent_start' }, { isIdle: () => false });
|
|
432
432
|
await wait(TICK_MS + 100);
|
|
433
433
|
const entry = appendInbox('node-cursor-committed', { from: 'child-2', tier: 'normal', kind: 'update', label: 'still working' });
|
|
434
|
-
await
|
|
434
|
+
await waitForFirstInjection(pi1);
|
|
435
435
|
assert.equal(pi1.injected.length, 1);
|
|
436
436
|
assert.equal(readCursor('node-cursor-committed'), undefined, 'not yet committed before the settle');
|
|
437
437
|
// The turn settles with pi's queue drained (the followUp was consumed) —
|
|
@@ -450,7 +450,7 @@ describe('canvas inbox watcher — durable cursor (at-least-once)', () => {
|
|
|
450
450
|
// No agent_start fired → watcher treats the node as idle.
|
|
451
451
|
await wait(TICK_MS + 100);
|
|
452
452
|
const entry = appendInbox('node-cursor-idle', { from: 'child-3', tier: 'normal', kind: 'update', label: 'idle update' });
|
|
453
|
-
await
|
|
453
|
+
await waitForFirstInjection(pi);
|
|
454
454
|
assert.equal(pi.injected.length, 1);
|
|
455
455
|
assert.equal(pi.injected[0].deliverAs, undefined, 'idle delivery has no deliverAs');
|
|
456
456
|
// The idle send triggers a turn, but its void return is a handoff, not proof
|
|
@@ -477,7 +477,7 @@ describe('canvas inbox watcher — durable cursor (at-least-once)', () => {
|
|
|
477
477
|
assert.equal(readCursor('node-cursor-failsend'), undefined, 'durable cursor not advanced past a re-buffered, never-delivered entry');
|
|
478
478
|
const pi2 = makeFakePi();
|
|
479
479
|
disposers.push(registerWatcher(pi2));
|
|
480
|
-
await
|
|
480
|
+
await waitForFirstInjection(pi2);
|
|
481
481
|
assert.equal(pi2.injected.length, 1, 'a fresh watcher re-reads and re-delivers the never-committed entry');
|
|
482
482
|
assert.match(pi2.injected[0].content, /never delivered/);
|
|
483
483
|
});
|
|
@@ -501,7 +501,7 @@ describe('canvas inbox watcher — durable cursor (at-least-once)', () => {
|
|
|
501
501
|
assert.equal(readCursor('node-cursor-critical'), undefined, 'abort-generated settle does not commit the pending critical entry before its retry');
|
|
502
502
|
const pi2 = makeFakePi();
|
|
503
503
|
disposers.push(registerWatcher(pi2));
|
|
504
|
-
await
|
|
504
|
+
await waitForFirstInjection(pi2);
|
|
505
505
|
assert.equal(pi2.injected.length, 1, 'a fresh watcher re-delivers the critical entry');
|
|
506
506
|
assert.match(pi2.injected[0].content, /critical work/);
|
|
507
507
|
});
|
|
@@ -517,7 +517,7 @@ describe('canvas inbox watcher — durable cursor (at-least-once)', () => {
|
|
|
517
517
|
pi1.fire('agent_start', { type: 'agent_start' }, { isIdle: () => false });
|
|
518
518
|
await wait(TICK_MS + 60);
|
|
519
519
|
const a = appendInbox('node-cursor-batchB', { from: 'child-a', tier: 'normal', kind: 'update', label: 'batch A' });
|
|
520
|
-
await
|
|
520
|
+
await waitForFirstInjection(pi1);
|
|
521
521
|
assert.equal(pi1.injected.length, 1, 'batch A delivered mid-turn');
|
|
522
522
|
assert.equal(pi1.injected[0].deliverAs, 'followUp');
|
|
523
523
|
const b = appendInbox('node-cursor-batchB', { from: 'child-b', tier: 'normal', kind: 'update', label: 'batch B' });
|
|
@@ -529,7 +529,7 @@ describe('canvas inbox watcher — durable cursor (at-least-once)', () => {
|
|
|
529
529
|
assert.notEqual(readCursor('node-cursor-batchB'), b.entry_id, 'commit does not cross the still-buffered batch B');
|
|
530
530
|
const pi2 = makeFakePi();
|
|
531
531
|
disposers.push(registerWatcher(pi2));
|
|
532
|
-
await
|
|
532
|
+
await waitForFirstInjection(pi2);
|
|
533
533
|
assert.equal(pi2.injected.length, 1, 'a fresh watcher re-reads from A and re-delivers B');
|
|
534
534
|
assert.match(pi2.injected[0].content, /batch B/);
|
|
535
535
|
assert.doesNotMatch(pi2.injected[0].content, /batch A/, 'A is already committed, only B is re-delivered');
|
|
@@ -548,7 +548,7 @@ describe('canvas inbox watcher — durable cursor (at-least-once)', () => {
|
|
|
548
548
|
pi1.fire('agent_start', { type: 'agent_start' }, { isIdle: () => false });
|
|
549
549
|
await wait(TICK_MS + 100);
|
|
550
550
|
const entry = appendInbox('node-cursor-failhold', { from: 'child-1', tier: 'normal', kind: 'update', label: 'queued across a failure' });
|
|
551
|
-
await
|
|
551
|
+
await waitForFirstInjection(pi1);
|
|
552
552
|
assert.equal(pi1.injected.length, 1, 'delivered mid-turn as a followUp');
|
|
553
553
|
assert.equal(pi1.injected[0].deliverAs, 'followUp');
|
|
554
554
|
// A failure/aborted agent_end is never a commit point regardless of queue
|
|
@@ -583,7 +583,7 @@ describe('canvas inbox watcher — durable cursor (at-least-once)', () => {
|
|
|
583
583
|
pi.fire('agent_start', { type: 'agent_start' }, { isIdle: () => false });
|
|
584
584
|
await wait(TICK_MS + 100);
|
|
585
585
|
appendInbox(meta.node_id, { from: 'child-x', tier: 'normal', kind: 'update', label: 'handoff at shutdown' });
|
|
586
|
-
await
|
|
586
|
+
await waitForFirstInjection(pi);
|
|
587
587
|
assert.equal(pi.injected.length, 1, 'the active conversation received the handoff before refresh');
|
|
588
588
|
setIntent(meta.node_id, 'refresh');
|
|
589
589
|
await shutdown(pi, { pending: false });
|
|
@@ -596,7 +596,7 @@ describe('canvas inbox watcher — durable cursor (at-least-once)', () => {
|
|
|
596
596
|
pi1.fire('agent_start', { type: 'agent_start' }, { isIdle: () => false });
|
|
597
597
|
await wait(TICK_MS + 100);
|
|
598
598
|
const entry = appendInbox('node-cursor-shutdownhold', { from: 'child-1', tier: 'normal', kind: 'update', label: 'still queued at shutdown' });
|
|
599
|
-
await
|
|
599
|
+
await waitForFirstInjection(pi1);
|
|
600
600
|
assert.equal(pi1.injected.length, 1, 'delivered mid-turn as a followUp');
|
|
601
601
|
// session_shutdown fires with no prior agent_settled, and the context
|
|
602
602
|
// reports an unconsumed queued send — the hold declines the commit.
|
|
@@ -606,7 +606,7 @@ describe('canvas inbox watcher — durable cursor (at-least-once)', () => {
|
|
|
606
606
|
// A fresh watcher re-reads and re-delivers, then a drained settle commits.
|
|
607
607
|
const pi2 = makeFakePi();
|
|
608
608
|
disposers.push(registerWatcher(pi2));
|
|
609
|
-
await
|
|
609
|
+
await waitForFirstInjection(pi2);
|
|
610
610
|
assert.equal(pi2.injected.length, 1, 'a fresh watcher re-reads and re-delivers the never-committed entry');
|
|
611
611
|
assert.match(pi2.injected[0].content, /still queued at shutdown/);
|
|
612
612
|
await settle(pi2);
|
|
@@ -8,7 +8,6 @@ import { tmpdir } from 'node:os';
|
|
|
8
8
|
import { join } from 'node:path';
|
|
9
9
|
import { createServer } from 'node:net';
|
|
10
10
|
import { spawn as spawnProcess } from 'node:child_process';
|
|
11
|
-
import { findDaemonPids, stopDaemonProcess } from '../../../daemon/manage.js';
|
|
12
11
|
import { createNode, getNode, getRow, subscribersOf } from '../../canvas/canvas.js';
|
|
13
12
|
import { closeDb } from '../../canvas/db.js';
|
|
14
13
|
import { nodeDir } from '../../canvas/paths.js';
|
|
@@ -21,6 +20,7 @@ import { piSessionsRoot } from '../../runtime/pi-vendored.js';
|
|
|
21
20
|
import { emptyContextExposureState, exposureTarget, loadContextExposureState, registerExposure, saveContextExposureState, } from '../../substrate/injected-store.js';
|
|
22
21
|
import { ROOT_PROFILE_ID } from '../../profiles/manifest.js';
|
|
23
22
|
let home;
|
|
23
|
+
let previousPidfile;
|
|
24
24
|
function spawner(id) {
|
|
25
25
|
return {
|
|
26
26
|
node_id: id,
|
|
@@ -38,15 +38,25 @@ before(() => {
|
|
|
38
38
|
// canvas home short because root readiness now opens a real view.sock.
|
|
39
39
|
home = mkdtempSync('/tmp/crtr-spawn-root-');
|
|
40
40
|
process.env['CRTR_HOME'] = home;
|
|
41
|
+
previousPidfile = process.env['CRTR_PIDFILE'];
|
|
42
|
+
process.env['CRTR_PIDFILE'] = join(home, 'crtrd.pid');
|
|
41
43
|
});
|
|
42
44
|
beforeEach(() => {
|
|
43
45
|
closeDb();
|
|
44
46
|
rmSync(home, { recursive: true, force: true });
|
|
47
|
+
mkdirSync(home, { recursive: true });
|
|
48
|
+
// These tests exercise direct spawn behavior, not daemon autostart. Keep
|
|
49
|
+
// ensureDaemon() from launching a background daemon whose startup verifier
|
|
50
|
+
// would outlive the test's next CRTR_HOME reset.
|
|
51
|
+
writeFileSync(join(home, 'crtrd.pid'), `${process.pid}\n`);
|
|
45
52
|
});
|
|
46
|
-
after(
|
|
53
|
+
after(() => {
|
|
47
54
|
closeDb();
|
|
48
|
-
await Promise.all(findDaemonPids().map((pid) => stopDaemonProcess(pid)));
|
|
49
55
|
rmSync(home, { recursive: true, force: true });
|
|
56
|
+
if (previousPidfile === undefined)
|
|
57
|
+
delete process.env['CRTR_PIDFILE'];
|
|
58
|
+
else
|
|
59
|
+
process.env['CRTR_PIDFILE'] = previousPidfile;
|
|
50
60
|
});
|
|
51
61
|
test('managed child: parent on the spine + active subscription + provenance', () => {
|
|
52
62
|
createNode(spawner('A'));
|
|
@@ -17,10 +17,11 @@ writeFileSync(pageSource, 'export default () => (\n <Page title="A page" subtit
|
|
|
17
17
|
const page = (id, emittedAt) => submitPage({ dir: ticketDir(id), sourceFile: pageSource, source: { emittedAt, nodeId: 'node-1' }, delivery: { placement: 'inline', inbox: true, reply: true } });
|
|
18
18
|
const answer = { go: { text: 'yes', edited: true } };
|
|
19
19
|
async function waitFor(paths) {
|
|
20
|
-
|
|
21
|
-
|
|
20
|
+
const deadline = Date.now() + 10_000;
|
|
21
|
+
while (!paths.every(existsSync)) {
|
|
22
|
+
if (Date.now() >= deadline)
|
|
22
23
|
throw new Error(`workers did not reach barrier: ${paths.join(', ')}`);
|
|
23
|
-
await new Promise((resolvePromise) => setTimeout(resolvePromise,
|
|
24
|
+
await new Promise((resolvePromise) => setTimeout(resolvePromise, 10));
|
|
24
25
|
}
|
|
25
26
|
}
|
|
26
27
|
page('z-last', '2025-01-01T00:00:00.000Z');
|
package/package.json
CHANGED
package/runtime.lock.json
CHANGED
|
@@ -1,12 +1,12 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@north-light/crouter",
|
|
3
|
-
"version": "0.3.
|
|
3
|
+
"version": "0.3.230",
|
|
4
4
|
"lockfileVersion": 3,
|
|
5
5
|
"requires": true,
|
|
6
6
|
"packages": {
|
|
7
7
|
"": {
|
|
8
8
|
"name": "@north-light/crouter",
|
|
9
|
-
"version": "0.3.
|
|
9
|
+
"version": "0.3.230",
|
|
10
10
|
"hasInstallScript": true,
|
|
11
11
|
"license": "MIT",
|
|
12
12
|
"dependencies": {
|