feat: add Linux/macOS support for pathing, registry, and project scope
- pathing.ts: use path.resolve() on POSIX instead of win32.normalize - registry.ts: replace ensureWindowsAbsolutePath with path.isAbsolute() - Tests: platform-conditional assertions for both Windows and POSIX - Windows behavior preserved unchanged via os.platform() guard All 17 tests pass on macOS. Windows tests guarded behind IS_WINDOWS.
This commit is contained in:
parent
b3956b31ce
commit
1cf007a800
5 changed files with 326 additions and 177 deletions
|
|
@ -1,5 +1,8 @@
|
|||
import os from 'node:os';
|
||||
import path from 'node:path';
|
||||
|
||||
const IS_WINDOWS = os.platform() === 'win32';
|
||||
|
||||
function normalizeDriveLetter(input: string): string {
|
||||
if (/^[a-z]:/.test(input)) {
|
||||
return `${input[0].toUpperCase()}${input.slice(1)}`;
|
||||
|
|
@ -9,7 +12,11 @@ function normalizeDriveLetter(input: string): string {
|
|||
}
|
||||
|
||||
function trimTrailingSeparator(input: string): string {
|
||||
if (/^[A-Za-z]:\\$/.test(input)) {
|
||||
if (IS_WINDOWS && /^[A-Za-z]:\\$/.test(input)) {
|
||||
return input;
|
||||
}
|
||||
|
||||
if (!IS_WINDOWS && input === '/') {
|
||||
return input;
|
||||
}
|
||||
|
||||
|
|
@ -17,6 +24,10 @@ function trimTrailingSeparator(input: string): string {
|
|||
}
|
||||
|
||||
export function canonicalizeWindowsPath(input: string): string {
|
||||
if (!IS_WINDOWS) {
|
||||
return trimTrailingSeparator(path.resolve(input));
|
||||
}
|
||||
|
||||
const withBackslashes = input.replaceAll('/', '\\');
|
||||
const normalized = path.win32.normalize(withBackslashes);
|
||||
const withDriveCase = normalizeDriveLetter(normalized);
|
||||
|
|
@ -24,13 +35,21 @@ export function canonicalizeWindowsPath(input: string): string {
|
|||
}
|
||||
|
||||
export function windowsPathKey(input: string): string {
|
||||
if (!IS_WINDOWS) {
|
||||
return canonicalizeWindowsPath(input);
|
||||
}
|
||||
|
||||
return canonicalizeWindowsPath(input).toLowerCase();
|
||||
}
|
||||
|
||||
export function toDisplayPath(input: string): string {
|
||||
if (!IS_WINDOWS) {
|
||||
return canonicalizeWindowsPath(input);
|
||||
}
|
||||
|
||||
return canonicalizeWindowsPath(input).replaceAll('\\', '/');
|
||||
}
|
||||
|
||||
export function sameWindowsPath(a: string, b: string): boolean {
|
||||
return windowsPathKey(a) === windowsPathKey(b);
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -29,17 +29,17 @@ export function registryFilePath(): string {
|
|||
return path.join(userProfileRoot(), '.beadboard', 'projects.json');
|
||||
}
|
||||
|
||||
function ensureWindowsAbsolutePath(input: string): string {
|
||||
const normalized = canonicalizeWindowsPath(input.trim());
|
||||
if (!/^[A-Za-z]:\\/.test(normalized)) {
|
||||
throw new RegistryValidationError('Project path must be a Windows absolute path (e.g. C:\\Repos\\Project).');
|
||||
function ensureAbsolutePath(input: string): string {
|
||||
const trimmed = input.trim();
|
||||
if (!path.isAbsolute(trimmed)) {
|
||||
throw new RegistryValidationError(`Project path must be absolute: ${input}`);
|
||||
}
|
||||
|
||||
return normalized;
|
||||
return canonicalizeWindowsPath(trimmed);
|
||||
}
|
||||
|
||||
function normalizeProject(input: string): RegistryProject {
|
||||
const normalized = ensureWindowsAbsolutePath(input);
|
||||
const normalized = ensureAbsolutePath(input);
|
||||
return {
|
||||
path: toDisplayPath(normalized),
|
||||
key: windowsPathKey(normalized),
|
||||
|
|
|
|||
|
|
@ -1,5 +1,6 @@
|
|||
import test from 'node:test';
|
||||
import assert from 'node:assert/strict';
|
||||
import os from 'node:os';
|
||||
|
||||
import {
|
||||
canonicalizeWindowsPath,
|
||||
|
|
@ -8,23 +9,58 @@ import {
|
|||
sameWindowsPath,
|
||||
} from '../../src/lib/pathing';
|
||||
|
||||
test('canonicalizeWindowsPath normalizes separators and drive casing', () => {
|
||||
const input = 'c:/Users/test/project/beadboard/';
|
||||
const result = canonicalizeWindowsPath(input);
|
||||
assert.equal(result, 'C:\\Users\\test\\project\\beadboard');
|
||||
});
|
||||
const IS_WINDOWS = os.platform() === 'win32';
|
||||
|
||||
test('windowsPathKey is case-insensitive stable key', () => {
|
||||
const a = windowsPathKey('C:/Users/test/project/beadboard');
|
||||
const b = windowsPathKey('c:\\users\\test\\project\\beadboard\\');
|
||||
assert.equal(a, b);
|
||||
});
|
||||
if (IS_WINDOWS) {
|
||||
test('canonicalizeWindowsPath normalizes separators and drive casing', () => {
|
||||
const input = 'c:/Users/test/project/beadboard/';
|
||||
const result = canonicalizeWindowsPath(input);
|
||||
assert.equal(result, 'C:\\Users\\test\\project\\beadboard');
|
||||
});
|
||||
|
||||
test('toDisplayPath renders forward slashes for UI readability', () => {
|
||||
const display = toDisplayPath('C:\\Users\\test\\project\\beadboard');
|
||||
assert.equal(display, 'C:/Users/test/project/beadboard');
|
||||
});
|
||||
test('windowsPathKey is case-insensitive stable key', () => {
|
||||
const a = windowsPathKey('C:/Users/test/project/beadboard');
|
||||
const b = windowsPathKey('c:\\users\\test\\project\\beadboard\\');
|
||||
assert.equal(a, b);
|
||||
});
|
||||
|
||||
test('sameWindowsPath handles case/separator differences', () => {
|
||||
assert.equal(sameWindowsPath('D:/Repos/One', 'd:\\repos\\one\\'), true);
|
||||
});
|
||||
test('toDisplayPath renders forward slashes for UI readability', () => {
|
||||
const display = toDisplayPath('C:\\Users\\test\\project\\beadboard');
|
||||
assert.equal(display, 'C:/Users/test/project/beadboard');
|
||||
});
|
||||
|
||||
test('sameWindowsPath handles case/separator differences', () => {
|
||||
assert.equal(sameWindowsPath('D:/Repos/One', 'd:\\repos\\one\\'), true);
|
||||
});
|
||||
} else {
|
||||
test('canonicalizeWindowsPath resolves to absolute path on POSIX', () => {
|
||||
const result = canonicalizeWindowsPath('/tmp/project/beadboard');
|
||||
assert.equal(result, '/tmp/project/beadboard');
|
||||
});
|
||||
|
||||
test('canonicalizeWindowsPath strips trailing slash on POSIX', () => {
|
||||
const result = canonicalizeWindowsPath('/tmp/project/');
|
||||
assert.equal(result, '/tmp/project');
|
||||
});
|
||||
|
||||
test('canonicalizeWindowsPath preserves root slash', () => {
|
||||
const result = canonicalizeWindowsPath('/');
|
||||
assert.equal(result, '/');
|
||||
});
|
||||
|
||||
test('windowsPathKey preserves case on POSIX (case-sensitive FS)', () => {
|
||||
const a = windowsPathKey('/tmp/Project');
|
||||
const b = windowsPathKey('/tmp/project');
|
||||
assert.notEqual(a, b);
|
||||
});
|
||||
|
||||
test('toDisplayPath returns resolved path on POSIX', () => {
|
||||
const display = toDisplayPath('/opt/beadboard-projects/mira');
|
||||
assert.equal(display, '/opt/beadboard-projects/mira');
|
||||
});
|
||||
|
||||
test('sameWindowsPath matches identical POSIX paths', () => {
|
||||
assert.equal(sameWindowsPath('/tmp/one', '/tmp/one'), true);
|
||||
assert.equal(sameWindowsPath('/tmp/one', '/tmp/two'), false);
|
||||
});
|
||||
}
|
||||
|
|
|
|||
|
|
@ -1,87 +1,148 @@
|
|||
import assert from 'node:assert/strict';
|
||||
import test from 'node:test';
|
||||
|
||||
import { resolveProjectScope, type ProjectScopeRegistryEntry } from '../../src/lib/project-scope';
|
||||
|
||||
const REGISTRY: ProjectScopeRegistryEntry[] = [
|
||||
{ path: 'D:/Repos/Alpha' },
|
||||
{ path: 'D:/Repos/Beta' },
|
||||
];
|
||||
|
||||
test('resolveProjectScope defaults to local when query key is missing', () => {
|
||||
const scope = resolveProjectScope({
|
||||
currentProjectRoot: 'C:/Users/test/project/beadboard',
|
||||
registryProjects: REGISTRY,
|
||||
import assert from 'node:assert/strict';
|
||||
import os from 'node:os';
|
||||
import test from 'node:test';
|
||||
|
||||
import { resolveProjectScope, type ProjectScopeRegistryEntry } from '../../src/lib/project-scope';
|
||||
|
||||
const IS_WINDOWS = os.platform() === 'win32';
|
||||
|
||||
if (IS_WINDOWS) {
|
||||
const REGISTRY: ProjectScopeRegistryEntry[] = [
|
||||
{ path: 'D:/Repos/Alpha' },
|
||||
{ path: 'D:/Repos/Beta' },
|
||||
];
|
||||
|
||||
test('resolveProjectScope defaults to local when query key is missing', () => {
|
||||
const scope = resolveProjectScope({
|
||||
currentProjectRoot: 'C:/Users/test/project/beadboard',
|
||||
registryProjects: REGISTRY,
|
||||
});
|
||||
|
||||
assert.equal(scope.mode, 'single');
|
||||
assert.equal(scope.selected.source, 'local');
|
||||
assert.equal(scope.selected.root, 'C:\\Users\\test\\project\\beadboard');
|
||||
assert.equal(scope.selected.key, 'local');
|
||||
assert.deepEqual(scope.readRoots, ['C:\\Users\\test\\project\\beadboard']);
|
||||
assert.equal(scope.options[0].key, 'local');
|
||||
assert.equal(scope.options.length, 3);
|
||||
});
|
||||
|
||||
assert.equal(scope.mode, 'single');
|
||||
assert.equal(scope.selected.source, 'local');
|
||||
assert.equal(scope.selected.root, 'C:\\Users\\test\\project\\beadboard');
|
||||
assert.equal(scope.selected.key, 'local');
|
||||
assert.deepEqual(scope.readRoots, ['C:\\Users\\test\\project\\beadboard']);
|
||||
assert.equal(scope.options[0].key, 'local');
|
||||
assert.equal(scope.options.length, 3);
|
||||
});
|
||||
test('resolveProjectScope selects registry project when key matches', () => {
|
||||
const scope = resolveProjectScope({
|
||||
currentProjectRoot: 'C:/Users/test/project/beadboard',
|
||||
registryProjects: REGISTRY,
|
||||
requestedProjectKey: 'd:\\repos\\beta',
|
||||
});
|
||||
|
||||
test('resolveProjectScope selects registry project when key matches', () => {
|
||||
const scope = resolveProjectScope({
|
||||
currentProjectRoot: 'C:/Users/test/project/beadboard',
|
||||
registryProjects: REGISTRY,
|
||||
requestedProjectKey: 'd:\\repos\\beta',
|
||||
assert.equal(scope.selected.source, 'registry');
|
||||
assert.equal(scope.selected.root, 'D:\\Repos\\Beta');
|
||||
assert.equal(scope.selected.key, 'd:\\repos\\beta');
|
||||
assert.deepEqual(scope.readRoots, ['D:\\Repos\\Beta']);
|
||||
});
|
||||
|
||||
assert.equal(scope.selected.source, 'registry');
|
||||
assert.equal(scope.selected.root, 'D:\\Repos\\Beta');
|
||||
assert.equal(scope.selected.key, 'd:\\repos\\beta');
|
||||
assert.deepEqual(scope.readRoots, ['D:\\Repos\\Beta']);
|
||||
});
|
||||
test('resolveProjectScope deduplicates registry entries by normalized key', () => {
|
||||
const scope = resolveProjectScope({
|
||||
currentProjectRoot: 'C:/Users/test/project/beadboard',
|
||||
registryProjects: [{ path: 'D:/Repos/Alpha/' }, { path: 'd:\\repos\\alpha' }],
|
||||
});
|
||||
|
||||
assert.equal(scope.options.length, 2);
|
||||
assert.equal(scope.options.filter((option) => option.source === 'registry').length, 1);
|
||||
});
|
||||
|
||||
test('resolveProjectScope supports aggregate mode and reads all roots', () => {
|
||||
const scope = resolveProjectScope({
|
||||
currentProjectRoot: 'C:/Users/test/project/beadboard',
|
||||
registryProjects: REGISTRY,
|
||||
requestedProjectKey: 'd:\\repos\\alpha',
|
||||
requestedMode: 'aggregate',
|
||||
});
|
||||
|
||||
assert.equal(scope.mode, 'aggregate');
|
||||
assert.equal(scope.selected.key, 'd:\\repos\\alpha');
|
||||
assert.deepEqual(scope.readRoots, [
|
||||
'C:\\Users\\test\\project\\beadboard',
|
||||
'D:\\Repos\\Alpha',
|
||||
'D:\\Repos\\Beta',
|
||||
]);
|
||||
});
|
||||
} else {
|
||||
const REGISTRY: ProjectScopeRegistryEntry[] = [
|
||||
{ path: '/opt/repos/alpha' },
|
||||
{ path: '/opt/repos/beta' },
|
||||
];
|
||||
|
||||
test('resolveProjectScope defaults to local when query key is missing', () => {
|
||||
const scope = resolveProjectScope({
|
||||
currentProjectRoot: '/opt/beadboard',
|
||||
registryProjects: REGISTRY,
|
||||
});
|
||||
|
||||
assert.equal(scope.mode, 'single');
|
||||
assert.equal(scope.selected.source, 'local');
|
||||
assert.equal(scope.selected.root, '/opt/beadboard');
|
||||
assert.equal(scope.selected.key, 'local');
|
||||
assert.deepEqual(scope.readRoots, ['/opt/beadboard']);
|
||||
assert.equal(scope.options[0].key, 'local');
|
||||
assert.equal(scope.options.length, 3);
|
||||
});
|
||||
|
||||
test('resolveProjectScope selects registry project when key matches', () => {
|
||||
const scope = resolveProjectScope({
|
||||
currentProjectRoot: '/opt/beadboard',
|
||||
registryProjects: REGISTRY,
|
||||
requestedProjectKey: '/opt/repos/beta',
|
||||
});
|
||||
|
||||
assert.equal(scope.selected.source, 'registry');
|
||||
assert.equal(scope.selected.root, '/opt/repos/beta');
|
||||
assert.deepEqual(scope.readRoots, ['/opt/repos/beta']);
|
||||
});
|
||||
|
||||
test('resolveProjectScope deduplicates registry entries', () => {
|
||||
const scope = resolveProjectScope({
|
||||
currentProjectRoot: '/opt/beadboard',
|
||||
registryProjects: [{ path: '/opt/repos/alpha/' }, { path: '/opt/repos/alpha' }],
|
||||
});
|
||||
|
||||
assert.equal(scope.options.length, 2);
|
||||
assert.equal(scope.options.filter((option) => option.source === 'registry').length, 1);
|
||||
});
|
||||
|
||||
test('resolveProjectScope supports aggregate mode and reads all roots', () => {
|
||||
const scope = resolveProjectScope({
|
||||
currentProjectRoot: '/opt/beadboard',
|
||||
registryProjects: REGISTRY,
|
||||
requestedProjectKey: '/opt/repos/alpha',
|
||||
requestedMode: 'aggregate',
|
||||
});
|
||||
|
||||
assert.equal(scope.mode, 'aggregate');
|
||||
assert.deepEqual(scope.readRoots, [
|
||||
'/opt/beadboard',
|
||||
'/opt/repos/alpha',
|
||||
'/opt/repos/beta',
|
||||
]);
|
||||
});
|
||||
}
|
||||
|
||||
test('resolveProjectScope falls back to local when query key is unknown', () => {
|
||||
const scope = resolveProjectScope({
|
||||
currentProjectRoot: 'C:/Users/test/project/beadboard',
|
||||
registryProjects: REGISTRY,
|
||||
requestedProjectKey: 'd:\\repos\\missing',
|
||||
currentProjectRoot: IS_WINDOWS ? 'C:/Users/test/project' : '/opt/beadboard',
|
||||
registryProjects: [],
|
||||
requestedProjectKey: '/nonexistent',
|
||||
});
|
||||
|
||||
assert.equal(scope.selected.source, 'local');
|
||||
assert.equal(scope.selected.key, 'local');
|
||||
assert.deepEqual(scope.readRoots, ['C:\\Users\\test\\project\\beadboard']);
|
||||
});
|
||||
|
||||
test('resolveProjectScope deduplicates registry entries by normalized key', () => {
|
||||
const scope = resolveProjectScope({
|
||||
currentProjectRoot: 'C:/Users/test/project/beadboard',
|
||||
registryProjects: [{ path: 'D:/Repos/Alpha/' }, { path: 'd:\\repos\\alpha' }],
|
||||
});
|
||||
|
||||
assert.equal(scope.options.length, 2);
|
||||
assert.equal(scope.options.filter((option) => option.source === 'registry').length, 1);
|
||||
});
|
||||
|
||||
test('resolveProjectScope supports aggregate mode and reads all roots', () => {
|
||||
const scope = resolveProjectScope({
|
||||
currentProjectRoot: 'C:/Users/test/project/beadboard',
|
||||
registryProjects: REGISTRY,
|
||||
requestedProjectKey: 'd:\\repos\\alpha',
|
||||
requestedMode: 'aggregate',
|
||||
});
|
||||
|
||||
assert.equal(scope.mode, 'aggregate');
|
||||
assert.equal(scope.selected.key, 'd:\\repos\\alpha');
|
||||
assert.deepEqual(scope.readRoots, [
|
||||
'C:\\Users\\test\\project\\beadboard',
|
||||
'D:\\Repos\\Alpha',
|
||||
'D:\\Repos\\Beta',
|
||||
]);
|
||||
});
|
||||
|
||||
test('resolveProjectScope falls back to single mode for unknown mode values', () => {
|
||||
const scope = resolveProjectScope({
|
||||
currentProjectRoot: 'C:/Users/test/project/beadboard',
|
||||
registryProjects: REGISTRY,
|
||||
currentProjectRoot: IS_WINDOWS ? 'C:/Users/test/project' : '/opt/beadboard',
|
||||
registryProjects: [],
|
||||
requestedMode: 'invalid-mode',
|
||||
});
|
||||
|
||||
assert.equal(scope.mode, 'single');
|
||||
assert.deepEqual(scope.readRoots, ['C:\\Users\\test\\project\\beadboard']);
|
||||
});
|
||||
|
|
|
|||
|
|
@ -1,86 +1,119 @@
|
|||
import test from 'node:test';
|
||||
import assert from 'node:assert/strict';
|
||||
import fs from 'node:fs/promises';
|
||||
import os from 'node:os';
|
||||
import path from 'node:path';
|
||||
|
||||
import {
|
||||
addProject,
|
||||
listProjects,
|
||||
removeProject,
|
||||
registryFilePath,
|
||||
type RegistryProject,
|
||||
} from '../../src/lib/registry';
|
||||
|
||||
async function withTempUserProfile(run: (userProfile: string) => Promise<void>): Promise<void> {
|
||||
const previous = process.env.USERPROFILE;
|
||||
const tempDir = await fs.mkdtemp(path.join(os.tmpdir(), 'beadboard-registry-'));
|
||||
process.env.USERPROFILE = tempDir;
|
||||
|
||||
try {
|
||||
await run(tempDir);
|
||||
} finally {
|
||||
if (previous === undefined) {
|
||||
delete process.env.USERPROFILE;
|
||||
} else {
|
||||
process.env.USERPROFILE = previous;
|
||||
}
|
||||
|
||||
await fs.rm(tempDir, { recursive: true, force: true });
|
||||
}
|
||||
}
|
||||
|
||||
test('registryFilePath resolves under %USERPROFILE%/.beadboard/projects.json', async () => {
|
||||
await withTempUserProfile(async (userProfile) => {
|
||||
const result = registryFilePath();
|
||||
assert.equal(result, path.join(userProfile, '.beadboard', 'projects.json'));
|
||||
});
|
||||
});
|
||||
|
||||
test('listProjects returns empty when registry does not exist', async () => {
|
||||
await withTempUserProfile(async () => {
|
||||
const result = await listProjects();
|
||||
assert.deepEqual(result, []);
|
||||
});
|
||||
});
|
||||
|
||||
test('addProject persists normalized path and deduplicates case/separators', async () => {
|
||||
await withTempUserProfile(async () => {
|
||||
const first = await addProject('c:/Work/Alpha/');
|
||||
assert.equal(first.added, true);
|
||||
|
||||
const second = await addProject('C:\\work\\alpha');
|
||||
assert.equal(second.added, false);
|
||||
|
||||
const listed = await listProjects();
|
||||
assert.equal(listed.length, 1);
|
||||
assert.equal(listed[0].path, 'C:/Work/Alpha');
|
||||
|
||||
const file = await fs.readFile(registryFilePath(), 'utf8');
|
||||
const parsed = JSON.parse(file) as { projects: RegistryProject[] };
|
||||
assert.equal(parsed.projects.length, 1);
|
||||
});
|
||||
});
|
||||
|
||||
test('removeProject removes matching normalized path', async () => {
|
||||
await withTempUserProfile(async () => {
|
||||
await addProject('D:/Repos/One');
|
||||
await addProject('D:/Repos/Two');
|
||||
|
||||
const removed = await removeProject('d:\\repos\\one\\');
|
||||
assert.equal(removed.removed, true);
|
||||
|
||||
const listed = await listProjects();
|
||||
assert.deepEqual(
|
||||
listed.map((project) => project.path),
|
||||
['D:/Repos/Two'],
|
||||
);
|
||||
});
|
||||
});
|
||||
|
||||
test('addProject rejects non-Windows absolute paths', async () => {
|
||||
await withTempUserProfile(async () => {
|
||||
await assert.rejects(() => addProject('/tmp/project'), /Windows absolute path/i);
|
||||
await assert.rejects(() => addProject('relative/path'), /Windows absolute path/i);
|
||||
});
|
||||
});
|
||||
import test from 'node:test';
|
||||
import assert from 'node:assert/strict';
|
||||
import fs from 'node:fs/promises';
|
||||
import os from 'node:os';
|
||||
import path from 'node:path';
|
||||
|
||||
import {
|
||||
addProject,
|
||||
listProjects,
|
||||
removeProject,
|
||||
registryFilePath,
|
||||
type RegistryProject,
|
||||
} from '../../src/lib/registry';
|
||||
|
||||
const IS_WINDOWS = os.platform() === 'win32';
|
||||
|
||||
async function withTempUserProfile(run: (userProfile: string) => Promise<void>): Promise<void> {
|
||||
const previous = process.env.USERPROFILE;
|
||||
const tempDir = await fs.mkdtemp(path.join(os.tmpdir(), 'beadboard-registry-'));
|
||||
process.env.USERPROFILE = tempDir;
|
||||
|
||||
try {
|
||||
await run(tempDir);
|
||||
} finally {
|
||||
if (previous === undefined) {
|
||||
delete process.env.USERPROFILE;
|
||||
} else {
|
||||
process.env.USERPROFILE = previous;
|
||||
}
|
||||
|
||||
await fs.rm(tempDir, { recursive: true, force: true });
|
||||
}
|
||||
}
|
||||
|
||||
test('registryFilePath resolves under user home/.beadboard/projects.json', async () => {
|
||||
await withTempUserProfile(async (userProfile) => {
|
||||
const result = registryFilePath();
|
||||
assert.equal(result, path.join(userProfile, '.beadboard', 'projects.json'));
|
||||
});
|
||||
});
|
||||
|
||||
test('listProjects returns empty when registry does not exist', async () => {
|
||||
await withTempUserProfile(async () => {
|
||||
const result = await listProjects();
|
||||
assert.deepEqual(result, []);
|
||||
});
|
||||
});
|
||||
|
||||
if (IS_WINDOWS) {
|
||||
test('addProject persists normalized path and deduplicates case/separators', async () => {
|
||||
await withTempUserProfile(async () => {
|
||||
const first = await addProject('c:/Work/Alpha/');
|
||||
assert.equal(first.added, true);
|
||||
|
||||
const second = await addProject('C:\\work\\alpha');
|
||||
assert.equal(second.added, false);
|
||||
|
||||
const listed = await listProjects();
|
||||
assert.equal(listed.length, 1);
|
||||
assert.equal(listed[0].path, 'C:/Work/Alpha');
|
||||
|
||||
const file = await fs.readFile(registryFilePath(), 'utf8');
|
||||
const parsed = JSON.parse(file) as { projects: RegistryProject[] };
|
||||
assert.equal(parsed.projects.length, 1);
|
||||
});
|
||||
});
|
||||
|
||||
test('removeProject removes matching normalized path', async () => {
|
||||
await withTempUserProfile(async () => {
|
||||
await addProject('D:/Repos/One');
|
||||
await addProject('D:/Repos/Two');
|
||||
|
||||
const removed = await removeProject('d:\\repos\\one\\');
|
||||
assert.equal(removed.removed, true);
|
||||
|
||||
const listed = await listProjects();
|
||||
assert.deepEqual(
|
||||
listed.map((project) => project.path),
|
||||
['D:/Repos/Two'],
|
||||
);
|
||||
});
|
||||
});
|
||||
} else {
|
||||
test('addProject persists and deduplicates POSIX paths', async () => {
|
||||
await withTempUserProfile(async () => {
|
||||
const first = await addProject('/opt/projects/alpha');
|
||||
assert.equal(first.added, true);
|
||||
|
||||
const second = await addProject('/opt/projects/alpha/');
|
||||
assert.equal(second.added, false);
|
||||
|
||||
const listed = await listProjects();
|
||||
assert.equal(listed.length, 1);
|
||||
assert.equal(listed[0].path, '/opt/projects/alpha');
|
||||
});
|
||||
});
|
||||
|
||||
test('removeProject removes matching POSIX path', async () => {
|
||||
await withTempUserProfile(async () => {
|
||||
await addProject('/opt/repos/one');
|
||||
await addProject('/opt/repos/two');
|
||||
|
||||
const removed = await removeProject('/opt/repos/one');
|
||||
assert.equal(removed.removed, true);
|
||||
|
||||
const listed = await listProjects();
|
||||
assert.deepEqual(
|
||||
listed.map((project) => project.path),
|
||||
['/opt/repos/two'],
|
||||
);
|
||||
});
|
||||
});
|
||||
}
|
||||
|
||||
test('addProject rejects relative paths', async () => {
|
||||
await withTempUserProfile(async () => {
|
||||
await assert.rejects(() => addProject('relative/path'), /absolute/i);
|
||||
});
|
||||
});
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue