Files

170 lines
6.0 KiB
TypeScript

import type { AutoReviewConfigFileSystem } from '../src/config-store.js'
import { describe, expect, it, vi } from 'vitest'
import { AutoReviewConfigStore } from '../src/config-store.js'
function createFileSystem(initial: Record<string, string> = {}) {
const files = new Map(Object.entries(initial))
const readFile = vi.fn((path: string) => files.get(path))
const writeFile = vi.fn((path: string, source: string) => {
files.set(path, source)
})
const rename = vi.fn((sourcePath: string, destinationPath: string) => {
const source = files.get(sourcePath)
if (source === undefined) {
throw new Error(`missing source ${sourcePath}`)
}
files.set(destinationPath, source)
files.delete(sourcePath)
})
const mkdir = vi.fn((_path: string) => {})
const unlink = vi.fn((path: string) => {
if (!files.delete(path)) {
const error = new Error(`missing file ${path}`)
Object.assign(error, { code: 'ENOENT' })
throw error
}
})
const fileSystem: AutoReviewConfigFileSystem = {
readFile,
writeFile,
rename,
mkdir,
unlink,
}
return { files, fileSystem, mkdir, rename, writeFile }
}
const globalPath = '/agent/extensions/pi-permission-auto-review/config.json'
const projectPath = '/project/.pi/extensions/pi-permission-auto-review/config.json'
describe('autoReviewConfigStore', () => {
it('atomically saves a scoped override and returns the merged config', () => {
const { files, fileSystem, mkdir, rename, writeFile } = createFileSystem({
[globalPath]: JSON.stringify({ provider: 'global-provider', timeoutMs: 10_000 }),
})
const store = new AutoReviewConfigStore({ agentDir: '/agent', fileSystem })
const snapshot = store.readScope('/project', 'project')
const result = store.save(snapshot, {
model: 'project-model',
timeoutMs: 20_000,
})
expect(result.ok).toBe(true)
if (!result.ok) {
return
}
expect(result.loadResult.config).toMatchObject({
provider: 'global-provider',
model: 'project-model',
timeoutMs: 20_000,
})
expect(mkdir).toHaveBeenCalledWith('/project/.pi/extensions/pi-permission-auto-review')
expect(writeFile).toHaveBeenCalledWith(`${projectPath}.tmp`, expect.any(String))
expect(rename).toHaveBeenCalledWith(`${projectPath}.tmp`, projectPath)
const stored: unknown = JSON.parse(files.get(projectPath) ?? '')
expect(stored).toEqual({
$schema:
'https://raw.githubusercontent.com/mzwing/pi-packages/main/packages/pi-permission-auto-review/schemas/config.schema.json',
model: 'project-model',
timeoutMs: 20_000,
})
expect(files.get(projectPath)).toMatch(/\n$/)
})
it('removes a project override by saving a draft without the field', () => {
const { files, fileSystem } = createFileSystem({
[globalPath]: JSON.stringify({ model: 'global-model' }),
[projectPath]: JSON.stringify({ model: 'project-model', reasoning: 'high' }),
})
const store = new AutoReviewConfigStore({ agentDir: '/agent', fileSystem })
const snapshot = store.readScope('/project', 'project')
const result = store.save(snapshot, { reasoning: 'high' })
expect(result.ok).toBe(true)
if (!result.ok) {
return
}
expect(result.loadResult.config).toMatchObject({
model: 'global-model',
reasoning: 'high',
})
const stored: unknown = JSON.parse(files.get(projectPath) ?? '')
expect(stored).not.toHaveProperty('model')
})
it('rejects a merged config that violates the cross-field policy invariant', () => {
const { fileSystem, writeFile } = createFileSystem()
const store = new AutoReviewConfigStore({ agentDir: '/agent', fileSystem })
const snapshot = store.readScope('/project', 'global')
const result = store.save(snapshot, { includeBaselinePolicy: false })
expect(result.ok).toBe(false)
if (!result.ok) {
expect(result.message).toContain('additionalPolicy is required')
}
expect(writeFile).not.toHaveBeenCalled()
})
it('detects an external edit before writing', () => {
const { files, fileSystem, writeFile } = createFileSystem({
[globalPath]: JSON.stringify({ reasoning: 'low' }),
})
const store = new AutoReviewConfigStore({ agentDir: '/agent', fileSystem })
const snapshot = store.readScope('/project', 'global')
files.set(globalPath, JSON.stringify({ reasoning: 'high' }))
const result = store.save(snapshot, { reasoning: 'medium' })
expect(result.ok).toBe(false)
if (!result.ok) {
expect(result.message).toContain('changed while it was being edited')
}
expect(writeFile).not.toHaveBeenCalled()
})
it('blocks ordinary saves for an invalid file but allows reset to repair it', () => {
const { files, fileSystem } = createFileSystem({
[projectPath]: JSON.stringify({ apiKey: 'not-allowed' }),
})
const store = new AutoReviewConfigStore({ agentDir: '/agent', fileSystem })
const snapshot = store.readScope('/project', 'project')
expect(snapshot.valid).toBe(false)
const saved = store.save(snapshot, {})
expect(saved.ok).toBe(false)
if (!saved.ok) {
expect(saved.message).toContain('Cannot save invalid config')
}
const reset = store.reset(snapshot)
expect(reset.ok).toBe(true)
expect(files.has(projectPath)).toBe(false)
if (reset.ok) {
expect(reset.loadResult.config).toMatchObject({
provider: 'openai-codex',
model: 'codex-auto-review',
})
}
})
it('cleans up the temporary file when rename fails', () => {
const { files, fileSystem, rename } = createFileSystem()
rename.mockImplementation(() => {
throw new Error('rename failed')
})
const store = new AutoReviewConfigStore({ agentDir: '/agent', fileSystem })
const snapshot = store.readScope('/project', 'global')
const result = store.save(snapshot, { reasoning: 'high' })
expect(result.ok).toBe(false)
if (!result.ok) {
expect(result.message).toContain('rename failed')
}
expect(files.has(`${globalPath}.tmp`)).toBe(false)
})
})