@gotgenes/pi-permission-system 10.1.0 → 10.3.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 +14 -0
- package/package.json +1 -1
- package/src/config-modal.ts +7 -10
- package/src/config-store.ts +243 -0
- package/src/index.ts +11 -15
- package/src/permission-manager.ts +69 -3
- package/src/permission-prompter.ts +3 -3
- package/src/permission-session.ts +13 -28
- package/src/runtime.ts +34 -203
- package/test/config-modal.test.ts +15 -10
- package/test/config-store.test.ts +452 -0
- package/test/handlers/external-directory-integration.test.ts +81 -176
- package/test/handlers/gates/bash-path.test.ts +26 -44
- package/test/handlers/gates/runner.test.ts +27 -119
- package/test/handlers/tool-call.test.ts +44 -153
- package/test/helpers/gate-fixtures.ts +66 -2
- package/test/helpers/handler-fixtures.ts +83 -2
- package/test/permission-manager-unified.test.ts +159 -1
- package/test/permission-prompter.test.ts +12 -5
- package/test/permission-session.test.ts +111 -120
- package/test/runtime.test.ts +11 -275
package/test/runtime.test.ts
CHANGED
|
@@ -1,4 +1,3 @@
|
|
|
1
|
-
import { join } from "node:path";
|
|
2
1
|
import { beforeEach, describe, expect, it, vi } from "vitest";
|
|
3
2
|
|
|
4
3
|
// ── logger stub ────────────────────────────────────────────────────────────
|
|
@@ -6,9 +5,6 @@ const {
|
|
|
6
5
|
mockLoggerDebug,
|
|
7
6
|
mockLoggerReview,
|
|
8
7
|
mockCreateLogger,
|
|
9
|
-
mockLoadAndMergeConfigs,
|
|
10
|
-
mockSyncPermissionSystemStatus,
|
|
11
|
-
mockBuildResolvedConfigLogEntry,
|
|
12
8
|
mockDiscoverGlobalNodeModulesRoot,
|
|
13
9
|
} = vi.hoisted(() => ({
|
|
14
10
|
mockLoggerDebug:
|
|
@@ -20,9 +16,6 @@ const {
|
|
|
20
16
|
(event: string, details?: Record<string, unknown>) => string | undefined
|
|
21
17
|
>(),
|
|
22
18
|
mockCreateLogger: vi.fn(),
|
|
23
|
-
mockLoadAndMergeConfigs: vi.fn(),
|
|
24
|
-
mockSyncPermissionSystemStatus: vi.fn(),
|
|
25
|
-
mockBuildResolvedConfigLogEntry: vi.fn(),
|
|
26
19
|
mockDiscoverGlobalNodeModulesRoot: vi.fn<() => string | null>(),
|
|
27
20
|
}));
|
|
28
21
|
|
|
@@ -34,21 +27,6 @@ vi.mock("../src/permission-manager", () => ({
|
|
|
34
27
|
PermissionManager: vi.fn(),
|
|
35
28
|
}));
|
|
36
29
|
|
|
37
|
-
vi.mock("../src/config-loader", () => ({
|
|
38
|
-
loadAndMergeConfigs: mockLoadAndMergeConfigs,
|
|
39
|
-
loadUnifiedConfig: vi.fn().mockReturnValue({ config: {} }),
|
|
40
|
-
}));
|
|
41
|
-
|
|
42
|
-
vi.mock("../src/status", () => ({
|
|
43
|
-
PERMISSION_SYSTEM_STATUS_KEY: "permission-system",
|
|
44
|
-
syncPermissionSystemStatus: mockSyncPermissionSystemStatus,
|
|
45
|
-
getPermissionSystemStatus: vi.fn(),
|
|
46
|
-
}));
|
|
47
|
-
|
|
48
|
-
vi.mock("../src/config-reporter", () => ({
|
|
49
|
-
buildResolvedConfigLogEntry: mockBuildResolvedConfigLogEntry,
|
|
50
|
-
}));
|
|
51
|
-
|
|
52
30
|
vi.mock("../src/subagent-context", () => ({
|
|
53
31
|
isSubagentExecutionContext: vi.fn().mockReturnValue(false),
|
|
54
32
|
}));
|
|
@@ -62,20 +40,9 @@ vi.mock("../src/session-rules", () => ({
|
|
|
62
40
|
deriveApprovalPattern: vi.fn(),
|
|
63
41
|
}));
|
|
64
42
|
|
|
65
|
-
import
|
|
66
|
-
import {
|
|
67
|
-
getGlobalConfigPath,
|
|
68
|
-
getGlobalLogsDir,
|
|
69
|
-
getProjectConfigPath,
|
|
70
|
-
} from "#src/config-paths";
|
|
43
|
+
import { getGlobalLogsDir } from "#src/config-paths";
|
|
71
44
|
import { DEFAULT_EXTENSION_CONFIG } from "#src/extension-config";
|
|
72
|
-
import {
|
|
73
|
-
import {
|
|
74
|
-
createExtensionRuntime,
|
|
75
|
-
createPermissionManagerForCwd,
|
|
76
|
-
derivePiProjectPaths,
|
|
77
|
-
refreshExtensionConfig,
|
|
78
|
-
} from "#src/runtime";
|
|
45
|
+
import { createExtensionRuntime } from "#src/runtime";
|
|
79
46
|
|
|
80
47
|
// ── test suite ─────────────────────────────────────────────────────────────
|
|
81
48
|
|
|
@@ -162,9 +129,9 @@ describe("createExtensionRuntime", () => {
|
|
|
162
129
|
|
|
163
130
|
// ── Default mutable state ────────────────────────────────────────────────
|
|
164
131
|
|
|
165
|
-
it("initializes
|
|
132
|
+
it("initializes configStore.current() to DEFAULT_EXTENSION_CONFIG", () => {
|
|
166
133
|
const runtime = createExtensionRuntime({ agentDir: "/test/agent" });
|
|
167
|
-
expect(runtime.
|
|
134
|
+
expect(runtime.configStore.current()).toEqual(DEFAULT_EXTENSION_CONFIG);
|
|
168
135
|
});
|
|
169
136
|
|
|
170
137
|
it("initializes runtimeContext to null", () => {
|
|
@@ -192,11 +159,6 @@ describe("createExtensionRuntime", () => {
|
|
|
192
159
|
expect(runtime.lastPromptStateCacheKey).toBeNull();
|
|
193
160
|
});
|
|
194
161
|
|
|
195
|
-
it("initializes lastConfigWarning to null", () => {
|
|
196
|
-
const runtime = createExtensionRuntime({ agentDir: "/test/agent" });
|
|
197
|
-
expect(runtime.lastConfigWarning).toBeNull();
|
|
198
|
-
});
|
|
199
|
-
|
|
200
162
|
it("creates a sessionRules instance", () => {
|
|
201
163
|
const runtime = createExtensionRuntime({ agentDir: "/test/agent" });
|
|
202
164
|
expect(runtime.sessionRules).toBeDefined();
|
|
@@ -204,17 +166,6 @@ describe("createExtensionRuntime", () => {
|
|
|
204
166
|
|
|
205
167
|
// ── Mutable state is writable ──────────────────────────────────────────
|
|
206
168
|
|
|
207
|
-
it("allows config to be updated", () => {
|
|
208
|
-
const runtime = createExtensionRuntime({ agentDir: "/test/agent" });
|
|
209
|
-
const newConfig = {
|
|
210
|
-
debugLog: true,
|
|
211
|
-
permissionReviewLog: false,
|
|
212
|
-
yoloMode: false,
|
|
213
|
-
};
|
|
214
|
-
runtime.config = newConfig;
|
|
215
|
-
expect(runtime.config).toEqual(newConfig);
|
|
216
|
-
});
|
|
217
|
-
|
|
218
169
|
it("allows runtimeContext to be updated", () => {
|
|
219
170
|
const runtime = createExtensionRuntime({ agentDir: "/test/agent" });
|
|
220
171
|
const mockCtx = { hasUI: false } as never;
|
|
@@ -237,19 +188,13 @@ describe("createExtensionRuntime", () => {
|
|
|
237
188
|
expect(opts.reviewLogPath).toContain(expectedLogsDir);
|
|
238
189
|
});
|
|
239
190
|
|
|
240
|
-
it("passes getConfig that reads
|
|
241
|
-
|
|
191
|
+
it("passes getConfig that reads from configStore.current()", () => {
|
|
192
|
+
createExtensionRuntime({ agentDir: "/test/agent" });
|
|
242
193
|
const opts = mockCreateLogger.mock.calls[0][0] as {
|
|
243
194
|
getConfig: () => typeof DEFAULT_EXTENSION_CONFIG;
|
|
244
195
|
};
|
|
245
|
-
|
|
246
|
-
|
|
247
|
-
permissionReviewLog: false,
|
|
248
|
-
yoloMode: false,
|
|
249
|
-
};
|
|
250
|
-
runtime.config = updatedConfig;
|
|
251
|
-
// getConfig() should reflect the updated value
|
|
252
|
-
expect(opts.getConfig()).toEqual(updatedConfig);
|
|
196
|
+
// getConfig() reads from configStore.current() — DEFAULT_EXTENSION_CONFIG at startup
|
|
197
|
+
expect(opts.getConfig()).toEqual(DEFAULT_EXTENSION_CONFIG);
|
|
253
198
|
});
|
|
254
199
|
|
|
255
200
|
// ── writeDebugLog delegates to logger.debug ──────────────────────────────
|
|
@@ -351,217 +296,8 @@ describe("createExtensionRuntime", () => {
|
|
|
351
296
|
});
|
|
352
297
|
});
|
|
353
298
|
|
|
354
|
-
//
|
|
355
|
-
|
|
356
|
-
describe("derivePiProjectPaths", () => {
|
|
357
|
-
it("returns null for null cwd", () => {
|
|
358
|
-
expect(derivePiProjectPaths(null)).toBeNull();
|
|
359
|
-
});
|
|
360
|
-
|
|
361
|
-
it("returns null for undefined cwd", () => {
|
|
362
|
-
expect(derivePiProjectPaths(undefined)).toBeNull();
|
|
363
|
-
});
|
|
364
|
-
|
|
365
|
-
it("returns null for empty string cwd", () => {
|
|
366
|
-
expect(derivePiProjectPaths("")).toBeNull();
|
|
367
|
-
});
|
|
368
|
-
|
|
369
|
-
it("returns projectGlobalConfigPath via getProjectConfigPath", () => {
|
|
370
|
-
const result = derivePiProjectPaths("/my/project");
|
|
371
|
-
expect(result?.projectGlobalConfigPath).toBe(
|
|
372
|
-
getProjectConfigPath("/my/project"),
|
|
373
|
-
);
|
|
374
|
-
});
|
|
375
|
-
|
|
376
|
-
it("returns projectAgentsDir as .pi/agent/agents under cwd", () => {
|
|
377
|
-
const result = derivePiProjectPaths("/my/project");
|
|
378
|
-
expect(result?.projectAgentsDir).toBe(
|
|
379
|
-
join("/my/project", ".pi", "agent", "agents"),
|
|
380
|
-
);
|
|
381
|
-
});
|
|
382
|
-
});
|
|
383
|
-
|
|
384
|
-
// ── createPermissionManagerForCwd ─────────────────────────────────────────
|
|
385
|
-
|
|
386
|
-
describe("createPermissionManagerForCwd", () => {
|
|
387
|
-
beforeEach(() => {
|
|
388
|
-
// PermissionManager is already mocked as vi.fn() at module scope.
|
|
389
|
-
});
|
|
390
|
-
|
|
391
|
-
it("creates a PermissionManager with globalConfigPath from agentDir", () => {
|
|
392
|
-
const MockPM = PermissionManager as ReturnType<typeof vi.fn>;
|
|
393
|
-
MockPM.mockClear();
|
|
394
|
-
createPermissionManagerForCwd("/test/agent", null);
|
|
395
|
-
expect(MockPM).toHaveBeenCalledWith(
|
|
396
|
-
expect.objectContaining({
|
|
397
|
-
globalConfigPath: getGlobalConfigPath("/test/agent"),
|
|
398
|
-
}),
|
|
399
|
-
);
|
|
400
|
-
});
|
|
401
|
-
|
|
402
|
-
it("includes projectGlobalConfigPath when cwd is provided", () => {
|
|
403
|
-
const MockPM = PermissionManager as ReturnType<typeof vi.fn>;
|
|
404
|
-
MockPM.mockClear();
|
|
405
|
-
createPermissionManagerForCwd("/test/agent", "/my/project");
|
|
406
|
-
expect(MockPM).toHaveBeenCalledWith(
|
|
407
|
-
expect.objectContaining({
|
|
408
|
-
globalConfigPath: getGlobalConfigPath("/test/agent"),
|
|
409
|
-
projectGlobalConfigPath: getProjectConfigPath("/my/project"),
|
|
410
|
-
}),
|
|
411
|
-
);
|
|
412
|
-
});
|
|
413
|
-
|
|
414
|
-
it("excludes projectGlobalConfigPath when cwd is null", () => {
|
|
415
|
-
const MockPM = PermissionManager as ReturnType<typeof vi.fn>;
|
|
416
|
-
MockPM.mockClear();
|
|
417
|
-
createPermissionManagerForCwd("/test/agent", null);
|
|
418
|
-
const callArg = MockPM.mock.calls[0][0] as Record<string, unknown>;
|
|
419
|
-
expect(callArg.projectGlobalConfigPath).toBeUndefined();
|
|
420
|
-
});
|
|
421
|
-
});
|
|
422
|
-
|
|
423
|
-
// ── refreshExtensionConfig ────────────────────────────────────────────────
|
|
424
|
-
|
|
425
|
-
describe("refreshExtensionConfig", () => {
|
|
426
|
-
function makeRuntime() {
|
|
427
|
-
mockCreateLogger.mockReturnValue({
|
|
428
|
-
debug: mockLoggerDebug,
|
|
429
|
-
review: mockLoggerReview,
|
|
430
|
-
});
|
|
431
|
-
return createExtensionRuntime({ agentDir: "/test/agent" });
|
|
432
|
-
}
|
|
433
|
-
|
|
434
|
-
function makeCtx(
|
|
435
|
-
overrides: Partial<ExtensionContext> = {},
|
|
436
|
-
): ExtensionContext {
|
|
437
|
-
return {
|
|
438
|
-
cwd: "/test/project",
|
|
439
|
-
hasUI: false,
|
|
440
|
-
ui: { notify: vi.fn(), setStatus: vi.fn() },
|
|
441
|
-
sessionManager: { getEntries: vi.fn(), addEntry: vi.fn() },
|
|
442
|
-
...overrides,
|
|
443
|
-
} as unknown as ExtensionContext;
|
|
444
|
-
}
|
|
445
|
-
|
|
446
|
-
beforeEach(() => {
|
|
447
|
-
mockLoggerDebug.mockReset().mockReturnValue(undefined);
|
|
448
|
-
mockLoggerReview.mockReset().mockReturnValue(undefined);
|
|
449
|
-
mockLoadAndMergeConfigs.mockReset().mockReturnValue({
|
|
450
|
-
merged: { ...DEFAULT_EXTENSION_CONFIG },
|
|
451
|
-
issues: [],
|
|
452
|
-
});
|
|
453
|
-
mockSyncPermissionSystemStatus.mockReset();
|
|
454
|
-
});
|
|
455
|
-
|
|
456
|
-
it("updates runtime.runtimeContext when ctx is provided", () => {
|
|
457
|
-
const runtime = makeRuntime();
|
|
458
|
-
const ctx = makeCtx();
|
|
459
|
-
refreshExtensionConfig(runtime, ctx);
|
|
460
|
-
expect(runtime.runtimeContext).toBe(ctx);
|
|
461
|
-
});
|
|
462
|
-
|
|
463
|
-
it("does not override runtimeContext when ctx is omitted", () => {
|
|
464
|
-
const runtime = makeRuntime();
|
|
465
|
-
const existing = makeCtx();
|
|
466
|
-
runtime.runtimeContext = existing;
|
|
467
|
-
refreshExtensionConfig(runtime);
|
|
468
|
-
expect(runtime.runtimeContext).toBe(existing);
|
|
469
|
-
});
|
|
470
|
-
|
|
471
|
-
it("updates runtime.config with normalized merged result", () => {
|
|
472
|
-
const runtime = makeRuntime();
|
|
473
|
-
mockLoadAndMergeConfigs.mockReturnValue({
|
|
474
|
-
merged: { debugLog: true, permissionReviewLog: false, yoloMode: false },
|
|
475
|
-
issues: [],
|
|
476
|
-
});
|
|
477
|
-
refreshExtensionConfig(runtime);
|
|
478
|
-
expect(runtime.config.debugLog).toBe(true);
|
|
479
|
-
expect(runtime.config.permissionReviewLog).toBe(false);
|
|
480
|
-
});
|
|
481
|
-
|
|
482
|
-
it("calls loadAndMergeConfigs with runtime.agentDir and cwd", () => {
|
|
483
|
-
const runtime = makeRuntime();
|
|
484
|
-
const ctx = makeCtx({ cwd: "/my/project" });
|
|
485
|
-
refreshExtensionConfig(runtime, ctx);
|
|
486
|
-
expect(mockLoadAndMergeConfigs).toHaveBeenCalledWith(
|
|
487
|
-
"/test/agent",
|
|
488
|
-
"/my/project",
|
|
489
|
-
expect.any(String), // EXTENSION_ROOT
|
|
490
|
-
);
|
|
491
|
-
});
|
|
492
|
-
|
|
493
|
-
it("writes config.loaded debug log", () => {
|
|
494
|
-
const runtime = makeRuntime();
|
|
495
|
-
refreshExtensionConfig(runtime);
|
|
496
|
-
expect(mockLoggerDebug).toHaveBeenCalledWith(
|
|
497
|
-
"config.loaded",
|
|
498
|
-
expect.objectContaining({ debugLog: false }),
|
|
499
|
-
);
|
|
500
|
-
});
|
|
501
|
-
|
|
502
|
-
it("sets lastConfigWarning when issues are present", () => {
|
|
503
|
-
const runtime = makeRuntime();
|
|
504
|
-
mockLoadAndMergeConfigs.mockReturnValue({
|
|
505
|
-
merged: { ...DEFAULT_EXTENSION_CONFIG },
|
|
506
|
-
issues: ["legacy config detected"],
|
|
507
|
-
});
|
|
508
|
-
refreshExtensionConfig(runtime);
|
|
509
|
-
expect(runtime.lastConfigWarning).toBe("legacy config detected");
|
|
510
|
-
});
|
|
511
|
-
|
|
512
|
-
it("clears lastConfigWarning when no issues", () => {
|
|
513
|
-
const runtime = makeRuntime();
|
|
514
|
-
runtime.lastConfigWarning = "old warning";
|
|
515
|
-
mockLoadAndMergeConfigs.mockReturnValue({
|
|
516
|
-
merged: { ...DEFAULT_EXTENSION_CONFIG },
|
|
517
|
-
issues: [],
|
|
518
|
-
});
|
|
519
|
-
refreshExtensionConfig(runtime);
|
|
520
|
-
expect(runtime.lastConfigWarning).toBeNull();
|
|
521
|
-
});
|
|
522
|
-
|
|
523
|
-
it("notifies UI when a new warning appears and hasUI is true", () => {
|
|
524
|
-
const runtime = makeRuntime();
|
|
525
|
-
const mockNotify = vi.fn();
|
|
526
|
-
const ctx = makeCtx({ hasUI: true, ui: { notify: mockNotify } as never });
|
|
527
|
-
mockLoadAndMergeConfigs.mockReturnValue({
|
|
528
|
-
merged: { ...DEFAULT_EXTENSION_CONFIG },
|
|
529
|
-
issues: ["new warning"],
|
|
530
|
-
});
|
|
531
|
-
refreshExtensionConfig(runtime, ctx);
|
|
532
|
-
expect(mockNotify).toHaveBeenCalledWith("new warning", "warning");
|
|
533
|
-
});
|
|
534
|
-
|
|
535
|
-
it("does not re-notify the same warning on subsequent calls", () => {
|
|
536
|
-
const runtime = makeRuntime();
|
|
537
|
-
const mockNotify = vi.fn();
|
|
538
|
-
const ctx = makeCtx({ hasUI: true, ui: { notify: mockNotify } as never });
|
|
539
|
-
mockLoadAndMergeConfigs.mockReturnValue({
|
|
540
|
-
merged: { ...DEFAULT_EXTENSION_CONFIG },
|
|
541
|
-
issues: ["persistent warning"],
|
|
542
|
-
});
|
|
543
|
-
refreshExtensionConfig(runtime, ctx);
|
|
544
|
-
refreshExtensionConfig(runtime, ctx);
|
|
545
|
-
expect(mockNotify).toHaveBeenCalledTimes(1);
|
|
546
|
-
});
|
|
547
|
-
|
|
548
|
-
it("calls syncPermissionSystemStatus when hasUI is true", () => {
|
|
549
|
-
const runtime = makeRuntime();
|
|
550
|
-
const ctx = makeCtx({ hasUI: true });
|
|
551
|
-
refreshExtensionConfig(runtime, ctx);
|
|
552
|
-
expect(mockSyncPermissionSystemStatus).toHaveBeenCalledWith(
|
|
553
|
-
ctx,
|
|
554
|
-
expect.any(Object),
|
|
555
|
-
);
|
|
556
|
-
});
|
|
557
|
-
|
|
558
|
-
it("does not call syncPermissionSystemStatus when hasUI is false", () => {
|
|
559
|
-
const runtime = makeRuntime();
|
|
560
|
-
const ctx = makeCtx({ hasUI: false });
|
|
561
|
-
refreshExtensionConfig(runtime, ctx);
|
|
562
|
-
expect(mockSyncPermissionSystemStatus).not.toHaveBeenCalled();
|
|
563
|
-
});
|
|
564
|
-
});
|
|
299
|
+
// refreshExtensionConfig / saveExtensionConfig / logResolvedConfigPaths are
|
|
300
|
+
// thin delegators to runtime.configStore — behavior covered in config-store.test.ts.
|
|
565
301
|
|
|
566
302
|
// resolveAgentName was moved to PermissionSession (#129)
|
|
567
|
-
// Tests live in
|
|
303
|
+
// Tests live in test/permission-session.test.ts
|