@gotgenes/pi-permission-system 10.3.0 → 10.3.1

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.
@@ -1,113 +1,200 @@
1
- import { describe, expect, it, vi } from "vitest";
2
- import type { ExtensionRuntime } from "#src/runtime";
1
+ import { existsSync, mkdtempSync, writeFileSync } from "node:fs";
2
+ import { tmpdir } from "node:os";
3
+ import { join } from "node:path";
4
+ import { beforeEach, describe, expect, it, vi } from "vitest";
5
+ import { DEBUG_LOG_FILENAME, REVIEW_LOG_FILENAME } from "#src/config-paths";
6
+ import {
7
+ DEFAULT_EXTENSION_CONFIG,
8
+ type PermissionSystemExtensionConfig,
9
+ } from "#src/extension-config";
10
+ import type { SessionLoggerDeps } from "#src/session-logger";
3
11
  import { createSessionLogger } from "#src/session-logger";
4
12
 
5
13
  // ── helpers ────────────────────────────────────────────────────────────────
6
14
 
7
- function makeRuntime(
8
- overrides: Partial<ExtensionRuntime> = {},
9
- ): ExtensionRuntime {
15
+ let tempDir: string;
16
+
17
+ beforeEach(() => {
18
+ tempDir = mkdtempSync(join(tmpdir(), "ps-session-logger-"));
19
+ });
20
+
21
+ function makeDeps(
22
+ overrides: {
23
+ globalLogsDir?: string;
24
+ getConfig?: () => PermissionSystemExtensionConfig;
25
+ } = {},
26
+ ) {
10
27
  return {
11
- runtimeContext: null,
12
- writeDebugLog: vi.fn(),
13
- writeReviewLog: vi.fn(),
14
- ...overrides,
15
- } as unknown as ExtensionRuntime;
28
+ globalLogsDir: overrides.globalLogsDir ?? tempDir,
29
+ getConfig:
30
+ overrides.getConfig ??
31
+ ((): PermissionSystemExtensionConfig => ({
32
+ ...DEFAULT_EXTENSION_CONFIG,
33
+ })),
34
+ notify: vi.fn<(message: string) => void>(),
35
+ };
36
+ }
37
+
38
+ /** A `globalLogsDir` that cannot be created: a file at the parent path blocks it. */
39
+ function makeBlockedLogsDir(): string {
40
+ const barrier = join(tempDir, "barrier");
41
+ writeFileSync(barrier, "");
42
+ return join(barrier, "logs");
16
43
  }
17
44
 
18
45
  // ── createSessionLogger ────────────────────────────────────────────────────
19
46
 
