gentle-pi 4.2.0 → 4.4.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +340 -138
- package/assets/agents/gentle-ai-explore.md +1 -0
- package/assets/agents/gentle-ai-verify.md +6 -4
- package/assets/agents/gentle-ai-worker.md +7 -1
- package/assets/design-canvas/canvas.css +648 -0
- package/assets/design-canvas/canvas.js +1497 -0
- package/assets/design-canvas/index.html +88 -0
- package/assets/design-canvas/inspector.js +290 -0
- package/assets/orchestrator-delegation-reference.md +163 -0
- package/assets/orchestrator-delegation.md +25 -176
- package/assets/orchestrator-tracking.md +1 -1
- package/assets/orchestrator-verification.md +14 -8
- package/assets/orchestrator-writer.md +5 -1
- package/assets/orchestrator.md +11 -11
- package/bin/engram-guard +1 -1
- package/bin/gentle-shell.mjs +237 -139
- package/contracts/review-provider-contract-mirror/provider-contract.lock.json +8 -7
- package/contracts/review-provider-contract-mirror/{v1.3.0 → v1.4.0}/bundle/README.md +7 -0
- package/contracts/review-provider-contract-mirror/{v1.3.0 → v1.4.0}/bundle/manifest.json +11 -2
- package/contracts/review-provider-contract-mirror/v1.4.0/bundle/routing/pi.md +92 -0
- package/contracts/review-provider-contract-mirror/{v1.3.0 → v1.4.0}/generated/provider-capabilities.baseline.json +8 -1
- package/contracts/review-provider-contract-mirror/{v1.3.0 → v1.4.0}/generated/provider-roles.baseline.json +1 -1
- package/docs/assets/diagrams/agent-orchestration.svg +54 -15
- package/docs/assets/diagrams/gentleman-workflow.svg +69 -11
- package/docs/assets/diagrams/measured-routing.svg +49 -0
- package/docs/assets/diagrams/native-review.svg +52 -12
- package/docs/assets/diagrams/odd-workflow.svg +128 -66
- package/docs/assets/diagrams/permission-modes.svg +51 -0
- package/docs/assets/features/audio-notifications.png +0 -0
- package/docs/assets/features/command-palette.png +0 -0
- package/docs/bundled-install.md +65 -30
- package/docs/delegated-verification.md +16 -8
- package/docs/gentle-agents-activity.md +16 -6
- package/docs/gentle-music.md +61 -0
- package/docs/gentle-shell-uninstall.md +8 -1
- package/docs/gentle-shell.md +80 -9
- package/docs/install-wizard.md +37 -6
- package/docs/permission-modes.md +8 -3
- package/docs/readme-reference.md +120 -28
- package/docs/session-profile-format.md +26 -0
- package/docs/voice.md +259 -0
- package/docs/yolo-mode.md +38 -10
- package/extensions/ask-user-choice.ts +4 -1
- package/extensions/codegraph-tools.ts +68 -6
- package/extensions/gentle-agents.ts +270 -48
- package/extensions/gentle-ai.ts +490 -113
- package/extensions/gentle-design.ts +158 -0
- package/extensions/gentle-herdr-activity.ts +2 -3
- package/extensions/gentle-music.ts +289 -0
- package/extensions/gentle-shell.ts +359 -85
- package/extensions/gentle-todo.ts +43 -9
- package/extensions/gentle-voice.ts +11 -0
- package/extensions/quiet-tools.ts +1 -1
- package/extensions/session-scratch.ts +384 -0
- package/extensions/skill-registry.ts +63 -8
- package/extensions/startup-banner.ts +2 -3
- package/lib/agent-home.ts +8 -3
- package/lib/agent-profiles.ts +26 -0
- package/lib/agents-config.ts +35 -0
- package/lib/agents-protocol.ts +60 -1
- package/lib/agents-runner.ts +107 -9
- package/lib/agents-session-transport.ts +101 -6
- package/lib/agents-view.ts +3 -1
- package/lib/agents-widget.ts +14 -1
- package/lib/auto-permission-memory.ts +48 -18
- package/lib/auto-permission-runtime.ts +21 -6
- package/lib/bounded-writer-admission.ts +51 -0
- package/lib/clipboard-image.ts +147 -0
- package/lib/command-palette-catalog.ts +2 -4
- package/lib/commit-slice-signal.ts +100 -0
- package/lib/context-backstop.ts +120 -0
- package/lib/delivered-test-lint.ts +80 -0
- package/lib/design-canvas-server.ts +947 -0
- package/lib/design-comments.ts +437 -0
- package/lib/design-handoffs.ts +313 -0
- package/lib/design-open-browser.ts +58 -0
- package/lib/design-sources.ts +120 -0
- package/lib/destructive-command-guard.ts +120 -9
- package/lib/external-editor.ts +31 -0
- package/lib/gauge-theme.ts +146 -0
- package/lib/gentle-ai-binary.ts +28 -7
- package/lib/gentle-design.ts +513 -0
- package/lib/gentle-shell-bundled.ts +146 -36
- package/lib/gentle-shell-import-pi.ts +233 -0
- package/lib/gentle-shell-launcher.ts +154 -15
- package/lib/gentle-shell-startup-update.ts +61 -9
- package/lib/gentle-shell-uninstall.ts +51 -2
- package/lib/herdr-activity.ts +24 -0
- package/lib/image-attachments.ts +150 -0
- package/lib/inprocess-reviewer.ts +33 -2
- package/lib/nan-provider.ts +3 -0
- package/lib/native-review-cli.ts +20 -5
- package/lib/notification-audio-native.ts +149 -13
- package/lib/notification-audio.ts +4 -0
- package/lib/notification-pulse-worker.ts +82 -18
- package/lib/notification-service.ts +5 -1
- package/lib/orchestrator-discovery.ts +5 -1
- package/lib/orchestrator-presence.ts +87 -8
- package/lib/orchestrator-scope.ts +31 -3
- package/lib/orchestrator-state.ts +11 -3
- package/lib/permission-mode-policy.ts +2 -2
- package/lib/provider-contract-bundle.ts +99 -53
- package/lib/review-candidate-view-owner.ts +29 -19
- package/lib/review-candidate-view.ts +36 -2
- package/lib/review-host-relay.ts +11 -2
- package/lib/review-reminder-receipt.ts +24 -1
- package/lib/review-risk-assessment.ts +43 -38
- package/lib/review-session-standing-permission.ts +3 -1
- package/lib/session-profile-append-controller.ts +713 -0
- package/lib/session-profile-disk-reader.ts +182 -0
- package/lib/session-profile-freeze.ts +30 -1
- package/lib/session-profile-persistence.ts +15 -7
- package/lib/session-scratch.ts +523 -0
- package/lib/session-worktree-registry.ts +7 -0
- package/lib/shell-bar.ts +29 -28
- package/lib/shell-changes-view.ts +80 -13
- package/lib/shell-changes.ts +81 -5
- package/lib/shell-gauge.ts +49 -6
- package/lib/shell-prompt.ts +4 -4
- package/lib/shell-sidebar-layout.ts +26 -6
- package/lib/shell-todo.ts +24 -5
- package/lib/shell-usage-view.ts +5 -1
- package/lib/shell-usage.ts +7 -7
- package/lib/verifier-admission.ts +30 -0
- package/lib/vim-editor-adapter.ts +11 -8
- package/lib/voice-artifacts.ts +188 -0
- package/lib/voice-command.ts +576 -0
- package/lib/voice-install.ts +455 -0
- package/lib/voice-platform.ts +99 -0
- package/lib/voice-push-to-talk.ts +262 -0
- package/lib/voice-session.ts +460 -0
- package/lib/voice-settings.ts +50 -0
- package/lib/voice-ui.ts +158 -0
- package/lib/yolo-session-policy.ts +58 -22
- package/lib/yolo-temp-scratch.ts +69 -0
- package/package.json +1 -2
- package/runtime/gentle-ai-binary.mjs +28 -7
- package/runtime/gentle-shell-bundled.mjs +145 -35
- package/runtime/gentle-shell-import-pi.mjs +234 -0
- package/runtime/gentle-shell-launcher.mjs +152 -13
- package/runtime/gentle-shell-startup-update.mjs +61 -9
- package/runtime/gentle-shell-uninstall.mjs +51 -2
- package/runtime/native-review-cli.mjs +19 -4
- package/runtime/review-risk-assessment.mjs +43 -38
- package/scripts/build-runtime-modules.mjs +1 -0
- package/scripts/bundled-engram-migration.mjs +119 -0
- package/scripts/gentle-ai-installer.mjs +10 -10
- package/scripts/installer-downloads.mjs +7 -7
- package/scripts/installer-preflight.mjs +5 -3
- package/scripts/installer-server.mjs +14 -4
- package/scripts/installer-windows.mjs +37 -7
- package/scripts/main-channel.mjs +94 -19
- package/scripts/pi-update.mjs +34 -6
- package/scripts/run-test-suite.mjs +5 -0
- package/scripts/verify-package-files.mjs +43 -13
- package/scripts/voice-worker.cjs +476 -0
- package/skills/design/SKILL.md +110 -0
- package/skills/gentle-ai/SKILL.md +3 -3
- package/skills/work-unit-commits/SKILL.md +5 -3
- package/tests/agent-profiles.test.ts +24 -0
- package/tests/agent-risk-escalation.test.ts +37 -10
- package/tests/agents-config.test.ts +61 -0
- package/tests/agents-fake-child.ts +8 -1
- package/tests/agents-protocol.test.ts +32 -0
- package/tests/agents-runner.test.ts +485 -7
- package/tests/agents-session-transport.test.ts +53 -3
- package/tests/ask-user-choice.test.ts +30 -0
- package/tests/auto-mode.test.ts +17 -0
- package/tests/auto-permission-memory-diagnostics.test.ts +56 -0
- package/tests/auto-permission-runtime.test.ts +85 -1
- package/tests/autonomous-guard.test.ts +142 -0
- package/tests/bounded-writer-admission.test.ts +11 -3
- package/tests/bundled-install.test.ts +3 -3
- package/tests/card-content-policy.test.ts +19 -9
- package/tests/child-safety.test.ts +39 -0
- package/tests/clipboard-image.test.ts +114 -0
- package/tests/codegraph-tools.test.ts +123 -2
- package/tests/command-palette.test.ts +15 -5
- package/tests/commit-slice-signal.test.ts +162 -0
- package/tests/context-backstop.test.ts +225 -0
- package/tests/delegate-for-reason-contract.test.ts +92 -24
- package/tests/delivered-test-lint.test.ts +105 -0
- package/tests/design-canvas-assets.test.ts +710 -0
- package/tests/design-canvas-server.test.ts +1096 -0
- package/tests/design-comments.test.ts +201 -0
- package/tests/design-handoffs.test.ts +282 -0
- package/tests/design-open-browser.test.ts +70 -0
- package/tests/design-sources.test.ts +132 -0
- package/tests/destructive-command-guard.test.ts +85 -1
- package/tests/engram-safety.test.ts +132 -57
- package/tests/external-editor.test.ts +84 -0
- package/tests/fixtures/primary-session-prompt/gentleman.golden.md +143 -0
- package/tests/fixtures/provider-contract-bundle/v1.4.0/README.md +29 -0
- package/tests/fixtures/provider-contract-bundle/v1.4.0/manifest.json +83 -0
- package/tests/fixtures/provider-contract-bundle/v1.4.0/orchestration/pi.md +70 -0
- package/tests/fixtures/provider-contract-bundle/v1.4.0/routing/pi.md +92 -0
- package/tests/fixtures/provider-contract-bundle/v1.4.0/schemas/lens.schema.json +16 -0
- package/tests/fixtures/provider-contract-bundle/v1.4.0/schemas/refuter.schema.json +1 -0
- package/tests/fixtures/provider-contract-bundle/v1.4.0/schemas/targeted-validator.schema.json +1 -0
- package/tests/fixtures/provider-contract-bundle/v1.4.0/vectors/lens.json +1 -0
- package/tests/fixtures/provider-contract-bundle/v1.4.0/vectors/refuter.json +1 -0
- package/tests/fixtures/provider-contract-bundle/v1.4.0/vectors/targeted-validator.json +1 -0
- package/tests/gauge-theme.test.ts +120 -0
- package/tests/gentle-agents.test.ts +645 -21
- package/tests/gentle-ai-binary.test.ts +1 -1
- package/tests/gentle-ai-child-guards.test.ts +61 -0
- package/tests/gentle-ai-doctor-herdr.test.ts +94 -0
- package/tests/gentle-ai-installer.test.ts +51 -51
- package/tests/gentle-ai.test.ts +292 -13
- package/tests/gentle-design.test.ts +1313 -0
- package/tests/gentle-music.test.ts +335 -0
- package/tests/gentle-shell-bin.test.ts +163 -0
- package/tests/gentle-shell-bundled.test.ts +392 -176
- package/tests/gentle-shell-import-pi.test.ts +262 -0
- package/tests/gentle-shell-launcher.test.ts +107 -2
- package/tests/gentle-shell-startup-update.test.ts +77 -1
- package/tests/gentle-shell-uninstall.test.ts +89 -0
- package/tests/gentle-shell.test.ts +225 -17
- package/tests/gentle-todo.test.ts +190 -0
- package/tests/headless-delivery-question.test.ts +58 -0
- package/tests/herdr-activity.test.ts +34 -1
- package/tests/image-attachments.test.ts +119 -0
- package/tests/image-paste-chips.test.ts +236 -0
- package/tests/inprocess-reviewer.test.ts +55 -2
- package/tests/installer-bundles.test.ts +12 -6
- package/tests/installer-posix-bootstrap.test.ts +8 -8
- package/tests/installer-preflight.test.ts +16 -0
- package/tests/installer-server.test.ts +10 -2
- package/tests/installer-windows-bootstrap.test.ts +94 -7
- package/tests/installer-windows-diagnostics.test.ts +111 -0
- package/tests/main-channel-upgrade.test.ts +232 -2
- package/tests/main-channel.test.ts +140 -21
- package/tests/nan-provider.test.ts +36 -0
- package/tests/native-review-capability-contract.test.ts +17 -4
- package/tests/native-review-parity.test.ts +23 -2
- package/tests/no-self-pause-contract.test.ts +5 -2
- package/tests/notification-audio-native.test.ts +128 -9
- package/tests/notification-customize.test.ts +17 -5
- package/tests/odd-integration.test.ts +5 -1
- package/tests/odd-routing-canonical-ratchet.test.ts +128 -162
- package/tests/odd-routing-contract.test.ts +115 -30
- package/tests/orchestrator-budget.test.ts +22 -8
- package/tests/orchestrator-discovery.test.ts +29 -0
- package/tests/orchestrator-presence.test.ts +100 -2
- package/tests/orchestrator-rdd-ownership.test.ts +3 -1
- package/tests/orchestrator-scope.test.ts +80 -0
- package/tests/orchestrator-state.test.ts +29 -0
- package/tests/orchestrator-status-publication-contract.test.ts +12 -8
- package/tests/package-files.test.ts +34 -0
- package/tests/package-manifest.test.ts +24 -20
- package/tests/permission-mode-policy.test.ts +10 -1
- package/tests/pi-update-launcher.test.ts +21 -2
- package/tests/pi-update.test.ts +99 -4
- package/tests/primary-session-prompt.test.ts +101 -0
- package/tests/provider-contract-bundle.test.ts +118 -0
- package/tests/provider-contract-mirror.test.ts +24 -0
- package/tests/quiet-tool-rendering.test.ts +23 -0
- package/tests/rdd-aware-verification-contract.test.ts +29 -11
- package/tests/rdd-status-line.test.ts +79 -0
- package/tests/review-agent-end-preflight.test.ts +6 -0
- package/tests/review-candidate-view.test.ts +49 -0
- package/tests/review-contract-prompt.test.ts +5 -2
- package/tests/review-controller-native-routing.test.ts +14 -1
- package/tests/review-controller.test.ts +1 -1
- package/tests/review-host-relay-routing.test.ts +147 -3
- package/tests/review-host-relay.test.ts +9 -2
- package/tests/review-json-arguments.test.ts +20 -3
- package/tests/review-loadout-rdd-off-sdk.test.ts +258 -0
- package/tests/review-owner-identity.test.ts +132 -0
- package/tests/review-relay-transport-agent.test.ts +26 -0
- package/tests/review-reminder-receipt.test.ts +31 -1
- package/tests/review-risk-assessment.test.ts +163 -76
- package/tests/review-session-standing-permission-controller.test.ts +14 -1
- package/tests/review-session-standing-permission.test.ts +27 -1
- package/tests/run-test-suite.test.ts +15 -0
- package/tests/runtime-harness.mjs +16 -6
- package/tests/session-changes-shell.test.ts +1 -1
- package/tests/session-changes.test.ts +2 -1
- package/tests/session-profile-append-controller-fork.test.ts +458 -0
- package/tests/session-profile-append-controller-pi.test.ts +290 -0
- package/tests/session-profile-append-controller.test.ts +731 -0
- package/tests/session-profile-disk-reader-pi.test.ts +183 -0
- package/tests/session-profile-disk-reader.test.ts +311 -0
- package/tests/session-profile-freeze.test.ts +23 -0
- package/tests/session-scratch.test.ts +1011 -0
- package/tests/session-subject-routing-contract.test.ts +2 -1
- package/tests/session-worktree-registry.test.ts +13 -1
- package/tests/shell-bar.test.ts +21 -2
- package/tests/shell-changes-view.test.ts +137 -1
- package/tests/shell-changes.test.ts +148 -1
- package/tests/shell-gauge.test.ts +42 -0
- package/tests/shell-prompt.test.ts +3 -3
- package/tests/shell-render-cache.test.ts +134 -1
- package/tests/shell-sidebar-layout.test.ts +50 -0
- package/tests/short-check-routing-contract.test.ts +1 -1
- package/tests/skill-collision-prefixes.test.ts +3 -1
- package/tests/skill-registry-watcher.test.ts +17 -5
- package/tests/skill-registry.test.ts +47 -3
- package/tests/small-task-inline-contract.test.ts +37 -0
- package/tests/standalone-config-home.test.ts +324 -0
- package/tests/startup-banner.test.ts +5 -4
- package/tests/startup-home-repo.test.ts +34 -0
- package/tests/support/orchestrator-modules.ts +1 -0
- package/tests/task-size-routing-contract.test.ts +70 -10
- package/tests/test-scratch-contract.test.ts +102 -0
- package/tests/test-strength-contract.test.ts +36 -1
- package/tests/tool-batching-contract.test.ts +64 -0
- package/tests/verification-proportionality.test.ts +46 -0
- package/tests/verifier-admission.test.ts +154 -0
- package/tests/verify-agent-contract.test.ts +8 -8
- package/tests/voice-command.test.ts +773 -0
- package/tests/voice-install.test.ts +323 -0
- package/tests/voice-platform.test.ts +176 -0
- package/tests/voice-push-to-talk.test.ts +444 -0
- package/tests/voice-session.test.ts +434 -0
- package/tests/voice-settings.test.ts +49 -0
- package/tests/voice-ui.test.ts +97 -0
- package/tests/voice-worker.test.ts +445 -0
- package/tests/weak-model-economy-prompt.test.ts +32 -0
- package/tests/windows-hidden-processes.test.ts +30 -2
- package/tests/writer-edit-surface-scope.test.ts +3 -0
- package/tests/writer-feature-document-admission.test.ts +156 -0
- package/tests/yolo-customize.test.ts +5 -1
- package/tests/yolo-mode.test.ts +19 -1
- package/tests/yolo-session-policy-capture.test.ts +104 -0
- package/tests/yolo-temp-scratch.test.ts +71 -0
- package/scripts/mirror-odd-routing.mjs +0 -242
- /package/contracts/review-provider-contract-mirror/{v1.3.0 → v1.4.0}/bundle/orchestration/pi.md +0 -0
- /package/contracts/review-provider-contract-mirror/{v1.3.0 → v1.4.0}/bundle/schemas/lens.schema.json +0 -0
- /package/contracts/review-provider-contract-mirror/{v1.3.0 → v1.4.0}/bundle/schemas/refuter.schema.json +0 -0
- /package/contracts/review-provider-contract-mirror/{v1.3.0 → v1.4.0}/bundle/schemas/targeted-validator.schema.json +0 -0
- /package/contracts/review-provider-contract-mirror/{v1.3.0 → v1.4.0}/bundle/vectors/lens.json +0 -0
- /package/contracts/review-provider-contract-mirror/{v1.3.0 → v1.4.0}/bundle/vectors/refuter.json +0 -0
- /package/contracts/review-provider-contract-mirror/{v1.3.0 → v1.4.0}/bundle/vectors/targeted-validator.json +0 -0
|
@@ -0,0 +1,183 @@
|
|
|
1
|
+
// Run only in the parent's isolated snapshot with supported existing Pi deps.
|
|
2
|
+
// Public API fixtures, never providers or production flush/rollback workarounds.
|
|
3
|
+
import assert from "node:assert/strict";
|
|
4
|
+
import {
|
|
5
|
+
appendFileSync,
|
|
6
|
+
existsSync,
|
|
7
|
+
mkdirSync,
|
|
8
|
+
mkdtempSync,
|
|
9
|
+
readFileSync,
|
|
10
|
+
renameSync,
|
|
11
|
+
rmSync,
|
|
12
|
+
} from "node:fs";
|
|
13
|
+
import { tmpdir } from "node:os";
|
|
14
|
+
import { dirname, join } from "node:path";
|
|
15
|
+
import { test, type TestContext } from "node:test";
|
|
16
|
+
import { fileURLToPath } from "node:url";
|
|
17
|
+
import { SessionManager } from "@earendil-works/pi-coding-agent";
|
|
18
|
+
import { readSessionProfileDisk } from "../lib/session-profile-disk-reader.ts";
|
|
19
|
+
import { SESSION_PROFILE_CUSTOM_TYPE as customType } from "../lib/session-profile-persistence.ts";
|
|
20
|
+
|
|
21
|
+
const packagePath = join(
|
|
22
|
+
dirname(fileURLToPath(import.meta.resolve("@earendil-works/pi-coding-agent"))),
|
|
23
|
+
"..",
|
|
24
|
+
"package.json",
|
|
25
|
+
);
|
|
26
|
+
const version: string = JSON.parse(readFileSync(packagePath, "utf8")).version;
|
|
27
|
+
// Fail closed rather than silently skipping tests against stale feature deps.
|
|
28
|
+
assert.match(version, /^\d+\.\d+\.\d+$/);
|
|
29
|
+
assert.ok(
|
|
30
|
+
Number(version.split(".")[0]) >= 1,
|
|
31
|
+
`Pi >=1.0.0 required, installed ${version}`,
|
|
32
|
+
);
|
|
33
|
+
console.info(
|
|
34
|
+
`Public SessionManager fixture API: ${version}; minimum tested baseline 1.0.0`,
|
|
35
|
+
);
|
|
36
|
+
|
|
37
|
+
function fixture(t: TestContext) {
|
|
38
|
+
const root = mkdtempSync(join(tmpdir(), "profile-reader-pi-"));
|
|
39
|
+
t.after(() => rmSync(root, { recursive: true, force: true }));
|
|
40
|
+
const sessions = join(root, "sessions");
|
|
41
|
+
const manager = SessionManager.create(root, sessions);
|
|
42
|
+
return { manager, sessions };
|
|
43
|
+
}
|
|
44
|
+
function bind(name: string) {
|
|
45
|
+
return {
|
|
46
|
+
kind: "bind",
|
|
47
|
+
origin: "user",
|
|
48
|
+
name,
|
|
49
|
+
modelProfiles: { worker: { model: `offline/${name}` } },
|
|
50
|
+
};
|
|
51
|
+
}
|
|
52
|
+
function flushFixture(manager: SessionManager) {
|
|
53
|
+
// An ordinary test-owned conversation message triggers Pi's normal persistence.
|
|
54
|
+
manager.appendMessage({
|
|
55
|
+
role: "user",
|
|
56
|
+
content: "isolated reader fixture",
|
|
57
|
+
timestamp: Date.now(),
|
|
58
|
+
});
|
|
59
|
+
}
|
|
60
|
+
function expectBound(manager: SessionManager, name: string) {
|
|
61
|
+
const result = readSessionProfileDisk(manager);
|
|
62
|
+
assert.equal(result.status, "bound");
|
|
63
|
+
if (result.status !== "bound")
|
|
64
|
+
throw new Error("Expected corroborated binding");
|
|
65
|
+
assert.equal(result.binding.name, name);
|
|
66
|
+
}
|
|
67
|
+
|
|
68
|
+
test("custom-only public session cannot restore memory; ordinary fixture flush corroborates", (t) => {
|
|
69
|
+
const { manager } = fixture(t);
|
|
70
|
+
manager.appendCustomEntry(customType, bind("initial"));
|
|
71
|
+
const path = manager.getSessionFile();
|
|
72
|
+
assert.ok(path);
|
|
73
|
+
assert.equal(existsSync(path), false);
|
|
74
|
+
assert.equal(readSessionProfileDisk(manager).status, "indeterminate");
|
|
75
|
+
flushFixture(manager);
|
|
76
|
+
expectBound(manager, "initial");
|
|
77
|
+
});
|
|
78
|
+
|
|
79
|
+
test("malformed line Pi skips on open does not block corroboration", (t) => {
|
|
80
|
+
const { manager, sessions } = fixture(t);
|
|
81
|
+
manager.appendCustomEntry(customType, bind("before"));
|
|
82
|
+
flushFixture(manager);
|
|
83
|
+
const path = manager.getSessionFile();
|
|
84
|
+
assert.ok(path);
|
|
85
|
+
appendFileSync(path, '{"type":"custom","id":"torn\n');
|
|
86
|
+
const reopened = SessionManager.open(path, sessions);
|
|
87
|
+
expectBound(reopened, "before");
|
|
88
|
+
reopened.appendCustomEntry(customType, bind("after"));
|
|
89
|
+
flushFixture(reopened);
|
|
90
|
+
expectBound(reopened, "after");
|
|
91
|
+
});
|
|
92
|
+
|
|
93
|
+
test("public selected branch, sibling and extracted session stay separated", (t) => {
|
|
94
|
+
const { manager, sessions } = fixture(t);
|
|
95
|
+
manager.appendCustomEntry(customType, bind("base"));
|
|
96
|
+
flushFixture(manager);
|
|
97
|
+
const base = manager.getLeafId();
|
|
98
|
+
assert.ok(base);
|
|
99
|
+
manager.appendCustomEntry(customType, bind("left"));
|
|
100
|
+
const left = manager.getLeafId();
|
|
101
|
+
assert.ok(left);
|
|
102
|
+
manager.branch(base);
|
|
103
|
+
manager.appendCustomEntry(customType, bind("right"));
|
|
104
|
+
expectBound(manager, "right");
|
|
105
|
+
manager.branch(left);
|
|
106
|
+
expectBound(manager, "left");
|
|
107
|
+
const originalFile = manager.getSessionFile();
|
|
108
|
+
const extracted = manager.createBranchedSession(left);
|
|
109
|
+
assert.ok(extracted);
|
|
110
|
+
assert.notEqual(extracted, originalFile);
|
|
111
|
+
const reopened = SessionManager.open(extracted, sessions);
|
|
112
|
+
expectBound(reopened, "left");
|
|
113
|
+
assert.equal(
|
|
114
|
+
reopened
|
|
115
|
+
.getBranch()
|
|
116
|
+
.some(
|
|
117
|
+
(row) =>
|
|
118
|
+
row.type === "custom" && (row.data as { name?: string })?.name === "right",
|
|
119
|
+
),
|
|
120
|
+
false,
|
|
121
|
+
);
|
|
122
|
+
});
|
|
123
|
+
|
|
124
|
+
for (const recovery of ["bind", "clear"] as const) {
|
|
125
|
+
test(`real EISDIR ghost quarantined; new ${recovery} corroborates missing parent even after reopen`, (t) => {
|
|
126
|
+
const { manager, sessions } = fixture(t);
|
|
127
|
+
manager.appendCustomEntry(customType, bind("good"));
|
|
128
|
+
flushFixture(manager);
|
|
129
|
+
const file = manager.getSessionFile();
|
|
130
|
+
assert.ok(file);
|
|
131
|
+
const saved = `${file}.saved`;
|
|
132
|
+
renameSync(file, saved);
|
|
133
|
+
mkdirSync(file);
|
|
134
|
+
assert.throws(
|
|
135
|
+
() => manager.appendCustomEntry(customType, bind("failed")),
|
|
136
|
+
(error: unknown) => {
|
|
137
|
+
assert.equal((error as NodeJS.ErrnoException).code, "EISDIR");
|
|
138
|
+
return true;
|
|
139
|
+
},
|
|
140
|
+
);
|
|
141
|
+
const failedId = manager.getLeafId();
|
|
142
|
+
assert.ok(failedId);
|
|
143
|
+
assert.equal(readSessionProfileDisk(manager).status, "indeterminate");
|
|
144
|
+
renameSync(file, `${file}.fault-directory`);
|
|
145
|
+
renameSync(saved, file);
|
|
146
|
+
assert.equal(readSessionProfileDisk(manager).status, "indeterminate");
|
|
147
|
+
const knownFailedEntryIds = new Set([failedId]);
|
|
148
|
+
const old = readSessionProfileDisk(manager, { knownFailedEntryIds });
|
|
149
|
+
assert.equal(old.status, "bound");
|
|
150
|
+
if (old.status === "bound") assert.equal(old.binding.name, "good");
|
|
151
|
+
manager.appendCustomEntry(
|
|
152
|
+
customType,
|
|
153
|
+
recovery === "bind" ? bind("recovered") : { kind: "clear" },
|
|
154
|
+
);
|
|
155
|
+
const latest = manager.getBranch().at(-1);
|
|
156
|
+
assert.ok(latest);
|
|
157
|
+
assert.equal(latest.parentId, failedId);
|
|
158
|
+
const disk = readFileSync(file, "utf8")
|
|
159
|
+
.trim()
|
|
160
|
+
.split("\n")
|
|
161
|
+
.map((line) => JSON.parse(line));
|
|
162
|
+
assert.equal(
|
|
163
|
+
disk.some((row) => row.id === failedId),
|
|
164
|
+
false,
|
|
165
|
+
);
|
|
166
|
+
assert.deepEqual(
|
|
167
|
+
disk.find((row) => row.id === latest.id),
|
|
168
|
+
JSON.parse(JSON.stringify(latest)),
|
|
169
|
+
);
|
|
170
|
+
assert.equal(
|
|
171
|
+
readSessionProfileDisk(manager, { knownFailedEntryIds }).status,
|
|
172
|
+
recovery === "bind" ? "bound" : "cleared",
|
|
173
|
+
);
|
|
174
|
+
const reopened = SessionManager.open(file, sessions);
|
|
175
|
+
assert.equal(reopened.getBranch().length, 1);
|
|
176
|
+
assert.equal(reopened.getBranch()[0].id, latest.id);
|
|
177
|
+
assert.equal(
|
|
178
|
+
readSessionProfileDisk(reopened).status,
|
|
179
|
+
recovery === "bind" ? "bound" : "cleared",
|
|
180
|
+
);
|
|
181
|
+
if (recovery === "bind") expectBound(reopened, "recovered");
|
|
182
|
+
});
|
|
183
|
+
}
|
|
@@ -0,0 +1,311 @@
|
|
|
1
|
+
import assert from "node:assert/strict";
|
|
2
|
+
import { mkdtempSync, readFileSync, rmSync, writeFileSync } from "node:fs";
|
|
3
|
+
import { tmpdir } from "node:os";
|
|
4
|
+
import { join } from "node:path";
|
|
5
|
+
import { test, type TestContext } from "node:test";
|
|
6
|
+
import {
|
|
7
|
+
type DiskProfileIndeterminateReason,
|
|
8
|
+
readSessionProfileDisk,
|
|
9
|
+
} from "../lib/session-profile-disk-reader.ts";
|
|
10
|
+
import { SESSION_PROFILE_CUSTOM_TYPE as customType } from "../lib/session-profile-persistence.ts";
|
|
11
|
+
|
|
12
|
+
const header = { type: "session", id: "session", version: 3 };
|
|
13
|
+
function entry(id = "selected", data: unknown = { kind: "clear" }) {
|
|
14
|
+
return {
|
|
15
|
+
type: "custom",
|
|
16
|
+
id,
|
|
17
|
+
parentId: null,
|
|
18
|
+
timestamp: "2026-10-05T00:00:00Z",
|
|
19
|
+
customType,
|
|
20
|
+
data,
|
|
21
|
+
};
|
|
22
|
+
}
|
|
23
|
+
function fixture(
|
|
24
|
+
t: TestContext,
|
|
25
|
+
branch: unknown[] = [],
|
|
26
|
+
rows: unknown[] = [header, ...branch],
|
|
27
|
+
) {
|
|
28
|
+
const root = mkdtempSync(join(tmpdir(), "profile-reader-"));
|
|
29
|
+
t.after(() => rmSync(root, { recursive: true, force: true }));
|
|
30
|
+
const path = join(root, "session.jsonl");
|
|
31
|
+
writeFileSync(
|
|
32
|
+
path,
|
|
33
|
+
rows
|
|
34
|
+
.map((row) => (typeof row === "string" ? row : JSON.stringify(row)))
|
|
35
|
+
.join("\n") + "\n",
|
|
36
|
+
);
|
|
37
|
+
const source = {
|
|
38
|
+
getSessionId: () => "session",
|
|
39
|
+
getSessionFile: (): string | undefined => path,
|
|
40
|
+
getBranch: () => branch,
|
|
41
|
+
};
|
|
42
|
+
return { path, source, read: () => readSessionProfileDisk(source) };
|
|
43
|
+
}
|
|
44
|
+
|
|
45
|
+
test("header only establishes absence", (t) => {
|
|
46
|
+
assert.deepEqual(fixture(t).read(), { status: "absent" });
|
|
47
|
+
});
|
|
48
|
+
|
|
49
|
+
test("ordinary branch without family remains absent despite disk sibling", (t) => {
|
|
50
|
+
const message = {
|
|
51
|
+
type: "message",
|
|
52
|
+
id: "message",
|
|
53
|
+
parentId: null,
|
|
54
|
+
timestamp: "fixture",
|
|
55
|
+
};
|
|
56
|
+
assert.deepEqual(
|
|
57
|
+
fixture(t, [message], [header, message, entry("sibling")]).read(),
|
|
58
|
+
{ status: "absent" },
|
|
59
|
+
);
|
|
60
|
+
});
|
|
61
|
+
|
|
62
|
+
test("omitted data is terminal invalid or unsupported, never older resurrection", (t) => {
|
|
63
|
+
for (const identifier of [customType, "gentle-pi.session-profile/v9"]) {
|
|
64
|
+
const selected = { ...entry(), customType: identifier, data: undefined };
|
|
65
|
+
const result = fixture(t, [entry("older"), selected]).read();
|
|
66
|
+
assert.equal(
|
|
67
|
+
result.status,
|
|
68
|
+
identifier === customType ? "invalid" : "unsupported",
|
|
69
|
+
);
|
|
70
|
+
}
|
|
71
|
+
});
|
|
72
|
+
|
|
73
|
+
for (const [label, data, status] of [
|
|
74
|
+
["clear", { kind: "clear" }, "cleared"],
|
|
75
|
+
[
|
|
76
|
+
"empty",
|
|
77
|
+
{ kind: "bind", origin: "user", name: "chosen", modelProfiles: {} },
|
|
78
|
+
"bound",
|
|
79
|
+
],
|
|
80
|
+
["invalid", { kind: "broken" }, "invalid"],
|
|
81
|
+
] as const) {
|
|
82
|
+
test(`corroborated ${label} retains semantic state and original positions`, (t) => {
|
|
83
|
+
const selected = entry("selected", data);
|
|
84
|
+
const message = {
|
|
85
|
+
type: "message",
|
|
86
|
+
id: "message",
|
|
87
|
+
parentId: null,
|
|
88
|
+
timestamp: "fixture",
|
|
89
|
+
};
|
|
90
|
+
const f = fixture(t, [message, selected], [header, "", message, selected]);
|
|
91
|
+
const result = f.read();
|
|
92
|
+
assert.equal(result.status, status);
|
|
93
|
+
assert.equal(result.entryIndex, 1);
|
|
94
|
+
assert.equal(result.lineNumber, 4);
|
|
95
|
+
});
|
|
96
|
+
}
|
|
97
|
+
|
|
98
|
+
test("future newest is terminal; sibling records never win", (t) => {
|
|
99
|
+
const older = entry("older");
|
|
100
|
+
const future = {
|
|
101
|
+
...entry("future"),
|
|
102
|
+
customType: "gentle-pi.session-profile/v2",
|
|
103
|
+
};
|
|
104
|
+
const f = fixture(
|
|
105
|
+
t,
|
|
106
|
+
[older, future],
|
|
107
|
+
[header, older, future, entry("sibling")],
|
|
108
|
+
);
|
|
109
|
+
assert.equal(f.read().status, "unsupported");
|
|
110
|
+
});
|
|
111
|
+
|
|
112
|
+
test("unknown unwritten newest never falls back; trusted failed evidence excludes only its ID", (t) => {
|
|
113
|
+
const older = entry("older");
|
|
114
|
+
const ghost = entry("ghost");
|
|
115
|
+
const f = fixture(t, [older, ghost], [header, older]);
|
|
116
|
+
assert.equal(f.read().status, "indeterminate");
|
|
117
|
+
const options = { knownFailedEntryIds: new Set(["ghost"]) };
|
|
118
|
+
assert.equal(readSessionProfileDisk(f.source, options).status, "cleared");
|
|
119
|
+
writeFileSync(
|
|
120
|
+
f.path,
|
|
121
|
+
[header, older, ghost].map((row) => JSON.stringify(row)).join("\n"),
|
|
122
|
+
);
|
|
123
|
+
assert.equal(readSessionProfileDisk(f.source, options).entryIndex, 0);
|
|
124
|
+
assert.equal(
|
|
125
|
+
readSessionProfileDisk(f.source, {
|
|
126
|
+
knownFailedEntryIds: new Set(["unrelated"]),
|
|
127
|
+
}).entryIndex,
|
|
128
|
+
1,
|
|
129
|
+
);
|
|
130
|
+
});
|
|
131
|
+
|
|
132
|
+
for (const [label, rows] of [
|
|
133
|
+
["wrong session", [{ ...header, id: "other" }, entry()]],
|
|
134
|
+
["wrong header", [{ ...header, type: "message" }, entry()]],
|
|
135
|
+
["duplicate ID", [header, entry(), entry()]],
|
|
136
|
+
["malformed", [header, "{secret"]],
|
|
137
|
+
["truncated", [header, '{"type":']],
|
|
138
|
+
["data mismatch", [header, entry("selected", { kind: "broken" })]],
|
|
139
|
+
["parent mismatch", [header, { ...entry(), parentId: "other" }]],
|
|
140
|
+
["metadata mismatch", [header, { ...entry(), timestamp: "other" }]],
|
|
141
|
+
["custom type mismatch", [header, { ...entry(), customType: "other" }]],
|
|
142
|
+
["extra metadata mismatch", [header, { ...entry(), extra: "changed" }]],
|
|
143
|
+
["nonobject record", [header, "null"]],
|
|
144
|
+
["empty file", []],
|
|
145
|
+
[
|
|
146
|
+
"missing metadata",
|
|
147
|
+
[
|
|
148
|
+
header,
|
|
149
|
+
{ type: "custom", id: "selected", customType, data: { kind: "clear" } },
|
|
150
|
+
],
|
|
151
|
+
],
|
|
152
|
+
] as const) {
|
|
153
|
+
test(`${label} is explicitly indeterminate`, (t) => {
|
|
154
|
+
const result = fixture(t, [entry()], [...rows]).read();
|
|
155
|
+
assert.equal(result.status, "indeterminate");
|
|
156
|
+
assert.ok(!JSON.stringify(result).includes("secret"));
|
|
157
|
+
});
|
|
158
|
+
}
|
|
159
|
+
|
|
160
|
+
test("malformed lines are skipped like Pi's session parser, keeping physical line numbers", (t) => {
|
|
161
|
+
const selected = entry();
|
|
162
|
+
for (const rows of [
|
|
163
|
+
[header, "{bad private contents", selected],
|
|
164
|
+
["{bad private contents", header, selected],
|
|
165
|
+
[header, selected, '{"type":"custom","id":"torn'],
|
|
166
|
+
]) {
|
|
167
|
+
const result = fixture(t, [selected], rows).read();
|
|
168
|
+
assert.equal(result.status, "cleared");
|
|
169
|
+
assert.equal(result.lineNumber, rows.indexOf(selected) + 1);
|
|
170
|
+
}
|
|
171
|
+
});
|
|
172
|
+
|
|
173
|
+
test("malformed selected record is missing, never admitted", (t) => {
|
|
174
|
+
const result = fixture(t, [entry()], [header, "{secret"]).read();
|
|
175
|
+
assert.deepEqual(result, {
|
|
176
|
+
status: "indeterminate",
|
|
177
|
+
reason: "selected-record-missing",
|
|
178
|
+
});
|
|
179
|
+
});
|
|
180
|
+
|
|
181
|
+
test("file with only blank or malformed lines has no header", (t) => {
|
|
182
|
+
for (const rows of [[], ["", "{bad"]]) {
|
|
183
|
+
assert.deepEqual(fixture(t, [entry()], rows).read(), {
|
|
184
|
+
status: "indeterminate",
|
|
185
|
+
reason: "missing-header",
|
|
186
|
+
});
|
|
187
|
+
}
|
|
188
|
+
});
|
|
189
|
+
|
|
190
|
+
test("non-string customType is ordinary, non-string kind and empty agent name are invalid", (t) => {
|
|
191
|
+
const ordinary = { ...entry(), customType: 7 };
|
|
192
|
+
assert.deepEqual(fixture(t, [ordinary]).read(), { status: "absent" });
|
|
193
|
+
for (const data of [
|
|
194
|
+
{ kind: 1 },
|
|
195
|
+
{ kind: ["clear"] },
|
|
196
|
+
{
|
|
197
|
+
kind: "bind",
|
|
198
|
+
origin: "user",
|
|
199
|
+
name: "chosen",
|
|
200
|
+
modelProfiles: { "": "provider/model" },
|
|
201
|
+
},
|
|
202
|
+
]) {
|
|
203
|
+
const selected = entry("selected", data);
|
|
204
|
+
assert.equal(fixture(t, [selected]).read().status, "invalid");
|
|
205
|
+
}
|
|
206
|
+
});
|
|
207
|
+
|
|
208
|
+
test("every indeterminate reason is a named literal", () => {
|
|
209
|
+
const reasons = {
|
|
210
|
+
"missing-source": true,
|
|
211
|
+
"invalid-candidate-metadata": true,
|
|
212
|
+
"unreadable-file": true,
|
|
213
|
+
"source-changed": true,
|
|
214
|
+
"invalid-record": true,
|
|
215
|
+
"session-header-mismatch": true,
|
|
216
|
+
"duplicate-id": true,
|
|
217
|
+
"missing-header": true,
|
|
218
|
+
"selected-record-missing": true,
|
|
219
|
+
"selected-record-mismatch": true,
|
|
220
|
+
"unserializable-source": true,
|
|
221
|
+
} satisfies Record<DiskProfileIndeterminateReason, true>;
|
|
222
|
+
assert.equal(Object.keys(reasons).length, 11);
|
|
223
|
+
});
|
|
224
|
+
|
|
225
|
+
test("missing, undefined and unreadable paths are conservative", (t) => {
|
|
226
|
+
const f = fixture(t);
|
|
227
|
+
rmSync(f.path);
|
|
228
|
+
assert.equal(f.read().status, "indeterminate");
|
|
229
|
+
f.source.getSessionFile = () => undefined;
|
|
230
|
+
assert.equal(f.read().status, "indeterminate");
|
|
231
|
+
f.source.getSessionFile = () => f.path;
|
|
232
|
+
assert.equal(
|
|
233
|
+
readSessionProfileDisk(f.source, {
|
|
234
|
+
readFile: () => {
|
|
235
|
+
throw new Error("private");
|
|
236
|
+
},
|
|
237
|
+
}).status,
|
|
238
|
+
"indeterminate",
|
|
239
|
+
);
|
|
240
|
+
});
|
|
241
|
+
|
|
242
|
+
for (const change of ["id", "path", "branch"] as const) {
|
|
243
|
+
test(`source ${change} change during read invalidates authority`, (t) => {
|
|
244
|
+
const f = fixture(t, [entry()]);
|
|
245
|
+
const result = readSessionProfileDisk(f.source, {
|
|
246
|
+
readFile: (path) => {
|
|
247
|
+
const text = readFileSync(path, "utf8");
|
|
248
|
+
if (change === "id") f.source.getSessionId = () => "changed";
|
|
249
|
+
if (change === "path") f.source.getSessionFile = () => "changed";
|
|
250
|
+
if (change === "branch") f.source.getBranch().push(entry("changed"));
|
|
251
|
+
return text;
|
|
252
|
+
},
|
|
253
|
+
});
|
|
254
|
+
assert.equal(result.status, "indeterminate");
|
|
255
|
+
});
|
|
256
|
+
}
|
|
257
|
+
|
|
258
|
+
test("JSON key order and omitted undefined follow serialization; orphan parent is allowed", (t) => {
|
|
259
|
+
const selected = { ...entry(), parentId: "missing-failed", extra: undefined };
|
|
260
|
+
const reversed = Object.fromEntries(Object.entries(selected).reverse());
|
|
261
|
+
assert.equal(
|
|
262
|
+
fixture(t, [selected], [header, reversed]).read().status,
|
|
263
|
+
"cleared",
|
|
264
|
+
);
|
|
265
|
+
});
|
|
266
|
+
|
|
267
|
+
test("unserializable memory and malformed own candidate metadata fail closed", (t) => {
|
|
268
|
+
const circular: any = entry();
|
|
269
|
+
circular.extra = circular;
|
|
270
|
+
assert.equal(
|
|
271
|
+
fixture(t, [circular], [header, entry()]).read().status,
|
|
272
|
+
"indeterminate",
|
|
273
|
+
);
|
|
274
|
+
for (const field of ["id", "parentId", "timestamp"]) {
|
|
275
|
+
const candidate: any = entry();
|
|
276
|
+
delete candidate[field];
|
|
277
|
+
assert.equal(
|
|
278
|
+
fixture(t, [candidate], [header, candidate]).read().status,
|
|
279
|
+
"indeterminate",
|
|
280
|
+
);
|
|
281
|
+
}
|
|
282
|
+
});
|
|
283
|
+
|
|
284
|
+
test("all origins, legacy routes and orchestrator are detached and preserved", (t) => {
|
|
285
|
+
for (const origin of ["user", "local", "repo", "global"]) {
|
|
286
|
+
const selected = entry("selected", {
|
|
287
|
+
kind: "bind",
|
|
288
|
+
origin,
|
|
289
|
+
name: "chosen",
|
|
290
|
+
modelProfiles: {
|
|
291
|
+
worker: "provider/model",
|
|
292
|
+
orchestrator: { effort: "high" },
|
|
293
|
+
},
|
|
294
|
+
});
|
|
295
|
+
const f = fixture(t, [selected]);
|
|
296
|
+
const result = f.read();
|
|
297
|
+
assert.equal(result.status, "bound");
|
|
298
|
+
if (result.status !== "bound") throw new Error("expected binding");
|
|
299
|
+
assert.equal(result.binding.origin, origin);
|
|
300
|
+
assert.deepEqual(result.binding.modelProfiles.orchestrator, {
|
|
301
|
+
thinking: "high",
|
|
302
|
+
});
|
|
303
|
+
result.binding.modelProfiles.worker = { model: "changed" };
|
|
304
|
+
const next = f.read();
|
|
305
|
+
assert.equal(next.status, "bound");
|
|
306
|
+
if (next.status === "bound")
|
|
307
|
+
assert.deepEqual(next.binding.modelProfiles.worker, {
|
|
308
|
+
model: "provider/model",
|
|
309
|
+
});
|
|
310
|
+
}
|
|
311
|
+
});
|
|
@@ -295,6 +295,29 @@ test("drift notice: shown once per distinct change of the defaults", () => {
|
|
|
295
295
|
assert.equal(readFrozenInheritedProfile("session-a")?.profile?.name, "frontier", "the notice never adopts the new default");
|
|
296
296
|
});
|
|
297
297
|
|
|
298
|
+
test("drift notice: an explicit binding learns its profile definition changed in profiles.json", () => {
|
|
299
|
+
const f = fixture({ team: { worker: { model: "openai/old" } } });
|
|
300
|
+
const opts = options(f, "session-a");
|
|
301
|
+
bindSessionProfile("session-a", "team", { worker: { model: "openai/old" } });
|
|
302
|
+
assert.equal(inheritedProfileDriftNotice(opts), undefined, "no drift while the definition matches the binding");
|
|
303
|
+
|
|
304
|
+
f.editProfile("team", { worker: { model: "openai/new" } });
|
|
305
|
+
const notice = inheritedProfileDriftNotice(opts);
|
|
306
|
+
assert.match(notice ?? "", /the profile definition changed to "team"/);
|
|
307
|
+
assert.match(notice ?? "", /this session keeps the binding from selection/);
|
|
308
|
+
assert.equal(inheritedProfileDriftNotice(opts), undefined, "the same change is not repeated");
|
|
309
|
+
|
|
310
|
+
f.editProfile("team", { worker: { model: "openai/older" } });
|
|
311
|
+
assert.match(inheritedProfileDriftNotice(opts) ?? "", /the profile definition changed to "team"/, "a different edit is a distinct change");
|
|
312
|
+
assert.equal(inheritedProfileDriftNotice(opts), undefined);
|
|
313
|
+
f.editProfile("team", { worker: { model: "openai/old" } });
|
|
314
|
+
assert.equal(inheritedProfileDriftNotice(opts), undefined, "reverting to the bound content is not drift");
|
|
315
|
+
|
|
316
|
+
const childOpts = options(f, "session-b", { GENTLE_PI_AGENTS_CHILD: "1" });
|
|
317
|
+
bindSessionProfile("session-b", "team", { worker: { model: "openai/old" } });
|
|
318
|
+
assert.equal(inheritedProfileDriftNotice(childOpts), undefined, "children never get the notice");
|
|
319
|
+
});
|
|
320
|
+
|
|
298
321
|
test("drift notice: an edit of the default profile's content counts as a change", () => {
|
|
299
322
|
const f = fixture();
|
|
300
323
|
f.setActive("frontier");
|