fix: apply the review items for #719 at merge

- src/web/settings-writer.ts:110: export assertSettingsWritable(), the writer's own read-for-write rule run on its own
- src/web/routes/case-routes.ts:1860: an admin's rename checks settings.json before it ends any session or moves the folder, and answers 409 CONFLICT with the writer's message (placed before the session-starting check, so nothing is awaited between it and the lock)
- test/routes/case-rename-routes.test.ts:314: the torn-file pin now expects 409 CONFLICT (no stoppedSessions, like the other pre-checks)
- test/routes/case-rename-routes.test.ts:331: new test, a live session with stopSessions and a torn file: 409 CONFLICT, cleanupSession not called
- docs/api-reference.md: the Settings paragraph says an admin's rename answers this refusal as 409 CONFLICT before ending sessions
- src/hooks-config.ts:1161, src/prompt-cache-keepwarm.ts:341: comments describe the atomic writer, not an in-place rewrite

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
This commit is contained in:
Codeman maintainer committed 2026-10-27 01:24:15 +01:00
1 parent 8fa5b340c1
commit 8574e3c880
6 files changed
+57 -10

No files matched your search

+20 -3
View File
@@ -317,18 +317,35 @@ describe('POST /api/cases/:name/rename (a linked case)', () => {
const torn = '{\n "agentSkillEnabled": true\n}tail"\n}';
writeFileSync(SETTINGS_PATH, torn);
const res = await rename(app, 'alpha', { newName: 'beta' });
expect(res.statusCode).toBe(500);
expect(res.statusCode).toBe(409);
expect(res.json()).toEqual({
success: false,
errorCode: 'OPERATION_FAILED',
errorCode: 'CONFLICT',
error: `${SETTINGS_PATH} is not a valid JSON object, so this change was not saved (saving would have replaced every other setting in it). Fix or delete the file, or restore it from a config backup, then try again.`,
stoppedSessions: 0,
});
expect(existsSync(oldDir)).toBe(true);
expect(existsSync(join(CASES_DIR(), 'beta'))).toBe(false);
expect(readFileSync(SETTINGS_PATH, 'utf8')).toBe(torn);
});
it('checks settings.json before it stops any session, so none is ended for a rename that cannot complete', async () => {
const { app, ctx } = await createRouteTestHarness(registerCaseRoutes);
const oldDir = localCase('alpha');
const torn = '{\n "agentSkillEnabled": true\n}tail"\n}';
writeFileSync(SETTINGS_PATH, torn);
(ctx.sessions as Map<string, unknown>).set('s1', { id: 's1', name: 'w1', workingDir: oldDir });
const res = await rename(app, 'alpha', { newName: 'beta', stopSessions: true });
expect(res.statusCode).toBe(409);
expect(res.json().errorCode).toBe('CONFLICT');
expect(res.json().error).toContain('is not a valid JSON object');
expect(ctx.cleanupSession).not.toHaveBeenCalled();
expect(existsSync(oldDir)).toBe(true);
expect(existsSync(join(CASES_DIR(), 'beta'))).toBe(false);
expect(readFileSync(SETTINGS_PATH, 'utf8')).toBe(torn);
});
it('says where the folder is when it cannot be moved back', async () => {
const { app } = await createRouteTestHarness(registerCaseRoutes);
const real = join(work, 'my-project');