@gotgenes/pi-permission-system 18.1.0 → 18.1.2

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.
Files changed (132) hide show
  1. package/CHANGELOG.md +23 -0
  2. package/docs/assets/logo.png +0 -0
  3. package/docs/assets/logo.svg +213 -0
  4. package/docs/configuration.md +700 -0
  5. package/docs/cross-extension-api.md +525 -0
  6. package/docs/guides/permission-frontmatter-for-subagent-extensions.md +201 -0
  7. package/docs/guides/upstream-issue-template.md +113 -0
  8. package/docs/migration/legacy-to-flat.md +365 -0
  9. package/docs/opencode-compatibility.md +213 -0
  10. package/docs/session-approvals.md +68 -0
  11. package/docs/subagent-integration.md +102 -0
  12. package/docs/troubleshooting.md +53 -0
  13. package/package.json +5 -2
  14. package/src/access-intent/access-path.ts +29 -4
  15. package/src/access-intent/bash/bash-path-resolver.ts +39 -18
  16. package/src/access-intent/bash/msys-bash-tokens.ts +64 -0
  17. package/src/path-normalizer.ts +105 -2
  18. package/test/access-intent/access-path.test.ts +0 -277
  19. package/test/access-intent/bash/node-text.test.ts +0 -148
  20. package/test/access-intent/bash/parser.test.ts +0 -19
  21. package/test/access-intent/bash/program.test.ts +0 -673
  22. package/test/access-intent/bash/token-classification.test.ts +0 -363
  23. package/test/access-intent/bash/token-collection.test.ts +0 -300
  24. package/test/active-agent.test.ts +0 -155
  25. package/test/async-cache.test.ts +0 -48
  26. package/test/bash-arity.test.ts +0 -144
  27. package/test/bash-external-directory.test.ts +0 -1022
  28. package/test/builtin-tool-input-formatters.test.ts +0 -109
  29. package/test/canonicalize-path.test.ts +0 -119
  30. package/test/composition-root.test.ts +0 -698
  31. package/test/config-loader.test.ts +0 -740
  32. package/test/config-modal.test.ts +0 -320
  33. package/test/config-paths.test.ts +0 -83
  34. package/test/config-pipeline.test.ts +0 -90
  35. package/test/config-reporter.test.ts +0 -147
  36. package/test/config-store.test.ts +0 -466
  37. package/test/decision-audit.test.ts +0 -72
  38. package/test/decision-reporter.test.ts +0 -112
  39. package/test/denial-messages.test.ts +0 -714
  40. package/test/detect-permissive-bash-fallback.test.ts +0 -56
  41. package/test/expand-home.test.ts +0 -93
  42. package/test/extension-config.test.ts +0 -129
  43. package/test/extension-paths.test.ts +0 -108
  44. package/test/forwarded-permissions/io.test.ts +0 -251
  45. package/test/forwarding-manager.test.ts +0 -200
  46. package/test/handlers/before-agent-start.test.ts +0 -314
  47. package/test/handlers/external-directory-integration.test.ts +0 -515
  48. package/test/handlers/external-directory-session-dedup.test.ts +0 -175
  49. package/test/handlers/external-directory-symlink-acceptance.test.ts +0 -167
  50. package/test/handlers/gates/bash-command-metamorphic.test.ts +0 -88
  51. package/test/handlers/gates/bash-command.test.ts +0 -257
  52. package/test/handlers/gates/bash-external-directory.test.ts +0 -268
  53. package/test/handlers/gates/bash-path.test.ts +0 -346
  54. package/test/handlers/gates/candidate-check.test.ts +0 -52
  55. package/test/handlers/gates/external-directory-messages.test.ts +0 -85
  56. package/test/handlers/gates/external-directory-policy.test.ts +0 -134
  57. package/test/handlers/gates/external-directory.test.ts +0 -267
  58. package/test/handlers/gates/helpers.test.ts +0 -165
  59. package/test/handlers/gates/path.test.ts +0 -369
  60. package/test/handlers/gates/runner.test.ts +0 -408
  61. package/test/handlers/gates/skill-input-gate-pipeline.test.ts +0 -176
  62. package/test/handlers/gates/skill-input.test.ts +0 -128
  63. package/test/handlers/gates/skill-read.test.ts +0 -161
  64. package/test/handlers/gates/tool-call-gate-pipeline.test.ts +0 -356
  65. package/test/handlers/gates/tool.test.ts +0 -244
  66. package/test/handlers/input-events.test.ts +0 -168
  67. package/test/handlers/input.test.ts +0 -199
  68. package/test/handlers/lifecycle.test.ts +0 -221
  69. package/test/handlers/tool-call-boundary.test.ts +0 -145
  70. package/test/handlers/tool-call-events.test.ts +0 -277
  71. package/test/handlers/tool-call.test.ts +0 -395
  72. package/test/handlers/validate-requested-tool.test.ts +0 -92
  73. package/test/helpers/external-directory-fixtures.ts +0 -269
  74. package/test/helpers/gate-fixtures.ts +0 -316
  75. package/test/helpers/handler-fixtures.ts +0 -335
  76. package/test/helpers/make-fake-pi.ts +0 -100
  77. package/test/helpers/manager-harness.ts +0 -112
  78. package/test/helpers/session-fixtures.ts +0 -199
  79. package/test/input-normalizer.test.ts +0 -325
  80. package/test/logging.test.ts +0 -51
  81. package/test/mcp-targets.test.ts +0 -233
  82. package/test/node-modules-discovery.test.ts +0 -97
  83. package/test/normalize.test.ts +0 -247
  84. package/test/path-containment.test.ts +0 -161
  85. package/test/path-normalization.test.ts +0 -233
  86. package/test/path-normalizer.test.ts +0 -196
  87. package/test/path-surfaces.test.ts +0 -55
  88. package/test/pattern-suggest.test.ts +0 -248
  89. package/test/permission-dialog.test.ts +0 -205
  90. package/test/permission-event-rpc.test.ts +0 -560
  91. package/test/permission-events.test.ts +0 -400
  92. package/test/permission-forwarder.test.ts +0 -370
  93. package/test/permission-forwarding.test.ts +0 -315
  94. package/test/permission-gate.test.ts +0 -269
  95. package/test/permission-manager-unified.test.ts +0 -3714
  96. package/test/permission-merge.test.ts +0 -61
  97. package/test/permission-prompter.test.ts +0 -518
  98. package/test/permission-prompts.test.ts +0 -363
  99. package/test/permission-resolver.test.ts +0 -277
  100. package/test/permission-session.test.ts +0 -404
  101. package/test/permission-ui-prompt.test.ts +0 -146
  102. package/test/permissions-service.test.ts +0 -192
  103. package/test/pi-infrastructure-read.test.ts +0 -432
  104. package/test/policy-loader.test.ts +0 -561
  105. package/test/prompting-gateway.test.ts +0 -231
  106. package/test/rule.test.ts +0 -650
  107. package/test/safe-system-paths.test.ts +0 -46
  108. package/test/scope-merge.test.ts +0 -116
  109. package/test/service-lifecycle.test.ts +0 -163
  110. package/test/service.test.ts +0 -261
  111. package/test/session-approval.test.ts +0 -75
  112. package/test/session-logger.test.ts +0 -200
  113. package/test/session-rules.test.ts +0 -321
  114. package/test/session-start.test.ts +0 -112
  115. package/test/skill-prompt-sanitizer.test.ts +0 -418
  116. package/test/status.test.ts +0 -10
  117. package/test/subagent-context.test.ts +0 -372
  118. package/test/subagent-lifecycle-events.test.ts +0 -132
  119. package/test/subagent-registry.test.ts +0 -145
  120. package/test/synthesize.test.ts +0 -302
  121. package/test/system-prompt-sanitizer.test.ts +0 -382
  122. package/test/tool-access-extractor-registry.test.ts +0 -77
  123. package/test/tool-input-formatter-registry.test.ts +0 -75
  124. package/test/tool-input-path.test.ts +0 -84
  125. package/test/tool-input-preview.test.ts +0 -129
  126. package/test/tool-input-prompt-formatters.test.ts +0 -115
  127. package/test/tool-preview-formatter.test.ts +0 -458
  128. package/test/tool-registry.test.ts +0 -197
  129. package/test/value-guards.test.ts +0 -193
  130. package/test/wildcard-matcher.test.ts +0 -424
  131. package/test/yaml-frontmatter.test.ts +0 -91
  132. package/test/yolo-mode.test.ts +0 -188
