@gotgenes/pi-permission-system 10.2.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 +14 -0
- package/package.json +1 -1
- package/src/config-modal.ts +7 -10
- package/src/config-store.ts +227 -0
- package/src/index.ts +65 -38
- package/src/permission-prompter.ts +3 -3
- package/src/permission-session.ts +10 -14
- 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-modal.test.ts +15 -10
- package/test/config-store.test.ts +428 -0
- package/test/permission-prompter.test.ts +12 -5
- package/test/permission-session.test.ts +57 -26
- package/test/session-logger.test.ts +151 -64
- package/src/runtime.ts +0 -280
- package/test/runtime.test.ts +0 -487
|
@@ -7,6 +7,7 @@ const mockRequestApproval = vi.fn();
|
|
|
7
7
|
// ── Imports ─────────────────────────────────────────────────────────────────
|
|
8
8
|
|
|
9
9
|
import type { ExtensionContext } from "@earendil-works/pi-coding-agent";
|
|
10
|
+
import type { ConfigReader } from "#src/config-store";
|
|
10
11
|
import { DEFAULT_EXTENSION_CONFIG } from "#src/extension-config";
|
|
11
12
|
import type { PermissionPromptDecision } from "#src/permission-dialog";
|
|
12
13
|
import type { PromptPermissionDetails } from "#src/permission-prompter";
|
|
@@ -38,11 +39,17 @@ function makeDetails(
|
|
|
38
39
|
};
|
|
39
40
|
}
|
|
40
41
|
|
|
42
|
+
function makeConfigReader(
|
|
43
|
+
config: Partial<typeof DEFAULT_EXTENSION_CONFIG> = {},
|
|
44
|
+
): ConfigReader {
|
|
45
|
+
return { current: () => ({ ...DEFAULT_EXTENSION_CONFIG, ...config }) };
|
|
46
|
+
}
|
|
47
|
+
|
|
41
48
|
function makeDeps(
|
|
42
49
|
overrides?: Partial<PermissionPrompterDeps>,
|
|
43
50
|
): PermissionPrompterDeps {
|
|
44
51
|
return {
|
|
45
|
-
|
|
52
|
+
config: makeConfigReader(),
|
|
46
53
|
writeReviewLog: vi.fn(),
|
|
47
54
|
events: { emit: vi.fn(), on: vi.fn().mockReturnValue(() => undefined) },
|
|
48
55
|
forwarder: { requestApproval: mockRequestApproval },
|
|
@@ -70,7 +77,7 @@ describe("PermissionPrompter", () => {
|
|
|
70
77
|
on: vi.fn().mockReturnValue(() => undefined),
|
|
71
78
|
};
|
|
72
79
|
const deps = makeDeps({
|
|
73
|
-
|
|
80
|
+
config: makeConfigReader({ yoloMode: true }),
|
|
74
81
|
events,
|
|
75
82
|
});
|
|
76
83
|
const prompter = new PermissionPrompter(deps);
|
|
@@ -92,7 +99,7 @@ describe("PermissionPrompter", () => {
|
|
|
92
99
|
it("logs permission_request.auto_approved in yolo mode", async () => {
|
|
93
100
|
const writeReviewLog = vi.fn();
|
|
94
101
|
const deps = makeDeps({
|
|
95
|
-
|
|
102
|
+
config: makeConfigReader({ yoloMode: true }),
|
|
96
103
|
writeReviewLog,
|
|
97
104
|
});
|
|
98
105
|
const prompter = new PermissionPrompter(deps);
|
|
@@ -108,7 +115,7 @@ describe("PermissionPrompter", () => {
|
|
|
108
115
|
it("does not log permission_request.waiting in yolo mode", async () => {
|
|
109
116
|
const writeReviewLog = vi.fn();
|
|
110
117
|
const deps = makeDeps({
|
|
111
|
-
|
|
118
|
+
config: makeConfigReader({ yoloMode: true }),
|
|
112
119
|
writeReviewLog,
|
|
113
120
|
});
|
|
114
121
|
const prompter = new PermissionPrompter(deps);
|
|
@@ -123,7 +130,7 @@ describe("PermissionPrompter", () => {
|
|
|
123
130
|
|
|
124
131
|
it("does not call confirmPermission with yoloMode even when ctx has UI", async () => {
|
|
125
132
|
const deps = makeDeps({
|
|
126
|
-
|
|
133
|
+
config: makeConfigReader({ yoloMode: true }),
|
|
127
134
|
});
|
|
128
135
|
const prompter = new PermissionPrompter(deps);
|
|
129
136
|
|
|
@@ -17,6 +17,8 @@ vi.mock("../src/active-agent", () => ({
|
|
|
17
17
|
|
|
18
18
|
// ── Test helpers ───────────────────────────────────────────────────────────
|
|
19
19
|
|
|
20
|
+
import type { SessionConfigStore } from "#src/config-store";
|
|
21
|
+
import { DEFAULT_EXTENSION_CONFIG } from "#src/extension-config";
|
|
20
22
|
import type { ExtensionPaths } from "#src/extension-paths";
|
|
21
23
|
import type { ForwardingController } from "#src/forwarding-manager";
|
|
22
24
|
import type { ScopedPermissionManager } from "#src/permission-manager";
|
|
@@ -27,6 +29,7 @@ import {
|
|
|
27
29
|
import type { Ruleset } from "#src/rule";
|
|
28
30
|
import { SessionApproval } from "#src/session-approval";
|
|
29
31
|
import type { SessionLogger } from "#src/session-logger";
|
|
32
|
+
import { SessionRules } from "#src/session-rules";
|
|
30
33
|
import type { SkillPromptEntry } from "#src/skill-prompt-sanitizer";
|
|
31
34
|
import type { PermissionCheckResult, PermissionState } from "#src/types";
|
|
32
35
|
import { makeCtx } from "#test/helpers/handler-fixtures";
|
|
@@ -66,11 +69,22 @@ function makeLogger(): SessionLogger {
|
|
|
66
69
|
};
|
|
67
70
|
}
|
|
68
71
|
|
|
72
|
+
function makeConfigStore(
|
|
73
|
+
overrides: Partial<SessionConfigStore> = {},
|
|
74
|
+
): SessionConfigStore {
|
|
75
|
+
return {
|
|
76
|
+
current:
|
|
77
|
+
overrides.current ??
|
|
78
|
+
vi
|
|
79
|
+
.fn<() => typeof DEFAULT_EXTENSION_CONFIG>()
|
|
80
|
+
.mockReturnValue({ ...DEFAULT_EXTENSION_CONFIG }),
|
|
81
|
+
refresh: overrides.refresh ?? vi.fn<(ctx?: ExtensionContext) => void>(),
|
|
82
|
+
logResolvedPaths: overrides.logResolvedPaths ?? vi.fn<() => void>(),
|
|
83
|
+
};
|
|
84
|
+
}
|
|
85
|
+
|
|
69
86
|
function makeRuntimeDeps(): PermissionSessionRuntimeDeps {
|
|
70
87
|
return {
|
|
71
|
-
refreshExtensionConfig: vi.fn(),
|
|
72
|
-
logResolvedConfigPaths: vi.fn(),
|
|
73
|
-
getConfig: vi.fn().mockReturnValue({}),
|
|
74
88
|
canRequestPermissionConfirmation: vi.fn().mockReturnValue(true),
|
|
75
89
|
promptPermission: vi
|
|
76
90
|
.fn()
|
|
@@ -116,12 +130,16 @@ function createSession(overrides?: {
|
|
|
116
130
|
logger?: SessionLogger;
|
|
117
131
|
forwarding?: ForwardingController;
|
|
118
132
|
permissionManager?: ScopedPermissionManager;
|
|
133
|
+
sessionRules?: SessionRules;
|
|
134
|
+
configStore?: SessionConfigStore;
|
|
119
135
|
runtimeDeps?: PermissionSessionRuntimeDeps;
|
|
120
136
|
}): {
|
|
121
137
|
session: PermissionSession;
|
|
122
138
|
paths: ExtensionPaths;
|
|
123
139
|
logger: SessionLogger;
|
|
124
140
|
forwarding: ForwardingController;
|
|
141
|
+
sessionRules: SessionRules;
|
|
142
|
+
configStore: SessionConfigStore;
|
|
125
143
|
runtimeDeps: PermissionSessionRuntimeDeps;
|
|
126
144
|
} {
|
|
127
145
|
const paths = makePaths(overrides?.paths);
|
|
@@ -129,15 +147,27 @@ function createSession(overrides?: {
|
|
|
129
147
|
const forwarding = overrides?.forwarding ?? makeForwarding();
|
|
130
148
|
const permissionManager =
|
|
131
149
|
overrides?.permissionManager ?? makePermissionManager();
|
|
150
|
+
const sessionRules = overrides?.sessionRules ?? new SessionRules();
|
|
151
|
+
const configStore = overrides?.configStore ?? makeConfigStore();
|
|
132
152
|
const runtimeDeps = overrides?.runtimeDeps ?? makeRuntimeDeps();
|
|
133
153
|
const session = new PermissionSession(
|
|
134
154
|
paths,
|
|
135
155
|
logger,
|
|
136
156
|
forwarding,
|
|
137
157
|
permissionManager,
|
|
158
|
+
sessionRules,
|
|
159
|
+
configStore,
|
|
138
160
|
runtimeDeps,
|
|
139
161
|
);
|
|
140
|
-
return {
|
|
162
|
+
return {
|
|
163
|
+
session,
|
|
164
|
+
paths,
|
|
165
|
+
logger,
|
|
166
|
+
forwarding,
|
|
167
|
+
sessionRules,
|
|
168
|
+
configStore,
|
|
169
|
+
runtimeDeps,
|
|
170
|
+
};
|
|
141
171
|
}
|
|
142
172
|
|
|
143
173
|
// ── Tests ──────────────────────────────────────────────────────────────────
|
|
@@ -484,11 +514,12 @@ describe("PermissionSession", () => {
|
|
|
484
514
|
|
|
485
515
|
describe("infrastructure paths", () => {
|
|
486
516
|
it("getInfrastructureReadDirs combines piInfrastructureDirs and piInfrastructureReadPaths", () => {
|
|
487
|
-
const
|
|
488
|
-
|
|
489
|
-
|
|
517
|
+
const configStore = makeConfigStore({
|
|
518
|
+
current: vi.fn().mockReturnValue({
|
|
519
|
+
piInfrastructureReadPaths: ["/extra/path"],
|
|
520
|
+
}),
|
|
490
521
|
});
|
|
491
|
-
const { session } = createSession({
|
|
522
|
+
const { session } = createSession({ configStore });
|
|
492
523
|
expect(session.getInfrastructureReadDirs()).toEqual([
|
|
493
524
|
"/test/agent",
|
|
494
525
|
"/test/agent/git",
|
|
@@ -506,36 +537,36 @@ describe("PermissionSession", () => {
|
|
|
506
537
|
});
|
|
507
538
|
|
|
508
539
|
describe("config delegation", () => {
|
|
509
|
-
it("refreshConfig delegates to
|
|
510
|
-
const { session,
|
|
540
|
+
it("refreshConfig delegates to configStore.refresh", () => {
|
|
541
|
+
const { session, configStore } = createSession();
|
|
511
542
|
const ctx = makeCtx();
|
|
512
543
|
session.refreshConfig(ctx);
|
|
513
|
-
expect(
|
|
544
|
+
expect(configStore.refresh).toHaveBeenCalledWith(ctx);
|
|
514
545
|
});
|
|
515
546
|
|
|
516
|
-
it("logResolvedConfigPaths delegates to
|
|
517
|
-
const { session,
|
|
547
|
+
it("logResolvedConfigPaths delegates to configStore.logResolvedPaths", () => {
|
|
548
|
+
const { session, configStore } = createSession();
|
|
518
549
|
session.logResolvedConfigPaths();
|
|
519
|
-
expect(
|
|
550
|
+
expect(configStore.logResolvedPaths).toHaveBeenCalled();
|
|
520
551
|
});
|
|
521
552
|
|
|
522
|
-
it("config getter delegates to
|
|
523
|
-
const
|
|
524
|
-
const
|
|
525
|
-
|
|
526
|
-
|
|
527
|
-
);
|
|
528
|
-
const { session } = createSession({ runtimeDeps });
|
|
553
|
+
it("config getter delegates to configStore.current()", () => {
|
|
554
|
+
const fakeConfig = { debugLog: true } as typeof DEFAULT_EXTENSION_CONFIG;
|
|
555
|
+
const configStore = makeConfigStore({
|
|
556
|
+
current: vi.fn().mockReturnValue(fakeConfig),
|
|
557
|
+
});
|
|
558
|
+
const { session } = createSession({ configStore });
|
|
529
559
|
expect(session.config).toBe(fakeConfig);
|
|
530
560
|
});
|
|
531
561
|
|
|
532
562
|
it("getToolPreviewLimits returns resolved preview limits from config", () => {
|
|
533
|
-
const
|
|
534
|
-
|
|
535
|
-
|
|
536
|
-
|
|
563
|
+
const configStore = makeConfigStore({
|
|
564
|
+
current: vi.fn().mockReturnValue({
|
|
565
|
+
toolInputPreviewMaxLength: 400,
|
|
566
|
+
toolTextSummaryMaxLength: 120,
|
|
567
|
+
}),
|
|
537
568
|
});
|
|
538
|
-
const { session } = createSession({
|
|
569
|
+
const { session } = createSession({ configStore });
|
|
539
570
|
const limits = session.getToolPreviewLimits();
|
|
540
571
|
expect(limits.toolInputPreviewMaxLength).toBe(400);
|
|
541
572
|
expect(limits.toolTextSummaryMaxLength).toBe(120);
|
|
@@ -1,113 +1,200 @@
|
|
|
1
|
-
import {
|
|
2
|
-
import
|
|
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
|
-
|
|
8
|
-
|
|
9
|
-
)
|
|
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
|
-
|
|
12
|
-
|
|
13
|
-
|
|
14
|
-
|
|
15
|
-
|
|
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("
|
|
23
|
-
const
|
|
24
|
-
|
|
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(
|
|
29
|
-
|
|
30
|
-
});
|
|
59
|
+
expect(existsSync(join(tempDir, DEBUG_LOG_FILENAME))).toBe(true);
|
|
60
|
+
expect(deps.notify).not.toHaveBeenCalled();
|
|
31
61
|
});
|
|
32
62
|
|
|
33
|
-
it("
|
|
34
|
-
|
|
35
|
-
const
|
|
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(
|
|
40
|
-
|
|
41
|
-
|
|
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("
|
|
48
|
-
|
|
49
|
-
const
|
|
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(
|
|
54
|
-
|
|
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("
|
|
60
|
-
const
|
|
61
|
-
|
|
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(
|
|
66
|
-
|
|
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
|
-
|
|
73
|
-
|
|
74
|
-
|
|
75
|
-
|
|
76
|
-
|
|
77
|
-
|
|
78
|
-
|
|
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(
|
|
126
|
+
const logger = createSessionLogger(deps);
|
|
81
127
|
|
|
82
|
-
logger.
|
|
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
|
-
|
|
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
|
-
|
|
88
|
-
const runtime = makeRuntime({ runtimeContext: null });
|
|
89
|
-
const logger = createSessionLogger(runtime);
|
|
167
|
+
// ── warn ──────────────────────────────────────────────────────────────────
|
|
90
168
|
|
|
91
|
-
|
|
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("
|
|
95
|
-
const
|
|
96
|
-
const logger = createSessionLogger(
|
|
179
|
+
it("calls notify for every warn — not deduplicated", () => {
|
|
180
|
+
const deps = makeDeps();
|
|
181
|
+
const logger = createSessionLogger(deps);
|
|
97
182
|
|
|
98
|
-
|
|
99
|
-
logger.warn("
|
|
183
|
+
logger.warn("same message");
|
|
184
|
+
logger.warn("same message");
|
|
100
185
|
|
|
101
|
-
|
|
102
|
-
|
|
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
|
-
|
|
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(
|
|
110
|
-
expect(notify).toHaveBeenCalledWith("late warning", "warning");
|
|
197
|
+
expect(() => logger.warn("test")).not.toThrow();
|
|
111
198
|
});
|
|
112
199
|
});
|
|
113
200
|
});
|