mirror of
https://github.com/stablyai/orca.git
synced 2026-10-30 16:47:34 +00:00
test(zcode): verify rollback with real Windows file permissions
This commit is contained in:
@@ -0,0 +1,34 @@
|
||||
name: Coding Plan credential contract
|
||||
on:
|
||||
pull_request:
|
||||
paths:
|
||||
- 'src/main/zcode/**'
|
||||
- 'src/shared/secure-file*'
|
||||
- 'src/shared/secure-path-windows-acl*'
|
||||
- 'src/shared/windows-security-descriptor*'
|
||||
- '.github/workflows/zcode-credential-contract.yml'
|
||||
workflow_dispatch:
|
||||
permissions:
|
||||
contents: read
|
||||
concurrency:
|
||||
group: coding-plan-contract-${{ github.event.pull_request.number || github.ref }}
|
||||
cancel-in-progress: true
|
||||
jobs:
|
||||
credential-files:
|
||||
strategy:
|
||||
fail-fast: false
|
||||
matrix:
|
||||
os: [ubuntu-22.04, macos-15, windows-2022]
|
||||
runs-on: ${{ matrix.os }}
|
||||
timeout-minutes: 20
|
||||
env:
|
||||
ORCA_BACKGROUND_LAUNCH: '1'
|
||||
steps:
|
||||
- uses: actions/checkout@v6
|
||||
with:
|
||||
persist-credentials: false
|
||||
- uses: ./.github/actions/install-node-dependencies
|
||||
with:
|
||||
native-runtime: node
|
||||
- name: Verify rollback and native Windows permissions
|
||||
run: pnpm test src/main/zcode/zcode-plan-api-key-store.test.ts src/main/zcode/zcode-plan-api-key-store-plaintext-rollback.test.ts src/main/zcode/zcode-plan-api-key-store-publication-race.test.ts src/main/zcode/zcode-plan-api-key-store-native-windows.test.ts src/shared/secure-file.test.ts
|
||||
@@ -0,0 +1,141 @@
|
||||
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<typeof os>()),
|
||||
homedir: () => state.home
|
||||
}))
|
||||
vi.mock('node:fs', async (original) => {
|
||||
const real = await original<typeof fs>()
|
||||
return {
|
||||
...real,
|
||||
writeFileSync: (...args: Parameters<typeof fs.writeFileSync>) => {
|
||||
if (++state.writes === 2 && state.fault === 'before-restore') {
|
||||
throw new Error('Synthetic restore staging failure')
|
||||
}
|
||||
return real.writeFileSync(...args)
|
||||
},
|
||||
renameSync: (...args: Parameters<typeof fs.renameSync>) => {
|
||||
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<typeof WindowsAcl>()
|
||||
const files = await vi.importActual<typeof fs>('node:fs')
|
||||
return {
|
||||
...real,
|
||||
restrictWindowsPathSync: (...args: Parameters<typeof WindowsAcl.restrictWindowsPathSync>) => {
|
||||
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
|
||||
)
|
||||
})
|
||||
Reference in New Issue
Block a user