import { describe, expect, it } from 'vitest'; import crypto from 'crypto'; import fsPromises from 'fs/promises'; import os from 'os'; import path from 'path'; import { createProjectIdFromPath } from '../projects/project-id.js'; import { createSettingsRuntime } from './settings-runtime.js'; const createRuntime = async () => { const tempRoot = await fsPromises.mkdtemp(path.join(os.tmpdir(), 'oc-settings-runtime-')); const settingsFilePath = path.join(tempRoot, 'settings.json'); const runtime = createSettingsRuntime({ fsPromises, path, crypto, SETTINGS_FILE_PATH: settingsFilePath, sanitizeProjects: (projects) => Array.isArray(projects) ? projects : [], sanitizeSettingsUpdate: (settings) => settings, mergePersistedSettings: (_current, changes) => changes, normalizeSettingsPaths: (settings) => ({ settings, changed: false }), normalizeStringArray: (values) => Array.isArray(values) ? values.filter((value) => typeof value === 'string') : [], formatSettingsResponse: (settings) => settings, resolveDirectoryCandidate: (value) => value, normalizeManagedRemoteTunnelHostname: (value) => value, normalizeManagedRemoteTunnelPresets: (value) => value, normalizeManagedRemoteTunnelPresetTokens: (value) => value, syncManagedRemoteTunnelConfigWithPresets: async () => {}, upsertManagedRemoteTunnelToken: async () => {}, }); return { runtime, settingsFilePath, tempRoot, cleanup: async () => { await fsPromises.rm(tempRoot, { recursive: true, force: true }); }, }; }; describe('settings runtime', () => { it('only remaps project plan paths within the migrated storage directory', async () => { const { runtime, settingsFilePath, tempRoot, cleanup } = await createRuntime(); try { const projectPath = path.join(tempRoot, 'project'); const oldProjectId = 'legacy-project-id'; const newProjectId = createProjectIdFromPath(projectPath); const projectsRoot = path.join(path.dirname(settingsFilePath), 'projects'); const oldStorageDir = path.join(projectsRoot, oldProjectId); const newStorageDir = path.join(projectsRoot, newProjectId); const siblingStorageDir = `${oldStorageDir}-sibling`; await fsPromises.mkdir(projectPath, { recursive: true }); await fsPromises.mkdir(projectsRoot, { recursive: true }); await fsPromises.writeFile( settingsFilePath, JSON.stringify({ projects: [{ id: oldProjectId, path: projectPath, addedAt: 1, lastOpenedAt: 1 }], activeProjectId: oldProjectId, }, null, 2), 'utf8', ); await fsPromises.writeFile( path.join(projectsRoot, `${oldProjectId}.json`), JSON.stringify({ projectPlanFiles: [ { id: 'inside', path: path.join(oldStorageDir, 'plans', 'inside.md') }, { id: 'sibling', path: path.join(siblingStorageDir, 'plans', 'outside.md') }, ], }, null, 2), 'utf8', ); await runtime.readSettingsFromDiskMigrated(); const migratedConfig = JSON.parse(await fsPromises.readFile(path.join(projectsRoot, `${newProjectId}.json`), 'utf8')); expect(migratedConfig.projectPlanFiles).toEqual([ { id: 'inside', path: path.join(newStorageDir, 'plans', 'inside.md') }, { id: 'sibling', path: path.join(siblingStorageDir, 'plans', 'outside.md') }, ]); } finally { await cleanup(); } }); it.skipIf(process.platform !== 'win32')('falls back when Windows blocks atomic settings replacement', async () => { const tempRoot = await fsPromises.mkdtemp(path.join(os.tmpdir(), 'oc-settings-runtime-')); const settingsFilePath = path.join(tempRoot, 'settings.json'); const wrappedFs = { ...fsPromises, rename: async () => { const error = new Error('operation not permitted'); error.code = 'EPERM'; throw error; }, }; const runtime = createSettingsRuntime({ fsPromises: wrappedFs, path, crypto, SETTINGS_FILE_PATH: settingsFilePath, sanitizeProjects: (projects) => Array.isArray(projects) ? projects : [], sanitizeSettingsUpdate: (settings) => settings, mergePersistedSettings: (_current, changes) => changes, normalizeSettingsPaths: (settings) => ({ settings, changed: false }), normalizeStringArray: (values) => Array.isArray(values) ? values.filter((value) => typeof value === 'string') : [], formatSettingsResponse: (settings) => settings, resolveDirectoryCandidate: (value) => value, normalizeManagedRemoteTunnelHostname: (value) => value, normalizeManagedRemoteTunnelPresets: (value) => value, normalizeManagedRemoteTunnelPresetTokens: (value) => value, syncManagedRemoteTunnelConfigWithPresets: async () => {}, upsertManagedRemoteTunnelToken: async () => {}, }); try { await runtime.writeSettingsToDisk({ theme: 'dark' }); await expect(fsPromises.readFile(settingsFilePath, 'utf8')).resolves.toBe(JSON.stringify({ theme: 'dark' }, null, 2)); } finally { await fsPromises.rm(tempRoot, { recursive: true, force: true }); } }); });