gentle-pi 3.7.0 → 4.0.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 +37 -6
- package/assets/agents/gentle-ai-explore.md +4 -4
- package/assets/agents/gentle-ai-verify.md +6 -4
- package/assets/agents/gentle-ai-worker.md +7 -9
- package/assets/orchestrator-delegation.md +40 -41
- package/assets/orchestrator-memory.md +1 -22
- package/assets/orchestrator-skills.md +1 -1
- package/assets/orchestrator.md +12 -24
- package/assets/support/strict-tdd-verify.md +4 -266
- package/assets/support/strict-tdd.md +8 -360
- package/bin/gentle-shell.mjs +254 -29
- package/docs/delegated-verification.md +26 -1
- package/docs/gentle-agents-activity.md +24 -0
- package/docs/gentle-shell.md +92 -28
- package/docs/native-authority-architecture.md +2 -2
- package/docs/prompt-history.md +280 -0
- package/docs/readme-reference.md +112 -206
- package/docs/telemetry.md +1 -1
- package/docs/yolo-mode.md +86 -0
- package/extensions/child-context.ts +26 -0
- package/extensions/child-safety.ts +23 -0
- package/extensions/gentle-agents.ts +429 -262
- package/extensions/gentle-ai.ts +781 -683
- package/extensions/gentle-shell.ts +1375 -88
- package/extensions/gentle-stats.ts +101 -0
- package/extensions/gentle-todo.ts +18 -7
- package/extensions/history/atomic-write.ts +38 -0
- package/extensions/history/hide-prompts.ts +183 -0
- package/extensions/history/index.ts +1419 -0
- package/extensions/history/load-shared-history.ts +39 -0
- package/extensions/history/selector-helpers.ts +538 -0
- package/extensions/history/session-scan.ts +233 -0
- package/extensions/history/store.ts +1119 -0
- package/extensions/nan-provider.ts +6 -0
- package/extensions/quiet-tools.ts +179 -87
- package/extensions/resume-hint.ts +60 -0
- package/extensions/skill-registry.ts +16 -12
- package/extensions/startup-banner.ts +60 -31
- package/lib/agent-assets.ts +604 -0
- package/lib/agent-profile-pin.ts +12 -0
- package/lib/agents-message-delivery.ts +181 -0
- package/lib/agents-protocol.ts +60 -0
- package/lib/agents-runner.ts +96 -98
- package/lib/agents-view.ts +26 -3
- package/lib/agents-widget.ts +16 -9
- package/lib/append-system-prompt.ts +21 -0
- package/lib/bounded-writer-admission.ts +147 -0
- package/lib/card-style-policy.ts +60 -0
- package/lib/child-context-files.ts +166 -0
- package/lib/codemode-renderer.ts +185 -0
- package/lib/command-palette-catalog.ts +3 -9
- package/lib/command-palette.ts +25 -14
- package/lib/destructive-command-guard.ts +144 -0
- package/lib/gentle-ai-elapsed-store.ts +87 -0
- package/lib/gentle-ai-renderer.ts +196 -39
- package/lib/gentle-shell-launcher.ts +24 -13
- package/lib/gentle-shell-resume-hint.ts +176 -0
- package/lib/history-capture-policy.ts +95 -0
- package/lib/model-routing-authority.ts +5 -1
- package/lib/nan-provider.ts +227 -0
- package/lib/native-review-cli.ts +49 -108
- package/lib/odd-phase-inference.ts +231 -0
- package/lib/odd-phase.ts +141 -0
- package/lib/overlay-repaint.ts +26 -0
- package/lib/pi-tui-keys.ts +53 -0
- package/lib/review-candidate-view-owner.ts +67 -17
- package/lib/review-candidate-view.ts +112 -25
- package/lib/review-reminder-receipt.ts +48 -8
- package/lib/review-risk-assessment.ts +156 -11
- package/lib/review-sidebar-state.ts +223 -0
- package/lib/selection-engine.ts +515 -0
- package/lib/session-messaging-grants.ts +135 -0
- package/lib/session-worktree-registry.ts +14 -2
- package/lib/shell-bar.ts +179 -24
- package/lib/shell-card.ts +287 -18
- package/lib/shell-changes-view.ts +2 -1
- package/lib/shell-prompt.ts +102 -6
- package/lib/shell-sidebar-layout.ts +78 -14
- package/lib/shell-sidebar.ts +15 -1
- package/lib/shell-todo.ts +23 -13
- package/lib/shell-usage-view.ts +9 -4
- package/lib/shell-usage.ts +66 -10
- package/lib/stats-collector.ts +381 -0
- package/lib/stats-view.ts +431 -0
- package/lib/theme-customization.ts +52 -0
- package/lib/vim-editor-adapter.ts +379 -0
- package/lib/vim-normal-engine.ts +154 -0
- package/lib/vim-operator-engine.ts +416 -0
- package/lib/vim-policy.ts +49 -0
- package/lib/vim-visual-engine.ts +107 -0
- package/lib/visual-customization-policy.ts +108 -0
- package/lib/visual-customize-view.ts +330 -0
- package/lib/visual-profiles.ts +228 -0
- package/lib/yolo-session-policy.ts +240 -0
- package/package.json +20 -8
- package/runtime/gentle-shell-launcher.mjs +23 -12
- package/runtime/gentle-shell-resume-hint.mjs +177 -0
- package/runtime/native-review-cli.mjs +49 -108
- package/runtime/review-risk-assessment.mjs +154 -9
- package/scripts/build-runtime-modules.mjs +1 -0
- package/scripts/gentle-ai-installer.mjs +14 -13
- package/scripts/mirror-odd-routing.mjs +2 -2
- package/scripts/run-test-suite.mjs +76 -0
- package/scripts/test-packed-runner.mjs +31 -14
- package/scripts/verify-package-files.mjs +11 -22
- package/skills/branch-pr/SKILL.md +24 -52
- package/skills/chained-pr/SKILL.md +31 -15
- package/skills/chained-pr/references/chaining-details.md +31 -20
- package/skills/gentle-ai/SKILL.md +9 -15
- package/skills/issue-creation/SKILL.md +8 -2
- package/skills/issue-creation/references/delegated-workflow-actions.md +19 -0
- package/skills/work-unit-commits/SKILL.md +4 -3
- package/tests/agent-profiles.test.ts +18 -0
- package/tests/agents-fake-child.ts +2 -2
- package/tests/agents-message-delivery.test.ts +106 -0
- package/tests/agents-protocol.test.ts +40 -0
- package/tests/agents-runner.test.ts +378 -89
- package/tests/agents-view-thread-identity.test.ts +169 -0
- package/tests/agents-view.test.ts +8 -2
- package/tests/agents-widget.test.ts +154 -15
- package/tests/append-system-prompt-route.test.ts +160 -0
- package/tests/append-system-prompt.test.ts +46 -0
- package/tests/artifact-language.test.ts +19 -213
- package/tests/ask-user-question.test.ts +44 -1
- package/tests/asset-installation-runtime.test.ts +5 -16
- package/tests/autonomous-guard.test.ts +69 -1
- package/tests/bounded-writer-admission.test.ts +95 -0
- package/tests/branch-pr-skill.test.ts +43 -0
- package/tests/card-style-policy.test.ts +55 -0
- package/tests/chained-pr-skill.test.ts +124 -0
- package/tests/child-context-files.test.ts +255 -0
- package/tests/child-safety.test.ts +82 -0
- package/tests/codemode-rendering.test.ts +491 -0
- package/tests/command-palette.test.ts +39 -3
- package/tests/delegated-key-learnings-contract.test.ts +0 -76
- package/tests/destructive-command-guard.test.ts +84 -0
- package/tests/devbinary/native-review-parity.devtest.ts +170 -2
- package/tests/devbinary/non-git-subagent-bootstrap.devtest.ts +193 -0
- package/tests/fixtures/stats/sessions/--work-alpha--/2026-09-28T10-00-00-000Z_aaa.jsonl +7 -0
- package/tests/fixtures/stats/sessions/--work-alpha--/2026-09-29T23-00-00-000Z_bbb.jsonl +3 -0
- package/tests/fixtures/stats/sessions/--work-alpha--/2026-09-30T08-00-00-000Z_ddd.jsonl +2 -0
- package/tests/fixtures/stats/sessions/--work-alpha--/2026-09-30T09-00-00-000Z_eee.jsonl +3 -0
- package/tests/fixtures/stats/sessions/--work-alpha--/run-1/session.jsonl +2 -0
- package/tests/fixtures/stats/sessions/--work-beta--/2026-09-01T12-00-00-000Z_ccc.jsonl +2 -0
- package/tests/fixtures/stats/user-pi/sessions/--work-alpha--/2026-09-28T10-00-00-000Z_aaa.jsonl +2 -0
- package/tests/fixtures/stats/user-pi/sessions/--work-alpha--/2026-09-29T23-00-00-000Z_bbb.jsonl +4 -0
- package/tests/fixtures/stats/user-pi/sessions/--work-gamma--/2026-09-20T09-00-00-000Z_fff.jsonl +3 -0
- package/tests/generic-agent-tools.test.ts +54 -0
- package/tests/gentle-agents.test.ts +1047 -252
- package/tests/gentle-ai-binary.test.ts +3 -3
- package/tests/gentle-ai-elapsed-store.test.ts +68 -0
- package/tests/gentle-ai-installer.test.ts +68 -54
- package/tests/gentle-ai-renderer.test.ts +487 -8
- package/tests/gentle-ai.test.ts +288 -65
- package/tests/gentle-card-text.ts +2 -1
- package/tests/gentle-shell-bin.test.ts +651 -114
- package/tests/gentle-shell-launcher.test.ts +99 -52
- package/tests/gentle-shell-resume-hint.test.ts +270 -0
- package/tests/gentle-shell.test.ts +3839 -187
- package/tests/gentle-stats.test.ts +152 -0
- package/tests/gentle-theme.test.ts +4 -1
- package/tests/gentle-todo.test.ts +80 -8
- package/tests/history-atomic-write.test.ts +57 -0
- package/tests/history-capture-policy.test.ts +102 -0
- package/tests/history-command-registration.test.ts +164 -0
- package/tests/history-dedupe-entries.test.ts +123 -0
- package/tests/history-delete-backfill.test.ts +190 -0
- package/tests/history-delete-confirm.test.ts +460 -0
- package/tests/history-dispatch.test.ts +180 -0
- package/tests/history-drain-hidden.test.ts +110 -0
- package/tests/history-drain-order.test.ts +98 -0
- package/tests/history-expanded-globals.test.ts +62 -0
- package/tests/history-gc.test.ts +832 -0
- package/tests/history-header-layout.test.ts +265 -0
- package/tests/history-hide-prompts.test.ts +275 -0
- package/tests/history-lazy-windowing.test.ts +508 -0
- package/tests/history-legacy-migrate-v2.test.ts +297 -0
- package/tests/history-load-shared-history.test.ts +53 -0
- package/tests/history-max-results-cap.test.ts +76 -0
- package/tests/history-multi-reader.test.ts +203 -0
- package/tests/history-off-path.test.ts +170 -0
- package/tests/history-openflow-integration.test.ts +173 -0
- package/tests/history-overlay-margin.test.ts +326 -0
- package/tests/history-preview-layout.test.ts +93 -0
- package/tests/history-registry.test.ts +143 -0
- package/tests/history-scope-delete.test.ts +411 -0
- package/tests/history-search-caret-keys.test.ts +142 -0
- package/tests/history-seed-bootstrap.test.ts +170 -0
- package/tests/history-seed-regen.test.ts +129 -0
- package/tests/history-selector-windowing.test.ts +94 -0
- package/tests/history-session-scan-directory.test.ts +87 -0
- package/tests/history-session-scan-extract.test.ts +583 -0
- package/tests/history-session-writer.test.ts +351 -0
- package/tests/history-store-paths.test.ts +79 -0
- package/tests/history-tombstone-exact.test.ts +139 -0
- package/tests/history-wheel-mouse.test.ts +242 -0
- package/tests/inprocess-reviewer.test.ts +29 -19
- package/tests/issue-creation-skill.test.ts +61 -0
- package/tests/model-routing-authority.test.ts +16 -0
- package/tests/nan-provider.test.ts +471 -0
- package/tests/native-review-capability-contract.test.ts +7 -1
- package/tests/native-review-cli.test.ts +6 -120
- package/tests/native-review-parity-runtime.test.ts +100 -3
- package/tests/odd-integration.test.ts +67 -0
- package/tests/odd-phase-inference.test.ts +213 -0
- package/tests/odd-phase-loader.test.ts +253 -0
- package/tests/odd-phase.test.ts +307 -0
- package/tests/odd-routing-canonical-ratchet.test.ts +11 -6
- package/tests/odd-routing-contract.test.ts +86 -35
- package/tests/orchestrator-budget.test.ts +14 -39
- package/tests/orchestrator-rdd-ownership.test.ts +3 -3
- package/tests/overlay-repaint.test.ts +74 -0
- package/tests/package-manifest.test.ts +252 -115
- package/tests/packed-runner-owned-path.test.ts +46 -0
- package/tests/persona-single-channel.test.ts +6 -6
- package/tests/provider-defect-handoff.test.ts +3 -11
- package/tests/quiet-bash-runtime.test.ts +76 -0
- package/tests/quiet-tool-rendering.test.ts +409 -184
- package/tests/rdd-aware-verification-contract.test.ts +76 -1
- package/tests/rdd-status-line.test.ts +9 -4
- package/tests/resume-hint-extension.test.ts +122 -0
- package/tests/review-agent-end-preflight.test.ts +176 -12
- package/tests/review-candidate-owner-retry.test.ts +22 -1
- package/tests/review-candidate-view.test.ts +298 -0
- package/tests/review-contract-prompt.test.ts +108 -43
- package/tests/review-controller-lock-status.test.ts +0 -1
- package/tests/review-controller-native-routing.test.ts +611 -5
- package/tests/review-controller-workspace-root.test.ts +163 -4
- package/tests/review-host-relay-routing.test.ts +338 -2
- package/tests/review-integration-v2-forward.test.ts +200 -0
- package/tests/review-ledger-contract.test.ts +10 -34
- package/tests/review-reminder-receipt.test.ts +47 -1
- package/tests/review-risk-assessment.test.ts +498 -6
- package/tests/review-sidebar-state.test.ts +402 -0
- package/tests/run-test-suite.test.ts +124 -0
- package/tests/runtime-harness.mjs +145 -786
- package/tests/runtime-metrics-children.test.ts +16 -23
- package/tests/selection-engine.test.ts +421 -0
- package/tests/session-messaging-grants.test.ts +255 -0
- package/tests/session-worktree-registry.test.ts +77 -0
- package/tests/shell-bar.test.ts +382 -1
- package/tests/shell-card.test.ts +353 -1
- package/tests/shell-changes-view.test.ts +52 -0
- package/tests/shell-prompt.test.ts +94 -2
- package/tests/shell-sidebar-layout.test.ts +325 -21
- package/tests/shell-sidebar-scroll-benchmark.test.ts +255 -0
- package/tests/shell-todo.test.ts +87 -1
- package/tests/shell-usage-view.test.ts +27 -0
- package/tests/shell-usage.test.ts +73 -0
- package/tests/skill-registry.test.ts +50 -1
- package/tests/startup-banner.test.ts +130 -2
- package/tests/stats-collector.test.ts +195 -0
- package/tests/stats-view.test.ts +202 -0
- package/tests/telemetry-trigger.test.ts +81 -20
- package/tests/theme-customization.test.ts +72 -0
- package/tests/vim-editor-adapter-host-resolution.test.ts +37 -0
- package/tests/vim-editor-adapter.test.ts +804 -0
- package/tests/vim-normal-engine.test.ts +101 -0
- package/tests/vim-operator-engine.test.ts +215 -0
- package/tests/vim-policy.test.ts +19 -0
- package/tests/vim-visual-engine.test.ts +52 -0
- package/tests/visual-customization-policy.test.ts +110 -0
- package/tests/visual-customize-view.test.ts +418 -0
- package/tests/visual-profiles.test.ts +87 -0
- package/tests/yolo-customize.test.ts +256 -0
- package/tests/yolo-mode-runtime.test.ts +161 -0
- package/tests/yolo-mode.test.ts +261 -0
- package/tests/yolo-session-policy.test.ts +59 -0
- package/themes/Gentle.json +2 -1
- package/themes/Gentleman-Cute.json +2 -1
- package/themes/Gentleman-Sexy.json +2 -1
- package/assets/agents/sdd-apply.md +0 -159
- package/assets/agents/sdd-archive.md +0 -228
- package/assets/agents/sdd-design.md +0 -49
- package/assets/agents/sdd-explore.md +0 -48
- package/assets/agents/sdd-init.md +0 -56
- package/assets/agents/sdd-onboard.md +0 -52
- package/assets/agents/sdd-proposal.md +0 -64
- package/assets/agents/sdd-remediate.md +0 -37
- package/assets/agents/sdd-research.md +0 -49
- package/assets/agents/sdd-spec.md +0 -192
- package/assets/agents/sdd-status.md +0 -54
- package/assets/agents/sdd-tasks.md +0 -108
- package/assets/agents/sdd-verify.md +0 -124
- package/assets/chains/sdd-full.chain.md +0 -83
- package/assets/chains/sdd-plan.chain.md +0 -56
- package/assets/chains/sdd-verify.chain.md +0 -43
- package/assets/sdd-orchestrator-workflow.md +0 -319
- package/assets/support/sdd-status-contract.md +0 -77
- package/docs/assets/diagrams/sdd-cycle.svg +0 -14
- package/extensions/sdd-init.ts +0 -816
- package/lib/openspec-deltas.ts +0 -156
- package/lib/sdd-preflight.ts +0 -1066
- package/lib/sdd-research-capabilities.ts +0 -94
- package/lib/sdd-status.ts +0 -26
- package/tests/fixtures/legacy/sdd-research-v2.5.0.md +0 -54
- package/tests/fixtures/native-review-cli/v2.1.3/bind-sdd.json +0 -25
- package/tests/fixtures/v0.10.7/assets/agents/sdd-apply.md +0 -132
- package/tests/openspec-deltas.test.ts +0 -209
- package/tests/sdd-agent-tools.test.ts +0 -156
- package/tests/sdd-archive-replay.test.ts +0 -82
- package/tests/sdd-classical-continuation.test.ts +0 -74
- package/tests/sdd-execution-routing-contract.test.ts +0 -44
- package/tests/sdd-managed-runtime-settlement.test.ts +0 -155
- package/tests/sdd-native-managed-uptake.test.ts +0 -243
- package/tests/sdd-no-attempts-contract.test.ts +0 -15
- package/tests/sdd-odd-integration.test.ts +0 -33
- package/tests/sdd-optional-research.test.ts +0 -124
- package/tests/sdd-planning-routing-contract.test.ts +0 -45
- package/tests/sdd-preflight-rpc-input.test.ts +0 -125
- package/tests/sdd-preflight.test.ts +0 -541
- package/tests/sdd-research-capabilities.test.ts +0 -114
- package/tests/sdd-research-live.test.ts +0 -241
- package/tests/sdd-selection-transport.test.ts +0 -653
- package/tests/sdd-status.test.ts +0 -9
- package/tests/sdd-task-truth.test.ts +0 -43
|
@@ -0,0 +1,202 @@
|
|
|
1
|
+
import assert from "node:assert/strict";
|
|
2
|
+
import test from "node:test";
|
|
3
|
+
import { fileURLToPath } from "node:url";
|
|
4
|
+
import { visibleWidth, type TuiMouseEvent } from "@earendil-works/pi-tui";
|
|
5
|
+
import { stripAnsi } from "../lib/terminal-theme.ts";
|
|
6
|
+
import { createStatsLoader, type CurrentSessionStats, type SessionRecord } from "../lib/stats-collector.ts";
|
|
7
|
+
import { StatsView, type StatsViewDeps } from "../lib/stats-view.ts";
|
|
8
|
+
|
|
9
|
+
// The /gentle:stats panel: Overview / Models / Session tabs over the
|
|
10
|
+
// collector, with range and scope toggles, q/esc close, and a clickable
|
|
11
|
+
// footer. Rendered against the collector fixtures with a fixed clock.
|
|
12
|
+
|
|
13
|
+
const SESSIONS = fileURLToPath(new URL("./fixtures/stats/sessions", import.meta.url));
|
|
14
|
+
const NOW = Date.parse("2026-10-01T12:00:00.000Z");
|
|
15
|
+
const ROWS = 40;
|
|
16
|
+
const plainTheme = { fg: (_role: string, text: string) => text };
|
|
17
|
+
// Zero-width role markers: one 256-color SGR per role, so widths stay honest.
|
|
18
|
+
const ROLES = ["accent", "borderMuted"];
|
|
19
|
+
const roleCode = (role: string) => `\x1b[38;5;${ROLES.indexOf(role) + 1}m`;
|
|
20
|
+
const roleTheme = { fg: (role: string, text: string) => `${roleCode(role)}${text}\x1b[39m` };
|
|
21
|
+
const CURRENT: CurrentSessionStats = {
|
|
22
|
+
totals: { input: 10, output: 5, cacheRead: 100, cacheWrite: 20, total: 135, cost: 0.125 },
|
|
23
|
+
messages: 1,
|
|
24
|
+
model: "claude-opus-5-5",
|
|
25
|
+
durationMs: 30 * 60 * 1000,
|
|
26
|
+
lines: { added: 2, removed: 1 },
|
|
27
|
+
};
|
|
28
|
+
|
|
29
|
+
function makeView(overrides: Partial<StatsViewDeps> = {}) {
|
|
30
|
+
const events: string[] = [];
|
|
31
|
+
const view = new StatsView({
|
|
32
|
+
theme: plainTheme,
|
|
33
|
+
rows: ROWS,
|
|
34
|
+
cwd: "/work/alpha",
|
|
35
|
+
now: () => NOW,
|
|
36
|
+
dayKey: (ms) => new Date(ms).toISOString().slice(0, 10),
|
|
37
|
+
load: () => createStatsLoader().load(SESSIONS),
|
|
38
|
+
current: () => CURRENT,
|
|
39
|
+
onClose: () => events.push("close"),
|
|
40
|
+
requestRender: () => events.push("render"),
|
|
41
|
+
...overrides,
|
|
42
|
+
});
|
|
43
|
+
return { view, events };
|
|
44
|
+
}
|
|
45
|
+
|
|
46
|
+
async function ready(overrides: Partial<StatsViewDeps> = {}) {
|
|
47
|
+
const made = makeView(overrides);
|
|
48
|
+
await made.view.load();
|
|
49
|
+
return made;
|
|
50
|
+
}
|
|
51
|
+
|
|
52
|
+
const plain = (view: StatsView, width = 100) => view.render(width).map(stripAnsi);
|
|
53
|
+
const joined = (view: StatsView, width = 100) => plain(view, width).join("\n");
|
|
54
|
+
|
|
55
|
+
function assertFits(view: StatsView, width: number) {
|
|
56
|
+
const lines = view.render(width);
|
|
57
|
+
assert.equal(lines.length, ROWS);
|
|
58
|
+
for (const line of lines) assert.equal(visibleWidth(line), width, `"${stripAnsi(line)}" is not ${width} wide`);
|
|
59
|
+
}
|
|
60
|
+
|
|
61
|
+
function click(view: StatsView, width: number, y: number, label: string) {
|
|
62
|
+
const lines = plain(view, width);
|
|
63
|
+
const x = lines[y].indexOf(label);
|
|
64
|
+
assert.ok(x >= 0, `"${label}" not on row ${y}: ${lines[y]}`);
|
|
65
|
+
const event: TuiMouseEvent = { type: "click", button: "left", x, y, screenX: x, screenY: y, width, height: ROWS, shift: false, alt: false, ctrl: false };
|
|
66
|
+
return view.handleMouse(event);
|
|
67
|
+
}
|
|
68
|
+
|
|
69
|
+
test("StatsView shows a loading state, then the overview once sessions load", async () => {
|
|
70
|
+
let resolve!: (sessions: SessionRecord[]) => void;
|
|
71
|
+
const { view, events } = makeView({ load: () => new Promise((done) => { resolve = done; }) });
|
|
72
|
+
const loading = view.load();
|
|
73
|
+
assert.match(joined(view), /Loading local sessions…/);
|
|
74
|
+
resolve(await createStatsLoader().load(SESSIONS));
|
|
75
|
+
await loading;
|
|
76
|
+
assert.ok(events.includes("render"));
|
|
77
|
+
const text = joined(view);
|
|
78
|
+
assert.doesNotMatch(text, /Loading/);
|
|
79
|
+
assert.match(text, /✿ Stats/);
|
|
80
|
+
assert.match(text, /\[× Close\]/);
|
|
81
|
+
assert.match(text, /Overview/);
|
|
82
|
+
assert.match(text, /All time · All projects/);
|
|
83
|
+
});
|
|
84
|
+
|
|
85
|
+
test("Overview pins every headline figure from the fixtures", async () => {
|
|
86
|
+
const { view } = await ready();
|
|
87
|
+
const text = joined(view);
|
|
88
|
+
assert.match(text, /Favorite model +claude-opus-5-5/);
|
|
89
|
+
assert.match(text, /Total tokens +2\.8k/);
|
|
90
|
+
assert.match(text, /Sessions +3/);
|
|
91
|
+
assert.match(text, /Longest session +2h00m/);
|
|
92
|
+
assert.match(text, /Active days +4\/31/);
|
|
93
|
+
assert.match(text, /Longest streak +3 days/);
|
|
94
|
+
assert.match(text, /Most active day +Sep 28/);
|
|
95
|
+
assert.match(text, /Current streak +3 days/);
|
|
96
|
+
assert.match(text, /in 810 · out 740 · cache read 1\.0k · cache write 200/);
|
|
97
|
+
assert.match(text, /Cost +\$2\.05/);
|
|
98
|
+
assert.match(text, /Don Quixote/);
|
|
99
|
+
assert.match(text, /Less .* More/);
|
|
100
|
+
assert.match(text, /Mon/);
|
|
101
|
+
assert.match(text, /Local Pi sessions on this machine · subagent runs not included/);
|
|
102
|
+
assertFits(view, 100);
|
|
103
|
+
});
|
|
104
|
+
|
|
105
|
+
test("the heatmap shades days with theme roles and leaves future days blank", async () => {
|
|
106
|
+
const { view } = await ready({ theme: roleTheme });
|
|
107
|
+
const text = view.render(100).join("\n");
|
|
108
|
+
const heat = text.split("\n").slice(4, 11).join("\n");
|
|
109
|
+
assert.ok(heat.includes(`${roleCode("accent")}█`), "the busiest day takes the strongest shade");
|
|
110
|
+
assert.ok(heat.includes(`${roleCode("accent")}░`), "a light day takes the faintest shade");
|
|
111
|
+
assert.ok(heat.includes(`${roleCode("borderMuted")}·`), "idle days stay visible as dots");
|
|
112
|
+
// 96 content cells hold 46 two-cell weeks after the 4-cell weekday label.
|
|
113
|
+
const x = 2 + 4 + (Math.floor((96 - 4) / 2) - 1) * 2;
|
|
114
|
+
const thisWeek = stripAnsi(text).split("\n").slice(4, 11).map((line) => line[x]);
|
|
115
|
+
assert.deepEqual(thisWeek, ["█", "░", "░", "·", " ", " ", " "], "Mon–Thu of this week are drawn, Fri–Sun are still blank");
|
|
116
|
+
assert.doesNotMatch(text, /#[0-9a-f]{6}|\x1b\[38;2/i, "no hardcoded colors");
|
|
117
|
+
});
|
|
118
|
+
|
|
119
|
+
test("range and scope toggles re-aggregate and relabel the panel", async () => {
|
|
120
|
+
const { view, events } = await ready();
|
|
121
|
+
view.handleInput("r");
|
|
122
|
+
assert.match(joined(view), /Last 7 days · All projects/);
|
|
123
|
+
assert.match(joined(view), /Total tokens +1\.8k/);
|
|
124
|
+
assert.match(joined(view), /Active days +3\/7/);
|
|
125
|
+
view.handleInput("r");
|
|
126
|
+
assert.match(joined(view), /Last 30 days/);
|
|
127
|
+
view.handleInput("r");
|
|
128
|
+
assert.match(joined(view), /All time/);
|
|
129
|
+
view.handleInput("s");
|
|
130
|
+
assert.match(joined(view), /All time · This project/);
|
|
131
|
+
assert.match(joined(view), /Sessions +2/);
|
|
132
|
+
assert.ok(events.filter((event) => event === "render").length >= 4);
|
|
133
|
+
});
|
|
134
|
+
|
|
135
|
+
test("tabs switch with Tab, shift+Tab, arrows, and digits", async () => {
|
|
136
|
+
const { view } = await ready();
|
|
137
|
+
view.handleInput("\t");
|
|
138
|
+
let text = joined(view);
|
|
139
|
+
assert.match(text, /claude-opus-5-5 .*53% .*1\.4k .*\$0\.800 .*3/);
|
|
140
|
+
assert.match(text, /gpt-5\.5 .*36%/);
|
|
141
|
+
assert.match(text, /gpt-6\.1-sol .*11%/);
|
|
142
|
+
view.handleInput("\x1b[C");
|
|
143
|
+
text = joined(view);
|
|
144
|
+
assert.match(text, /Model +claude-opus-5-5/);
|
|
145
|
+
assert.match(text, /Cost +\$0\.125/);
|
|
146
|
+
assert.match(text, /Duration +30m00s/);
|
|
147
|
+
assert.match(text, /Lines +\+2 −1/);
|
|
148
|
+
assert.match(text, /135 total · in 10 · out 5 · cache read 100 · cache write 20/);
|
|
149
|
+
view.handleInput("\x1b[Z");
|
|
150
|
+
assert.match(joined(view), /Share/);
|
|
151
|
+
view.handleInput("1");
|
|
152
|
+
assert.match(joined(view), /Favorite model/);
|
|
153
|
+
view.handleInput("\x1b[D");
|
|
154
|
+
assert.match(joined(view), /Duration/);
|
|
155
|
+
view.handleInput("2");
|
|
156
|
+
assert.match(joined(view), /Share/);
|
|
157
|
+
});
|
|
158
|
+
|
|
159
|
+
test("q and esc close exactly once", async () => {
|
|
160
|
+
const { view, events } = await ready();
|
|
161
|
+
view.handleInput("q");
|
|
162
|
+
view.handleInput("q");
|
|
163
|
+
view.handleInput("\x1b");
|
|
164
|
+
assert.equal(events.filter((event) => event === "close").length, 1);
|
|
165
|
+
const escaped = await ready();
|
|
166
|
+
escaped.view.handleInput("\x1b");
|
|
167
|
+
assert.equal(escaped.events.filter((event) => event === "close").length, 1);
|
|
168
|
+
});
|
|
169
|
+
|
|
170
|
+
test("empty and failing loads render a calm state without throwing", async () => {
|
|
171
|
+
const empty = await ready({ load: async () => [] });
|
|
172
|
+
assert.match(joined(empty.view), /No usage recorded yet/);
|
|
173
|
+
empty.view.handleInput("2");
|
|
174
|
+
assert.match(joined(empty.view), /No usage recorded yet/);
|
|
175
|
+
assertFits(empty.view, 100);
|
|
176
|
+
const failed = await ready({ load: async () => { throw new Error("disk on fire"); } });
|
|
177
|
+
assert.match(joined(failed.view), /Could not read local sessions: disk on fire/);
|
|
178
|
+
assertFits(failed.view, 100);
|
|
179
|
+
});
|
|
180
|
+
|
|
181
|
+
test("narrow terminals keep every line inside the frame", async () => {
|
|
182
|
+
const { view } = await ready();
|
|
183
|
+
for (const tab of ["1", "2", "3"]) {
|
|
184
|
+
view.handleInput(tab);
|
|
185
|
+
assertFits(view, 48);
|
|
186
|
+
assertFits(view, 120);
|
|
187
|
+
}
|
|
188
|
+
});
|
|
189
|
+
|
|
190
|
+
test("clickable header and footer controls switch tabs, toggle filters, and close", async () => {
|
|
191
|
+
const { view, events } = await ready();
|
|
192
|
+
const width = 100;
|
|
193
|
+
assert.deepEqual(click(view, width, 1, "Models"), { handled: true, render: true });
|
|
194
|
+
assert.match(joined(view), /Share/);
|
|
195
|
+
const footer = ROWS - 2;
|
|
196
|
+
click(view, width, footer, "r range");
|
|
197
|
+
assert.match(joined(view), /Last 7 days/);
|
|
198
|
+
click(view, width, footer, "s scope");
|
|
199
|
+
assert.match(joined(view), /This project/);
|
|
200
|
+
click(view, width, 0, "[× Close]");
|
|
201
|
+
assert.equal(events.filter((event) => event === "close").length, 1);
|
|
202
|
+
});
|
|
@@ -1,5 +1,8 @@
|
|
|
1
1
|
import assert from "node:assert/strict";
|
|
2
|
-
import test from "node:test";
|
|
2
|
+
import test, { after, before } from "node:test";
|
|
3
|
+
import { mkdirSync, mkdtempSync, rmSync } from "node:fs";
|
|
4
|
+
import { tmpdir } from "node:os";
|
|
5
|
+
import { join } from "node:path";
|
|
3
6
|
import type { ExtensionAPI, ExtensionContext } from "@earendil-works/pi-coding-agent";
|
|
4
7
|
import { __testing, createGentleAiExtension } from "../extensions/gentle-ai.ts";
|
|
5
8
|
import type { ExecFileAdapter, ExecFileResult } from "../lib/native-review-cli.ts";
|
|
@@ -22,6 +25,36 @@ import {
|
|
|
22
25
|
// and the foreground /gentle:telemetry slash command relay.
|
|
23
26
|
// ---------------------------------------------------------------------------
|
|
24
27
|
|
|
28
|
+
let fixtureRoot: string | undefined;
|
|
29
|
+
let fixtureCwd: string;
|
|
30
|
+
const fixtureEnvironment: NodeJS.ProcessEnv = {};
|
|
31
|
+
const previousEnvironment = new Map<string, string | undefined>();
|
|
32
|
+
before(() => {
|
|
33
|
+
fixtureRoot = mkdtempSync(join(tmpdir(), "gentle-pi-telemetry-fixture-"));
|
|
34
|
+
fixtureCwd = join(fixtureRoot, "project");
|
|
35
|
+
const home = join(fixtureRoot, "home");
|
|
36
|
+
mkdirSync(fixtureCwd);
|
|
37
|
+
mkdirSync(home);
|
|
38
|
+
Object.assign(fixtureEnvironment, {
|
|
39
|
+
HOME: home, USERPROFILE: home,
|
|
40
|
+
GENTLE_PI_CONFIG_HOME: join(home, "config"),
|
|
41
|
+
GENTLE_PI_AGENT_HOME: join(home, "agents"),
|
|
42
|
+
PI_CODING_AGENT_DIR: join(home, "pi"),
|
|
43
|
+
XDG_CONFIG_HOME: join(home, "xdg"),
|
|
44
|
+
});
|
|
45
|
+
for (const [key, value] of Object.entries(fixtureEnvironment)) {
|
|
46
|
+
previousEnvironment.set(key, process.env[key]);
|
|
47
|
+
process.env[key] = value;
|
|
48
|
+
}
|
|
49
|
+
});
|
|
50
|
+
after(() => {
|
|
51
|
+
for (const [key, value] of previousEnvironment) {
|
|
52
|
+
if (value === undefined) delete process.env[key];
|
|
53
|
+
else process.env[key] = value;
|
|
54
|
+
}
|
|
55
|
+
if (fixtureRoot !== undefined) rmSync(fixtureRoot, { recursive: true, force: true });
|
|
56
|
+
});
|
|
57
|
+
|
|
25
58
|
interface FakeSpawnRecord {
|
|
26
59
|
command: string;
|
|
27
60
|
args: string[];
|
|
@@ -172,13 +205,20 @@ function buildExtensionHarness(overrides: Parameters<typeof createGentleAiExtens
|
|
|
172
205
|
registerTool() {},
|
|
173
206
|
events: { emit() {} },
|
|
174
207
|
} as unknown as ExtensionAPI;
|
|
175
|
-
createGentleAiExtension({
|
|
208
|
+
createGentleAiExtension({
|
|
209
|
+
nativeReviewCli: null,
|
|
210
|
+
resolveTelemetryTriggerBinary: () => assert.fail("Telemetry fixtures must inject a binary resolver"),
|
|
211
|
+
telemetryTriggerSpawn: () => assert.fail("Telemetry fixtures must inject a spawn mock"),
|
|
212
|
+
telemetryExecFileAdapter: async () => assert.fail("Telemetry fixtures must inject an exec adapter"),
|
|
213
|
+
...overrides,
|
|
214
|
+
processEnv: { ...fixtureEnvironment, GENTLE_PI_AGENTS_CHILD: "0", ...overrides.processEnv },
|
|
215
|
+
})(pi);
|
|
176
216
|
return { handlers, commands };
|
|
177
217
|
}
|
|
178
218
|
|
|
179
|
-
function fakeContext(
|
|
219
|
+
function fakeContext(notifications: Array<{ message: string; severity: string }>): ExtensionContext {
|
|
180
220
|
return {
|
|
181
|
-
cwd,
|
|
221
|
+
cwd: fixtureCwd,
|
|
182
222
|
hasUI: true,
|
|
183
223
|
ui: {
|
|
184
224
|
notify(message: string, severity: string) {
|
|
@@ -202,7 +242,7 @@ test("activation: spawns the telemetry trigger exactly once for a primary sessio
|
|
|
202
242
|
const beforeAgentStart = handlers.get("before_agent_start");
|
|
203
243
|
assert.equal(typeof beforeAgentStart, "function");
|
|
204
244
|
const notifications: Array<{ message: string; severity: string }> = [];
|
|
205
|
-
const ctx = fakeContext(
|
|
245
|
+
const ctx = fakeContext(notifications);
|
|
206
246
|
|
|
207
247
|
// A primary-session event carries no agent name at all.
|
|
208
248
|
await beforeAgentStart!({ systemPrompt: "" }, ctx);
|
|
@@ -210,14 +250,17 @@ test("activation: spawns the telemetry trigger exactly once for a primary sessio
|
|
|
210
250
|
|
|
211
251
|
assert.equal(fake.calls.length, 1, "the trigger must be attempted at most once per process");
|
|
212
252
|
const [call] = fake.calls;
|
|
253
|
+
assert.equal(call.options.env.GENTLE_PI_AGENTS_CHILD, "0");
|
|
254
|
+
assert.equal(call.options.env.HOME, fixtureEnvironment.HOME);
|
|
255
|
+
assert.equal(call.options.env.GENTLE_PI_AGENTS_PARENT_PERMISSION_FD, undefined);
|
|
213
256
|
assert.deepEqual(call.args, ["telemetry", "trigger", "--json"]);
|
|
214
|
-
assert.equal(call.options.cwd,
|
|
257
|
+
assert.equal(call.options.cwd, fixtureCwd);
|
|
215
258
|
assert.equal(call.options.detached, true);
|
|
216
259
|
assert.equal(call.options.windowsHide, true);
|
|
217
260
|
assert.equal(call.options.stdio, "ignore");
|
|
218
261
|
});
|
|
219
262
|
|
|
220
|
-
test("activation: never spawns for
|
|
263
|
+
test("activation: never spawns for named agents, even with legacy prompt text", async (t) => {
|
|
221
264
|
t.after(() => __testing.resetTelemetryTriggerGuardForTesting());
|
|
222
265
|
__testing.resetTelemetryTriggerGuardForTesting();
|
|
223
266
|
const fake = fakeSpawn();
|
|
@@ -227,12 +270,27 @@ test("activation: never spawns for a named or SDD agent event", async (t) => {
|
|
|
227
270
|
});
|
|
228
271
|
const beforeAgentStart = handlers.get("before_agent_start");
|
|
229
272
|
const notifications: Array<{ message: string; severity: string }> = [];
|
|
230
|
-
const ctx = fakeContext(
|
|
273
|
+
const ctx = fakeContext(notifications);
|
|
231
274
|
|
|
232
275
|
await beforeAgentStart!({ agentName: "review-risk", systemPrompt: "" }, ctx);
|
|
233
|
-
await beforeAgentStart!({ systemPrompt: "SDD apply executor" }, ctx);
|
|
276
|
+
await beforeAgentStart!({ agentName: "gentle-ai-worker", systemPrompt: "SDD apply executor" }, ctx);
|
|
234
277
|
|
|
235
|
-
assert.equal(fake.calls.length, 0, "named
|
|
278
|
+
assert.equal(fake.calls.length, 0, "named agents must never trigger the nudge");
|
|
279
|
+
});
|
|
280
|
+
|
|
281
|
+
test("activation: a child session neither spawns telemetry nor appends the primary harness", async (t) => {
|
|
282
|
+
t.after(() => __testing.resetTelemetryTriggerGuardForTesting());
|
|
283
|
+
__testing.resetTelemetryTriggerGuardForTesting();
|
|
284
|
+
const fake = fakeSpawn();
|
|
285
|
+
const { handlers } = buildExtensionHarness({
|
|
286
|
+
resolveTelemetryTriggerBinary: () => "/opt/gentle-ai/gentle-ai",
|
|
287
|
+
telemetryTriggerSpawn: fake.spawn,
|
|
288
|
+
processEnv: { GENTLE_PI_AGENTS_CHILD: "1" },
|
|
289
|
+
});
|
|
290
|
+
const event = { systemPrompt: "base", systemPromptOptions: { appendSystemPrompt: "Child instructions" } };
|
|
291
|
+
assert.equal(await handlers.get("before_agent_start")!(event, fakeContext([])), undefined);
|
|
292
|
+
assert.equal(fake.calls.length, 0);
|
|
293
|
+
assert.equal(event.systemPromptOptions.appendSystemPrompt, "Child instructions");
|
|
236
294
|
});
|
|
237
295
|
|
|
238
296
|
test("activation: a missing binary or spawn error never affects activation", async (t) => {
|
|
@@ -245,12 +303,15 @@ test("activation: a missing binary or spawn error never affects activation", asy
|
|
|
245
303
|
});
|
|
246
304
|
const beforeAgentStart = handlers.get("before_agent_start");
|
|
247
305
|
const notifications: Array<{ message: string; severity: string }> = [];
|
|
248
|
-
const ctx = fakeContext(
|
|
249
|
-
|
|
250
|
-
// Must resolve cleanly and produce the ordinary orchestrator prompt fields
|
|
251
|
-
// never
|
|
252
|
-
|
|
253
|
-
|
|
306
|
+
const ctx = fakeContext(notifications);
|
|
307
|
+
|
|
308
|
+
// Must resolve cleanly and produce the ordinary orchestrator prompt fields
|
|
309
|
+
// in appendSystemPrompt (never a returned systemPrompt), and never throw
|
|
310
|
+
// or notify about the missing binary.
|
|
311
|
+
const event = { systemPrompt: "base", systemPromptOptions: { appendSystemPrompt: "" } };
|
|
312
|
+
const outcome = await beforeAgentStart!(event, ctx);
|
|
313
|
+
assert.equal(outcome, undefined, "the handler must not return a replacement systemPrompt");
|
|
314
|
+
assert.match(event.systemPromptOptions.appendSystemPrompt, /el Gentleman/);
|
|
254
315
|
assert.equal(notifications.length, 0);
|
|
255
316
|
});
|
|
256
317
|
|
|
@@ -284,7 +345,7 @@ test("/gentle:telemetry relays a status --json payload", async () => {
|
|
|
284
345
|
const command = commands.get("gentle:telemetry");
|
|
285
346
|
assert.ok(command, "gentle:telemetry must be registered");
|
|
286
347
|
const notifications: Array<{ message: string; severity: string }> = [];
|
|
287
|
-
await command!.handler("", fakeContext(
|
|
348
|
+
await command!.handler("", fakeContext(notifications));
|
|
288
349
|
|
|
289
350
|
assert.equal(requests.length, 1);
|
|
290
351
|
assert.deepEqual(requests[0].arguments, ["telemetry", "status", "--json"]);
|
|
@@ -308,7 +369,7 @@ test("/gentle:telemetry disable prints a one-line confirmation", async () => {
|
|
|
308
369
|
});
|
|
309
370
|
const command = commands.get("gentle:telemetry");
|
|
310
371
|
const notifications: Array<{ message: string; severity: string }> = [];
|
|
311
|
-
await command!.handler("disable", fakeContext(
|
|
372
|
+
await command!.handler("disable", fakeContext(notifications));
|
|
312
373
|
|
|
313
374
|
assert.deepEqual(notifications, [{ message: "Gentle AI telemetry disabled.", severity: "info" }]);
|
|
314
375
|
});
|
|
@@ -328,7 +389,7 @@ test("/gentle:telemetry relays a typed non-zero failure", async () => {
|
|
|
328
389
|
});
|
|
329
390
|
const command = commands.get("gentle:telemetry");
|
|
330
391
|
const notifications: Array<{ message: string; severity: string }> = [];
|
|
331
|
-
await command!.handler("preview", fakeContext(
|
|
392
|
+
await command!.handler("preview", fakeContext(notifications));
|
|
332
393
|
|
|
333
394
|
assert.equal(notifications.length, 1);
|
|
334
395
|
assert.equal(notifications[0].severity, "error");
|
|
@@ -343,7 +404,7 @@ test("/gentle:telemetry rejects an unknown sub-action without calling the binary
|
|
|
343
404
|
});
|
|
344
405
|
const command = commands.get("gentle:telemetry");
|
|
345
406
|
const notifications: Array<{ message: string; severity: string }> = [];
|
|
346
|
-
await command!.handler("frobnicate", fakeContext(
|
|
407
|
+
await command!.handler("frobnicate", fakeContext(notifications));
|
|
347
408
|
|
|
348
409
|
assert.equal(requests.length, 0);
|
|
349
410
|
assert.equal(notifications.length, 1);
|
|
@@ -0,0 +1,72 @@
|
|
|
1
|
+
import assert from "node:assert/strict";
|
|
2
|
+
import { test } from "node:test";
|
|
3
|
+
import { mkdtempSync, readFileSync, rmSync, symlinkSync, writeFileSync } from "node:fs";
|
|
4
|
+
import { tmpdir } from "node:os";
|
|
5
|
+
import { join } from "node:path";
|
|
6
|
+
import { sourcePalettePreview } from "../lib/theme-customization.ts";
|
|
7
|
+
import { colorToRgb, parseColor } from "@earendil-works/pi-tui";
|
|
8
|
+
|
|
9
|
+
test("Pi 0.99.1 source palette matches the public host OKHSL conversion", () => {
|
|
10
|
+
const source = new URL("../node_modules/@earendil-works/pi-coding-agent/dist/modes/interactive/theme/dark.json", import.meta.url);
|
|
11
|
+
const document = JSON.parse(readFileSync(source, "utf8"));
|
|
12
|
+
const accent = document.vars[document.colors.accent];
|
|
13
|
+
const text = document.vars[document.colors.text];
|
|
14
|
+
assert.match(accent, /^okhsl\(/);
|
|
15
|
+
assert.match(text, /^okhsl\(/);
|
|
16
|
+
const a = colorToRgb(parseColor(accent));
|
|
17
|
+
const b = colorToRgb(parseColor(text));
|
|
18
|
+
assert.equal(sourcePalettePreview("dark", source.pathname).sample,
|
|
19
|
+
`\x1b[48;2;${a.r};${a.g};${a.b}m \x1b[0m \x1b[38;2;${b.r};${b.g};${b.b}mAa sample text\x1b[0m`);
|
|
20
|
+
});
|
|
21
|
+
|
|
22
|
+
test("OKHSL extremes have exact RGB values and unsupported colors fail closed", (t) => {
|
|
23
|
+
const root = mkdtempSync(join(tmpdir(), "theme-preview-"));
|
|
24
|
+
t.after(() => rmSync(root, { recursive: true, force: true }));
|
|
25
|
+
const source = join(root, "theme.json");
|
|
26
|
+
const write = (accent: unknown) => writeFileSync(source, JSON.stringify({ name: "dark", colors: { accent, text: "okhsl(120 0% 100%)" } }));
|
|
27
|
+
write("okhsl(0 0% 0%)");
|
|
28
|
+
assert.equal(sourcePalettePreview("dark", source).sample,
|
|
29
|
+
"\x1b[48;2;0;0;0m \x1b[0m \x1b[38;2;255;255;255mAa sample text\x1b[0m");
|
|
30
|
+
for (const invalid of ["okhsl(0 101% 50%)", "okhsl(0 10% -1%)", "okhsl(NaN 10% 50%)", "okhsl(0 10% 50%)suffix", "rgb(1 2 3)", "red", "", {}, 256]) {
|
|
31
|
+
write(invalid);
|
|
32
|
+
assert.throws(() => sourcePalettePreview("dark", source));
|
|
33
|
+
}
|
|
34
|
+
});
|
|
35
|
+
|
|
36
|
+
test("installed source palette resolves vars without activating a theme", (t) => {
|
|
37
|
+
const root = mkdtempSync(join(tmpdir(), "theme-preview-"));
|
|
38
|
+
t.after(() => rmSync(root, { recursive: true, force: true }));
|
|
39
|
+
const source = join(root, "theme.json");
|
|
40
|
+
const document = JSON.parse(readFileSync(new URL("../node_modules/@earendil-works/pi-coding-agent/dist/modes/interactive/theme/dark.json", import.meta.url), "utf8"));
|
|
41
|
+
writeFileSync(source, JSON.stringify({ ...document, name: "dark", vars: { ...document.vars, previewAccent: "#123456" }, colors: { ...document.colors, accent: "previewAccent" } }));
|
|
42
|
+
const alias = join(root, "alias.json");
|
|
43
|
+
symlinkSync(source, alias);
|
|
44
|
+
const preview = sourcePalettePreview("dark", alias);
|
|
45
|
+
assert.match(preview.title, /dark.*source palette/);
|
|
46
|
+
assert.match(preview.sample, /48;2;18;52;86m/); // The accent swatch is a background; text is foreground.
|
|
47
|
+
assert.match(preview.sample, /38;2;/);
|
|
48
|
+
assert.match(preview.sample, /sample text/);
|
|
49
|
+
});
|
|
50
|
+
|
|
51
|
+
test("preview fails closed for missing, mismatched, malformed and oversized sources", (t) => {
|
|
52
|
+
const root = mkdtempSync(join(tmpdir(), "theme-preview-"));
|
|
53
|
+
t.after(() => rmSync(root, { recursive: true, force: true }));
|
|
54
|
+
const path = join(root, "theme.json");
|
|
55
|
+
assert.throws(() => sourcePalettePreview("dark", undefined));
|
|
56
|
+
writeFileSync(path, JSON.stringify({ name: "wrong", colors: { accent: "#abcdef", text: "#000000" } }));
|
|
57
|
+
assert.throws(() => sourcePalettePreview("dark", path));
|
|
58
|
+
writeFileSync(path, "not json");
|
|
59
|
+
assert.throws(() => sourcePalettePreview("dark", path));
|
|
60
|
+
writeFileSync(path, " ".repeat(256_001));
|
|
61
|
+
assert.throws(() => sourcePalettePreview("dark", path));
|
|
62
|
+
});
|
|
63
|
+
|
|
64
|
+
test("source palette supports indexed colors and rejects unresolved references", (t) => {
|
|
65
|
+
const root = mkdtempSync(join(tmpdir(), "theme-preview-"));
|
|
66
|
+
t.after(() => rmSync(root, { recursive: true, force: true }));
|
|
67
|
+
const path = join(root, "theme.json");
|
|
68
|
+
writeFileSync(path, JSON.stringify({ name: "dark", colors: { accent: 42, text: 255 } }));
|
|
69
|
+
assert.match(sourcePalettePreview("dark", path).sample, /48;5;42m.*38;5;255m/);
|
|
70
|
+
writeFileSync(path, JSON.stringify({ name: "dark", vars: { a: "b", b: "a" }, colors: { accent: "a", text: "#ffffff" } }));
|
|
71
|
+
assert.throws(() => sourcePalettePreview("dark", path));
|
|
72
|
+
});
|
|
@@ -0,0 +1,37 @@
|
|
|
1
|
+
import assert from "node:assert/strict";
|
|
2
|
+
import test from "node:test";
|
|
3
|
+
import { spawnSync } from "node:child_process";
|
|
4
|
+
import { copyFileSync, mkdirSync, mkdtempSync, rmSync, writeFileSync } from "node:fs";
|
|
5
|
+
import { tmpdir } from "node:os";
|
|
6
|
+
import { join } from "node:path";
|
|
7
|
+
import { fileURLToPath, pathToFileURL } from "node:url";
|
|
8
|
+
|
|
9
|
+
// A git install (`pi install git:...`) has no node_modules next to the
|
|
10
|
+
// extension: Pi aliases ES imports of host packages, but nothing else
|
|
11
|
+
// resolves them (#1586). Reproduce that layout with an ESM-only alias.
|
|
12
|
+
test("vim adapter loads without an extension-local pi-tui and fails closed without a verified version", (t) => {
|
|
13
|
+
const root = mkdtempSync(join(tmpdir(), "gentle-vim-host-resolution-"));
|
|
14
|
+
t.after(() => rmSync(root, { recursive: true, force: true }));
|
|
15
|
+
mkdirSync(join(root, "lib"));
|
|
16
|
+
copyFileSync(fileURLToPath(new URL("../lib/vim-editor-adapter.ts", import.meta.url)), join(root, "lib", "vim-editor-adapter.ts"));
|
|
17
|
+
const tuiUrl = import.meta.resolve("@earendil-works/pi-tui");
|
|
18
|
+
writeFileSync(join(root, "alias-hooks.mjs"), `export async function resolve(specifier, context, next) {
|
|
19
|
+
if (specifier === "@earendil-works/pi-tui") return { url: ${JSON.stringify(tuiUrl)}, shortCircuit: true };
|
|
20
|
+
return next(specifier, context);
|
|
21
|
+
}
|
|
22
|
+
`);
|
|
23
|
+
writeFileSync(join(root, "register.mjs"), `import { register } from "node:module";
|
|
24
|
+
register("./alias-hooks.mjs", import.meta.url);
|
|
25
|
+
`);
|
|
26
|
+
writeFileSync(join(root, "probe.mjs"), `import { Editor } from "@earendil-works/pi-tui";
|
|
27
|
+
const { createVimEditorAdapter } = await import("./lib/vim-editor-adapter.ts");
|
|
28
|
+
const editor = new Editor({ terminal: { rows: 24 }, requestRender() {} }, { borderColor: (s) => s });
|
|
29
|
+
let unverified = "admitted";
|
|
30
|
+
try { createVimEditorAdapter(editor, "0.99.1"); } catch (error) { unverified = error.message; }
|
|
31
|
+
const verified = typeof createVimEditorAdapter(editor, "0.99.1", Editor, "0.99.1").move;
|
|
32
|
+
process.stdout.write(JSON.stringify({ unverified, verified }));
|
|
33
|
+
`);
|
|
34
|
+
const result = spawnSync(process.execPath, ["--experimental-strip-types", "--import", pathToFileURL(join(root, "register.mjs")).href, join(root, "probe.mjs")], { cwd: root, encoding: "utf8" });
|
|
35
|
+
assert.equal(result.status, 0, result.stderr);
|
|
36
|
+
assert.deepEqual(JSON.parse(result.stdout), { unverified: "Unsupported Pi editor layout/version", verified: "function" });
|
|
37
|
+
});
|