gentle-pi 4.1.0 → 4.3.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 +352 -128
- 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/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 +9 -9
- package/bin/engram-guard +1 -1
- package/bin/gentle-shell.mjs +188 -35
- 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/auto-permission-answers.md +25 -0
- package/docs/auto-permission-evidence.md +39 -0
- package/docs/bundled-install.md +40 -8
- package/docs/delegated-verification.md +10 -8
- package/docs/gentle-agents-activity.md +11 -5
- package/docs/gentle-shell-uninstall.md +8 -1
- package/docs/gentle-shell.md +20 -12
- package/docs/install-wizard.md +17 -10
- package/docs/permission-modes.md +51 -6
- package/docs/readme-reference.md +155 -31
- package/docs/session-profile-format.md +39 -0
- package/docs/yolo-mode.md +21 -7
- package/extensions/ask-user-choice.ts +4 -0
- package/extensions/ask-user-question.ts +6 -1
- package/extensions/codegraph-tools.ts +68 -6
- package/extensions/gentle-agents.ts +146 -28
- package/extensions/gentle-ai.ts +380 -68
- package/extensions/gentle-herdr-activity.ts +2 -3
- package/extensions/gentle-shell.ts +53 -43
- package/extensions/gentle-todo.ts +62 -18
- package/extensions/quiet-tools.ts +1 -1
- package/extensions/session-scratch.ts +365 -0
- package/extensions/startup-banner.ts +2 -3
- package/lib/agent-home.ts +8 -3
- package/lib/agents-protocol.ts +3 -0
- package/lib/agents-runner.ts +55 -6
- package/lib/agents-view.ts +3 -1
- package/lib/auto-permission-answers.ts +133 -0
- package/lib/auto-permission-classifier.ts +215 -36
- package/lib/auto-permission-evidence.ts +88 -0
- package/lib/auto-permission-memory.ts +69 -0
- package/lib/auto-permission-runtime.ts +251 -35
- package/lib/auto-permission-tool-context.ts +61 -0
- package/lib/bounded-writer-admission.ts +51 -0
- package/lib/command-palette-catalog.ts +0 -4
- package/lib/commit-slice-signal.ts +100 -0
- package/lib/context-backstop.ts +84 -0
- package/lib/delivered-test-lint.ts +80 -0
- package/lib/destructive-command-guard.ts +215 -28
- package/lib/gauge-theme.ts +146 -0
- package/lib/gentle-ai-binary.ts +28 -7
- package/lib/gentle-shell-bundled.ts +62 -8
- package/lib/gentle-shell-import-pi.ts +233 -0
- package/lib/gentle-shell-launcher.ts +146 -12
- package/lib/gentle-shell-uninstall.ts +18 -1
- package/lib/herdr-activity.ts +24 -0
- package/lib/model-routing-authority.ts +6 -1
- package/lib/native-review-cli.ts +9 -1
- package/lib/orchestrator-discovery.ts +5 -1
- package/lib/orchestrator-presence.ts +9 -0
- package/lib/orchestrator-scope.ts +21 -3
- 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-risk-assessment.ts +43 -38
- package/lib/review-session-standing-permission.ts +3 -1
- package/lib/selection-engine.ts +4 -1
- package/lib/session-profile-freeze.ts +40 -16
- package/lib/session-profile-persistence.ts +187 -0
- package/lib/session-scratch.ts +474 -0
- package/lib/shell-bar.ts +25 -25
- package/lib/shell-changes-view.ts +80 -13
- package/lib/shell-changes.ts +2 -0
- package/lib/shell-gauge.ts +49 -6
- package/lib/shell-prompt.ts +6 -4
- package/lib/shell-todo.ts +39 -8
- package/lib/shell-usage-view.ts +5 -1
- package/lib/shell-usage.ts +7 -7
- package/lib/verifier-admission.ts +29 -0
- package/lib/vim-editor-adapter.ts +22 -4
- package/lib/yolo-temp-scratch.ts +69 -0
- package/package.json +5 -6
- package/runtime/gentle-ai-binary.mjs +28 -7
- package/runtime/gentle-shell-bundled.mjs +61 -7
- package/runtime/gentle-shell-import-pi.mjs +234 -0
- package/runtime/gentle-shell-launcher.mjs +144 -10
- package/runtime/gentle-shell-uninstall.mjs +18 -1
- package/runtime/native-review-cli.mjs +8 -0
- package/runtime/review-risk-assessment.mjs +43 -38
- package/scripts/build-runtime-modules.mjs +1 -0
- package/scripts/bundled-engram-migration.mjs +125 -0
- package/scripts/gentle-ai-installer.mjs +10 -10
- package/scripts/install-wizard-preview.mjs +2 -2
- package/scripts/installer-downloads.mjs +7 -7
- package/scripts/installer-preflight.mjs +1 -1
- package/scripts/installer-server.mjs +14 -4
- package/scripts/main-channel.mjs +3 -2
- package/scripts/pi-update.mjs +223 -0
- package/scripts/run-test-suite.mjs +5 -0
- package/scripts/verify-package-files.mjs +19 -13
- package/skills/gentle-ai/SKILL.md +3 -3
- package/skills/work-unit-commits/SKILL.md +5 -3
- package/tests/agent-risk-escalation.test.ts +37 -10
- package/tests/agents-runner.test.ts +260 -3
- package/tests/append-system-prompt-route.test.ts +18 -1
- package/tests/auto-mode-human-answers.test.ts +68 -0
- package/tests/auto-mode-invariants.test.ts +23 -5
- package/tests/auto-mode-sdk.test.ts +29 -2
- package/tests/auto-permission-answers.test.ts +170 -0
- package/tests/auto-permission-classifier.test.ts +353 -2
- package/tests/auto-permission-compound.test.ts +24 -0
- package/tests/auto-permission-evidence.test.ts +88 -0
- package/tests/auto-permission-memory.test.ts +46 -0
- package/tests/auto-permission-public-regressions.test.ts +111 -0
- package/tests/auto-permission-runtime.test.ts +270 -12
- package/tests/autonomous-guard.test.ts +208 -16
- 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/codegraph-tools.test.ts +123 -2
- package/tests/command-palette.test.ts +14 -4
- package/tests/commit-slice-signal.test.ts +162 -0
- package/tests/context-backstop.test.ts +159 -0
- package/tests/delegate-for-reason-contract.test.ts +91 -24
- package/tests/delivered-test-lint.test.ts +105 -0
- package/tests/destructive-command-guard.test.ts +85 -1
- package/tests/engram-safety.test.ts +35 -7
- 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 +443 -8
- 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-shell-bin.test.ts +180 -17
- package/tests/gentle-shell-bundled.test.ts +235 -38
- package/tests/gentle-shell-import-pi.test.ts +262 -0
- package/tests/gentle-shell-launcher.test.ts +97 -13
- package/tests/gentle-shell-uninstall.test.ts +30 -0
- package/tests/gentle-shell.test.ts +113 -14
- package/tests/gentle-todo.test.ts +258 -27
- package/tests/headless-delivery-question.test.ts +58 -0
- package/tests/herdr-activity.test.ts +34 -1
- package/tests/install-wizard.test.ts +2 -2
- package/tests/installer-posix-bootstrap.test.ts +8 -8
- package/tests/installer-preflight.test.ts +3 -3
- package/tests/installer-probes.test.ts +8 -8
- package/tests/installer-runner.test.ts +14 -13
- package/tests/installer-server.test.ts +13 -5
- package/tests/installer-windows-bootstrap.test.ts +33 -4
- package/tests/main-channel-upgrade.test.ts +7 -1
- package/tests/native-review-capability-contract.test.ts +7 -1
- package/tests/native-review-parity.test.ts +22 -1
- package/tests/no-self-pause-contract.test.ts +5 -2
- package/tests/odd-integration.test.ts +5 -1
- package/tests/odd-routing-canonical-ratchet.test.ts +127 -162
- package/tests/odd-routing-contract.test.ts +115 -30
- package/tests/odd-runtime-delegation-gate.test.ts +4 -1
- package/tests/orchestrator-budget.test.ts +19 -8
- package/tests/orchestrator-discovery.test.ts +29 -0
- package/tests/orchestrator-rdd-ownership.test.ts +3 -1
- package/tests/orchestrator-scope.test.ts +66 -0
- package/tests/orchestrator-status-publication-contract.test.ts +12 -8
- package/tests/package-manifest.test.ts +28 -24
- package/tests/pi-update-launcher.test.ts +106 -0
- package/tests/pi-update.test.ts +186 -0
- 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 +131 -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 +59 -2
- package/tests/review-loadout-rdd-off-sdk.test.ts +237 -0
- package/tests/review-owner-identity.test.ts +132 -0
- 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/selection-engine.test.ts +35 -5
- package/tests/session-changes-shell.test.ts +1 -1
- package/tests/session-profile-freeze.test.ts +37 -1
- package/tests/session-profile-persistence.test.ts +334 -0
- package/tests/session-scratch.test.ts +945 -0
- package/tests/session-subject-routing-contract.test.ts +2 -1
- package/tests/shell-bar.test.ts +7 -0
- package/tests/shell-changes-view.test.ts +137 -1
- package/tests/shell-changes.test.ts +18 -0
- package/tests/shell-gauge.test.ts +42 -0
- package/tests/shell-todo.test.ts +15 -12
- 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 +69 -9
- 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 +137 -0
- package/tests/verify-agent-contract.test.ts +8 -8
- package/tests/vim-editor-adapter.test.ts +41 -15
- package/tests/windows-hidden-processes.test.ts +5 -0
- package/tests/writer-edit-surface-scope.test.ts +3 -0
- package/tests/writer-feature-document-admission.test.ts +156 -0
- package/tests/yolo-mode.test.ts +24 -1
- 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
|
@@ -123,6 +123,8 @@ console.log("ENV_PROBE=" + JSON.stringify({
|
|
|
123
123
|
home,
|
|
124
124
|
files: home && existsSync(home) ? readdirSync(home) : null,
|
|
125
125
|
sentinel: process.env.TEST_ENV_SENTINEL,
|
|
126
|
+
tmp: process.env.TMPDIR ?? null,
|
|
127
|
+
tmpExists: process.env.TMPDIR ? existsSync(process.env.TMPDIR) : null,
|
|
126
128
|
}));
|
|
127
129
|
`);
|
|
128
130
|
writeFileSync(stagesPath, JSON.stringify([
|
|
@@ -200,3 +202,16 @@ test("direct invocation rejects a malformed stages file with a non-zero exit", (
|
|
|
200
202
|
assert.equal(result.status, 1);
|
|
201
203
|
assert.match(result.stderr ?? "", /stages file must be an array/);
|
|
202
204
|
});
|
|
205
|
+
|
|
206
|
+
// Harness roadmap T3: each subagent gets its own TMPDIR, and unit fixtures that
|
|
207
|
+
// leave a fake child unfinished would leave it behind; the stage owns them all.
|
|
208
|
+
test("unit stage gets a private TMPDIR removed after the stage; later stages keep the inherited one", (t) => {
|
|
209
|
+
const inherited = mkdtempSync(join(tmpdir(), "gentle-pi-caller-tmp-"));
|
|
210
|
+
t.after(() => rmSync(inherited, { recursive: true, force: true }));
|
|
211
|
+
const [unit, provider] = probeStageEnvironment(t, { TMPDIR: inherited });
|
|
212
|
+
assert.notEqual(unit.tmp, inherited);
|
|
213
|
+
assert.match(unit.tmp, /gentle-pi-unit-tmp-/);
|
|
214
|
+
assert.equal(unit.tmpExists, true, "the private TMPDIR exists while the stage runs");
|
|
215
|
+
assert.equal(existsSync(unit.tmp), false, "the private TMPDIR is removed after the stage");
|
|
216
|
+
assert.equal(provider.tmp, inherited);
|
|
217
|
+
});
|
|
@@ -604,9 +604,19 @@ async function run() {
|
|
|
604
604
|
assert.equal(writerDispatch.task, task, "writer guard must not mutate child input");
|
|
605
605
|
}
|
|
606
606
|
|
|
607
|
+
// S16: generic writers are admitted only for a task of an existing feature document (or at the measured
|
|
608
|
+
// context backstop); the accepted cases below name one, and an undocumented launch is told to work inline.
|
|
609
|
+
await mkdir(join(toolCwd, "odd", "tasks"), { recursive: true });
|
|
610
|
+
await writeFile(join(toolCwd, "odd", "tasks", "harness.md"), "# Harness\n");
|
|
611
|
+
const undocumentedWriter = await toolHook(
|
|
612
|
+
{ toolName: "subagent_run", input: { agent: "gentle-ai-worker", task: "Implement the requested change.\n\n## Allowed edit surfaces\nextensions/gentle-ai.ts", mode: "task" } },
|
|
613
|
+
createCtx(toolCwd),
|
|
614
|
+
);
|
|
615
|
+
assert.equal(undocumentedWriter?.block, true, "an undocumented writer must be blocked before dispatch");
|
|
616
|
+
assert.match(undocumentedWriter?.reason ?? "", /write this unit inline yourself/);
|
|
607
617
|
const scopedWriterDispatch = {
|
|
608
618
|
agent: "gentle-ai-worker",
|
|
609
|
-
task: "Implement the requested change.\n\n## Allowed edit surfaces\nextensions/gentle-ai.ts\ntests/runtime-harness.mjs",
|
|
619
|
+
task: "Spec: odd/tasks/harness.md, T1. Implement the requested change.\n\n## Allowed edit surfaces\nextensions/gentle-ai.ts\ntests/runtime-harness.mjs",
|
|
610
620
|
mode: "task",
|
|
611
621
|
};
|
|
612
622
|
assert.equal(
|
|
@@ -620,7 +630,7 @@ async function run() {
|
|
|
620
630
|
toolName: "subagent_run",
|
|
621
631
|
input: {
|
|
622
632
|
agent: "worker",
|
|
623
|
-
task: "Implement the requested change.",
|
|
633
|
+
task: "Spec: odd/tasks/harness.md, T2. Implement the requested change.",
|
|
624
634
|
context: "## Allowed edit surfaces\n- assets/orchestrator.md",
|
|
625
635
|
mode: "task",
|
|
626
636
|
},
|
|
@@ -717,7 +727,7 @@ async function run() {
|
|
|
717
727
|
"valid scope followed by a repository-root scope",
|
|
718
728
|
{
|
|
719
729
|
agent: "gentle-ai-worker",
|
|
720
|
-
task: "## Allowed edit surfaces\nextensions/gentle-ai.ts\n\n## Allowed edit surfaces\n.",
|
|
730
|
+
task: "Spec: odd/tasks/harness.md, T4.\n\n## Allowed edit surfaces\nextensions/gentle-ai.ts\n\n## Allowed edit surfaces\n.",
|
|
721
731
|
mode: "task",
|
|
722
732
|
},
|
|
723
733
|
],
|
|
@@ -725,7 +735,7 @@ async function run() {
|
|
|
725
735
|
"valid task scope plus invalid context scope",
|
|
726
736
|
{
|
|
727
737
|
agent: "gentle-ai-worker",
|
|
728
|
-
task: "## Allowed edit surfaces\nextensions/gentle-ai.ts",
|
|
738
|
+
task: "Spec: odd/tasks/harness.md, T5.\n\n## Allowed edit surfaces\nextensions/gentle-ai.ts",
|
|
729
739
|
context: "## Allowed edit surfaces\n.",
|
|
730
740
|
mode: "task",
|
|
731
741
|
},
|
|
@@ -734,7 +744,7 @@ async function run() {
|
|
|
734
744
|
"conflicting valid task and context scopes",
|
|
735
745
|
{
|
|
736
746
|
agent: "gentle-ai-worker",
|
|
737
|
-
task: "## Allowed edit surfaces\nextensions/gentle-ai.ts",
|
|
747
|
+
task: "Spec: odd/tasks/harness.md, T6.\n\n## Allowed edit surfaces\nextensions/gentle-ai.ts",
|
|
738
748
|
context: "## Allowed edit surfaces\ntests/runtime-harness.mjs",
|
|
739
749
|
mode: "task",
|
|
740
750
|
},
|
|
@@ -749,7 +759,7 @@ async function run() {
|
|
|
749
759
|
toolName: "subagent_run",
|
|
750
760
|
input: {
|
|
751
761
|
agent: "gentle-ai-worker",
|
|
752
|
-
task: "## Allowed edit surfaces\nextensions/gentle-ai.ts\ntests/runtime-harness.mjs\n\n## Allowed edit surfaces\n- `tests/runtime-harness.mjs`\n- `extensions/gentle-ai.ts`",
|
|
762
|
+
task: "Spec: odd/tasks/harness.md, T3.\n\n## Allowed edit surfaces\nextensions/gentle-ai.ts\ntests/runtime-harness.mjs\n\n## Allowed edit surfaces\n- `tests/runtime-harness.mjs`\n- `extensions/gentle-ai.ts`",
|
|
753
763
|
mode: "task",
|
|
754
764
|
},
|
|
755
765
|
},
|
|
@@ -2,11 +2,16 @@ import assert from "node:assert/strict";
|
|
|
2
2
|
import test from "node:test";
|
|
3
3
|
import { CustomEditor } from "@earendil-works/pi-coding-agent";
|
|
4
4
|
import { GentlePromptEditor } from "../extensions/gentle-shell.ts";
|
|
5
|
-
import { SelectionEngine } from "../lib/selection-engine.ts";
|
|
5
|
+
import { SelectionEngine, withReverseSpan } from "../lib/selection-engine.ts";
|
|
6
6
|
import { decodePrintableKey } from "../lib/pi-tui-keys.ts";
|
|
7
|
-
import { CURSOR_MARKER, visibleWidth, type TuiMouseEvent } from "@earendil-works/pi-tui";
|
|
7
|
+
import { CURSOR_MARKER, stripTerminalSequences as stripAnsi, TuiMainScreen, visibleWidth, type TuiMouseEvent } from "@earendil-works/pi-tui";
|
|
8
8
|
import { CARD_STYLE, cardStyle, setCardStyle } from "../lib/shell-card.ts";
|
|
9
|
-
import {
|
|
9
|
+
import { withPromptHint } from "../lib/shell-prompt.ts";
|
|
10
|
+
|
|
11
|
+
function resolveFakeCursors(rows: string[], hardware = false): string[] {
|
|
12
|
+
const host = TuiMainScreen.prototype as unknown as { resolveFakeCursors(rows: string[]): string[] };
|
|
13
|
+
return host.resolveFakeCursors.call({ showHardwareCursor: hardware }, rows);
|
|
14
|
+
}
|
|
10
15
|
|
|
11
16
|
// Native selection engine tests: drive a real CustomEditor through the
|
|
12
17
|
// SelectionEngine the same way GentlePromptEditor wires it — engine.handleInput
|
|
@@ -45,6 +50,30 @@ const click = (x: number, y: number, width: number): TuiMouseEvent => ({
|
|
|
45
50
|
type: "click", button: "left", x, y, width, height: 30, screenX: x, screenY: y, shift: false, alt: false, ctrl: false,
|
|
46
51
|
});
|
|
47
52
|
|
|
53
|
+
test("legacy and APC cursors preserve native selection and empty hints", () => {
|
|
54
|
+
const legacy = `${CURSOR_MARKER}\x1b[7ma\x1b[0mbc`;
|
|
55
|
+
assert.equal(withReverseSpan(legacy, 0, 3), `\x1b[7m${CURSOR_MARKER}\x1b[7ma\x1b[0m\x1b[7mbc\x1b[27m`);
|
|
56
|
+
const apc = `${CURSOR_MARKER}\x1b_pi:fc\x07a\x1b_pi:/fc\x07bc`;
|
|
57
|
+
const selected = withReverseSpan(apc, 0, 3);
|
|
58
|
+
assert.ok(selected.includes(`${CURSOR_MARKER}\x1b_pi:fc\x07`));
|
|
59
|
+
for (const hardware of [false, true]) {
|
|
60
|
+
const resolved = resolveFakeCursors([selected], hardware)[0];
|
|
61
|
+
assert.ok(resolved.includes("\x1b[7mbc\x1b[27m"));
|
|
62
|
+
assert.equal(stripAnsi(resolved), "abc");
|
|
63
|
+
}
|
|
64
|
+
// Outside selection the host still suppresses only the focused fake cursor.
|
|
65
|
+
assert.equal(resolveFakeCursors([apc], true)[0], `${CURSOR_MARKER}abc`);
|
|
66
|
+
assert.ok(resolveFakeCursors([apc.replace(CURSOR_MARKER, "")], true)[0].includes("\x1b[7ma\x1b[27m"));
|
|
67
|
+
for (const cursor of ["\x1b[7m \x1b[0m", "\x1b_pi:fc\x07 \x1b_pi:/fc\x07"]) {
|
|
68
|
+
const row = `${CURSOR_MARKER}${cursor}${" ".repeat(30)}`;
|
|
69
|
+
const hinted = withPromptHint(row, "type here", (_role, text) => text);
|
|
70
|
+
assert.ok(hinted.startsWith(`${CURSOR_MARKER}${cursor}`));
|
|
71
|
+
assert.ok(hinted.includes("type here"));
|
|
72
|
+
assert.equal(visibleWidth(hinted), visibleWidth(row));
|
|
73
|
+
assert.equal(withPromptHint(row + "draft", "type here", (_role, text) => text), row + "draft");
|
|
74
|
+
}
|
|
75
|
+
});
|
|
76
|
+
|
|
48
77
|
test("T2 float prompt native cursor, selection, replacement and mouse share geometry", () => {
|
|
49
78
|
const previous = cardStyle();
|
|
50
79
|
setCardStyle(CARD_STYLE.FLOAT);
|
|
@@ -56,11 +85,12 @@ test("T2 float prompt native cursor, selection, replacement and mouse share geom
|
|
|
56
85
|
let rows = editor.render(80);
|
|
57
86
|
assert.match(rows[0], /\x1b\[35m▎\x1b\[39m/);
|
|
58
87
|
assert.equal(visibleWidth(rows[1].split(CURSOR_MARKER)[0]), 6);
|
|
59
|
-
assert.ok(rows[1].includes(`${CURSOR_MARKER}\
|
|
88
|
+
assert.ok(rows[1].includes(`${CURSOR_MARKER}\x1b_pi:fc\x07d\x1b_pi:/fc\x07`));
|
|
89
|
+
assert.ok(resolveFakeCursors(rows)[1].includes(`${CURSOR_MARKER}\x1b[7md\x1b[27m`));
|
|
60
90
|
editor.handleInput("\x1b[1;2H");
|
|
61
91
|
rows = editor.render(80);
|
|
62
92
|
assert.equal(stripAnsi(rows[1].replaceAll(CURSOR_MARKER, "")).trim(), "▎ abcd");
|
|
63
|
-
assert.ok(rows[1].includes(`${CURSOR_MARKER}\
|
|
93
|
+
assert.ok(rows[1].includes(`${CURSOR_MARKER}\x1b_pi:fc\x07a\x1b_pi:/fc\x07\x1b[7mbc\x1b[27md`), "native selection inversion survives the deferred software cursor reset");
|
|
64
94
|
assert.match(rows[2], /3 chars selected/);
|
|
65
95
|
assert.doesNotMatch(rows[2], /╯/);
|
|
66
96
|
assert.ok(rows[2].includes("\x1b[44m"));
|
|
@@ -39,7 +39,7 @@ function fixture(entries: any[] = [], options: { root?: string; sessionId?: stri
|
|
|
39
39
|
for(let i=0;i<200&&!overlay.view;i++){ if(await Promise.race([opened.then(()=>true),new Promise(r=>setTimeout(()=>r(false),5))])) break; }
|
|
40
40
|
if(!overlay.view) return undefined;
|
|
41
41
|
const text=overlay.view.render(200).join("\n");
|
|
42
|
-
const trees=(overlay.view.trees as any[]).map(tree=>({root:tree.root as string,branch:tree.branch as string|undefined,files:tree.model.files.map((file:any)=>file.path) as string[],added:tree.model.added as number,deleted:tree.model.deleted as number}));
|
|
42
|
+
const trees=(overlay.view.children[0].trees as any[]).map(tree=>({root:tree.root as string,branch:tree.branch as string|undefined,files:tree.model.files.map((file:any)=>file.path) as string[],added:tree.model.added as number,deleted:tree.model.deleted as number}));
|
|
43
43
|
overlay.close!(); await opened;
|
|
44
44
|
return {text,trees,files:trees.flatMap(tree=>tree.files)};
|
|
45
45
|
};
|
|
@@ -4,7 +4,7 @@ import { tmpdir } from "node:os";
|
|
|
4
4
|
import { join } from "node:path";
|
|
5
5
|
import test, { after, beforeEach } from "node:test";
|
|
6
6
|
import { createProfile, emptyProfilesFile, profilesFilePath, setActiveProfile, writeProfilesFileSync } from "../lib/agent-profiles.ts";
|
|
7
|
-
import { clearProfilePinSync, localProfilePinPath, repoProfileDeclarationPath, writeProfilePinSync } from "../lib/agent-profile-pin.ts";
|
|
7
|
+
import { clearProfilePinSync, localProfilePinPath, repoProfileDeclarationPath, setProfilePinWorktreeResolverForTesting, writeProfilePinSync } from "../lib/agent-profile-pin.ts";
|
|
8
8
|
import type { AgentModelConfig } from "../lib/model-routing-authority.ts";
|
|
9
9
|
import { bindSessionProfile, resetSessionProfileBindingsForTesting } from "../lib/session-profile-binding.ts";
|
|
10
10
|
import {
|
|
@@ -120,6 +120,19 @@ test("resolveInheritedProfile falls to the global active profile in profiles.jso
|
|
|
120
120
|
});
|
|
121
121
|
});
|
|
122
122
|
|
|
123
|
+
test("resolveInheritedProfile asks the default worktree resolver once when no resolver is passed", (t) => {
|
|
124
|
+
const f = fixture();
|
|
125
|
+
f.setActive("local");
|
|
126
|
+
let lookups = 0;
|
|
127
|
+
setProfilePinWorktreeResolverForTesting(() => { lookups++; return f.identity; });
|
|
128
|
+
t.after(() => setProfilePinWorktreeResolverForTesting());
|
|
129
|
+
assert.equal(resolveInheritedProfile({ cwd: f.cwd, configHome: f.configHome })?.name, "local");
|
|
130
|
+
assert.equal(lookups, 1, "the no-pin path reuses one worktree lookup");
|
|
131
|
+
f.pinLocal("frontier");
|
|
132
|
+
assert.equal(resolveInheritedProfile({ cwd: f.cwd, configHome: f.configHome })?.origin, "local");
|
|
133
|
+
assert.equal(lookups, 2);
|
|
134
|
+
});
|
|
135
|
+
|
|
123
136
|
test("resolveInheritedProfile returns undefined when no layer has a profile", () => {
|
|
124
137
|
const f = fixture();
|
|
125
138
|
assert.equal(resolveInheritedProfile({ cwd: f.cwd, configHome: f.configHome, resolveWorktree: f.resolveWorktree }), undefined);
|
|
@@ -282,6 +295,29 @@ test("drift notice: shown once per distinct change of the defaults", () => {
|
|
|
282
295
|
assert.equal(readFrozenInheritedProfile("session-a")?.profile?.name, "frontier", "the notice never adopts the new default");
|
|
283
296
|
});
|
|
284
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
|
+
|
|
285
321
|
test("drift notice: an edit of the default profile's content counts as a change", () => {
|
|
286
322
|
const f = fixture();
|
|
287
323
|
f.setActive("frontier");
|
|
@@ -0,0 +1,334 @@
|
|
|
1
|
+
import assert from "node:assert/strict";
|
|
2
|
+
import test from "node:test";
|
|
3
|
+
import {
|
|
4
|
+
SESSION_PROFILE_CUSTOM_TYPE,
|
|
5
|
+
createSessionProfileBind,
|
|
6
|
+
createSessionProfileClear,
|
|
7
|
+
readSessionProfileEntry,
|
|
8
|
+
replaySessionProfileBranch,
|
|
9
|
+
} from "../lib/session-profile-persistence.ts";
|
|
10
|
+
|
|
11
|
+
import { normalizeModelConfig } from "../lib/model-routing-authority.ts";
|
|
12
|
+
import {
|
|
13
|
+
normalizeProfilesFile,
|
|
14
|
+
PROFILES_KIND,
|
|
15
|
+
PROFILES_VERSION,
|
|
16
|
+
} from "../lib/agent-profiles.ts";
|
|
17
|
+
|
|
18
|
+
const custom = (data: unknown, customType = SESSION_PROFILE_CUSTOM_TYPE) => ({
|
|
19
|
+
type: "custom",
|
|
20
|
+
customType,
|
|
21
|
+
data,
|
|
22
|
+
});
|
|
23
|
+
const bind = (modelProfiles: unknown = {}) => ({
|
|
24
|
+
kind: "bind",
|
|
25
|
+
origin: "user",
|
|
26
|
+
name: "work",
|
|
27
|
+
modelProfiles,
|
|
28
|
+
});
|
|
29
|
+
|
|
30
|
+
// These are supplied branch entries, not evidence of persistence on disk.
|
|
31
|
+
test("writer emits only the v1 user binding fields and a minimal clear", () => {
|
|
32
|
+
assert.equal(SESSION_PROFILE_CUSTOM_TYPE, "gentle-pi.session-profile/v1");
|
|
33
|
+
assert.deepEqual(createSessionProfileBind("work", {}), bind());
|
|
34
|
+
assert.deepEqual(createSessionProfileClear(), { kind: "clear" });
|
|
35
|
+
});
|
|
36
|
+
|
|
37
|
+
test("writer accepts real normalized optional fields without persisting undefined", () => {
|
|
38
|
+
const raw = {
|
|
39
|
+
worker: "provider/model",
|
|
40
|
+
reviewer: { effort: "high" },
|
|
41
|
+
orchestrator: { model: "provider/main" },
|
|
42
|
+
inheriting: {},
|
|
43
|
+
};
|
|
44
|
+
const routes = normalizeModelConfig(raw);
|
|
45
|
+
const profiles = normalizeProfilesFile({
|
|
46
|
+
kind: PROFILES_KIND,
|
|
47
|
+
version: PROFILES_VERSION,
|
|
48
|
+
profiles: { work: raw },
|
|
49
|
+
});
|
|
50
|
+
assert.ok(routes);
|
|
51
|
+
assert.ok(profiles);
|
|
52
|
+
assert.equal(Object.hasOwn(routes.reviewer, "model"), true);
|
|
53
|
+
assert.equal(Object.hasOwn(routes.reviewer, "thinking"), true);
|
|
54
|
+
const expected = bind({
|
|
55
|
+
worker: { model: "provider/model" },
|
|
56
|
+
reviewer: { thinking: "high" },
|
|
57
|
+
orchestrator: { model: "provider/main" },
|
|
58
|
+
inheriting: {},
|
|
59
|
+
});
|
|
60
|
+
for (const config of [routes, profiles.file.profiles.work]) {
|
|
61
|
+
const payload = createSessionProfileBind("work", config);
|
|
62
|
+
assert.deepEqual(payload, expected);
|
|
63
|
+
assert.deepEqual(readSessionProfileEntry(custom(payload)), {
|
|
64
|
+
status: "bound",
|
|
65
|
+
binding: expected,
|
|
66
|
+
});
|
|
67
|
+
assert.notEqual(payload.modelProfiles.reviewer, config.reviewer);
|
|
68
|
+
}
|
|
69
|
+
assert.equal(Object.hasOwn(routes.reviewer, "model"), true);
|
|
70
|
+
});
|
|
71
|
+
|
|
72
|
+
test("decoder stays strict about present undefined known fields", () => {
|
|
73
|
+
for (const route of [
|
|
74
|
+
{ model: undefined },
|
|
75
|
+
{ thinking: undefined },
|
|
76
|
+
{ effort: undefined },
|
|
77
|
+
]) {
|
|
78
|
+
assert.deepEqual(readSessionProfileEntry(custom(bind({ worker: route }))), {
|
|
79
|
+
status: "invalid",
|
|
80
|
+
});
|
|
81
|
+
}
|
|
82
|
+
});
|
|
83
|
+
|
|
84
|
+
test("bound empty snapshot is distinct from absent and cleared", () => {
|
|
85
|
+
assert.deepEqual(readSessionProfileEntry(custom(bind())), {
|
|
86
|
+
status: "bound",
|
|
87
|
+
binding: bind(),
|
|
88
|
+
});
|
|
89
|
+
assert.deepEqual(replaySessionProfileBranch([]), { status: "absent" });
|
|
90
|
+
assert.deepEqual(readSessionProfileEntry(custom({ kind: "clear" })), {
|
|
91
|
+
status: "cleared",
|
|
92
|
+
});
|
|
93
|
+
});
|
|
94
|
+
|
|
95
|
+
test("reader accepts all four origins and ignores unknown payload fields", () => {
|
|
96
|
+
for (const origin of ["user", "local", "repo", "global"]) {
|
|
97
|
+
assert.deepEqual(
|
|
98
|
+
readSessionProfileEntry(
|
|
99
|
+
custom({ ...bind(), origin, extra: true, version: 99 }),
|
|
100
|
+
),
|
|
101
|
+
{
|
|
102
|
+
status: "bound",
|
|
103
|
+
binding: { ...bind(), origin },
|
|
104
|
+
},
|
|
105
|
+
);
|
|
106
|
+
}
|
|
107
|
+
assert.deepEqual(
|
|
108
|
+
readSessionProfileEntry(custom({ kind: "clear", extra: true })),
|
|
109
|
+
{ status: "cleared" },
|
|
110
|
+
);
|
|
111
|
+
});
|
|
112
|
+
|
|
113
|
+
test("legacy strings and effort normalize while orchestrator remains in the snapshot", () => {
|
|
114
|
+
const result = readSessionProfileEntry(
|
|
115
|
+
custom(
|
|
116
|
+
bind({
|
|
117
|
+
worker: " provider/model ",
|
|
118
|
+
reviewer: { effort: "high", ignored: true },
|
|
119
|
+
orchestrator: { model: "provider/main", thinking: "low", effort: "max" },
|
|
120
|
+
inheriting: {},
|
|
121
|
+
}),
|
|
122
|
+
),
|
|
123
|
+
);
|
|
124
|
+
assert.deepEqual(result, {
|
|
125
|
+
status: "bound",
|
|
126
|
+
binding: bind({
|
|
127
|
+
worker: { model: "provider/model" },
|
|
128
|
+
reviewer: { thinking: "high" },
|
|
129
|
+
orchestrator: { model: "provider/main", thinking: "low" },
|
|
130
|
+
inheriting: {},
|
|
131
|
+
}),
|
|
132
|
+
});
|
|
133
|
+
});
|
|
134
|
+
|
|
135
|
+
test("missing required fields and invalid known meanings fail closed", () => {
|
|
136
|
+
const invalid: unknown[] = [
|
|
137
|
+
null,
|
|
138
|
+
[],
|
|
139
|
+
{},
|
|
140
|
+
{ kind: "other" },
|
|
141
|
+
{ kind: "bind", name: "work", modelProfiles: {} },
|
|
142
|
+
{ ...bind(), origin: "future" },
|
|
143
|
+
{ ...bind(), name: "../work" },
|
|
144
|
+
{ ...bind(), name: "constructor" },
|
|
145
|
+
{ ...bind(), modelProfiles: undefined },
|
|
146
|
+
{ ...bind(), modelProfiles: [] },
|
|
147
|
+
];
|
|
148
|
+
for (const data of invalid) {
|
|
149
|
+
assert.deepEqual(readSessionProfileEntry(custom(data)), {
|
|
150
|
+
status: "invalid",
|
|
151
|
+
});
|
|
152
|
+
}
|
|
153
|
+
});
|
|
154
|
+
|
|
155
|
+
test("one invalid route invalidates the whole snapshot instead of silently dropping it", () => {
|
|
156
|
+
const routes: unknown[] = [
|
|
157
|
+
null,
|
|
158
|
+
[],
|
|
159
|
+
3,
|
|
160
|
+
"bad model",
|
|
161
|
+
{ model: "bad model", thinking: "low" },
|
|
162
|
+
{ model: "provider/good", thinking: "extreme" },
|
|
163
|
+
{ effort: "extreme" },
|
|
164
|
+
{ thinking: "low", effort: "extreme" },
|
|
165
|
+
{ model: null },
|
|
166
|
+
];
|
|
167
|
+
for (const route of routes) {
|
|
168
|
+
assert.deepEqual(readSessionProfileEntry(custom(bind({ worker: route }))), {
|
|
169
|
+
status: "invalid",
|
|
170
|
+
});
|
|
171
|
+
assert.deepEqual(
|
|
172
|
+
readSessionProfileEntry(
|
|
173
|
+
custom(bind({ good: "provider/good", worker: route })),
|
|
174
|
+
),
|
|
175
|
+
{ status: "invalid" },
|
|
176
|
+
);
|
|
177
|
+
}
|
|
178
|
+
assert.deepEqual(readSessionProfileEntry(custom(bind({ "bad role": {} }))), {
|
|
179
|
+
status: "invalid",
|
|
180
|
+
});
|
|
181
|
+
});
|
|
182
|
+
|
|
183
|
+
test("unknown route fields are ignored, including an unknown-only route", () => {
|
|
184
|
+
assert.deepEqual(
|
|
185
|
+
readSessionProfileEntry(custom(bind({ worker: { future: true } }))),
|
|
186
|
+
{
|
|
187
|
+
status: "bound",
|
|
188
|
+
binding: bind({ worker: {} }),
|
|
189
|
+
},
|
|
190
|
+
);
|
|
191
|
+
});
|
|
192
|
+
|
|
193
|
+
test("hostile record keys remain own data without changing prototypes", () => {
|
|
194
|
+
const routes = JSON.parse(
|
|
195
|
+
'{"__proto__":{"model":"provider/safe"},"constructor":{},"prototype":{}}',
|
|
196
|
+
);
|
|
197
|
+
const result = readSessionProfileEntry(custom(bind(routes)));
|
|
198
|
+
assert.equal(result.status, "bound");
|
|
199
|
+
if (result.status !== "bound") return;
|
|
200
|
+
assert.equal(
|
|
201
|
+
Object.getPrototypeOf(result.binding.modelProfiles),
|
|
202
|
+
Object.prototype,
|
|
203
|
+
);
|
|
204
|
+
assert.equal(Object.hasOwn(result.binding.modelProfiles, "__proto__"), true);
|
|
205
|
+
assert.deepEqual(result.binding.modelProfiles["__proto__"], {
|
|
206
|
+
model: "provider/safe",
|
|
207
|
+
});
|
|
208
|
+
for (const key of ["constructor", "prototype"]) {
|
|
209
|
+
assert.equal(Object.hasOwn(result.binding.modelProfiles, key), true);
|
|
210
|
+
assert.deepEqual(result.binding.modelProfiles[key], {});
|
|
211
|
+
}
|
|
212
|
+
});
|
|
213
|
+
|
|
214
|
+
test("required fields cannot be supplied by prototypes", () => {
|
|
215
|
+
assert.deepEqual(readSessionProfileEntry(custom(Object.create(bind()))), {
|
|
216
|
+
status: "invalid",
|
|
217
|
+
});
|
|
218
|
+
});
|
|
219
|
+
|
|
220
|
+
test("writer rejects invalid input rather than manufacturing an empty snapshot", () => {
|
|
221
|
+
assert.throws(() => createSessionProfileBind("../bad", {}));
|
|
222
|
+
assert.throws(() =>
|
|
223
|
+
createSessionProfileBind("work", { worker: { model: "bad model" } }),
|
|
224
|
+
);
|
|
225
|
+
for (const route of [
|
|
226
|
+
{ model: null },
|
|
227
|
+
{ model: "bad model", thinking: undefined },
|
|
228
|
+
{ model: undefined, thinking: "extreme" },
|
|
229
|
+
{ effort: undefined },
|
|
230
|
+
]) {
|
|
231
|
+
assert.throws(() =>
|
|
232
|
+
createSessionProfileBind("work", {
|
|
233
|
+
good: { model: "provider/good" },
|
|
234
|
+
worker: route,
|
|
235
|
+
} as Parameters<typeof createSessionProfileBind>[1]),
|
|
236
|
+
);
|
|
237
|
+
}
|
|
238
|
+
});
|
|
239
|
+
|
|
240
|
+
test("writer and reader return defensive snapshots", () => {
|
|
241
|
+
const source = { worker: { model: "provider/first" } };
|
|
242
|
+
const payload = createSessionProfileBind("work", source);
|
|
243
|
+
source.worker.model = "provider/second";
|
|
244
|
+
assert.equal(payload.modelProfiles.worker.model, "provider/first");
|
|
245
|
+
const entry = custom(payload);
|
|
246
|
+
const first = readSessionProfileEntry(entry);
|
|
247
|
+
assert.equal(first.status, "bound");
|
|
248
|
+
if (first.status !== "bound") return;
|
|
249
|
+
first.binding.modelProfiles.worker.model = "provider/third";
|
|
250
|
+
const second = readSessionProfileEntry(entry);
|
|
251
|
+
assert.equal(second.status, "bound");
|
|
252
|
+
if (second.status === "bound")
|
|
253
|
+
assert.equal(second.binding.modelProfiles.worker.model, "provider/first");
|
|
254
|
+
});
|
|
255
|
+
|
|
256
|
+
test("noncustom and unrelated custom entries are absent even with matching payloads", () => {
|
|
257
|
+
for (const entry of [
|
|
258
|
+
{ type: "message", customType: SESSION_PROFILE_CUSTOM_TYPE, data: bind() },
|
|
259
|
+
{ type: "message", message: { role: "user", content: "hello" } },
|
|
260
|
+
custom(bind(), "other/v1"),
|
|
261
|
+
])
|
|
262
|
+
assert.deepEqual(readSessionProfileEntry(entry), { status: "absent" });
|
|
263
|
+
});
|
|
264
|
+
|
|
265
|
+
test("newest family entry is terminal and retains its actual branch index", () => {
|
|
266
|
+
const old = custom(bind({ worker: "provider/old" }));
|
|
267
|
+
const message = {
|
|
268
|
+
type: "message",
|
|
269
|
+
message: { role: "user", content: "hello" },
|
|
270
|
+
};
|
|
271
|
+
for (const [entry, status] of [
|
|
272
|
+
[custom({ kind: "clear" }), "cleared"],
|
|
273
|
+
[custom({ kind: "bind" }), "invalid"],
|
|
274
|
+
[custom(bind(), "gentle-pi.session-profile/v2"), "unsupported"],
|
|
275
|
+
] as const) {
|
|
276
|
+
assert.deepEqual(
|
|
277
|
+
replaySessionProfileBranch([old, message, entry, custom({}, "other/v1")]),
|
|
278
|
+
{
|
|
279
|
+
status,
|
|
280
|
+
entryIndex: 2,
|
|
281
|
+
},
|
|
282
|
+
);
|
|
283
|
+
}
|
|
284
|
+
});
|
|
285
|
+
|
|
286
|
+
test("family identifier, not payload version, determines support", () => {
|
|
287
|
+
for (const customType of [
|
|
288
|
+
"gentle-pi.session-profile/v0",
|
|
289
|
+
"gentle-pi.session-profile/v99",
|
|
290
|
+
"gentle-pi.session-profile/future",
|
|
291
|
+
]) {
|
|
292
|
+
assert.deepEqual(readSessionProfileEntry(custom(null, customType)), {
|
|
293
|
+
status: "unsupported",
|
|
294
|
+
});
|
|
295
|
+
}
|
|
296
|
+
assert.deepEqual(
|
|
297
|
+
readSessionProfileEntry(custom(bind(), "gentle-pi.session-profile-other/v1")),
|
|
298
|
+
{ status: "absent" },
|
|
299
|
+
);
|
|
300
|
+
});
|
|
301
|
+
|
|
302
|
+
test("branch order selects latest bind; clear and bad entries can be explicitly superseded", () => {
|
|
303
|
+
const latest = custom(bind({ worker: "provider/new" }));
|
|
304
|
+
for (const earlier of [
|
|
305
|
+
custom({ kind: "clear" }),
|
|
306
|
+
custom(null),
|
|
307
|
+
custom(null, "gentle-pi.session-profile/v2"),
|
|
308
|
+
]) {
|
|
309
|
+
assert.deepEqual(replaySessionProfileBranch([earlier, latest]), {
|
|
310
|
+
status: "bound",
|
|
311
|
+
entryIndex: 1,
|
|
312
|
+
binding: bind({ worker: { model: "provider/new" } }),
|
|
313
|
+
});
|
|
314
|
+
}
|
|
315
|
+
});
|
|
316
|
+
|
|
317
|
+
test("replay sees only caller-supplied active branch, not siblings or later paths", () => {
|
|
318
|
+
const ancestor = custom(bind());
|
|
319
|
+
const sibling = custom({ kind: "clear" });
|
|
320
|
+
const future = custom(null, "gentle-pi.session-profile/v2");
|
|
321
|
+
assert.equal(
|
|
322
|
+
replaySessionProfileBranch([ancestor, sibling]).status,
|
|
323
|
+
"cleared",
|
|
324
|
+
);
|
|
325
|
+
assert.equal(
|
|
326
|
+
replaySessionProfileBranch([ancestor, future]).status,
|
|
327
|
+
"unsupported",
|
|
328
|
+
);
|
|
329
|
+
assert.deepEqual(replaySessionProfileBranch([ancestor, { type: "message" }]), {
|
|
330
|
+
status: "bound",
|
|
331
|
+
entryIndex: 0,
|
|
332
|
+
binding: bind(),
|
|
333
|
+
});
|
|
334
|
+
});
|