/** * Shared real-instance test fixtures for PermissionSession and * PermissionResolver. * * Use these instead of hand-rolling per-file mock intersection types. * Build a real PermissionSession from small per-collaborator fakes so tests * assert against actual behavior rather than mock contracts. * * Note: tests that exercise `resolveAgentName` must mock `active-agent` in * their own file (the vi.hoisted / vi.mock pattern from permission-session.test.ts) * since that mock is module-scoped. */ import type { ExtensionContext } from "@earendil-works/pi-coding-agent"; import { vi } from "vitest"; import type { ResolvedAccessIntent } from "#src/access-intent/access-intent"; import type { AuthorizerSelectionLifecycle } from "#src/authority/authorizer-selection"; import type { ForwardingController } from "#src/authority/forwarding-manager"; import type { SessionConfigStore } from "#src/config-store"; import { DEFAULT_EXTENSION_CONFIG } from "#src/extension-config"; import type { ExtensionPaths } from "#src/extension-paths"; import { type PathFlavor, pathFlavorForPlatform } from "#src/path/path-flavor"; import type { ScopedPermissionManager } from "#src/permission-manager"; import { PermissionResolver } from "#src/permission-resolver"; import { PermissionSession } from "#src/permission-session"; import type { Ruleset } from "#src/rule"; import type { SessionLogger } from "#src/session-logger"; import { SessionRules } from "#src/session-rules"; import type { PermissionCheckResult, PermissionState } from "#src/types"; // ── Per-collaborator fake factories ──────────────────────────────────────── export function makePaths( overrides: Partial = {}, ): ExtensionPaths { return { agentDir: "/test/agent", sessionsDir: "/test/agent/sessions", subagentSessionsDir: "/test/agent/subagent-sessions", forwardingDir: "/test/agent/sessions/permission-forwarding", globalLogsDir: "/test/agent/logs", piInfrastructureDirs: ["/test/agent", "/test/agent/git"], ...overrides, }; } export function makeLogger(): SessionLogger { return { debug: vi.fn(), review: vi.fn(), warn: vi.fn(), }; } export function makeConfigStore( overrides: Partial = {}, ): SessionConfigStore { return { current: overrides.current ?? vi .fn<() => typeof DEFAULT_EXTENSION_CONFIG>() .mockReturnValue({ ...DEFAULT_EXTENSION_CONFIG }), refresh: overrides.refresh ?? vi.fn< (ctx: ExtensionContext | undefined, projectTrusted: boolean) => void >(), logResolvedPaths: overrides.logResolvedPaths ?? vi.fn<() => void>(), }; } export function makeAuthorizerSelection(): AuthorizerSelectionLifecycle { return { activate: vi.fn(), deactivate: vi.fn(), }; } export function makeForwarding(): ForwardingController { return { start: vi.fn(), stop: vi.fn(), }; } /** * Fake `ScopedPermissionManager` with vi.fn() stubs. * * Return type is intentionally unannotated so callers retain full `vi.fn()` * mock access (`mock.calls`, `toHaveBeenCalledWith`, `mockReturnValue`, etc.). */ export function makeFakePermissionManager() { return { configureForCwd: vi.fn<(cwd: string | undefined | null) => void>(), check: vi .fn< ( intent: ResolvedAccessIntent, sessionRules?: Ruleset, ) => PermissionCheckResult >() .mockReturnValue({ state: "allow", toolName: "read", source: "tool", origin: "builtin", }), getToolPermission: vi .fn<(toolName: string, agentName?: string) => PermissionState>() .mockReturnValue("allow"), getConfigIssues: vi.fn((): string[] => []), }; } // ── Real-instance factories ──────────────────────────────────────────────── /** * Build a real PermissionSession from per-collaborator fakes. * * Returns the session and every collaborator so callers can destructure only * what they need and assert against collaborator spies directly. * The `permissionManager` is a `makeFakePermissionManager()` result unless * the caller passes an explicit `ScopedPermissionManager`. */ export function makeRealSession(overrides?: { paths?: Partial; logger?: SessionLogger; forwarding?: ForwardingController; permissionManager?: ScopedPermissionManager; sessionRules?: SessionRules; configStore?: SessionConfigStore; authorizerSelection?: AuthorizerSelectionLifecycle; flavor?: PathFlavor; }): { session: PermissionSession; paths: ExtensionPaths; logger: SessionLogger; forwarding: ForwardingController; permissionManager: ReturnType; sessionRules: SessionRules; configStore: SessionConfigStore; authorizerSelection: AuthorizerSelectionLifecycle; } { const paths = makePaths(overrides?.paths); const logger = overrides?.logger ?? makeLogger(); const forwarding = overrides?.forwarding ?? makeForwarding(); const permissionManager = (overrides?.permissionManager as | ReturnType | undefined) ?? makeFakePermissionManager(); const sessionRules = overrides?.sessionRules ?? new SessionRules(); const configStore = overrides?.configStore ?? makeConfigStore(); const authorizerSelection = overrides?.authorizerSelection ?? makeAuthorizerSelection(); const flavor = overrides?.flavor ?? pathFlavorForPlatform(process.platform); const session = new PermissionSession( paths, forwarding, permissionManager, sessionRules, configStore, authorizerSelection, flavor, ); return { session, paths, logger, forwarding, permissionManager, sessionRules, configStore, authorizerSelection, }; } /** * Build a real PermissionResolver from a fake manager and a SessionRules * instance. * * When called with no arguments, creates a fresh fake manager and fresh * SessionRules. Pass shared instances to connect the resolver to the same * manager/rules used by a real session. */ export function makeRealResolver( manager?: ReturnType, sessionRules?: SessionRules, ): { resolver: PermissionResolver; manager: ReturnType; sessionRules: SessionRules; } { const resolvedManager = manager ?? makeFakePermissionManager(); const resolvedRules = sessionRules ?? new SessionRules(); const resolver = new PermissionResolver(resolvedManager, resolvedRules); return { resolver, manager: resolvedManager, sessionRules: resolvedRules }; }