@north-light/crouter 0.3.262 → 0.3.269
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 +5 -1
- package/dist/api/client.js +6 -0
- package/dist/api/dto/canvas.d.ts +53 -2
- package/dist/api/dto/health.d.ts +2 -0
- package/dist/api/dto/nodes.d.ts +6 -2
- package/dist/api/routes.d.ts +1 -0
- package/dist/api/routes.js +1 -0
- package/dist/builtin-memory/05-kinds/advisor/advice-contract.md +2 -2
- package/dist/builtin-memory/internal/memory-loading.md +3 -3
- package/dist/builtin-pi-packages/pi-crtr-extensions/extensions/__tests__/integration/provider-rotation.test.ts +50 -0
- package/dist/builtin-pi-packages/pi-crtr-extensions/extensions/memory-slash-commands.ts +46 -6
- package/dist/builtin-pi-packages/pi-crtr-extensions/extensions/provider-rotation.js +10 -5
- package/dist/builtin-pi-packages/pi-crtr-extensions/extensions/provider-rotation.ts +12 -5
- package/dist/clients/attach/input/controller.d.ts +4 -0
- package/dist/clients/attach/input/controller.js +8 -0
- package/dist/clients/attach/session/bindings.d.ts +3 -0
- package/dist/clients/attach/session/bindings.js +7 -0
- package/dist/clients/attach/session/keys.d.ts +2 -0
- package/dist/clients/attach/session/keys.js +3 -3
- package/dist/clients/attach/session/whip-achievements.d.ts +4 -0
- package/dist/clients/attach/session/whip-achievements.js +145 -0
- package/dist/clients/attach/slash/dispatch.js +5 -3
- package/dist/clients/attach/viewer.js +668 -667
- package/dist/commands/canvas-history/grep.js +3 -22
- package/dist/commands/canvas-history/read.js +5 -3
- package/dist/commands/canvas-history/search.js +3 -22
- package/dist/commands/canvas-history/shared.d.ts +8 -0
- package/dist/commands/canvas-history/shared.js +64 -1
- package/dist/commands/canvas-history/stats.d.ts +1 -0
- package/dist/commands/canvas-history/stats.js +68 -0
- package/dist/commands/canvas-history.js +4 -3
- package/dist/commands/memory/read.js +15 -38
- package/dist/commands/memory/shared.d.ts +1 -1
- package/dist/commands/memory/shared.js +1 -1
- package/dist/commands/node/create.js +2 -2
- package/dist/commands/sys/config.js +19 -2
- package/dist/commands/sys/context/admin/docs-panel.d.ts +3 -3
- package/dist/commands/sys/context/admin/model.d.ts +7 -2
- package/dist/commands/sys/context/admin/model.js +7 -1
- package/dist/commands/sys/context/admin/read-view.d.ts +5 -5
- package/dist/commands/sys/context/admin/read-view.js +9 -4
- package/dist/commands/sys/context/resolve.d.ts +32 -3
- package/dist/commands/sys/context/resolve.js +74 -25
- package/dist/commands/sys/panels/models-panel.d.ts +28 -5
- package/dist/commands/sys/panels/models-panel.js +560 -75
- package/dist/core/__tests__/integration/broker-sdk-wiring.test.js +1 -1
- package/dist/core/__tests__/integration/revive.test.js +4 -2
- package/dist/core/__tests__/model-pin-durability.test.js +25 -1
- package/dist/core/__tests__/model-routes-config.test.d.ts +1 -0
- package/dist/core/__tests__/model-routes-config.test.js +142 -0
- package/dist/core/__tests__/on-read-nested-store.test.js +8 -0
- package/dist/core/__tests__/profile-project-memory-delivery.test.js +264 -1
- package/dist/core/__tests__/relaunch-root.test.js +17 -0
- package/dist/core/__tests__/seam/broker-provider-retry.test.js +21 -6
- package/dist/core/__tests__/seam/memory-slash-node-relative-inventory.test.js +19 -2
- package/dist/core/broker-client/__tests__/transport-relay.test.js +3 -1
- package/dist/core/broker-client/transport-relay.js +5 -1
- package/dist/core/canvas/__tests__/history-transcript.test.d.ts +1 -0
- package/dist/core/canvas/__tests__/history-transcript.test.js +102 -0
- package/dist/core/canvas/browse/app.d.ts +1 -1
- package/dist/core/canvas/browse/app.js +13 -10
- package/dist/core/canvas/browse/model.js +16 -1
- package/dist/core/canvas/browse/render.d.ts +23 -3
- package/dist/core/canvas/browse/render.js +429 -64
- package/dist/core/canvas/canvas.js +12 -8
- package/dist/core/canvas/extensions.d.ts +1 -1
- package/dist/core/canvas/extensions.js +7 -1
- package/dist/core/canvas/history.d.ts +36 -1
- package/dist/core/canvas/history.js +328 -7
- package/dist/core/canvas/labels.d.ts +2 -2
- package/dist/core/canvas/migrations.js +32 -0
- package/dist/core/canvas/node-recap.d.ts +23 -0
- package/dist/core/canvas/node-recap.js +67 -0
- package/dist/core/canvas/render-source.d.ts +34 -2
- package/dist/core/canvas/render-source.js +83 -24
- package/dist/core/canvas/types.d.ts +9 -0
- package/dist/core/config.d.ts +19 -2
- package/dist/core/config.js +174 -51
- package/dist/core/keybindings/catalog.d.ts +2 -2
- package/dist/core/keybindings/catalog.js +11 -8
- package/dist/core/model-routes.js +11 -4
- package/dist/core/runtime/broker/auth-reload.js +4 -1
- package/dist/core/runtime/broker/fault-retry.js +7 -2
- package/dist/core/runtime/broker/frame-client.js +4 -0
- package/dist/core/runtime/broker/frame-dispatch.js +7 -0
- package/dist/core/runtime/broker/inbox.d.ts +12 -2
- package/dist/core/runtime/broker/inbox.js +68 -38
- package/dist/core/runtime/broker-protocol.d.ts +6 -0
- package/dist/core/runtime/broker.js +14 -0
- package/dist/core/runtime/canvas-extensions.d.ts +1 -0
- package/dist/core/runtime/canvas-extensions.js +2 -0
- package/dist/core/runtime/interactive-deliver.d.ts +8 -2
- package/dist/core/runtime/interactive-deliver.js +12 -3
- package/dist/core/runtime/launch-target.js +1 -1
- package/dist/core/runtime/launch.d.ts +12 -6
- package/dist/core/runtime/launch.js +19 -17
- package/dist/core/runtime/model-swap.d.ts +5 -4
- package/dist/core/runtime/model-swap.js +7 -5
- package/dist/core/runtime/promote.js +2 -1
- package/dist/core/runtime/reset.js +4 -2
- package/dist/core/runtime/revive.js +22 -0
- package/dist/core/runtime/spawn.d.ts +3 -3
- package/dist/core/runtime/spawn.js +20 -3
- package/dist/core/runtime/stamp/channel.d.ts +18 -0
- package/dist/core/runtime/stamp/channel.js +74 -0
- package/dist/core/runtime/stamp/protocol.d.ts +86 -0
- package/dist/core/runtime/stamp/protocol.js +113 -0
- package/dist/core/runtime/tmux-driver.d.ts +11 -1
- package/dist/core/runtime/tmux-driver.js +27 -11
- package/dist/core/self-update.js +2 -5
- package/dist/core/substrate/listings.d.ts +1 -1
- package/dist/core/substrate/listings.js +6 -2
- package/dist/core/substrate/on-read.d.ts +61 -5
- package/dist/core/substrate/on-read.js +129 -22
- package/dist/core/substrate/render.d.ts +3 -2
- package/dist/core/substrate/render.js +133 -38
- package/dist/core/tui/draw.d.ts +1 -0
- package/dist/core/tui/draw.js +5 -1
- package/dist/core/tui/markdown.d.ts +17 -0
- package/dist/core/tui/markdown.js +407 -0
- package/dist/core/user-settings.d.ts +4 -0
- package/dist/core/user-settings.js +1 -0
- package/dist/daemon/api/__tests__/node-model-validation.test.js +21 -0
- package/dist/daemon/api/__tests__/seam/api-server.test.js +34 -0
- package/dist/daemon/api/bridge.d.ts +3 -2
- package/dist/daemon/api/bridge.js +34 -9
- package/dist/daemon/api/handlers/canvas.js +80 -18
- package/dist/daemon/api/handlers/health.js +17 -12
- package/dist/daemon/api/handlers/messages.js +12 -1
- package/dist/daemon/api/handlers/nodes.js +10 -3
- package/dist/daemon/api/map.js +2 -0
- package/dist/daemon/messaging/node-message.js +15 -1
- package/dist/daemon/reconcilers/broker-supervision.js +26 -9
- package/dist/daemon/reconcilers/live-obligation.d.ts +8 -1
- package/dist/daemon/reconcilers/live-obligation.js +17 -0
- package/dist/pi-extensions/__tests__/pre-command-gate.test.js +2 -2
- package/dist/pi-extensions/canvas-inbox-watcher.js +27 -2
- package/dist/pi-extensions/canvas-recap.js +4 -0
- package/dist/pi-extensions/canvas-stamp.d.ts +14 -0
- package/dist/pi-extensions/canvas-stamp.js +77 -0
- package/dist/pi-extensions/canvas-stophook.js +3 -0
- package/dist/shared/__tests__/env-boundary.test.js +4 -2
- package/dist/shared/crtr-version.d.ts +2 -0
- package/dist/shared/crtr-version.js +18 -0
- package/dist/shared/env.d.ts +10 -0
- package/dist/shared/env.js +19 -4
- package/dist/types.d.ts +14 -2
- package/dist/types.js +4 -0
- package/package.json +1 -1
- package/runtime.lock.json +2 -2
|
@@ -212,7 +212,7 @@ test('C3f — a logical request can reach a non-managed raw-registry fallback wh
|
|
|
212
212
|
family: 'custom',
|
|
213
213
|
credentialSource: 'user',
|
|
214
214
|
providerId: 'c3prov',
|
|
215
|
-
models: { strong: 'c3prov/c3model' },
|
|
215
|
+
models: { ultra: 'c3prov/c3model', strong: 'c3prov/c3model', medium: 'c3prov/c3model', light: 'c3prov/c3model' },
|
|
216
216
|
},
|
|
217
217
|
},
|
|
218
218
|
})}\n`);
|
|
@@ -180,7 +180,8 @@ test('a resumed revive replays the persisted LaunchSpec (model, tools, extension
|
|
|
180
180
|
pi_session_id: 'uuid-9',
|
|
181
181
|
pi_session_file: sessionFile,
|
|
182
182
|
launch: {
|
|
183
|
-
model: 'anthropic/
|
|
183
|
+
model: 'anthropic/claude-opus-5:high',
|
|
184
|
+
modelExact: true,
|
|
184
185
|
tools: ['bash', 'read'],
|
|
185
186
|
extensions: ['/ext/a.js', '/ext/b.js'],
|
|
186
187
|
systemPrompt: 'You are a developer orchestrator.',
|
|
@@ -196,7 +197,8 @@ test('a resumed revive replays the persisted LaunchSpec (model, tools, extension
|
|
|
196
197
|
assert.equal(argv[ea - 1], '-e');
|
|
197
198
|
// Model + tools + system prompt + resume target.
|
|
198
199
|
const mi = argv.indexOf('--model');
|
|
199
|
-
assert.equal(argv[mi + 1], 'anthropic/
|
|
200
|
+
assert.equal(argv[mi + 1], 'anthropic/claude-opus-5:high');
|
|
201
|
+
assert.ok(argv.includes('--crtr-model-exact'), 'revive preserves the exact-model contract');
|
|
200
202
|
const ti = argv.indexOf('--tools');
|
|
201
203
|
assert.equal(argv[ti + 1], 'bash,read');
|
|
202
204
|
assert.ok(argv.includes('--append-system-prompt'), 'system prompt replayed');
|
|
@@ -20,7 +20,7 @@ import { join } from 'node:path';
|
|
|
20
20
|
import { buildLaunchSpec, isProviderPinnedModelToken, normalizeModel } from '../runtime/launch.js';
|
|
21
21
|
import { defaultModelLaddersConfig } from '../../types.js';
|
|
22
22
|
import { persistDormantModel } from '../runtime/model-swap.js';
|
|
23
|
-
import { promote } from '../runtime/promote.js';
|
|
23
|
+
import { promote, reshapeNode } from '../runtime/promote.js';
|
|
24
24
|
import { createNode, getNode } from '../canvas/canvas.js';
|
|
25
25
|
import { closeDb } from '../canvas/db.js';
|
|
26
26
|
const ladders = defaultModelLaddersConfig();
|
|
@@ -86,6 +86,15 @@ test('only concrete provider/id tokens pin; logical family/tier refs remain pref
|
|
|
86
86
|
assert.equal(isProviderPinnedModelToken('ultra'), false, 'a bare tier has no `/` and stays unpinned');
|
|
87
87
|
assert.equal(isProviderPinnedModelToken('opus'), false, 'a bare family alias has no `/` and stays unpinned');
|
|
88
88
|
});
|
|
89
|
+
test('a new --model override resolves once and freezes portable and concrete requests', () => {
|
|
90
|
+
for (const requested of ['anthropic/opus', 'strong', 'anthropic/claude-fable-5:high']) {
|
|
91
|
+
const launch = buildSpec({ model: requested, modelOverride: true });
|
|
92
|
+
assert.equal(launch.model, normalizeModel(requested, ladders), `${requested} resolves through the configured ladder`);
|
|
93
|
+
assert.equal(launch.modelExact, true, `${requested} is exact after resolution`);
|
|
94
|
+
assert.equal(launch.modelIntent, undefined, `${requested} has no portable route intent`);
|
|
95
|
+
assert.equal(launch.env.CRTR_MODEL_INTENT, undefined, `${requested} cannot re-enable fallback on revive`);
|
|
96
|
+
}
|
|
97
|
+
});
|
|
89
98
|
// Direction 1: an EXPLICIT concrete pin must SURVIVE a config/lifecycle rebuild
|
|
90
99
|
// even when the carried override has portablized back to a bare tier.
|
|
91
100
|
test('an explicit concrete pin survives a config/lifecycle rebuild whose portablized override reads as a bare tier', () => {
|
|
@@ -103,6 +112,20 @@ test('an explicit concrete pin survives a config/lifecycle rebuild whose portabl
|
|
|
103
112
|
// Direction 2: a deliberately TIER-run (unpinned) node must NOT be wrongly
|
|
104
113
|
// re-pinned when a subsequent rebuild/persist normalizes its override to the
|
|
105
114
|
// concrete cell it happens to share bytes with.
|
|
115
|
+
test('reshape resolves and persists a new --model override as the exact revive recipe', () => {
|
|
116
|
+
createNode(node('override-node', {
|
|
117
|
+
model_override: 'strong',
|
|
118
|
+
launch: { extensions: [], env: { CRTR_MODEL_INTENT: 'anthropic/strong' }, model: normalizeModel('strong', ladders), modelIntent: { family: 'anthropic', strength: 'strong' } },
|
|
119
|
+
}));
|
|
120
|
+
reshapeNode('override-node', { model: 'anthropic/opus' });
|
|
121
|
+
const after = getNode('override-node');
|
|
122
|
+
const expected = normalizeModel('anthropic/opus', ladders);
|
|
123
|
+
assert.equal(after?.model_override, expected, 'the raw alias never becomes the durable override');
|
|
124
|
+
assert.equal(after?.launch?.model, expected, 'revive replays the resolved provider/model');
|
|
125
|
+
assert.equal(after?.launch?.modelExact, true, 'retries and revives reject substitutes');
|
|
126
|
+
assert.equal(after?.launch?.modelIntent, undefined);
|
|
127
|
+
assert.equal(after?.launch?.env.CRTR_MODEL_INTENT, undefined);
|
|
128
|
+
});
|
|
106
129
|
test('promotion preserves the exact-model contract required to revive a private human-work fork', () => {
|
|
107
130
|
const exactModel = 'openai-codex/gpt-5.6-sol:max';
|
|
108
131
|
createNode(node('exact-node', {
|
|
@@ -134,6 +157,7 @@ test('a deliberately tier-run node is not wrongly re-pinned when a rebuild or pe
|
|
|
134
157
|
const after = getNode('tier-node');
|
|
135
158
|
assert.equal(after?.launch?.model, ULTRA_CELL, 'the concrete spec is persisted as the launch model');
|
|
136
159
|
assert.equal(after?.model_override, ULTRA_CELL, 'the concrete spec is persisted as the override');
|
|
160
|
+
assert.equal(after?.launch?.modelExact, true, 'an explicit model change remains exact on revive');
|
|
137
161
|
assert.equal(after?.launch?.modelIntent, undefined, 'an explicit concrete choice clears the prior logical route intent');
|
|
138
162
|
assert.equal(after?.launch?.env.CRTR_MODEL_INTENT, undefined, 'the revived broker does not inherit the prior logical route intent');
|
|
139
163
|
assert.equal(after?.launch?.modelProviderPinned, false, 'an explicit bare-tier decision clears a prior pin — the raw-token decision wins over the concrete spec shape');
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
export {};
|
|
@@ -0,0 +1,142 @@
|
|
|
1
|
+
import { after, afterEach, before, beforeEach, test } from 'node:test';
|
|
2
|
+
import assert from 'node:assert/strict';
|
|
3
|
+
import { mkdtempSync, mkdirSync, readFileSync, rmSync, writeFileSync } from 'node:fs';
|
|
4
|
+
import { tmpdir } from 'node:os';
|
|
5
|
+
import { join } from 'node:path';
|
|
6
|
+
import { persistModelRoute, readConfig, readMergedLaunchConfig } from '../config.js';
|
|
7
|
+
import { expandModelCandidates } from '../model-routes.js';
|
|
8
|
+
import { resetScopeCache } from '../scope.js';
|
|
9
|
+
let home;
|
|
10
|
+
let work;
|
|
11
|
+
let previousHome;
|
|
12
|
+
function route(providerId = 'provider-x', family = 'family-x', credentialSource = 'source-x') {
|
|
13
|
+
return {
|
|
14
|
+
family,
|
|
15
|
+
credentialSource,
|
|
16
|
+
providerId,
|
|
17
|
+
models: {
|
|
18
|
+
ultra: `${providerId}/ultra`,
|
|
19
|
+
strong: `${providerId}/strong`,
|
|
20
|
+
medium: `${providerId}/medium`,
|
|
21
|
+
light: `${providerId}/light`,
|
|
22
|
+
},
|
|
23
|
+
};
|
|
24
|
+
}
|
|
25
|
+
function writeConfig(root, config) {
|
|
26
|
+
mkdirSync(root, { recursive: true });
|
|
27
|
+
writeFileSync(join(root, 'config.json'), JSON.stringify(config));
|
|
28
|
+
}
|
|
29
|
+
before(() => {
|
|
30
|
+
previousHome = process.env.HOME;
|
|
31
|
+
});
|
|
32
|
+
beforeEach(() => {
|
|
33
|
+
home = mkdtempSync(join(tmpdir(), 'crtr-model-routes-home-'));
|
|
34
|
+
work = mkdtempSync(join(tmpdir(), 'crtr-model-routes-work-'));
|
|
35
|
+
process.env.HOME = home;
|
|
36
|
+
resetScopeCache();
|
|
37
|
+
});
|
|
38
|
+
afterEach(() => {
|
|
39
|
+
resetScopeCache();
|
|
40
|
+
rmSync(home, { recursive: true, force: true });
|
|
41
|
+
rmSync(work, { recursive: true, force: true });
|
|
42
|
+
});
|
|
43
|
+
after(() => {
|
|
44
|
+
if (previousHome === undefined)
|
|
45
|
+
delete process.env.HOME;
|
|
46
|
+
else
|
|
47
|
+
process.env.HOME = previousHome;
|
|
48
|
+
resetScopeCache();
|
|
49
|
+
});
|
|
50
|
+
test('complete routes preserve arbitrary names and credential-source ordering', () => {
|
|
51
|
+
writeConfig(join(home, '.crouter'), {
|
|
52
|
+
modelRoutes: {
|
|
53
|
+
personal: route('shared-provider', 'family/one', 'personal source'),
|
|
54
|
+
funded: route('shared-provider', 'family/one', 'funded source'),
|
|
55
|
+
},
|
|
56
|
+
modelRouting: {
|
|
57
|
+
credentialSourceOrder: ['funded source', 'personal source'],
|
|
58
|
+
familyOrder: ['family/one'],
|
|
59
|
+
strengthFallback: ['strong'],
|
|
60
|
+
},
|
|
61
|
+
});
|
|
62
|
+
const config = readMergedLaunchConfig(work, null);
|
|
63
|
+
assert.deepEqual(config.modelRoutes?.personal?.models, route('shared-provider', 'family/one', 'personal source').models);
|
|
64
|
+
assert.deepEqual(expandModelCandidates({ kind: 'logical', family: 'family/one', strength: 'strong', spec: 'family/one/strong' }, work, null).map((candidate) => candidate.routeId), ['funded', 'personal'], 'same provider/model refs remain distinct candidates when routes differ');
|
|
65
|
+
});
|
|
66
|
+
test('a sparse upper-scope route patch overlays a complete lower route', () => {
|
|
67
|
+
writeConfig(join(home, '.crouter'), { modelRoutes: { shared: route() } });
|
|
68
|
+
writeConfig(join(work, '.crouter'), { modelRoutes: { shared: { models: { medium: 'provider-x/project-medium' } } } });
|
|
69
|
+
const config = readMergedLaunchConfig(work, null);
|
|
70
|
+
assert.equal(config.modelRoutes?.shared?.models.medium, 'provider-x/project-medium');
|
|
71
|
+
assert.equal(config.modelRoutes?.shared?.models.ultra, 'provider-x/ultra');
|
|
72
|
+
});
|
|
73
|
+
test('legacy sparse routes remain readable with no invented cells', () => {
|
|
74
|
+
writeConfig(join(home, '.crouter'), {
|
|
75
|
+
modelRoutes: {
|
|
76
|
+
legacy: { family: 'legacy', credentialSource: 'old', providerId: 'legacy-provider', models: { medium: 'legacy-provider/medium' } },
|
|
77
|
+
},
|
|
78
|
+
});
|
|
79
|
+
const config = readMergedLaunchConfig(work, null);
|
|
80
|
+
assert.equal(config.modelRoutes, undefined);
|
|
81
|
+
assert.deepEqual(config.legacyModelRoutes?.legacy?.models, { medium: 'legacy-provider/medium' });
|
|
82
|
+
assert.deepEqual(readConfig('user').legacyModelRoutes?.legacy?.models, { medium: 'legacy-provider/medium' });
|
|
83
|
+
assert.deepEqual(expandModelCandidates({ kind: 'logical', family: 'legacy', strength: 'medium', spec: 'legacy/medium' }, work, null).map((candidate) => candidate.modelId), ['medium']);
|
|
84
|
+
});
|
|
85
|
+
test('route records retain __proto__ ids when read and persisted', () => {
|
|
86
|
+
const root = join(home, '.crouter');
|
|
87
|
+
writeConfig(root, JSON.parse(JSON.stringify({ modelRoutes: { ['__proto__']: route() } })));
|
|
88
|
+
assert.deepEqual(readMergedLaunchConfig(work, null).modelRoutes?.['__proto__'], route());
|
|
89
|
+
persistModelRoute(root, '__proto__', route('persisted-provider'));
|
|
90
|
+
const raw = JSON.parse(readFileSync(join(root, 'config.json'), 'utf8'));
|
|
91
|
+
assert.deepEqual(raw.modelRoutes['__proto__'], route('persisted-provider'));
|
|
92
|
+
});
|
|
93
|
+
test('route candidate identity distinguishes delimiter-containing route and provider ids', () => {
|
|
94
|
+
writeConfig(join(home, '.crouter'), {
|
|
95
|
+
modelRoutes: {
|
|
96
|
+
a: route('b:c', 'family', 'first'),
|
|
97
|
+
'a:b': route('c', 'family', 'second'),
|
|
98
|
+
},
|
|
99
|
+
modelRouting: {
|
|
100
|
+
credentialSourceOrder: ['first', 'second'],
|
|
101
|
+
familyOrder: ['family'],
|
|
102
|
+
strengthFallback: ['strong'],
|
|
103
|
+
},
|
|
104
|
+
});
|
|
105
|
+
assert.deepEqual(expandModelCandidates({ kind: 'logical', family: 'family', strength: 'strong', spec: 'family/strong' }, work, null).map((candidate) => candidate.routeId), ['a', 'a:b']);
|
|
106
|
+
});
|
|
107
|
+
test('route writes reject missing cells and provider-prefix mismatches with actionable diagnostics', () => {
|
|
108
|
+
const root = join(home, '.crouter');
|
|
109
|
+
const incomplete = { ...route(), models: { strong: 'provider-x/strong' } };
|
|
110
|
+
assert.throws(() => persistModelRoute(root, 'needs-all-rungs', incomplete), /model route "needs-all-rungs" is incomplete; missing strengths: ultra, medium, light/);
|
|
111
|
+
assert.throws(() => persistModelRoute(root, 'wrong-provider', { ...route(), models: { ...route().models, light: 'other/light' } }), /model route "wrong-provider" models.light provider prefix must equal providerId/);
|
|
112
|
+
});
|
|
113
|
+
test('invalid route entries do not block valid routes, and no valid routes falls back to local ladders', () => {
|
|
114
|
+
writeConfig(join(home, '.crouter'), {
|
|
115
|
+
modelRoutes: {
|
|
116
|
+
valid: route('valid-provider', 'valid family', 'valid source'),
|
|
117
|
+
invalid: { family: 'bad', credentialSource: 'bad', providerId: 'one', models: { ultra: 'two/u', strong: 'two/s', medium: 'two/m', light: 'two/l' } },
|
|
118
|
+
},
|
|
119
|
+
});
|
|
120
|
+
assert.deepEqual(Object.keys(readMergedLaunchConfig(work, null).modelRoutes ?? {}), ['valid']);
|
|
121
|
+
writeConfig(join(home, '.crouter'), {
|
|
122
|
+
modelRoutes: {
|
|
123
|
+
invalid: { family: 'bad', credentialSource: 'bad', providerId: 'one', models: { ultra: 'two/u', strong: 'two/s', medium: 'two/m', light: 'two/l' } },
|
|
124
|
+
},
|
|
125
|
+
});
|
|
126
|
+
assert.equal(readMergedLaunchConfig(work, null).modelRoutes, undefined);
|
|
127
|
+
assert.equal(expandModelCandidates({ kind: 'logical', family: 'anthropic', strength: 'strong', spec: 'anthropic/strong' }, work, null)[0]?.routeId, 'anthropic');
|
|
128
|
+
});
|
|
129
|
+
test('persistModelRoute touches only its raw route and deletes by restoring inheritance', () => {
|
|
130
|
+
const root = join(home, '.crouter');
|
|
131
|
+
const other = route('other-provider');
|
|
132
|
+
writeConfig(root, { modelRoutes: { other }, max_panes_per_window: 7 });
|
|
133
|
+
persistModelRoute(root, 'edited', route('edited-provider'));
|
|
134
|
+
let raw = JSON.parse(readFileSync(join(root, 'config.json'), 'utf8'));
|
|
135
|
+
assert.deepEqual(raw.modelRoutes.other, other);
|
|
136
|
+
assert.deepEqual(raw.modelRoutes.edited, route('edited-provider'));
|
|
137
|
+
assert.equal(raw.modelLadders, undefined);
|
|
138
|
+
assert.equal(raw.max_panes_per_window, 7);
|
|
139
|
+
persistModelRoute(root, 'edited', undefined);
|
|
140
|
+
raw = JSON.parse(readFileSync(join(root, 'config.json'), 'utf8'));
|
|
141
|
+
assert.deepEqual(raw.modelRoutes, { other });
|
|
142
|
+
});
|
|
@@ -69,6 +69,11 @@ function makeWorkTree() {
|
|
|
69
69
|
' - {on: workspace-open, at: content}\n' +
|
|
70
70
|
' - {on: read, match: "./**", at: content}\n---\n' +
|
|
71
71
|
`${NESTED_BODY}\n`);
|
|
72
|
+
// A bootless neighbour in the same discovered store: it wins no delivery of
|
|
73
|
+
// its own, so only a listing can ever disclose it.
|
|
74
|
+
writeFileSync(join(nestedStore, 'constraint.md'), '---\nkind: knowledge\n' +
|
|
75
|
+
'when-and-why-to-read: When changing pkg internals, this knowledge should be read because it names the constraint.\n---\n' +
|
|
76
|
+
'NESTED NEIGHBOUR BODY\n');
|
|
72
77
|
return work;
|
|
73
78
|
}
|
|
74
79
|
test('a nested store front door surfaces on a read beneath its owning dir, not outside it', () => {
|
|
@@ -79,6 +84,9 @@ test('a nested store front door surfaces on a read beneath its owning dir, not o
|
|
|
79
84
|
writeFileSync(inside, 'export const x = 1;\n');
|
|
80
85
|
const rendered = renderOnReadDocs(node, inside, freshTranscriptTarget());
|
|
81
86
|
assert.ok(rendered.includes(NESTED_BODY), `read beneath pkg surfaces the nested front door; got: ${rendered}`);
|
|
87
|
+
// A content delivery is a read: the discovered store's own listing comes
|
|
88
|
+
// with it, answered from the corpus the read walk discovered.
|
|
89
|
+
assert.match(rendered, /\[\[work\/pkg\/constraint\]\]: When changing pkg internals/, `the delivered front door discloses its directory's bootless member; got: ${rendered}`);
|
|
82
90
|
const outside = join(work, 'docs', 'notes.ts');
|
|
83
91
|
mkdirSync(join(work, 'docs'), { recursive: true });
|
|
84
92
|
writeFileSync(outside, 'export const y = 2;\n');
|
|
@@ -25,11 +25,18 @@ import { spawnNode } from '../runtime/nodes.js';
|
|
|
25
25
|
import { memoryDir } from '../runtime/memory.js';
|
|
26
26
|
import { clearSessionCache } from '../substrate/session-cache.js';
|
|
27
27
|
import { renderKnowledgeBlock } from '../substrate/render-node.js';
|
|
28
|
+
import { renderKnowledgeForSubject, renderPreferencesForSubject } from '../substrate/render.js';
|
|
29
|
+
import { assembleNodeSubject } from '../substrate/subject.js';
|
|
28
30
|
import { renderWorkspaceOpenDocs } from '../substrate/on-read-node.js';
|
|
31
|
+
import { renderWorkspaceOpenDocsForSubject } from '../substrate/on-read.js';
|
|
29
32
|
import { documentExposedAtOrAbove, emptyContextExposureState, exposedAtOrAbove, exposureTarget, loadContextExposureState, saveContextExposureState, } from '../substrate/injected-store.js';
|
|
30
33
|
import { readLeaf } from '../../commands/memory/read.js';
|
|
31
34
|
import { listLeaf } from '../../commands/memory/list.js';
|
|
32
35
|
import { findLeaf } from '../../commands/memory/find.js';
|
|
36
|
+
import { assembleEventAccounting, docRow, effectivePlan, } from '../../commands/sys/context/resolve.js';
|
|
37
|
+
import { planDelivery } from '../substrate/plan.js';
|
|
38
|
+
import { PlanSet } from '../../commands/sys/context/admin/model.js';
|
|
39
|
+
import { buildRows } from '../../commands/sys/context/admin/docs-panel.js';
|
|
33
40
|
function freshTranscriptTarget() {
|
|
34
41
|
return exposureTarget(emptyContextExposureState(), 'transcript');
|
|
35
42
|
}
|
|
@@ -48,6 +55,14 @@ const B = {
|
|
|
48
55
|
dupFar: 'BODY-T9-DUP-FROM-FAR',
|
|
49
56
|
kindSplit: 'BODY-T9-KIND-SPLIT',
|
|
50
57
|
documentGateWins: 'BODY-T9-DOCUMENT-GATE-WINS',
|
|
58
|
+
codeCraftIndex: 'BODY-T9-CODE-CRAFT-INDEX',
|
|
59
|
+
companionSource: 'BODY-T9-COMPANION-SOURCE',
|
|
60
|
+
companionA: 'BODY-T9-COMPANION-A',
|
|
61
|
+
companionB: 'BODY-T9-COMPANION-B',
|
|
62
|
+
prefRouter: 'BODY-T9-PREF-ROUTER',
|
|
63
|
+
crossPartition: 'BODY-T9-CROSS-PARTITION',
|
|
64
|
+
outsideGuide: 'BODY-T9-OUTSIDE-GUIDE',
|
|
65
|
+
pnameNeighbour: 'BODY-T9-NEIGHBOUR-IN-PNAMEDIR',
|
|
51
66
|
};
|
|
52
67
|
let home;
|
|
53
68
|
let canvasHome;
|
|
@@ -58,6 +73,7 @@ let prevProfile;
|
|
|
58
73
|
let prevNode;
|
|
59
74
|
let prevCwd;
|
|
60
75
|
let cwdProject;
|
|
76
|
+
let outsideProject;
|
|
61
77
|
let ppreviewDocPath;
|
|
62
78
|
/** A doc delivered automatically at the top rung by both automatic channels —
|
|
63
79
|
* every cap observed below is the relationship's, never the author's. */
|
|
@@ -157,6 +173,60 @@ before(() => {
|
|
|
157
173
|
B.kindSplit,
|
|
158
174
|
'',
|
|
159
175
|
].join('\n'));
|
|
176
|
+
writeDoc(user, 'dev/code-craft/INDEX', [
|
|
177
|
+
'---',
|
|
178
|
+
'kind: knowledge',
|
|
179
|
+
'when-and-why-to-read: "When planning code work, this knowledge should be read because it routes the applicable craft guidance"',
|
|
180
|
+
'surfaces:',
|
|
181
|
+
' - {on: boot, gate: {kind: {in: [design, spec, plan, developer]}}, at: content}',
|
|
182
|
+
'---',
|
|
183
|
+
'',
|
|
184
|
+
B.codeCraftIndex,
|
|
185
|
+
'',
|
|
186
|
+
].join('\n'));
|
|
187
|
+
for (let i = 1; i <= 10; i++) {
|
|
188
|
+
writeDoc(user, `dev/code-craft/member-${i}`, bootlessDoc(`When code-craft member ${i} applies`, `BODY-T9-CODE-CRAFT-MEMBER-${i}`));
|
|
189
|
+
}
|
|
190
|
+
// The ANCESTOR half of a content delivery's listings: a bootless sibling of
|
|
191
|
+
// the `dev/code-craft` directory, disclosed because `dev/` is the enclosing
|
|
192
|
+
// directory of the doc whose body delivered.
|
|
193
|
+
writeDoc(user, 'dev/sibling', bootlessDoc('When the dev sibling applies', 'BODY-T9-DEV-SIBLING'));
|
|
194
|
+
writeDoc(user, 't9-companion-source', [
|
|
195
|
+
'---',
|
|
196
|
+
'kind: knowledge',
|
|
197
|
+
'when-and-why-to-read: "When companion delivery starts, this knowledge should be read because it proves content reads cascade"',
|
|
198
|
+
'surfaces:',
|
|
199
|
+
' - {on: boot, at: content}',
|
|
200
|
+
'---',
|
|
201
|
+
'',
|
|
202
|
+
B.companionSource,
|
|
203
|
+
'',
|
|
204
|
+
].join('\n'));
|
|
205
|
+
// A companion that FRONTS a directory: its content delivery discloses its own
|
|
206
|
+
// members exactly as the source doc's did, one level deeper in the fixpoint.
|
|
207
|
+
writeDoc(user, 't9-companion-a/INDEX', [
|
|
208
|
+
'---',
|
|
209
|
+
'kind: knowledge',
|
|
210
|
+
'when-and-why-to-read: "When the source is read, this knowledge should be read because it proves the first companion route"',
|
|
211
|
+
'surfaces:',
|
|
212
|
+
' - {on: memory-read, match: t9-companion-source, at: content}',
|
|
213
|
+
'---',
|
|
214
|
+
'',
|
|
215
|
+
B.companionA,
|
|
216
|
+
'',
|
|
217
|
+
].join('\n'));
|
|
218
|
+
writeDoc(user, 't9-companion-a/member-1', bootlessDoc('When the companion member applies', 'BODY-T9-COMPANION-A-MEMBER-1'));
|
|
219
|
+
writeDoc(user, 't9-companion-b', [
|
|
220
|
+
'---',
|
|
221
|
+
'kind: knowledge',
|
|
222
|
+
'when-and-why-to-read: "When the first companion is read, this knowledge should be read because it proves the route reaches a fixpoint"',
|
|
223
|
+
'surfaces:',
|
|
224
|
+
' - {on: memory-read, match: t9-companion-a, at: content}',
|
|
225
|
+
'---',
|
|
226
|
+
'',
|
|
227
|
+
B.companionB,
|
|
228
|
+
'',
|
|
229
|
+
].join('\n'));
|
|
160
230
|
writeDoc(user, 't9-document-gate-wins', [
|
|
161
231
|
'---',
|
|
162
232
|
'kind: knowledge',
|
|
@@ -169,6 +239,78 @@ before(() => {
|
|
|
169
239
|
B.documentGateWins,
|
|
170
240
|
'',
|
|
171
241
|
].join('\n'));
|
|
242
|
+
// Cross-partition single emission: a preference body routes a KNOWLEDGE doc
|
|
243
|
+
// that carries boot content of its own. Both partitions render against one
|
|
244
|
+
// exposure state, so that body must be delivered exactly once.
|
|
245
|
+
writeDoc(user, 't9-pref-router', [
|
|
246
|
+
'---',
|
|
247
|
+
'kind: preference',
|
|
248
|
+
'when-and-why-to-read: "When the preference partition renders, this preference should be read because it routes a knowledge companion"',
|
|
249
|
+
'surfaces:',
|
|
250
|
+
' - {on: boot, at: content}',
|
|
251
|
+
'---',
|
|
252
|
+
'',
|
|
253
|
+
B.prefRouter,
|
|
254
|
+
'',
|
|
255
|
+
].join('\n'));
|
|
256
|
+
writeDoc(user, 't9-cross-partition', [
|
|
257
|
+
'---',
|
|
258
|
+
'kind: knowledge',
|
|
259
|
+
'when-and-why-to-read: "When the preference routes here, this knowledge should be read because it proves one body crosses both partitions once"',
|
|
260
|
+
'surfaces:',
|
|
261
|
+
' - {on: boot, at: content}',
|
|
262
|
+
' - {on: memory-read, match: t9-pref-router, at: content}',
|
|
263
|
+
'---',
|
|
264
|
+
'',
|
|
265
|
+
B.crossPartition,
|
|
266
|
+
'',
|
|
267
|
+
].join('\n'));
|
|
268
|
+
// Cap safety: a name-tree directory merges across stores, so this user doc
|
|
269
|
+
// sits in the SAME canonical directory as the name-capped project's docs. Its
|
|
270
|
+
// content delivery discloses that directory — and must not raise a capped
|
|
271
|
+
// neighbour above the bare name its relationship allows.
|
|
272
|
+
writeDoc(user, 'pname/user-guide', [
|
|
273
|
+
'---',
|
|
274
|
+
'kind: knowledge',
|
|
275
|
+
'when-and-why-to-read: "When the merged directory renders, this knowledge should be read because it discloses that directory"',
|
|
276
|
+
'surfaces:',
|
|
277
|
+
' - {on: boot, at: content}',
|
|
278
|
+
'---',
|
|
279
|
+
'',
|
|
280
|
+
B.pnameNeighbour,
|
|
281
|
+
'',
|
|
282
|
+
].join('\n'));
|
|
283
|
+
// A project OUTSIDE the profile manifest: its store mounts only for a target
|
|
284
|
+
// whose cwd sits inside it, so a workspace-open render of it proves the
|
|
285
|
+
// listings answer from the opened workspace rather than the ambient one.
|
|
286
|
+
const poutside = store('poutside', 'poutside');
|
|
287
|
+
outsideProject = join(home, 'poutside');
|
|
288
|
+
writeDoc(poutside, 't9-ws-guide/INDEX', [
|
|
289
|
+
'---',
|
|
290
|
+
'kind: knowledge',
|
|
291
|
+
'when-and-why-to-read: "When that workspace opens, this knowledge should be read because it fronts its guide directory"',
|
|
292
|
+
'surfaces:',
|
|
293
|
+
' - {on: workspace-open, at: content}',
|
|
294
|
+
'---',
|
|
295
|
+
'',
|
|
296
|
+
B.outsideGuide,
|
|
297
|
+
'',
|
|
298
|
+
].join('\n'));
|
|
299
|
+
writeDoc(poutside, 't9-ws-guide/member-1', bootlessDoc('When the outside guide member applies', 'BODY-T9-OUTSIDE-MEMBER-1'));
|
|
300
|
+
// Workspace-open plans project stores only, but a content delivery's
|
|
301
|
+
// read-equivalent attachments answer from the whole opened target.
|
|
302
|
+
writeDoc(user, 'poutside/listing-member', bootlessDoc('When the workspace listing member applies', 'BODY-T9-WORKSPACE-LISTING-MEMBER'));
|
|
303
|
+
writeDoc(user, 't9-workspace-companion', [
|
|
304
|
+
'---',
|
|
305
|
+
'kind: knowledge',
|
|
306
|
+
'when-and-why-to-read: "When the workspace guide is read, this companion proves full-view accounting"',
|
|
307
|
+
'surfaces:',
|
|
308
|
+
' - {on: memory-read, match: poutside/t9-ws-guide, at: content}',
|
|
309
|
+
'---',
|
|
310
|
+
'',
|
|
311
|
+
'BODY-T9-WORKSPACE-COMPANION',
|
|
312
|
+
'',
|
|
313
|
+
].join('\n'));
|
|
172
314
|
// Hidden-count pair: a top-level dir always renders, so a dir holding only a
|
|
173
315
|
// bootless doc shows as `<dir>/` + `[+1 more]` — unless its project is `none`.
|
|
174
316
|
writeDoc(pnone, join('t9-none-hidden', 'probe'), bootlessDoc('When counted', 'HIDDEN-NONE'));
|
|
@@ -219,7 +361,10 @@ test('boot delivery caps every profile project at its relationship, including th
|
|
|
219
361
|
// does, and the relationship still decides the ceiling.
|
|
220
362
|
assert.ok(!boot.includes(B.ppreview), `the cwd project is capped at preview — its body must not render; got:\n${boot}`);
|
|
221
363
|
assert.match(boot, /t9-ppreview-doc {2}# read when: When the cwd project delivers\./, `the cwd project's doc discloses at preview; got:\n${boot}`);
|
|
222
|
-
// `name` discloses the bare identifier and nothing else
|
|
364
|
+
// `name` discloses the bare identifier and nothing else — including when a
|
|
365
|
+
// content delivery in the same merged directory (the user-store neighbour)
|
|
366
|
+
// discloses that directory's listing.
|
|
367
|
+
assert.ok(boot.includes(B.pnameNeighbour), `the merged directory's uncapped document delivers; got:\n${boot}`);
|
|
223
368
|
assert.ok(!boot.includes(B.pname), 'a name-capped project must not render a body');
|
|
224
369
|
assert.match(boot, /[├└]─ t9-pname-doc$/m, `the name-capped doc discloses as a bare name; got:\n${boot}`);
|
|
225
370
|
assert.ok(!boot.includes('When the name project delivers'), 'a name-capped doc discloses no preview line');
|
|
@@ -242,6 +387,88 @@ test('surface-entry gates select the rung by node kind while the document gate r
|
|
|
242
387
|
const developer = renderKnowledgeBlock(newNode('developer'), freshTranscriptTarget());
|
|
243
388
|
assert.ok(!developer.includes('t9-document-gate-wins'), `a failing document gate excludes every entry; got:\n${developer}`);
|
|
244
389
|
assert.ok(!developer.includes(B.documentGateWins), 'a failing document gate excludes its content');
|
|
390
|
+
assert.ok(developer.includes(B.codeCraftIndex), 'the directory document delivers its boot content');
|
|
391
|
+
for (let i = 1; i <= 10; i++) {
|
|
392
|
+
assert.match(developer, new RegExp(`member-${i} {2}# read when: When code-craft member ${i} applies`), `content delivery exposes code-craft member ${i} at preview; got:\n${developer}`);
|
|
393
|
+
}
|
|
394
|
+
// The ancestor listing, not just the doc's own directory.
|
|
395
|
+
assert.match(developer, /sibling {2}# read when: When the dev sibling applies\./, `content delivery exposes the enclosing directory's members too; got:\n${developer}`);
|
|
396
|
+
});
|
|
397
|
+
test('context docs accounting reports content attachment raises from the renderer', () => {
|
|
398
|
+
const node = newNode('developer');
|
|
399
|
+
const subject = assembleNodeSubject(node);
|
|
400
|
+
assert.ok(subject !== null, 'the fixture node assembles a subject');
|
|
401
|
+
const target = { cwd: cwdProject, profileId, nodeId: node };
|
|
402
|
+
const direct = planDelivery(subject, target, 'boot');
|
|
403
|
+
const rendered = assembleEventAccounting({ subject, target, nodeMissing: false }, 'boot', {});
|
|
404
|
+
const rows = effectivePlan(direct, rendered.accounting.deliveries).docs.map((record) => docRow(record, true, subject));
|
|
405
|
+
for (let i = 1; i <= 10; i++) {
|
|
406
|
+
const row = rows.find((candidate) => candidate.name === `dev/code-craft/member-${i}`);
|
|
407
|
+
assert.equal(row?.final_rung, 'preview', `member ${i} is raised to the listing preview rung`);
|
|
408
|
+
assert.equal(row?.delivery_cause, 'directory listing attached to content delivery');
|
|
409
|
+
}
|
|
410
|
+
const companion = rows.find((row) => row.name === 't9-companion-a');
|
|
411
|
+
assert.equal(companion?.final_rung, 'content', 'a memory-read companion reaches its rendered rung');
|
|
412
|
+
assert.equal(companion?.delivery_cause, 'memory-read companion of content delivery');
|
|
413
|
+
const plans = new PlanSet({
|
|
414
|
+
kind: 'developer',
|
|
415
|
+
mode: 'base',
|
|
416
|
+
lifecycle: 'terminal',
|
|
417
|
+
hasManager: true,
|
|
418
|
+
profileId,
|
|
419
|
+
cwd: cwdProject,
|
|
420
|
+
nodeId: node,
|
|
421
|
+
depth: 0,
|
|
422
|
+
event: 'boot',
|
|
423
|
+
file: '',
|
|
424
|
+
docName: '',
|
|
425
|
+
command: '',
|
|
426
|
+
});
|
|
427
|
+
plans.plan('boot');
|
|
428
|
+
const viewerMember = buildRows(plans, 'boot').find((row) => row.name === 'dev/code-craft/member-1');
|
|
429
|
+
const viewerRecord = viewerMember?.records.get('boot');
|
|
430
|
+
assert.equal(viewerRecord?.finalRung, 'preview', 'the admin row reads the renderer-raised rung');
|
|
431
|
+
assert.equal(viewerRecord?.deliveryCause, 'directory listing attached to content delivery');
|
|
432
|
+
plans.dispose();
|
|
433
|
+
});
|
|
434
|
+
test('context docs accounting adds full-view workspace content attachments', () => {
|
|
435
|
+
const node = newNode();
|
|
436
|
+
const subject = assembleNodeSubject(node);
|
|
437
|
+
assert.ok(subject !== null, 'the fixture node assembles a subject');
|
|
438
|
+
const target = { cwd: outsideProject, profileId, nodeId: node };
|
|
439
|
+
const direct = planDelivery(subject, target, 'workspace-open');
|
|
440
|
+
const rendered = assembleEventAccounting({ subject, target, nodeMissing: false }, 'workspace-open', {});
|
|
441
|
+
const rows = effectivePlan(direct, rendered.accounting.deliveries).docs.map((record) => docRow(record, true, subject));
|
|
442
|
+
const listing = rows.find((row) => row.name === 'poutside/listing-member');
|
|
443
|
+
assert.equal(listing?.final_rung, 'preview');
|
|
444
|
+
assert.equal(listing?.delivery_cause, 'directory listing attached to content delivery');
|
|
445
|
+
const companion = rows.find((row) => row.name === 't9-workspace-companion');
|
|
446
|
+
assert.equal(companion?.final_rung, 'content');
|
|
447
|
+
assert.equal(companion?.delivery_cause, 'memory-read companion of content delivery');
|
|
448
|
+
});
|
|
449
|
+
test('boot content triggers memory-read companions to a deduplicated fixpoint', () => {
|
|
450
|
+
const boot = renderKnowledgeBlock(newNode(), freshTranscriptTarget());
|
|
451
|
+
for (const body of [B.companionSource, B.companionA, B.companionB]) {
|
|
452
|
+
assert.equal(boot.split(body).length - 1, 1, `content companion ${body} delivers once; got:\n${boot}`);
|
|
453
|
+
}
|
|
454
|
+
// A companion is read-equivalent in its own right: the directory it fronts
|
|
455
|
+
// discloses its members exactly as the doc that routed to it did.
|
|
456
|
+
assert.match(boot, /member-1 {2}# read when: When the companion member applies\./, `a content companion exposes its own directory's members; got:\n${boot}`);
|
|
457
|
+
});
|
|
458
|
+
test('one body delivered across both boot partitions renders exactly once', () => {
|
|
459
|
+
const node = newNode();
|
|
460
|
+
const subject = assembleNodeSubject(node);
|
|
461
|
+
assert.ok(subject !== null, 'the fixture node assembles a subject');
|
|
462
|
+
const state = emptyContextExposureState();
|
|
463
|
+
// The live boot order: the knowledge block and the preference splice share
|
|
464
|
+
// ONE exposure state, so a knowledge body already delivered as a preference's
|
|
465
|
+
// companion must not render again as knowledge prose.
|
|
466
|
+
const preferences = renderPreferencesForSubject(subject, exposureTarget(state, 'system')).block;
|
|
467
|
+
const knowledge = renderKnowledgeForSubject(subject, node, exposureTarget(state, 'transcript'));
|
|
468
|
+
assert.ok(preferences.includes(B.prefRouter), `the routing preference delivers its body; got:\n${preferences}`);
|
|
469
|
+
assert.ok(preferences.includes(B.crossPartition), `its companion delivers with it; got:\n${preferences}`);
|
|
470
|
+
const occurrences = (preferences.split(B.crossPartition).length - 1) + (knowledge.split(B.crossPartition).length - 1);
|
|
471
|
+
assert.equal(occurrences, 1, `the companion body renders once across both partitions; got:\n${preferences}\n---\n${knowledge}`);
|
|
245
472
|
});
|
|
246
473
|
test('a node-local doc floors to name only without boot entries', () => {
|
|
247
474
|
const node = newNode('explore');
|
|
@@ -312,6 +539,17 @@ test('workspace-open clamps each project store the same way boot does', () => {
|
|
|
312
539
|
assert.ok(!opened.includes('t9-pnone-doc'), 'the none-capped project delivers nothing at workspace-open');
|
|
313
540
|
assert.ok(!opened.includes(B.sharedFromPnone), 'nor its side of a name collision');
|
|
314
541
|
});
|
|
542
|
+
test('workspace-open answers its listings from the workspace it opened, not the ambient one', () => {
|
|
543
|
+
const node = newNode();
|
|
544
|
+
const subject = assembleNodeSubject(node);
|
|
545
|
+
assert.ok(subject !== null, 'the fixture node assembles a subject');
|
|
546
|
+
// The process cwd is `ppreview`; this workspace is outside the profile
|
|
547
|
+
// manifest, so ONLY the opened target's corpus knows its store at all.
|
|
548
|
+
const opened = renderWorkspaceOpenDocsForSubject(subject, join(home, 'poutside'), profileId, freshTranscriptTarget());
|
|
549
|
+
assert.ok(opened.includes(B.outsideGuide), `the opened workspace's guide delivers its body; got:\n${opened}`);
|
|
550
|
+
assert.match(opened, /<memory-listing dir="poutside\/t9-ws-guide">/, `its directory listing renders from that workspace's corpus; got:\n${opened}`);
|
|
551
|
+
assert.match(opened, /\[\[poutside\/t9-ws-guide\/member-1\]\]: When the outside guide member applies\./, `the listing carries that directory's own members; got:\n${opened}`);
|
|
552
|
+
});
|
|
315
553
|
test('memory list and find record synthetic pages without crediting matched documents', async () => {
|
|
316
554
|
const node = newNode();
|
|
317
555
|
process.env['CRTR_NODE_ID'] = node;
|
|
@@ -350,6 +588,31 @@ test('memory list and find record synthetic pages without crediting matched docu
|
|
|
350
588
|
delete process.env['CRTR_NODE_ID'];
|
|
351
589
|
}
|
|
352
590
|
});
|
|
591
|
+
test('an explicit read after boot content adds no duplicate directory attachments', async () => {
|
|
592
|
+
const node = newNode('developer');
|
|
593
|
+
process.env['CRTR_NODE_ID'] = node;
|
|
594
|
+
try {
|
|
595
|
+
const state = loadContextExposureState(node);
|
|
596
|
+
renderKnowledgeBlock(node, exposureTarget(state, 'transcript'));
|
|
597
|
+
saveContextExposureState(node, state);
|
|
598
|
+
const result = (await readLeaf.run({
|
|
599
|
+
name: 'dev/code-craft',
|
|
600
|
+
kind: undefined,
|
|
601
|
+
frontmatter: false,
|
|
602
|
+
scope: undefined,
|
|
603
|
+
dir: undefined,
|
|
604
|
+
}));
|
|
605
|
+
assert.equal(result.content.trim(), B.codeCraftIndex, 'the repeated read returns its body without a new attachment block');
|
|
606
|
+
assert.equal(result.listing.length, 10, 'the deliberate directory read still returns its immediate members');
|
|
607
|
+
const afterRead = loadContextExposureState(node);
|
|
608
|
+
for (let i = 1; i <= 10; i++) {
|
|
609
|
+
assert.ok(documentExposedAtOrAbove(afterRead, join(home, '.crouter', 'memory', 'dev', 'code-craft', `member-${i}.md`), `BODY-T9-CODE-CRAFT-MEMBER-${i}`, 'preview'), `boot content registered member ${i} at preview before the explicit read`);
|
|
610
|
+
}
|
|
611
|
+
}
|
|
612
|
+
finally {
|
|
613
|
+
delete process.env['CRTR_NODE_ID'];
|
|
614
|
+
}
|
|
615
|
+
});
|
|
353
616
|
test('an explicit `crtr memory read` returns and records the full body a cap withheld', async () => {
|
|
354
617
|
const node = newNode();
|
|
355
618
|
process.env['CRTR_NODE_ID'] = node;
|
|
@@ -18,6 +18,7 @@ import { createNode, getNode, subscribe } from '../canvas/canvas.js';
|
|
|
18
18
|
import { closeDb } from '../canvas/db.js';
|
|
19
19
|
import { apiSocketPath } from '../canvas/paths.js';
|
|
20
20
|
import { brokerCapReached, resetFleetForTests } from '../runtime/fleet.js';
|
|
21
|
+
import { buildPiArgv } from '../runtime/launch.js';
|
|
21
22
|
import { relaunchRoot } from '../runtime/reset.js';
|
|
22
23
|
import { bindTestFleet } from './helpers/fleet.js';
|
|
23
24
|
let home;
|
|
@@ -106,6 +107,22 @@ test('relaunchRoot parks the old root done and mints a fresh active root, reapin
|
|
|
106
107
|
assert.equal(getNode('child')?.status, 'canceled', 'descendant child canceled');
|
|
107
108
|
assert.equal(getNode('grand')?.status, 'canceled', 'descendant grandchild canceled');
|
|
108
109
|
});
|
|
110
|
+
test('relaunchRoot preserves an explicit model override as an exact replay recipe', async () => {
|
|
111
|
+
const exactModel = 'anthropic/claude-opus-5:high';
|
|
112
|
+
createNode(node('root', {
|
|
113
|
+
parent: null,
|
|
114
|
+
lifecycle: 'resident',
|
|
115
|
+
model_override: exactModel,
|
|
116
|
+
launch: { extensions: [], env: {}, model: exactModel, modelExact: true },
|
|
117
|
+
}));
|
|
118
|
+
const { deps } = makeDeps(4242);
|
|
119
|
+
const result = await relaunchRoot('root', deps);
|
|
120
|
+
assert.ok(result);
|
|
121
|
+
const replacement = getNode(result.newNodeId);
|
|
122
|
+
assert.equal(replacement?.launch?.model, exactModel);
|
|
123
|
+
assert.equal(replacement?.launch?.modelExact, true, 'the replacement cannot route away from the override');
|
|
124
|
+
assert.ok(buildPiArgv(replacement, {}).argv.includes('--crtr-model-exact'), 'broker replay preserves the exact-model contract');
|
|
125
|
+
});
|
|
109
126
|
test('relaunchRoot on a boot failure leaves the old root + descendants fully intact', async () => {
|
|
110
127
|
// The mint-and-boot-FIRST ordering: a launch failure (pid=null) must occur
|
|
111
128
|
// BEFORE the old root is parked or its descendants reaped.
|