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,114 @@
|
|
|
1
|
+
import assert from "node:assert/strict";
|
|
2
|
+
import test from "node:test";
|
|
3
|
+
import { isWsl, readClipboardImage, sniffImageMimeType, type ClipboardCommandRunner } from "../lib/clipboard-image.ts";
|
|
4
|
+
|
|
5
|
+
// A 2×1 PNG header: signature, IHDR length/type, width 2, height 1.
|
|
6
|
+
const PNG_2X1 = new Uint8Array([
|
|
7
|
+
0x89, 0x50, 0x4e, 0x47, 0x0d, 0x0a, 0x1a, 0x0a,
|
|
8
|
+
0x00, 0x00, 0x00, 0x0d, 0x49, 0x48, 0x44, 0x52,
|
|
9
|
+
0x00, 0x00, 0x00, 0x02, 0x00, 0x00, 0x00, 0x01,
|
|
10
|
+
0x08, 0x06, 0x00, 0x00, 0x00,
|
|
11
|
+
]);
|
|
12
|
+
|
|
13
|
+
type Call = { command: string; args: readonly string[] };
|
|
14
|
+
type Reply = Buffer | undefined | ((call: Call) => Buffer | undefined);
|
|
15
|
+
|
|
16
|
+
// Scripted backends keyed by `command firstArg`; anything unscripted is "not installed".
|
|
17
|
+
function runner(replies: Record<string, Reply>): { run: ClipboardCommandRunner; calls: Call[] } {
|
|
18
|
+
const calls: Call[] = [];
|
|
19
|
+
const run: ClipboardCommandRunner = async (command, args) => {
|
|
20
|
+
const call = { command, args };
|
|
21
|
+
calls.push(call);
|
|
22
|
+
const reply = replies[`${command} ${args[0] ?? ""}`.trim()] ?? replies[command];
|
|
23
|
+
return typeof reply === "function" ? reply(call) : reply;
|
|
24
|
+
};
|
|
25
|
+
return { run, calls };
|
|
26
|
+
}
|
|
27
|
+
|
|
28
|
+
const BMP = new Uint8Array([0x42, 0x4d, 0x00, 0x00]);
|
|
29
|
+
const noProc = () => undefined;
|
|
30
|
+
|
|
31
|
+
test("WSL reads a Windows screenshot through PowerShell when the Linux clipboard has none", async () => {
|
|
32
|
+
const { run, calls } = runner({
|
|
33
|
+
"wl-paste --list-types": Buffer.from("text/plain\n"),
|
|
34
|
+
"powershell.exe": Buffer.from(`${Buffer.from(PNG_2X1).toString("base64")}\r\n`),
|
|
35
|
+
});
|
|
36
|
+
const image = await readClipboardImage({ env: { WSL_DISTRO_NAME: "Ubuntu" }, platform: "linux", run, native: () => undefined, procVersion: noProc });
|
|
37
|
+
assert.deepEqual(image && { mimeType: image.mimeType, bytes: [...image.bytes] }, { mimeType: "image/png", bytes: [...PNG_2X1] });
|
|
38
|
+
const powershell = calls.find((call) => call.command === "powershell.exe");
|
|
39
|
+
assert.ok(powershell, "PowerShell was consulted");
|
|
40
|
+
// The image travels on stdout as base64: no temp file path is ever passed.
|
|
41
|
+
const script = powershell.args.at(-1) ?? "";
|
|
42
|
+
assert.match(script, /ToBase64String/);
|
|
43
|
+
assert.doesNotMatch(script, /\$path|Save\('|tmp/i);
|
|
44
|
+
});
|
|
45
|
+
|
|
46
|
+
test("WSL falls back to PowerShell when wl-paste and xclip are not installed", async () => {
|
|
47
|
+
const { run, calls } = runner({ "powershell.exe": Buffer.from(Buffer.from(PNG_2X1).toString("base64")) });
|
|
48
|
+
const image = await readClipboardImage({ env: { WSL_INTEROP: "/run/WSL/1_interop" }, platform: "linux", run, native: () => undefined, procVersion: noProc });
|
|
49
|
+
assert.equal(image?.mimeType, "image/png");
|
|
50
|
+
assert.deepEqual(calls.map((call) => call.command), ["wl-paste", "xclip", "powershell.exe"]);
|
|
51
|
+
});
|
|
52
|
+
|
|
53
|
+
test("Wayland prefers PNG and never asks Windows on a native Linux desktop", async () => {
|
|
54
|
+
const { run, calls } = runner({
|
|
55
|
+
"wl-paste --list-types": Buffer.from("image/jpeg\nimage/png\ntext/plain\n"),
|
|
56
|
+
"wl-paste --type": (call) => call.args[1] === "image/png" ? Buffer.from(PNG_2X1) : undefined,
|
|
57
|
+
});
|
|
58
|
+
const image = await readClipboardImage({ env: { WAYLAND_DISPLAY: "wayland-0" }, platform: "linux", run, native: () => undefined, procVersion: noProc });
|
|
59
|
+
assert.equal(image?.mimeType, "image/png");
|
|
60
|
+
assert.ok(!calls.some((call) => call.command === "powershell.exe"));
|
|
61
|
+
});
|
|
62
|
+
|
|
63
|
+
test("an empty Windows clipboard, an unsupported format, or Termux leave the paste to Pi", async () => {
|
|
64
|
+
const empty = runner({ "powershell.exe": Buffer.from("\r\n") });
|
|
65
|
+
assert.equal(await readClipboardImage({ env: { WSL_DISTRO_NAME: "Ubuntu" }, platform: "linux", run: empty.run, native: () => undefined, procVersion: noProc }), null);
|
|
66
|
+
const bmp = runner({ "xclip -selection": (call) => call.args.includes("TARGETS") ? Buffer.from("image/png\n") : Buffer.from(BMP) });
|
|
67
|
+
assert.equal(await readClipboardImage({ env: {}, platform: "linux", run: bmp.run, native: () => undefined, procVersion: noProc }), null);
|
|
68
|
+
const termux = runner({});
|
|
69
|
+
assert.equal(await readClipboardImage({ env: { TERMUX_VERSION: "0.118" }, platform: "linux", run: termux.run, procVersion: noProc }), null);
|
|
70
|
+
assert.equal(termux.calls.length, 0);
|
|
71
|
+
});
|
|
72
|
+
|
|
73
|
+
test("macOS reads the native clipboard but leaves copied files to Pi", async () => {
|
|
74
|
+
const native = (files: string[] | null) => () => ({
|
|
75
|
+
getText: async () => null,
|
|
76
|
+
getImage: async () => PNG_2X1,
|
|
77
|
+
getFilePaths: async () => files,
|
|
78
|
+
});
|
|
79
|
+
const image = await readClipboardImage({ env: {}, platform: "darwin", native: native(null) });
|
|
80
|
+
assert.equal(image?.mimeType, "image/png");
|
|
81
|
+
assert.equal(await readClipboardImage({ env: {}, platform: "darwin", native: native(["/Users/me/shot.png"]) }), null);
|
|
82
|
+
assert.equal(await readClipboardImage({ env: {}, platform: "win32", native: () => undefined }), null);
|
|
83
|
+
});
|
|
84
|
+
|
|
85
|
+
test("a failed wl-paste read falls through to xclip, as Pi does", async () => {
|
|
86
|
+
const { run, calls } = runner({
|
|
87
|
+
"wl-paste --list-types": Buffer.from("image/png\n"),
|
|
88
|
+
"xclip -selection": (call) => call.args.includes("TARGETS") ? Buffer.from("image/png\n") : Buffer.from(PNG_2X1),
|
|
89
|
+
});
|
|
90
|
+
const image = await readClipboardImage({ env: { WAYLAND_DISPLAY: "wayland-0" }, platform: "linux", run, native: () => undefined, procVersion: noProc });
|
|
91
|
+
assert.equal(image?.mimeType, "image/png");
|
|
92
|
+
assert.deepEqual(calls.map((call) => call.command), ["wl-paste", "wl-paste", "xclip", "xclip"]);
|
|
93
|
+
});
|
|
94
|
+
|
|
95
|
+
test("files copied in a Linux file manager stay Pi's even when an image is offered too", async () => {
|
|
96
|
+
const { run, calls } = runner({ "wl-paste --list-types": Buffer.from("image/png\ntext/uri-list\n") });
|
|
97
|
+
const native = () => ({ getText: async () => null, getImage: async () => PNG_2X1, getFilePaths: async () => ["/home/me/shot.png"] });
|
|
98
|
+
assert.equal(await readClipboardImage({ env: { WAYLAND_DISPLAY: "wayland-0" }, platform: "linux", run, native, procVersion: noProc }), null);
|
|
99
|
+
assert.equal(calls.length, 0);
|
|
100
|
+
});
|
|
101
|
+
|
|
102
|
+
test("image formats are identified by signature", () => {
|
|
103
|
+
assert.equal(sniffImageMimeType(PNG_2X1), "image/png");
|
|
104
|
+
assert.equal(sniffImageMimeType(new Uint8Array([0xff, 0xd8, 0xff, 0xe0])), "image/jpeg");
|
|
105
|
+
assert.equal(sniffImageMimeType(new Uint8Array([0x47, 0x49, 0x46, 0x38, 0x39, 0x61])), "image/gif");
|
|
106
|
+
assert.equal(sniffImageMimeType(new Uint8Array([0x52, 0x49, 0x46, 0x46, 0, 0, 0, 0, 0x57, 0x45, 0x42, 0x50])), "image/webp");
|
|
107
|
+
assert.equal(sniffImageMimeType(BMP), undefined);
|
|
108
|
+
});
|
|
109
|
+
|
|
110
|
+
test("WSL is detected from its environment or the kernel version", () => {
|
|
111
|
+
assert.equal(isWsl({ WSL_DISTRO_NAME: "Ubuntu" }, noProc), true);
|
|
112
|
+
assert.equal(isWsl({}, () => "Linux version 6.6.87.2-microsoft-standard-WSL2"), true);
|
|
113
|
+
assert.equal(isWsl({}, () => "Linux version 6.8.0-generic"), false);
|
|
114
|
+
});
|
|
@@ -1,8 +1,8 @@
|
|
|
1
1
|
import assert from "node:assert/strict";
|
|
2
2
|
import { execFileSync } from "node:child_process";
|
|
3
|
-
import { chmodSync, existsSync, mkdirSync, mkdtempSync, realpathSync, rmSync, symlinkSync, writeFileSync } from "node:fs";
|
|
3
|
+
import { chmodSync, existsSync, mkdirSync, mkdtempSync, readFileSync, realpathSync, rmSync, symlinkSync, writeFileSync } from "node:fs";
|
|
4
4
|
import { homedir, tmpdir } from "node:os";
|
|
5
|
-
import { join } from "node:path";
|
|
5
|
+
import { join, resolve } from "node:path";
|
|
6
6
|
import test from "node:test";
|
|
7
7
|
import type { ExtensionAPI, ExtensionContext } from "@earendil-works/pi-coding-agent";
|
|
8
8
|
import codeGraphTools, {
|
|
@@ -356,3 +356,124 @@ test("Windows launcher passes command metacharacters as one literal argv value",
|
|
|
356
356
|
}]);
|
|
357
357
|
assert.equal(existsSync(join(cwd, "shell-injection.txt")), false);
|
|
358
358
|
});
|
|
359
|
+
|
|
360
|
+
// Real CodeGraph writes .codegraph/.gitignore with `*` and `!.gitignore`, so the
|
|
361
|
+
// .gitignore itself stays untracked and would otherwise enter every diff.
|
|
362
|
+
const writeIndex: CodeGraphRunner = async (_args, options) => {
|
|
363
|
+
mkdirSync(join(options.cwd, ".codegraph"), { recursive: true });
|
|
364
|
+
writeFileSync(join(options.cwd, ".codegraph", ".gitignore"), "*\n!.gitignore\n");
|
|
365
|
+
writeFileSync(join(options.cwd, ".codegraph", "codegraph.db"), "index");
|
|
366
|
+
return { stdout: "indexed", stderr: "" };
|
|
367
|
+
};
|
|
368
|
+
|
|
369
|
+
function gitOutput(cwd: string, ...args: string[]): string {
|
|
370
|
+
return execFileSync("git", args, { cwd, encoding: "utf8", stdio: ["ignore", "pipe", "pipe"] }).trim();
|
|
371
|
+
}
|
|
372
|
+
|
|
373
|
+
function excludeFile(cwd: string): string {
|
|
374
|
+
return realpathSync(resolve(cwd, gitOutput(cwd, "rev-parse", "--git-path", "info/exclude")));
|
|
375
|
+
}
|
|
376
|
+
|
|
377
|
+
function excludeLines(cwd: string): string[] {
|
|
378
|
+
return readFileSync(excludeFile(cwd), "utf8").split("\n").filter((line) => line.trim() === ".codegraph/");
|
|
379
|
+
}
|
|
380
|
+
|
|
381
|
+
test("CodeGraph init keeps the whole .codegraph index out of git through the local exclude file, idempotently", async (t) => {
|
|
382
|
+
const cwd = workspace(t);
|
|
383
|
+
writeFileSync(join(cwd, "tracked.txt"), "tracked\n");
|
|
384
|
+
execFileSync("git", ["add", "tracked.txt"], { cwd });
|
|
385
|
+
const tool = createCodeGraphTool(writeIndex);
|
|
386
|
+
const ctx = { cwd } as ExtensionContext;
|
|
387
|
+
|
|
388
|
+
const first = await tool.execute("test", { operation: "init" }, undefined, undefined, ctx);
|
|
389
|
+
const second = await tool.execute("test", { operation: "init" }, undefined, undefined, ctx);
|
|
390
|
+
|
|
391
|
+
assert.deepEqual(first.content, [{ type: "text", text: "indexed" }]);
|
|
392
|
+
assert.equal(gitOutput(cwd, "status", "--porcelain", "--untracked-files=all"), "A tracked.txt");
|
|
393
|
+
assert.deepEqual(excludeLines(cwd), [".codegraph/"], "exactly one exclude line after repeated init");
|
|
394
|
+
assert.equal(existsSync(join(cwd, ".gitignore")), false, "never creates or edits a tracked .gitignore");
|
|
395
|
+
assert.deepEqual((first.details as { localExclude?: unknown }).localExclude, { status: "added", path: excludeFile(cwd) });
|
|
396
|
+
assert.deepEqual((second.details as { localExclude?: unknown }).localExclude, { status: "present", path: excludeFile(cwd) });
|
|
397
|
+
});
|
|
398
|
+
|
|
399
|
+
test("CodeGraph exclusion appends on a new line and recognizes an equivalent existing pattern", async (t) => {
|
|
400
|
+
const appended = workspace(t);
|
|
401
|
+
writeFileSync(excludeFile(appended), "# local\n*.log");
|
|
402
|
+
await createCodeGraphTool(writeIndex).execute("test", { operation: "init" }, undefined, undefined, { cwd: appended } as ExtensionContext);
|
|
403
|
+
assert.equal(readFileSync(excludeFile(appended), "utf8"), "# local\n*.log\n.codegraph/\n");
|
|
404
|
+
|
|
405
|
+
const equivalent = workspace(t);
|
|
406
|
+
writeFileSync(excludeFile(equivalent), "/.codegraph\n");
|
|
407
|
+
const result = await createCodeGraphTool(writeIndex).execute("test", { operation: "init" }, undefined, undefined, { cwd: equivalent } as ExtensionContext);
|
|
408
|
+
assert.equal(readFileSync(excludeFile(equivalent), "utf8"), "/.codegraph\n");
|
|
409
|
+
assert.equal((result.details as { localExclude?: { status: string } }).localExclude?.status, "present");
|
|
410
|
+
assert.equal(gitOutput(equivalent, "status", "--porcelain", "--untracked-files=all"), "");
|
|
411
|
+
});
|
|
412
|
+
|
|
413
|
+
test("CodeGraph exclusion uses the exclude file git resolves for a linked worktree", async (t) => {
|
|
414
|
+
const base = realpathSync(mkdtempSync(join(tmpdir(), "gentle-pi-codegraph-worktree-")));
|
|
415
|
+
t.after(() => rmSync(base, { recursive: true, force: true }));
|
|
416
|
+
const primary = join(base, "primary");
|
|
417
|
+
const linked = join(base, "linked");
|
|
418
|
+
mkdirSync(primary);
|
|
419
|
+
execFileSync("git", ["init", "-b", "main"], { cwd: primary, stdio: "ignore" });
|
|
420
|
+
execFileSync("git", ["-c", "user.name=Fixture", "-c", "user.email=fixture@example.invalid", "-c", "commit.gpgsign=false", "commit", "--allow-empty", "-m", "fixture"], { cwd: primary, stdio: "ignore" });
|
|
421
|
+
execFileSync("git", ["worktree", "add", "-b", "feature", linked], { cwd: primary, stdio: "ignore" });
|
|
422
|
+
|
|
423
|
+
const result = await createCodeGraphTool(writeIndex).execute("test", { operation: "init" }, undefined, undefined, { cwd: linked } as ExtensionContext);
|
|
424
|
+
|
|
425
|
+
assert.deepEqual(result.content, [{ type: "text", text: "indexed" }]);
|
|
426
|
+
assert.equal(gitOutput(linked, "status", "--porcelain", "--untracked-files=all"), "");
|
|
427
|
+
assert.deepEqual(excludeLines(linked), [".codegraph/"]);
|
|
428
|
+
assert.equal((result.details as { localExclude?: { path: string } }).localExclude?.path, excludeFile(linked));
|
|
429
|
+
assert.equal(existsSync(join(linked, ".git", "info")), false, "the linked worktree's .git file is never treated as a directory");
|
|
430
|
+
});
|
|
431
|
+
|
|
432
|
+
test("CodeGraph query excludes an index that already exists, and leaves the exclude file alone without one", async (t) => {
|
|
433
|
+
const indexed = workspace(t);
|
|
434
|
+
await writeIndex(["init", indexed], { cwd: indexed, maxBuffer: 1 });
|
|
435
|
+
const answer: CodeGraphRunner = async () => ({ stdout: "found", stderr: "" });
|
|
436
|
+
await createCodeGraphTool(answer).execute("test", { operation: "query", query: "symbol" }, undefined, undefined, { cwd: indexed } as ExtensionContext);
|
|
437
|
+
assert.deepEqual(excludeLines(indexed), [".codegraph/"]);
|
|
438
|
+
assert.equal(gitOutput(indexed, "status", "--porcelain", "--untracked-files=all"), "");
|
|
439
|
+
|
|
440
|
+
const bare = workspace(t);
|
|
441
|
+
const result = await createCodeGraphTool(answer).execute("test", { operation: "query", query: "symbol" }, undefined, undefined, { cwd: bare } as ExtensionContext);
|
|
442
|
+
assert.deepEqual(excludeLines(bare), []);
|
|
443
|
+
assert.equal((result.details as { localExclude?: unknown }).localExclude, undefined);
|
|
444
|
+
});
|
|
445
|
+
|
|
446
|
+
test("CodeGraph init still succeeds and reports when git is unavailable for the local exclusion", async (t) => {
|
|
447
|
+
const cwd = workspace(t);
|
|
448
|
+
const empty = realpathSync(mkdtempSync(join(tmpdir(), "gentle-pi-codegraph-nogit-")));
|
|
449
|
+
const originalPath = process.env.PATH;
|
|
450
|
+
t.after(() => {
|
|
451
|
+
process.env.PATH = originalPath;
|
|
452
|
+
rmSync(empty, { recursive: true, force: true });
|
|
453
|
+
});
|
|
454
|
+
const result = await createCodeGraphTool(async (args, options) => {
|
|
455
|
+
const output = await writeIndex(args, options);
|
|
456
|
+
process.env.PATH = empty; // git disappears after the workspace check
|
|
457
|
+
return output;
|
|
458
|
+
}).execute("test", { operation: "init" }, undefined, undefined, { cwd } as ExtensionContext);
|
|
459
|
+
process.env.PATH = originalPath;
|
|
460
|
+
|
|
461
|
+
assert.match(result.content[0].text, /^indexed\n\n/);
|
|
462
|
+
assert.match(result.content[0].text, /\.codegraph\/ is not excluded from git locally/);
|
|
463
|
+
assert.match(result.content[0].text, /keep \.codegraph\/ out of the change/i);
|
|
464
|
+
assert.equal((result.details as { localExclude?: { status: string } }).localExclude?.status, "failed");
|
|
465
|
+
assert.deepEqual(excludeLines(cwd), []);
|
|
466
|
+
});
|
|
467
|
+
|
|
468
|
+
test("CodeGraph excludes a partial index left behind by a failed init without changing the fallback text", async (t) => {
|
|
469
|
+
const cwd = workspace(t);
|
|
470
|
+
const result = await createCodeGraphTool(async (args, options) => {
|
|
471
|
+
await writeIndex(args, options);
|
|
472
|
+
throw new Error("CodeGraph exited with status 1");
|
|
473
|
+
}).execute("test", { operation: "init" }, undefined, undefined, { cwd } as ExtensionContext);
|
|
474
|
+
|
|
475
|
+
assert.deepEqual(result.content, [{ type: "text", text: "CodeGraph failed to run. Use read, grep, and find for this exploration." }]);
|
|
476
|
+
assert.equal((result.details as { status: string }).status, "failed");
|
|
477
|
+
assert.equal((result.details as { localExclude?: { status: string } }).localExclude?.status, "added");
|
|
478
|
+
assert.equal(gitOutput(cwd, "status", "--porcelain", "--untracked-files=all"), "");
|
|
479
|
+
});
|
|
@@ -4,6 +4,8 @@ import { existsSync, readFileSync } from "node:fs";
|
|
|
4
4
|
import { visibleWidth } from "@earendil-works/pi-tui";
|
|
5
5
|
import { CommandPalette, commandsKey, rankPaletteGroups, type CommandPaletteGroup, type CommandPaletteItem, type CommandPaletteResult, type CommandPaletteTheme } from "../lib/command-palette.ts";
|
|
6
6
|
import { buildCommandPaletteGroups, COMMAND_PALETTE_CATALOG } from "../lib/command-palette-catalog.ts";
|
|
7
|
+
import type { ExtensionAPI } from "@earendil-works/pi-coding-agent";
|
|
8
|
+
import startupBanner from "../extensions/startup-banner.ts";
|
|
7
9
|
|
|
8
10
|
// Command palette: a pure, curated, grouped overlay component for
|
|
9
11
|
// `/gentle:commands` and its `alt+k` shortcut. These tests drive ranking,
|
|
@@ -370,18 +372,26 @@ test("COMMAND_PALETTE_CATALOG matches the curated command set, in order", () =>
|
|
|
370
372
|
"gentle:animations",
|
|
371
373
|
"gentle:vim",
|
|
372
374
|
"gentle:telemetry",
|
|
373
|
-
"gentle:banner",
|
|
374
|
-
"gentle:banner-color",
|
|
375
|
-
"gentle:toggle-rose",
|
|
376
|
-
"gentle:toggle-text-logo",
|
|
377
375
|
"gentle:dev-binary",
|
|
378
376
|
]);
|
|
379
|
-
assert.deepEqual(byTitle("Session"), ["gentle:yolo", "gentle:changes", "gentle:agents", "gentle:jobs", "gentle:usage", "gentle:review-session-permission"]);
|
|
377
|
+
assert.deepEqual(byTitle("Session"), ["gentle:yolo", "gentle:changes", "gentle:agents", "gentle:jobs", "gentle:usage", "gentle:review-session-permission", "gentle:voice", "gentle:design"]);
|
|
380
378
|
assert.deepEqual(byTitle("Diagnostics"), ["gentle:status", "gentle:doctor"]);
|
|
381
379
|
assert.equal(byTitle("SDD"), undefined);
|
|
382
380
|
assert.deepEqual(byTitle("Skills"), ["skill-registry:refresh"]);
|
|
383
381
|
});
|
|
384
382
|
|
|
383
|
+
// #1664: banner settings live under Customization > Banner; the palette keeps
|
|
384
|
+
// one route while the slash commands stay registered for compatibility.
|
|
385
|
+
test("banner settings have one palette route and keep their slash commands", () => {
|
|
386
|
+
const banner = ["gentle:banner", "gentle:banner-color", "gentle:toggle-rose", "gentle:toggle-text-logo"];
|
|
387
|
+
const catalogCommands = COMMAND_PALETTE_CATALOG.flatMap((group) => group.items.map((item) => item.command));
|
|
388
|
+
for (const command of banner) assert.ok(!catalogCommands.includes(command), `${command} must not appear in the palette`);
|
|
389
|
+
assert.ok(catalogCommands.includes("gentle:customize"));
|
|
390
|
+
const registered: string[] = [];
|
|
391
|
+
startupBanner({ on() {}, registerCommand(name: string) { registered.push(name); }, getCommands: () => [], getAllTools: () => [] } as unknown as ExtensionAPI);
|
|
392
|
+
for (const command of banner) assert.ok(registered.includes(command), `${command} stays registered`);
|
|
393
|
+
});
|
|
394
|
+
|
|
385
395
|
test("retired SDD commands are absent from the palette and extension registrations", () => {
|
|
386
396
|
const retired = ["gentle:sdd-preflight", "gentle-sdd-status", "gentle-sdd-continue", "gentle-sdd-init", "gentle:install-sdd"];
|
|
387
397
|
const catalogCommands = COMMAND_PALETTE_CATALOG.flatMap((group) => group.items.map((item) => item.command));
|
|
@@ -0,0 +1,162 @@
|
|
|
1
|
+
import assert from "node:assert/strict";
|
|
2
|
+
import { execFileSync } from "node:child_process";
|
|
3
|
+
import { appendFileSync, mkdirSync, mkdtempSync, realpathSync, rmSync, writeFileSync } from "node:fs";
|
|
4
|
+
import { tmpdir } from "node:os";
|
|
5
|
+
import { join } from "node:path";
|
|
6
|
+
import test, { type TestContext } from "node:test";
|
|
7
|
+
import { commitSliceReport, commitSliceToolResult, commitTargetDirectory, isGitCommitCommand } from "../lib/commit-slice-signal.ts";
|
|
8
|
+
|
|
9
|
+
// Harness roadmap T4 (R1): the ~400-line slice rule lived in the prompt and the
|
|
10
|
+
// model had to estimate line counts. After each parent commit the runtime now
|
|
11
|
+
// reports the authored changed lines on the branch, so slicing reads a number.
|
|
12
|
+
|
|
13
|
+
function repo(t: TestContext): { dir: string; git: (...args: string[]) => string; commitFile: (path: string, lines: number) => void } {
|
|
14
|
+
const dir = realpathSync(mkdtempSync(join(tmpdir(), "commit-slice-")));
|
|
15
|
+
t.after(() => rmSync(dir, { recursive: true, force: true }));
|
|
16
|
+
const git = (...args: string[]) => execFileSync("git", ["-c", "user.name=Fixture", "-c", "user.email=fixture@example.com", "-c", "commit.gpgsign=false", ...args], { cwd: dir, encoding: "utf8" });
|
|
17
|
+
const commitFile = (path: string, lines: number) => {
|
|
18
|
+
mkdirSync(join(dir, path, ".."), { recursive: true });
|
|
19
|
+
writeFileSync(join(dir, path), Array.from({ length: lines }, (_, i) => `line ${i}`).join("\n") + "\n");
|
|
20
|
+
git("add", "--", path);
|
|
21
|
+
git("commit", "-q", "-m", `add ${path}`);
|
|
22
|
+
};
|
|
23
|
+
git("init", "-q", "-b", "main");
|
|
24
|
+
commitFile("README.md", 3);
|
|
25
|
+
return { dir, git, commitFile };
|
|
26
|
+
}
|
|
27
|
+
|
|
28
|
+
test("isGitCommitCommand recognizes commits inside shell commands, nothing else", () => {
|
|
29
|
+
for (const command of ["git commit -m 'feat: x'", "git add -A && git commit -qm x", "cd pkg && git -C . commit --amend --no-edit", "git -c user.name=a commit -m x"]) {
|
|
30
|
+
assert.equal(isGitCommitCommand(command), true, command);
|
|
31
|
+
}
|
|
32
|
+
for (const command of ["git status", "git log --grep commit", "echo git commit", "npm test", "git commit-tree abc"]) {
|
|
33
|
+
assert.equal(isGitCommitCommand(command), false, command);
|
|
34
|
+
}
|
|
35
|
+
});
|
|
36
|
+
|
|
37
|
+
// R1 review advisory: any `echo ... git` segment hid a real commit elsewhere in
|
|
38
|
+
// the command, while text that only mentioned `git commit` counted as one.
|
|
39
|
+
test("isGitCommitCommand counts a git invocation that starts a shell segment, not text that mentions one", () => {
|
|
40
|
+
for (const command of ["echo 'staging git files' && git commit -m x", "echo git status; git commit -m x", "GIT_AUTHOR_DATE=now git commit -m x", "(cd pkg && git commit -m x)", "git add -A\ngit commit -m x", "git commit -m 'feat: git commit counting'"]) {
|
|
41
|
+
assert.equal(isGitCommitCommand(command), true, command);
|
|
42
|
+
}
|
|
43
|
+
for (const command of ["printf 'run git commit now'", "gh pr create --body \"then git commit -m x\"", "grep -rn 'git commit' docs", "echo \"git commit -m x\""]) {
|
|
44
|
+
assert.equal(isGitCommitCommand(command), false, command);
|
|
45
|
+
}
|
|
46
|
+
});
|
|
47
|
+
|
|
48
|
+
test("commitTargetDirectory follows git -C and a leading cd", () => {
|
|
49
|
+
assert.equal(commitTargetDirectory("git commit -m x", "/work/repo"), "/work/repo");
|
|
50
|
+
assert.equal(commitTargetDirectory("git -C ../other commit -m x", "/work/repo"), "/work/other");
|
|
51
|
+
assert.equal(commitTargetDirectory("cd /tmp/x && git commit -m x", "/work/repo"), "/tmp/x");
|
|
52
|
+
});
|
|
53
|
+
|
|
54
|
+
// PR #2290 review: only a leading `cd <dir> &&` moved the count, so a commit after
|
|
55
|
+
// `cd <dir>;` or a later `cd` counted the session cwd instead.
|
|
56
|
+
test("commitTargetDirectory follows a cd that precedes the commit through ;, && or ||", () => {
|
|
57
|
+
assert.equal(commitTargetDirectory("cd /tmp/x; git commit -m x", "/work/repo"), "/tmp/x");
|
|
58
|
+
assert.equal(commitTargetDirectory("git add -A && cd pkg && git commit -m x", "/work/repo"), "/work/repo/pkg");
|
|
59
|
+
assert.equal(commitTargetDirectory("cd /tmp/x || exit 1; git commit -m x", "/work/repo"), "/tmp/x");
|
|
60
|
+
assert.equal(commitTargetDirectory("cd a && cd 'b c'\ngit -C d commit -m x", "/work/repo"), "/work/repo/a/b c/d");
|
|
61
|
+
assert.equal(commitTargetDirectory("git commit -m x && cd /tmp/x", "/work/repo"), "/work/repo", "a cd after the commit does not move it");
|
|
62
|
+
});
|
|
63
|
+
|
|
64
|
+
test("a feature branch reports authored changed lines since the default branch, lock files excluded", async (t) => {
|
|
65
|
+
const { dir, git, commitFile } = repo(t);
|
|
66
|
+
git("switch", "-q", "-c", "feat/slice");
|
|
67
|
+
commitFile("src/a.ts", 120);
|
|
68
|
+
commitFile("pnpm-lock.yaml", 900);
|
|
69
|
+
commitFile("tests/a.test.ts", 30);
|
|
70
|
+
const report = await commitSliceReport(dir);
|
|
71
|
+
assert.match(report ?? "", /^Commit slice: 150 authored changed lines in 3 commits on feat\/slice since main/);
|
|
72
|
+
assert.match(report ?? "", /250 lines left before the ~400-line slice/);
|
|
73
|
+
});
|
|
74
|
+
|
|
75
|
+
// PR #2290 review: `--no-renames` counted a moved file as all of its lines deleted
|
|
76
|
+
// and added again; rename detection counts only what the move changed.
|
|
77
|
+
test("a move-only commit counts no authored lines", async (t) => {
|
|
78
|
+
const { dir, git, commitFile } = repo(t);
|
|
79
|
+
commitFile("src/old.ts", 120);
|
|
80
|
+
git("switch", "-q", "-c", "feat/move");
|
|
81
|
+
git("mv", "src/old.ts", "src/new.ts");
|
|
82
|
+
git("commit", "-q", "-m", "move src/old.ts");
|
|
83
|
+
assert.match(await commitSliceReport(dir) ?? "", /^Commit slice: 0 authored changed lines in 1 commit on feat\/move/);
|
|
84
|
+
git("mv", "src/new.ts", "src/newer.ts");
|
|
85
|
+
appendFileSync(join(dir, "src/newer.ts"), "line 120\n");
|
|
86
|
+
git("commit", "-qam", "move and extend src/new.ts");
|
|
87
|
+
assert.match(await commitSliceReport(dir) ?? "", /^Commit slice: 1 authored changed lines in 2 commits on feat\/move/, "a move that edits counts only the edit");
|
|
88
|
+
});
|
|
89
|
+
|
|
90
|
+
// PR #2290 review: Git quotes an unusual path in plain numstat output, so the
|
|
91
|
+
// trailing quote hid a lock file from the generated-file filter.
|
|
92
|
+
test("a lock file whose path Git quotes is still excluded", async (t) => {
|
|
93
|
+
const { dir, git, commitFile } = repo(t);
|
|
94
|
+
git("switch", "-q", "-c", "feat/quoted");
|
|
95
|
+
commitFile("packages/caf\u00e9/pnpm-lock.yaml", 900);
|
|
96
|
+
commitFile("src/a.ts", 10);
|
|
97
|
+
assert.match(git("-c", "core.quotePath=true", "diff", "--numstat", "main", "HEAD"), /"packages\/caf\\303\\251\/pnpm-lock\.yaml"/, "the fixture path needs quoting");
|
|
98
|
+
assert.match(await commitSliceReport(dir) ?? "", /^Commit slice: 10 authored changed lines in 2 commits on feat\/quoted/);
|
|
99
|
+
});
|
|
100
|
+
|
|
101
|
+
test("past ~400 lines the report says to apply the delivery strategy before the next commit", async (t) => {
|
|
102
|
+
const { dir, git, commitFile } = repo(t);
|
|
103
|
+
git("switch", "-q", "-c", "feat/big");
|
|
104
|
+
commitFile("src/big.ts", 450);
|
|
105
|
+
assert.match(await commitSliceReport(dir) ?? "", /450 authored changed lines in 1 commit on feat\/big since main\b.*Past the ~400-line slice: apply the feature's delivery strategy before the next commit/);
|
|
106
|
+
});
|
|
107
|
+
|
|
108
|
+
// R1 review advisory: the report ran git synchronously in the parent's
|
|
109
|
+
// tool_result handler, so the interface froze for as long as git took.
|
|
110
|
+
test("the parent keeps running while git computes the report", async (t) => {
|
|
111
|
+
const { dir, git, commitFile } = repo(t);
|
|
112
|
+
git("switch", "-q", "-c", "feat/async");
|
|
113
|
+
commitFile("src/a.ts", 10);
|
|
114
|
+
let ran = false;
|
|
115
|
+
setImmediate(() => { ran = true; });
|
|
116
|
+
const report = await commitSliceReport(dir);
|
|
117
|
+
assert.equal(ran, true, "the event loop ran while git answered");
|
|
118
|
+
assert.match(report ?? "", /^Commit slice: 10 authored changed lines in 1 commit on feat\/async/);
|
|
119
|
+
});
|
|
120
|
+
|
|
121
|
+
// R1 review advisory: a failed or timed-out `git diff` read as an empty diff
|
|
122
|
+
// and reported "0 authored changed lines"; no number beats a wrong one.
|
|
123
|
+
test("a failed diff reports nothing instead of zero lines", async () => {
|
|
124
|
+
const answers: Record<string, string | undefined> = {
|
|
125
|
+
"symbolic-ref -q --short HEAD": "feat/slice",
|
|
126
|
+
"symbolic-ref -q --short refs/remotes/origin/HEAD": "origin/main",
|
|
127
|
+
"merge-base HEAD origin/main": "base",
|
|
128
|
+
"rev-list --count base..HEAD": "2",
|
|
129
|
+
"diff --numstat -z -M base HEAD": "12\t3\tsrc/a.ts\0",
|
|
130
|
+
};
|
|
131
|
+
const git = async (args: string[]) => answers[args.join(" ")];
|
|
132
|
+
assert.match(await commitSliceReport("/repo", git) ?? "", /^Commit slice: 15 authored changed lines in 2 commits/);
|
|
133
|
+
delete answers["diff --numstat -z -M base HEAD"];
|
|
134
|
+
assert.equal(await commitSliceReport("/repo", git), undefined);
|
|
135
|
+
});
|
|
136
|
+
|
|
137
|
+
// R1 review advisory: a background job's result arrives when the job starts,
|
|
138
|
+
// before its commit exists, so its slice line would describe the old HEAD.
|
|
139
|
+
test("only a foreground bash commit carries the slice line", async () => {
|
|
140
|
+
const git = async (args: string[]) => ({
|
|
141
|
+
"symbolic-ref -q --short HEAD": "feat/slice",
|
|
142
|
+
"symbolic-ref -q --short refs/remotes/origin/HEAD": "origin/main",
|
|
143
|
+
"merge-base HEAD origin/main": "base",
|
|
144
|
+
"rev-list --count base..HEAD": "1",
|
|
145
|
+
"diff --numstat -z -M base HEAD": "5\t0\tsrc/a.ts\0",
|
|
146
|
+
} as Record<string, string>)[args.join(" ")];
|
|
147
|
+
const event = (toolName: string) => ({ toolName, input: { command: "git commit -m x" }, isError: false, content: [{ type: "text" as const, text: "ok" }], structuredContent: undefined });
|
|
148
|
+
assert.match(JSON.stringify((await commitSliceToolResult(event("bash"), "/repo", git))?.content), /Commit slice: 5 authored changed lines/);
|
|
149
|
+
assert.equal(await commitSliceToolResult(event("bash_background"), "/repo", git), undefined);
|
|
150
|
+
assert.equal(await commitSliceToolResult(event("monitor"), "/repo", git), undefined);
|
|
151
|
+
});
|
|
152
|
+
|
|
153
|
+
test("commits on the default branch, outside Git, or with nothing ahead report nothing", async (t) => {
|
|
154
|
+
const { dir, git, commitFile } = repo(t);
|
|
155
|
+
commitFile("src/direct.ts", 10);
|
|
156
|
+
assert.equal(await commitSliceReport(dir), undefined, "the default branch has no slice");
|
|
157
|
+
git("switch", "-q", "-c", "feat/empty");
|
|
158
|
+
assert.equal(await commitSliceReport(dir), undefined, "nothing ahead of the default branch");
|
|
159
|
+
const plain = realpathSync(mkdtempSync(join(tmpdir(), "commit-slice-plain-")));
|
|
160
|
+
t.after(() => rmSync(plain, { recursive: true, force: true }));
|
|
161
|
+
assert.equal(await commitSliceReport(plain), undefined, "outside Git");
|
|
162
|
+
});
|