import * as fs from 'node:fs' import * as os from 'node:os' import { join } from 'node:path' import { beforeEach, afterEach, describe, expect, it, vi } from 'vitest' import type * as WindowsAcl from '../../shared/secure-path-windows-acl' import type * as Store from './zcode-plan-api-key-store' type NativeCase = { home: string fault: 'none' | 'before-restore' | 'after-restore' | 'competing' reject: boolean writes: number renames: number stages: number nativeOutcomes: boolean[] directories: boolean[] } const state = vi.hoisted((): NativeCase => ({ home: '', fault: 'none', reject: false, writes: 0, renames: 0, stages: 0, nativeOutcomes: [], directories: [] })) vi.mock('electron', () => ({ safeStorage: { isEncryptionAvailable: () => false } })) vi.mock('node:os', async (original) => ({ ...(await original()), homedir: () => state.home })) vi.mock('node:fs', async (original) => { const real = await original() return { ...real, writeFileSync: (...args: Parameters) => { if (++state.writes === 2 && state.fault === 'before-restore') { throw new Error('Synthetic restore staging failure') } return real.writeFileSync(...args) }, renameSync: (...args: Parameters) => { real.renameSync(...args) if (++state.renames === 2 && state.fault === 'after-restore') { throw new Error('Synthetic failure after restore publication') } } } }) vi.mock('../../shared/secure-path-windows-acl', async (original) => { const real = await original() const files = await vi.importActual('node:fs') return { ...real, restrictWindowsPathSync: (...args: Parameters) => { const outcome = real.restrictWindowsPathSync(...args) state.nativeOutcomes.push(outcome) if (state.fault === 'competing' && args[0].endsWith('.tmp') && ++state.stages === 2) { const competing = join(state.home, '.orca', 'competing-key') files.writeFileSync(competing, envelope('synthetic-competing')) expect(real.restrictWindowsPathSync(competing, false)).toBe(true) files.renameSync(competing, join(state.home, '.orca', 'zcode-plan-api-key.enc')) } return state.reject ? false : outcome }, bestEffortRestrictWindowsPath: ( path: string, directory: boolean, settled?: (restricted: boolean) => void ) => real.bestEffortRestrictWindowsPath(path, directory, (restricted) => { state.directories.push(restricted) settled?.(restricted) }) } }) const envelope = (key: string) => `orca-zcode-plan-api-key:v1:plaintext:${Buffer.from(key).toString('base64')}` let store: typeof Store let keyPath: string describe.skipIf(process.platform !== 'win32')('native Windows Coding Plan rollback', () => { beforeEach(async () => { state.home = fs.mkdtempSync(join(os.tmpdir(), 'orca-native-plan-')) state.fault = 'none' state.reject = false state.writes = 0 state.renames = 0 state.stages = 0 state.nativeOutcomes = [] state.directories = [] keyPath = join(state.home, '.orca', 'zcode-plan-api-key.enc') vi.spyOn(console, 'warn').mockImplementation(() => undefined) vi.resetModules() store = await import('./zcode-plan-api-key-store') store.saveZcodePlanApiKey('synthetic-previous') expect(fs.readFileSync(keyPath, 'utf8')).toBe(envelope('synthetic-previous')) expect(state.nativeOutcomes.length).toBeGreaterThan(0) expect(state.nativeOutcomes.every(Boolean)).toBe(true) await vi.waitFor( () => { expect(state.directories.length).toBeGreaterThan(0) expect(state.directories.every(Boolean)).toBe(true) }, { timeout: 15_000 } ) state.writes = 0 state.renames = 0 state.stages = 0 state.reject = true }) afterEach(() => { vi.restoreAllMocks() fs.rmSync(state.home, { recursive: true, force: true }) }) it.each(['before-restore', 'after-restore', 'competing'] as const)( 'preserves the file contract after %s with real file and directory ACLs', (fault) => { state.fault = fault expect(() => store.saveZcodePlanApiKey('synthetic-rejected')).toThrow( 'could not be stored securely' ) if (fault === 'before-restore') { expect(fs.existsSync(keyPath)).toBe(false) } else { expect(fs.readFileSync(keyPath, 'utf8')).toBe( envelope(fault === 'competing' ? 'synthetic-competing' : 'synthetic-previous') ) } expect(fs.readdirSync(join(state.home, '.orca')).some((name) => name.endsWith('.tmp'))).toBe( false ) expect(state.nativeOutcomes.every(Boolean)).toBe(true) }, 30_000 ) })