@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.
- package/CHANGELOG.md +7 -0
- package/package.json +1 -1
- package/src/config-store.ts +7 -23
- package/src/index.ts +65 -30
- package/src/permission-session.ts +4 -5
- package/src/permissions-service.ts +3 -5
- package/src/session-logger.ts +46 -9
- package/test/composition-root.test.ts +85 -1
- package/test/config-store.test.ts +16 -40
- package/test/permission-session.test.ts +14 -1
- package/test/session-logger.test.ts +151 -64
- package/src/runtime.ts +0 -147
- package/test/runtime.test.ts +0 -303
package/test/runtime.test.ts
DELETED
|
@@ -1,303 +0,0 @@
|
|
|
1
|
-
import { beforeEach, describe, expect, it, vi } from "vitest";
|
|
2
|
-
|
|
3
|
-
// ── logger stub ────────────────────────────────────────────────────────────
|
|
4
|
-
const {
|
|
5
|
-
mockLoggerDebug,
|
|
6
|
-
mockLoggerReview,
|
|
7
|
-
mockCreateLogger,
|
|
8
|
-
mockDiscoverGlobalNodeModulesRoot,
|
|
9
|
-
} = vi.hoisted(() => ({
|
|
10
|
-
mockLoggerDebug:
|
|
11
|
-
vi.fn<
|
|
12
|
-
(event: string, details?: Record<string, unknown>) => string | undefined
|
|
13
|
-
>(),
|
|
14
|
-
mockLoggerReview:
|
|
15
|
-
vi.fn<
|
|
16
|
-
(event: string, details?: Record<string, unknown>) => string | undefined
|
|
17
|
-
>(),
|
|
18
|
-
mockCreateLogger: vi.fn(),
|
|
19
|
-
mockDiscoverGlobalNodeModulesRoot: vi.fn<() => string | null>(),
|
|
20
|
-
}));
|
|
21
|
-
|
|
22
|
-
vi.mock("../src/logging", () => ({
|
|
23
|
-
createPermissionSystemLogger: mockCreateLogger,
|
|
24
|
-
}));
|
|
25
|
-
|
|
26
|
-
vi.mock("../src/permission-manager", () => ({
|
|
27
|
-
PermissionManager: vi.fn(),
|
|
28
|
-
}));
|
|
29
|
-
|
|
30
|
-
vi.mock("../src/subagent-context", () => ({
|
|
31
|
-
isSubagentExecutionContext: vi.fn().mockReturnValue(false),
|
|
32
|
-
}));
|
|
33
|
-
|
|
34
|
-
vi.mock("../src/node-modules-discovery", () => ({
|
|
35
|
-
discoverGlobalNodeModulesRoot: mockDiscoverGlobalNodeModulesRoot,
|
|
36
|
-
}));
|
|
37
|
-
|
|
38
|
-
vi.mock("../src/session-rules", () => ({
|
|
39
|
-
SessionRules: vi.fn(),
|
|
40
|
-
deriveApprovalPattern: vi.fn(),
|
|
41
|
-
}));
|
|
42
|
-
|
|
43
|
-
import { getGlobalLogsDir } from "#src/config-paths";
|
|
44
|
-
import { DEFAULT_EXTENSION_CONFIG } from "#src/extension-config";
|
|
45
|
-
import { createExtensionRuntime } from "#src/runtime";
|
|
46
|
-
|
|
47
|
-
// ── test suite ─────────────────────────────────────────────────────────────
|
|
48
|
-
|
|
49
|
-
describe("createExtensionRuntime", () => {
|
|
50
|
-
beforeEach(() => {
|
|
51
|
-
mockLoggerDebug.mockReset();
|
|
52
|
-
mockLoggerDebug.mockReturnValue(undefined);
|
|
53
|
-
mockLoggerReview.mockReset();
|
|
54
|
-
mockLoggerReview.mockReturnValue(undefined);
|
|
55
|
-
mockCreateLogger.mockReset();
|
|
56
|
-
mockCreateLogger.mockReturnValue({
|
|
57
|
-
debug: mockLoggerDebug,
|
|
58
|
-
review: mockLoggerReview,
|
|
59
|
-
});
|
|
60
|
-
mockDiscoverGlobalNodeModulesRoot.mockReset();
|
|
61
|
-
mockDiscoverGlobalNodeModulesRoot.mockReturnValue(
|
|
62
|
-
"/mock/global/node_modules",
|
|
63
|
-
);
|
|
64
|
-
});
|
|
65
|
-
|
|
66
|
-
// ── Path derivation ──────────────────────────────────────────────────────
|
|
67
|
-
|
|
68
|
-
it("sets agentDir from provided option", () => {
|
|
69
|
-
const runtime = createExtensionRuntime({ agentDir: "/test/agent" });
|
|
70
|
-
expect(runtime.agentDir).toBe("/test/agent");
|
|
71
|
-
});
|
|
72
|
-
|
|
73
|
-
it("derives sessionsDir from agentDir", () => {
|
|
74
|
-
const runtime = createExtensionRuntime({ agentDir: "/test/agent" });
|
|
75
|
-
expect(runtime.sessionsDir).toBe("/test/agent/sessions");
|
|
76
|
-
});
|
|
77
|
-
|
|
78
|
-
it("derives subagentSessionsDir from agentDir", () => {
|
|
79
|
-
const runtime = createExtensionRuntime({ agentDir: "/test/agent" });
|
|
80
|
-
expect(runtime.subagentSessionsDir).toBe("/test/agent/subagent-sessions");
|
|
81
|
-
});
|
|
82
|
-
|
|
83
|
-
it("derives forwardingDir as sessions/permission-forwarding", () => {
|
|
84
|
-
const runtime = createExtensionRuntime({ agentDir: "/test/agent" });
|
|
85
|
-
expect(runtime.forwardingDir).toBe(
|
|
86
|
-
"/test/agent/sessions/permission-forwarding",
|
|
87
|
-
);
|
|
88
|
-
});
|
|
89
|
-
|
|
90
|
-
it("derives globalLogsDir via getGlobalLogsDir", () => {
|
|
91
|
-
const runtime = createExtensionRuntime({ agentDir: "/test/agent" });
|
|
92
|
-
expect(runtime.globalLogsDir).toBe(getGlobalLogsDir("/test/agent"));
|
|
93
|
-
});
|
|
94
|
-
|
|
95
|
-
// ── piInfrastructureDirs ─────────────────────────────────────────────────
|
|
96
|
-
|
|
97
|
-
it("includes agentDir in piInfrastructureDirs", () => {
|
|
98
|
-
const runtime = createExtensionRuntime({ agentDir: "/test/agent" });
|
|
99
|
-
expect(runtime.piInfrastructureDirs).toContain("/test/agent");
|
|
100
|
-
});
|
|
101
|
-
|
|
102
|
-
it("includes agentDir/git in piInfrastructureDirs", () => {
|
|
103
|
-
const runtime = createExtensionRuntime({ agentDir: "/test/agent" });
|
|
104
|
-
expect(runtime.piInfrastructureDirs).toContain("/test/agent/git");
|
|
105
|
-
});
|
|
106
|
-
|
|
107
|
-
it("includes discovered global node_modules root in piInfrastructureDirs", () => {
|
|
108
|
-
const runtime = createExtensionRuntime({ agentDir: "/test/agent" });
|
|
109
|
-
expect(runtime.piInfrastructureDirs).toContain("/mock/global/node_modules");
|
|
110
|
-
});
|
|
111
|
-
|
|
112
|
-
it("excludes null when discoverGlobalNodeModulesRoot returns null", () => {
|
|
113
|
-
mockDiscoverGlobalNodeModulesRoot.mockReturnValue(null);
|
|
114
|
-
const runtime = createExtensionRuntime({ agentDir: "/test/agent" });
|
|
115
|
-
for (const dir of runtime.piInfrastructureDirs) {
|
|
116
|
-
expect(dir).not.toBeNull();
|
|
117
|
-
expect(typeof dir).toBe("string");
|
|
118
|
-
}
|
|
119
|
-
});
|
|
120
|
-
|
|
121
|
-
it("omits global node_modules from piInfrastructureDirs when discovery returns null", () => {
|
|
122
|
-
mockDiscoverGlobalNodeModulesRoot.mockReturnValue(null);
|
|
123
|
-
const runtime = createExtensionRuntime({ agentDir: "/test/agent" });
|
|
124
|
-
// Only agentDir and agentDir/git should be present.
|
|
125
|
-
expect(runtime.piInfrastructureDirs).toHaveLength(2);
|
|
126
|
-
expect(runtime.piInfrastructureDirs).toContain("/test/agent");
|
|
127
|
-
expect(runtime.piInfrastructureDirs).toContain("/test/agent/git");
|
|
128
|
-
});
|
|
129
|
-
|
|
130
|
-
// ── Default mutable state ────────────────────────────────────────────────
|
|
131
|
-
|
|
132
|
-
it("initializes configStore.current() to DEFAULT_EXTENSION_CONFIG", () => {
|
|
133
|
-
const runtime = createExtensionRuntime({ agentDir: "/test/agent" });
|
|
134
|
-
expect(runtime.configStore.current()).toEqual(DEFAULT_EXTENSION_CONFIG);
|
|
135
|
-
});
|
|
136
|
-
|
|
137
|
-
it("initializes runtimeContext to null", () => {
|
|
138
|
-
const runtime = createExtensionRuntime({ agentDir: "/test/agent" });
|
|
139
|
-
expect(runtime.runtimeContext).toBeNull();
|
|
140
|
-
});
|
|
141
|
-
|
|
142
|
-
it("initializes activeSkillEntries to empty array", () => {
|
|
143
|
-
const runtime = createExtensionRuntime({ agentDir: "/test/agent" });
|
|
144
|
-
expect(runtime.activeSkillEntries).toEqual([]);
|
|
145
|
-
});
|
|
146
|
-
|
|
147
|
-
it("initializes lastKnownActiveAgentName to null", () => {
|
|
148
|
-
const runtime = createExtensionRuntime({ agentDir: "/test/agent" });
|
|
149
|
-
expect(runtime.lastKnownActiveAgentName).toBeNull();
|
|
150
|
-
});
|
|
151
|
-
|
|
152
|
-
it("initializes lastActiveToolsCacheKey to null", () => {
|
|
153
|
-
const runtime = createExtensionRuntime({ agentDir: "/test/agent" });
|
|
154
|
-
expect(runtime.lastActiveToolsCacheKey).toBeNull();
|
|
155
|
-
});
|
|
156
|
-
|
|
157
|
-
it("initializes lastPromptStateCacheKey to null", () => {
|
|
158
|
-
const runtime = createExtensionRuntime({ agentDir: "/test/agent" });
|
|
159
|
-
expect(runtime.lastPromptStateCacheKey).toBeNull();
|
|
160
|
-
});
|
|
161
|
-
|
|
162
|
-
it("creates a sessionRules instance", () => {
|
|
163
|
-
const runtime = createExtensionRuntime({ agentDir: "/test/agent" });
|
|
164
|
-
expect(runtime.sessionRules).toBeDefined();
|
|
165
|
-
});
|
|
166
|
-
|
|
167
|
-
// ── Mutable state is writable ──────────────────────────────────────────
|
|
168
|
-
|
|
169
|
-
it("allows runtimeContext to be updated", () => {
|
|
170
|
-
const runtime = createExtensionRuntime({ agentDir: "/test/agent" });
|
|
171
|
-
const mockCtx = { hasUI: false } as never;
|
|
172
|
-
runtime.runtimeContext = mockCtx;
|
|
173
|
-
expect(runtime.runtimeContext).toBe(mockCtx);
|
|
174
|
-
});
|
|
175
|
-
|
|
176
|
-
// ── Logger is created with runtime-derived paths ─────────────────────────
|
|
177
|
-
|
|
178
|
-
it("creates the logger with derived debugLogPath and reviewLogPath", () => {
|
|
179
|
-
const agentDir = "/test/agent";
|
|
180
|
-
const expectedLogsDir = getGlobalLogsDir(agentDir);
|
|
181
|
-
createExtensionRuntime({ agentDir });
|
|
182
|
-
expect(mockCreateLogger).toHaveBeenCalledOnce();
|
|
183
|
-
const opts = mockCreateLogger.mock.calls[0][0] as {
|
|
184
|
-
debugLogPath: string;
|
|
185
|
-
reviewLogPath: string;
|
|
186
|
-
};
|
|
187
|
-
expect(opts.debugLogPath).toContain(expectedLogsDir);
|
|
188
|
-
expect(opts.reviewLogPath).toContain(expectedLogsDir);
|
|
189
|
-
});
|
|
190
|
-
|
|
191
|
-
it("passes getConfig that reads from configStore.current()", () => {
|
|
192
|
-
createExtensionRuntime({ agentDir: "/test/agent" });
|
|
193
|
-
const opts = mockCreateLogger.mock.calls[0][0] as {
|
|
194
|
-
getConfig: () => typeof DEFAULT_EXTENSION_CONFIG;
|
|
195
|
-
};
|
|
196
|
-
// getConfig() reads from configStore.current() — DEFAULT_EXTENSION_CONFIG at startup
|
|
197
|
-
expect(opts.getConfig()).toEqual(DEFAULT_EXTENSION_CONFIG);
|
|
198
|
-
});
|
|
199
|
-
|
|
200
|
-
// ── writeDebugLog delegates to logger.debug ──────────────────────────────
|
|
201
|
-
|
|
202
|
-
it("writeDebugLog calls logger.debug with event and details", () => {
|
|
203
|
-
const runtime = createExtensionRuntime({ agentDir: "/test/agent" });
|
|
204
|
-
runtime.writeDebugLog("test.event", { key: "value" });
|
|
205
|
-
expect(mockLoggerDebug).toHaveBeenCalledWith("test.event", {
|
|
206
|
-
key: "value",
|
|
207
|
-
});
|
|
208
|
-
});
|
|
209
|
-
|
|
210
|
-
it("writeDebugLog uses empty object as default details", () => {
|
|
211
|
-
const runtime = createExtensionRuntime({ agentDir: "/test/agent" });
|
|
212
|
-
runtime.writeDebugLog("test.event");
|
|
213
|
-
expect(mockLoggerDebug).toHaveBeenCalledWith("test.event", {});
|
|
214
|
-
});
|
|
215
|
-
|
|
216
|
-
// ── writeReviewLog delegates to logger.review ────────────────────────────
|
|
217
|
-
|
|
218
|
-
it("writeReviewLog calls logger.review with event and details", () => {
|
|
219
|
-
const runtime = createExtensionRuntime({ agentDir: "/test/agent" });
|
|
220
|
-
runtime.writeReviewLog("test.event", { key: "value" });
|
|
221
|
-
expect(mockLoggerReview).toHaveBeenCalledWith("test.event", {
|
|
222
|
-
key: "value",
|
|
223
|
-
});
|
|
224
|
-
});
|
|
225
|
-
|
|
226
|
-
it("writeReviewLog uses empty object as default details", () => {
|
|
227
|
-
const runtime = createExtensionRuntime({ agentDir: "/test/agent" });
|
|
228
|
-
runtime.writeReviewLog("test.event");
|
|
229
|
-
expect(mockLoggerReview).toHaveBeenCalledWith("test.event", {});
|
|
230
|
-
});
|
|
231
|
-
|
|
232
|
-
// ── Logging warning reporter ──────────────────────────────────────────────
|
|
233
|
-
|
|
234
|
-
it("notifies runtimeContext.ui when logger returns a warning", () => {
|
|
235
|
-
const runtime = createExtensionRuntime({ agentDir: "/test/agent" });
|
|
236
|
-
const mockNotify = vi.fn();
|
|
237
|
-
runtime.runtimeContext = {
|
|
238
|
-
hasUI: true,
|
|
239
|
-
ui: { notify: mockNotify },
|
|
240
|
-
} as never;
|
|
241
|
-
mockLoggerDebug.mockReturnValueOnce("log dir not writable");
|
|
242
|
-
runtime.writeDebugLog("some.event");
|
|
243
|
-
expect(mockNotify).toHaveBeenCalledWith("log dir not writable", "warning");
|
|
244
|
-
});
|
|
245
|
-
|
|
246
|
-
it("does not notify when runtimeContext is null", () => {
|
|
247
|
-
const runtime = createExtensionRuntime({ agentDir: "/test/agent" });
|
|
248
|
-
mockLoggerDebug.mockReturnValueOnce("a warning");
|
|
249
|
-
// runtimeContext is null, should not throw
|
|
250
|
-
expect(() => runtime.writeDebugLog("some.event")).not.toThrow();
|
|
251
|
-
});
|
|
252
|
-
|
|
253
|
-
it("deduplicates logging warnings (same warning not reported twice)", () => {
|
|
254
|
-
const runtime = createExtensionRuntime({ agentDir: "/test/agent" });
|
|
255
|
-
const mockNotify = vi.fn();
|
|
256
|
-
runtime.runtimeContext = {
|
|
257
|
-
hasUI: true,
|
|
258
|
-
ui: { notify: mockNotify },
|
|
259
|
-
} as never;
|
|
260
|
-
mockLoggerDebug
|
|
261
|
-
.mockReturnValueOnce("duplicate warning")
|
|
262
|
-
.mockReturnValueOnce("duplicate warning");
|
|
263
|
-
runtime.writeDebugLog("event.one");
|
|
264
|
-
runtime.writeDebugLog("event.two");
|
|
265
|
-
// The same warning should only be notified once
|
|
266
|
-
expect(mockNotify).toHaveBeenCalledTimes(1);
|
|
267
|
-
expect(mockNotify).toHaveBeenCalledWith("duplicate warning", "warning");
|
|
268
|
-
});
|
|
269
|
-
|
|
270
|
-
it("reports a different warning even after a duplicate has been suppressed", () => {
|
|
271
|
-
const runtime = createExtensionRuntime({ agentDir: "/test/agent" });
|
|
272
|
-
const mockNotify = vi.fn();
|
|
273
|
-
runtime.runtimeContext = {
|
|
274
|
-
hasUI: true,
|
|
275
|
-
ui: { notify: mockNotify },
|
|
276
|
-
} as never;
|
|
277
|
-
mockLoggerDebug
|
|
278
|
-
.mockReturnValueOnce("warning A")
|
|
279
|
-
.mockReturnValueOnce("warning A")
|
|
280
|
-
.mockReturnValueOnce("warning B");
|
|
281
|
-
runtime.writeDebugLog("event.one");
|
|
282
|
-
runtime.writeDebugLog("event.two");
|
|
283
|
-
runtime.writeDebugLog("event.three");
|
|
284
|
-
expect(mockNotify).toHaveBeenCalledTimes(2);
|
|
285
|
-
expect(mockNotify).toHaveBeenNthCalledWith(1, "warning A", "warning");
|
|
286
|
-
expect(mockNotify).toHaveBeenNthCalledWith(2, "warning B", "warning");
|
|
287
|
-
});
|
|
288
|
-
|
|
289
|
-
// ── Multiple independent runtimes ─────────────────────────────────────────
|
|
290
|
-
|
|
291
|
-
it("two runtimes have independent state", () => {
|
|
292
|
-
const rt1 = createExtensionRuntime({ agentDir: "/agent/a" });
|
|
293
|
-
const rt2 = createExtensionRuntime({ agentDir: "/agent/b" });
|
|
294
|
-
rt1.lastKnownActiveAgentName = "agent-a";
|
|
295
|
-
expect(rt2.lastKnownActiveAgentName).toBeNull();
|
|
296
|
-
});
|
|
297
|
-
});
|
|
298
|
-
|
|
299
|
-
// refreshExtensionConfig / saveExtensionConfig / logResolvedConfigPaths are
|
|
300
|
-
// thin delegators to runtime.configStore — behavior covered in config-store.test.ts.
|
|
301
|
-
|
|
302
|
-
// resolveAgentName was moved to PermissionSession (#129)
|
|
303
|
-
// Tests live in test/permission-session.test.ts
|