* fix: make upstream sync actions target the selected remote Ensure fetch and pull actually honor upstream selection so fork maintenance works from the Git sidebar, and surface upstream branch status alongside the primary origin-tracking indicators. * feat: add Windows Electron desktop foundation * fix(electron): stabilize Windows desktop packaging * fix(electron): stabilize Windows desktop chrome Use native Windows titlebar behavior with an Alt-accessible hidden menu, and harden Windows dev command launching so the desktop app follows platform conventions. * fix(electron): stabilize Windows dev startup * fix(electron): clarify desktop artifact names * fix(electron): harden Windows desktop release and launch * fix(electron): address Windows release review * fix(electron): point updater and release links to org repo * Fix Windows settings persistence fallback * Fix Windows Electron dev startup * Add Windows Electron window controls * Fix Windows Electron install and opencode launch * fix: resolve git status for repositories without upstream Fixes repository detection stuck on Checking repository Handles git status when no upstream is configured Adds regression coverage for git status loading * Add Windows app menu button * fix: preserve file editor line endings * ci: add desktop release smoke workflow --------- Co-authored-by: Bohdan Triapitsyn <artmore@protonmail.com>
125 lines
5.1 KiB
JavaScript
125 lines
5.1 KiB
JavaScript
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 });
|
|
}
|
|
});
|
|
});
|