Merge pull request #567: fix(cases): only refuse the cases folder and its direct children for a custom-folder case

Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
This commit is contained in:
Codeman maintainer committed 2026-10-18 08:16:42 +02:00
commit 7504a2bc8b
5 files changed
+50 -6

No files matched your search

+16 -1
View File
@@ -54,14 +54,29 @@ describe('blockedReason', () => {
expect(blockedReason('/usrlocal', c)).toBeNull();
});
it('refuses a cases directory and anything inside it, when given', () => {
it('refuses a cases directory and its direct children, when given', () => {
const withCases = { ...c, casesDirs: ['/home/u/codeman-cases'] };
expect(blockedReason('/home/u/codeman-cases', withCases)).toMatch(/plain Create New/);
expect(blockedReason('/home/u/codeman-cases/foo', withCases)).toMatch(/plain Create New/);
expect(blockedReason('/home/u/codeman-cases/foo', withCases)).toContain('/home/u/codeman-cases');
// Two messages: the cases folder is not "in" itself, and a child that does not exist yet is not
// "already" listed.
expect(blockedReason('/home/u/codeman-cases', withCases)).toMatch(/cases folder itself/);
expect(blockedReason('/home/u/codeman-cases/foo', withCases)).toMatch(/directly in the cases folder/);
expect(blockedReason('/home/u/codeman-cases/foo', withCases)).not.toMatch(/already/);
expect(blockedReason('/home/u/codeman-cases-old/foo', withCases)).toBeNull();
expect(blockedReason('/home/u/codeman-cases/foo', c)).toBeNull();
});
it('allows a folder deeper than a direct child of the cases directory (it is not listed as a local case)', () => {
// CODEMAN_CASES_PATH can be a broad root such as /mnt/user/Scripts; GET /api/cases lists only its
// direct children, so a folder further down can be linked without appearing twice.
const broad = { ...c, casesDirs: ['/mnt/user/Scripts'] };
expect(blockedReason('/mnt/user/Scripts/GitHub', broad)).toMatch(/plain Create New/);
expect(blockedReason('/mnt/user/Scripts/GitHub/opticon454', broad)).toBeNull();
expect(blockedReason('/mnt/user/Scripts/GitHub/opticon454/app', broad)).toBeNull();
});
it('judges against the system roots it is given', () => {
expect(blockedReason('/private/etc/x', c)).toBeNull();
expect(blockedReason('/private/etc/x', c, ['/private/etc'])).toMatch(/system directory/);
@@ -183,6 +183,21 @@ describe('POST /api/cases with a custom path', () => {
}
});
it('creates and links a folder two levels under the cases directory (not listed as a local case)', async () => {
const { app } = await createRouteTestHarness(registerCaseRoutes);
mkdirSync(join(CASES_DIR(), 'parent-dir'), { recursive: true });
try {
const target = join(realpathSync(join(CASES_DIR(), 'parent-dir')), 'nested');
const res = await create(app, { name: 'nested', path: target });
expect(res.statusCode).toBe(200);
expect(res.json().data.case).toEqual({ name: 'nested', path: target });
expect(linked()).toEqual({ nested: target });
expect(existsSync(join(target, 'CLAUDE.md'))).toBe(true);
} finally {
safeRmHomeTree(join(CASES_DIR(), 'parent-dir'));
}
});
it('multi-user: a non-admin is refused (403) and nothing is created; an admin is allowed', async () => {
process.env.CODEMAN_MULTIUSER = '1';
const user = await createRouteTestHarness(registerCaseRoutes, { authUser: { username: 'bob', role: 'user' } });