@komarspn/pi-permission-system 16.0.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.
- package/CHANGELOG.md +2234 -0
- package/LICENSE +21 -0
- package/README.md +158 -0
- package/config/config.example.json +39 -0
- package/package.json +82 -0
- package/schemas/permissions.schema.json +158 -0
- package/src/active-agent.ts +72 -0
- package/src/async-cache.ts +21 -0
- package/src/bash-arity.ts +210 -0
- package/src/builtin-tool-input-formatters.ts +82 -0
- package/src/canonicalize-path.ts +30 -0
- package/src/common.ts +121 -0
- package/src/config-loader.ts +432 -0
- package/src/config-modal.ts +259 -0
- package/src/config-paths.ts +47 -0
- package/src/config-reporter.ts +34 -0
- package/src/config-store.ts +222 -0
- package/src/decision-audit.ts +75 -0
- package/src/decision-reporter.ts +41 -0
- package/src/denial-messages.ts +232 -0
- package/src/expand-home.ts +28 -0
- package/src/extension-config.ts +79 -0
- package/src/extension-paths.ts +66 -0
- package/src/forwarded-permissions/io.ts +404 -0
- package/src/forwarded-permissions/permission-forwarder.ts +580 -0
- package/src/forwarding-manager.ts +74 -0
- package/src/gate-prompter.ts +12 -0
- package/src/handlers/before-agent-start.ts +94 -0
- package/src/handlers/gates/bash-command.ts +75 -0
- package/src/handlers/gates/bash-external-directory.ts +127 -0
- package/src/handlers/gates/bash-path-extractor.ts +15 -0
- package/src/handlers/gates/bash-path.ts +152 -0
- package/src/handlers/gates/bash-program.ts +1143 -0
- package/src/handlers/gates/bash-token-classification.ts +105 -0
- package/src/handlers/gates/candidate-check.ts +32 -0
- package/src/handlers/gates/descriptor.ts +81 -0
- package/src/handlers/gates/external-directory-messages.ts +20 -0
- package/src/handlers/gates/external-directory.ts +133 -0
- package/src/handlers/gates/helpers.ts +76 -0
- package/src/handlers/gates/path.ts +91 -0
- package/src/handlers/gates/runner.ts +186 -0
- package/src/handlers/gates/skill-input-gate-pipeline.ts +104 -0
- package/src/handlers/gates/skill-input.ts +46 -0
- package/src/handlers/gates/skill-read.ts +87 -0
- package/src/handlers/gates/tool-call-gate-pipeline.ts +129 -0
- package/src/handlers/gates/tool.ts +102 -0
- package/src/handlers/gates/types.ts +13 -0
- package/src/handlers/index.ts +3 -0
- package/src/handlers/lifecycle.ts +95 -0
- package/src/handlers/permission-gate-handler.ts +190 -0
- package/src/handlers/tool-call-boundary.ts +91 -0
- package/src/index.ts +225 -0
- package/src/input-normalizer.ts +157 -0
- package/src/logging.ts +113 -0
- package/src/mcp-targets.ts +170 -0
- package/src/node-modules-discovery.ts +76 -0
- package/src/normalize.ts +43 -0
- package/src/path-utils.ts +355 -0
- package/src/pattern-suggest.ts +132 -0
- package/src/permission-dialog.ts +138 -0
- package/src/permission-event-rpc.ts +223 -0
- package/src/permission-events.ts +266 -0
- package/src/permission-forwarding.ts +188 -0
- package/src/permission-gate.ts +94 -0
- package/src/permission-manager.ts +392 -0
- package/src/permission-merge.ts +32 -0
- package/src/permission-prompter.ts +142 -0
- package/src/permission-prompts.ts +93 -0
- package/src/permission-resolver.ts +109 -0
- package/src/permission-session.ts +189 -0
- package/src/permission-ui-prompt.ts +127 -0
- package/src/permissions-service.ts +63 -0
- package/src/persistent-approval-recorder.ts +139 -0
- package/src/policy-loader.ts +350 -0
- package/src/prompting-gateway.ts +104 -0
- package/src/rule.ts +188 -0
- package/src/scope-merge.ts +72 -0
- package/src/service-lifecycle.ts +49 -0
- package/src/service.ts +163 -0
- package/src/session-approval-recorder.ts +6 -0
- package/src/session-approval.ts +43 -0
- package/src/session-logger.ts +91 -0
- package/src/session-rules.ts +79 -0
- package/src/skill-prompt-sanitizer.ts +292 -0
- package/src/status.ts +35 -0
- package/src/subagent-context.ts +104 -0
- package/src/subagent-lifecycle-events.ts +72 -0
- package/src/subagent-registry.ts +105 -0
- package/src/synthesize.ts +92 -0
- package/src/system-prompt-sanitizer.ts +274 -0
- package/src/tool-access-extractor-registry.ts +68 -0
- package/src/tool-input-formatter-registry.ts +67 -0
- package/src/tool-input-preview.ts +34 -0
- package/src/tool-input-prompt-formatters.ts +63 -0
- package/src/tool-preview-formatter.ts +207 -0
- package/src/tool-registry.ts +148 -0
- package/src/types.ts +64 -0
- package/src/wildcard-matcher.ts +120 -0
- package/src/yolo-mode.ts +30 -0
- package/test/active-agent.test.ts +155 -0
- package/test/async-cache.test.ts +48 -0
- package/test/bash-arity.test.ts +144 -0
- package/test/bash-external-directory.test.ts +956 -0
- package/test/builtin-tool-input-formatters.test.ts +109 -0
- package/test/canonicalize-path.test.ts +93 -0
- package/test/common.test.ts +287 -0
- package/test/composition-root.test.ts +603 -0
- package/test/config-loader.test.ts +740 -0
- package/test/config-modal.test.ts +320 -0
- package/test/config-paths.test.ts +83 -0
- package/test/config-pipeline.test.ts +90 -0
- package/test/config-reporter.test.ts +147 -0
- package/test/config-store.test.ts +466 -0
- package/test/decision-audit.test.ts +72 -0
- package/test/decision-reporter.test.ts +112 -0
- package/test/denial-messages.test.ts +656 -0
- package/test/detect-permissive-bash-fallback.test.ts +56 -0
- package/test/expand-home.test.ts +93 -0
- package/test/extension-config.test.ts +129 -0
- package/test/extension-paths.test.ts +108 -0
- package/test/forwarded-permissions/io.test.ts +251 -0
- package/test/forwarding-manager.test.ts +194 -0
- package/test/handlers/before-agent-start.test.ts +317 -0
- package/test/handlers/external-directory-integration.test.ts +623 -0
- package/test/handlers/external-directory-session-dedup.test.ts +430 -0
- package/test/handlers/external-directory-symlink-acceptance.test.ts +149 -0
- package/test/handlers/gates/bash-command-metamorphic.test.ts +83 -0
- package/test/handlers/gates/bash-command.test.ts +191 -0
- package/test/handlers/gates/bash-external-directory.test.ts +269 -0
- package/test/handlers/gates/bash-path.test.ts +337 -0
- package/test/handlers/gates/bash-program.test.ts +410 -0
- package/test/handlers/gates/bash-token-classification.test.ts +241 -0
- package/test/handlers/gates/candidate-check.test.ts +52 -0
- package/test/handlers/gates/external-directory-messages.test.ts +61 -0
- package/test/handlers/gates/external-directory.test.ts +259 -0
- package/test/handlers/gates/helpers.test.ts +177 -0
- package/test/handlers/gates/path.test.ts +294 -0
- package/test/handlers/gates/runner.test.ts +447 -0
- package/test/handlers/gates/skill-input-gate-pipeline.test.ts +176 -0
- package/test/handlers/gates/skill-input.test.ts +131 -0
- package/test/handlers/gates/skill-read.test.ts +158 -0
- package/test/handlers/gates/tool-call-gate-pipeline.test.ts +252 -0
- package/test/handlers/gates/tool.test.ts +223 -0
- package/test/handlers/input-events.test.ts +168 -0
- package/test/handlers/input.test.ts +199 -0
- package/test/handlers/lifecycle.test.ts +221 -0
- package/test/handlers/tool-call-boundary.test.ts +145 -0
- package/test/handlers/tool-call-events.test.ts +277 -0
- package/test/handlers/tool-call.test.ts +395 -0
- package/test/handlers/validate-requested-tool.test.ts +92 -0
- package/test/helpers/gate-fixtures.ts +323 -0
- package/test/helpers/handler-fixtures.ts +335 -0
- package/test/helpers/make-fake-pi.ts +100 -0
- package/test/helpers/manager-harness.ts +112 -0
- package/test/helpers/session-fixtures.ts +204 -0
- package/test/input-normalizer.test.ts +367 -0
- package/test/logging.test.ts +51 -0
- package/test/mcp-targets.test.ts +233 -0
- package/test/node-modules-discovery.test.ts +97 -0
- package/test/normalize.test.ts +247 -0
- package/test/path-utils.test.ts +650 -0
- package/test/pattern-suggest.test.ts +248 -0
- package/test/permission-dialog.test.ts +241 -0
- package/test/permission-event-rpc.test.ts +541 -0
- package/test/permission-events.test.ts +402 -0
- package/test/permission-forwarder.test.ts +369 -0
- package/test/permission-forwarding.test.ts +315 -0
- package/test/permission-gate.test.ts +305 -0
- package/test/permission-manager-unified.test.ts +3368 -0
- package/test/permission-merge.test.ts +61 -0
- package/test/permission-prompter.test.ts +518 -0
- package/test/permission-prompts.test.ts +363 -0
- package/test/permission-resolver.test.ts +265 -0
- package/test/permission-session.test.ts +363 -0
- package/test/permission-ui-prompt.test.ts +146 -0
- package/test/permissions-service.test.ts +177 -0
- package/test/persistent-approval-recorder.test.ts +133 -0
- package/test/pi-infrastructure-read.test.ts +369 -0
- package/test/policy-loader.test.ts +561 -0
- package/test/prompting-gateway.test.ts +230 -0
- package/test/rule.test.ts +604 -0
- package/test/scope-merge.test.ts +116 -0
- package/test/service-lifecycle.test.ts +163 -0
- package/test/service.test.ts +308 -0
- package/test/session-approval.test.ts +75 -0
- package/test/session-logger.test.ts +200 -0
- package/test/session-rules.test.ts +304 -0
- package/test/session-start.test.ts +112 -0
- package/test/skill-prompt-sanitizer.test.ts +374 -0
- package/test/status.test.ts +10 -0
- package/test/subagent-context.test.ts +326 -0
- package/test/subagent-lifecycle-events.test.ts +132 -0
- package/test/subagent-registry.test.ts +145 -0
- package/test/synthesize.test.ts +300 -0
- package/test/system-prompt-sanitizer.test.ts +382 -0
- package/test/tool-access-extractor-registry.test.ts +77 -0
- package/test/tool-input-formatter-registry.test.ts +75 -0
- package/test/tool-input-preview.test.ts +129 -0
- package/test/tool-input-prompt-formatters.test.ts +115 -0
- package/test/tool-preview-formatter.test.ts +458 -0
- package/test/tool-registry.test.ts +197 -0
- package/test/wildcard-matcher.test.ts +424 -0
- package/test/yolo-mode.test.ts +188 -0
|
@@ -0,0 +1,109 @@
|
|
|
1
|
+
import { describe, expect, test } from "vitest";
|
|
2
|
+
|
|
3
|
+
import {
|
|
4
|
+
formatMcpInputForPrompt,
|
|
5
|
+
registerBuiltinToolInputFormatters,
|
|
6
|
+
} from "#src/builtin-tool-input-formatters";
|
|
7
|
+
import { ToolInputFormatterRegistry } from "#src/tool-input-formatter-registry";
|
|
8
|
+
|
|
9
|
+
// ── formatMcpInputForPrompt ───────────────────────────────────────────────
|
|
10
|
+
|
|
11
|
+
describe("formatMcpInputForPrompt", () => {
|
|
12
|
+
test("returns undefined when arguments is absent", () => {
|
|
13
|
+
expect(formatMcpInputForPrompt({ tool: "exa:search" })).toBeUndefined();
|
|
14
|
+
});
|
|
15
|
+
|
|
16
|
+
test("returns undefined when arguments is an empty object", () => {
|
|
17
|
+
expect(
|
|
18
|
+
formatMcpInputForPrompt({ tool: "exa:search", arguments: {} }),
|
|
19
|
+
).toBeUndefined();
|
|
20
|
+
});
|
|
21
|
+
|
|
22
|
+
test("returns a summary for a single string argument", () => {
|
|
23
|
+
const result = formatMcpInputForPrompt({
|
|
24
|
+
tool: "exa:search",
|
|
25
|
+
arguments: { query: "typescript generics" },
|
|
26
|
+
});
|
|
27
|
+
expect(result).toBeDefined();
|
|
28
|
+
expect(result).toContain("query");
|
|
29
|
+
expect(result).toContain("typescript generics");
|
|
30
|
+
expect(result).toMatch(/^with /);
|
|
31
|
+
});
|
|
32
|
+
|
|
33
|
+
test("returns a comma-separated summary for multiple arguments", () => {
|
|
34
|
+
const result = formatMcpInputForPrompt({
|
|
35
|
+
tool: "exa:search",
|
|
36
|
+
arguments: { query: "test", numResults: 5 },
|
|
37
|
+
});
|
|
38
|
+
expect(result).toContain("query");
|
|
39
|
+
expect(result).toContain("numResults");
|
|
40
|
+
expect(result).toContain("5");
|
|
41
|
+
});
|
|
42
|
+
|
|
43
|
+
test("renders number arguments without quotes", () => {
|
|
44
|
+
const result = formatMcpInputForPrompt({
|
|
45
|
+
arguments: { count: 42 },
|
|
46
|
+
});
|
|
47
|
+
expect(result).toContain("42");
|
|
48
|
+
expect(result).not.toContain('"42"');
|
|
49
|
+
});
|
|
50
|
+
|
|
51
|
+
test("renders boolean arguments without quotes", () => {
|
|
52
|
+
const result = formatMcpInputForPrompt({
|
|
53
|
+
arguments: { verbose: true },
|
|
54
|
+
});
|
|
55
|
+
expect(result).toContain("true");
|
|
56
|
+
});
|
|
57
|
+
|
|
58
|
+
test("renders array arguments as '[N items]'", () => {
|
|
59
|
+
const result = formatMcpInputForPrompt({
|
|
60
|
+
arguments: { ids: [1, 2, 3] },
|
|
61
|
+
});
|
|
62
|
+
expect(result).toContain("[3 items]");
|
|
63
|
+
});
|
|
64
|
+
|
|
65
|
+
test("renders nested object arguments as '{…}'", () => {
|
|
66
|
+
const result = formatMcpInputForPrompt({
|
|
67
|
+
arguments: { filter: { type: "file" } },
|
|
68
|
+
});
|
|
69
|
+
expect(result).toContain("{…}");
|
|
70
|
+
});
|
|
71
|
+
|
|
72
|
+
test("truncates the full summary when it exceeds the limit", () => {
|
|
73
|
+
// Need multiple long-valued args so the joined summary exceeds 160 chars
|
|
74
|
+
const result = formatMcpInputForPrompt({
|
|
75
|
+
arguments: {
|
|
76
|
+
first: "x".repeat(80),
|
|
77
|
+
second: "y".repeat(80),
|
|
78
|
+
third: "z".repeat(80),
|
|
79
|
+
},
|
|
80
|
+
});
|
|
81
|
+
expect(result).toBeDefined();
|
|
82
|
+
expect(result!.endsWith("…")).toBe(true);
|
|
83
|
+
});
|
|
84
|
+
|
|
85
|
+
test("truncates long string argument values", () => {
|
|
86
|
+
const result = formatMcpInputForPrompt({
|
|
87
|
+
arguments: { query: "x".repeat(100) },
|
|
88
|
+
});
|
|
89
|
+
expect(result).toBeDefined();
|
|
90
|
+
// Should not include the full 100-char string verbatim
|
|
91
|
+
expect(result).not.toContain("x".repeat(100));
|
|
92
|
+
});
|
|
93
|
+
});
|
|
94
|
+
|
|
95
|
+
// ── registerBuiltinToolInputFormatters ────────────────────────────────────
|
|
96
|
+
|
|
97
|
+
describe("registerBuiltinToolInputFormatters", () => {
|
|
98
|
+
test("registers the mcp formatter in the registry", () => {
|
|
99
|
+
const registry = new ToolInputFormatterRegistry();
|
|
100
|
+
registerBuiltinToolInputFormatters(registry);
|
|
101
|
+
expect(registry.get("mcp")).toBe(formatMcpInputForPrompt);
|
|
102
|
+
});
|
|
103
|
+
|
|
104
|
+
test("throws if called twice (duplicate registration guard)", () => {
|
|
105
|
+
const registry = new ToolInputFormatterRegistry();
|
|
106
|
+
registerBuiltinToolInputFormatters(registry);
|
|
107
|
+
expect(() => registerBuiltinToolInputFormatters(registry)).toThrow("mcp");
|
|
108
|
+
});
|
|
109
|
+
});
|
|
@@ -0,0 +1,93 @@
|
|
|
1
|
+
import { beforeEach, describe, expect, test, vi } from "vitest";
|
|
2
|
+
|
|
3
|
+
const realpathSync = vi.hoisted(() => vi.fn<(path: string) => string>());
|
|
4
|
+
|
|
5
|
+
vi.mock("node:fs", () => ({
|
|
6
|
+
realpathSync,
|
|
7
|
+
default: { realpathSync },
|
|
8
|
+
}));
|
|
9
|
+
|
|
10
|
+
import { canonicalizePath } from "#src/canonicalize-path";
|
|
11
|
+
|
|
12
|
+
function enoent(p: string): NodeJS.ErrnoException {
|
|
13
|
+
return Object.assign(new Error(`ENOENT: no such file or directory '${p}'`), {
|
|
14
|
+
code: "ENOENT",
|
|
15
|
+
});
|
|
16
|
+
}
|
|
17
|
+
|
|
18
|
+
describe("canonicalizePath", () => {
|
|
19
|
+
beforeEach(() => {
|
|
20
|
+
realpathSync.mockReset();
|
|
21
|
+
});
|
|
22
|
+
|
|
23
|
+
test("returns empty string for empty input", () => {
|
|
24
|
+
expect(canonicalizePath("")).toBe("");
|
|
25
|
+
});
|
|
26
|
+
|
|
27
|
+
test("returns realpathSync result when path exists", () => {
|
|
28
|
+
realpathSync.mockReturnValueOnce("/real/projects/app");
|
|
29
|
+
expect(canonicalizePath("/projects/link")).toBe("/real/projects/app");
|
|
30
|
+
});
|
|
31
|
+
|
|
32
|
+
test("re-appends a non-existent leaf to the canonical parent", () => {
|
|
33
|
+
realpathSync
|
|
34
|
+
.mockImplementationOnce(() => {
|
|
35
|
+
throw enoent("/projects/app/new-file.ts");
|
|
36
|
+
})
|
|
37
|
+
.mockReturnValueOnce("/canonical/app");
|
|
38
|
+
expect(canonicalizePath("/projects/app/new-file.ts")).toBe(
|
|
39
|
+
"/canonical/app/new-file.ts",
|
|
40
|
+
);
|
|
41
|
+
});
|
|
42
|
+
|
|
43
|
+
test("walks up multiple levels for a deeply non-existent path", () => {
|
|
44
|
+
realpathSync
|
|
45
|
+
.mockImplementationOnce(() => {
|
|
46
|
+
throw enoent("/projects/app/src/new-file.ts");
|
|
47
|
+
})
|
|
48
|
+
.mockImplementationOnce(() => {
|
|
49
|
+
throw enoent("/projects/app/src");
|
|
50
|
+
})
|
|
51
|
+
.mockImplementationOnce(() => {
|
|
52
|
+
throw enoent("/projects/app");
|
|
53
|
+
})
|
|
54
|
+
.mockReturnValueOnce("/canonical/projects");
|
|
55
|
+
expect(canonicalizePath("/projects/app/src/new-file.ts")).toBe(
|
|
56
|
+
"/canonical/projects/app/src/new-file.ts",
|
|
57
|
+
);
|
|
58
|
+
});
|
|
59
|
+
|
|
60
|
+
test("returns input unchanged when walk reaches filesystem root (all ENOENT)", () => {
|
|
61
|
+
realpathSync.mockImplementation(() => {
|
|
62
|
+
throw enoent("");
|
|
63
|
+
});
|
|
64
|
+
expect(canonicalizePath("/nonexistent/path/file.ts")).toBe(
|
|
65
|
+
"/nonexistent/path/file.ts",
|
|
66
|
+
);
|
|
67
|
+
});
|
|
68
|
+
|
|
69
|
+
test("returns input unchanged on ELOOP (symlink loop)", () => {
|
|
70
|
+
realpathSync.mockImplementation(() => {
|
|
71
|
+
throw Object.assign(new Error("ELOOP"), { code: "ELOOP" });
|
|
72
|
+
});
|
|
73
|
+
expect(canonicalizePath("/some/looping/path")).toBe("/some/looping/path");
|
|
74
|
+
});
|
|
75
|
+
|
|
76
|
+
test("returns input unchanged on EACCES (permission denied)", () => {
|
|
77
|
+
realpathSync.mockImplementation(() => {
|
|
78
|
+
throw Object.assign(new Error("EACCES"), { code: "EACCES" });
|
|
79
|
+
});
|
|
80
|
+
expect(canonicalizePath("/restricted/path")).toBe("/restricted/path");
|
|
81
|
+
});
|
|
82
|
+
|
|
83
|
+
test("handles ENOTDIR by walking up (like ENOENT)", () => {
|
|
84
|
+
realpathSync
|
|
85
|
+
.mockImplementationOnce(() => {
|
|
86
|
+
throw Object.assign(new Error("ENOTDIR"), { code: "ENOTDIR" });
|
|
87
|
+
})
|
|
88
|
+
.mockReturnValueOnce("/real/parent");
|
|
89
|
+
expect(canonicalizePath("/real/parent/not-a-dir")).toBe(
|
|
90
|
+
"/real/parent/not-a-dir",
|
|
91
|
+
);
|
|
92
|
+
});
|
|
93
|
+
});
|
|
@@ -0,0 +1,287 @@
|
|
|
1
|
+
import { afterEach, describe, expect, it, test, vi } from "vitest";
|
|
2
|
+
|
|
3
|
+
import {
|
|
4
|
+
extractFrontmatter,
|
|
5
|
+
getNonEmptyString,
|
|
6
|
+
isDenyWithReason,
|
|
7
|
+
isPermissionState,
|
|
8
|
+
normalizeOptionalPositiveInt,
|
|
9
|
+
normalizeOptionalStringArray,
|
|
10
|
+
parseSimpleYamlMap,
|
|
11
|
+
toRecord,
|
|
12
|
+
} from "#src/common";
|
|
13
|
+
|
|
14
|
+
afterEach(() => {
|
|
15
|
+
vi.restoreAllMocks();
|
|
16
|
+
});
|
|
17
|
+
|
|
18
|
+
describe("toRecord", () => {
|
|
19
|
+
test("returns empty object for null", () => {
|
|
20
|
+
expect(toRecord(null)).toEqual({});
|
|
21
|
+
});
|
|
22
|
+
|
|
23
|
+
test("returns empty object for undefined", () => {
|
|
24
|
+
expect(toRecord(undefined)).toEqual({});
|
|
25
|
+
});
|
|
26
|
+
|
|
27
|
+
test("returns empty object for a string", () => {
|
|
28
|
+
expect(toRecord("hello")).toEqual({});
|
|
29
|
+
});
|
|
30
|
+
|
|
31
|
+
test("returns empty object for a number", () => {
|
|
32
|
+
expect(toRecord(42)).toEqual({});
|
|
33
|
+
});
|
|
34
|
+
|
|
35
|
+
test("returns empty object for an array", () => {
|
|
36
|
+
expect(toRecord(["a", "b"])).toEqual({});
|
|
37
|
+
});
|
|
38
|
+
|
|
39
|
+
test("returns the object itself for a plain object", () => {
|
|
40
|
+
const input = { a: 1, b: "two" };
|
|
41
|
+
expect(toRecord(input)).toBe(input);
|
|
42
|
+
});
|
|
43
|
+
|
|
44
|
+
test("returns the object for a nested object", () => {
|
|
45
|
+
const input = { x: { y: 3 } };
|
|
46
|
+
expect(toRecord(input)).toBe(input);
|
|
47
|
+
});
|
|
48
|
+
});
|
|
49
|
+
|
|
50
|
+
describe("getNonEmptyString", () => {
|
|
51
|
+
test("returns null for non-string values", () => {
|
|
52
|
+
expect(getNonEmptyString(null)).toBeNull();
|
|
53
|
+
expect(getNonEmptyString(undefined)).toBeNull();
|
|
54
|
+
expect(getNonEmptyString(42)).toBeNull();
|
|
55
|
+
expect(getNonEmptyString({})).toBeNull();
|
|
56
|
+
expect(getNonEmptyString([])).toBeNull();
|
|
57
|
+
});
|
|
58
|
+
|
|
59
|
+
test("returns null for empty string", () => {
|
|
60
|
+
expect(getNonEmptyString("")).toBeNull();
|
|
61
|
+
});
|
|
62
|
+
|
|
63
|
+
test("returns null for whitespace-only string", () => {
|
|
64
|
+
expect(getNonEmptyString(" ")).toBeNull();
|
|
65
|
+
expect(getNonEmptyString("\t\n")).toBeNull();
|
|
66
|
+
});
|
|
67
|
+
|
|
68
|
+
test("returns trimmed string for valid string", () => {
|
|
69
|
+
expect(getNonEmptyString("hello")).toBe("hello");
|
|
70
|
+
expect(getNonEmptyString(" hello ")).toBe("hello");
|
|
71
|
+
});
|
|
72
|
+
|
|
73
|
+
test("returns single non-whitespace character", () => {
|
|
74
|
+
expect(getNonEmptyString("a")).toBe("a");
|
|
75
|
+
});
|
|
76
|
+
});
|
|
77
|
+
|
|
78
|
+
describe("isPermissionState", () => {
|
|
79
|
+
test("returns true for 'allow'", () => {
|
|
80
|
+
expect(isPermissionState("allow")).toBe(true);
|
|
81
|
+
});
|
|
82
|
+
|
|
83
|
+
test("returns true for 'deny'", () => {
|
|
84
|
+
expect(isPermissionState("deny")).toBe(true);
|
|
85
|
+
});
|
|
86
|
+
|
|
87
|
+
test("returns true for 'ask'", () => {
|
|
88
|
+
expect(isPermissionState("ask")).toBe(true);
|
|
89
|
+
});
|
|
90
|
+
|
|
91
|
+
test("returns false for unrecognized strings", () => {
|
|
92
|
+
expect(isPermissionState("ALLOW")).toBe(false);
|
|
93
|
+
expect(isPermissionState("permit")).toBe(false);
|
|
94
|
+
expect(isPermissionState("")).toBe(false);
|
|
95
|
+
expect(isPermissionState("block")).toBe(false);
|
|
96
|
+
});
|
|
97
|
+
|
|
98
|
+
test("returns false for non-string types", () => {
|
|
99
|
+
expect(isPermissionState(null)).toBe(false);
|
|
100
|
+
expect(isPermissionState(undefined)).toBe(false);
|
|
101
|
+
expect(isPermissionState(1)).toBe(false);
|
|
102
|
+
expect(isPermissionState({})).toBe(false);
|
|
103
|
+
});
|
|
104
|
+
});
|
|
105
|
+
|
|
106
|
+
describe("isDenyWithReason", () => {
|
|
107
|
+
test("returns true for { action: 'deny' } without a reason", () => {
|
|
108
|
+
expect(isDenyWithReason({ action: "deny" })).toBe(true);
|
|
109
|
+
});
|
|
110
|
+
|
|
111
|
+
test("returns true for { action: 'deny', reason: '...' }", () => {
|
|
112
|
+
expect(isDenyWithReason({ action: "deny", reason: "Use pnpm" })).toBe(true);
|
|
113
|
+
});
|
|
114
|
+
|
|
115
|
+
test("returns false for non-deny actions", () => {
|
|
116
|
+
expect(isDenyWithReason({ action: "allow" })).toBe(false);
|
|
117
|
+
expect(isDenyWithReason({ action: "ask" })).toBe(false);
|
|
118
|
+
});
|
|
119
|
+
|
|
120
|
+
test("returns false for a non-string reason", () => {
|
|
121
|
+
expect(isDenyWithReason({ action: "deny", reason: 42 })).toBe(false);
|
|
122
|
+
expect(isDenyWithReason({ action: "deny", reason: null })).toBe(false);
|
|
123
|
+
});
|
|
124
|
+
|
|
125
|
+
test("returns false for non-object types", () => {
|
|
126
|
+
expect(isDenyWithReason(null)).toBe(false);
|
|
127
|
+
expect(isDenyWithReason(undefined)).toBe(false);
|
|
128
|
+
expect(isDenyWithReason("deny")).toBe(false);
|
|
129
|
+
expect(isDenyWithReason(["deny"])).toBe(false);
|
|
130
|
+
});
|
|
131
|
+
});
|
|
132
|
+
|
|
133
|
+
describe("extractFrontmatter", () => {
|
|
134
|
+
test("returns empty string when no frontmatter delimiter", () => {
|
|
135
|
+
expect(extractFrontmatter("# Hello\nSome content")).toBe("");
|
|
136
|
+
});
|
|
137
|
+
|
|
138
|
+
test("returns empty string when only opening delimiter with no closing", () => {
|
|
139
|
+
expect(extractFrontmatter("---\nkey: value")).toBe("");
|
|
140
|
+
});
|
|
141
|
+
|
|
142
|
+
test("returns frontmatter body between delimiters", () => {
|
|
143
|
+
const markdown = "---\nissue: 1\ntitle: Test\n---\n# Content";
|
|
144
|
+
expect(extractFrontmatter(markdown)).toBe("issue: 1\ntitle: Test");
|
|
145
|
+
});
|
|
146
|
+
|
|
147
|
+
test("returns empty string when file does not start with ---", () => {
|
|
148
|
+
expect(extractFrontmatter("content\n---\nkey: val\n---")).toBe("");
|
|
149
|
+
});
|
|
150
|
+
|
|
151
|
+
test("handles CRLF line endings", () => {
|
|
152
|
+
const markdown = "---\r\nissue: 5\r\n---\r\n# Content";
|
|
153
|
+
expect(extractFrontmatter(markdown)).toBe("issue: 5");
|
|
154
|
+
});
|
|
155
|
+
|
|
156
|
+
test("returns empty string for empty string input", () => {
|
|
157
|
+
expect(extractFrontmatter("")).toBe("");
|
|
158
|
+
});
|
|
159
|
+
|
|
160
|
+
test("returns empty frontmatter for --- \\n--- with nothing between", () => {
|
|
161
|
+
const markdown = "---\n---\n# Content";
|
|
162
|
+
expect(extractFrontmatter(markdown)).toBe("");
|
|
163
|
+
});
|
|
164
|
+
});
|
|
165
|
+
|
|
166
|
+
describe("parseSimpleYamlMap", () => {
|
|
167
|
+
test("returns empty object for empty string", () => {
|
|
168
|
+
expect(parseSimpleYamlMap("")).toEqual({});
|
|
169
|
+
});
|
|
170
|
+
|
|
171
|
+
test("parses simple key-value pairs", () => {
|
|
172
|
+
const yaml = "issue: 21\ntitle: Test";
|
|
173
|
+
expect(parseSimpleYamlMap(yaml)).toEqual({ issue: "21", title: "Test" });
|
|
174
|
+
});
|
|
175
|
+
|
|
176
|
+
test("strips surrounding quotes from values", () => {
|
|
177
|
+
const yaml = 'title: "My Title"';
|
|
178
|
+
expect(parseSimpleYamlMap(yaml)).toEqual({ title: "My Title" });
|
|
179
|
+
|
|
180
|
+
const yaml2 = "title: 'My Title'";
|
|
181
|
+
expect(parseSimpleYamlMap(yaml2)).toEqual({ title: "My Title" });
|
|
182
|
+
});
|
|
183
|
+
|
|
184
|
+
test("skips lines without colon or with colon at position 0", () => {
|
|
185
|
+
const yaml = "no separator here\n:starts-with-colon: val\nkey: val";
|
|
186
|
+
const result = parseSimpleYamlMap(yaml);
|
|
187
|
+
expect(result.key).toBe("val");
|
|
188
|
+
expect(result["no separator here"]).toBeUndefined();
|
|
189
|
+
});
|
|
190
|
+
|
|
191
|
+
test("skips comment lines", () => {
|
|
192
|
+
const yaml = "# This is a comment\nkey: value";
|
|
193
|
+
expect(parseSimpleYamlMap(yaml)).toEqual({ key: "value" });
|
|
194
|
+
});
|
|
195
|
+
|
|
196
|
+
test("skips blank lines", () => {
|
|
197
|
+
const yaml = "\n\nkey: value\n\n";
|
|
198
|
+
expect(parseSimpleYamlMap(yaml)).toEqual({ key: "value" });
|
|
199
|
+
});
|
|
200
|
+
|
|
201
|
+
test("parses nested map (child indented under parent)", () => {
|
|
202
|
+
const yaml = "parent:\n child: nested_value";
|
|
203
|
+
const result = parseSimpleYamlMap(yaml);
|
|
204
|
+
expect(result.parent).toEqual({ child: "nested_value" });
|
|
205
|
+
});
|
|
206
|
+
|
|
207
|
+
test("handles multi-line values correctly (second line is new key)", () => {
|
|
208
|
+
const yaml = "key1: val1\nkey2: val2";
|
|
209
|
+
const result = parseSimpleYamlMap(yaml);
|
|
210
|
+
expect(result.key1).toBe("val1");
|
|
211
|
+
expect(result.key2).toBe("val2");
|
|
212
|
+
});
|
|
213
|
+
|
|
214
|
+
test("strips quotes from keys", () => {
|
|
215
|
+
const yaml = '"quoted-key": value';
|
|
216
|
+
const result = parseSimpleYamlMap(yaml);
|
|
217
|
+
expect(result["quoted-key"]).toBe("value");
|
|
218
|
+
});
|
|
219
|
+
});
|
|
220
|
+
|
|
221
|
+
describe("normalizeOptionalStringArray", () => {
|
|
222
|
+
it("returns the array for a valid string array", () => {
|
|
223
|
+
expect(normalizeOptionalStringArray(["a", "b", "c"])).toEqual([
|
|
224
|
+
"a",
|
|
225
|
+
"b",
|
|
226
|
+
"c",
|
|
227
|
+
]);
|
|
228
|
+
});
|
|
229
|
+
|
|
230
|
+
it("returns an empty array for an empty array", () => {
|
|
231
|
+
expect(normalizeOptionalStringArray([])).toEqual([]);
|
|
232
|
+
});
|
|
233
|
+
|
|
234
|
+
it("returns undefined for a plain string", () => {
|
|
235
|
+
expect(normalizeOptionalStringArray("x")).toBeUndefined();
|
|
236
|
+
});
|
|
237
|
+
|
|
238
|
+
it("returns undefined for a number", () => {
|
|
239
|
+
expect(normalizeOptionalStringArray(42)).toBeUndefined();
|
|
240
|
+
});
|
|
241
|
+
|
|
242
|
+
it("returns undefined for a plain object", () => {
|
|
243
|
+
expect(normalizeOptionalStringArray({ a: "b" })).toBeUndefined();
|
|
244
|
+
});
|
|
245
|
+
|
|
246
|
+
it("returns undefined for a mixed-type array", () => {
|
|
247
|
+
expect(normalizeOptionalStringArray(["a", 1])).toBeUndefined();
|
|
248
|
+
});
|
|
249
|
+
|
|
250
|
+
it("returns undefined for undefined", () => {
|
|
251
|
+
expect(normalizeOptionalStringArray(undefined)).toBeUndefined();
|
|
252
|
+
});
|
|
253
|
+
|
|
254
|
+
it("returns undefined for null", () => {
|
|
255
|
+
expect(normalizeOptionalStringArray(null)).toBeUndefined();
|
|
256
|
+
});
|
|
257
|
+
});
|
|
258
|
+
|
|
259
|
+
describe("normalizeOptionalPositiveInt", () => {
|
|
260
|
+
it("returns the value for a valid positive integer", () => {
|
|
261
|
+
expect(normalizeOptionalPositiveInt(1)).toBe(1);
|
|
262
|
+
expect(normalizeOptionalPositiveInt(200)).toBe(200);
|
|
263
|
+
expect(normalizeOptionalPositiveInt(9999)).toBe(9999);
|
|
264
|
+
});
|
|
265
|
+
|
|
266
|
+
it("returns undefined for zero", () => {
|
|
267
|
+
expect(normalizeOptionalPositiveInt(0)).toBeUndefined();
|
|
268
|
+
});
|
|
269
|
+
|
|
270
|
+
it("returns undefined for negative integers", () => {
|
|
271
|
+
expect(normalizeOptionalPositiveInt(-1)).toBeUndefined();
|
|
272
|
+
expect(normalizeOptionalPositiveInt(-100)).toBeUndefined();
|
|
273
|
+
});
|
|
274
|
+
|
|
275
|
+
it("returns undefined for non-integer numbers (floats)", () => {
|
|
276
|
+
expect(normalizeOptionalPositiveInt(400.5)).toBeUndefined();
|
|
277
|
+
expect(normalizeOptionalPositiveInt(1.1)).toBeUndefined();
|
|
278
|
+
});
|
|
279
|
+
|
|
280
|
+
it("returns undefined for non-number types", () => {
|
|
281
|
+
expect(normalizeOptionalPositiveInt("200")).toBeUndefined();
|
|
282
|
+
expect(normalizeOptionalPositiveInt(true)).toBeUndefined();
|
|
283
|
+
expect(normalizeOptionalPositiveInt(null)).toBeUndefined();
|
|
284
|
+
expect(normalizeOptionalPositiveInt(undefined)).toBeUndefined();
|
|
285
|
+
expect(normalizeOptionalPositiveInt({})).toBeUndefined();
|
|
286
|
+
});
|
|
287
|
+
});
|