feat: add Windows project registry API and persistence
This commit is contained in:
parent
cc616c1543
commit
c836be46cf
5 changed files with 396 additions and 1 deletions
109
tests/api/projects-route.test.ts
Normal file
109
tests/api/projects-route.test.ts
Normal file
|
|
@ -0,0 +1,109 @@
|
|||
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 { DELETE, GET, POST } from '../../src/app/api/projects/route';
|
||||
|
||||
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-api-'));
|
||||
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 });
|
||||
}
|
||||
}
|
||||
|
||||
async function readJson(response: Response): Promise<unknown> {
|
||||
return response.json();
|
||||
}
|
||||
|
||||
test('GET /api/projects returns empty list initially', async () => {
|
||||
await withTempUserProfile(async () => {
|
||||
const response = await GET();
|
||||
assert.equal(response.status, 200);
|
||||
|
||||
const body = (await readJson(response)) as { projects: unknown[] };
|
||||
assert.deepEqual(body.projects, []);
|
||||
});
|
||||
});
|
||||
|
||||
test('POST /api/projects validates payload and path', async () => {
|
||||
await withTempUserProfile(async () => {
|
||||
const missing = await POST(new Request('http://localhost/api/projects', { method: 'POST', body: '{}' }));
|
||||
assert.equal(missing.status, 400);
|
||||
|
||||
const missingBody = (await readJson(missing)) as { error: string };
|
||||
assert.match(missingBody.error, /path/i);
|
||||
|
||||
const invalidPath = await POST(
|
||||
new Request('http://localhost/api/projects', {
|
||||
method: 'POST',
|
||||
body: JSON.stringify({ path: '/tmp/project' }),
|
||||
headers: { 'content-type': 'application/json' },
|
||||
}),
|
||||
);
|
||||
assert.equal(invalidPath.status, 400);
|
||||
});
|
||||
});
|
||||
|
||||
test('POST deduplicates and GET returns normalized path', async () => {
|
||||
await withTempUserProfile(async () => {
|
||||
const first = await POST(
|
||||
new Request('http://localhost/api/projects', {
|
||||
method: 'POST',
|
||||
body: JSON.stringify({ path: 'c:/Users/Zenchant/codex/beadboard/' }),
|
||||
headers: { 'content-type': 'application/json' },
|
||||
}),
|
||||
);
|
||||
assert.equal(first.status, 201);
|
||||
|
||||
const dup = await POST(
|
||||
new Request('http://localhost/api/projects', {
|
||||
method: 'POST',
|
||||
body: JSON.stringify({ path: 'C:\\users\\zenchant\\codex\\beadboard' }),
|
||||
headers: { 'content-type': 'application/json' },
|
||||
}),
|
||||
);
|
||||
assert.equal(dup.status, 200);
|
||||
|
||||
const list = await GET();
|
||||
const body = (await readJson(list)) as { projects: Array<{ path: string }> };
|
||||
assert.deepEqual(body.projects, [{ path: 'C:/Users/Zenchant/codex/beadboard' }]);
|
||||
});
|
||||
});
|
||||
|
||||
test('DELETE /api/projects removes by normalized path', async () => {
|
||||
await withTempUserProfile(async () => {
|
||||
await POST(
|
||||
new Request('http://localhost/api/projects', {
|
||||
method: 'POST',
|
||||
body: JSON.stringify({ path: 'D:/Repos/One' }),
|
||||
headers: { 'content-type': 'application/json' },
|
||||
}),
|
||||
);
|
||||
|
||||
const removed = await DELETE(
|
||||
new Request('http://localhost/api/projects', {
|
||||
method: 'DELETE',
|
||||
body: JSON.stringify({ path: 'd:\\repos\\one\\' }),
|
||||
headers: { 'content-type': 'application/json' },
|
||||
}),
|
||||
);
|
||||
assert.equal(removed.status, 200);
|
||||
|
||||
const list = await GET();
|
||||
const body = (await readJson(list)) as { projects: unknown[] };
|
||||
assert.deepEqual(body.projects, []);
|
||||
});
|
||||
});
|
||||
86
tests/lib/registry.test.ts
Normal file
86
tests/lib/registry.test.ts
Normal file
|
|
@ -0,0 +1,86 @@
|
|||
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);
|
||||
});
|
||||
});
|
||||
Loading…
Add table
Add a link
Reference in a new issue