import { mkdtemp, readFile, readdir, rm, writeFile } from 'node:fs/promises' import { tmpdir } from 'node:os' import { join } from 'node:path' import { afterEach, describe, expect, it } from 'vitest' import { defaultDocumentParsingSettings, DocumentParsingSettingsStore } from './document-parsing-settings-store' import { defaultHttpOcrSettings } from '../shared/document-parsing-contracts' const temporaryDirectories: string[] = [] async function createStore(): Promise<{ directory: string filePath: string store: DocumentParsingSettingsStore }> { const directory = await mkdtemp( join(tmpdir(), 'goodbuddy-document-parsing-settings-') ) temporaryDirectories.push(directory) const filePath = join(directory, 'document-parsing-settings.json') return { directory, filePath, store: new DocumentParsingSettingsStore(filePath) } } afterEach(async () => { await Promise.all( temporaryDirectories.splice(0).map((directory) => rm(directory, { recursive: true, force: true }) ) ) }) describe('DocumentParsingSettingsStore', () => { it('freezes credentials with the task settings and keeps hidden provider configuration', async () => { const { filePath } = await createStore() const cipher = { isAvailable: () => true, encrypt: (value: string) => Buffer.from(value).reverse(), decrypt: (value: Buffer) => Buffer.from(value).reverse().toString('utf8') } const store = new DocumentParsingSettingsStore(filePath, cipher) await store.update({ ...defaultDocumentParsingSettings, ocrProvider: 'paddleocr-vl', httpOcr: { ...defaultHttpOcrSettings, baseUrl: 'http://first.test', authentication: 'bearer' }, apiKey: 'fixture-key-a' }) const operation = await store.forOperation() await store.update({ ...(await store.get()), ocrProvider: 'local', httpOcr: { ...defaultHttpOcrSettings, baseUrl: 'http://second.test', authentication: 'bearer' }, apiKey: 'fixture-key-b' }) expect(operation.settings.httpOcr?.baseUrl).toBe('http://first.test') expect(operation.apiKey()).toBe('fixture-key-a') expect(await store.getApiKey()).toBe('fixture-key-b') expect(JSON.stringify(await store.get())).not.toContain('fixture-key') expect(await readFile(filePath, 'utf8')).not.toContain('fixture-key') await store.update({ ...(await store.get()), apiKey: '' }) expect(await store.getApiKey()).toBe('fixture-key-b') await store.update({ ...(await store.get()), clearApiKey: true }) expect(await store.getApiKey()).toBeUndefined() }) it('returns local-first defaults without creating a file', async () => { const { directory, store } = await createStore() await expect(store.get()).resolves.toEqual( defaultDocumentParsingSettings ) expect(store.getWarnings()).toEqual([]) await expect(readdir(directory)).resolves.toEqual([]) }) it('persists a complete versioned settings document', async () => { const { filePath, store } = await createStore() const settings = { ...defaultDocumentParsingSettings, chatWorkflow: 'fast-text' as const, maximumPages: 42 } await expect(store.update(settings)).resolves.toEqual(settings) expect(JSON.parse(await readFile(filePath, 'utf8'))).toEqual({ version: 4, ...settings }) await expect( new DocumentParsingSettingsStore(filePath).get() ).resolves.toEqual(settings) }) it('migrates version 2 OCR switches into scenario modes', async () => { const { filePath, store } = await createStore() await writeFile( filePath, JSON.stringify({ version: 2, chatWorkflow: 'auto', knowledgeWorkflow: 'complete-index', pdfOcrMode: 'always', ocrProvider: 'local', localOcrEnabled: true, localOcrModelId: 'pp-ocrv6-small', maximumPages: 42, ocrConcurrency: 4, pageTimeoutSeconds: 90 }), 'utf8' ) await expect(store.get()).resolves.toEqual({ chatWorkflow: 'high-fidelity', knowledgeWorkflow: 'high-fidelity', localOcrModelId: 'pp-ocrv6-small', maximumPages: 42, pageTimeoutSeconds: 90 }) }) it('migrates version 1 cloud settings into local scenario modes', async () => { const { filePath, store } = await createStore() await writeFile( filePath, JSON.stringify({ version: 1, chatWorkflow: 'auto', knowledgeWorkflow: 'complete-index', pdfOcrMode: 'auto', localOcrEnabled: false, localOcrModelId: 'pp-ocrv6-tiny', maximumPages: 100, ocrConcurrency: 1, pageTimeoutSeconds: 60, chatCloudPermission: 'always', knowledgeCloudPermission: 'never' }), 'utf8' ) await expect(store.get()).resolves.toEqual({ ...defaultDocumentParsingSettings, chatWorkflow: 'fast-text', knowledgeWorkflow: 'fast-index' }) }) it('rejects incomplete or out-of-range settings', async () => { const { directory, store } = await createStore() await expect(store.update({})).rejects.toThrow() await expect( store.update({ ...defaultDocumentParsingSettings, maximumPages: 0 }) ).rejects.toThrow() await expect(readdir(directory)).resolves.toEqual([]) }) it('isolates corrupt settings and restores defaults', async () => { const { directory, filePath, store } = await createStore() await writeFile(filePath, '{not-json', 'utf8') await expect(store.get()).resolves.toEqual( defaultDocumentParsingSettings ) expect(store.getWarnings()).toEqual([ { code: 'document-parsing-settings-recovered' } ]) const entries = await readdir(directory) expect(entries).toHaveLength(1) expect(entries[0]).toMatch( /^document-parsing-settings\.json\.corrupt-\d+-[a-f0-9]{12}$/u ) }) it('preserves settings created by a newer unsupported version', async () => { const { directory, filePath, store } = await createStore() const futureSettings = JSON.stringify({ version: 99, futureField: 'keep-me' }) await writeFile(filePath, futureSettings, 'utf8') await expect(store.get()).rejects.toThrow( '不支持文档解析设置版本 99' ) expect(await readFile(filePath, 'utf8')).toBe(futureSettings) expect( (await readdir(directory)).some((name) => name.startsWith('document-parsing-settings.json.corrupt-') ) ).toBe(false) }) })