2026-05-24 08:46:11 -04:00
|
|
|
import { describe, expect, it } from 'vitest';
|
|
|
|
|
|
|
|
|
|
import { createSettingsNormalizationRuntime } from './settings-normalization-runtime.js';
|
|
|
|
|
|
|
|
|
|
const createTestRuntime = (overrides = {}) => {
|
|
|
|
|
const defaults = {
|
|
|
|
|
os: { homedir: () => '/home/testuser' },
|
|
|
|
|
path: {
|
|
|
|
|
resolve: (...args) => args[args.length - 1],
|
|
|
|
|
sep: '/',
|
|
|
|
|
dirname: (p) => p.split('/').slice(0, -1).join('/') || '/',
|
|
|
|
|
},
|
|
|
|
|
processLike: { platform: 'linux', env: {} },
|
|
|
|
|
realpathSync: (p) => p,
|
|
|
|
|
tunnelBootstrapTtlDefaultMs: 600000,
|
|
|
|
|
tunnelBootstrapTtlMinMs: 60000,
|
|
|
|
|
tunnelBootstrapTtlMaxMs: 3600000,
|
|
|
|
|
tunnelSessionTtlDefaultMs: 86400000,
|
|
|
|
|
tunnelSessionTtlMinMs: 3600000,
|
|
|
|
|
tunnelSessionTtlMaxMs: 604800000,
|
|
|
|
|
};
|
|
|
|
|
|
|
|
|
|
return createSettingsNormalizationRuntime({ ...defaults, ...overrides });
|
|
|
|
|
};
|
|
|
|
|
|
|
|
|
|
describe('settings normalization runtime - symlink resolution', () => {
|
|
|
|
|
describe('normalizePathForPersistence', () => {
|
|
|
|
|
it('resolves symlinks via realpathSync', () => {
|
|
|
|
|
const runtime = createTestRuntime({
|
|
|
|
|
realpathSync: (p) =>
|
|
|
|
|
p === '/home/user/workplace' ? '/workplace/user' : p,
|
|
|
|
|
});
|
|
|
|
|
|
|
|
|
|
const result = runtime.normalizePathForPersistence('/home/user/workplace');
|
|
|
|
|
expect(result).toBe('/workplace/user');
|
|
|
|
|
});
|
|
|
|
|
|
|
|
|
|
it('falls back to original path when realpathSync throws', () => {
|
|
|
|
|
const runtime = createTestRuntime({
|
|
|
|
|
realpathSync: () => {
|
|
|
|
|
throw new Error('ENOENT');
|
|
|
|
|
},
|
|
|
|
|
});
|
|
|
|
|
|
|
|
|
|
const result = runtime.normalizePathForPersistence('/nonexistent/path');
|
|
|
|
|
expect(result).toBe('/nonexistent/path');
|
|
|
|
|
});
|
|
|
|
|
|
|
|
|
|
it('passes through when realpathSync is not provided', () => {
|
|
|
|
|
const runtime = createTestRuntime({ realpathSync: undefined });
|
|
|
|
|
|
|
|
|
|
const result = runtime.normalizePathForPersistence('/some/path');
|
|
|
|
|
expect(result).toBe('/some/path');
|
|
|
|
|
});
|
2026-07-13 09:43:35 +11:00
|
|
|
|
|
|
|
|
it('preserves lowercase colon-prefixed paths on non-Windows platforms', () => {
|
|
|
|
|
const runtime = createTestRuntime({ realpathSync: undefined });
|
|
|
|
|
|
|
|
|
|
expect(runtime.normalizePathForPersistence('c:project')).toBe('c:project');
|
|
|
|
|
});
|
|
|
|
|
|
|
|
|
|
it('uppercases Windows drive letter before and after realpath resolution', () => {
|
|
|
|
|
const runtime = createTestRuntime({
|
|
|
|
|
processLike: { platform: 'win32', env: {} },
|
|
|
|
|
realpathSync: (p) => {
|
|
|
|
|
// Simulate safeRealpathSync returning a lowercase drive letter
|
|
|
|
|
if (p === 'C:\\Users\\me\\project') return 'c:\\real\\project';
|
|
|
|
|
return p;
|
|
|
|
|
},
|
|
|
|
|
});
|
|
|
|
|
|
|
|
|
|
const result = runtime.normalizePathForPersistence('c:\\Users\\me\\project');
|
|
|
|
|
// Drive letter uppercased on input AND after realpath
|
|
|
|
|
expect(result).toBe('C:\\real\\project');
|
|
|
|
|
});
|
2026-05-24 08:46:11 -04:00
|
|
|
});
|
|
|
|
|
|
|
|
|
|
describe('sanitizeProjects', () => {
|
|
|
|
|
it('resolves symlinks in project paths', () => {
|
|
|
|
|
const runtime = createTestRuntime({
|
|
|
|
|
realpathSync: (p) =>
|
|
|
|
|
p === '/home/user/workplace/MyProject'
|
|
|
|
|
? '/workplace/user/MyProject'
|
|
|
|
|
: p,
|
|
|
|
|
});
|
|
|
|
|
|
|
|
|
|
const projects = [
|
|
|
|
|
{ id: 'proj1', path: '/home/user/workplace/MyProject', label: 'MyProject', color: 'primary', addedAt: 1000, lastOpenedAt: 1000 },
|
|
|
|
|
];
|
|
|
|
|
|
|
|
|
|
const result = runtime.sanitizeProjects(projects);
|
|
|
|
|
expect(result[0].path).toBe('/workplace/user/MyProject');
|
|
|
|
|
});
|
|
|
|
|
|
|
|
|
|
it('falls back to path.resolve when realpathSync throws', () => {
|
|
|
|
|
const runtime = createTestRuntime({
|
|
|
|
|
realpathSync: () => { throw new Error('ENOENT'); },
|
|
|
|
|
path: { resolve: (p) => '/resolved' + p, sep: '/', dirname: (p) => p.split('/').slice(0, -1).join('/') || '/' },
|
|
|
|
|
});
|
|
|
|
|
|
|
|
|
|
const projects = [
|
|
|
|
|
{ id: 'proj1', path: '/missing/path', label: 'Missing', color: 'primary', addedAt: 1000, lastOpenedAt: 1000 },
|
|
|
|
|
];
|
|
|
|
|
|
|
|
|
|
const result = runtime.sanitizeProjects(projects);
|
|
|
|
|
expect(result[0].path).toBe('/resolved/missing/path');
|
|
|
|
|
});
|
|
|
|
|
|
2026-08-22 20:31:20 +03:00
|
|
|
it('keeps a default thinking level next to its model and drops it alone', () => {
|
|
|
|
|
const runtime = createTestRuntime({
|
|
|
|
|
realpathSync: (p) => p,
|
|
|
|
|
path: { resolve: (p) => p, sep: '/', dirname: (p) => p.split('/').slice(0, -1).join('/') || '/' },
|
|
|
|
|
});
|
|
|
|
|
|
|
|
|
|
const projects = [
|
|
|
|
|
{ id: 'proj1', path: '/a', defaultModel: 'anthropic/claude-opus-5', defaultVariant: 'high' },
|
|
|
|
|
{ id: 'proj2', path: '/b', defaultVariant: 'high' },
|
|
|
|
|
];
|
|
|
|
|
|
|
|
|
|
const result = runtime.sanitizeProjects(projects);
|
|
|
|
|
expect(result[0].defaultVariant).toBe('high');
|
|
|
|
|
expect(result[1].defaultVariant).toBe(undefined);
|
|
|
|
|
});
|
|
|
|
|
|
2026-05-24 08:46:11 -04:00
|
|
|
it('deduplicates projects that resolve to the same realpath', () => {
|
|
|
|
|
const runtime = createTestRuntime({
|
|
|
|
|
realpathSync: (p) => p.startsWith('/symlink') ? '/real/project' : p,
|
|
|
|
|
path: { resolve: (p) => p, sep: '/', dirname: (p) => p.split('/').slice(0, -1).join('/') || '/' },
|
|
|
|
|
});
|
|
|
|
|
|
|
|
|
|
const projects = [
|
|
|
|
|
{ id: 'proj1', path: '/symlink/a', label: 'A', color: 'primary', addedAt: 1000, lastOpenedAt: 1000 },
|
|
|
|
|
{ id: 'proj2', path: '/symlink/b', label: 'B', color: 'keyword', addedAt: 2000, lastOpenedAt: 2000 },
|
|
|
|
|
];
|
|
|
|
|
|
|
|
|
|
const result = runtime.sanitizeProjects(projects);
|
|
|
|
|
expect(result).toHaveLength(1);
|
|
|
|
|
expect(result[0].id).toBe('proj1');
|
|
|
|
|
});
|
|
|
|
|
});
|
|
|
|
|
|
|
|
|
|
describe('normalizeSettingsPaths', () => {
|
|
|
|
|
it('resolves symlinks in lastDirectory', () => {
|
|
|
|
|
const runtime = createTestRuntime({
|
|
|
|
|
realpathSync: (p) =>
|
|
|
|
|
p === '/home/user/workplace/LyraRefactoring'
|
|
|
|
|
? '/workplace/user/LyraRefactoring'
|
|
|
|
|
: p,
|
|
|
|
|
});
|
|
|
|
|
|
|
|
|
|
const result = runtime.normalizeSettingsPaths({
|
|
|
|
|
lastDirectory: '/home/user/workplace/LyraRefactoring',
|
|
|
|
|
});
|
|
|
|
|
|
|
|
|
|
expect(result.changed).toBe(true);
|
|
|
|
|
expect(result.settings.lastDirectory).toBe('/workplace/user/LyraRefactoring');
|
|
|
|
|
});
|
|
|
|
|
|
|
|
|
|
it('does not flag as changed when path is already canonical', () => {
|
|
|
|
|
const runtime = createTestRuntime();
|
|
|
|
|
|
|
|
|
|
const result = runtime.normalizeSettingsPaths({
|
|
|
|
|
lastDirectory: '/real/path',
|
|
|
|
|
});
|
|
|
|
|
|
|
|
|
|
expect(result.changed).toBe(false);
|
|
|
|
|
});
|
|
|
|
|
});
|
|
|
|
|
});
|