mirror of
https://github.com/Ark0N/Codeman.git
synced 2026-10-09 16:59:43 +02:00
fix(cases): custom-folder create landing fixes (#535)
- Route test hygiene: each test works in its own mkdtemp folder, every deletion goes through safeRmHomeTree, and the suite refuses to start outside test/setup.ts's temp HOME, so a raw `npx vitest` can no longer delete a real ~/projects or the live linked-cases registry. - Path policy: the symlink-resolved target is also judged against the resolved home, data dir and system roots (home reached through a link, macOS /etc -> /private/etc); test expectations are realpath-safe. - Refuse a target equal to or inside the caller's or the shared cases directory, pointing at plain Create New (it would list twice, and deleting the local copy removes files). - The registry re-read comment no longer claims to prevent the lost-update race; documented as narrowing it, like /api/cases/link. - UI: the success toast names the folder the server created, the "under ~/codeman-cases" blurb and name hint change while a custom folder is ticked, a "/" parent previews and sends /<name> instead of an empty path, and the new labels have zh-CN entries. Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
This commit is contained in:
@@ -446,7 +446,8 @@ export function registerCaseRoutes(app: FastifyInstance, ctx: EventPort & Config
|
||||
req: FastifyRequest,
|
||||
reply: { code: (n: number) => unknown }
|
||||
): Promise<ApiResponse<{ case: { name: string; path: string } }>> {
|
||||
if (existsSync(join(resolveCasesDir(getAuthUser(req)), name))) {
|
||||
const ownCasesDir = resolveCasesDir(getAuthUser(req));
|
||||
if (existsSync(join(ownCasesDir, name))) {
|
||||
reply.code(409);
|
||||
return createErrorResponse(ApiErrorCode.ALREADY_EXISTS, 'A case with this name already exists in codeman-cases.');
|
||||
}
|
||||
@@ -459,7 +460,9 @@ export function registerCaseRoutes(app: FastifyInstance, ctx: EventPort & Config
|
||||
);
|
||||
}
|
||||
|
||||
const prepared = await prepareNewCasePath(customPath, { home: homedir(), dataDir: getDataDir() });
|
||||
// The caller's own cases dir and the shared one (the same folder outside multi-user mode).
|
||||
const casesDirs = [...new Set([ownCasesDir, resolveCasesDir()])];
|
||||
const prepared = await prepareNewCasePath(customPath, { home: homedir(), dataDir: getDataDir(), casesDirs });
|
||||
if (!prepared.ok) {
|
||||
const status = prepared.code === 'NOT_FOUND' ? 404 : prepared.code === 'EXISTS' ? 409 : 400;
|
||||
reply.code(status);
|
||||
@@ -494,7 +497,9 @@ export function registerCaseRoutes(app: FastifyInstance, ctx: EventPort & Config
|
||||
|
||||
const codemanDir = getDataDir();
|
||||
if (!existsSync(codemanDir)) mkdirSync(codemanDir, { recursive: true });
|
||||
// Re-read right before writing: another request may have linked a case since the check above.
|
||||
// Re-read right before writing, so a case linked since the check above is not dropped. This only
|
||||
// narrows the window: like POST /api/cases/link, the registry write is not serialized, and two
|
||||
// requests that both read before either writes can still lose one entry.
|
||||
const fresh = await readLinkedCases();
|
||||
if (fresh[name]) throw Object.assign(new Error(`Case "${name}" was just linked`), { conflict: true });
|
||||
fresh[name] = casePath;
|
||||
|
||||
Reference in New Issue
Block a user