@gotgenes/pi-permission-system 5.6.3 → 5.8.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 +31 -0
- package/package.json +1 -1
- package/src/extension-paths.ts +55 -0
- package/src/handlers/gates/bash-external-directory.ts +6 -6
- package/src/{bash-path-extractor.ts → handlers/gates/bash-path-extractor.ts} +1 -1
- package/src/handlers/gates/external-directory.ts +7 -5
- package/src/handlers/gates/tool.ts +1 -1
- package/src/handlers/input.ts +1 -1
- package/src/handlers/lifecycle.ts +3 -3
- package/src/handlers/tool-call.ts +1 -1
- package/src/handlers/types.ts +4 -6
- package/src/index.ts +2 -4
- package/src/runtime.ts +11 -38
- package/src/session-logger.ts +29 -0
- package/tests/bash-external-directory.test.ts +2 -2
- package/tests/extension-paths.test.ts +89 -0
- package/tests/handlers/before-agent-start.test.ts +1 -3
- package/tests/{external-directory-messages.test.ts → handlers/gates/external-directory-messages.test.ts} +1 -1
- package/tests/handlers/input-events.test.ts +1 -3
- package/tests/handlers/input.test.ts +1 -3
- package/tests/handlers/lifecycle.test.ts +9 -11
- package/tests/handlers/tool-call-events.test.ts +1 -3
- package/tests/handlers/tool-call.test.ts +1 -3
- package/tests/pi-infrastructure-read.test.ts +2 -4
- package/tests/runtime.test.ts +1 -1
- package/tests/session-logger.test.ts +113 -0
- package/src/external-directory.ts +0 -24
- /package/src/{external-directory-messages.ts → handlers/gates/external-directory-messages.ts} +0 -0
package/CHANGELOG.md
CHANGED
|
@@ -5,6 +5,37 @@ All notable changes to this project will be documented in this file.
|
|
|
5
5
|
The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.1.0/),
|
|
6
6
|
and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0.html).
|
|
7
7
|
|
|
8
|
+
## [5.8.0](https://github.com/gotgenes/pi-permission-system/compare/v5.7.0...v5.8.0) (2026-05-08)
|
|
9
|
+
|
|
10
|
+
|
|
11
|
+
### Features
|
|
12
|
+
|
|
13
|
+
* add SessionLogger interface and createSessionLogger factory ([#127](https://github.com/gotgenes/pi-permission-system/issues/127)) ([8765ab8](https://github.com/gotgenes/pi-permission-system/commit/8765ab8cfe461324fc2a89c80486d3dde190d9d9))
|
|
14
|
+
|
|
15
|
+
|
|
16
|
+
### Documentation
|
|
17
|
+
|
|
18
|
+
* plan SessionLogger extraction ([#127](https://github.com/gotgenes/pi-permission-system/issues/127)) ([b13ac62](https://github.com/gotgenes/pi-permission-system/commit/b13ac62513d4b233ee4fc3f554324a54518f75ba))
|
|
19
|
+
* **retro:** add retro notes for issue [#126](https://github.com/gotgenes/pi-permission-system/issues/126) ([3d8a38a](https://github.com/gotgenes/pi-permission-system/commit/3d8a38a09f9dfd2570178c856aec260ebdba89b1))
|
|
20
|
+
* update architecture doc for SessionLogger ([#127](https://github.com/gotgenes/pi-permission-system/issues/127)) ([8fa4123](https://github.com/gotgenes/pi-permission-system/commit/8fa41237dc1b43cbe4487ba7d0acf75dc768ad9c))
|
|
21
|
+
|
|
22
|
+
## [5.7.0](https://github.com/gotgenes/pi-permission-system/compare/v5.6.3...v5.7.0) (2026-05-08)
|
|
23
|
+
|
|
24
|
+
|
|
25
|
+
### Features
|
|
26
|
+
|
|
27
|
+
* extract ExtensionPaths value object ([#126](https://github.com/gotgenes/pi-permission-system/issues/126)) ([85bc347](https://github.com/gotgenes/pi-permission-system/commit/85bc347d3ed487210ffbed4c1c53616b5cf0d978))
|
|
28
|
+
|
|
29
|
+
|
|
30
|
+
### Documentation
|
|
31
|
+
|
|
32
|
+
* add handler decomposition plan ([#126](https://github.com/gotgenes/pi-permission-system/issues/126), [#127](https://github.com/gotgenes/pi-permission-system/issues/127), [#128](https://github.com/gotgenes/pi-permission-system/issues/128), [#129](https://github.com/gotgenes/pi-permission-system/issues/129), [#130](https://github.com/gotgenes/pi-permission-system/issues/130)) ([5a116a6](https://github.com/gotgenes/pi-permission-system/commit/5a116a6cf2f6ef29f5e6550821bb26b6e1c3a90f))
|
|
33
|
+
* add structural design heuristics, design-review skill, and plan-issue hook ([d8e3233](https://github.com/gotgenes/pi-permission-system/commit/d8e32330baa25fa5a5abaf75f8e442ce650fe5a9))
|
|
34
|
+
* extract code-style, testing, and markdown-conventions skills from AGENTS.md ([9d5ba7a](https://github.com/gotgenes/pi-permission-system/commit/9d5ba7a4a4a7a9e9fbdfe869fb42840238351b81))
|
|
35
|
+
* plan ExtensionPaths value object extraction ([#126](https://github.com/gotgenes/pi-permission-system/issues/126)) ([d76e6cc](https://github.com/gotgenes/pi-permission-system/commit/d76e6cc255dc124a7a914e8d178113e5b7c8bddd))
|
|
36
|
+
* rename target-architecture to architecture, strip progress indicators ([9776550](https://github.com/gotgenes/pi-permission-system/commit/9776550351f3b59f96bdad614cc5129a7be52a51))
|
|
37
|
+
* **retro:** add retro notes for issue [#110](https://github.com/gotgenes/pi-permission-system/issues/110) ([5597de3](https://github.com/gotgenes/pi-permission-system/commit/5597de3c9c64c3d672a1fc77ba7910e952545824))
|
|
38
|
+
|
|
8
39
|
## [5.6.3](https://github.com/gotgenes/pi-permission-system/compare/v5.6.2...v5.6.3) (2026-05-07)
|
|
9
40
|
|
|
10
41
|
|
package/package.json
CHANGED
|
@@ -0,0 +1,55 @@
|
|
|
1
|
+
import { join } from "node:path";
|
|
2
|
+
import { getGlobalLogsDir } from "./config-paths";
|
|
3
|
+
import { discoverGlobalNodeModulesRoot } from "./node-modules-discovery";
|
|
4
|
+
|
|
5
|
+
/**
|
|
6
|
+
* Immutable path constants derived from `agentDir` at construction time.
|
|
7
|
+
*
|
|
8
|
+
* Computed once at startup in `computeExtensionPaths()` and embedded into
|
|
9
|
+
* `ExtensionRuntime`. Later refactorings (#129 PermissionSession, #130
|
|
10
|
+
* handler classes) consume this as a single dep instead of individual fields.
|
|
11
|
+
*/
|
|
12
|
+
export interface ExtensionPaths {
|
|
13
|
+
readonly agentDir: string;
|
|
14
|
+
readonly sessionsDir: string;
|
|
15
|
+
readonly subagentSessionsDir: string;
|
|
16
|
+
readonly forwardingDir: string;
|
|
17
|
+
readonly globalLogsDir: string;
|
|
18
|
+
/**
|
|
19
|
+
* Static Pi infrastructure directories used for external-directory
|
|
20
|
+
* read auto-allow. Computed once from `agentDir` and
|
|
21
|
+
* `discoverGlobalNodeModulesRoot()`. Config-based extras
|
|
22
|
+
* (`piInfrastructureReadPaths`) are read from `runtime.config` at
|
|
23
|
+
* call time in the handler so they pick up config reloads.
|
|
24
|
+
*/
|
|
25
|
+
readonly piInfrastructureDirs: readonly string[];
|
|
26
|
+
}
|
|
27
|
+
|
|
28
|
+
/**
|
|
29
|
+
* Compute all immutable path constants from `agentDir`.
|
|
30
|
+
*
|
|
31
|
+
* Calls `discoverGlobalNodeModulesRoot()` internally so the result is
|
|
32
|
+
* self-contained. Call this once at extension startup, not at module scope.
|
|
33
|
+
*/
|
|
34
|
+
export function computeExtensionPaths(agentDir: string): ExtensionPaths {
|
|
35
|
+
const sessionsDir = join(agentDir, "sessions");
|
|
36
|
+
const subagentSessionsDir = join(agentDir, "subagent-sessions");
|
|
37
|
+
const forwardingDir = join(sessionsDir, "permission-forwarding");
|
|
38
|
+
const globalLogsDir = getGlobalLogsDir(agentDir);
|
|
39
|
+
|
|
40
|
+
const globalNodeModulesRoot = discoverGlobalNodeModulesRoot();
|
|
41
|
+
const piInfrastructureDirs: string[] = [
|
|
42
|
+
agentDir,
|
|
43
|
+
join(agentDir, "git"),
|
|
44
|
+
...(globalNodeModulesRoot ? [globalNodeModulesRoot] : []),
|
|
45
|
+
];
|
|
46
|
+
|
|
47
|
+
return {
|
|
48
|
+
agentDir,
|
|
49
|
+
sessionsDir,
|
|
50
|
+
subagentSessionsDir,
|
|
51
|
+
forwardingDir,
|
|
52
|
+
globalLogsDir,
|
|
53
|
+
piInfrastructureDirs,
|
|
54
|
+
};
|
|
55
|
+
}
|
|
@@ -1,14 +1,14 @@
|
|
|
1
1
|
import { getNonEmptyString, toRecord } from "../../common";
|
|
2
|
-
import {
|
|
3
|
-
extractExternalPathsFromBashCommand,
|
|
4
|
-
formatBashExternalDirectoryAskPrompt,
|
|
5
|
-
formatBashExternalDirectoryDenyReason,
|
|
6
|
-
formatExternalDirectoryHardStopHint,
|
|
7
|
-
} from "../../external-directory";
|
|
8
2
|
import type { Rule } from "../../rule";
|
|
9
3
|
import { deriveApprovalPattern } from "../../session-rules";
|
|
10
4
|
import type { PermissionCheckResult } from "../../types";
|
|
5
|
+
import { extractExternalPathsFromBashCommand } from "./bash-path-extractor";
|
|
11
6
|
import type { GateResult } from "./descriptor";
|
|
7
|
+
import {
|
|
8
|
+
formatBashExternalDirectoryAskPrompt,
|
|
9
|
+
formatBashExternalDirectoryDenyReason,
|
|
10
|
+
formatExternalDirectoryHardStopHint,
|
|
11
|
+
} from "./external-directory-messages";
|
|
12
12
|
import type { ToolCallContext } from "./types";
|
|
13
13
|
|
|
14
14
|
/** Function type for checkPermission used by the descriptor factory. */
|
|
@@ -1,14 +1,16 @@
|
|
|
1
1
|
import {
|
|
2
|
-
formatExternalDirectoryAskPrompt,
|
|
3
|
-
formatExternalDirectoryDenyReason,
|
|
4
|
-
formatExternalDirectoryUserDeniedReason,
|
|
5
2
|
getPathBearingToolPath,
|
|
6
3
|
isPathOutsideWorkingDirectory,
|
|
7
4
|
isPiInfrastructureRead,
|
|
8
|
-
|
|
9
|
-
|
|
5
|
+
normalizePathForComparison,
|
|
6
|
+
} from "../../path-utils";
|
|
10
7
|
import { deriveApprovalPattern } from "../../session-rules";
|
|
11
8
|
import type { GateResult } from "./descriptor";
|
|
9
|
+
import {
|
|
10
|
+
formatExternalDirectoryAskPrompt,
|
|
11
|
+
formatExternalDirectoryDenyReason,
|
|
12
|
+
formatExternalDirectoryUserDeniedReason,
|
|
13
|
+
} from "./external-directory-messages";
|
|
12
14
|
import type { ToolCallContext } from "./types";
|
|
13
15
|
|
|
14
16
|
/**
|
package/src/handlers/input.ts
CHANGED
|
@@ -33,11 +33,11 @@ export async function handleSessionStart(
|
|
|
33
33
|
const policyIssues =
|
|
34
34
|
deps.session.permissionManager.getConfigIssues(agentName);
|
|
35
35
|
for (const issue of policyIssues) {
|
|
36
|
-
deps.
|
|
36
|
+
deps.logger.warn(issue);
|
|
37
37
|
}
|
|
38
38
|
|
|
39
39
|
if (event.reason === "reload") {
|
|
40
|
-
deps.
|
|
40
|
+
deps.logger.debug("lifecycle.reload", {
|
|
41
41
|
triggeredBy: "session_start",
|
|
42
42
|
reason: event.reason,
|
|
43
43
|
cwd: ctx.cwd,
|
|
@@ -60,7 +60,7 @@ export async function handleResourcesDiscover(
|
|
|
60
60
|
deps.session.activeSkillEntries = [];
|
|
61
61
|
deps.session.lastActiveToolsCacheKey = null;
|
|
62
62
|
deps.session.lastPromptStateCacheKey = null;
|
|
63
|
-
deps.
|
|
63
|
+
deps.logger.debug("lifecycle.reload", {
|
|
64
64
|
triggeredBy: "resources_discover",
|
|
65
65
|
reason: event.reason,
|
|
66
66
|
cwd: runtimeContext?.cwd ?? null,
|
|
@@ -91,7 +91,7 @@ export async function handleToolCall(
|
|
|
91
91
|
deps.promptPermission(ctx, details);
|
|
92
92
|
const emitDecision: GateRunnerDeps["emitDecision"] = (e) =>
|
|
93
93
|
emitDecisionEvent(deps.events, e);
|
|
94
|
-
const { writeReviewLog } = deps;
|
|
94
|
+
const { review: writeReviewLog } = deps.logger;
|
|
95
95
|
const checkPermission: GateRunnerDeps["checkPermission"] = (
|
|
96
96
|
surface,
|
|
97
97
|
input,
|
package/src/handlers/types.ts
CHANGED
|
@@ -4,6 +4,7 @@ import type { PermissionPromptDecision } from "../permission-dialog";
|
|
|
4
4
|
import type { PermissionEventBus } from "../permission-events";
|
|
5
5
|
import type { PermissionManager } from "../permission-manager";
|
|
6
6
|
import type { SessionState } from "../runtime";
|
|
7
|
+
import type { SessionLogger } from "../session-logger";
|
|
7
8
|
|
|
8
9
|
export type PermissionReviewSource = "tool_call" | "skill_input" | "skill_read";
|
|
9
10
|
|
|
@@ -37,12 +38,11 @@ export interface HandlerDeps {
|
|
|
37
38
|
/** Mutable session state: permissionManager, sessionRules, cache keys. */
|
|
38
39
|
readonly session: SessionState;
|
|
39
40
|
|
|
40
|
-
// ── Logging
|
|
41
|
-
|
|
42
|
-
writeReviewLog(event: string, details?: Record<string, unknown>): void;
|
|
41
|
+
// ── Logging ────────────────────────────────────────────────────────────
|
|
42
|
+
readonly logger: SessionLogger;
|
|
43
43
|
|
|
44
44
|
// ── Immutable infrastructure paths ───────────────────────────────────
|
|
45
|
-
readonly piInfrastructureDirs: string[];
|
|
45
|
+
readonly piInfrastructureDirs: readonly string[];
|
|
46
46
|
/** Returns config-derived infrastructure read paths (current at call time). */
|
|
47
47
|
getPiInfrastructureReadPaths(): string[];
|
|
48
48
|
|
|
@@ -59,8 +59,6 @@ export interface HandlerDeps {
|
|
|
59
59
|
// ── Config & lifecycle helpers ─────────────────────────────────────────
|
|
60
60
|
/** Reload merged config from disk; optionally update the stored runtime context. */
|
|
61
61
|
refreshExtensionConfig(ctx?: ExtensionContext): void;
|
|
62
|
-
/** Show a warning notification to the user (no-op when no UI is available). */
|
|
63
|
-
notifyWarning(message: string): void;
|
|
64
62
|
/** Write the resolved config path set to the review and debug logs. */
|
|
65
63
|
logResolvedConfigPaths(): void;
|
|
66
64
|
|
package/src/index.ts
CHANGED
|
@@ -25,6 +25,7 @@ import {
|
|
|
25
25
|
startForwardedPermissionPolling,
|
|
26
26
|
stopForwardedPermissionPolling,
|
|
27
27
|
} from "./runtime";
|
|
28
|
+
import { createSessionLogger } from "./session-logger";
|
|
28
29
|
import { isSubagentExecutionContext } from "./subagent-context";
|
|
29
30
|
import {
|
|
30
31
|
canResolveAskPermissionRequest,
|
|
@@ -79,8 +80,7 @@ export default function piPermissionSystemExtension(pi: ExtensionAPI): void {
|
|
|
79
80
|
|
|
80
81
|
const deps: HandlerDeps = {
|
|
81
82
|
session: runtime,
|
|
82
|
-
|
|
83
|
-
writeReviewLog: (event, details) => runtime.writeReviewLog(event, details),
|
|
83
|
+
logger: createSessionLogger(runtime),
|
|
84
84
|
piInfrastructureDirs: runtime.piInfrastructureDirs,
|
|
85
85
|
getPiInfrastructureReadPaths: () =>
|
|
86
86
|
runtime.config.piInfrastructureReadPaths ?? [],
|
|
@@ -88,8 +88,6 @@ export default function piPermissionSystemExtension(pi: ExtensionAPI): void {
|
|
|
88
88
|
createPermissionManagerForCwd: (cwd) =>
|
|
89
89
|
createPermissionManagerForCwd(runtime.agentDir, cwd),
|
|
90
90
|
refreshExtensionConfig: (ctx) => refreshExtensionConfig(runtime, ctx),
|
|
91
|
-
notifyWarning: (message) =>
|
|
92
|
-
runtime.runtimeContext?.ui.notify(message, "warning"),
|
|
93
91
|
logResolvedConfigPaths: () => logResolvedConfigPaths(runtime),
|
|
94
92
|
resolveAgentName: (ctx, systemPrompt) =>
|
|
95
93
|
resolveAgentName(runtime, ctx, systemPrompt),
|
package/src/runtime.ts
CHANGED
|
@@ -11,6 +11,7 @@ import {
|
|
|
11
11
|
type ExtensionContext,
|
|
12
12
|
getAgentDir,
|
|
13
13
|
} from "@mariozechner/pi-coding-agent";
|
|
14
|
+
|
|
14
15
|
import {
|
|
15
16
|
getActiveAgentName,
|
|
16
17
|
getActiveAgentNameFromSystemPrompt,
|
|
@@ -19,7 +20,6 @@ import { loadAndMergeConfigs, loadUnifiedConfig } from "./config-loader";
|
|
|
19
20
|
import {
|
|
20
21
|
DEBUG_LOG_FILENAME,
|
|
21
22
|
getGlobalConfigPath,
|
|
22
|
-
getGlobalLogsDir,
|
|
23
23
|
getLegacyExtensionConfigPath,
|
|
24
24
|
getLegacyGlobalPolicyPath,
|
|
25
25
|
getLegacyProjectPolicyPath,
|
|
@@ -34,7 +34,10 @@ import {
|
|
|
34
34
|
normalizePermissionSystemConfig,
|
|
35
35
|
type PermissionSystemExtensionConfig,
|
|
36
36
|
} from "./extension-config";
|
|
37
|
-
import {
|
|
37
|
+
import { computeExtensionPaths, type ExtensionPaths } from "./extension-paths";
|
|
38
|
+
|
|
39
|
+
export type { ExtensionPaths } from "./extension-paths";
|
|
40
|
+
|
|
38
41
|
import {
|
|
39
42
|
type PermissionForwardingDeps,
|
|
40
43
|
processForwardedPermissionRequests,
|
|
@@ -73,22 +76,7 @@ export interface SessionState {
|
|
|
73
76
|
* Tests construct this via `createExtensionRuntime({ agentDir: tmpDir })`
|
|
74
77
|
* without timing issues around `PI_CODING_AGENT_DIR`.
|
|
75
78
|
*/
|
|
76
|
-
export interface ExtensionRuntime extends SessionState {
|
|
77
|
-
// ── Immutable paths (derived from agentDir at construction) ───────────
|
|
78
|
-
readonly agentDir: string;
|
|
79
|
-
readonly sessionsDir: string;
|
|
80
|
-
readonly subagentSessionsDir: string;
|
|
81
|
-
readonly forwardingDir: string;
|
|
82
|
-
readonly globalLogsDir: string;
|
|
83
|
-
/**
|
|
84
|
-
* Static Pi infrastructure directories used for external-directory
|
|
85
|
-
* read auto-allow. Computed once at construction from `agentDir` and
|
|
86
|
-
* `discoverGlobalNodeModulesRoot()`. Config-based extras
|
|
87
|
-
* (`piInfrastructureReadPaths`) are read from `runtime.config` at
|
|
88
|
-
* call time in the handler so they pick up config reloads.
|
|
89
|
-
*/
|
|
90
|
-
readonly piInfrastructureDirs: string[];
|
|
91
|
-
|
|
79
|
+
export interface ExtensionRuntime extends ExtensionPaths, SessionState {
|
|
92
80
|
// ── Mutable state (beyond SessionState) ───────────────────────────────────
|
|
93
81
|
config: PermissionSystemExtensionConfig;
|
|
94
82
|
lastConfigWarning: string | null;
|
|
@@ -353,27 +341,12 @@ export function createExtensionRuntime(options?: {
|
|
|
353
341
|
agentDir?: string;
|
|
354
342
|
}): ExtensionRuntime {
|
|
355
343
|
const agentDir = options?.agentDir ?? getAgentDir();
|
|
356
|
-
const
|
|
357
|
-
const subagentSessionsDir = join(agentDir, "subagent-sessions");
|
|
358
|
-
const forwardingDir = join(sessionsDir, "permission-forwarding");
|
|
359
|
-
const globalLogsDir = getGlobalLogsDir(agentDir);
|
|
360
|
-
|
|
361
|
-
const globalNodeModulesRoot = discoverGlobalNodeModulesRoot();
|
|
362
|
-
const piInfrastructureDirs: string[] = [
|
|
363
|
-
agentDir,
|
|
364
|
-
join(agentDir, "git"),
|
|
365
|
-
...(globalNodeModulesRoot ? [globalNodeModulesRoot] : []),
|
|
366
|
-
];
|
|
344
|
+
const paths = computeExtensionPaths(agentDir);
|
|
367
345
|
|
|
368
346
|
// Build a plain-object runtime first so the logger's `getConfig` closure
|
|
369
347
|
// can reference `runtime.config` directly (always reads current value).
|
|
370
348
|
const runtime: ExtensionRuntime = {
|
|
371
|
-
|
|
372
|
-
sessionsDir,
|
|
373
|
-
subagentSessionsDir,
|
|
374
|
-
forwardingDir,
|
|
375
|
-
globalLogsDir,
|
|
376
|
-
piInfrastructureDirs,
|
|
349
|
+
...paths,
|
|
377
350
|
config: { ...DEFAULT_EXTENSION_CONFIG },
|
|
378
351
|
runtimeContext: null,
|
|
379
352
|
permissionManager: createPermissionManagerForCwd(agentDir, undefined),
|
|
@@ -395,10 +368,10 @@ export function createExtensionRuntime(options?: {
|
|
|
395
368
|
const logger = createPermissionSystemLogger({
|
|
396
369
|
// Reads runtime.config at call time — always current.
|
|
397
370
|
getConfig: () => runtime.config,
|
|
398
|
-
debugLogPath: join(globalLogsDir, DEBUG_LOG_FILENAME),
|
|
399
|
-
reviewLogPath: join(globalLogsDir, REVIEW_LOG_FILENAME),
|
|
371
|
+
debugLogPath: join(paths.globalLogsDir, DEBUG_LOG_FILENAME),
|
|
372
|
+
reviewLogPath: join(paths.globalLogsDir, REVIEW_LOG_FILENAME),
|
|
400
373
|
ensureLogsDirectory: () =>
|
|
401
|
-
ensurePermissionSystemLogsDirectory(globalLogsDir),
|
|
374
|
+
ensurePermissionSystemLogsDirectory(paths.globalLogsDir),
|
|
402
375
|
});
|
|
403
376
|
|
|
404
377
|
const reportLoggingWarning = (message: string): void => {
|
|
@@ -0,0 +1,29 @@
|
|
|
1
|
+
import type { ExtensionRuntime } from "./runtime";
|
|
2
|
+
|
|
3
|
+
/**
|
|
4
|
+
* Unified logging + notification surface for handler deps.
|
|
5
|
+
*
|
|
6
|
+
* Replaces three separate HandlerDeps fields (`writeDebugLog`,
|
|
7
|
+
* `writeReviewLog`, `notifyWarning`) with a single typed collaborator.
|
|
8
|
+
* This is an intermediate abstraction on the path to PermissionSession (#129).
|
|
9
|
+
*/
|
|
10
|
+
export interface SessionLogger {
|
|
11
|
+
debug(event: string, details?: Record<string, unknown>): void;
|
|
12
|
+
review(event: string, details?: Record<string, unknown>): void;
|
|
13
|
+
warn(message: string): void;
|
|
14
|
+
}
|
|
15
|
+
|
|
16
|
+
/**
|
|
17
|
+
* Create a SessionLogger backed by an ExtensionRuntime.
|
|
18
|
+
*
|
|
19
|
+
* Captures `runtime` by reference so `warn` always reads the current
|
|
20
|
+
* `runtimeContext` at call time — matching the behavior of the inline
|
|
21
|
+
* closures it replaces in `src/index.ts`.
|
|
22
|
+
*/
|
|
23
|
+
export function createSessionLogger(runtime: ExtensionRuntime): SessionLogger {
|
|
24
|
+
return {
|
|
25
|
+
debug: (event, details) => runtime.writeDebugLog(event, details),
|
|
26
|
+
review: (event, details) => runtime.writeReviewLog(event, details),
|
|
27
|
+
warn: (message) => runtime.runtimeContext?.ui.notify(message, "warning"),
|
|
28
|
+
};
|
|
29
|
+
}
|
|
@@ -9,11 +9,11 @@ vi.mock("node:os", () => {
|
|
|
9
9
|
};
|
|
10
10
|
});
|
|
11
11
|
|
|
12
|
+
import { extractExternalPathsFromBashCommand } from "../src/handlers/gates/bash-path-extractor";
|
|
12
13
|
import {
|
|
13
|
-
extractExternalPathsFromBashCommand,
|
|
14
14
|
formatBashExternalDirectoryAskPrompt,
|
|
15
15
|
formatBashExternalDirectoryDenyReason,
|
|
16
|
-
} from "../src/external-directory";
|
|
16
|
+
} from "../src/handlers/gates/external-directory-messages";
|
|
17
17
|
|
|
18
18
|
afterEach(() => {
|
|
19
19
|
vi.restoreAllMocks();
|
|
@@ -0,0 +1,89 @@
|
|
|
1
|
+
import { join } from "node:path";
|
|
2
|
+
import { beforeEach, describe, expect, it, vi } from "vitest";
|
|
3
|
+
|
|
4
|
+
const { mockDiscoverGlobalNodeModulesRoot } = vi.hoisted(() => ({
|
|
5
|
+
mockDiscoverGlobalNodeModulesRoot: vi.fn<() => string | null>(),
|
|
6
|
+
}));
|
|
7
|
+
|
|
8
|
+
vi.mock("../src/node-modules-discovery", () => ({
|
|
9
|
+
discoverGlobalNodeModulesRoot: mockDiscoverGlobalNodeModulesRoot,
|
|
10
|
+
}));
|
|
11
|
+
|
|
12
|
+
import { getGlobalLogsDir } from "../src/config-paths";
|
|
13
|
+
import { computeExtensionPaths } from "../src/extension-paths";
|
|
14
|
+
|
|
15
|
+
describe("computeExtensionPaths", () => {
|
|
16
|
+
beforeEach(() => {
|
|
17
|
+
mockDiscoverGlobalNodeModulesRoot.mockReset();
|
|
18
|
+
mockDiscoverGlobalNodeModulesRoot.mockReturnValue(
|
|
19
|
+
"/mock/global/node_modules",
|
|
20
|
+
);
|
|
21
|
+
});
|
|
22
|
+
|
|
23
|
+
it("sets agentDir from argument", () => {
|
|
24
|
+
const paths = computeExtensionPaths("/test/agent");
|
|
25
|
+
expect(paths.agentDir).toBe("/test/agent");
|
|
26
|
+
});
|
|
27
|
+
|
|
28
|
+
it("derives sessionsDir as agentDir/sessions", () => {
|
|
29
|
+
const paths = computeExtensionPaths("/test/agent");
|
|
30
|
+
expect(paths.sessionsDir).toBe("/test/agent/sessions");
|
|
31
|
+
});
|
|
32
|
+
|
|
33
|
+
it("derives subagentSessionsDir as agentDir/subagent-sessions", () => {
|
|
34
|
+
const paths = computeExtensionPaths("/test/agent");
|
|
35
|
+
expect(paths.subagentSessionsDir).toBe("/test/agent/subagent-sessions");
|
|
36
|
+
});
|
|
37
|
+
|
|
38
|
+
it("derives forwardingDir as sessionsDir/permission-forwarding", () => {
|
|
39
|
+
const paths = computeExtensionPaths("/test/agent");
|
|
40
|
+
expect(paths.forwardingDir).toBe(
|
|
41
|
+
join("/test/agent/sessions", "permission-forwarding"),
|
|
42
|
+
);
|
|
43
|
+
});
|
|
44
|
+
|
|
45
|
+
it("derives globalLogsDir via getGlobalLogsDir(agentDir)", () => {
|
|
46
|
+
const paths = computeExtensionPaths("/test/agent");
|
|
47
|
+
expect(paths.globalLogsDir).toBe(getGlobalLogsDir("/test/agent"));
|
|
48
|
+
});
|
|
49
|
+
|
|
50
|
+
it("includes agentDir in piInfrastructureDirs", () => {
|
|
51
|
+
const paths = computeExtensionPaths("/test/agent");
|
|
52
|
+
expect(paths.piInfrastructureDirs).toContain("/test/agent");
|
|
53
|
+
});
|
|
54
|
+
|
|
55
|
+
it("includes agentDir/git in piInfrastructureDirs", () => {
|
|
56
|
+
const paths = computeExtensionPaths("/test/agent");
|
|
57
|
+
expect(paths.piInfrastructureDirs).toContain("/test/agent/git");
|
|
58
|
+
});
|
|
59
|
+
|
|
60
|
+
it("includes discovered global node_modules root in piInfrastructureDirs", () => {
|
|
61
|
+
const paths = computeExtensionPaths("/test/agent");
|
|
62
|
+
expect(paths.piInfrastructureDirs).toContain("/mock/global/node_modules");
|
|
63
|
+
});
|
|
64
|
+
|
|
65
|
+
it("omits global node_modules from piInfrastructureDirs when discovery returns null", () => {
|
|
66
|
+
mockDiscoverGlobalNodeModulesRoot.mockReturnValue(null);
|
|
67
|
+
const paths = computeExtensionPaths("/test/agent");
|
|
68
|
+
expect(paths.piInfrastructureDirs).toHaveLength(2);
|
|
69
|
+
expect(paths.piInfrastructureDirs).toContain("/test/agent");
|
|
70
|
+
expect(paths.piInfrastructureDirs).toContain("/test/agent/git");
|
|
71
|
+
});
|
|
72
|
+
|
|
73
|
+
it("all entries in piInfrastructureDirs are strings (no null)", () => {
|
|
74
|
+
mockDiscoverGlobalNodeModulesRoot.mockReturnValue(null);
|
|
75
|
+
const paths = computeExtensionPaths("/test/agent");
|
|
76
|
+
for (const dir of paths.piInfrastructureDirs) {
|
|
77
|
+
expect(typeof dir).toBe("string");
|
|
78
|
+
}
|
|
79
|
+
});
|
|
80
|
+
|
|
81
|
+
it("two calls with different agentDirs produce independent results", () => {
|
|
82
|
+
const a = computeExtensionPaths("/agent/a");
|
|
83
|
+
const b = computeExtensionPaths("/agent/b");
|
|
84
|
+
expect(a.agentDir).toBe("/agent/a");
|
|
85
|
+
expect(b.agentDir).toBe("/agent/b");
|
|
86
|
+
expect(a.sessionsDir).toBe("/agent/a/sessions");
|
|
87
|
+
expect(b.sessionsDir).toBe("/agent/b/sessions");
|
|
88
|
+
});
|
|
89
|
+
});
|
|
@@ -77,13 +77,11 @@ function makeSession(overrides: Partial<SessionState> = {}): SessionState {
|
|
|
77
77
|
function makeDeps(overrides: Partial<HandlerDeps> = {}): HandlerDeps {
|
|
78
78
|
return {
|
|
79
79
|
session: makeSession(),
|
|
80
|
-
|
|
81
|
-
writeReviewLog: vi.fn(),
|
|
80
|
+
logger: { debug: vi.fn(), review: vi.fn(), warn: vi.fn() },
|
|
82
81
|
piInfrastructureDirs: ["/test/agent", "/test/agent/git"],
|
|
83
82
|
getPiInfrastructureReadPaths: vi.fn().mockReturnValue([]),
|
|
84
83
|
createPermissionManagerForCwd: vi.fn().mockReturnValue(makePm()),
|
|
85
84
|
refreshExtensionConfig: vi.fn(),
|
|
86
|
-
notifyWarning: vi.fn(),
|
|
87
85
|
logResolvedConfigPaths: vi.fn(),
|
|
88
86
|
resolveAgentName: vi.fn().mockReturnValue(null),
|
|
89
87
|
canRequestPermissionConfirmation: vi.fn().mockReturnValue(false),
|
|
@@ -7,7 +7,7 @@ import {
|
|
|
7
7
|
formatExternalDirectoryDenyReason,
|
|
8
8
|
formatExternalDirectoryHardStopHint,
|
|
9
9
|
formatExternalDirectoryUserDeniedReason,
|
|
10
|
-
} from "
|
|
10
|
+
} from "../../../src/handlers/gates/external-directory-messages";
|
|
11
11
|
|
|
12
12
|
describe("formatExternalDirectoryHardStopHint", () => {
|
|
13
13
|
test("returns the hard stop instruction string", () => {
|
|
@@ -68,14 +68,12 @@ function makeDeps(
|
|
|
68
68
|
): HandlerDeps {
|
|
69
69
|
return {
|
|
70
70
|
session: makeSession(state),
|
|
71
|
-
|
|
72
|
-
writeReviewLog: vi.fn(),
|
|
71
|
+
logger: { debug: vi.fn(), review: vi.fn(), warn: vi.fn() },
|
|
73
72
|
piInfrastructureDirs: ["/test/agent"],
|
|
74
73
|
getPiInfrastructureReadPaths: vi.fn().mockReturnValue([]),
|
|
75
74
|
events: makeEvents(),
|
|
76
75
|
createPermissionManagerForCwd: vi.fn(),
|
|
77
76
|
refreshExtensionConfig: vi.fn(),
|
|
78
|
-
notifyWarning: vi.fn(),
|
|
79
77
|
logResolvedConfigPaths: vi.fn(),
|
|
80
78
|
resolveAgentName: vi.fn().mockReturnValue(null),
|
|
81
79
|
canRequestPermissionConfirmation: vi.fn().mockReturnValue(true),
|
|
@@ -56,13 +56,11 @@ function makeSession(overrides: Partial<SessionState> = {}): SessionState {
|
|
|
56
56
|
function makeDeps(overrides: Partial<HandlerDeps> = {}): HandlerDeps {
|
|
57
57
|
return {
|
|
58
58
|
session: makeSession(),
|
|
59
|
-
|
|
60
|
-
writeReviewLog: vi.fn(),
|
|
59
|
+
logger: { debug: vi.fn(), review: vi.fn(), warn: vi.fn() },
|
|
61
60
|
piInfrastructureDirs: ["/test/agent", "/test/agent/git"],
|
|
62
61
|
getPiInfrastructureReadPaths: vi.fn().mockReturnValue([]),
|
|
63
62
|
createPermissionManagerForCwd: vi.fn(),
|
|
64
63
|
refreshExtensionConfig: vi.fn(),
|
|
65
|
-
notifyWarning: vi.fn(),
|
|
66
64
|
logResolvedConfigPaths: vi.fn(),
|
|
67
65
|
resolveAgentName: vi.fn().mockReturnValue(null),
|
|
68
66
|
canRequestPermissionConfirmation: vi.fn().mockReturnValue(true),
|
|
@@ -83,15 +83,13 @@ function makeSession(overrides: Partial<SessionState> = {}): SessionState {
|
|
|
83
83
|
function makeDeps(overrides: Partial<HandlerDeps> = {}): HandlerDeps {
|
|
84
84
|
return {
|
|
85
85
|
session: makeSession(),
|
|
86
|
-
|
|
87
|
-
writeReviewLog: vi.fn(),
|
|
86
|
+
logger: { debug: vi.fn(), review: vi.fn(), warn: vi.fn() },
|
|
88
87
|
piInfrastructureDirs: ["/test/agent", "/test/agent/git"],
|
|
89
88
|
getPiInfrastructureReadPaths: vi.fn().mockReturnValue([]),
|
|
90
89
|
createPermissionManagerForCwd: vi
|
|
91
90
|
.fn()
|
|
92
91
|
.mockReturnValue(makePermissionManager()),
|
|
93
92
|
refreshExtensionConfig: vi.fn(),
|
|
94
|
-
notifyWarning: vi.fn(),
|
|
95
93
|
logResolvedConfigPaths: vi.fn(),
|
|
96
94
|
resolveAgentName: vi.fn().mockReturnValue(null),
|
|
97
95
|
canRequestPermissionConfirmation: vi.fn().mockReturnValue(false),
|
|
@@ -189,21 +187,21 @@ describe("handleSessionStart", () => {
|
|
|
189
187
|
createPermissionManagerForCwd: vi.fn().mockReturnValue(pm),
|
|
190
188
|
});
|
|
191
189
|
await handleSessionStart(deps, { reason: "startup" }, makeCtx());
|
|
192
|
-
expect(deps.
|
|
193
|
-
expect(deps.
|
|
190
|
+
expect(deps.logger.warn).toHaveBeenCalledWith("issue A");
|
|
191
|
+
expect(deps.logger.warn).toHaveBeenCalledWith("issue B");
|
|
194
192
|
});
|
|
195
193
|
|
|
196
194
|
it("does not call notifyWarning when there are no policy issues", async () => {
|
|
197
195
|
const deps = makeDeps();
|
|
198
196
|
await handleSessionStart(deps, { reason: "startup" }, makeCtx());
|
|
199
|
-
expect(deps.
|
|
197
|
+
expect(deps.logger.warn).not.toHaveBeenCalled();
|
|
200
198
|
});
|
|
201
199
|
|
|
202
200
|
it("writes lifecycle.reload debug log when reason is reload", async () => {
|
|
203
201
|
const ctx = makeCtx({ cwd: "/proj" });
|
|
204
202
|
const deps = makeDeps();
|
|
205
203
|
await handleSessionStart(deps, { reason: "reload" }, ctx);
|
|
206
|
-
expect(deps.
|
|
204
|
+
expect(deps.logger.debug).toHaveBeenCalledWith("lifecycle.reload", {
|
|
207
205
|
triggeredBy: "session_start",
|
|
208
206
|
reason: "reload",
|
|
209
207
|
cwd: "/proj",
|
|
@@ -213,7 +211,7 @@ describe("handleSessionStart", () => {
|
|
|
213
211
|
it("does not write lifecycle.reload debug log for non-reload reasons", async () => {
|
|
214
212
|
const deps = makeDeps();
|
|
215
213
|
await handleSessionStart(deps, { reason: "startup" }, makeCtx());
|
|
216
|
-
expect(deps.
|
|
214
|
+
expect(deps.logger.debug).not.toHaveBeenCalled();
|
|
217
215
|
});
|
|
218
216
|
});
|
|
219
217
|
|
|
@@ -224,7 +222,7 @@ describe("handleResourcesDiscover", () => {
|
|
|
224
222
|
const deps = makeDeps();
|
|
225
223
|
await handleResourcesDiscover(deps, { reason: "startup" });
|
|
226
224
|
expect(deps.createPermissionManagerForCwd).not.toHaveBeenCalled();
|
|
227
|
-
expect(deps.
|
|
225
|
+
expect(deps.logger.debug).not.toHaveBeenCalled();
|
|
228
226
|
});
|
|
229
227
|
|
|
230
228
|
it("creates and stores a new PM using runtimeContext.cwd on reload", async () => {
|
|
@@ -259,7 +257,7 @@ describe("handleResourcesDiscover", () => {
|
|
|
259
257
|
const ctx = makeCtx({ cwd: "/proj" });
|
|
260
258
|
const deps = makeDeps({ session: makeSession({ runtimeContext: ctx }) });
|
|
261
259
|
await handleResourcesDiscover(deps, { reason: "reload" });
|
|
262
|
-
expect(deps.
|
|
260
|
+
expect(deps.logger.debug).toHaveBeenCalledWith("lifecycle.reload", {
|
|
263
261
|
triggeredBy: "resources_discover",
|
|
264
262
|
reason: "reload",
|
|
265
263
|
cwd: "/proj",
|
|
@@ -269,7 +267,7 @@ describe("handleResourcesDiscover", () => {
|
|
|
269
267
|
it("logs cwd as null when runtimeContext is null on reload", async () => {
|
|
270
268
|
const deps = makeDeps();
|
|
271
269
|
await handleResourcesDiscover(deps, { reason: "reload" });
|
|
272
|
-
expect(deps.
|
|
270
|
+
expect(deps.logger.debug).toHaveBeenCalledWith("lifecycle.reload", {
|
|
273
271
|
triggeredBy: "resources_discover",
|
|
274
272
|
reason: "reload",
|
|
275
273
|
cwd: null,
|
|
@@ -89,14 +89,12 @@ function makeSession(overrides: Partial<SessionState> = {}): SessionState {
|
|
|
89
89
|
function makeDeps(overrides: Partial<HandlerDeps> = {}): HandlerDeps {
|
|
90
90
|
return {
|
|
91
91
|
session: makeSession(),
|
|
92
|
-
|
|
93
|
-
writeReviewLog: vi.fn(),
|
|
92
|
+
logger: { debug: vi.fn(), review: vi.fn(), warn: vi.fn() },
|
|
94
93
|
piInfrastructureDirs: ["/test/agent", "/test/agent/git"],
|
|
95
94
|
getPiInfrastructureReadPaths: vi.fn().mockReturnValue([]),
|
|
96
95
|
events: makeEvents(),
|
|
97
96
|
createPermissionManagerForCwd: vi.fn(),
|
|
98
97
|
refreshExtensionConfig: vi.fn(),
|
|
99
|
-
notifyWarning: vi.fn(),
|
|
100
98
|
logResolvedConfigPaths: vi.fn(),
|
|
101
99
|
resolveAgentName: vi.fn().mockReturnValue(null),
|
|
102
100
|
canRequestPermissionConfirmation: vi.fn().mockReturnValue(true),
|
|
@@ -77,13 +77,11 @@ function makeSession(overrides: Partial<SessionState> = {}): SessionState {
|
|
|
77
77
|
function makeDeps(overrides: Partial<HandlerDeps> = {}): HandlerDeps {
|
|
78
78
|
return {
|
|
79
79
|
session: makeSession(),
|
|
80
|
-
|
|
81
|
-
writeReviewLog: vi.fn(),
|
|
80
|
+
logger: { debug: vi.fn(), review: vi.fn(), warn: vi.fn() },
|
|
82
81
|
piInfrastructureDirs: ["/test/agent", "/test/agent/git"],
|
|
83
82
|
getPiInfrastructureReadPaths: vi.fn().mockReturnValue([]),
|
|
84
83
|
createPermissionManagerForCwd: vi.fn(),
|
|
85
84
|
refreshExtensionConfig: vi.fn(),
|
|
86
|
-
notifyWarning: vi.fn(),
|
|
87
85
|
logResolvedConfigPaths: vi.fn(),
|
|
88
86
|
resolveAgentName: vi.fn().mockReturnValue(null),
|
|
89
87
|
canRequestPermissionConfirmation: vi.fn().mockReturnValue(true),
|
|
@@ -14,10 +14,8 @@ vi.mock("node:child_process", () => ({
|
|
|
14
14
|
default: { spawnSync: mockSpawnSync },
|
|
15
15
|
}));
|
|
16
16
|
|
|
17
|
-
import {
|
|
18
|
-
|
|
19
|
-
isPiInfrastructureRead,
|
|
20
|
-
} from "../src/external-directory";
|
|
17
|
+
import { discoverGlobalNodeModulesRoot } from "../src/node-modules-discovery";
|
|
18
|
+
import { isPiInfrastructureRead } from "../src/path-utils";
|
|
21
19
|
|
|
22
20
|
// ── discoverGlobalNodeModulesRoot ──────────────────────────────────────────
|
|
23
21
|
|
package/tests/runtime.test.ts
CHANGED
|
@@ -67,7 +67,7 @@ vi.mock("../src/subagent-context", () => ({
|
|
|
67
67
|
isSubagentExecutionContext: vi.fn().mockReturnValue(false),
|
|
68
68
|
}));
|
|
69
69
|
|
|
70
|
-
vi.mock("../src/
|
|
70
|
+
vi.mock("../src/node-modules-discovery", () => ({
|
|
71
71
|
discoverGlobalNodeModulesRoot: mockDiscoverGlobalNodeModulesRoot,
|
|
72
72
|
}));
|
|
73
73
|
|
|
@@ -0,0 +1,113 @@
|
|
|
1
|
+
import { describe, expect, it, vi } from "vitest";
|
|
2
|
+
import type { ExtensionRuntime } from "../src/runtime";
|
|
3
|
+
import { createSessionLogger } from "../src/session-logger";
|
|
4
|
+
|
|
5
|
+
// ── helpers ────────────────────────────────────────────────────────────────
|
|
6
|
+
|
|
7
|
+
function makeRuntime(
|
|
8
|
+
overrides: Partial<ExtensionRuntime> = {},
|
|
9
|
+
): ExtensionRuntime {
|
|
10
|
+
return {
|
|
11
|
+
runtimeContext: null,
|
|
12
|
+
writeDebugLog: vi.fn(),
|
|
13
|
+
writeReviewLog: vi.fn(),
|
|
14
|
+
...overrides,
|
|
15
|
+
} as unknown as ExtensionRuntime;
|
|
16
|
+
}
|
|
17
|
+
|
|
18
|
+
// ── createSessionLogger ────────────────────────────────────────────────────
|
|
19
|
+
|
|
20
|
+
describe("createSessionLogger", () => {
|
|
21
|
+
describe("debug", () => {
|
|
22
|
+
it("delegates to runtime.writeDebugLog with event and details", () => {
|
|
23
|
+
const runtime = makeRuntime();
|
|
24
|
+
const logger = createSessionLogger(runtime);
|
|
25
|
+
|
|
26
|
+
logger.debug("test.event", { key: "value" });
|
|
27
|
+
|
|
28
|
+
expect(runtime.writeDebugLog).toHaveBeenCalledWith("test.event", {
|
|
29
|
+
key: "value",
|
|
30
|
+
});
|
|
31
|
+
});
|
|
32
|
+
|
|
33
|
+
it("delegates to runtime.writeDebugLog with event and no details", () => {
|
|
34
|
+
const runtime = makeRuntime();
|
|
35
|
+
const logger = createSessionLogger(runtime);
|
|
36
|
+
|
|
37
|
+
logger.debug("test.event");
|
|
38
|
+
|
|
39
|
+
expect(runtime.writeDebugLog).toHaveBeenCalledWith(
|
|
40
|
+
"test.event",
|
|
41
|
+
undefined,
|
|
42
|
+
);
|
|
43
|
+
});
|
|
44
|
+
});
|
|
45
|
+
|
|
46
|
+
describe("review", () => {
|
|
47
|
+
it("delegates to runtime.writeReviewLog with event and details", () => {
|
|
48
|
+
const runtime = makeRuntime();
|
|
49
|
+
const logger = createSessionLogger(runtime);
|
|
50
|
+
|
|
51
|
+
logger.review("permission.granted", { agentName: "coder" });
|
|
52
|
+
|
|
53
|
+
expect(runtime.writeReviewLog).toHaveBeenCalledWith(
|
|
54
|
+
"permission.granted",
|
|
55
|
+
{ agentName: "coder" },
|
|
56
|
+
);
|
|
57
|
+
});
|
|
58
|
+
|
|
59
|
+
it("delegates to runtime.writeReviewLog with event and no details", () => {
|
|
60
|
+
const runtime = makeRuntime();
|
|
61
|
+
const logger = createSessionLogger(runtime);
|
|
62
|
+
|
|
63
|
+
logger.review("permission.granted");
|
|
64
|
+
|
|
65
|
+
expect(runtime.writeReviewLog).toHaveBeenCalledWith(
|
|
66
|
+
"permission.granted",
|
|
67
|
+
undefined,
|
|
68
|
+
);
|
|
69
|
+
});
|
|
70
|
+
});
|
|
71
|
+
|
|
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"],
|
|
79
|
+
});
|
|
80
|
+
const logger = createSessionLogger(runtime);
|
|
81
|
+
|
|
82
|
+
logger.warn("Something went wrong");
|
|
83
|
+
|
|
84
|
+
expect(notify).toHaveBeenCalledWith("Something went wrong", "warning");
|
|
85
|
+
});
|
|
86
|
+
|
|
87
|
+
it("does not throw when runtimeContext is null", () => {
|
|
88
|
+
const runtime = makeRuntime({ runtimeContext: null });
|
|
89
|
+
const logger = createSessionLogger(runtime);
|
|
90
|
+
|
|
91
|
+
expect(() => logger.warn("no-op warning")).not.toThrow();
|
|
92
|
+
});
|
|
93
|
+
|
|
94
|
+
it("reads runtimeContext at call time, not at creation time", () => {
|
|
95
|
+
const runtime = makeRuntime({ runtimeContext: null });
|
|
96
|
+
const logger = createSessionLogger(runtime);
|
|
97
|
+
|
|
98
|
+
// runtimeContext is null at creation — warn should be a no-op now
|
|
99
|
+
logger.warn("early warning");
|
|
100
|
+
|
|
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"];
|
|
106
|
+
|
|
107
|
+
logger.warn("late warning");
|
|
108
|
+
|
|
109
|
+
expect(notify).toHaveBeenCalledOnce();
|
|
110
|
+
expect(notify).toHaveBeenCalledWith("late warning", "warning");
|
|
111
|
+
});
|
|
112
|
+
});
|
|
113
|
+
});
|
|
@@ -1,24 +0,0 @@
|
|
|
1
|
-
export {
|
|
2
|
-
extractExternalPathsFromBashCommand,
|
|
3
|
-
resetParserForTesting,
|
|
4
|
-
} from "./bash-path-extractor";
|
|
5
|
-
export {
|
|
6
|
-
formatBashExternalDirectoryAskPrompt,
|
|
7
|
-
formatBashExternalDirectoryDenyReason,
|
|
8
|
-
formatExternalDirectoryAskPrompt,
|
|
9
|
-
formatExternalDirectoryDenyReason,
|
|
10
|
-
formatExternalDirectoryHardStopHint,
|
|
11
|
-
formatExternalDirectoryUserDeniedReason,
|
|
12
|
-
} from "./external-directory-messages";
|
|
13
|
-
export { discoverGlobalNodeModulesRoot } from "./node-modules-discovery";
|
|
14
|
-
export {
|
|
15
|
-
getPathBearingToolPath,
|
|
16
|
-
isPathOutsideWorkingDirectory,
|
|
17
|
-
isPathWithinDirectory,
|
|
18
|
-
isPiInfrastructureRead,
|
|
19
|
-
isSafeSystemPath,
|
|
20
|
-
normalizePathForComparison,
|
|
21
|
-
PATH_BEARING_TOOLS,
|
|
22
|
-
READ_ONLY_PATH_BEARING_TOOLS,
|
|
23
|
-
SAFE_SYSTEM_PATHS,
|
|
24
|
-
} from "./path-utils";
|
/package/src/{external-directory-messages.ts → handlers/gates/external-directory-messages.ts}
RENAMED
|
File without changes
|