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,132 @@
|
|
|
1
|
+
import assert from "node:assert/strict";
|
|
2
|
+
import { mkdirSync, mkdtempSync, rmSync, symlinkSync, utimesSync, writeFileSync } from "node:fs";
|
|
3
|
+
import { tmpdir } from "node:os";
|
|
4
|
+
import { dirname, join } from "node:path";
|
|
5
|
+
import test, { type TestContext } from "node:test";
|
|
6
|
+
import { DESIGN_SOURCE_LIMITS, detectDesignSources, staleDesignSources } from "../lib/design-sources.ts";
|
|
7
|
+
|
|
8
|
+
// S13: the design sources a project has, found by a bounded deterministic
|
|
9
|
+
// walk, and the ones changed after design/system.md was written.
|
|
10
|
+
|
|
11
|
+
function scratch(t: TestContext): string {
|
|
12
|
+
const dir = mkdtempSync(join(tmpdir(), "gentle-design-sources-"));
|
|
13
|
+
t.after(() => rmSync(dir, { recursive: true, force: true }));
|
|
14
|
+
return dir;
|
|
15
|
+
}
|
|
16
|
+
|
|
17
|
+
function write(root: string, path: string, content = ""): void {
|
|
18
|
+
mkdirSync(dirname(join(root, path)), { recursive: true });
|
|
19
|
+
writeFileSync(join(root, path), content);
|
|
20
|
+
}
|
|
21
|
+
|
|
22
|
+
test("each design source kind is detected, sorted, and plain files are not", (t) => {
|
|
23
|
+
const cwd = scratch(t);
|
|
24
|
+
write(cwd, "tailwind.config.ts", "export default {}");
|
|
25
|
+
write(cwd, "web/tailwind.config.cjs", "module.exports = {}");
|
|
26
|
+
write(cwd, "src/styles/globals.css", ":root {\n --brand: #0a84ff;\n}\n");
|
|
27
|
+
write(cwd, "src/styles/dark.scss", "[data-theme='dark'] { --surface: #111; }");
|
|
28
|
+
write(cwd, "src/app.pcss", "@import 'tailwindcss';\n@theme {\n --color-ink: oklch(20% 0 0);\n}\n");
|
|
29
|
+
write(cwd, "src/theme-entry.css", "@theme inline;");
|
|
30
|
+
write(cwd, "src/design-tokens.json", "{}");
|
|
31
|
+
write(cwd, "src/tokens/Tokens.ts", "export const x = 1;");
|
|
32
|
+
write(cwd, "src/theme.ts", "export const theme = { colors: { primary: '#000' } };");
|
|
33
|
+
write(cwd, "src/plain.css", ".btn { color: red; background: var(--brand); }");
|
|
34
|
+
write(cwd, "src/commented.css", "/* :root { --x: 1px; } */\n.a { margin: 0; }");
|
|
35
|
+
write(cwd, "src/nested.css", ".card { --pad: 4px; }");
|
|
36
|
+
write(cwd, "src/theme.js", "export const spacing = { sm: 4 };");
|
|
37
|
+
write(cwd, "src/tokens.md", "# tokens");
|
|
38
|
+
write(cwd, "tailwind.config.json", "{}");
|
|
39
|
+
assert.deepEqual(detectDesignSources(cwd), [
|
|
40
|
+
"src/app.pcss",
|
|
41
|
+
"src/design-tokens.json",
|
|
42
|
+
"src/styles/dark.scss",
|
|
43
|
+
"src/styles/globals.css",
|
|
44
|
+
"src/theme-entry.css",
|
|
45
|
+
"src/theme.ts",
|
|
46
|
+
"src/tokens/Tokens.ts",
|
|
47
|
+
"tailwind.config.ts",
|
|
48
|
+
"web/tailwind.config.cjs",
|
|
49
|
+
]);
|
|
50
|
+
});
|
|
51
|
+
|
|
52
|
+
test("token files count only when named as design tokens, never auth or tokenizer files", (t) => {
|
|
53
|
+
const cwd = scratch(t);
|
|
54
|
+
write(cwd, "src/theme.tokens.css", ".a { margin: 0; }");
|
|
55
|
+
write(cwd, "src/tokens_v2.js", "export {};");
|
|
56
|
+
for (const path of ["src/auth/token.ts", "src/auth/refreshToken.ts", "src/csrf-token.js", "src/tokenizer.ts", "src/jwt-token.test.ts", "src/tokens.test.ts", "src/design-tokens.spec.js"]) write(cwd, path, "export {};");
|
|
57
|
+
assert.deepEqual(detectDesignSources(cwd), ["src/theme.tokens.css", "src/tokens_v2.js"]);
|
|
58
|
+
});
|
|
59
|
+
|
|
60
|
+
test("a project without design sources, or a missing cwd, has none", (t) => {
|
|
61
|
+
const cwd = scratch(t);
|
|
62
|
+
write(cwd, "src/index.ts", "export {};");
|
|
63
|
+
write(cwd, "src/plain.css", "body { margin: 0; }");
|
|
64
|
+
assert.deepEqual(detectDesignSources(cwd), []);
|
|
65
|
+
assert.deepEqual(detectDesignSources(join(cwd, "missing")), []);
|
|
66
|
+
});
|
|
67
|
+
|
|
68
|
+
test("the walk skips dependency, build, VCS, design and dot directories", (t) => {
|
|
69
|
+
const cwd = scratch(t);
|
|
70
|
+
const css = ":root { --x: 1px; }";
|
|
71
|
+
for (const dir of ["node_modules/pkg", ".git", "dist", "build", "out", "coverage", ".next", ".worktrees/other", "design", ".cache", "src/.hidden"]) {
|
|
72
|
+
write(cwd, `${dir}/tokens.json`, "{}");
|
|
73
|
+
write(cwd, `${dir}/vars.css`, css);
|
|
74
|
+
}
|
|
75
|
+
write(cwd, "packages/ui/design/tokens.json", "{}");
|
|
76
|
+
assert.deepEqual(detectDesignSources(cwd), ["packages/ui/design/tokens.json"], "only the root design/ is the design output");
|
|
77
|
+
});
|
|
78
|
+
|
|
79
|
+
test("symlinks are not followed, to files or directories", (t) => {
|
|
80
|
+
const cwd = scratch(t);
|
|
81
|
+
const outside = scratch(t);
|
|
82
|
+
write(outside, "shared/tokens.json", "{}");
|
|
83
|
+
write(outside, "vars.css", ":root { --x: 1px; }");
|
|
84
|
+
write(cwd, "src/index.ts", "");
|
|
85
|
+
symlinkSync(join(outside, "shared"), join(cwd, "src", "shared"));
|
|
86
|
+
symlinkSync(join(outside, "vars.css"), join(cwd, "src", "vars.css"));
|
|
87
|
+
symlinkSync(join(outside, "shared", "tokens.json"), join(cwd, "tokens.json"));
|
|
88
|
+
assert.deepEqual(detectDesignSources(cwd), []);
|
|
89
|
+
});
|
|
90
|
+
|
|
91
|
+
test("the depth cap holds: six directory levels are walked, the seventh is not", (t) => {
|
|
92
|
+
const cwd = scratch(t);
|
|
93
|
+
assert.equal(DESIGN_SOURCE_LIMITS.maxDepth, 6);
|
|
94
|
+
write(cwd, "a/b/c/d/e/f/tokens.json", "{}");
|
|
95
|
+
write(cwd, "a/b/c/d/e/f/g/tokens.json", "{}");
|
|
96
|
+
assert.deepEqual(detectDesignSources(cwd), ["a/b/c/d/e/f/tokens.json"]);
|
|
97
|
+
});
|
|
98
|
+
|
|
99
|
+
test("the file cap holds: the walk stops after 5000 files in name order", (t) => {
|
|
100
|
+
const cwd = scratch(t);
|
|
101
|
+
assert.equal(DESIGN_SOURCE_LIMITS.maxFiles, 5000);
|
|
102
|
+
for (let index = 0; index < 4999; index++) write(cwd, `a${String(index).padStart(4, "0")}.txt`);
|
|
103
|
+
write(cwd, "z-tokens.json", "{}");
|
|
104
|
+
assert.deepEqual(detectDesignSources(cwd), ["z-tokens.json"], "the 5000th file is still read");
|
|
105
|
+
write(cwd, "a4999.txt");
|
|
106
|
+
assert.deepEqual(detectDesignSources(cwd), [], "the 5001st file is not");
|
|
107
|
+
});
|
|
108
|
+
|
|
109
|
+
test("only the first 256 KiB of a stylesheet is read", (t) => {
|
|
110
|
+
const cwd = scratch(t);
|
|
111
|
+
assert.equal(DESIGN_SOURCE_LIMITS.maxBytes, 256 * 1024);
|
|
112
|
+
const filler = `/*${"x".repeat(256 * 1024)}*/\n`;
|
|
113
|
+
write(cwd, "late.css", `.a{}\n${filler}:root { --x: 1px; }`);
|
|
114
|
+
write(cwd, "early.css", `:root { --x: 1px; }\n${filler}`);
|
|
115
|
+
assert.deepEqual(detectDesignSources(cwd), ["early.css"]);
|
|
116
|
+
});
|
|
117
|
+
|
|
118
|
+
test("stale sources are the ones modified after design/system.md", (t) => {
|
|
119
|
+
const cwd = scratch(t);
|
|
120
|
+
write(cwd, "tailwind.config.js", "module.exports = {}");
|
|
121
|
+
write(cwd, "src/tokens.json", "{}");
|
|
122
|
+
const sources = detectDesignSources(cwd);
|
|
123
|
+
assert.deepEqual(staleDesignSources(cwd, sources), [], "no design/system.md, nothing is stale");
|
|
124
|
+
write(cwd, "design/system.md", "# Tokens");
|
|
125
|
+
utimesSync(join(cwd, "design", "system.md"), 2000, 2000);
|
|
126
|
+
utimesSync(join(cwd, "tailwind.config.js"), 1000, 1000);
|
|
127
|
+
utimesSync(join(cwd, "src", "tokens.json"), 3000, 3000);
|
|
128
|
+
assert.deepEqual(staleDesignSources(cwd, sources), ["src/tokens.json"]);
|
|
129
|
+
utimesSync(join(cwd, "src", "tokens.json"), 2000, 2000);
|
|
130
|
+
assert.deepEqual(staleDesignSources(cwd, sources), [], "an equal mtime is not newer");
|
|
131
|
+
assert.deepEqual(staleDesignSources(cwd, [...sources, "gone.css"]), [], "a source that vanished is skipped");
|
|
132
|
+
});
|
|
@@ -78,7 +78,91 @@ test("recognizes later compound destruction and shell wrappers without matching
|
|
|
78
78
|
});
|
|
79
79
|
|
|
80
80
|
test("root/home recursive removal and destructive git retain hard denial", () => {
|
|
81
|
-
for (const command of [`rm -fr /`, `rm -r "$HOME"`, `git -C /repo reset --hard`, `git push -f`, `git push ';' -f main`, `rm -r ';' /`]) {
|
|
81
|
+
for (const command of [`rm -fr /`, `rm -r "$HOME"`, `git -C /repo reset --hard`, `git push -f`, `git push ';' -f main`, `rm -r ';' /`, `rm -rf /tmp/tmp.abcdef1234 ~/foo`, `rm -rf /tmp/tmp.abcdef1234 /`]) {
|
|
82
82
|
assert.equal(recognizeDestructiveCommands(command)[0]?.hardDeny, true, command);
|
|
83
83
|
}
|
|
84
84
|
});
|
|
85
|
+
|
|
86
|
+
// gentle-pi#2285: a reserved word that opens a compound body is not the
|
|
87
|
+
// segment head, and backtick and literal `eval` payloads run like `$(...)`.
|
|
88
|
+
const compoundRemovals = [
|
|
89
|
+
`if true; then rm -rf data; fi`,
|
|
90
|
+
`for d in a b; do rm -rf "$d"; done`,
|
|
91
|
+
"echo `rm -rf data`",
|
|
92
|
+
`eval 'rm -rf data'`,
|
|
93
|
+
`if false; then :; else rm -rf data; fi`,
|
|
94
|
+
`if false; then :; elif rm -rf data; then :; fi`,
|
|
95
|
+
`while rm -rf data; do :; done`,
|
|
96
|
+
`until rm -rf data; do :; done`,
|
|
97
|
+
`if rm -rf data; then :; fi`,
|
|
98
|
+
`{ rm -rf data; }`,
|
|
99
|
+
`! rm -rf data`,
|
|
100
|
+
`if true; then sudo rm -rf data; fi`,
|
|
101
|
+
"echo \"now: `rm -rf data`\"",
|
|
102
|
+
"echo `true; rm -rf data`",
|
|
103
|
+
`eval "rm -rf data"`,
|
|
104
|
+
`eval rm -rf data`,
|
|
105
|
+
`eval -- 'rm -rf data'`,
|
|
106
|
+
`command eval 'rm -rf data'`,
|
|
107
|
+
`eval 'if true; then rm -rf data; fi'`,
|
|
108
|
+
`eval 'git reset --hard'`,
|
|
109
|
+
];
|
|
110
|
+
for (const command of compoundRemovals) {
|
|
111
|
+
test(`recognizes destruction after reserved words, in backticks and in eval: ${command}`, () => {
|
|
112
|
+
const matches = recognizeDestructiveCommands(command);
|
|
113
|
+
assert.ok(matches.length > 0, command);
|
|
114
|
+
assert.ok(matches.every((match) => match.triggerIndex >= 0 && match.triggerIndex < command.length), command);
|
|
115
|
+
assert.ok(matches.every((match) => match.removalTargets === undefined), command);
|
|
116
|
+
});
|
|
117
|
+
}
|
|
118
|
+
|
|
119
|
+
test("issue #2285 examples are recursive filesystem data loss like a plain rm", () => {
|
|
120
|
+
for (const command of [`if true; then rm -rf data; fi`, `for d in a b; do rm -rf "$d"; done`, "echo `rm -rf data`", `eval 'rm -rf data'`]) {
|
|
121
|
+
const matches = recognizeDestructiveCommands(command);
|
|
122
|
+
assert.ok(matches.some((match) => match.kind === "filesystem"), command);
|
|
123
|
+
assert.ok(matches.every((match) => !match.hardDeny), command);
|
|
124
|
+
}
|
|
125
|
+
});
|
|
126
|
+
|
|
127
|
+
test("root/home recursive removal behind reserved words, backticks or eval keeps its hard denial", () => {
|
|
128
|
+
for (const command of [
|
|
129
|
+
`if true; then rm -rf ~; fi`, `for d in a; do rm -rf /; done`, `{ rm -rf "$HOME"; }`, `! rm -rf ..`,
|
|
130
|
+
"echo `rm -rf ~`", `eval 'rm -rf /'`, `while true; do rm -rf ~/foo; done`,
|
|
131
|
+
]) {
|
|
132
|
+
assert.ok(recognizeDestructiveCommands(command).some((match) => match.hardDeny), command);
|
|
133
|
+
}
|
|
134
|
+
});
|
|
135
|
+
|
|
136
|
+
for (const command of [
|
|
137
|
+
`if [ -f x ]; then echo ok; fi`, `for f in *; do cat "$f"; done`, "echo `date`", `eval "$(ssh-agent -s)"`,
|
|
138
|
+
`grep "then rm -rf" x`, `echo 'then rm -rf data'`, "echo '`rm -rf data`'", "echo \\`rm -rf data\\`",
|
|
139
|
+
`eval 'echo rm -rf data'`, `eval "$cmd"`, `while read -r line; do echo "$line"; done < x`,
|
|
140
|
+
`if git diff --quiet; then git push origin main; fi`, "echo 'unterminated `rm -rf data'",
|
|
141
|
+
]) {
|
|
142
|
+
test(`leaves ordinary compound, backtick and eval commands unchanged: ${command}`, () => {
|
|
143
|
+
assert.deepEqual(recognizeDestructiveCommands(command), []);
|
|
144
|
+
});
|
|
145
|
+
}
|
|
146
|
+
|
|
147
|
+
// Only a direct, unwrapped, unquoted `rm` exposes its literal targets; the
|
|
148
|
+
// YOLO temp-scratch waiver has nothing to inspect anywhere else.
|
|
149
|
+
test("exposes literal removal targets only for a direct top-level rm that runs before every other segment", () => {
|
|
150
|
+
assert.deepEqual(recognizeDestructiveCommands(`rm -rf /tmp/tmp.abcdef1234 /tmp/x.log; git log -1`)[0]?.removalTargets, ["/tmp/tmp.abcdef1234", "/tmp/x.log"]);
|
|
151
|
+
assert.deepEqual(recognizeDestructiveCommands(`rm -r -f -- /tmp/tmp.abcdef1234`)[0]?.removalTargets, ["/tmp/tmp.abcdef1234"]);
|
|
152
|
+
assert.deepEqual(recognizeDestructiveCommands(`rm -rf /tmp/a && rm -r /tmp/b\n\nrm -r /tmp/c || true`).map((match) => match.removalTargets), [["/tmp/a"], ["/tmp/b"], ["/tmp/c"]]);
|
|
153
|
+
assert.deepEqual(recognizeDestructiveCommands(`rm -rf /tmp/a; mv x /tmp/b; rm -rf /tmp/b`).map((match) => match.removalTargets), [["/tmp/a"], undefined]);
|
|
154
|
+
for (const command of [
|
|
155
|
+
`sudo rm -rf /tmp/tmp.abcdef1234`, `env rm -rf /tmp/tmp.abcdef1234`, `X=1 rm -rf /tmp/tmp.abcdef1234`,
|
|
156
|
+
`xargs rm -rf /tmp/tmp.abcdef1234`, `/bin/rm -rf /tmp/tmp.abcdef1234`, `sh -c 'rm -rf /tmp/tmp.abcdef1234'`,
|
|
157
|
+
`find /tmp -name tmp.abcdef1234 -exec rm -rf {} +`, `rm -rf "$TMPDIR/x"`, `rm -rf '/tmp/tmp.abcdef1234'`,
|
|
158
|
+
`rm -rf --no-preserve-root /tmp/tmp.abcdef1234`, `rm -rf`,
|
|
159
|
+
`mv x /tmp/tmp.abcdef1234 && rm -rf /tmp/tmp.abcdef1234`, `cp -r x /tmp/tmp.abcdef1234; rm -rf /tmp/tmp.abcdef1234`,
|
|
160
|
+
`(rm -rf /tmp/tmp.abcdef1234)`, `rm -rf /tmp/tmp.abcdef1234 & true`, `rm -rf /tmp/tmp.abcdef1234 | cat`,
|
|
161
|
+
`{ rm -rf /tmp/tmp.abcdef1234; }`, `! rm -rf /tmp/tmp.abcdef1234`, `if true; then rm -rf /tmp/tmp.abcdef1234; fi`,
|
|
162
|
+
"echo `rm -rf /tmp/tmp.abcdef1234`", `eval 'rm -rf /tmp/tmp.abcdef1234'`, `eval rm -rf /tmp/tmp.abcdef1234`,
|
|
163
|
+
]) {
|
|
164
|
+
const matches = recognizeDestructiveCommands(command);
|
|
165
|
+
assert.ok(matches.length > 0, command);
|
|
166
|
+
assert.ok(matches.every((match) => match.removalTargets === undefined), command);
|
|
167
|
+
}
|
|
168
|
+
});
|
|
@@ -16,7 +16,7 @@ const OTHER_ID = "fedcba9876543210fedcba9876543210";
|
|
|
16
16
|
const READY = { revision: 1, source: "active", status: "ready", offline_automatic_opening_authorized: false, active_reuse_authorized: true,
|
|
17
17
|
instance_id: ID, runtime_id: "aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa", capability: "startup-verified-store-v1" };
|
|
18
18
|
const REFUSED: shell.EngramProbe = { kind: "refused" };
|
|
19
|
-
const CORE = "3.
|
|
19
|
+
const CORE = "3.5.2";
|
|
20
20
|
const listening = (compatible: boolean, healthInstanceId?: string, healthVersion = CORE): shell.EngramProbe =>
|
|
21
21
|
({ kind: "listening", compatible, healthInstanceId, healthVersion: healthInstanceId === undefined ? undefined : healthVersion });
|
|
22
22
|
|
|
@@ -28,59 +28,152 @@ const clean = (extra: Record<string, string> = {}) => {
|
|
|
28
28
|
return env;
|
|
29
29
|
};
|
|
30
30
|
const classify = (input: Partial<Parameters<typeof shell.classifyEngramStartup>[0]> = {}) =>
|
|
31
|
-
shell.classifyEngramStartup({ env: {},
|
|
31
|
+
shell.classifyEngramStartup({ env: {}, endpoint: REFUSED, core: true, coreVersion: CORE, ...input });
|
|
32
32
|
|
|
33
|
-
test("
|
|
34
|
-
|
|
33
|
+
test("shared Engram environment drops only session scratch temp paths", () => {
|
|
34
|
+
for (const platform of ["win32", "linux"]) {
|
|
35
|
+
for (const prefix of ["gp", "ga"]) {
|
|
36
|
+
const scratch = platform === "win32" ? `C:\\Temp\\${prefix}Ab12Z9\\` : `/tmp/${prefix}Ab12Z9/`;
|
|
37
|
+
const base = { TEMP: scratch, TMP: scratch, TMPDIR: scratch, PATH: "keep", OTHER: scratch };
|
|
38
|
+
assert.deepEqual(shell.sharedEngramEnv(base, platform), { ...(platform === "win32" ? { TEMP: "C:\\Temp", TMP: "C:\\Temp", TMPDIR: "C:\\Temp" } : {}), PATH: "keep", OTHER: scratch, ENGRAM_NO_UPDATE_CHECK: "1" });
|
|
39
|
+
assert.equal(base.TEMP, scratch, "does not mutate parent");
|
|
40
|
+
}
|
|
41
|
+
for (const chosen of ["/tmp/custom", "/tmp/gp12345", "/tmp/ga1234567", "/tmp/gp123456/child", "/tmp/gp12-456"]) {
|
|
42
|
+
assert.equal(shell.sharedEngramEnv({ TEMP: chosen }, platform).TEMP, chosen);
|
|
43
|
+
}
|
|
44
|
+
}
|
|
45
|
+
assert.deepEqual(shell.sharedEngramEnv({ Temp: "C:\\Temp\\gp123456\\gaAb12Z9\\", tMp: "D:/custom", tmpdir: "C:/chosen" }, "win32"),
|
|
46
|
+
{ Temp: "C:\\Temp", tMp: "D:/custom", tmpdir: "C:/chosen", ENGRAM_NO_UPDATE_CHECK: "1" });
|
|
47
|
+
for (const key of ["TMP", "TMPDIR"]) {
|
|
48
|
+
assert.deepEqual(shell.sharedEngramEnv({ [key]: "C:\\Temp\\gp123456" }, "win32"),
|
|
49
|
+
{ [key]: "C:\\Temp", TEMP: "C:\\Temp", ENGRAM_NO_UPDATE_CHECK: "1" });
|
|
50
|
+
assert.equal(shell.sharedEngramEnv({ [key]: "C:\\Temp\\gp123456", Temp: "D:\\chosen" }, "win32").Temp, "D:\\chosen");
|
|
51
|
+
}
|
|
52
|
+
for (const invalid of ["gp123456", "relative\\ga123456", "C:gp123456", "\\gp123456"]) {
|
|
53
|
+
assert.throws(() => shell.sharedEngramEnv({ TEMP: invalid }, "win32"), /absolute.*scratch/i);
|
|
54
|
+
}
|
|
55
|
+
assert.equal(shell.sharedEngramEnv({ tmp: "/tmp/gp123456" }, "linux").tmp, "/tmp/gp123456");
|
|
56
|
+
});
|
|
57
|
+
|
|
58
|
+
test("shared Engram environment detects Windows scratch after a trailing dot", () => {
|
|
59
|
+
assert.equal(shell.sharedEngramEnv({ TEMP: "C:\\Temp\\gp123456\\." }, "win32").TEMP, "C:\\Temp");
|
|
60
|
+
});
|
|
61
|
+
|
|
62
|
+
test("shared Engram environment detects POSIX scratch after child/..", () => {
|
|
63
|
+
assert.deepEqual(shell.sharedEngramEnv({ TMPDIR: "/tmp/gp123456/child/.." }, "linux"), { ENGRAM_NO_UPDATE_CHECK: "1" });
|
|
35
64
|
});
|
|
36
65
|
|
|
37
|
-
test("
|
|
66
|
+
test("shared Engram environment refuses original Windows .. even when normalization hides it", () => {
|
|
67
|
+
assert.throws(() => shell.sharedEngramEnv({ TEMP: "C:\\Temp\\gp123456\\child\\.." }, "win32"), /absolute.*scratch/i);
|
|
68
|
+
});
|
|
69
|
+
|
|
70
|
+
test("shared Engram environment refuses scratch-shaped UNC share roots without looping", () => {
|
|
71
|
+
const script = `import assert from 'node:assert/strict';
|
|
72
|
+
import { sharedEngramEnv } from './lib/gentle-shell-bundled.ts';
|
|
73
|
+
assert.throws(() => sharedEngramEnv({ TEMP: ${JSON.stringify("\\\\server\\gp123456\\")} }, 'win32'), /absolute.*scratch/i);`;
|
|
74
|
+
const result = spawnSync(process.execPath, ["--disable-warning=ExperimentalWarning", "--experimental-strip-types", "--input-type=module", "-e", script], { cwd: repository, encoding: "utf8", timeout: 3000 });
|
|
75
|
+
assert.equal(result.error, undefined);
|
|
76
|
+
assert.equal(result.status, 0);
|
|
77
|
+
assert.equal(result.stdout, "");
|
|
78
|
+
assert.equal(result.stderr, "");
|
|
79
|
+
assert.equal(shell.sharedEngramEnv({ TEMP: "\\\\server\\share\\gp123456\\gaAb12Z9\\" }, "win32").TEMP, "\\\\server\\share\\");
|
|
80
|
+
assert.equal(shell.sharedEngramEnv({ TEMP: "C:\\gp123456\\" }, "win32").TEMP, "C:\\");
|
|
81
|
+
});
|
|
82
|
+
|
|
83
|
+
test("shared Engram environment reaches an actual Windows Node child with explicit stable TEMP", { skip: process.platform !== "win32" }, () => {
|
|
84
|
+
const stable = "C:\\chosen-temp";
|
|
85
|
+
const scratch = `${stable}\\gp123456\\gaAb12Z9`;
|
|
86
|
+
const probe = `const os = require('node:os'); process.stdout.write(JSON.stringify({TEMP: process.env.TEMP, TMP: process.env.TMP, TMPDIR: process.env.TMPDIR, tmpdir: os.tmpdir()}));`;
|
|
87
|
+
// Set known ambient scratch only inside an isolated parent process. No files
|
|
88
|
+
// are created: neither chosen path needs to exist for this zero-write probe.
|
|
89
|
+
const script = `import { sharedEngramEnv } from './lib/gentle-shell-bundled.ts';
|
|
90
|
+
import { spawnSync } from 'node:child_process';
|
|
91
|
+
process.env.TEMP = ${JSON.stringify(scratch)};
|
|
92
|
+
const env = sharedEngramEnv({ TMPDIR: ${JSON.stringify(scratch)} });
|
|
93
|
+
const child = spawnSync(process.execPath, ['-e', ${JSON.stringify(probe)}], {env, encoding: 'utf8'});
|
|
94
|
+
process.stdout.write(child.stdout); process.stderr.write(child.stderr); process.exit(child.status ?? 99);`;
|
|
95
|
+
const result = spawnSync(process.execPath, ["--disable-warning=ExperimentalWarning", "--experimental-strip-types", "--input-type=module", "-e", script], { cwd: repository, encoding: "utf8", timeout: 10000 });
|
|
96
|
+
assert.equal(result.error, undefined);
|
|
97
|
+
assert.equal(result.status, 0);
|
|
98
|
+
assert.equal(result.stderr, "");
|
|
99
|
+
assert.equal(result.stdout, JSON.stringify({ TEMP: stable, TMPDIR: stable, tmpdir: stable }));
|
|
100
|
+
});
|
|
101
|
+
|
|
102
|
+
test("shared Engram environment retains sanitization and detached spawn wiring", () => {
|
|
103
|
+
assert.deepEqual(shell.sharedEngramEnv({ ENGRAM_BIN: "chosen", ENGRAM_OTHER: "value", ENGRAM_NO_UPDATE_CHECK: "0", GH_TOKEN: "token", GITHUB_TOKEN: "token", HOME: "home", TEMP: "chosen" }),
|
|
104
|
+
{ HOME: "home", TEMP: "chosen", ENGRAM_NO_UPDATE_CHECK: "1" });
|
|
105
|
+
const launcher = readFileSync(join(repository, "bin", "gentle-shell.mjs"), "utf8");
|
|
106
|
+
assert.match(launcher, /const env = sharedEngramEnv\(process\.env\);/);
|
|
107
|
+
assert.match(launcher, /spawn\(core, \["serve"\], \{ cwd: homedir\(\), env, detached: true, stdio: "ignore", windowsHide: true \}\)/);
|
|
108
|
+
});
|
|
109
|
+
|
|
110
|
+
test("nothing listening with a verified bundled core starts the shared server and reuses it through the default URL", () => {
|
|
111
|
+
assert.deepEqual(classify(), { policy: "allow", url: "http://127.0.0.1:7437", spawn: true });
|
|
112
|
+
});
|
|
113
|
+
|
|
114
|
+
test("nothing listening without the verified bundled core is disabled, never a PATH fallback", () => {
|
|
38
115
|
const result = classify({ core: false });
|
|
39
116
|
assert.equal(result.policy, "disabled");
|
|
117
|
+
assert.equal(result.spawn, undefined);
|
|
40
118
|
assert.match(result.notice!, /bundled Engram core/);
|
|
41
119
|
});
|
|
42
120
|
|
|
43
|
-
test("existing
|
|
44
|
-
for (const
|
|
45
|
-
|
|
46
|
-
|
|
121
|
+
test("an existing store no longer disables memory by itself; only a newer schema header keeps the server stopped", () => {
|
|
122
|
+
for (const userVersion of [undefined, 0, 1]) {
|
|
123
|
+
assert.deepEqual(classify({ userVersion }), { policy: "allow", url: "http://127.0.0.1:7437", spawn: true }, String(userVersion));
|
|
124
|
+
}
|
|
125
|
+
const newer = classify({ userVersion: 2 });
|
|
126
|
+
assert.deepEqual([newer.policy, newer.spawn, newer.url], ["disabled", undefined, undefined]);
|
|
127
|
+
assert.equal(newer.notice, shell.ENGRAM_NEWER_SCHEMA_NOTICE);
|
|
128
|
+
assert.match(newer.notice!, /newer Engram.*Back up/s);
|
|
129
|
+
// A pin without a verified schema cannot rule out a newer store: never started.
|
|
130
|
+
assert.equal(classify({ coreVersion: "9.9.9", userVersion: 1 }).policy, "disabled");
|
|
131
|
+
assert.equal(classify({ coreVersion: "9.9.9" }).spawn, true);
|
|
132
|
+
});
|
|
133
|
+
|
|
134
|
+
test("a listener that does not verify keeps memory disabled and is never replaced", () => {
|
|
135
|
+
for (const endpoint of [listening(false), listening(false, ID), { kind: "unknown" } as shell.EngramProbe]) {
|
|
136
|
+
const result = classify({ endpoint });
|
|
137
|
+
assert.deepEqual([result.policy, result.spawn, result.url], ["disabled", undefined, undefined], JSON.stringify(endpoint));
|
|
47
138
|
assert.match(result.notice!, /could not verify.*compatibility.*Back up/is);
|
|
48
|
-
assert.doesNotMatch(result.notice!, /update Engram and restart/i);
|
|
49
139
|
}
|
|
50
|
-
|
|
140
|
+
// Not even the bundled core version on /health stands in for /compatibility.
|
|
141
|
+
assert.equal(classify({ endpoint: listening(false, ID, CORE) }).policy, "disabled");
|
|
51
142
|
});
|
|
52
143
|
|
|
53
144
|
test("a running server older than the first compatibility-capable core is named in the notice, without changing the decision", () => {
|
|
54
145
|
for (const version of ["3.3.2", "v3.3.2", "2.9.0"]) {
|
|
55
|
-
const result = classify({
|
|
146
|
+
const result = classify({ endpoint: listening(false, OTHER_ID, version) });
|
|
56
147
|
assert.equal(result.policy, "disabled", version);
|
|
57
148
|
assert.equal(result.url, undefined, version);
|
|
58
149
|
assert.match(result.notice!, new RegExp(`reports version ${version.replace(/^v/, "").replaceAll(".", "\\.")}`), version);
|
|
59
150
|
assert.match(result.notice!, /update that server to 3\.4\.0 or later.*restart it/s, version);
|
|
60
151
|
assert.match(result.notice!, /Back up/, version);
|
|
61
152
|
}
|
|
62
|
-
// Shell's own store behind an older server gets the same guidance and stays disabled.
|
|
63
|
-
const own = classify({ store: "present", provenance: { instanceId: ID }, endpoint: listening(false, ID, "3.3.2") });
|
|
64
|
-
assert.equal(own.policy, "disabled");
|
|
65
|
-
assert.match(own.notice!, /reports version 3\.3\.2/);
|
|
66
153
|
});
|
|
67
154
|
|
|
68
155
|
test("a current, unparsable or hostile server version keeps the generic notice and is never echoed", () => {
|
|
69
156
|
for (const version of ["3.4.0", "3.10.0", "4.0.0", "dev", "", "3.3.2\u001b[31m", "3.3.2 update now", "03.3.2", "3.03.2", "3.3.02", "v03.3.2"]) {
|
|
70
|
-
const result = classify({
|
|
157
|
+
const result = classify({ endpoint: listening(false, OTHER_ID, version) });
|
|
71
158
|
assert.equal(result.policy, "disabled", JSON.stringify(version));
|
|
72
159
|
assert.equal(result.notice, shell.ENGRAM_RECOVERY_NOTICE, JSON.stringify(version));
|
|
73
160
|
}
|
|
74
|
-
assert.equal(classify({
|
|
161
|
+
assert.equal(classify({ endpoint: listening(false) }).notice, shell.ENGRAM_RECOVERY_NOTICE);
|
|
75
162
|
});
|
|
76
163
|
|
|
77
164
|
test("explicit selections and invalid policies are disabled without depending on probes", () => {
|
|
78
|
-
for (const key of ["ENGRAM_BIN", "ENGRAM_URL", "ENGRAM_DATA_DIR", "ENGRAM_PORT", "ENGRAM_DATABASE_URL"]) {
|
|
79
|
-
|
|
165
|
+
for (const key of ["ENGRAM_BIN", "ENGRAM_URL", "ENGRAM_DATA_DIR", "ENGRAM_PORT", "ENGRAM_SOCKET", "ENGRAM_DATABASE_URL", "ENGRAM_SERVER_MODE"]) {
|
|
166
|
+
for (const endpoint of [listening(true), REFUSED]) {
|
|
167
|
+
const result = classify({ env: { [key]: "chosen" }, endpoint });
|
|
168
|
+
assert.deepEqual([result.policy, result.spawn], ["disabled", undefined], key);
|
|
169
|
+
}
|
|
80
170
|
}
|
|
81
|
-
assert.equal(classify({ env: { ENGRAM_STARTUP_POLICY: "allow" } }).
|
|
171
|
+
assert.equal(classify({ env: { ENGRAM_STARTUP_POLICY: "allow" } }).spawn, true);
|
|
82
172
|
for (const value of ["", "disabled", "ALLOW", " allow ", "invalid"]) {
|
|
83
|
-
|
|
173
|
+
for (const endpoint of [listening(true), REFUSED]) {
|
|
174
|
+
const result = classify({ env: { ENGRAM_STARTUP_POLICY: value }, endpoint });
|
|
175
|
+
assert.deepEqual([result.policy, result.spawn], ["disabled", undefined], value);
|
|
176
|
+
}
|
|
84
177
|
}
|
|
85
178
|
assert.equal(shell.engramProbeAllowed({ ENGRAM_URL: "https://chosen.invalid" }), false);
|
|
86
179
|
assert.equal(shell.engramProbeAllowed({ ENGRAM_STARTUP_POLICY: "" }), false);
|
|
@@ -88,25 +181,12 @@ test("explicit selections and invalid policies are disabled without depending on
|
|
|
88
181
|
});
|
|
89
182
|
|
|
90
183
|
test("a verified /compatibility contract reuses the running server through an explicit URL", () => {
|
|
91
|
-
|
|
92
|
-
for (const
|
|
93
|
-
assert.
|
|
184
|
+
// Reused unchanged, whatever the store header or the bundled core.
|
|
185
|
+
for (const input of [{}, { userVersion: 2 }, { core: false }]) {
|
|
186
|
+
assert.deepEqual(classify({ ...input, endpoint: listening(true) }), { policy: "allow", url: "http://127.0.0.1:7437" }, JSON.stringify(input));
|
|
94
187
|
}
|
|
95
188
|
});
|
|
96
189
|
|
|
97
|
-
test("Shell provenance allows its own store only for the same instance and the same core binary", () => {
|
|
98
|
-
const provenance = { instanceId: ID };
|
|
99
|
-
assert.deepEqual(classify({ store: "present", provenance }), { policy: "allow" });
|
|
100
|
-
assert.deepEqual(classify({ store: "present", provenance, endpoint: listening(false, ID) }), { policy: "allow" });
|
|
101
|
-
assert.equal(classify({ store: "present", provenance, endpoint: listening(false, OTHER_ID) }).policy, "disabled");
|
|
102
|
-
for (const version of ["3.3.3", "v3.4.0", "dev", ""]) {
|
|
103
|
-
assert.equal(classify({ store: "present", provenance, endpoint: listening(false, ID, version) }).policy, "disabled", version);
|
|
104
|
-
}
|
|
105
|
-
assert.equal(classify({ store: "present", provenance, endpoint: listening(false) }).policy, "disabled");
|
|
106
|
-
assert.equal(classify({ store: "present", provenance, endpoint: { kind: "unknown" } }).policy, "disabled");
|
|
107
|
-
assert.equal(classify({ store: "present", provenance, core: false }).policy, "disabled");
|
|
108
|
-
});
|
|
109
|
-
|
|
110
190
|
test("the compatibility contract must match exactly", () => {
|
|
111
191
|
assert.equal(shell.isReadyCompatibility(JSON.stringify(READY)), true);
|
|
112
192
|
assert.equal(shell.isReadyCompatibility(JSON.stringify({ ...READY, offline_automatic_opening_authorized: undefined })), true);
|
|
@@ -161,20 +241,11 @@ test("the SQLite header user_version is read from raw bytes and malformed header
|
|
|
161
241
|
}
|
|
162
242
|
});
|
|
163
243
|
|
|
164
|
-
test("provenance matches only the same directory, instance and core digest", () => {
|
|
165
|
-
const record = { schema: 1 as const, dataDir: "/home/ana/.engram", instanceId: ID, coreSha256: "a".repeat(64) };
|
|
166
|
-
const observed = { dataDir: record.dataDir, instanceId: ID, coreSha256: record.coreSha256 };
|
|
167
|
-
assert.equal(shell.engramProvenanceMatches(record, observed), true);
|
|
168
|
-
for (const change of [{ dataDir: "/elsewhere/.engram" }, { instanceId: OTHER_ID }, { coreSha256: "b".repeat(64) }, { instanceId: undefined }, { coreSha256: undefined }]) {
|
|
169
|
-
assert.equal(shell.engramProvenanceMatches(record, { ...observed, ...change }), false, JSON.stringify(change));
|
|
170
|
-
}
|
|
171
|
-
assert.equal(shell.engramProvenanceMatches(undefined, observed), false);
|
|
172
|
-
});
|
|
173
|
-
|
|
174
244
|
test("the header is a newer-schema tripwire against the verified bundled schema, never a compatibility proof", () => {
|
|
175
245
|
// v3.3.2:internal/store/store.go declares schemaVersion = 1.
|
|
176
246
|
assert.equal(shell.bundledEngramSchema("3.3.2"), 1);
|
|
177
247
|
assert.equal(shell.bundledEngramSchema("3.4.0"), 1);
|
|
248
|
+
assert.equal(shell.bundledEngramSchema("3.5.2"), 1);
|
|
178
249
|
assert.equal(shell.bundledEngramSchema("3.3.3"), undefined);
|
|
179
250
|
for (const userVersion of [0, 1]) assert.equal(shell.engramHeaderAllows(userVersion, 1), true, String(userVersion));
|
|
180
251
|
assert.equal(shell.engramHeaderAllows(2, 1), false);
|
|
@@ -241,10 +312,10 @@ test("probe reports refused for a closed port and unknown for a silent listener"
|
|
|
241
312
|
assert.ok(Date.now() - started < 2000);
|
|
242
313
|
});
|
|
243
314
|
|
|
244
|
-
const base = { node: "/node", cli: "/pi", prefix: "/prefix", engram: "/prefix/runtime/engram-3.
|
|
315
|
+
const base = { node: "/node", cli: "/pi", prefix: "/prefix", engram: "/prefix/runtime/engram-3.5.2/engram", guard: GUARD };
|
|
245
316
|
|
|
246
317
|
test("disabled memory without explicit selections points ENGRAM_BIN at the guard and keeps the policy", () => {
|
|
247
|
-
const memory = classify({
|
|
318
|
+
const memory = classify({ endpoint: listening(false) });
|
|
248
319
|
const env = shell.bundledChildEnv({ ENGRAM_STARTUP_NOTICE: "display only" }, { ...base, memory });
|
|
249
320
|
assert.equal(env.ENGRAM_BIN, GUARD);
|
|
250
321
|
assert.equal(env.ENGRAM_URL, undefined);
|
|
@@ -254,7 +325,7 @@ test("disabled memory without explicit selections points ENGRAM_BIN at the guard
|
|
|
254
325
|
});
|
|
255
326
|
|
|
256
327
|
test("explicit ENGRAM_BIN or ENGRAM_URL are never replaced or removed", () => {
|
|
257
|
-
const memory = classify({
|
|
328
|
+
const memory = classify({ endpoint: listening(false) });
|
|
258
329
|
for (const selected of [{ ENGRAM_BIN: "/explicit" }, { ENGRAM_URL: "https://chosen.invalid" }, { ENGRAM_BIN: "/explicit", ENGRAM_URL: "https://chosen.invalid" }]) {
|
|
259
330
|
const before = { ...selected };
|
|
260
331
|
const env = shell.bundledChildEnv(selected, { ...base, memory });
|
|
@@ -267,11 +338,15 @@ test("explicit ENGRAM_BIN or ENGRAM_URL are never replaced or removed", () => {
|
|
|
267
338
|
assert.equal(shell.bundledChildEnv({ ENGRAM_URL: "https://chosen.invalid" }, { ...base, engram: undefined, memory }).ENGRAM_BIN, GUARD);
|
|
268
339
|
});
|
|
269
340
|
|
|
270
|
-
test("allowed
|
|
271
|
-
|
|
272
|
-
|
|
273
|
-
|
|
274
|
-
|
|
341
|
+
test("allowed memory always reaches the shared server through the reuse URL and the guard, never a session-owned server", () => {
|
|
342
|
+
for (const memory of [classify(), classify({ endpoint: listening(true) })]) {
|
|
343
|
+
const env = shell.bundledChildEnv({}, { ...base, memory });
|
|
344
|
+
assert.deepEqual([env.ENGRAM_URL, env.ENGRAM_BIN, env.ENGRAM_STARTUP_POLICY, env.ENGRAM_SERVER_MODE], ["http://127.0.0.1:7437", GUARD, "allow", undefined]);
|
|
345
|
+
// The user's own server mode, if any, is passed through untouched.
|
|
346
|
+
assert.equal(shell.bundledChildEnv({ ENGRAM_SERVER_MODE: "shared" }, { ...base, memory }).ENGRAM_SERVER_MODE, "shared");
|
|
347
|
+
}
|
|
348
|
+
assert.equal("ENGRAM_PRIVATE_REQUIRED_NOTICE" in shell, false);
|
|
349
|
+
assert.equal("supportsEngramSessionOwned" in shell, false);
|
|
275
350
|
const missing = shell.bundledChildEnv({}, { ...base, engram: undefined, memory: classify({ core: false }) });
|
|
276
351
|
assert.equal(missing.ENGRAM_BIN, GUARD);
|
|
277
352
|
assert.throws(() => shell.bundledChildEnv({}, { ...base, guard: undefined, memory: classify() }), /guard/);
|
|
@@ -309,7 +384,7 @@ test("the guard fails non-zero, reports the notice and leaves Engram data byte-i
|
|
|
309
384
|
});
|
|
310
385
|
|
|
311
386
|
test("a provider-style instance-id spawn against the environment Shell builds fails closed", { skip: process.platform === "win32" }, () => {
|
|
312
|
-
const env = shell.bundledChildEnv(clean(), { ...base, memory: classify({
|
|
387
|
+
const env = shell.bundledChildEnv(clean(), { ...base, memory: classify({ endpoint: listening(false) }) });
|
|
313
388
|
const result = spawnSync(env.ENGRAM_BIN!, ["instance-id"], { env, encoding: "utf8", timeout: 5000 });
|
|
314
389
|
assert.notEqual(result.status, 0);
|
|
315
390
|
assert.doesNotMatch(result.stdout, /^[a-f0-9]{32}$/m);
|
|
@@ -0,0 +1,84 @@
|
|
|
1
|
+
import assert from "node:assert/strict";
|
|
2
|
+
import { spawnSync } from "node:child_process";
|
|
3
|
+
import { mkdtempSync, writeFileSync, rmSync } from "node:fs";
|
|
4
|
+
import { join, relative } from "node:path";
|
|
5
|
+
import test from "node:test";
|
|
6
|
+
import { openInExternalEditor } from "../lib/external-editor.ts";
|
|
7
|
+
|
|
8
|
+
function fixture(result: Partial<ReturnType<typeof spawnSync>> = { status: 0 }) {
|
|
9
|
+
const events: string[] = [];
|
|
10
|
+
const host = { stop: () => events.push("stop"), start: () => events.push("start"), requestRender: (force?: boolean) => events.push(`render:${force}`) };
|
|
11
|
+
type Options = { cwd?: string; stdio: string; shell: boolean };
|
|
12
|
+
const calls: { command: string; args: string[]; options: Options }[] = [];
|
|
13
|
+
const spawn = ((command: string, args: string[], options: Options) => {
|
|
14
|
+
events.push("spawn");
|
|
15
|
+
calls.push({ command, args, options });
|
|
16
|
+
return result;
|
|
17
|
+
}) as typeof spawnSync;
|
|
18
|
+
return { host, spawn, calls, events };
|
|
19
|
+
}
|
|
20
|
+
|
|
21
|
+
test("editor paths remain single arguments without a shell", () => {
|
|
22
|
+
const f = fixture();
|
|
23
|
+
const path = "a&echo GS_2426_EXECUTED&rem tail.txt";
|
|
24
|
+
assert.equal(openInExternalEditor(f.host, path, { VISUAL: "editor.exe -w", EDITOR: "ignored" }, f.spawn, "/linked"), true);
|
|
25
|
+
assert.deepEqual(f.calls, [{ command: "editor.exe", args: ["-w", path], options: { cwd: "/linked", stdio: "inherit", shell: false } }]);
|
|
26
|
+
assert.deepEqual(f.events, ["stop", "spawn", "start", "render:true"]);
|
|
27
|
+
});
|
|
28
|
+
|
|
29
|
+
test("Windows batch editors are refused before spawning and name a direct executable alternative", { skip: process.platform !== "win32" }, () => {
|
|
30
|
+
for (const editor of ["editor.cmd", "editor.BAT"]) {
|
|
31
|
+
const f = fixture();
|
|
32
|
+
assert.throws(() => openInExternalEditor(f.host, "safe.txt", { EDITOR: editor }, f.spawn), /executable.*\.exe/i);
|
|
33
|
+
assert.deepEqual(f.calls, []);
|
|
34
|
+
assert.deepEqual(f.events, []);
|
|
35
|
+
}
|
|
36
|
+
});
|
|
37
|
+
|
|
38
|
+
test("editor launch failures are reported after restoring the TUI", () => {
|
|
39
|
+
for (const result of [{ error: new Error("ENOENT"), status: null }, { status: 7 }, { status: null, signal: "SIGTERM" as const }]) {
|
|
40
|
+
const f = fixture(result);
|
|
41
|
+
assert.throws(() => openInExternalEditor(f.host, "safe.txt", { EDITOR: "editor.exe" }, f.spawn), /Could not open editor/);
|
|
42
|
+
assert.deepEqual(f.events, ["stop", "spawn", "start", "render:true"]);
|
|
43
|
+
}
|
|
44
|
+
});
|
|
45
|
+
|
|
46
|
+
test("editor retains VISUAL precedence, flags, cwd, and missing-editor behavior", () => {
|
|
47
|
+
const f = fixture();
|
|
48
|
+
assert.equal(openInExternalEditor(f.host, "lib/a.ts", { VISUAL: "nvim -u none", EDITOR: "vi" }, f.spawn, "/linked"), true);
|
|
49
|
+
assert.equal(f.calls[0].command, "nvim");
|
|
50
|
+
assert.deepEqual(f.calls[0].args, ["-u", "none", "lib/a.ts"]);
|
|
51
|
+
assert.equal(f.calls[0].options.cwd, "/linked");
|
|
52
|
+
assert.equal(openInExternalEditor(f.host, "lib/a.ts", {}, f.spawn), false);
|
|
53
|
+
assert.deepEqual(f.events, ["stop", "spawn", "start", "render:true"]);
|
|
54
|
+
});
|
|
55
|
+
|
|
56
|
+
test("a real editor process receives a metacharacter filename unchanged", () => {
|
|
57
|
+
const root = mkdtempSync(join(process.cwd(), ".editor-test-"));
|
|
58
|
+
try {
|
|
59
|
+
const script = join(root, "editor.cjs");
|
|
60
|
+
writeFileSync(script, "process.stdout.write(JSON.stringify(process.argv.slice(2)))");
|
|
61
|
+
const path = join(root, "a&echo GS_2426_EXECUTED&rem tail.txt");
|
|
62
|
+
writeFileSync(path, "unchanged");
|
|
63
|
+
const f = fixture();
|
|
64
|
+
let child: ReturnType<typeof spawnSync> | undefined;
|
|
65
|
+
const spawn = ((command: string, args: string[], options: object) => {
|
|
66
|
+
child = spawnSync(command, args, { ...options, stdio: "pipe", encoding: "utf8", timeout: 3000 });
|
|
67
|
+
return child;
|
|
68
|
+
}) as typeof spawnSync;
|
|
69
|
+
assert.equal(openInExternalEditor(f.host, path, { EDITOR: `node ${relative(process.cwd(), script)}` }, spawn, process.cwd()), true);
|
|
70
|
+
assert.equal(child?.status, 0);
|
|
71
|
+
assert.equal(child?.stdout, JSON.stringify([path]));
|
|
72
|
+
assert.equal(child?.stderr, "");
|
|
73
|
+
assert.deepEqual(f.events, ["stop", "start", "render:true"]);
|
|
74
|
+
} finally {
|
|
75
|
+
rmSync(root, { recursive: true, force: true });
|
|
76
|
+
}
|
|
77
|
+
});
|
|
78
|
+
|
|
79
|
+
test("synchronous spawn exceptions restore the TUI before reporting failure", () => {
|
|
80
|
+
const f = fixture();
|
|
81
|
+
const spawn = (() => { throw new Error("spawn failed"); }) as typeof spawnSync;
|
|
82
|
+
assert.throws(() => openInExternalEditor(f.host, "safe.txt", { EDITOR: "editor.exe" }, spawn), { message: "Could not open editor: spawn failed" });
|
|
83
|
+
assert.deepEqual(f.events, ["stop", "start", "render:true"]);
|
|
84
|
+
});
|