@@ -0,0 +1,64 @@
1
+ /**
2
+ * Pure shape classifier for a bash-command token on a win32 host, where Pi core
3
+ * executes commands through Git Bash and POSIX-shaped absolute tokens carry
4
+ * MSYS mount semantics rather than `node:path.win32` semantics.
5
+ *
6
+ * Consumed only by {@link PathNormalizer.forBashToken}; kept as a standalone
7
+ * module so the shape knowledge is unit-testable in isolation (no filesystem,
8
+ * no platform read).
9
+ */
10
+ import { isSafeSystemPath } from "#src/safe-system-paths";
11
+
12
+ /**
13
+ * The MSYS interpretation of a win32 bash token:
14
+ *
15
+ * - `device` — a safe MSYS runtime device (`/dev/null`, `/dev/std{in,out,err}`);
16
+ * never a filesystem path.
17
+ * - `drive-mount` — an MSYS drive mount (`/c/…`, `/d/…`); `windowsPath` is its
18
+ * deterministic Windows equivalent (`C:\…`).
19
+ * - `posix-absolute` — any other absolute POSIX path (`/tmp/foo`, `/usr/bin`);
20
+ * its Windows target is install-dependent and not deterministically knowable,
21
+ * so it is treated literally.
22
+ * - `plain` — everything else (relative tokens, `~/…`, native Windows drive
23
+ * paths); handled by ordinary win32 resolution.
24
+ */
25
+ export type Win32BashTokenKind =
26
+ | { kind: "device" }
27
+ | { kind: "drive-mount"; windowsPath: string }
28
+ | { kind: "posix-absolute" }
29
+ | { kind: "plain" };
30
+
31
+ /**
32
+ * A single-letter first path segment identifies an MSYS drive mount: `/c`,
33
+ * `/c/`, or `/c/rest`. A multi-letter first segment (`/dev`, `/tmp`) is not a
34
+ * mount. The device set is checked before this pattern, so `/dev/*` never
35
+ * reaches it.
36
+ */
37
+ const MSYS_DRIVE_MOUNT_PATTERN = /^\/([a-zA-Z])(\/.*)?$/;
38
+
39
+ export function classifyWin32BashToken(token: string): Win32BashTokenKind {
40
+ if (isSafeSystemPath(token)) return { kind: "device" };
41
+
42
+ const driveMatch = MSYS_DRIVE_MOUNT_PATTERN.exec(token);
43
+ if (driveMatch) {
44
+ return {
45
+ kind: "drive-mount",
46
+ windowsPath: toWindowsDrivePath(driveMatch[1], driveMatch[2]),
47
+ };
48
+ }
49
+
50
+ if (token.startsWith("/")) return { kind: "posix-absolute" };
51
+
52
+ return { kind: "plain" };
53
+ }
54
+
55
+ /**
56
+ * Build the Windows equivalent of an MSYS drive mount: uppercase drive letter,
57
+ * `:\`, and the remainder with `/` separators rewritten to `\`. A bare or
58
+ * trailing-slash mount (`/c`, `/c/`) maps to the drive root (`C:\`).
59
+ */
60
+ function toWindowsDrivePath(letter: string, rest: string | undefined): string {
61
+ const drive = `${letter.toUpperCase()}:`;
62
+ const tail = (rest ?? "").replace(/^\//, "").replaceAll("/", "\\");
63
+ return tail ? `${drive}\\${tail}` : `${drive}\\`;
64
+ }
@@ -1,9 +1,11 @@
1
1
  import { posix as posixPath, win32 as winPath } from "node:path";
2
2
 
3
3
  import { AccessPath } from "./access-intent/access-path";
4
+ import { classifyWin32BashToken } from "./access-intent/bash/msys-bash-tokens";
4
5
  import {
5
6
  canonicalNormalizePathForComparison,
6
7
  normalizePathForComparison,
8
+ normalizePathPolicyLiteral,
7
9
  } from "./access-intent/path-normalization";
8
10
  import {
9
11
  isPathOutsideWorkingDirectory,
@@ -11,6 +13,22 @@ import {
11
13
  } from "./path-containment";
12
14
  import { isPiInfrastructureRead } from "./pi-infrastructure-read";
13
15
 
16
+ /**
17
+ * The interpreted effect of a literal `cd` target on the effective base, under
18
+ * the host platform's (and, on win32, Git Bash's) semantics.
19
+ *
20
+ * - `absolute` — the target names a resolvable absolute base (`value`); an
21
+ * earlier unknown base is recovered.
22
+ * - `relative` — the target folds into the current base.
23
+ * - `unknown` — the target is not deterministically resolvable (a win32
24
+ * non-mount POSIX absolute like `cd /tmp`, or a device), so the base becomes
25
+ * conservatively unknown.
26
+ */
27
+ export type BashCdTarget =
28
+ | { readonly kind: "absolute"; readonly value: string }
29
+ | { readonly kind: "relative" }
30
+ | { readonly kind: "unknown" };
31
+
14
32
  /**
15
33
  * Path-interpretation collaborator, constructed once at the session edge with
16
34
  * the two ambient inputs — the host `platform` and the session `cwd` — baked
@@ -47,8 +65,46 @@ export class PathNormalizer {
47
65
  }
48
66
 
49
67
  /** Build a literal-only AccessPath (unknown base after a non-literal `cd`). */
50
- forLiteral(literal: string): AccessPath {
51
- return AccessPath.forLiteral(literal);
68
+ forLiteral(literal: string, matchAliases?: readonly string[]): AccessPath {
69
+ return AccessPath.forLiteral(literal, matchAliases);
70
+ }
71
+
72
+ /**
73
+ * Build an AccessPath for a bash-command token, applying Git Bash/MSYS
74
+ * semantics on a win32 host.
75
+ *
76
+ * Pi core always executes bash through Git Bash on Windows, so a POSIX-shaped
77
+ * absolute token carries MSYS semantics, not `node:path.win32` semantics. On
78
+ * win32 the recognized safe device paths (`/dev/null`, `/dev/std{in,out,err}`)
79
+ * are preserved verbatim as devices instead of being resolved into
80
+ * `c:\dev\null`, and MSYS drive mounts (`/c/…`) are translated to their
81
+ * Windows equivalent (`C:\…`) before resolution; every other token delegates
82
+ * to {@link forPath}. On POSIX this is a straight delegation to
83
+ * {@link forPath}.
84
+ */
85
+ forBashToken(token: string, options?: { resolveBase?: string }): AccessPath {
86
+ if (this.platform !== "win32") return this.forPath(token, options);
87
+
88
+ const shape = classifyWin32BashToken(token);
89
+ switch (shape.kind) {
90
+ case "device":
91
+ return AccessPath.forDevice(token);
92
+ case "drive-mount":
93
+ return this.forPath(shape.windowsPath, options);
94
+ case "posix-absolute": {
95
+ // A non-mount POSIX absolute (`/tmp`, `/usr`) has an install-dependent
96
+ // Windows target this package cannot know, so it is kept literal: always
97
+ // external, matched and displayed as typed, never fabricated into
98
+ // `c:\tmp` (#533). The win32 path matcher folds a rule's separators
99
+ // (`/` -> `\`), so a forward-slash value is unmatchable; carry a
100
+ // backslash match alias so a natural `/tmp/*` external_directory rule
101
+ // still resolves, while `value()` stays as typed for display.
102
+ const literal = normalizePathPolicyLiteral(token);
103
+ return this.forLiteral(literal, [literal.replaceAll("/", "\\")]);
104
+ }
105
+ case "plain":
106
+ return this.forPath(token, options);
107
+ }
52
108
  }
53
109
 
54
110
  /** Platform-aware absoluteness (`win32` vs `posix` rules). */
@@ -56,6 +112,36 @@ export class PathNormalizer {
56
112
  return this.impl.isAbsolute(pathValue);
57
113
  }
58
114
 
115
+ /**
116
+ * Interpret a literal `cd` target's effect on the effective base.
117
+ *
118
+ * On win32 the target carries Git Bash/MSYS semantics: a drive mount
119
+ * (`cd /c/x`) resolves to a translated Windows base (`C:\x`), a non-mount
120
+ * POSIX absolute (`cd /tmp`) is not deterministically resolvable and yields an
121
+ * `unknown` base, and a native/relative target is handled as usual. On POSIX
122
+ * an absolute target is absolute and everything else is relative.
123
+ */
124
+ interpretBashCdTarget(target: string): BashCdTarget {
125
+ if (this.platform !== "win32") {
126
+ return this.impl.isAbsolute(target)
127
+ ? { kind: "absolute", value: target }
128
+ : { kind: "relative" };
129
+ }
130
+
131
+ const shape = classifyWin32BashToken(target);
132
+ switch (shape.kind) {
133
+ case "drive-mount":
134
+ return { kind: "absolute", value: shape.windowsPath };
135
+ case "device":
136
+ case "posix-absolute":
137
+ return { kind: "unknown" };
138
+ case "plain":
139
+ return this.impl.isAbsolute(target)
140
+ ? { kind: "absolute", value: target }
141
+ : { kind: "relative" };
142
+ }
143
+ }
144
+
59
145
  /** Resolve a `cd`-folded offset against the baked cwd (platform-aware). */
60
146
  resolveBase(offset: string): string {
61
147
  return this.impl.resolve(this.cwd, offset);
@@ -85,6 +171,23 @@ export class PathNormalizer {
85
171
  );
86
172
  }
87
173
 
174
+ /**
175
+ * Outside-cwd test for an already-canonical boundary value (from
176
+ * {@link AccessPath.boundaryValue}), against the baked cwd.
177
+ *
178
+ * Unlike {@link isOutsideWorkingDirectory}, it does not re-derive the
179
+ * canonical form — the caller passes a value the {@link AccessPath} already
180
+ * canonicalized, so a device's preserved `/dev/null` reaches the pure check's
181
+ * `isSafeSystemPath` exclusion intact.
182
+ */
183
+ isBoundaryOutsideWorkingDirectory(canonicalPath: string): boolean {
184
+ return isPathOutsideWorkingDirectory(
185
+ canonicalPath,
186
+ this.canonicalCwd,
187
+ this.platform,
188
+ );
189
+ }
190
+
88
191
  /**
89
192
  * Lexical (not symlink-resolved) comparison value, resolved against the baked
90
193
  * cwd. Mirrors the as-typed absolute form used for skill-prompt matching;
@@ -1,277 +0,0 @@
1
- import { beforeEach, describe, expect, test, vi } from "vitest";
2
-
3
- // Mock node:os so tilde-expansion is deterministic across platforms.
4
- vi.mock("node:os", () => {
5
- const homedir = vi.fn(() => "/mock/home");
6
- return {
7
- homedir,
8
- default: { homedir },
9
- };
10
- });
11
-
12
- // Mock node:fs so realpathSync (used by canonicalizePath) is controllable.
13
- // Default implementation is identity — lexical tests are unaffected.
14
- const realpathSync = vi.hoisted(() =>
15
- vi.fn<(path: string) => string>((p) => p),
16
- );
17
- vi.mock("node:fs", () => ({
18
- realpathSync,
19
- default: { realpathSync },
20
- }));
21
-
22
- import { AccessPath } from "#src/access-intent/access-path";
23
-
24
- describe("AccessPath.forPath", () => {
25
- const cwd = "/projects/my-app";
26
-
27
- beforeEach(() => {
28
- realpathSync.mockReset();
29
- realpathSync.mockImplementation((p: string) => p);
30
- });
31
-
32
- describe("matchValues()", () => {
33
- test("adds the symlink-resolved alias alongside the typed path", () => {
34
- // /tmp -> /private/tmp (the macOS symlink from the bug report, #418).
35
- realpathSync.mockImplementation((p: string) =>
36
- p.startsWith("/tmp") ? `/private${p}` : p,
37
- );
38
- expect(
39
- AccessPath.forPath("/tmp/x", { cwd, platform: "linux" }).matchValues(),
40
- ).toEqual(["/tmp/x", "/private/tmp/x"]);
41
- });
42
-
43
- test("deduplicates when the canonical form equals the lexical form", () => {
44
- expect(
45
- AccessPath.forPath("/etc/hosts", {
46
- cwd,
47
- platform: "linux",
48
- }).matchValues(),
49
- ).toEqual(["/etc/hosts"]);
50
- });
51
-
52
- test("keeps the relative aliases for an in-cwd token without duplicating", () => {
53
- expect(
54
- AccessPath.forPath("src/foo.ts", {
55
- cwd,
56
- platform: "linux",
57
- }).matchValues(),
58
- ).toEqual(["/projects/my-app/src/foo.ts", "src/foo.ts"]);
59
- });
60
-
61
- test("includes only the lexical aliases when canonical is empty", () => {
62
- // Force canonicalizePath to return the original (no-op symlink resolution
63
- // effectively means canonical === lexical, handled by dedup).
64
- expect(
65
- AccessPath.forPath("/etc/hosts", {
66
- cwd,
67
- platform: "linux",
68
- }).matchValues(),
69
- ).not.toHaveLength(0);
70
- });
71
-
72
- test("resolves a relative token against an explicit resolveBase", () => {
73
- // The cd-folded effective base differs from cwd (the bash-path case).
74
- expect(
75
- AccessPath.forPath("foo.ts", {
76
- cwd,
77
- resolveBase: "/projects/my-app/sub",
78
- platform: "linux",
79
- }).matchValues(),
80
- ).toEqual(["/projects/my-app/sub/foo.ts", "sub/foo.ts", "foo.ts"]);
81
- });
82
-
83
- test("adds the canonical alias resolved against resolveBase", () => {
84
- realpathSync.mockImplementation((p: string) =>
85
- p === "/projects/my-app/sub/foo.ts" ? "/real/foo.ts" : p,
86
- );
87
- expect(
88
- AccessPath.forPath("foo.ts", {
89
- cwd,
90
- resolveBase: "/projects/my-app/sub",
91
- platform: "linux",
92
- }).matchValues(),
93
- ).toEqual([
94
- "/projects/my-app/sub/foo.ts",
95
- "sub/foo.ts",
96
- "foo.ts",
97
- "/real/foo.ts",
98
- ]);
99
- });
100
- });
101
-
102
- describe("platform option", () => {
103
- test("win32: builds lexical/match/boundary values with win32 rules", () => {
104
- const ap = AccessPath.forPath("src\\foo.ts", {
105
- cwd: "C:\\Projects\\App",
106
- platform: "win32",
107
- });
108
- expect(ap.value()).toBe("c:\\projects\\app\\src\\foo.ts");
109
- expect(ap.boundaryValue()).toBe("c:\\projects\\app\\src\\foo.ts");
110
- expect(ap.matchValues()).toEqual([
111
- "c:\\projects\\app\\src\\foo.ts",
112
- "src\\foo.ts",
113
- ]);
114
- });
115
-
116
- test("win32: lowercases the symlink-resolved boundary value", () => {
117
- realpathSync.mockImplementation((p: string) =>
118
- p === "c:\\projects\\app\\link" ? "C:\\Real\\App" : p,
119
- );
120
- expect(
121
- AccessPath.forPath("link", {
122
- cwd: "C:\\Projects\\App",
123
- platform: "win32",
124
- }).boundaryValue(),
125
- ).toBe("c:\\real\\app");
126
- });
127
- });
128
-
129
- describe("boundaryValue()", () => {
130
- test("returns the canonical (symlink-resolved) form", () => {
131
- realpathSync.mockImplementation((p: string) =>
132
- p.startsWith("/tmp") ? `/private${p}` : p,
133
- );
134
- expect(
135
- AccessPath.forPath("/tmp/x", {
136
- cwd,
137
- platform: "linux",
138
- }).boundaryValue(),
139
- ).toBe("/private/tmp/x");
140
- });
141
-
142
- test("returns the lexical form when path has no symlinks", () => {
143
- expect(
144
- AccessPath.forPath("/etc/hosts", {
145
- cwd,
146
- platform: "linux",
147
- }).boundaryValue(),
148
- ).toBe("/etc/hosts");
149
- });
150
-
151
- test("returns empty string for empty input", () => {
152
- expect(
153
- AccessPath.forPath("", { cwd, platform: "linux" }).boundaryValue(),
154
- ).toBe("");
155
- });
156
- });
157
-
158
- describe("value()", () => {
159
- test("returns the lexical (as-typed, normalized) form", () => {
160
- realpathSync.mockImplementation((p: string) =>
161
- p.startsWith("/tmp") ? `/private${p}` : p,
162
- );
163
- // Even when the path resolves to a different canonical, value() stays lexical.
164
- expect(
165
- AccessPath.forPath("/tmp/x", { cwd, platform: "linux" }).value(),
166
- ).toBe("/tmp/x");
167
- });
168
-
169
- test("normalizes the path against cwd", () => {
170
- // A relative path becomes an absolute lexical value.
171
- expect(
172
- AccessPath.forPath("src/foo.ts", { cwd, platform: "linux" }).value(),
173
- ).toBe("/projects/my-app/src/foo.ts");
174
- });
175
-
176
- test("normalizes a relative path against an explicit resolveBase", () => {
177
- expect(
178
- AccessPath.forPath("foo.ts", {
179
- cwd,
180
- resolveBase: "/projects/my-app/sub",
181
- platform: "linux",
182
- }).value(),
183
- ).toBe("/projects/my-app/sub/foo.ts");
184
- });
185
-
186
- test("returns empty string for empty input", () => {
187
- expect(AccessPath.forPath("", { cwd, platform: "linux" }).value()).toBe(
188
- "",
189
- );
190
- });
191
- });
192
- });
193
-
194
- describe("resolvedAlias()", () => {
195
- const cwd = "/projects/my-app";
196
-
197
- beforeEach(() => {
198
- realpathSync.mockReset();
199
- realpathSync.mockImplementation((p: string) => p);
200
- });
201
-
202
- test("returns the canonical form when a symlink resolves elsewhere", () => {
203
- realpathSync.mockImplementation((p: string) =>
204
- p === "/projects/my-app/demo-symlink-passwd" ? "/etc/passwd" : p,
205
- );
206
- expect(
207
- AccessPath.forPath("demo-symlink-passwd", {
208
- cwd,
209
- platform: "linux",
210
- }).resolvedAlias(),
211
- ).toBe("/etc/passwd");
212
- });
213
-
214
- test("returns undefined when the path has no symlinks (canonical equals lexical)", () => {
215
- expect(
216
- AccessPath.forPath("/etc/hosts", {
217
- cwd,
218
- platform: "linux",
219
- }).resolvedAlias(),
220
- ).toBeUndefined();
221
- });
222
-
223
- test("returns undefined for a literal-only path (no canonical)", () => {
224
- expect(AccessPath.forLiteral("foo.ts").resolvedAlias()).toBeUndefined();
225
- });
226
-
227
- test("returns undefined for empty input", () => {
228
- expect(
229
- AccessPath.forPath("", { cwd, platform: "linux" }).resolvedAlias(),
230
- ).toBeUndefined();
231
- });
232
-
233
- test("win32: returns the lowercased canonical form for a real symlink target", () => {
234
- realpathSync.mockImplementation((p: string) =>
235
- p === "c:\\projects\\app\\link" ? "C:\\Real\\App" : p,
236
- );
237
- expect(
238
- AccessPath.forPath("link", {
239
- cwd: "C:\\Projects\\App",
240
- platform: "win32",
241
- }).resolvedAlias(),
242
- ).toBe("c:\\real\\app");
243
- });
244
-
245
- test("win32: returns undefined for a case-only difference (both forms lowercased)", () => {
246
- expect(
247
- AccessPath.forPath("src\\foo.ts", {
248
- cwd: "C:\\Projects\\App",
249
- platform: "win32",
250
- }).resolvedAlias(),
251
- ).toBeUndefined();
252
- });
253
- });
254
-
255
- describe("AccessPath.forLiteral", () => {
256
- beforeEach(() => {
257
- realpathSync.mockReset();
258
- realpathSync.mockImplementation((p: string) => p);
259
- });
260
-
261
- test("matchValues() carries only the literal — no canonical, no absolute", () => {
262
- expect(AccessPath.forLiteral("foo.ts").matchValues()).toEqual(["foo.ts"]);
263
- });
264
-
265
- test("boundaryValue() is empty (no outside-cwd notion for an unknown base)", () => {
266
- expect(AccessPath.forLiteral("foo.ts").boundaryValue()).toBe("");
267
- });
268
-
269
- test("value() returns the literal", () => {
270
- expect(AccessPath.forLiteral("foo.ts").value()).toBe("foo.ts");
271
- });
272
-
273
- test("an empty literal yields no match values", () => {
274
- expect(AccessPath.forLiteral("").matchValues()).toEqual([]);
275
- expect(AccessPath.forLiteral("").value()).toBe("");
276
- });
277
- });
@@ -1,148 +0,0 @@
1
- import { describe, expect, it } from "vitest";
2
- import {
3
- resolveNodeText,
4
- SKIP_SUBTREE_TYPES,
5
- } from "#src/access-intent/bash/node-text";
6
- import type { TSNode } from "#src/access-intent/bash/parser";
7
-
8
- // Minimal fake TSNode builder — only fills the fields resolveNodeText reads.
9
- function makeNode(type: string, text: string, children: TSNode[] = []): TSNode {
10
- return {
11
- type,
12
- text,
13
- startIndex: 0,
14
- childCount: children.length,
15
- isNamed: true,
16
- child: (i) => children[i] ?? null,
17
- };
18
- }
19
-
20
- describe("SKIP_SUBTREE_TYPES", () => {
21
- it("contains the three node types that must not be descended", () => {
22
- expect(SKIP_SUBTREE_TYPES.has("heredoc_body")).toBe(true);
23
- expect(SKIP_SUBTREE_TYPES.has("heredoc_end")).toBe(true);
24
- expect(SKIP_SUBTREE_TYPES.has("comment")).toBe(true);
25
- });
26
-
27
- it("does not contain common argument node types", () => {
28
- expect(SKIP_SUBTREE_TYPES.has("word")).toBe(false);
29
- expect(SKIP_SUBTREE_TYPES.has("string")).toBe(false);
30
- expect(SKIP_SUBTREE_TYPES.has("raw_string")).toBe(false);
31
- });
32
- });
33
-
34
- describe("resolveNodeText", () => {
35
- describe("word nodes", () => {
36
- it("returns the node text unchanged", () => {
37
- expect(resolveNodeText(makeNode("word", "hello"))).toBe("hello");
38
- });
39
- });
40
-
41
- describe("raw_string nodes (single-quoted)", () => {
42
- it("strips surrounding single quotes", () => {
43
- expect(resolveNodeText(makeNode("raw_string", "'content'"))).toBe(
44
- "content",
45
- );
46
- });
47
-
48
- it("strips single quotes around a path", () => {
49
- expect(resolveNodeText(makeNode("raw_string", "'/etc/hosts'"))).toBe(
50
- "/etc/hosts",
51
- );
52
- });
53
-
54
- it("returns text as-is when not fully single-quoted", () => {
55
- // A raw_string node without enclosing quotes (defensive fallback)
56
- expect(resolveNodeText(makeNode("raw_string", "noquotes"))).toBe(
57
- "noquotes",
58
- );
59
- });
60
- });
61
-
62
- describe("string nodes (double-quoted)", () => {
63
- it("concatenates inner word children, skipping quote delimiters", () => {
64
- const quoteOpen = makeNode('"', '"');
65
- const content = makeNode("string_content", "hello world");
66
- const quoteClose = makeNode('"', '"');
67
- const node = makeNode("string", '"hello world"', [
68
- quoteOpen,
69
- content,
70
- quoteClose,
71
- ]);
72
- expect(resolveNodeText(node)).toBe("hello world");
73
- });
74
-
75
- it("concatenates multiple inner children", () => {
76
- const quoteOpen = makeNode('"', '"');
77
- const part1 = makeNode("string_content", "foo");
78
- const part2 = makeNode("simple_expansion", "$BAR");
79
- const quoteClose = makeNode('"', '"');
80
- const node = makeNode("string", '"foo$BAR"', [
81
- quoteOpen,
82
- part1,
83
- part2,
84
- quoteClose,
85
- ]);
86
- expect(resolveNodeText(node)).toBe("foo$BAR");
87
- });
88
-
89
- it("returns empty string for an empty double-quoted string", () => {
90
- const quoteOpen = makeNode('"', '"');
91
- const quoteClose = makeNode('"', '"');
92
- const node = makeNode("string", '""', [quoteOpen, quoteClose]);
93
- expect(resolveNodeText(node)).toBe("");
94
- });
95
- });
96
-
97
- describe("string_content, simple_expansion, and expansion nodes", () => {
98
- it("returns text as-is for string_content", () => {
99
- expect(resolveNodeText(makeNode("string_content", "plain text"))).toBe(
100
- "plain text",
101
- );
102
- });
103
-
104
- it("returns text as-is for simple_expansion (e.g. $HOME)", () => {
105
- // retro 0350: $HOME returns the literal text of a simple_expansion node
106
- expect(resolveNodeText(makeNode("simple_expansion", "$HOME"))).toBe(
107
- "$HOME",
108
- );
109
- });
110
-
111
- it("returns text as-is for expansion", () => {
112
- // biome-ignore lint/suspicious/noTemplateCurlyInString: intentional literal — testing that expansion node text is returned verbatim
113
- expect(resolveNodeText(makeNode("expansion", "${VAR}"))).toBe("${VAR}");
114
- });
115
- });
116
-
117
- describe("concatenation nodes", () => {
118
- it("concatenates resolved children", () => {
119
- const word = makeNode("word", "/etc/");
120
- const expansion = makeNode("simple_expansion", "$FILE");
121
- const node = makeNode("concatenation", "/etc/$FILE", [word, expansion]);
122
- expect(resolveNodeText(node)).toBe("/etc/$FILE");
123
- });
124
-
125
- it("handles nested concatenation-of-string", () => {
126
- // A concatenation whose child is a double-quoted string
127
- const quoteOpen = makeNode('"', '"');
128
- const content = makeNode("string_content", "bar");
129
- const quoteClose = makeNode('"', '"');
130
- const inner = makeNode("string", '"bar"', [
131
- quoteOpen,
132
- content,
133
- quoteClose,
134
- ]);
135
- const prefix = makeNode("word", "foo");
136
- const node = makeNode("concatenation", 'foo"bar"', [prefix, inner]);
137
- expect(resolveNodeText(node)).toBe("foobar");
138
- });
139
- });
140
-
141
- describe("default fallback", () => {
142
- it("returns the raw text for unknown node types", () => {
143
- expect(resolveNodeText(makeNode("unknown_type", "rawtext"))).toBe(
144
- "rawtext",
145
- );
146
- });
147
- });
148
- });
@@ -1,19 +0,0 @@
1
- import { describe, expect, it } from "vitest";
2
- import { getParser } from "#src/access-intent/bash/parser";
3
-
4
- describe("getParser", () => {
5
- it("parses a simple bash command and returns a non-null root node", async () => {
6
- const parser = await getParser();
7
- const tree = parser.parse("echo hi");
8
- expect(tree).not.toBeNull();
9
- expect(tree?.rootNode).toBeDefined();
10
- expect(tree?.rootNode.type).toBe("program");
11
- tree?.delete();
12
- });
13
-
14
- it("returns the same memoized parser instance on repeated calls", async () => {
15
- const first = await getParser();
16
- const second = await getParser();
17
- expect(first).toBe(second);
18
- });
19
- });