20
47
  describe("createSessionLogger", () => {
48
+ // ── debug ────────────────────────────────────────────────────────────────
49
+
21
50
  describe("debug", () => {
22
- it("delegates to runtime.writeDebugLog with event and details", () => {
23
- const runtime = makeRuntime();
24
- const logger = createSessionLogger(runtime);
51
+ it("writes a JSONL line to the debug log file when debugLog is true", () => {
52
+ const deps = makeDeps({
53
+ getConfig: () => ({ ...DEFAULT_EXTENSION_CONFIG, debugLog: true }),
54
+ });
55
+ const logger = createSessionLogger(deps);
25
56
 
26
57
  logger.debug("test.event", { key: "value" });
27
58
 
28
- expect(runtime.writeDebugLog).toHaveBeenCalledWith("test.event", {
29
- key: "value",
30
- });
59
+ expect(existsSync(join(tempDir, DEBUG_LOG_FILENAME))).toBe(true);
60
+ expect(deps.notify).not.toHaveBeenCalled();
31
61
  });
32
62
 
33
- it("delegates to runtime.writeDebugLog with event and no details", () => {
34
- const runtime = makeRuntime();
35
- const logger = createSessionLogger(runtime);
63
+ it("does not write to the debug log when debugLog is false", () => {
64
+ // DEFAULT_EXTENSION_CONFIG.debugLog === false
65
+ const deps = makeDeps();
66
+ const logger = createSessionLogger(deps);
36
67
 
37
68
  logger.debug("test.event");
38
69
 
39
- expect(runtime.writeDebugLog).toHaveBeenCalledWith(
40
- "test.event",
41
- undefined,
42
- );
70
+ expect(existsSync(join(tempDir, DEBUG_LOG_FILENAME))).toBe(false);
71
+ expect(deps.notify).not.toHaveBeenCalled();
72
+ });
73
+
74
+ it("reads getConfig at write time — a mid-session toggle change takes effect", () => {
75
+ let debugLog = true;
76
+ const deps = makeDeps({
77
+ getConfig: () => ({ ...DEFAULT_EXTENSION_CONFIG, debugLog }),
78
+ });
79
+ const logger = createSessionLogger(deps);
80
+ debugLog = false;
81
+
82
+ logger.debug("test.event");
83
+
84
+ expect(existsSync(join(tempDir, DEBUG_LOG_FILENAME))).toBe(false);
43
85
  });
44
86
  });
45
87
 
88
+ // ── review ───────────────────────────────────────────────────────────────
89
+
46
90
  describe("review", () => {
47
- it("delegates to runtime.writeReviewLog with event and details", () => {
48
- const runtime = makeRuntime();
49
- const logger = createSessionLogger(runtime);
91
+ it("writes a JSONL line to the review log file when permissionReviewLog is true", () => {
92
+ // DEFAULT_EXTENSION_CONFIG.permissionReviewLog === true
93
+ const deps = makeDeps();
94
+ const logger = createSessionLogger(deps);
50
95
 
51
96
  logger.review("permission.granted", { agentName: "coder" });
52
97
 
53
- expect(runtime.writeReviewLog).toHaveBeenCalledWith(
54
- "permission.granted",
55
- { agentName: "coder" },
56
- );
98
+ expect(existsSync(join(tempDir, REVIEW_LOG_FILENAME))).toBe(true);
99
+ expect(deps.notify).not.toHaveBeenCalled();
57
100
  });
58
101
 
59
- it("delegates to runtime.writeReviewLog with event and no details", () => {
60
- const runtime = makeRuntime();
61
- const logger = createSessionLogger(runtime);
102
+ it("does not write to the review log when permissionReviewLog is false", () => {
103
+ const deps = makeDeps({
104
+ getConfig: () => ({
105
+ ...DEFAULT_EXTENSION_CONFIG,
106
+ permissionReviewLog: false,
107
+ }),
108
+ });
109
+ const logger = createSessionLogger(deps);
62
110
 
63
111
  logger.review("permission.granted");
64
112
 
65
- expect(runtime.writeReviewLog).toHaveBeenCalledWith(
66
- "permission.granted",
67
- undefined,
68
- );
113
+ expect(existsSync(join(tempDir, REVIEW_LOG_FILENAME))).toBe(false);
114
+ expect(deps.notify).not.toHaveBeenCalled();
69
115
  });
70
116
  });
71
117
 
72
- describe("warn", () => {
73
- it("calls ui.notify with the message and 'warning' severity when runtimeContext is present", () => {
74
- const notify = vi.fn();
75
- const runtime = makeRuntime({
76
- runtimeContext: {
77
- ui: { notify, setStatus: vi.fn(), select: vi.fn(), input: vi.fn() },
78
- } as unknown as ExtensionRuntime["runtimeContext"],
118
+ // ── IO-failure warnings ───────────────────────────────────────────────────
119
+
120
+ describe("IO-failure warnings", () => {
121
+ it("calls notify with the error message when the logs directory cannot be created", () => {
122
+ const deps = makeDeps({
123
+ globalLogsDir: makeBlockedLogsDir(),
124
+ getConfig: () => ({ ...DEFAULT_EXTENSION_CONFIG, debugLog: true }),
79
125
  });
80
- const logger = createSessionLogger(runtime);
126
+ const logger = createSessionLogger(deps);
81
127
 
82
- logger.warn("Something went wrong");
128
+ logger.debug("test.event");
129
+
130
+ expect(deps.notify).toHaveBeenCalledOnce();
131
+ expect(deps.notify).toHaveBeenCalledWith(
132
+ expect.stringContaining("Failed to"),
133
+ );
134
+ });
135
+
136
+ it("deduplicates the same IO-failure warning across multiple writes", () => {
137
+ const deps = makeDeps({
138
+ globalLogsDir: makeBlockedLogsDir(),
139
+ getConfig: () => ({ ...DEFAULT_EXTENSION_CONFIG, debugLog: true }),
140
+ });
141
+ const logger = createSessionLogger(deps);
142
+
143
+ logger.debug("event.one");
144
+ logger.debug("event.two");
145
+
146
+ expect(deps.notify).toHaveBeenCalledOnce();
147
+ });
148
+
149
+ it("shares the dedup set across debug and review — same message notified only once", () => {
150
+ const deps = makeDeps({
151
+ globalLogsDir: makeBlockedLogsDir(),
152
+ getConfig: () => ({
153
+ ...DEFAULT_EXTENSION_CONFIG,
154
+ debugLog: true,
155
+ permissionReviewLog: true,
156
+ }),
157
+ });
158
+ const logger = createSessionLogger(deps);
83
159
 
84
- expect(notify).toHaveBeenCalledWith("Something went wrong", "warning");
160
+ logger.debug("event.one"); // emits warning
161
+ logger.review("event.two"); // same error message → suppressed
162
+
163
+ expect(deps.notify).toHaveBeenCalledOnce();
85
164
  });
165
+ });
86
166
 
87
- it("does not throw when runtimeContext is null", () => {
88
- const runtime = makeRuntime({ runtimeContext: null });
89
- const logger = createSessionLogger(runtime);
167
+ // ── warn ──────────────────────────────────────────────────────────────────
90
168
 
91
- expect(() => logger.warn("no-op warning")).not.toThrow();
169
+ describe("warn", () => {
170
+ it("calls notify with the message directly", () => {
171
+ const deps = makeDeps();
172
+ const logger = createSessionLogger(deps);
173
+
174
+ logger.warn("Something went wrong");
175
+
176
+ expect(deps.notify).toHaveBeenCalledWith("Something went wrong");
92
177
  });
93
178
 
94
- it("reads runtimeContext at call time, not at creation time", () => {
95
- const runtime = makeRuntime({ runtimeContext: null });
96
- const logger = createSessionLogger(runtime);
179
+ it("calls notify for every warn not deduplicated", () => {
180
+ const deps = makeDeps();
181
+ const logger = createSessionLogger(deps);
97
182
 
98
- // runtimeContext is null at creation — warn should be a no-op now
99
- logger.warn("early warning");
183
+ logger.warn("same message");
184
+ logger.warn("same message");
100
185
 
101
- // Later runtimeContext is set
102
- const notify = vi.fn();
103
- runtime.runtimeContext = {
104
- ui: { notify, setStatus: vi.fn(), select: vi.fn(), input: vi.fn() },
105
- } as unknown as ExtensionRuntime["runtimeContext"];
186
+ expect(deps.notify).toHaveBeenCalledTimes(2);
187
+ });
106
188
 
107
- logger.warn("late warning");
189
+ it("does not throw when notify is a no-op", () => {
190
+ const deps: SessionLoggerDeps = {
191
+ globalLogsDir: tempDir,
192
+ getConfig: () => ({ ...DEFAULT_EXTENSION_CONFIG }),
193
+ notify: () => {},
194
+ };
195
+ const logger = createSessionLogger(deps);
108
196
 
109
- expect(notify).toHaveBeenCalledOnce();
110
- expect(notify).toHaveBeenCalledWith("late warning", "warning");
197
+ expect(() => logger.warn("test")).not.toThrow();
111
198
  });
112
199
  });
113
200
  });
package/src/runtime.ts DELETED
@@ -1,147 +0,0 @@
1
- import { join } from "node:path";
2
- import {
3
- type ExtensionContext,
4
- getAgentDir,
5
- } from "@earendil-works/pi-coding-agent";
6
-
7
- import { DEBUG_LOG_FILENAME, REVIEW_LOG_FILENAME } from "./config-paths";
8
- import { ConfigStore, type RuntimeContextRef } from "./config-store";
9
- import { ensurePermissionSystemLogsDirectory } from "./extension-config";
10
- import { computeExtensionPaths, type ExtensionPaths } from "./extension-paths";
11
-
12
- export type { ExtensionPaths } from "./extension-paths";
13
-
14
- import { createPermissionSystemLogger } from "./logging";
15
- import { PermissionManager } from "./permission-manager";
16
- import { SessionRules } from "./session-rules";
17
- import type { SkillPromptEntry } from "./skill-prompt-sanitizer";
18
-
19
- /**
20
- * Mutable session state — the subset of ExtensionRuntime that holds
21
- * per-session fields. `PermissionSession` now owns these for handler
22
- * use; this interface remains so `ExtensionRuntime` can still serve
23
- * as the internal composition root (config-modal, RPC handlers).
24
- */
25
- interface SessionState {
26
- runtimeContext: ExtensionContext | null;
27
- permissionManager: PermissionManager;
28
- readonly sessionRules: SessionRules;
29
- activeSkillEntries: SkillPromptEntry[];
30
- lastKnownActiveAgentName: string | null;
31
- lastActiveToolsCacheKey: string | null;
32
- lastPromptStateCacheKey: string | null;
33
- }
34
-
35
- /**
36
- * Runtime context object created once inside `piPermissionSystemExtension()`.
37
- *
38
- * Holds all path constants (derived from `getAgentDir()` at construction time),
39
- * mutable extension state, and the log-writing methods — eliminating the
40
- * module-scope cached constants and setter-injection pattern that previously
41
- * lived in `src/index.ts`.
42
- *
43
- * Tests construct this via `createExtensionRuntime({ agentDir: tmpDir })`
44
- * without timing issues around `PI_CODING_AGENT_DIR`.
45
- */
46
- export interface ExtensionRuntime extends ExtensionPaths, SessionState {
47
- /** The store that owns extension config. */
48
- configStore: ConfigStore;
49
-
50
- // ── Logging (backed by logger created at construction) ─────────────────
51
- writeDebugLog(event: string, details?: Record<string, unknown>): void;
52
- writeReviewLog(event: string, details?: Record<string, unknown>): void;
53
- }
54
-
55
- // ── Factory ────────────────────────────────────────────────────────────────
56
-
57
- /**
58
- * Create a fully-initialized `ExtensionRuntime`.
59
- *
60
- * Calls `getAgentDir()` at invocation time (never at module scope), so tests
61
- * may set `PI_CODING_AGENT_DIR` before calling the factory.
62
- */
63
- export function createExtensionRuntime(options?: {
64
- agentDir?: string;
65
- }): ExtensionRuntime {
66
- const agentDir = options?.agentDir ?? getAgentDir();
67
- const paths = computeExtensionPaths(agentDir);
68
-
69
- const permissionManager = new PermissionManager({ agentDir });
70
-
71
- const runtime: ExtensionRuntime = {
72
- ...paths,
73
- runtimeContext: null,
74
- configStore: null as unknown as ConfigStore,
75
- permissionManager,
76
- activeSkillEntries: [],
77
- lastKnownActiveAgentName: null,
78
- lastActiveToolsCacheKey: null,
79
- lastPromptStateCacheKey: null,
80
- sessionRules: new SessionRules(),
81
- // Logging methods are replaced below after the logger is constructed.
82
- writeDebugLog: () => {},
83
- writeReviewLog: () => {},
84
- };
85
-
86
- // Transitional RuntimeContextRef: reads/writes the still-runtime-owned
87
- // `runtimeContext` field until Step 4 (#337) unifies context onto
88
- // PermissionSession.
89
- const contextRef: RuntimeContextRef = {
90
- get: () => runtime.runtimeContext,
91
- set: (ctx) => {
92
- runtime.runtimeContext = ctx;
93
- },
94
- };
95
-
96
- const configStore = new ConfigStore({
97
- agentDir,
98
- context: contextRef,
99
- policyPaths: permissionManager,
100
- logger: {
101
- // Deferred-binding: `runtime.writeDebugLog` is replaced below after
102
- // the logger is constructed — same deferred pattern as before Step 2.
103
- writeDebugLog: (e, d) => runtime.writeDebugLog(e, d),
104
- writeReviewLog: (e, d) => runtime.writeReviewLog(e, d),
105
- },
106
- });
107
- runtime.configStore = configStore;
108
-
109
- const reportedLoggingWarnings = new Set<string>();
110
- const logger = createPermissionSystemLogger({
111
- getConfig: () => configStore.current(),
112
- debugLogPath: join(paths.globalLogsDir, DEBUG_LOG_FILENAME),
113
- reviewLogPath: join(paths.globalLogsDir, REVIEW_LOG_FILENAME),
114
- ensureLogsDirectory: () =>
115
- ensurePermissionSystemLogsDirectory(paths.globalLogsDir),
116
- });
117
-
118
- const reportLoggingWarning = (message: string): void => {
119
- if (reportedLoggingWarnings.has(message)) {
120
- return;
121
- }
122
- reportedLoggingWarnings.add(message);
123
- runtime.runtimeContext?.ui.notify(message, "warning");
124
- };
125
-
126
- runtime.writeDebugLog = (
127
- event: string,
128
- details: Record<string, unknown> = {},
129
- ): void => {
130
- const warning = logger.debug(event, details);
131
- if (warning) {
132
- reportLoggingWarning(warning);
133
- }
134
- };
135
-
136
- runtime.writeReviewLog = (
137
- event: string,
138
- details: Record<string, unknown> = {},
139
- ): void => {
140
- const warning = logger.review(event, details);
141
- if (warning) {
142
- reportLoggingWarning(warning);
143
- }
144
- };
145
-
146
- return runtime;
147
- }