/** * Shared gate-level test fixtures for gate descriptor and runner tests. */ import { vi } from "vitest"; import type { AskEscalator } from "#src/authority/authorizer-selection"; import type { ShellToolsConfig } from "#src/config-schema"; import type { DecisionReporter } from "#src/decision-reporter"; import type { GateDescriptor } from "#src/handlers/gates/descriptor"; import { GateRunner } from "#src/handlers/gates/runner"; import type { SkillInputGateInputs } from "#src/handlers/gates/skill-input-gate-pipeline"; import type { ToolCallGateInputs } from "#src/handlers/gates/tool-call-gate-pipeline"; import type { ToolCallContext } from "#src/handlers/gates/types"; import { pathFlavorForPlatform } from "#src/path/path-flavor"; import { PathNormalizer } from "#src/path-normalizer"; import type { ScopedPermissionResolver } from "#src/permission-resolver"; import type { SessionApprovalRecorder } from "#src/session-approval-recorder"; import type { SkillPromptEntry } from "#src/skill-prompt-sanitizer"; import type { ToolPreviewFormatterOptions } from "#src/tool-preview-formatter"; import type { PermissionCheckResult } from "#src/types"; import { DECIDED_BY_HUMAN } from "#test/helpers/decision-fixtures"; import { makeCheckResult } from "#test/helpers/handler-fixtures"; import { makeGatePromptDetails, makePromptPayload, } from "#test/helpers/prompt-details-fixtures"; /** * Permission resolver mock with an optional default check result. * * Returns a plain object whose `resolve` is a `vi.fn` so callers retain full * mock access (`mockReturnValue`, `mockImplementation`, `mock.calls`). */ export function makeResolver(defaultCheck?: PermissionCheckResult) { const resolve = vi.fn(); if (defaultCheck) { resolve.mockReturnValue(defaultCheck); } return { resolve }; } /** * Gate descriptor factory with runner-test defaults. * * Carries the payload every render over this descriptor reads, so a test that * verifies a block path gets rendered denial text without overriding it. */ export function makeDescriptor( overrides: Partial = {}, ): GateDescriptor { return { surface: "read", input: {}, payload: makePromptPayload({ request: { ...makePromptPayload().request, matchedPattern: "*", }, }), promptDetails: makeGatePromptDetails({ toolCallId: "tc-1", toolName: "read", }), logContext: { source: "tool_call", toolCallId: "tc-1", toolName: "read", }, decision: { surface: "read", value: "read", }, ...overrides, }; } /** * Reporter mock with independently inspectable vi.fn() stubs. */ export function makeReporter( overrides: Partial = {}, ): DecisionReporter { return { writeReviewLog: vi.fn(), emitDecision: vi.fn(), ...overrides, }; } /** * Gate runner factory for `GateRunner` unit tests. * * Builds one `GateRunner` from four role mocks and returns `{ runner, deps }` * so tests can both invoke `runner.run(...)` and assert on the individual * mock call records (`deps.reporter.*`, `deps.resolve`, etc.). */ export function makeGateRunner( overrides: { resolveResult?: PermissionCheckResult; resolve?: ScopedPermissionResolver["resolve"]; recordSessionApproval?: SessionApprovalRecorder["recordSessionApproval"]; escalate?: AskEscalator["escalate"]; reporter?: Partial; /** Standing yolo setting for the runner's residual-ask grant. */ yolo?: boolean; /** Live yolo reader, for tests that toggle the setting between runs. */ isYoloEnabled?: () => boolean; } = {}, ) { const reporter = makeReporter(overrides.reporter); const resolve = overrides.resolve ?? vi .fn() .mockReturnValue( overrides.resolveResult ?? makeCheckResult({ matchedPattern: "*" }), ); const recordSessionApproval = overrides.recordSessionApproval ?? (vi.fn() as SessionApprovalRecorder["recordSessionApproval"]); const escalate = overrides.escalate ?? vi.fn().mockResolvedValue({ approved: true, state: "approved", decidedBy: DECIDED_BY_HUMAN, }); const isYoloEnabled = overrides.isYoloEnabled ?? ((): boolean => overrides.yolo ?? false); const runner = new GateRunner( { resolve }, { recordSessionApproval }, { escalate }, reporter, isYoloEnabled, ); return { runner, deps: { resolve, recordSessionApproval, escalate, reporter, }, }; } /** * Tool-call context factory with bash defaults. * * path.test.ts uses different defaults (toolName "read", path input) and * keeps a local wrapper; bash-path.test.ts uses this factory directly. */ export function makeTcc( overrides: Partial = {}, ): ToolCallContext { return { toolName: "bash", agentName: null, input: { command: "cat .env" }, toolCallId: "tc-1", cwd: "/test/project", ...overrides, }; } /** * Resolver whose `resolve` dispatches on `input.path`, falling back to a * default result for any path not in the map. * * Use when a test needs different results for different path tokens without * writing a full `mockImplementation` block. * * Return type is intentionally unannotated so callers retain full `vi.fn()` * mock access (`mock.calls`, `toHaveBeenCalledWith`, etc.). */ export function makePathDispatchResolver( byPath: Record, defaultResult: PermissionCheckResult, ) { const resolve = vi.fn(); resolve.mockImplementation((intent) => { if (intent.kind === "tool") { const path = (intent.input as Record).path; if (typeof path === "string" && path in byPath) { return byPath[path]; } return defaultResult; } const values = intent.path.matchValues(); for (const value of values) { if (value in byPath) return byPath[value]; } return defaultResult; }); return { resolve }; } /** * Path-surface check result factory. * * Shared between bash-path.test.ts and path.test.ts; both use * toolName "path", source "special", origin "global" as defaults. */ export function makeGateCheckResult( overrides: Partial = {}, ): PermissionCheckResult { return { toolName: "path", state: "allow", source: "special", origin: "global", ...overrides, }; } /** * Mock of `ToolCallGateInputs` for `ToolCallGatePipeline` unit tests. * * Each method is a `vi.fn()` stub so callers retain full mock access * (`mock.calls`, `mockReturnValue`, etc.) on the returned object. * Pass `overrides` to replace individual stubs without rebuilding the whole * mock from scratch. */ export function makeGateInputs( overrides: { getActiveSkillEntries?: () => SkillPromptEntry[]; getInfrastructureReadDirs?: () => string[]; getToolPreviewLimits?: () => ToolPreviewFormatterOptions; getPathNormalizer?: () => PathNormalizer; getShellToolAliases?: () => ShellToolsConfig | undefined; } = {}, ): ToolCallGateInputs { return { getActiveSkillEntries: overrides.getActiveSkillEntries ?? vi.fn<() => SkillPromptEntry[]>(() => []), getInfrastructureReadDirs: overrides.getInfrastructureReadDirs ?? vi.fn<() => string[]>(() => []), getToolPreviewLimits: overrides.getToolPreviewLimits ?? vi.fn<() => ToolPreviewFormatterOptions>(() => ({ toolInputPreviewMaxLength: 500, toolTextSummaryMaxLength: 100, })), getPathNormalizer: overrides.getPathNormalizer ?? vi.fn<() => PathNormalizer>( () => new PathNormalizer( pathFlavorForPlatform(process.platform), "/test/cwd", ), ), getShellToolAliases: overrides.getShellToolAliases ?? vi.fn<() => ShellToolsConfig | undefined>(() => undefined), }; } /** * Mock of `SkillInputGateInputs` for `SkillInputGatePipeline` unit tests. * * Returns a plain object with a `checkPermission` `vi.fn()` stub so callers * retain full mock access (`mockReturnValue`, `mock.calls`, etc.). */ export function makeSkillInputInputs( overrides: { checkPermission?: SkillInputGateInputs["checkPermission"] } = {}, ): SkillInputGateInputs { return { checkPermission: overrides.checkPermission ?? vi .fn() .mockReturnValue(makeCheckResult()), }; } /** * Mock `GateNotifier` for `SkillInputGatePipeline` unit tests. * * Return type is intentionally unannotated so callers retain full `vi.fn()` * mock access (`mock.calls`, `toHaveBeenCalledWith`, etc.) — annotating with * `GateNotifier` would erase `Mock<...>` methods from the inferred type. */ export function makeNotifier() { return { warn: vi.fn<(message: string) => void>(), }; }