/** * Shared test utilities for git-handler tests. * * Why: oxlint max-lines (300) requires splitting large test suites. * This module exports the mock dispatcher factory and git helpers * so multiple test files can reuse them without duplication. */ import { vi } from 'vitest' import { execFileSync } from 'node:child_process' import type { RelayDispatcher } from './dispatcher' const TEST_GIT_USER_EMAIL = 'test@test.com' const TEST_GIT_USER_NAME = 'Test' // Why: declare an explicit type so the inferred return type of // createMockDispatcher doesn't transitively reference `@vitest/spy`'s // internal `Procedure` type (from `vi.fn(...)`). Without this annotation, // TS2883 fires under `pnpm run tc:node` because the generated .d.ts would // need to name a type that isn't portably resolvable from this module. export type MockDispatcher = { onRequest: ( method: string, handler: ( params: Record, context: { isStale: () => boolean; signal?: AbortSignal } ) => Promise ) => void onNotification: (method: string, handler: (params: Record) => void) => void notify: (method: string, params?: Record) => void _requestHandlers: Map< string, ( params: Record, context: { isStale: () => boolean; signal?: AbortSignal } ) => Promise > callRequest( method: string, params?: Record, context?: { isStale: () => boolean; signal?: AbortSignal } ): Promise } export function createMockDispatcher(): MockDispatcher { const requestHandlers = new Map< string, ( params: Record, context: { isStale: () => boolean; signal?: AbortSignal } ) => Promise >() return { onRequest: vi.fn( ( method: string, handler: ( params: Record, context: { isStale: () => boolean; signal?: AbortSignal } ) => Promise ) => { requestHandlers.set(method, handler) } ), onNotification: vi.fn(), notify: vi.fn(), _requestHandlers: requestHandlers, async callRequest( method: string, params: Record = {}, context: { isStale: () => boolean; signal?: AbortSignal } = { isStale: () => false } ) { const handler = requestHandlers.get(method) if (!handler) { throw new Error(`No handler for ${method}`) } return handler(params, context) } } } export function gitInit(dir: string): void { execFileSync('git', ['init'], { cwd: dir, stdio: 'pipe' }) execFileSync('git', ['config', 'user.email', TEST_GIT_USER_EMAIL], { cwd: dir, stdio: 'pipe' }) execFileSync('git', ['config', 'user.name', TEST_GIT_USER_NAME], { cwd: dir, stdio: 'pipe' }) } export function gitCommit(dir: string, message: string): void { execFileSync('git', ['add', '.'], { cwd: dir, stdio: 'pipe' }) // Why: `git submodule add` creates a checkout that does not inherit the // source repo's local identity config, and CI may have no global identity. execFileSync( 'git', [ '-c', `user.email=${TEST_GIT_USER_EMAIL}`, '-c', `user.name=${TEST_GIT_USER_NAME}`, 'commit', '-m', message, '--allow-empty' ], { cwd: dir, stdio: 'pipe' } ) } export type { RelayDispatcher }