@gotgenes/pi-permission-system 5.8.0 → 5.9.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 +13 -0
- package/package.json +1 -1
- package/src/forwarding-manager.ts +76 -0
- package/src/handlers/before-agent-start.ts +1 -1
- package/src/handlers/input.ts +1 -1
- package/src/handlers/lifecycle.ts +2 -2
- package/src/handlers/tool-call.ts +1 -1
- package/src/handlers/types.ts +2 -2
- package/src/index.ts +5 -6
- package/src/runtime.ts +0 -66
- package/tests/forwarding-manager.test.ts +211 -0
- package/tests/handlers/before-agent-start.test.ts +2 -3
- package/tests/handlers/input-events.test.ts +1 -2
- package/tests/handlers/input.test.ts +2 -3
- package/tests/handlers/lifecycle.test.ts +3 -4
- package/tests/handlers/tool-call-events.test.ts +1 -2
- package/tests/handlers/tool-call.test.ts +2 -3
- package/tests/runtime.test.ts +0 -15
package/CHANGELOG.md
CHANGED
|
@@ -5,6 +5,19 @@ 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.9.0](https://github.com/gotgenes/pi-permission-system/compare/v5.8.0...v5.9.0) (2026-05-08)
|
|
9
|
+
|
|
10
|
+
|
|
11
|
+
### Features
|
|
12
|
+
|
|
13
|
+
* add ForwardingManager class ([#128](https://github.com/gotgenes/pi-permission-system/issues/128)) ([7790380](https://github.com/gotgenes/pi-permission-system/commit/7790380eb0291f55724425a0bd6bd0b45cf15d91))
|
|
14
|
+
|
|
15
|
+
|
|
16
|
+
### Documentation
|
|
17
|
+
|
|
18
|
+
* plan ForwardingManager extraction ([#128](https://github.com/gotgenes/pi-permission-system/issues/128)) ([2f10450](https://github.com/gotgenes/pi-permission-system/commit/2f10450974adaedd7a43e8a7d986f8f61a0508db))
|
|
19
|
+
* **retro:** add retro notes for issue [#127](https://github.com/gotgenes/pi-permission-system/issues/127) ([2dde534](https://github.com/gotgenes/pi-permission-system/commit/2dde53416c535331972367ca2a44ba302b25d2a0))
|
|
20
|
+
|
|
8
21
|
## [5.8.0](https://github.com/gotgenes/pi-permission-system/compare/v5.7.0...v5.8.0) (2026-05-08)
|
|
9
22
|
|
|
10
23
|
|
package/package.json
CHANGED
|
@@ -0,0 +1,76 @@
|
|
|
1
|
+
import type { ExtensionContext } from "@mariozechner/pi-coding-agent";
|
|
2
|
+
|
|
3
|
+
import type { PermissionForwardingDeps } from "./forwarded-permissions/polling";
|
|
4
|
+
import { processForwardedPermissionRequests } from "./forwarded-permissions/polling";
|
|
5
|
+
import { PERMISSION_FORWARDING_POLL_INTERVAL_MS } from "./permission-forwarding";
|
|
6
|
+
import { isSubagentExecutionContext } from "./subagent-context";
|
|
7
|
+
|
|
8
|
+
/**
|
|
9
|
+
* Narrow interface for the forwarding lifecycle used by `HandlerDeps`.
|
|
10
|
+
* `ForwardingManager` satisfies it; tests can provide a plain object mock.
|
|
11
|
+
*/
|
|
12
|
+
export interface ForwardingController {
|
|
13
|
+
start(ctx: ExtensionContext): void;
|
|
14
|
+
stop(): void;
|
|
15
|
+
}
|
|
16
|
+
|
|
17
|
+
/**
|
|
18
|
+
* Encapsulates the forwarded-permission polling lifecycle.
|
|
19
|
+
*
|
|
20
|
+
* Owns the timer, current context, and processing-lock state that previously
|
|
21
|
+
* lived as 3 mutable fields on `ExtensionRuntime`. Call `start(ctx)` on each
|
|
22
|
+
* session event that may activate forwarding; call `stop()` on session
|
|
23
|
+
* shutdown.
|
|
24
|
+
*/
|
|
25
|
+
export class ForwardingManager {
|
|
26
|
+
private timer: NodeJS.Timeout | null = null;
|
|
27
|
+
private context: ExtensionContext | null = null;
|
|
28
|
+
private processing = false;
|
|
29
|
+
|
|
30
|
+
constructor(
|
|
31
|
+
private readonly subagentSessionsDir: string,
|
|
32
|
+
private readonly forwardingDeps: PermissionForwardingDeps,
|
|
33
|
+
) {}
|
|
34
|
+
|
|
35
|
+
/**
|
|
36
|
+
* Start polling if `ctx` has UI and is not a subagent execution context.
|
|
37
|
+
* No-op (timer stays running) if already polling — updates the stored
|
|
38
|
+
* context so the next tick uses the latest session.
|
|
39
|
+
* Stops any existing poll when the context does not qualify for forwarding.
|
|
40
|
+
*/
|
|
41
|
+
start(ctx: ExtensionContext): void {
|
|
42
|
+
if (
|
|
43
|
+
!ctx.hasUI ||
|
|
44
|
+
isSubagentExecutionContext(ctx, this.subagentSessionsDir)
|
|
45
|
+
) {
|
|
46
|
+
this.stop();
|
|
47
|
+
return;
|
|
48
|
+
}
|
|
49
|
+
this.context = ctx;
|
|
50
|
+
if (this.timer) {
|
|
51
|
+
return;
|
|
52
|
+
}
|
|
53
|
+
this.timer = setInterval(() => {
|
|
54
|
+
if (!this.context || this.processing) {
|
|
55
|
+
return;
|
|
56
|
+
}
|
|
57
|
+
this.processing = true;
|
|
58
|
+
void processForwardedPermissionRequests(
|
|
59
|
+
this.context,
|
|
60
|
+
this.forwardingDeps,
|
|
61
|
+
).finally(() => {
|
|
62
|
+
this.processing = false;
|
|
63
|
+
});
|
|
64
|
+
}, PERMISSION_FORWARDING_POLL_INTERVAL_MS);
|
|
65
|
+
}
|
|
66
|
+
|
|
67
|
+
/** Stop polling and clear all internal state. */
|
|
68
|
+
stop(): void {
|
|
69
|
+
if (this.timer) {
|
|
70
|
+
clearInterval(this.timer);
|
|
71
|
+
this.timer = null;
|
|
72
|
+
}
|
|
73
|
+
this.context = null;
|
|
74
|
+
this.processing = false;
|
|
75
|
+
}
|
|
76
|
+
}
|
|
@@ -43,7 +43,7 @@ export async function handleBeforeAgentStart(
|
|
|
43
43
|
): Promise<BeforeAgentStartEventResult> {
|
|
44
44
|
deps.session.runtimeContext = ctx;
|
|
45
45
|
deps.refreshExtensionConfig(ctx);
|
|
46
|
-
deps.
|
|
46
|
+
deps.forwarding.start(ctx);
|
|
47
47
|
|
|
48
48
|
const agentName = deps.resolveAgentName(ctx, event.systemPrompt);
|
|
49
49
|
const { permissionManager } = deps.session;
|
package/src/handlers/input.ts
CHANGED
|
@@ -41,7 +41,7 @@ export async function handleInput(
|
|
|
41
41
|
ctx: ExtensionContext,
|
|
42
42
|
): Promise<InputEventResult> {
|
|
43
43
|
deps.session.runtimeContext = ctx;
|
|
44
|
-
deps.
|
|
44
|
+
deps.forwarding.start(ctx);
|
|
45
45
|
|
|
46
46
|
const skillName = extractSkillNameFromInput(event.text);
|
|
47
47
|
if (!skillName) {
|
|
@@ -26,7 +26,7 @@ export async function handleSessionStart(
|
|
|
26
26
|
deps.session.lastActiveToolsCacheKey = null;
|
|
27
27
|
deps.session.lastPromptStateCacheKey = null;
|
|
28
28
|
deps.session.lastKnownActiveAgentName = getActiveAgentName(ctx);
|
|
29
|
-
deps.
|
|
29
|
+
deps.forwarding.start(ctx);
|
|
30
30
|
deps.logResolvedConfigPaths();
|
|
31
31
|
|
|
32
32
|
const agentName = deps.session.lastKnownActiveAgentName;
|
|
@@ -77,6 +77,6 @@ export async function handleSessionShutdown(deps: HandlerDeps): Promise<void> {
|
|
|
77
77
|
deps.session.lastActiveToolsCacheKey = null;
|
|
78
78
|
deps.session.lastPromptStateCacheKey = null;
|
|
79
79
|
deps.session.sessionRules.clear();
|
|
80
|
-
deps.
|
|
80
|
+
deps.forwarding.stop();
|
|
81
81
|
deps.stopPermissionRpcHandlers();
|
|
82
82
|
}
|
|
@@ -44,7 +44,7 @@ export async function handleToolCall(
|
|
|
44
44
|
ctx: ExtensionContext,
|
|
45
45
|
): Promise<{ block?: true; reason?: string }> {
|
|
46
46
|
deps.session.runtimeContext = ctx;
|
|
47
|
-
deps.
|
|
47
|
+
deps.forwarding.start(ctx);
|
|
48
48
|
|
|
49
49
|
const agentName = deps.resolveAgentName(ctx);
|
|
50
50
|
const toolName = getToolNameFromValue(event);
|
package/src/handlers/types.ts
CHANGED
|
@@ -1,5 +1,6 @@
|
|
|
1
1
|
import type { ExtensionContext } from "@mariozechner/pi-coding-agent";
|
|
2
2
|
|
|
3
|
+
import type { ForwardingController } from "../forwarding-manager";
|
|
3
4
|
import type { PermissionPromptDecision } from "../permission-dialog";
|
|
4
5
|
import type { PermissionEventBus } from "../permission-events";
|
|
5
6
|
import type { PermissionManager } from "../permission-manager";
|
|
@@ -79,8 +80,7 @@ export interface HandlerDeps {
|
|
|
79
80
|
createPermissionRequestId(prefix: string): string;
|
|
80
81
|
|
|
81
82
|
// ── Forwarding ─────────────────────────────────────────────────────────
|
|
82
|
-
|
|
83
|
-
stopForwardedPermissionPolling(): void;
|
|
83
|
+
readonly forwarding: ForwardingController;
|
|
84
84
|
/** Unsubscribe the permissions:rpc:check and permissions:rpc:prompt handlers. */
|
|
85
85
|
stopPermissionRpcHandlers(): void;
|
|
86
86
|
|
package/src/index.ts
CHANGED
|
@@ -2,6 +2,7 @@ import type { ExtensionAPI } from "@mariozechner/pi-coding-agent";
|
|
|
2
2
|
import { registerPermissionSystemCommand } from "./config-modal";
|
|
3
3
|
import { getGlobalConfigPath } from "./config-paths";
|
|
4
4
|
import type { PermissionForwardingDeps } from "./forwarded-permissions/polling";
|
|
5
|
+
import { ForwardingManager } from "./forwarding-manager";
|
|
5
6
|
import {
|
|
6
7
|
type HandlerDeps,
|
|
7
8
|
handleBeforeAgentStart,
|
|
@@ -22,8 +23,6 @@ import {
|
|
|
22
23
|
refreshExtensionConfig,
|
|
23
24
|
resolveAgentName,
|
|
24
25
|
saveExtensionConfig,
|
|
25
|
-
startForwardedPermissionPolling,
|
|
26
|
-
stopForwardedPermissionPolling,
|
|
27
26
|
} from "./runtime";
|
|
28
27
|
import { createSessionLogger } from "./session-logger";
|
|
29
28
|
import { isSubagentExecutionContext } from "./subagent-context";
|
|
@@ -102,10 +101,10 @@ export default function piPermissionSystemExtension(pi: ExtensionAPI): void {
|
|
|
102
101
|
}),
|
|
103
102
|
promptPermission: (ctx, details) => prompter.prompt(ctx, details),
|
|
104
103
|
createPermissionRequestId,
|
|
105
|
-
|
|
106
|
-
|
|
107
|
-
|
|
108
|
-
|
|
104
|
+
forwarding: new ForwardingManager(
|
|
105
|
+
runtime.subagentSessionsDir,
|
|
106
|
+
forwardingDeps,
|
|
107
|
+
),
|
|
109
108
|
stopPermissionRpcHandlers: () => {
|
|
110
109
|
rpcHandles.unsubCheck();
|
|
111
110
|
rpcHandles.unsubPrompt();
|
package/src/runtime.ts
CHANGED
|
@@ -38,17 +38,11 @@ import { computeExtensionPaths, type ExtensionPaths } from "./extension-paths";
|
|
|
38
38
|
|
|
39
39
|
export type { ExtensionPaths } from "./extension-paths";
|
|
40
40
|
|
|
41
|
-
import {
|
|
42
|
-
type PermissionForwardingDeps,
|
|
43
|
-
processForwardedPermissionRequests,
|
|
44
|
-
} from "./forwarded-permissions/polling";
|
|
45
41
|
import { createPermissionSystemLogger } from "./logging";
|
|
46
|
-
import { PERMISSION_FORWARDING_POLL_INTERVAL_MS } from "./permission-forwarding";
|
|
47
42
|
import { PermissionManager } from "./permission-manager";
|
|
48
43
|
import { SessionRules } from "./session-rules";
|
|
49
44
|
import type { SkillPromptEntry } from "./skill-prompt-sanitizer";
|
|
50
45
|
import { syncPermissionSystemStatus } from "./status";
|
|
51
|
-
import { isSubagentExecutionContext } from "./subagent-context";
|
|
52
46
|
|
|
53
47
|
/**
|
|
54
48
|
* Mutable session state — the subset of ExtensionRuntime that handlers
|
|
@@ -81,11 +75,6 @@ export interface ExtensionRuntime extends ExtensionPaths, SessionState {
|
|
|
81
75
|
config: PermissionSystemExtensionConfig;
|
|
82
76
|
lastConfigWarning: string | null;
|
|
83
77
|
|
|
84
|
-
// ── Forwarding polling state ───────────────────────────────────────────
|
|
85
|
-
permissionForwardingContext: ExtensionContext | null;
|
|
86
|
-
permissionForwardingTimer: NodeJS.Timeout | null;
|
|
87
|
-
isProcessingForwardedRequests: boolean;
|
|
88
|
-
|
|
89
78
|
// ── Logging (backed by logger created at construction) ─────────────────
|
|
90
79
|
writeDebugLog(event: string, details?: Record<string, unknown>): void;
|
|
91
80
|
writeReviewLog(event: string, details?: Record<string, unknown>): void;
|
|
@@ -277,58 +266,6 @@ export function logResolvedConfigPaths(runtime: ExtensionRuntime): void {
|
|
|
277
266
|
);
|
|
278
267
|
}
|
|
279
268
|
|
|
280
|
-
// ── Forwarding polling lifecycle ───────────────────────────────────────────
|
|
281
|
-
|
|
282
|
-
/** Stop the forwarded-permission polling interval and clear related state. */
|
|
283
|
-
export function stopForwardedPermissionPolling(
|
|
284
|
-
runtime: ExtensionRuntime,
|
|
285
|
-
): void {
|
|
286
|
-
if (runtime.permissionForwardingTimer) {
|
|
287
|
-
clearInterval(runtime.permissionForwardingTimer);
|
|
288
|
-
runtime.permissionForwardingTimer = null;
|
|
289
|
-
}
|
|
290
|
-
runtime.permissionForwardingContext = null;
|
|
291
|
-
runtime.isProcessingForwardedRequests = false;
|
|
292
|
-
}
|
|
293
|
-
|
|
294
|
-
/**
|
|
295
|
-
* Start the forwarded-permission polling interval.
|
|
296
|
-
* No-ops (and stops any existing poll) when the context has no UI or is a
|
|
297
|
-
* subagent execution context.
|
|
298
|
-
*/
|
|
299
|
-
export function startForwardedPermissionPolling(
|
|
300
|
-
runtime: ExtensionRuntime,
|
|
301
|
-
forwardingDeps: PermissionForwardingDeps,
|
|
302
|
-
ctx: ExtensionContext,
|
|
303
|
-
): void {
|
|
304
|
-
if (
|
|
305
|
-
!ctx.hasUI ||
|
|
306
|
-
isSubagentExecutionContext(ctx, runtime.subagentSessionsDir)
|
|
307
|
-
) {
|
|
308
|
-
stopForwardedPermissionPolling(runtime);
|
|
309
|
-
return;
|
|
310
|
-
}
|
|
311
|
-
runtime.permissionForwardingContext = ctx;
|
|
312
|
-
if (runtime.permissionForwardingTimer) {
|
|
313
|
-
return;
|
|
314
|
-
}
|
|
315
|
-
runtime.permissionForwardingTimer = setInterval(() => {
|
|
316
|
-
if (
|
|
317
|
-
!runtime.permissionForwardingContext ||
|
|
318
|
-
runtime.isProcessingForwardedRequests
|
|
319
|
-
) {
|
|
320
|
-
return;
|
|
321
|
-
}
|
|
322
|
-
runtime.isProcessingForwardedRequests = true;
|
|
323
|
-
void processForwardedPermissionRequests(
|
|
324
|
-
runtime.permissionForwardingContext,
|
|
325
|
-
forwardingDeps,
|
|
326
|
-
).finally(() => {
|
|
327
|
-
runtime.isProcessingForwardedRequests = false;
|
|
328
|
-
});
|
|
329
|
-
}, PERMISSION_FORWARDING_POLL_INTERVAL_MS);
|
|
330
|
-
}
|
|
331
|
-
|
|
332
269
|
// ── Factory ────────────────────────────────────────────────────────────────
|
|
333
270
|
|
|
334
271
|
/**
|
|
@@ -356,9 +293,6 @@ export function createExtensionRuntime(options?: {
|
|
|
356
293
|
lastPromptStateCacheKey: null,
|
|
357
294
|
lastConfigWarning: null,
|
|
358
295
|
sessionRules: new SessionRules(),
|
|
359
|
-
permissionForwardingContext: null,
|
|
360
|
-
permissionForwardingTimer: null,
|
|
361
|
-
isProcessingForwardedRequests: false,
|
|
362
296
|
// Logging methods are replaced below after the logger is constructed.
|
|
363
297
|
writeDebugLog: () => {},
|
|
364
298
|
writeReviewLog: () => {},
|
|
@@ -0,0 +1,211 @@
|
|
|
1
|
+
import { afterEach, beforeEach, describe, expect, it, vi } from "vitest";
|
|
2
|
+
|
|
3
|
+
import { ForwardingManager } from "../src/forwarding-manager";
|
|
4
|
+
|
|
5
|
+
// ── Mocks ─────────────────────────────────────────────────────────────────
|
|
6
|
+
|
|
7
|
+
const mockProcessForwardedPermissionRequests = vi.hoisted(() => vi.fn());
|
|
8
|
+
const mockIsSubagentExecutionContext = vi.hoisted(() => vi.fn());
|
|
9
|
+
|
|
10
|
+
vi.mock("../src/forwarded-permissions/polling", () => ({
|
|
11
|
+
processForwardedPermissionRequests: mockProcessForwardedPermissionRequests,
|
|
12
|
+
}));
|
|
13
|
+
|
|
14
|
+
vi.mock("../src/subagent-context", () => ({
|
|
15
|
+
isSubagentExecutionContext: mockIsSubagentExecutionContext,
|
|
16
|
+
}));
|
|
17
|
+
|
|
18
|
+
// ── Helpers ───────────────────────────────────────────────────────────────
|
|
19
|
+
|
|
20
|
+
function makeCtx(overrides: { hasUI?: boolean; sessionId?: string } = {}) {
|
|
21
|
+
return {
|
|
22
|
+
hasUI: overrides.hasUI ?? true,
|
|
23
|
+
sessionManager: {
|
|
24
|
+
getSessionId: vi.fn().mockReturnValue(overrides.sessionId ?? "sess-1"),
|
|
25
|
+
},
|
|
26
|
+
cwd: "/project",
|
|
27
|
+
} as unknown as import("@mariozechner/pi-coding-agent").ExtensionContext;
|
|
28
|
+
}
|
|
29
|
+
|
|
30
|
+
function makeForwardingDeps() {
|
|
31
|
+
return {
|
|
32
|
+
forwardingDir: "/agent/sessions/permission-forwarding",
|
|
33
|
+
subagentSessionsDir: "/agent/subagent-sessions",
|
|
34
|
+
logger: { writeReviewLog: vi.fn(), writeDebugLog: vi.fn() },
|
|
35
|
+
writeReviewLog: vi.fn(),
|
|
36
|
+
requestPermissionDecisionFromUi: vi.fn(),
|
|
37
|
+
shouldAutoApprove: vi.fn().mockReturnValue(false),
|
|
38
|
+
} as unknown as import("../src/forwarded-permissions/polling").PermissionForwardingDeps;
|
|
39
|
+
}
|
|
40
|
+
|
|
41
|
+
function makeManager() {
|
|
42
|
+
return new ForwardingManager(
|
|
43
|
+
"/agent/subagent-sessions",
|
|
44
|
+
makeForwardingDeps(),
|
|
45
|
+
);
|
|
46
|
+
}
|
|
47
|
+
|
|
48
|
+
// ── Tests ─────────────────────────────────────────────────────────────────
|
|
49
|
+
|
|
50
|
+
describe("ForwardingManager", () => {
|
|
51
|
+
beforeEach(() => {
|
|
52
|
+
vi.useFakeTimers();
|
|
53
|
+
mockIsSubagentExecutionContext.mockReset();
|
|
54
|
+
mockIsSubagentExecutionContext.mockReturnValue(false);
|
|
55
|
+
mockProcessForwardedPermissionRequests.mockReset();
|
|
56
|
+
mockProcessForwardedPermissionRequests.mockResolvedValue(undefined);
|
|
57
|
+
});
|
|
58
|
+
|
|
59
|
+
afterEach(() => {
|
|
60
|
+
vi.useRealTimers();
|
|
61
|
+
});
|
|
62
|
+
|
|
63
|
+
describe("stop()", () => {
|
|
64
|
+
it("is a no-op when not started", () => {
|
|
65
|
+
const manager = makeManager();
|
|
66
|
+
expect(() => manager.stop()).not.toThrow();
|
|
67
|
+
});
|
|
68
|
+
|
|
69
|
+
it("clears the timer and processing state after start()", async () => {
|
|
70
|
+
const manager = makeManager();
|
|
71
|
+
const ctx = makeCtx();
|
|
72
|
+
manager.start(ctx);
|
|
73
|
+
manager.stop();
|
|
74
|
+
|
|
75
|
+
// After stop, the timer fires no more callbacks.
|
|
76
|
+
mockProcessForwardedPermissionRequests.mockClear();
|
|
77
|
+
await vi.advanceTimersByTimeAsync(500);
|
|
78
|
+
expect(mockProcessForwardedPermissionRequests).not.toHaveBeenCalled();
|
|
79
|
+
});
|
|
80
|
+
});
|
|
81
|
+
|
|
82
|
+
describe("start()", () => {
|
|
83
|
+
it("does not start polling when hasUI is false", async () => {
|
|
84
|
+
const manager = makeManager();
|
|
85
|
+
const ctx = makeCtx({ hasUI: false });
|
|
86
|
+
manager.start(ctx);
|
|
87
|
+
|
|
88
|
+
await vi.advanceTimersByTimeAsync(500);
|
|
89
|
+
expect(mockProcessForwardedPermissionRequests).not.toHaveBeenCalled();
|
|
90
|
+
});
|
|
91
|
+
|
|
92
|
+
it("stops any existing poll and does not start a new one when hasUI is false", async () => {
|
|
93
|
+
const manager = makeManager();
|
|
94
|
+
const uiCtx = makeCtx({ hasUI: true });
|
|
95
|
+
const noUiCtx = makeCtx({ hasUI: false });
|
|
96
|
+
|
|
97
|
+
manager.start(uiCtx);
|
|
98
|
+
// Now stop the polling by calling start() with no-UI ctx.
|
|
99
|
+
manager.start(noUiCtx);
|
|
100
|
+
|
|
101
|
+
mockProcessForwardedPermissionRequests.mockClear();
|
|
102
|
+
await vi.advanceTimersByTimeAsync(500);
|
|
103
|
+
expect(mockProcessForwardedPermissionRequests).not.toHaveBeenCalled();
|
|
104
|
+
});
|
|
105
|
+
|
|
106
|
+
it("does not start polling when isSubagentExecutionContext returns true", async () => {
|
|
107
|
+
mockIsSubagentExecutionContext.mockReturnValue(true);
|
|
108
|
+
const manager = makeManager();
|
|
109
|
+
const ctx = makeCtx();
|
|
110
|
+
manager.start(ctx);
|
|
111
|
+
|
|
112
|
+
await vi.advanceTimersByTimeAsync(500);
|
|
113
|
+
expect(mockProcessForwardedPermissionRequests).not.toHaveBeenCalled();
|
|
114
|
+
});
|
|
115
|
+
|
|
116
|
+
it("stops any existing poll when called with a subagent context", async () => {
|
|
117
|
+
mockIsSubagentExecutionContext.mockReturnValueOnce(false);
|
|
118
|
+
const manager = makeManager();
|
|
119
|
+
const ctx1 = makeCtx();
|
|
120
|
+
manager.start(ctx1);
|
|
121
|
+
|
|
122
|
+
// Second call with a subagent context.
|
|
123
|
+
mockIsSubagentExecutionContext.mockReturnValue(true);
|
|
124
|
+
const ctx2 = makeCtx();
|
|
125
|
+
manager.start(ctx2);
|
|
126
|
+
|
|
127
|
+
mockProcessForwardedPermissionRequests.mockClear();
|
|
128
|
+
await vi.advanceTimersByTimeAsync(500);
|
|
129
|
+
expect(mockProcessForwardedPermissionRequests).not.toHaveBeenCalled();
|
|
130
|
+
});
|
|
131
|
+
|
|
132
|
+
it("starts polling and calls processForwardedPermissionRequests on tick", async () => {
|
|
133
|
+
const manager = makeManager();
|
|
134
|
+
const ctx = makeCtx();
|
|
135
|
+
manager.start(ctx);
|
|
136
|
+
|
|
137
|
+
await vi.advanceTimersByTimeAsync(250);
|
|
138
|
+
expect(mockProcessForwardedPermissionRequests).toHaveBeenCalledWith(
|
|
139
|
+
ctx,
|
|
140
|
+
expect.anything(),
|
|
141
|
+
);
|
|
142
|
+
});
|
|
143
|
+
|
|
144
|
+
it("is idempotent — calling start() twice does not create a second timer", async () => {
|
|
145
|
+
const manager = makeManager();
|
|
146
|
+
const ctx = makeCtx();
|
|
147
|
+
manager.start(ctx);
|
|
148
|
+
manager.start(ctx);
|
|
149
|
+
|
|
150
|
+
await vi.advanceTimersByTimeAsync(250);
|
|
151
|
+
// Only one tick should fire per interval, not two.
|
|
152
|
+
expect(mockProcessForwardedPermissionRequests).toHaveBeenCalledTimes(1);
|
|
153
|
+
});
|
|
154
|
+
|
|
155
|
+
it("updates the context when called again while already running", async () => {
|
|
156
|
+
const manager = makeManager();
|
|
157
|
+
const ctx1 = makeCtx({ sessionId: "sess-1" });
|
|
158
|
+
const ctx2 = makeCtx({ sessionId: "sess-2" });
|
|
159
|
+
manager.start(ctx1);
|
|
160
|
+
manager.start(ctx2);
|
|
161
|
+
|
|
162
|
+
await vi.advanceTimersByTimeAsync(250);
|
|
163
|
+
// The process call should use the newer context.
|
|
164
|
+
expect(mockProcessForwardedPermissionRequests).toHaveBeenCalledWith(
|
|
165
|
+
ctx2,
|
|
166
|
+
expect.anything(),
|
|
167
|
+
);
|
|
168
|
+
});
|
|
169
|
+
|
|
170
|
+
it("skips a tick while processing is in progress", async () => {
|
|
171
|
+
// Make processForwardedPermissionRequests hang so processing=true persists.
|
|
172
|
+
let resolveProcess: () => void;
|
|
173
|
+
mockProcessForwardedPermissionRequests.mockReturnValue(
|
|
174
|
+
new Promise<void>((resolve) => {
|
|
175
|
+
resolveProcess = resolve;
|
|
176
|
+
}),
|
|
177
|
+
);
|
|
178
|
+
|
|
179
|
+
const manager = makeManager();
|
|
180
|
+
const ctx = makeCtx();
|
|
181
|
+
manager.start(ctx);
|
|
182
|
+
|
|
183
|
+
// First tick starts processing.
|
|
184
|
+
await vi.advanceTimersByTimeAsync(250);
|
|
185
|
+
expect(mockProcessForwardedPermissionRequests).toHaveBeenCalledTimes(1);
|
|
186
|
+
|
|
187
|
+
// Second tick is skipped because processing flag is still true.
|
|
188
|
+
await vi.advanceTimersByTimeAsync(250);
|
|
189
|
+
expect(mockProcessForwardedPermissionRequests).toHaveBeenCalledTimes(1);
|
|
190
|
+
|
|
191
|
+
// Resolve and a third tick should fire.
|
|
192
|
+
resolveProcess!();
|
|
193
|
+
await vi.advanceTimersByTimeAsync(250);
|
|
194
|
+
expect(mockProcessForwardedPermissionRequests).toHaveBeenCalledTimes(2);
|
|
195
|
+
});
|
|
196
|
+
|
|
197
|
+
it("passes subagentSessionsDir from the constructor to isSubagentExecutionContext", () => {
|
|
198
|
+
const manager = new ForwardingManager(
|
|
199
|
+
"/custom/subagent-dir",
|
|
200
|
+
makeForwardingDeps(),
|
|
201
|
+
);
|
|
202
|
+
const ctx = makeCtx();
|
|
203
|
+
manager.start(ctx);
|
|
204
|
+
|
|
205
|
+
expect(mockIsSubagentExecutionContext).toHaveBeenCalledWith(
|
|
206
|
+
ctx,
|
|
207
|
+
"/custom/subagent-dir",
|
|
208
|
+
);
|
|
209
|
+
});
|
|
210
|
+
});
|
|
211
|
+
});
|
|
@@ -90,8 +90,7 @@ function makeDeps(overrides: Partial<HandlerDeps> = {}): HandlerDeps {
|
|
|
90
90
|
.mockResolvedValue({ approved: true, state: "approved" }),
|
|
91
91
|
createPermissionRequestId: vi.fn().mockReturnValue("test-id"),
|
|
92
92
|
events: { emit: vi.fn(), on: vi.fn().mockReturnValue(() => undefined) },
|
|
93
|
-
|
|
94
|
-
stopForwardedPermissionPolling: vi.fn(),
|
|
93
|
+
forwarding: { start: vi.fn(), stop: vi.fn() },
|
|
95
94
|
stopPermissionRpcHandlers: vi.fn(),
|
|
96
95
|
getAllTools: vi.fn().mockReturnValue([]),
|
|
97
96
|
setActiveTools: vi.fn(),
|
|
@@ -144,7 +143,7 @@ describe("handleBeforeAgentStart", () => {
|
|
|
144
143
|
const ctx = makeCtx();
|
|
145
144
|
const deps = makeDeps();
|
|
146
145
|
await handleBeforeAgentStart(deps, makeEvent(), ctx);
|
|
147
|
-
expect(deps.
|
|
146
|
+
expect(deps.forwarding.start).toHaveBeenCalledWith(ctx);
|
|
148
147
|
});
|
|
149
148
|
|
|
150
149
|
it("resolves agent name using systemPrompt", async () => {
|
|
@@ -81,8 +81,7 @@ function makeDeps(
|
|
|
81
81
|
.fn()
|
|
82
82
|
.mockResolvedValue({ approved: true, state: "approved" }),
|
|
83
83
|
createPermissionRequestId: vi.fn().mockReturnValue("req-id"),
|
|
84
|
-
|
|
85
|
-
stopForwardedPermissionPolling: vi.fn(),
|
|
84
|
+
forwarding: { start: vi.fn(), stop: vi.fn() },
|
|
86
85
|
stopPermissionRpcHandlers: vi.fn(),
|
|
87
86
|
getAllTools: vi.fn().mockReturnValue([]),
|
|
88
87
|
setActiveTools: vi.fn(),
|
|
@@ -69,8 +69,7 @@ function makeDeps(overrides: Partial<HandlerDeps> = {}): HandlerDeps {
|
|
|
69
69
|
.mockResolvedValue({ approved: true, state: "approved" }),
|
|
70
70
|
createPermissionRequestId: vi.fn().mockReturnValue("req-id"),
|
|
71
71
|
events: { emit: vi.fn(), on: vi.fn().mockReturnValue(() => undefined) },
|
|
72
|
-
|
|
73
|
-
stopForwardedPermissionPolling: vi.fn(),
|
|
72
|
+
forwarding: { start: vi.fn(), stop: vi.fn() },
|
|
74
73
|
stopPermissionRpcHandlers: vi.fn(),
|
|
75
74
|
getAllTools: vi.fn().mockReturnValue([]),
|
|
76
75
|
setActiveTools: vi.fn(),
|
|
@@ -126,7 +125,7 @@ describe("handleInput", () => {
|
|
|
126
125
|
const ctx = makeCtx();
|
|
127
126
|
const deps = makeDeps();
|
|
128
127
|
await handleInput(deps, makeInputEvent("hello"), ctx);
|
|
129
|
-
expect(deps.
|
|
128
|
+
expect(deps.forwarding.start).toHaveBeenCalledWith(ctx);
|
|
130
129
|
});
|
|
131
130
|
|
|
132
131
|
it("returns continue for non-skill input", async () => {
|
|
@@ -98,8 +98,7 @@ function makeDeps(overrides: Partial<HandlerDeps> = {}): HandlerDeps {
|
|
|
98
98
|
.mockResolvedValue({ approved: true, state: "approved" }),
|
|
99
99
|
createPermissionRequestId: vi.fn().mockReturnValue("test-id"),
|
|
100
100
|
events: { emit: vi.fn(), on: vi.fn().mockReturnValue(() => undefined) },
|
|
101
|
-
|
|
102
|
-
stopForwardedPermissionPolling: vi.fn(),
|
|
101
|
+
forwarding: { start: vi.fn(), stop: vi.fn() },
|
|
103
102
|
stopPermissionRpcHandlers: vi.fn(),
|
|
104
103
|
getAllTools: vi.fn().mockReturnValue([]),
|
|
105
104
|
setActiveTools: vi.fn(),
|
|
@@ -171,7 +170,7 @@ describe("handleSessionStart", () => {
|
|
|
171
170
|
const ctx = makeCtx();
|
|
172
171
|
const deps = makeDeps();
|
|
173
172
|
await handleSessionStart(deps, { reason: "startup" }, ctx);
|
|
174
|
-
expect(deps.
|
|
173
|
+
expect(deps.forwarding.start).toHaveBeenCalledWith(ctx);
|
|
175
174
|
});
|
|
176
175
|
|
|
177
176
|
it("logs resolved config paths", async () => {
|
|
@@ -319,7 +318,7 @@ describe("handleSessionShutdown", () => {
|
|
|
319
318
|
it("stops forwarded permission polling", async () => {
|
|
320
319
|
const deps = makeDeps();
|
|
321
320
|
await handleSessionShutdown(deps);
|
|
322
|
-
expect(deps.
|
|
321
|
+
expect(deps.forwarding.stop).toHaveBeenCalledOnce();
|
|
323
322
|
});
|
|
324
323
|
|
|
325
324
|
it("calls stopPermissionRpcHandlers on shutdown", async () => {
|
|
@@ -102,8 +102,7 @@ function makeDeps(overrides: Partial<HandlerDeps> = {}): HandlerDeps {
|
|
|
102
102
|
.fn()
|
|
103
103
|
.mockResolvedValue({ approved: true, state: "approved" }),
|
|
104
104
|
createPermissionRequestId: vi.fn().mockReturnValue("req-id"),
|
|
105
|
-
|
|
106
|
-
stopForwardedPermissionPolling: vi.fn(),
|
|
105
|
+
forwarding: { start: vi.fn(), stop: vi.fn() },
|
|
107
106
|
stopPermissionRpcHandlers: vi.fn(),
|
|
108
107
|
getAllTools: vi.fn().mockReturnValue([{ name: "read" }, { name: "bash" }]),
|
|
109
108
|
setActiveTools: vi.fn(),
|
|
@@ -90,8 +90,7 @@ function makeDeps(overrides: Partial<HandlerDeps> = {}): HandlerDeps {
|
|
|
90
90
|
.mockResolvedValue({ approved: true, state: "approved" }),
|
|
91
91
|
createPermissionRequestId: vi.fn().mockReturnValue("req-id"),
|
|
92
92
|
events: { emit: vi.fn(), on: vi.fn().mockReturnValue(() => undefined) },
|
|
93
|
-
|
|
94
|
-
stopForwardedPermissionPolling: vi.fn(),
|
|
93
|
+
forwarding: { start: vi.fn(), stop: vi.fn() },
|
|
95
94
|
stopPermissionRpcHandlers: vi.fn(),
|
|
96
95
|
getAllTools: vi.fn().mockReturnValue([{ name: "read" }, { name: "bash" }]),
|
|
97
96
|
setActiveTools: vi.fn(),
|
|
@@ -139,7 +138,7 @@ describe("handleToolCall", () => {
|
|
|
139
138
|
const ctx = makeCtx();
|
|
140
139
|
const deps = makeDeps();
|
|
141
140
|
await handleToolCall(deps, makeToolCallEvent("read"), ctx);
|
|
142
|
-
expect(deps.
|
|
141
|
+
expect(deps.forwarding.start).toHaveBeenCalledWith(ctx);
|
|
143
142
|
});
|
|
144
143
|
|
|
145
144
|
it("blocks when tool name cannot be resolved", async () => {
|
package/tests/runtime.test.ts
CHANGED
|
@@ -212,21 +212,6 @@ describe("createExtensionRuntime", () => {
|
|
|
212
212
|
expect(runtime.lastConfigWarning).toBeNull();
|
|
213
213
|
});
|
|
214
214
|
|
|
215
|
-
it("initializes permissionForwardingContext to null", () => {
|
|
216
|
-
const runtime = createExtensionRuntime({ agentDir: "/test/agent" });
|
|
217
|
-
expect(runtime.permissionForwardingContext).toBeNull();
|
|
218
|
-
});
|
|
219
|
-
|
|
220
|
-
it("initializes permissionForwardingTimer to null", () => {
|
|
221
|
-
const runtime = createExtensionRuntime({ agentDir: "/test/agent" });
|
|
222
|
-
expect(runtime.permissionForwardingTimer).toBeNull();
|
|
223
|
-
});
|
|
224
|
-
|
|
225
|
-
it("initializes isProcessingForwardedRequests to false", () => {
|
|
226
|
-
const runtime = createExtensionRuntime({ agentDir: "/test/agent" });
|
|
227
|
-
expect(runtime.isProcessingForwardedRequests).toBe(false);
|
|
228
|
-
});
|
|
229
|
-
|
|
230
215
|
it("creates a sessionRules instance", () => {
|
|
231
216
|
const runtime = createExtensionRuntime({ agentDir: "/test/agent" });
|
|
232
217
|
expect(runtime.sessionRules).toBeDefined();
|