@gotgenes/pi-permission-system 17.1.0 → 18.0.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/CHANGELOG.md +33 -0
- package/README.md +1 -0
- package/package.json +1 -1
- package/src/access-intent/access-intent.ts +6 -3
- package/src/access-intent/bash/bash-path-resolver.ts +16 -12
- package/src/access-intent/bash/program.ts +0 -4
- package/src/access-intent/bash/token-classification.ts +23 -0
- package/src/handlers/before-agent-start.ts +1 -2
- package/src/handlers/gates/external-directory.ts +2 -12
- package/src/handlers/gates/skill-read.ts +4 -8
- package/src/handlers/gates/tool-call-gate-pipeline.ts +62 -24
- package/src/handlers/gates/tool.ts +10 -14
- package/src/index.ts +8 -6
- package/src/input-normalizer.ts +54 -56
- package/src/path-normalizer.ts +30 -0
- package/src/permission-event-rpc.ts +21 -15
- package/src/permission-manager.ts +3 -6
- package/src/permission-session.ts +0 -5
- package/src/permissions-service.ts +33 -12
- package/src/skill-prompt-sanitizer.ts +8 -21
- package/test/access-intent/bash/program.test.ts +1 -1
- package/test/access-intent/bash/token-classification.test.ts +75 -0
- package/test/bash-external-directory.test.ts +38 -0
- package/test/composition-root.test.ts +22 -0
- package/test/handlers/external-directory-symlink-acceptance.test.ts +0 -3
- package/test/handlers/gates/external-directory.test.ts +0 -1
- package/test/handlers/gates/skill-read.test.ts +16 -12
- package/test/handlers/gates/tool-call-gate-pipeline.test.ts +73 -0
- package/test/handlers/gates/tool.test.ts +25 -16
- package/test/helpers/gate-fixtures.ts +0 -3
- package/test/input-normalizer.test.ts +163 -270
- package/test/path-normalizer.test.ts +43 -0
- package/test/path-utils.test.ts +1 -1
- package/test/permission-event-rpc.test.ts +80 -65
- package/test/permission-manager-unified.test.ts +134 -145
- package/test/permissions-service.test.ts +84 -72
- package/test/service.test.ts +56 -103
- package/test/skill-prompt-sanitizer.test.ts +31 -65
|
@@ -1,8 +1,12 @@
|
|
|
1
1
|
import { describe, expect, it, vi } from "vitest";
|
|
2
2
|
import { describeSkillReadGate } from "#src/handlers/gates/skill-read";
|
|
3
3
|
import type { ToolCallContext } from "#src/handlers/gates/types";
|
|
4
|
+
import { PathNormalizer } from "#src/path-normalizer";
|
|
4
5
|
import type { SkillPromptEntry } from "#src/skill-prompt-sanitizer";
|
|
5
6
|
|
|
7
|
+
// All test tccs use cwd "/test/project"; one normalizer serves every call.
|
|
8
|
+
const normalizer = new PathNormalizer("linux", "/test/project");
|
|
9
|
+
|
|
6
10
|
// ── SDK stubs ──────────────────────────────────────────────────────────────
|
|
7
11
|
vi.mock("@earendil-works/pi-coding-agent", async (importOriginal) => {
|
|
8
12
|
const original =
|
|
@@ -43,21 +47,21 @@ describe("describeSkillReadGate", () => {
|
|
|
43
47
|
it("returns null when tool is not read", () => {
|
|
44
48
|
const result = describeSkillReadGate(
|
|
45
49
|
makeTcc({ toolName: "write" }),
|
|
46
|
-
|
|
50
|
+
normalizer,
|
|
47
51
|
() => [makeSkillEntry()],
|
|
48
52
|
);
|
|
49
53
|
expect(result).toBeNull();
|
|
50
54
|
});
|
|
51
55
|
|
|
52
56
|
it("returns null when no active skill entries", () => {
|
|
53
|
-
const result = describeSkillReadGate(makeTcc(),
|
|
57
|
+
const result = describeSkillReadGate(makeTcc(), normalizer, () => []);
|
|
54
58
|
expect(result).toBeNull();
|
|
55
59
|
});
|
|
56
60
|
|
|
57
61
|
it("returns null when read path does not match any skill", () => {
|
|
58
62
|
const result = describeSkillReadGate(
|
|
59
63
|
makeTcc({ input: { path: "/test/project/src/index.ts" } }),
|
|
60
|
-
|
|
64
|
+
normalizer,
|
|
61
65
|
() => [makeSkillEntry()],
|
|
62
66
|
);
|
|
63
67
|
expect(result).toBeNull();
|
|
@@ -66,14 +70,14 @@ describe("describeSkillReadGate", () => {
|
|
|
66
70
|
it("returns null when input has no path", () => {
|
|
67
71
|
const result = describeSkillReadGate(
|
|
68
72
|
makeTcc({ input: {} }),
|
|
69
|
-
|
|
73
|
+
normalizer,
|
|
70
74
|
() => [makeSkillEntry()],
|
|
71
75
|
);
|
|
72
76
|
expect(result).toBeNull();
|
|
73
77
|
});
|
|
74
78
|
|
|
75
79
|
it("returns GateDescriptor with preResolved.state matching skill entry state (ask)", () => {
|
|
76
|
-
const result = describeSkillReadGate(makeTcc(),
|
|
80
|
+
const result = describeSkillReadGate(makeTcc(), normalizer, () => [
|
|
77
81
|
makeSkillEntry({ state: "ask" }),
|
|
78
82
|
]);
|
|
79
83
|
expect(result).not.toBeNull();
|
|
@@ -82,7 +86,7 @@ describe("describeSkillReadGate", () => {
|
|
|
82
86
|
});
|
|
83
87
|
|
|
84
88
|
it("returns GateDescriptor with preResolved.state matching skill entry state (allow)", () => {
|
|
85
|
-
const result = describeSkillReadGate(makeTcc(),
|
|
89
|
+
const result = describeSkillReadGate(makeTcc(), normalizer, () => [
|
|
86
90
|
makeSkillEntry({ state: "allow" }),
|
|
87
91
|
]);
|
|
88
92
|
expect(result).not.toBeNull();
|
|
@@ -91,7 +95,7 @@ describe("describeSkillReadGate", () => {
|
|
|
91
95
|
});
|
|
92
96
|
|
|
93
97
|
it("returns GateDescriptor with preResolved.state matching skill entry state (deny)", () => {
|
|
94
|
-
const result = describeSkillReadGate(makeTcc(),
|
|
98
|
+
const result = describeSkillReadGate(makeTcc(), normalizer, () => [
|
|
95
99
|
makeSkillEntry({ state: "deny" }),
|
|
96
100
|
]);
|
|
97
101
|
expect(result).not.toBeNull();
|
|
@@ -100,7 +104,7 @@ describe("describeSkillReadGate", () => {
|
|
|
100
104
|
});
|
|
101
105
|
|
|
102
106
|
it("decision surface is 'skill' and decision value is the skill name", () => {
|
|
103
|
-
const result = describeSkillReadGate(makeTcc(),
|
|
107
|
+
const result = describeSkillReadGate(makeTcc(), normalizer, () => [
|
|
104
108
|
makeSkillEntry({ name: "my-skill" }),
|
|
105
109
|
])!;
|
|
106
110
|
expect(result.decision.surface).toBe("skill");
|
|
@@ -108,7 +112,7 @@ describe("describeSkillReadGate", () => {
|
|
|
108
112
|
});
|
|
109
113
|
|
|
110
114
|
it("denialContext contains the skill name and read path", () => {
|
|
111
|
-
const result = describeSkillReadGate(makeTcc(),
|
|
115
|
+
const result = describeSkillReadGate(makeTcc(), normalizer, () => [
|
|
112
116
|
makeSkillEntry({ name: "librarian" }),
|
|
113
117
|
])!;
|
|
114
118
|
expect(result.denialContext).toEqual({
|
|
@@ -122,7 +126,7 @@ describe("describeSkillReadGate", () => {
|
|
|
122
126
|
it("promptDetails includes skill_read source and skillName", () => {
|
|
123
127
|
const result = describeSkillReadGate(
|
|
124
128
|
makeTcc({ agentName: "test-agent", toolCallId: "tc-42" }),
|
|
125
|
-
|
|
129
|
+
normalizer,
|
|
126
130
|
() => [makeSkillEntry({ name: "my-skill" })],
|
|
127
131
|
)!;
|
|
128
132
|
expect(result.promptDetails).toMatchObject({
|
|
@@ -138,7 +142,7 @@ describe("describeSkillReadGate", () => {
|
|
|
138
142
|
it("logContext includes skill_read source and skillName", () => {
|
|
139
143
|
const result = describeSkillReadGate(
|
|
140
144
|
makeTcc({ agentName: "agent-1" }),
|
|
141
|
-
|
|
145
|
+
normalizer,
|
|
142
146
|
() => [makeSkillEntry({ name: "librarian" })],
|
|
143
147
|
)!;
|
|
144
148
|
expect(result.logContext).toMatchObject({
|
|
@@ -149,7 +153,7 @@ describe("describeSkillReadGate", () => {
|
|
|
149
153
|
});
|
|
150
154
|
|
|
151
155
|
it("surface is 'skill' on the descriptor", () => {
|
|
152
|
-
const result = describeSkillReadGate(makeTcc(),
|
|
156
|
+
const result = describeSkillReadGate(makeTcc(), normalizer, () => [
|
|
153
157
|
makeSkillEntry(),
|
|
154
158
|
])!;
|
|
155
159
|
expect(result.surface).toBe("skill");
|
|
@@ -22,6 +22,16 @@ vi.mock("#src/access-intent/bash/program", () => ({
|
|
|
22
22
|
BashProgram: { parse: mockBashProgramParse },
|
|
23
23
|
}));
|
|
24
24
|
|
|
25
|
+
// Mock node:fs so realpathSync (used by canonicalizePath) is controllable for
|
|
26
|
+
// the per-tool symlink-resolution test. Default implementation is identity.
|
|
27
|
+
const realpathSync = vi.hoisted(() =>
|
|
28
|
+
vi.fn<(path: string) => string>((p) => p),
|
|
29
|
+
);
|
|
30
|
+
vi.mock("node:fs", () => ({
|
|
31
|
+
realpathSync,
|
|
32
|
+
default: { realpathSync },
|
|
33
|
+
}));
|
|
34
|
+
|
|
25
35
|
function makeMockBashProgram() {
|
|
26
36
|
return {
|
|
27
37
|
commands: vi.fn<() => []>(() => []),
|
|
@@ -36,6 +46,8 @@ describe("ToolCallGatePipeline", () => {
|
|
|
36
46
|
beforeEach(() => {
|
|
37
47
|
mockBashProgramParse.mockReset();
|
|
38
48
|
mockBashProgramParse.mockResolvedValue(makeMockBashProgram());
|
|
49
|
+
realpathSync.mockReset();
|
|
50
|
+
realpathSync.mockImplementation((p: string) => p);
|
|
39
51
|
});
|
|
40
52
|
|
|
41
53
|
// ── non-bash tools ───────────────────────────────────────────────────────
|
|
@@ -254,4 +266,65 @@ describe("ToolCallGatePipeline", () => {
|
|
|
254
266
|
expect(result).toEqual({ action: "allow" });
|
|
255
267
|
});
|
|
256
268
|
});
|
|
269
|
+
|
|
270
|
+
// ── per-tool path-bearing gate (#502) ────────────────────────────────────
|
|
271
|
+
|
|
272
|
+
describe("evaluate — per-tool path-bearing gate (#502)", () => {
|
|
273
|
+
it("emits an access-path intent on the tool-name surface for a path-bearing tool", async () => {
|
|
274
|
+
const resolver = makeResolver(makeCheckResult());
|
|
275
|
+
const inputs = makeGateInputs();
|
|
276
|
+
const { runner } = makeGateRunner();
|
|
277
|
+
const pipeline = new ToolCallGatePipeline(resolver, inputs);
|
|
278
|
+
|
|
279
|
+
await pipeline.evaluate(
|
|
280
|
+
makeTcc({ toolName: "read", input: { path: "/test/cwd/foo.ts" } }),
|
|
281
|
+
runner,
|
|
282
|
+
);
|
|
283
|
+
|
|
284
|
+
const perTool = resolver.resolve.mock.calls.find(
|
|
285
|
+
([intent]) => intent.surface === "read",
|
|
286
|
+
);
|
|
287
|
+
expect(perTool?.[0].kind).toBe("access-path");
|
|
288
|
+
});
|
|
289
|
+
|
|
290
|
+
it("keeps a path-bearing tool with no path on the tool intent", async () => {
|
|
291
|
+
const resolver = makeResolver(makeCheckResult());
|
|
292
|
+
const inputs = makeGateInputs();
|
|
293
|
+
const { runner } = makeGateRunner();
|
|
294
|
+
const pipeline = new ToolCallGatePipeline(resolver, inputs);
|
|
295
|
+
|
|
296
|
+
await pipeline.evaluate(makeTcc({ toolName: "read", input: {} }), runner);
|
|
297
|
+
|
|
298
|
+
const perTool = resolver.resolve.mock.calls.find(
|
|
299
|
+
([intent]) => intent.surface === "read",
|
|
300
|
+
);
|
|
301
|
+
expect(perTool?.[0].kind).toBe("tool");
|
|
302
|
+
});
|
|
303
|
+
|
|
304
|
+
it("blocks when a per-tool rule matches the symlink-resolved form", async () => {
|
|
305
|
+
// /test/cwd/foo.env is a symlink to /vault/foo.env; the per-tool rule is
|
|
306
|
+
// keyed on the resolved target, which is only reachable via matchValues().
|
|
307
|
+
realpathSync.mockImplementation((p: string) =>
|
|
308
|
+
p === "/test/cwd/foo.env" ? "/vault/foo.env" : p,
|
|
309
|
+
);
|
|
310
|
+
const resolver = makeResolver();
|
|
311
|
+
resolver.resolve.mockImplementation((intent) =>
|
|
312
|
+
intent.kind === "access-path" &&
|
|
313
|
+
intent.surface === "read" &&
|
|
314
|
+
intent.path.matchValues().includes("/vault/foo.env")
|
|
315
|
+
? makeCheckResult({ state: "deny", matchedPattern: "*.env" })
|
|
316
|
+
: makeCheckResult(),
|
|
317
|
+
);
|
|
318
|
+
const inputs = makeGateInputs();
|
|
319
|
+
const { runner } = makeGateRunner();
|
|
320
|
+
const pipeline = new ToolCallGatePipeline(resolver, inputs);
|
|
321
|
+
|
|
322
|
+
const result = await pipeline.evaluate(
|
|
323
|
+
makeTcc({ toolName: "read", input: { path: "/test/cwd/foo.env" } }),
|
|
324
|
+
runner,
|
|
325
|
+
);
|
|
326
|
+
|
|
327
|
+
expect(result).toMatchObject({ action: "block" });
|
|
328
|
+
});
|
|
329
|
+
});
|
|
257
330
|
});
|
|
@@ -2,6 +2,7 @@ import { describe, expect, it } from "vitest";
|
|
|
2
2
|
|
|
3
3
|
import { describeToolGate } from "#src/handlers/gates/tool";
|
|
4
4
|
import type { ToolCallContext } from "#src/handlers/gates/types";
|
|
5
|
+
import { PathNormalizer } from "#src/path-normalizer";
|
|
5
6
|
import {
|
|
6
7
|
TOOL_INPUT_LOG_PREVIEW_MAX_LENGTH,
|
|
7
8
|
TOOL_INPUT_PREVIEW_MAX_LENGTH,
|
|
@@ -45,6 +46,10 @@ function makeCheckResult(
|
|
|
45
46
|
};
|
|
46
47
|
}
|
|
47
48
|
|
|
49
|
+
// The per-tool gate now receives the AccessPath the pipeline builds, bound to
|
|
50
|
+
// the makeTcc default cwd; approval values derive from `accessPath.value()`.
|
|
51
|
+
const normalizer = new PathNormalizer("linux", "/test/project");
|
|
52
|
+
|
|
48
53
|
// ── tests ──────────────────────────────────────────────────────────────────
|
|
49
54
|
|
|
50
55
|
describe("describeToolGate", () => {
|
|
@@ -53,7 +58,6 @@ describe("describeToolGate", () => {
|
|
|
53
58
|
makeTcc({ toolName: "read" }),
|
|
54
59
|
makeCheckResult("ask"),
|
|
55
60
|
makeFormatter(),
|
|
56
|
-
"linux",
|
|
57
61
|
);
|
|
58
62
|
expect(desc.surface).toBe("read");
|
|
59
63
|
expect(desc.decision.surface).toBe("read");
|
|
@@ -64,7 +68,6 @@ describe("describeToolGate", () => {
|
|
|
64
68
|
makeTcc({ toolName: "write" }),
|
|
65
69
|
makeCheckResult("ask"),
|
|
66
70
|
makeFormatter(),
|
|
67
|
-
"linux",
|
|
68
71
|
);
|
|
69
72
|
expect(desc.decision.value).toBe("write");
|
|
70
73
|
});
|
|
@@ -78,7 +81,6 @@ describe("describeToolGate", () => {
|
|
|
78
81
|
makeTcc({ toolName: "bash", input: { command: "git status" } }),
|
|
79
82
|
check,
|
|
80
83
|
makeFormatter(),
|
|
81
|
-
"linux",
|
|
82
84
|
);
|
|
83
85
|
expect(desc.surface).toBe("bash");
|
|
84
86
|
expect(desc.decision.surface).toBe("bash");
|
|
@@ -94,7 +96,6 @@ describe("describeToolGate", () => {
|
|
|
94
96
|
makeTcc({ toolName: "mcp", input: { tool: "server:tool" } }),
|
|
95
97
|
check,
|
|
96
98
|
makeFormatter(),
|
|
97
|
-
"linux",
|
|
98
99
|
);
|
|
99
100
|
expect(desc.surface).toBe("mcp");
|
|
100
101
|
expect(desc.decision.surface).toBe("mcp");
|
|
@@ -103,7 +104,7 @@ describe("describeToolGate", () => {
|
|
|
103
104
|
|
|
104
105
|
it("populates denialContext with kind 'tool' and check result", () => {
|
|
105
106
|
const check = makeCheckResult("deny", { toolName: "read" });
|
|
106
|
-
const desc = describeToolGate(makeTcc(), check, makeFormatter()
|
|
107
|
+
const desc = describeToolGate(makeTcc(), check, makeFormatter());
|
|
107
108
|
expect(desc.denialContext).toEqual({
|
|
108
109
|
kind: "tool",
|
|
109
110
|
check,
|
|
@@ -118,7 +119,6 @@ describe("describeToolGate", () => {
|
|
|
118
119
|
makeTcc({ agentName: "my-agent" }),
|
|
119
120
|
check,
|
|
120
121
|
makeFormatter(),
|
|
121
|
-
"linux",
|
|
122
122
|
);
|
|
123
123
|
expect(desc.denialContext.agentName).toBe("my-agent");
|
|
124
124
|
});
|
|
@@ -129,7 +129,6 @@ describe("describeToolGate", () => {
|
|
|
129
129
|
makeTcc({ toolName: "bash", input: { command: "ls" } }),
|
|
130
130
|
check,
|
|
131
131
|
makeFormatter(),
|
|
132
|
-
"linux",
|
|
133
132
|
);
|
|
134
133
|
expect(desc.denialContext).toMatchObject({
|
|
135
134
|
kind: "tool",
|
|
@@ -146,7 +145,6 @@ describe("describeToolGate", () => {
|
|
|
146
145
|
makeTcc({ toolName: "bash", input: { command: "git status" } }),
|
|
147
146
|
check,
|
|
148
147
|
makeFormatter(),
|
|
149
|
-
"linux",
|
|
150
148
|
);
|
|
151
149
|
expect(desc.sessionApproval).toBeDefined();
|
|
152
150
|
expect(desc.sessionApproval?.surface).toBe("bash");
|
|
@@ -163,16 +161,17 @@ describe("describeToolGate", () => {
|
|
|
163
161
|
}),
|
|
164
162
|
check,
|
|
165
163
|
makeFormatter(),
|
|
166
|
-
"
|
|
164
|
+
normalizer.forPath("index.html"),
|
|
167
165
|
);
|
|
168
166
|
expect(desc.sessionApproval?.surface).toBe("edit");
|
|
169
167
|
expect(desc.sessionApproval?.representativePattern).toBe("/test/project/*");
|
|
170
168
|
});
|
|
171
169
|
|
|
172
170
|
it("resolves a sub-directory file's session approval to an absolute pattern", () => {
|
|
173
|
-
//
|
|
174
|
-
// so sub-directory approvals are absolute too — the deliberate
|
|
175
|
-
// that keeps the pattern aligned with the policy values it is
|
|
171
|
+
// The approval value derives from the AccessPath's lexical absolute form
|
|
172
|
+
// (`value()`), so sub-directory approvals are absolute too — the deliberate
|
|
173
|
+
// tradeoff that keeps the pattern aligned with the policy values it is
|
|
174
|
+
// matched against.
|
|
176
175
|
const check = makeCheckResult("ask", { toolName: "edit" });
|
|
177
176
|
const desc = describeToolGate(
|
|
178
177
|
makeTcc({
|
|
@@ -182,20 +181,31 @@ describe("describeToolGate", () => {
|
|
|
182
181
|
}),
|
|
183
182
|
check,
|
|
184
183
|
makeFormatter(),
|
|
185
|
-
"
|
|
184
|
+
normalizer.forPath("src/foo.ts"),
|
|
186
185
|
);
|
|
187
186
|
expect(desc.sessionApproval?.representativePattern).toBe(
|
|
188
187
|
"/test/project/src/*",
|
|
189
188
|
);
|
|
190
189
|
});
|
|
191
190
|
|
|
191
|
+
it("falls back to a wildcard session approval when no AccessPath is given", () => {
|
|
192
|
+
// A path-bearing tool with no `input.path` keeps the `tool` intent and gets
|
|
193
|
+
// no AccessPath, so the suggestion collapses to the catch-all.
|
|
194
|
+
const desc = describeToolGate(
|
|
195
|
+
makeTcc({ toolName: "read", input: {} }),
|
|
196
|
+
makeCheckResult("ask"),
|
|
197
|
+
makeFormatter(),
|
|
198
|
+
);
|
|
199
|
+
expect(desc.sessionApproval?.surface).toBe("read");
|
|
200
|
+
expect(desc.sessionApproval?.representativePattern).toBe("*");
|
|
201
|
+
});
|
|
202
|
+
|
|
192
203
|
it("populates promptDetails with correct fields", () => {
|
|
193
204
|
const check = makeCheckResult("ask");
|
|
194
205
|
const desc = describeToolGate(
|
|
195
206
|
makeTcc({ toolName: "read", agentName: "my-agent", toolCallId: "tc-42" }),
|
|
196
207
|
check,
|
|
197
208
|
makeFormatter(),
|
|
198
|
-
"linux",
|
|
199
209
|
);
|
|
200
210
|
expect(desc.promptDetails).toMatchObject({
|
|
201
211
|
source: "tool_call",
|
|
@@ -213,7 +223,6 @@ describe("describeToolGate", () => {
|
|
|
213
223
|
makeTcc({ toolName: "bash", input: { command: "ls" } }),
|
|
214
224
|
check,
|
|
215
225
|
makeFormatter(),
|
|
216
|
-
"linux",
|
|
217
226
|
);
|
|
218
227
|
expect(desc.logContext).toMatchObject({
|
|
219
228
|
source: "tool_call",
|
|
@@ -227,7 +236,7 @@ describe("describeToolGate", () => {
|
|
|
227
236
|
makeTcc({ toolName: "edit", input: { path: "/a.ts" } }),
|
|
228
237
|
makeCheckResult("ask", { toolName: "edit" }),
|
|
229
238
|
makeFormatter(),
|
|
230
|
-
"
|
|
239
|
+
normalizer.forPath("/a.ts"),
|
|
231
240
|
);
|
|
232
241
|
expect(desc.surface).toBe("edit");
|
|
233
242
|
expect(desc.input).toEqual({ path: "/a.ts" });
|
|
@@ -255,7 +255,6 @@ export function makeGateInputs(
|
|
|
255
255
|
getInfrastructureReadDirs?: () => string[];
|
|
256
256
|
getToolPreviewLimits?: () => ToolPreviewFormatterOptions;
|
|
257
257
|
getPathNormalizer?: () => PathNormalizer;
|
|
258
|
-
getPlatform?: () => NodeJS.Platform;
|
|
259
258
|
} = {},
|
|
260
259
|
): ToolCallGateInputs {
|
|
261
260
|
return {
|
|
@@ -276,8 +275,6 @@ export function makeGateInputs(
|
|
|
276
275
|
vi.fn<() => PathNormalizer>(
|
|
277
276
|
() => new PathNormalizer(process.platform, "/test/cwd"),
|
|
278
277
|
),
|
|
279
|
-
getPlatform:
|
|
280
|
-
overrides.getPlatform ?? vi.fn<() => NodeJS.Platform>(() => "linux"),
|
|
281
278
|
};
|
|
282
279
|
}
|
|
283
280
|
|