@north-light/crouter 0.3.282 → 0.3.284
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/dist/api/client.d.ts +22 -4
- package/dist/api/client.js +34 -0
- package/dist/api/dto/broker-ops.d.ts +11 -0
- package/dist/api/dto/broker.d.ts +3 -8
- package/dist/api/dto/common.d.ts +1 -1
- package/dist/api/dto/messages.d.ts +2 -2
- package/dist/api/dto/node-outcomes.d.ts +80 -0
- package/dist/api/dto/node-outcomes.js +2 -0
- package/dist/api/dto/nodes.d.ts +9 -0
- package/dist/api/dto/profiles.d.ts +5 -0
- package/dist/api/dto/reports.d.ts +45 -0
- package/dist/api/index.d.ts +1 -0
- package/dist/api/index.js +1 -0
- package/dist/api/routes.d.ts +7 -0
- package/dist/api/routes.js +7 -0
- package/dist/builtin-pi-packages/pi-crtr-extensions/extensions/provider-rotation.js +24 -3
- package/dist/builtin-pi-packages/pi-crtr-extensions/extensions/provider-rotation.ts +31 -4
- package/dist/clients/attach/overlays/graph.d.ts +5 -0
- package/dist/clients/attach/overlays/graph.js +65 -47
- package/dist/clients/attach/overlays/pickers.js +8 -9
- package/dist/clients/attach/render/card-presentation.js +1 -1
- package/dist/clients/attach/viewer.js +1286 -1260
- package/dist/commands/api-client.js +22 -23
- package/dist/commands/node/create.js +38 -3
- package/dist/commands/node/inspect.js +3 -7
- package/dist/commands/node/message.js +3 -3
- package/dist/commands/node/outcome.d.ts +2 -0
- package/dist/commands/node/outcome.js +134 -0
- package/dist/commands/node.js +2 -1
- package/dist/commands/profile/pause.js +17 -14
- package/dist/commands/push.js +102 -1
- package/dist/commands/sys/__tests__/setup-core.test.js +4 -1
- package/dist/commands/sys/panels/broker-limits-panel.js +7 -1
- package/dist/commands/sys/panels/models-panel.js +2 -2
- package/dist/commands/sys/settings-shell.d.ts +1 -1
- package/dist/commands/sys/settings-shell.js +6 -0
- package/dist/commands/sys/settings.js +3 -3
- package/dist/core/__tests__/fixtures/fake-engine.d.ts +2 -0
- package/dist/core/__tests__/fixtures/fake-engine.js +42 -4
- package/dist/core/__tests__/helpers/harness.d.ts +1 -0
- package/dist/core/__tests__/helpers/harness.js +9 -0
- package/dist/core/__tests__/integration/profile-pause.test.d.ts +1 -0
- package/dist/core/__tests__/integration/profile-pause.test.js +115 -0
- package/dist/core/__tests__/node-outcome.test.d.ts +1 -0
- package/dist/core/__tests__/node-outcome.test.js +184 -0
- package/dist/core/__tests__/revive-parked-fresh.test.js +13 -20
- package/dist/core/__tests__/seam/broker-provider-retry.test.js +6 -2
- package/dist/core/__tests__/seam/dormancy-release.test.js +3 -3
- package/dist/core/__tests__/seam/node-outcome-delivery.test.d.ts +1 -0
- package/dist/core/__tests__/seam/node-outcome-delivery.test.js +153 -0
- package/dist/core/bash-jobs.d.ts +6 -0
- package/dist/core/bash-jobs.js +22 -0
- package/dist/core/canvas/canvas.d.ts +41 -3
- package/dist/core/canvas/canvas.js +153 -39
- package/dist/core/canvas/crons.js +2 -1
- package/dist/core/canvas/index.d.ts +1 -0
- package/dist/core/canvas/index.js +1 -0
- package/dist/core/canvas/migrations.js +47 -0
- package/dist/core/canvas/node-outcome-deliveries.d.ts +80 -0
- package/dist/core/canvas/node-outcome-deliveries.js +163 -0
- package/dist/core/canvas/pid.d.ts +7 -0
- package/dist/core/canvas/pid.js +17 -0
- package/dist/core/canvas/render-source.js +2 -2
- package/dist/core/canvas/types.d.ts +42 -1
- package/dist/core/canvas/types.js +2 -1
- package/dist/core/config.d.ts +4 -1
- package/dist/core/config.js +25 -1
- package/dist/core/feed/feed.d.ts +8 -0
- package/dist/core/feed/feed.js +25 -4
- package/dist/core/feed/inbox.d.ts +5 -0
- package/dist/core/feed/inbox.js +13 -0
- package/dist/core/keybindings/catalog.d.ts +1 -1
- package/dist/core/keybindings/catalog.js +3 -0
- package/dist/core/profiles/manifest.d.ts +3 -1
- package/dist/core/profiles/manifest.js +6 -1
- package/dist/core/runtime/broker/daemon-ops.d.ts +3 -1
- package/dist/core/runtime/broker/daemon-ops.js +6 -0
- package/dist/core/runtime/broker/engine-drive.d.ts +2 -3
- package/dist/core/runtime/broker/engine-drive.js +74 -27
- package/dist/core/runtime/broker/frame-dispatch.js +1 -0
- package/dist/core/runtime/broker/read-ops.js +14 -0
- package/dist/core/runtime/broker/rebind.js +0 -7
- package/dist/core/runtime/broker-protocol.d.ts +1 -6
- package/dist/core/runtime/fleet.d.ts +7 -0
- package/dist/core/runtime/headless-pi.d.ts +13 -0
- package/dist/core/runtime/headless-pi.js +10 -2
- package/dist/core/runtime/lifecycle.d.ts +6 -1
- package/dist/core/runtime/lifecycle.js +50 -1
- package/dist/core/runtime/nodes.d.ts +5 -1
- package/dist/core/runtime/nodes.js +2 -1
- package/dist/core/runtime/outcome-document.d.ts +70 -0
- package/dist/core/runtime/outcome-document.js +97 -0
- package/dist/core/runtime/revive.js +16 -21
- package/dist/core/runtime/spawn.d.ts +5 -1
- package/dist/core/runtime/spawn.js +18 -7
- package/dist/core/runtime/stamp/channel.d.ts +3 -5
- package/dist/core/runtime/stamp/channel.js +3 -10
- package/dist/core/runtime/stop-guard.js +2 -2
- package/dist/core/runtime/structured-output.d.ts +24 -3
- package/dist/core/runtime/structured-output.js +42 -3
- package/dist/core/runtime/tmux-bindings.js +4 -0
- package/dist/core/runtime/warm-pool.d.ts +5 -2
- package/dist/core/runtime/warm-pool.js +5 -5
- package/dist/core/subscription-state.js +7 -5
- package/dist/core/substrate/render.js +10 -10
- package/dist/core/user-settings.d.ts +18 -1
- package/dist/core/user-settings.js +37 -0
- package/dist/daemon/__tests__/node-deadline-lane.test.d.ts +1 -0
- package/dist/daemon/__tests__/node-deadline-lane.test.js +76 -0
- package/dist/daemon/__tests__/node-outcome-birth-invariants.test.d.ts +1 -0
- package/dist/daemon/__tests__/node-outcome-birth-invariants.test.js +122 -0
- package/dist/daemon/__tests__/node-outcome-delivery.test.d.ts +1 -0
- package/dist/daemon/__tests__/node-outcome-delivery.test.js +86 -0
- package/dist/daemon/api/__tests__/broker-settle-park.test.js +86 -18
- package/dist/daemon/api/__tests__/profile-launch-gates.test.js +10 -1
- package/dist/daemon/api/__tests__/seam/node-outcome-api.test.d.ts +1 -0
- package/dist/daemon/api/__tests__/seam/node-outcome-api.test.js +159 -0
- package/dist/daemon/api/__tests__/submit-result.test.d.ts +1 -0
- package/dist/daemon/api/__tests__/submit-result.test.js +214 -0
- package/dist/daemon/api/handlers/broker-ops.js +53 -24
- package/dist/daemon/api/handlers/canvas.js +2 -2
- package/dist/daemon/api/handlers/messages.js +12 -2
- package/dist/daemon/api/handlers/node-outcomes.d.ts +2 -0
- package/dist/daemon/api/handlers/node-outcomes.js +136 -0
- package/dist/daemon/api/handlers/nodes.js +67 -1
- package/dist/daemon/api/handlers/profiles.js +53 -1
- package/dist/daemon/api/handlers/reports.js +197 -33
- package/dist/daemon/api/map.d.ts +7 -2
- package/dist/daemon/api/map.js +78 -4
- package/dist/daemon/api/server.js +74 -7
- package/dist/daemon/crtrd.js +12 -0
- package/dist/daemon/fleet.d.ts +4 -8
- package/dist/daemon/fleet.js +48 -9
- package/dist/daemon/messaging/node-message.js +2 -1
- package/dist/daemon/node-outcome/deliver-outcome.d.ts +14 -0
- package/dist/daemon/node-outcome/deliver-outcome.js +155 -0
- package/dist/daemon/park-activity.d.ts +9 -0
- package/dist/daemon/park-activity.js +30 -0
- package/dist/daemon/park-pending.d.ts +10 -5
- package/dist/daemon/park-pending.js +18 -8
- package/dist/daemon/reconcilers/bash-deadline.js +6 -0
- package/dist/daemon/reconcilers/broker-supervision.d.ts +3 -7
- package/dist/daemon/reconcilers/broker-supervision.js +29 -33
- package/dist/daemon/reconcilers/node-deadline.d.ts +5 -0
- package/dist/daemon/reconcilers/node-deadline.js +82 -0
- package/dist/daemon/reconcilers/node-lifecycle/freeze-lane.js +9 -1
- package/dist/daemon/reconcilers/node-lifecycle/tick.js +12 -3
- package/dist/daemon/reconcilers/node-outcome-delivery-lane.d.ts +10 -0
- package/dist/daemon/reconcilers/node-outcome-delivery-lane.js +41 -0
- package/dist/pi-extensions/__tests__/canvas-structured-output.test.js +66 -66
- package/dist/pi-extensions/canvas-structured-output.d.ts +9 -5
- package/dist/pi-extensions/canvas-structured-output.js +23 -297
- package/dist/shared/env.d.ts +8 -0
- package/dist/shared/env.js +14 -0
- package/dist/shared/generated-context.d.ts +3 -9
- package/dist/shared/generated-context.js +8 -16
- package/dist/types.d.ts +12 -0
- package/dist/types.js +1 -0
- package/package.json +9 -8
- package/runtime.lock.json +227 -383
|
@@ -66,6 +66,12 @@ export class SettingsShell {
|
|
|
66
66
|
label: "Models",
|
|
67
67
|
panel: this.models,
|
|
68
68
|
},
|
|
69
|
+
{
|
|
70
|
+
id: "provider-options",
|
|
71
|
+
group: "Intelligence",
|
|
72
|
+
label: "Provider options",
|
|
73
|
+
panel: prefs("providers", "Provider options", "How a provider serves your turns, once it is authenticated."),
|
|
74
|
+
},
|
|
69
75
|
{
|
|
70
76
|
id: "keybindings",
|
|
71
77
|
group: "Interface",
|
|
@@ -9,11 +9,11 @@ import { BROKER_LIMIT_OVERRIDE_PRECEDENCE } from './setup.js';
|
|
|
9
9
|
/** Rail destinations, in rail order. Kept here rather than imported from the
|
|
10
10
|
* shell so registering the `sys` subtree still costs no pi module graph. */
|
|
11
11
|
const PAGES = [
|
|
12
|
-
'providers', 'models', 'keybindings', 'display', 'profiles', 'panes', 'broker-limits', 'updates',
|
|
12
|
+
'providers', 'models', 'provider-options', 'keybindings', 'display', 'profiles', 'panes', 'broker-limits', 'updates',
|
|
13
13
|
];
|
|
14
14
|
export const sysSettingsLeaf = defineLeaf({
|
|
15
15
|
name: 'settings',
|
|
16
|
-
description: 'open the settings page — providers, models, keybindings, display, profiles, panes, broker limits, updates',
|
|
16
|
+
description: 'open the settings page — providers, models, provider options, keybindings, display, profiles, panes, broker limits, updates',
|
|
17
17
|
whenToUse: 'the user wants to change crouter configuration interactively. Agents should use sys config get/set, which is scriptable; sys setup installs a fresh machine.',
|
|
18
18
|
help: {
|
|
19
19
|
name: 'sys settings',
|
|
@@ -31,7 +31,7 @@ export const sysSettingsLeaf = defineLeaf({
|
|
|
31
31
|
output: [],
|
|
32
32
|
outputKind: 'object',
|
|
33
33
|
effects: [
|
|
34
|
-
'Opens a pi-tui page whose left rail groups ongoing configuration: Intelligence (providers, models), Interface (keybindings, display), and Workspace (profiles, panes, broker limits, updates).',
|
|
34
|
+
'Opens a pi-tui page whose left rail groups ongoing configuration: Intelligence (providers, models, provider options), Interface (keybindings, display), and Workspace (profiles, panes, broker limits, updates).',
|
|
35
35
|
'Profiles edits are immediate: adding or removing a project directory, cycling its memory rung, setting a home, renaming, pausing and pinning a directory default all write the same profile manifests and pin file as the `crtr profile` leaves; deleting a profile runs the same cascade as `crtr profile delete --force`.',
|
|
36
36
|
'Preference, model-ladder and keybinding edits save to the user config as you make them; saved keybinding changes also refresh crouter-owned tmux bindings without overwriting occupied user keys.',
|
|
37
37
|
`Broker warning and automatic-revive hard-cap values save under user config brokerThresholds; crtrd reads them on its next supervision tick. ${BROKER_LIMIT_OVERRIDE_PRECEDENCE}`,
|
|
@@ -171,6 +171,8 @@ declare class FakeSession {
|
|
|
171
171
|
get model(): FakeModel | undefined;
|
|
172
172
|
private currentModel;
|
|
173
173
|
private currentThinkingLevel;
|
|
174
|
+
private bashAbort;
|
|
175
|
+
private bashRun;
|
|
174
176
|
get thinkingLevel(): ThinkingLevel | undefined;
|
|
175
177
|
setThinkingLevel(level: ThinkingLevel): void;
|
|
176
178
|
cycleThinkingLevel(): ThinkingLevel;
|
|
@@ -35,6 +35,7 @@ import { join, dirname } from 'node:path';
|
|
|
35
35
|
import { randomUUID } from 'node:crypto';
|
|
36
36
|
import { DefaultResourceLoader, getAgentDir } from '@earendil-works/pi-coding-agent';
|
|
37
37
|
import { MANAGED_PROVIDER_COOLING_DIAGNOSTIC } from '../../runtime/managed-provider-cooling.js';
|
|
38
|
+
import registerCanvasBashValve, { createValveOperations } from '../../../pi-extensions/canvas-bash-valve.js';
|
|
38
39
|
/** Test seams for prompt-scoped failures before session_start. The one-shot
|
|
39
40
|
* variant records its first failure per node so the next cycling retry can
|
|
40
41
|
* reach boot confirmation. */
|
|
@@ -369,6 +370,8 @@ class FakeSession {
|
|
|
369
370
|
// this getter — store whatever it hands us.
|
|
370
371
|
currentModel;
|
|
371
372
|
currentThinkingLevel;
|
|
373
|
+
bashAbort;
|
|
374
|
+
bashRun;
|
|
372
375
|
get thinkingLevel() {
|
|
373
376
|
return this.currentThinkingLevel;
|
|
374
377
|
}
|
|
@@ -517,10 +520,12 @@ class FakeSession {
|
|
|
517
520
|
}
|
|
518
521
|
abort() {
|
|
519
522
|
const held = this.heldTurn;
|
|
520
|
-
if (held
|
|
521
|
-
|
|
522
|
-
|
|
523
|
-
|
|
523
|
+
if (held !== undefined) {
|
|
524
|
+
held.release();
|
|
525
|
+
return held.settled;
|
|
526
|
+
}
|
|
527
|
+
this.bashAbort?.abort();
|
|
528
|
+
return this.bashRun ?? Promise.resolve();
|
|
524
529
|
}
|
|
525
530
|
/** In-place tree rewind (broker `navigate_tree`). Mirrors the real SDK's
|
|
526
531
|
* contract: the session file is unchanged, the live history is truncated,
|
|
@@ -975,6 +980,39 @@ class FakeSession {
|
|
|
975
980
|
tool: cmd.tool,
|
|
976
981
|
});
|
|
977
982
|
break;
|
|
983
|
+
case 'bash': {
|
|
984
|
+
const nodeId = process.env['CRTR_NODE_ID'];
|
|
985
|
+
const contextDir = process.env['CRTR_CONTEXT_DIR'];
|
|
986
|
+
if (nodeId === undefined || contextDir === undefined)
|
|
987
|
+
throw new Error('bash control requires canvas node paths');
|
|
988
|
+
this.streaming = true;
|
|
989
|
+
await this.fire('agent_start', {}, ctx);
|
|
990
|
+
this.emit({ type: 'agent_start' });
|
|
991
|
+
// The resource loader imports extensions through jiti while this fixture
|
|
992
|
+
// imports its valve operations through Node. Register the same module
|
|
993
|
+
// instance here so its SIGTERM listener owns this real test job too.
|
|
994
|
+
registerCanvasBashValve({ on: () => { } });
|
|
995
|
+
const controller = new AbortController();
|
|
996
|
+
this.bashAbort = controller;
|
|
997
|
+
const run = createValveOperations(nodeId, contextDir).exec(cmd.command ?? 'true', process.cwd(), {
|
|
998
|
+
onData: () => { },
|
|
999
|
+
signal: controller.signal,
|
|
1000
|
+
}).then(() => { });
|
|
1001
|
+
this.bashRun = run;
|
|
1002
|
+
try {
|
|
1003
|
+
await run;
|
|
1004
|
+
}
|
|
1005
|
+
finally {
|
|
1006
|
+
this.bashAbort = undefined;
|
|
1007
|
+
this.bashRun = undefined;
|
|
1008
|
+
this.streaming = false;
|
|
1009
|
+
}
|
|
1010
|
+
await this.fire('agent_end', { messages: [] }, ctx);
|
|
1011
|
+
this.emit({ type: 'agent_end', messages: [], willRetry: false });
|
|
1012
|
+
await this.fire('agent_settled', { type: 'agent_settled' }, ctx);
|
|
1013
|
+
this.emit({ type: 'agent_settled' });
|
|
1014
|
+
break;
|
|
1015
|
+
}
|
|
978
1016
|
case 'turn':
|
|
979
1017
|
this.streaming = true;
|
|
980
1018
|
await this.fire('agent_start', {}, ctx);
|
|
@@ -514,6 +514,7 @@ export async function createHarness(opts = {}) {
|
|
|
514
514
|
...(o.mode !== undefined ? { mode: o.mode } : {}),
|
|
515
515
|
...(o.lifecycle !== undefined ? { rootLifecycle: o.lifecycle } : {}),
|
|
516
516
|
...(o.model !== undefined ? { model: o.model } : {}),
|
|
517
|
+
...(o.profile !== undefined ? { profile: o.profile } : {}),
|
|
517
518
|
});
|
|
518
519
|
await harness.awaitBoot(result.node.node_id);
|
|
519
520
|
return result.node.node_id;
|
|
@@ -572,6 +573,14 @@ export async function createHarness(opts = {}) {
|
|
|
572
573
|
`pi_pid_identity=${n?.pi_pid_identity ?? 'null'} status=${n?.status ?? 'null'} ` +
|
|
573
574
|
`intent=${n?.intent ?? 'null'} pi_session_id=${n?.pi_session_id ?? 'null'} ` +
|
|
574
575
|
`bootCount=${bootCount(nodeId)} minCount=${minCount}\n`);
|
|
576
|
+
// Diagnostic (2026-09-03 CI investigation): confirm or rule out the
|
|
577
|
+
// backoff-gated respawn path (fleet.ts #respawn's `assess` branch)
|
|
578
|
+
// stranding this row behind a `respawn_not_before` window that never
|
|
579
|
+
// gets a further h.tick() to clear it, since awaitBoot itself never
|
|
580
|
+
// ticks. Remove once the real CI failure is diagnosed.
|
|
581
|
+
process.stderr.write(`[awaitBoot] respawn state: respawn_failures=${n?.respawn_failures ?? 'null'} ` +
|
|
582
|
+
`respawn_not_before=${n?.respawn_not_before ?? 'null'} launched_at=${n?.launched_at ?? 'null'} ` +
|
|
583
|
+
`last_settled_at=${n?.last_settled_at ?? 'null'} now=${new Date().toISOString()}\n`);
|
|
575
584
|
// Identity-aware liveness: what does the current process state say about recorded identity?
|
|
576
585
|
if (n?.pi_pid !== null && n?.pi_pid !== undefined) {
|
|
577
586
|
const liveness = recordedPidLiveness(n.pi_pid, n.pi_pid_identity);
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
export {};
|
|
@@ -0,0 +1,115 @@
|
|
|
1
|
+
import { test } from 'node:test';
|
|
2
|
+
import assert from 'node:assert/strict';
|
|
3
|
+
import { existsSync, readFileSync, readdirSync } from 'node:fs';
|
|
4
|
+
import { contextDir } from '../../canvas/paths.js';
|
|
5
|
+
import { getRow } from '../../canvas/canvas.js';
|
|
6
|
+
import { bashJobPaths, bashJobsDir } from '../../bash-jobs.js';
|
|
7
|
+
import { readCursor } from '../../feed/inbox.js';
|
|
8
|
+
import { transition } from '../../runtime/lifecycle.js';
|
|
9
|
+
import { ensureRootProfile, pauseProfile } from '../../profiles/manifest.js';
|
|
10
|
+
import { createHeadlessHarness } from '../helpers/harness.js';
|
|
11
|
+
function runningJob(nodeId) {
|
|
12
|
+
const dir = bashJobsDir(contextDir(nodeId));
|
|
13
|
+
try {
|
|
14
|
+
for (const jobId of readdirSync(dir)) {
|
|
15
|
+
const paths = bashJobPaths(contextDir(nodeId), jobId);
|
|
16
|
+
if (existsSync(paths.jobRun))
|
|
17
|
+
return paths;
|
|
18
|
+
}
|
|
19
|
+
}
|
|
20
|
+
catch {
|
|
21
|
+
// The valve has not created its job directory yet.
|
|
22
|
+
}
|
|
23
|
+
return undefined;
|
|
24
|
+
}
|
|
25
|
+
test('profile pause backgrounds real bash, retains its inbox cursor, and recovers after a quick resume without failure', { timeout: 120_000 }, async () => {
|
|
26
|
+
const h = await createHeadlessHarness({ sessionPrefix: 'crtr-profile-pause' });
|
|
27
|
+
try {
|
|
28
|
+
const profile = ensureRootProfile();
|
|
29
|
+
const parent = h.fabricateBrokerNode({ lifecycle: 'resident' });
|
|
30
|
+
const child = await h.spawnHeadlessChild(parent, 'stay available while the profile pauses', { profile: profile.profileId });
|
|
31
|
+
h.fakeCmd(child, { cmd: 'bash', command: "sh -c 'sleep 2; printf preserved'" });
|
|
32
|
+
const job = await h.waitFor(() => runningJob(child), { label: 'valve job.run' });
|
|
33
|
+
assert.ok(existsSync(job.jobRun), 'the bash valve owns the foreground command before pause');
|
|
34
|
+
const paused = h.cli(null, ['profile', 'pause', profile.profileId]);
|
|
35
|
+
assert.equal(paused.code, 0, paused.stderr);
|
|
36
|
+
await h.tick();
|
|
37
|
+
assert.equal(h.bootCount(child), 1, 'the lifecycle pass while paused launches no replacement');
|
|
38
|
+
const retained = h.cli(null, ['node', 'message', 'send', '--to', child, 'mail retained while profile is paused']);
|
|
39
|
+
assert.equal(retained.code, 0, retained.stderr);
|
|
40
|
+
const retainedEntry = h.inbox(child).find((entry) => entry.data?.['body'] === 'mail retained while profile is paused');
|
|
41
|
+
assert.ok(retainedEntry, 'the message was durably appended');
|
|
42
|
+
assert.equal(readCursor(child), undefined, 'pause leaves the durable inbox cursor unadvanced');
|
|
43
|
+
const resumed = h.cli(null, ['profile', 'resume', profile.profileId]);
|
|
44
|
+
assert.equal(resumed.code, 0, resumed.stderr);
|
|
45
|
+
await h.awaitFleetExit(child);
|
|
46
|
+
assert.ok(existsSync(job.jobBg), 'SIGTERM atomically handed job.run to job.bg');
|
|
47
|
+
assert.equal(existsSync(job.jobRun), false, 'the foreground valve ownership is gone');
|
|
48
|
+
await h.waitFor(() => existsSync(job.jobExit), { label: 'background bash completion' });
|
|
49
|
+
assert.equal(readFileSync(job.jobExit, 'utf8').trim(), '0', 'the handoff command completed after its broker exited');
|
|
50
|
+
assert.equal(readCursor(child), undefined, 'neither retained message nor background completion advanced the stopped broker cursor');
|
|
51
|
+
await h.tick();
|
|
52
|
+
assert.equal(h.bootCount(child), 1, 'the first resumed policy pass only consumes the pause interruption');
|
|
53
|
+
assert.equal(getRow(child)?.respawn_failures, 0, 'the intentional SIGTERM never counts as a crash');
|
|
54
|
+
await h.tick();
|
|
55
|
+
// `#respawn`'s backoff-assessment defers this exact relaunch on its first
|
|
56
|
+
// assessment: this node's original spawn (`spawnChild`'s direct
|
|
57
|
+
// `headlessBrokerHost.launch`) never records `launched_at`, so
|
|
58
|
+
// `decideRespawnAttempt` can never see `last_settled_at > launched_at` on
|
|
59
|
+
// this first-ever `#respawn` call for the row and always takes the
|
|
60
|
+
// `respawn_not_before` backoff branch (fleet.ts's RESPAWN_BACKOFF_BASE_MS,
|
|
61
|
+
// currently 5s) instead of launching. A THIRD tick, issued once real time
|
|
62
|
+
// has passed that window, finds `respawn_not_before` already set (so
|
|
63
|
+
// `#respawn` skips reassessment this time) and actually calls `reviveNode`.
|
|
64
|
+
// Real-time sleep, not a fake clock: `now()` here is the daemon's real
|
|
65
|
+
// `Date.now`, so only the wall clock — not another tick — advances it.
|
|
66
|
+
await new Promise((r) => setTimeout(r, 5_500));
|
|
67
|
+
await h.tick();
|
|
68
|
+
await h.awaitBoot(child, { minCount: 2, timeoutMs: 30_000 });
|
|
69
|
+
const deliveries = await h.awaitWake(child, { match: /mail retained while profile is paused/ });
|
|
70
|
+
assert.ok(deliveries.some((content) => /mail retained while profile is paused/.test(content)), 'the resumed watcher delivers the retained backlog');
|
|
71
|
+
}
|
|
72
|
+
finally {
|
|
73
|
+
await h.dispose();
|
|
74
|
+
}
|
|
75
|
+
});
|
|
76
|
+
test('profile pause preserves an idle-release row so mail queued during pause wakes it', { timeout: 120_000 }, async () => {
|
|
77
|
+
const h = await createHeadlessHarness({ sessionPrefix: 'crtr-profile-pause-idle' });
|
|
78
|
+
try {
|
|
79
|
+
const profile = ensureRootProfile();
|
|
80
|
+
const parent = h.fabricateBrokerNode({ lifecycle: 'resident' });
|
|
81
|
+
const child = await h.spawnHeadlessChild(parent, 'await ordinary mail', { profile: profile.profileId, lifecycle: 'resident' });
|
|
82
|
+
transition(child, 'release');
|
|
83
|
+
const paused = h.cli(null, ['profile', 'pause', profile.profileId]);
|
|
84
|
+
assert.equal(paused.code, 0, paused.stderr);
|
|
85
|
+
const queued = h.cli(null, ['node', 'message', 'send', '--to', child, 'ordinary mail queued during pause']);
|
|
86
|
+
assert.equal(queued.code, 0, queued.stderr);
|
|
87
|
+
await h.awaitFleetExit(child);
|
|
88
|
+
const resumed = h.cli(null, ['profile', 'resume', profile.profileId]);
|
|
89
|
+
assert.equal(resumed.code, 0, resumed.stderr);
|
|
90
|
+
await h.tick();
|
|
91
|
+
await h.tick();
|
|
92
|
+
await h.awaitBoot(child, { minCount: 2, timeoutMs: 60_000 });
|
|
93
|
+
const deliveries = await h.awaitWake(child, { match: /ordinary mail queued during pause/ });
|
|
94
|
+
assert.ok(deliveries.some((content) => /ordinary mail queued during pause/.test(content)), 'the idle-release wake is not stranded behind the pause marker');
|
|
95
|
+
}
|
|
96
|
+
finally {
|
|
97
|
+
await h.dispose();
|
|
98
|
+
}
|
|
99
|
+
});
|
|
100
|
+
test('lifecycle catches a broker that became live after the pause command fast scan', { timeout: 60_000 }, async () => {
|
|
101
|
+
const h = await createHeadlessHarness({ sessionPrefix: 'crtr-profile-pause-race' });
|
|
102
|
+
try {
|
|
103
|
+
const profile = ensureRootProfile();
|
|
104
|
+
const parent = h.fabricateBrokerNode({ lifecycle: 'resident' });
|
|
105
|
+
const child = await h.spawnHeadlessChild(parent, 'broker launched after pause scan', { profile: profile.profileId });
|
|
106
|
+
pauseProfile(profile.profileId);
|
|
107
|
+
await h.tick();
|
|
108
|
+
await h.awaitFleetExit(child);
|
|
109
|
+
assert.equal(h.bootCount(child), 1, 'the paused lifecycle tick terminates the late live broker instead of leaving it running');
|
|
110
|
+
assert.equal(getRow(child)?.respawn_failures, 0, 'the late broker interruption has no failure cost');
|
|
111
|
+
}
|
|
112
|
+
finally {
|
|
113
|
+
await h.dispose();
|
|
114
|
+
}
|
|
115
|
+
});
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
export {};
|
|
@@ -0,0 +1,184 @@
|
|
|
1
|
+
import { after, afterEach, beforeEach, test } from 'node:test';
|
|
2
|
+
import assert from 'node:assert/strict';
|
|
3
|
+
import { DatabaseSync } from 'node:sqlite';
|
|
4
|
+
import { mkdirSync, mkdtempSync, rmSync } from 'node:fs';
|
|
5
|
+
import { tmpdir } from 'node:os';
|
|
6
|
+
import { join } from 'node:path';
|
|
7
|
+
import { closeDb, MIGRATIONS } from '../canvas/db.js';
|
|
8
|
+
import { createNode, getRow } from '../canvas/canvas.js';
|
|
9
|
+
import { armOutcomeDelivery, claimOutcomeDelivery, outcomeDeliveryBackoffMs, readOutcomeDelivery, recoverRunningOutcomeDeliveries, settleOutcomeDelivery, } from '../canvas/node-outcome-deliveries.js';
|
|
10
|
+
import { transition } from '../runtime/lifecycle.js';
|
|
11
|
+
import { composeNodeOutcomeDocument } from '../runtime/outcome-document.js';
|
|
12
|
+
import { pushFinal } from '../feed/feed.js';
|
|
13
|
+
import { OUTCOME_PAYLOAD_MAX_BYTES } from '../canvas/types.js';
|
|
14
|
+
let home;
|
|
15
|
+
const priorHome = process.env['CRTR_HOME'];
|
|
16
|
+
function node(nodeId) {
|
|
17
|
+
return {
|
|
18
|
+
node_id: nodeId,
|
|
19
|
+
name: nodeId,
|
|
20
|
+
created: new Date().toISOString(),
|
|
21
|
+
cwd: home,
|
|
22
|
+
kind: 'general',
|
|
23
|
+
mode: 'base',
|
|
24
|
+
lifecycle: 'terminal',
|
|
25
|
+
status: 'active',
|
|
26
|
+
};
|
|
27
|
+
}
|
|
28
|
+
beforeEach(() => {
|
|
29
|
+
closeDb();
|
|
30
|
+
home = mkdtempSync(join(tmpdir(), 'crtr-node-outcome-'));
|
|
31
|
+
process.env['CRTR_HOME'] = home;
|
|
32
|
+
});
|
|
33
|
+
afterEach(() => {
|
|
34
|
+
closeDb();
|
|
35
|
+
rmSync(home, { recursive: true, force: true });
|
|
36
|
+
});
|
|
37
|
+
after(() => {
|
|
38
|
+
closeDb();
|
|
39
|
+
if (priorHome === undefined)
|
|
40
|
+
delete process.env['CRTR_HOME'];
|
|
41
|
+
else
|
|
42
|
+
process.env['CRTR_HOME'] = priorHome;
|
|
43
|
+
});
|
|
44
|
+
test('the terminal outcome latch survives competing terminal transitions and revive', () => {
|
|
45
|
+
createNode(node('latched'));
|
|
46
|
+
transition('latched', 'finish');
|
|
47
|
+
transition('latched', 'cancel');
|
|
48
|
+
let row = getRow('latched');
|
|
49
|
+
assert.deepEqual({ kind: row.outcome_kind, reason: row.outcome_reason, revision: row.outcome_revision }, {
|
|
50
|
+
kind: 'failure', reason: 'finished', revision: 1,
|
|
51
|
+
});
|
|
52
|
+
transition('latched', 'revive');
|
|
53
|
+
row = getRow('latched');
|
|
54
|
+
assert.equal(row.outcome_at === null, false, 'revive must not clear a latched outcome');
|
|
55
|
+
assert.equal(row.outcome_revision, 1);
|
|
56
|
+
});
|
|
57
|
+
test('outcome detail keeps only a 500-byte first line', () => {
|
|
58
|
+
createNode(node('detail'));
|
|
59
|
+
transition('detail', 'crash', {
|
|
60
|
+
outcome: { detail: { schema: 'crtr.node-outcome-detail/v1', message: `${'x'.repeat(1_000)}\nignored` } },
|
|
61
|
+
});
|
|
62
|
+
const detail = JSON.parse(getRow('detail').outcome_detail_json);
|
|
63
|
+
assert.equal(Buffer.byteLength(detail.message, 'utf8'), 500);
|
|
64
|
+
assert.equal(detail.message.includes('\n'), false);
|
|
65
|
+
});
|
|
66
|
+
test('the outcome composer is total and omits an oversized structured result', () => {
|
|
67
|
+
const meta = node('document');
|
|
68
|
+
meta.description = 'x'.repeat(2 * 1024);
|
|
69
|
+
createNode(meta);
|
|
70
|
+
const document = composeNodeOutcomeDocument({
|
|
71
|
+
node: meta,
|
|
72
|
+
outcome: { kind: 'result', reason: 'finalized', at: new Date().toISOString(), revision: 1, detailJson: null },
|
|
73
|
+
action: { name: 'test-action', payload: { callback: 'one' } },
|
|
74
|
+
finalReport: 'final.md',
|
|
75
|
+
structuredResult: { value: 'y'.repeat(300 * 1024) },
|
|
76
|
+
});
|
|
77
|
+
assert.ok(Buffer.byteLength(JSON.stringify(document), 'utf8') <= 256 * 1024);
|
|
78
|
+
assert.equal(Buffer.byteLength(document.node.description, 'utf8') <= 1024, true);
|
|
79
|
+
assert.equal(document.result?.structuredResult, undefined);
|
|
80
|
+
assert.equal(document.result?.structuredResultOmitted, true);
|
|
81
|
+
});
|
|
82
|
+
test('the outcome composer caps a failure document with an oversized node name', () => {
|
|
83
|
+
const meta = node('failure-document');
|
|
84
|
+
meta.name = 'x'.repeat(300 * 1024);
|
|
85
|
+
const document = composeNodeOutcomeDocument({
|
|
86
|
+
node: meta,
|
|
87
|
+
outcome: { kind: 'failure', reason: 'crashed', at: new Date().toISOString(), revision: 1, detailJson: null },
|
|
88
|
+
action: { name: 'test-action', payload: { callback: 'one' } },
|
|
89
|
+
});
|
|
90
|
+
assert.ok(Buffer.byteLength(JSON.stringify(document), 'utf8') <= 256 * 1024);
|
|
91
|
+
assert.ok(Buffer.byteLength(document.node.name, 'utf8') <= 1024);
|
|
92
|
+
assert.equal(document.result, undefined);
|
|
93
|
+
assert.ok(document.failure);
|
|
94
|
+
});
|
|
95
|
+
test('a canonical final supersedes a prior failure outcome and increments revision', async () => {
|
|
96
|
+
createNode(node('supersede'));
|
|
97
|
+
transition('supersede', 'crash');
|
|
98
|
+
transition('supersede', 'revive');
|
|
99
|
+
await pushFinal('supersede', 'recovered and finished');
|
|
100
|
+
const row = getRow('supersede');
|
|
101
|
+
assert.deepEqual({ kind: row.outcome_kind, reason: row.outcome_reason, revision: row.outcome_revision }, {
|
|
102
|
+
kind: 'result', reason: 'finalized', revision: 2,
|
|
103
|
+
});
|
|
104
|
+
assert.equal(row.outcome_detail_json, null);
|
|
105
|
+
});
|
|
106
|
+
test('outcome delivery rejects an oversized payload before a node is armed', () => {
|
|
107
|
+
createNode(node('oversized-payload'));
|
|
108
|
+
assert.throws(() => armOutcomeDelivery({
|
|
109
|
+
nodeId: 'oversized-payload', actionName: 'test-action', argv: ['/bin/true'], cwd: home, configOrigin: 'test', payload: { value: 'x'.repeat(OUTCOME_PAYLOAD_MAX_BYTES) },
|
|
110
|
+
}), /exceeds/);
|
|
111
|
+
assert.equal(readOutcomeDelivery('oversized-payload'), null);
|
|
112
|
+
});
|
|
113
|
+
test('outcome delivery armed after settlement is born pending with its document', () => {
|
|
114
|
+
createNode(node('settled-delivery'));
|
|
115
|
+
transition('settled-delivery', 'crash');
|
|
116
|
+
const delivery = armOutcomeDelivery({
|
|
117
|
+
nodeId: 'settled-delivery', actionName: 'test-action', argv: ['/bin/true'], cwd: home, configOrigin: 'test', payload: { callback: 'one' }, now: 123,
|
|
118
|
+
});
|
|
119
|
+
assert.equal(delivery?.state, 'pending');
|
|
120
|
+
assert.equal(delivery?.nextAttemptAt, 123);
|
|
121
|
+
assert.deepEqual(delivery?.document && {
|
|
122
|
+
schema: delivery.document.schema,
|
|
123
|
+
outcomeId: delivery.document.outcomeId,
|
|
124
|
+
action: delivery.document.action,
|
|
125
|
+
}, {
|
|
126
|
+
schema: 'crtr.node-outcome/v1',
|
|
127
|
+
outcomeId: 'settled-delivery:1',
|
|
128
|
+
action: { name: 'test-action', payload: { callback: 'one' } },
|
|
129
|
+
});
|
|
130
|
+
});
|
|
131
|
+
test('outcome delivery state is fenced, recoverable, and follows the durable backoff', () => {
|
|
132
|
+
createNode(node('delivery'));
|
|
133
|
+
assert.ok(armOutcomeDelivery({
|
|
134
|
+
nodeId: 'delivery', actionName: 'test-action', argv: ['/bin/true'], cwd: home, configOrigin: 'test', payload: { callback: 'one' }, now: 100,
|
|
135
|
+
}));
|
|
136
|
+
transition('delivery', 'crash');
|
|
137
|
+
assert.equal(readOutcomeDelivery('delivery')?.state, 'pending');
|
|
138
|
+
assert.ok(readOutcomeDelivery('delivery')?.document, 'the armed row is stamped with its outcome document');
|
|
139
|
+
const first = claimOutcomeDelivery('delivery', Date.now(), 'daemon-a');
|
|
140
|
+
assert.equal(first.state, 'running');
|
|
141
|
+
assert.equal(settleOutcomeDelivery('delivery', first.attempt, 'daemon-b', { state: 'accepted' }, Date.now()), false, 'a stale claimant cannot settle');
|
|
142
|
+
assert.equal(settleOutcomeDelivery('delivery', first.attempt, 'daemon-a', { state: 'accepted' }, Date.now()), true);
|
|
143
|
+
assert.equal(readOutcomeDelivery('delivery')?.state, 'accepted');
|
|
144
|
+
createNode(node('recover'));
|
|
145
|
+
armOutcomeDelivery({ nodeId: 'recover', actionName: 'test-action', argv: ['/bin/true'], cwd: home, configOrigin: 'test', now: 100 });
|
|
146
|
+
transition('recover', 'crash');
|
|
147
|
+
const running = claimOutcomeDelivery('recover', Date.now(), 'daemon-a');
|
|
148
|
+
assert.equal(running.state, 'running');
|
|
149
|
+
const recoveredAt = Date.now();
|
|
150
|
+
assert.equal(recoverRunningOutcomeDeliveries(recoveredAt), 1);
|
|
151
|
+
assert.deepEqual(readOutcomeDelivery('recover') && {
|
|
152
|
+
state: readOutcomeDelivery('recover').state,
|
|
153
|
+
nextAttemptAt: readOutcomeDelivery('recover').nextAttemptAt,
|
|
154
|
+
claimOwner: readOutcomeDelivery('recover').claimOwner,
|
|
155
|
+
}, { state: 'pending', nextAttemptAt: recoveredAt, claimOwner: null });
|
|
156
|
+
assert.equal(outcomeDeliveryBackoffMs(1), 5_000);
|
|
157
|
+
assert.equal(outcomeDeliveryBackoffMs(2), 10_000);
|
|
158
|
+
assert.equal(outcomeDeliveryBackoffMs(3), 20_000);
|
|
159
|
+
assert.equal(outcomeDeliveryBackoffMs(11), 3_600_000);
|
|
160
|
+
assert.equal(outcomeDeliveryBackoffMs(100), 3_600_000);
|
|
161
|
+
});
|
|
162
|
+
test('v39 migrates a v38 database and backfills finalized and terminal rows', () => {
|
|
163
|
+
const dbPath = join(home, 'v38.db');
|
|
164
|
+
mkdirSync(join(home, 'nodes'), { recursive: true });
|
|
165
|
+
const db = new DatabaseSync(dbPath);
|
|
166
|
+
db.exec('PRAGMA foreign_keys = ON;');
|
|
167
|
+
for (const migration of MIGRATIONS.slice(0, 38))
|
|
168
|
+
migration(db);
|
|
169
|
+
db.prepare(`INSERT INTO nodes (node_id, name, kind, mode, lifecycle, status, cwd, created, final_report, finalized_at, terminal_reason)
|
|
170
|
+
VALUES (?, ?, 'general', 'base', 'terminal', ?, ?, ?, ?, ?, ?)`)
|
|
171
|
+
.run('finalized', 'finalized', 'done', home, '2026-01-01T00:00:00.000Z', 'report.md', '2026-01-01T01:00:00.000Z', 'finalized');
|
|
172
|
+
db.prepare(`INSERT INTO nodes (node_id, name, kind, mode, lifecycle, status, cwd, created, terminal_reason)
|
|
173
|
+
VALUES (?, ?, 'general', 'base', 'terminal', ?, ?, ?, ?)`)
|
|
174
|
+
.run('dead', 'dead', 'dead', home, '2026-01-02T00:00:00.000Z', 'crashed');
|
|
175
|
+
MIGRATIONS[38](db);
|
|
176
|
+
const rows = db.prepare('SELECT node_id, outcome_kind, outcome_reason, outcome_at, outcome_revision FROM nodes ORDER BY node_id').all().map((row) => ({ ...row }));
|
|
177
|
+
assert.deepEqual(rows, [
|
|
178
|
+
{ node_id: 'dead', outcome_kind: 'failure', outcome_reason: 'crashed', outcome_at: '2026-01-02T00:00:00.000Z', outcome_revision: 1 },
|
|
179
|
+
{ node_id: 'finalized', outcome_kind: 'result', outcome_reason: 'finalized', outcome_at: '2026-01-01T01:00:00.000Z', outcome_revision: 1 },
|
|
180
|
+
]);
|
|
181
|
+
assert.ok(db.prepare("SELECT 1 FROM sqlite_master WHERE type = 'table' AND name = 'node_outcome_deliveries'").get());
|
|
182
|
+
assert.ok(db.prepare("SELECT 1 FROM sqlite_master WHERE type = 'index' AND name = 'idx_nodes_deadline'").get());
|
|
183
|
+
db.close();
|
|
184
|
+
});
|
|
@@ -1,17 +1,10 @@
|
|
|
1
1
|
// Run with: node --conditions=crtr-src --import tsx/esm --test src/core/__tests__/revive-parked-fresh.test.ts
|
|
2
2
|
//
|
|
3
|
-
// Re-entering a parked node
|
|
4
|
-
//
|
|
5
|
-
//
|
|
6
|
-
//
|
|
7
|
-
//
|
|
8
|
-
// input-less revival (a bare lifecycle revive, a viewer attach, a focus) has
|
|
9
|
-
// nothing to answer, so it quietly resumes the old transcript and injects no
|
|
10
|
-
// kickoff. The rule is a property of the node, evaluated once inside
|
|
11
|
-
// reviveNode, so every doorway gets it from one strict-resume veto. Its other
|
|
12
|
-
// exception is a park with nothing on disk to stand on (reachable only on the
|
|
13
|
-
// degraded path, which writes no summary): resuming an old transcript beats
|
|
14
|
-
// waking amnesiac.
|
|
3
|
+
// Re-entering a parked node resumes its main transcript: the isolated parking
|
|
4
|
+
// turn never wrote to it. Wake-worthy mail remains durable for the inbox watcher
|
|
5
|
+
// to deliver after boot; it does not create a fresh-cycle kickoff. An input-less
|
|
6
|
+
// revival likewise resumes quietly. The rule is a property of the node,
|
|
7
|
+
// evaluated once inside reviveNode, so every doorway gets it uniformly.
|
|
15
8
|
import { after, afterEach, before, beforeEach, test } from 'node:test';
|
|
16
9
|
import assert from 'node:assert/strict';
|
|
17
10
|
import { mkdirSync, mkdtempSync, rmSync, writeFileSync } from 'node:fs';
|
|
@@ -103,23 +96,23 @@ after(() => {
|
|
|
103
96
|
else
|
|
104
97
|
process.env['CRTR_HOME'] = priorHome;
|
|
105
98
|
});
|
|
106
|
-
test('a parked node with mail waiting
|
|
99
|
+
test('a parked node with mail waiting resumes its main transcript and clears the marker', async () => {
|
|
107
100
|
parkResident('parked', { roadmap: '# Roadmap\n\n- one open item\n' });
|
|
108
101
|
deliverMail('parked');
|
|
109
102
|
const result = await reviveNode('parked', { resume: true, capacity: 'freeze' });
|
|
110
|
-
assert.equal(result.resumed,
|
|
111
|
-
assert.deepEqual(placements(), [{ nodeId: 'parked', resuming:
|
|
112
|
-
assert.equal(kickedOff(launches[0]),
|
|
103
|
+
assert.equal(result.resumed, true, 'the isolated parking turn never concluded the main transcript');
|
|
104
|
+
assert.deepEqual(placements(), [{ nodeId: 'parked', resuming: true }]);
|
|
105
|
+
assert.equal(kickedOff(launches[0]), false, 'the inbox watcher delivers the mail after the resumed boot');
|
|
113
106
|
const reopened = getNode('parked');
|
|
114
107
|
assert.equal(reopened.status, 'active');
|
|
115
108
|
assert.equal(reopened.intent ?? null, null, 'the park marker clears on revival');
|
|
116
|
-
assert.equal(reopened.cycle_pending,
|
|
109
|
+
assert.equal(reopened.cycle_pending, false, 'a strict resume does not open a new cycle');
|
|
117
110
|
});
|
|
118
|
-
test('a parked node grounded only by a goal still
|
|
111
|
+
test('a parked node grounded only by a goal still resumes when mail rides the revive', async () => {
|
|
119
112
|
parkResident('goal-only', { goal: 'Watch the deploy lane.' });
|
|
120
113
|
deliverMail('goal-only');
|
|
121
|
-
assert.equal((await reviveNode('goal-only', { resume: true, capacity: 'freeze' })).resumed,
|
|
122
|
-
assert.equal(kickedOff(launches[0]),
|
|
114
|
+
assert.equal((await reviveNode('goal-only', { resume: true, capacity: 'freeze' })).resumed, true);
|
|
115
|
+
assert.equal(kickedOff(launches[0]), false);
|
|
123
116
|
});
|
|
124
117
|
test('an input-less revival of a parked node quietly resumes and injects no kickoff', async () => {
|
|
125
118
|
parkResident('quiet', { roadmap: '# Roadmap\n\n- one open item\n' });
|
|
@@ -394,7 +394,11 @@ test('reload_auth restores a newly authenticated personal route before the next
|
|
|
394
394
|
const unauthenticatedProvider = join(h.home, 'fake-pi.unauthenticated-provider');
|
|
395
395
|
mkdirSync(userConfigDir, { recursive: true });
|
|
396
396
|
writeFileSync(userConfigPath, JSON.stringify({
|
|
397
|
-
|
|
397
|
+
// A fixture-private kind, never `general`: the child launches with the
|
|
398
|
+
// package root as cwd, so an ambient project-scope config above the repo
|
|
399
|
+
// (`~/.crouter/config.json`) overrides this user-scope `general` and the
|
|
400
|
+
// child boots on that machine's default strength instead of medium.
|
|
401
|
+
kinds: { 'test/logical-anthropic': { whenToUse: 'Logical Anthropic routing test.', model: 'anthropic/medium' } },
|
|
398
402
|
modelRoutes: {
|
|
399
403
|
personal: { family: 'anthropic', credentialSource: 'user', providerId: personal.provider, models: { ultra: personalSpec, strong: personalSpec, medium: personalSpec, light: personalSpec } },
|
|
400
404
|
funded: { family: 'anthropic', credentialSource: 'northlight', providerId: funded.provider, models: { ultra: `${funded.provider}/${funded.id}:medium`, strong: `${funded.provider}/${funded.id}:medium`, medium: `${funded.provider}/${funded.id}:medium`, light: `${funded.provider}/${funded.id}:medium` } },
|
|
@@ -404,7 +408,7 @@ test('reload_auth restores a newly authenticated personal route before the next
|
|
|
404
408
|
writeFileSync(unauthenticatedProvider, personal.provider);
|
|
405
409
|
try {
|
|
406
410
|
const parent = h.spawnRoot('auth route reconciliation parent');
|
|
407
|
-
const child = await h.spawnHeadlessChild(parent, 'auth route reconciliation child');
|
|
411
|
+
const child = await h.spawnHeadlessChild(parent, 'auth route reconciliation child', { kind: 'test/logical-anthropic' });
|
|
408
412
|
assert.deepEqual(model(child), funded, 'the logical request starts on the funded route while personal auth is absent');
|
|
409
413
|
const controller = await kit.connectRaw(child, { read: true });
|
|
410
414
|
controller.send({ type: 'hello', role: 'controller', client_id: 'auth-route-reconciliation' });
|
|
@@ -175,7 +175,7 @@ test('an unattended resident with no live obligation completes on the daemon clo
|
|
|
175
175
|
await reviveNode(nodeId, { resume: true, capacity: 'freeze' });
|
|
176
176
|
const boot = await h.awaitBoot(nodeId);
|
|
177
177
|
assert.equal(h.fleet.has(nodeId), true, 'the revived broker is fleet-owned');
|
|
178
|
-
const seenAt =
|
|
178
|
+
const seenAt = Date.now();
|
|
179
179
|
await h.tick(seenAt);
|
|
180
180
|
await h.tick(seenAt + 1499);
|
|
181
181
|
assert.equal(h.fleet.has(nodeId), true, 'the daemon keeps the broker through the full grace');
|
|
@@ -228,7 +228,7 @@ test('a non-attending observer does not prevent unattended resident parking', {
|
|
|
228
228
|
await attach.attach(nodeId, 'observer', 'system-observer-test', { attends: false });
|
|
229
229
|
assert.equal(JSON.parse(readFileSync(join(h.home, 'nodes', nodeId, 'job', 'attach.json'), 'utf8')).viewers, 0, 'a non-attending hello is not persisted as human attendance');
|
|
230
230
|
try {
|
|
231
|
-
const seenAt =
|
|
231
|
+
const seenAt = Date.now();
|
|
232
232
|
await h.tick(seenAt);
|
|
233
233
|
await h.tick(seenAt + 1500);
|
|
234
234
|
await h.tick(seenAt + 1500 + 1000);
|
|
@@ -383,7 +383,7 @@ test('a resident managed child parks without finalizing while a terminal child i
|
|
|
383
383
|
};
|
|
384
384
|
const residentPid = await boot(resident);
|
|
385
385
|
const terminalPid = await boot(terminal);
|
|
386
|
-
const seenAt =
|
|
386
|
+
const seenAt = Date.now();
|
|
387
387
|
await h.tick(seenAt);
|
|
388
388
|
// Clock expiry arms the resident's pending park and requests its summary
|
|
389
389
|
// turn; the terminal child is cancelled outright by the same pass.
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
export {};
|