fix(rooms): keep rooms routable when mode_source is corrupt

A room_settings row with a valid room_mode but an unrecognized mode_source
(e.g. the stray 'room' value that disabled the cgv-macro channel) was
silently dropped by getStoredRoomSettingsRowFromDatabase. That removed the
room from every binding lookup, so the router ignored the channel, it
vanished from the status list, and re-registration wedged on the
UNIQUE(chat_jid) constraint because assignRoom's "existing" probe uses the
same loader.

Coerce an invalid mode_source to 'explicit' (preserving the stored
room_mode) and log a warning so the corruption stays visible and self-heals
on the next assignRoom, instead of taking the channel silently offline.
room_mode is already protected by a column CHECK; mode_source was not.

Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
This commit is contained in:
Codex
2026-08-18 15:06:46 +09:00
parent 44c54a759c
commit 3930c21625
2 changed files with 96 additions and 2 deletions

View File

@@ -0,0 +1,79 @@
import { describe, expect, it } from 'vitest';
import { openInitializedInMemoryDatabase } from './database-lifecycle.js';
import {
getStoredRoomRowsFromDatabase,
getStoredRoomSettingsRowFromDatabase,
insertStoredRoomSettings,
} from './room-registration.js';
// Regression guard for the cgv-macro incident: a room_settings row that carried
// an invalid mode_source ('room') was silently dropped from every binding
// lookup, so the router ignored the channel, it vanished from the status list,
// and re-registration wedged on the UNIQUE(chat_jid) constraint. The loader must
// keep such a room routable by coercing the bad source to a valid one.
describe('getStoredRoomSettingsRowFromDatabase mode_source resilience', () => {
const jid = 'dc:invalid-source-room';
const snapshot = {
name: 'cgv-macro',
folder: 'cgv-macro',
triggerPattern: '',
requiresTrigger: false,
isMain: false,
ownerAgentType: 'claude-code' as const,
workDir: null,
};
function seedWithSource(source: string) {
const db = openInitializedInMemoryDatabase();
insertStoredRoomSettings(db, jid, 'single', 'explicit', snapshot);
db.prepare('UPDATE room_settings SET mode_source = ? WHERE chat_jid = ?').run(
source,
jid,
);
return db;
}
it('coerces an unrecognized mode_source to explicit instead of dropping the room', () => {
const db = seedWithSource('room');
const stored = getStoredRoomSettingsRowFromDatabase(db, jid);
expect(stored).toBeDefined();
expect(stored?.roomMode).toBe('single');
expect(stored?.modeSource).toBe('explicit');
expect(stored?.folder).toBe('cgv-macro');
// And it stays visible in the full-room listing that feeds getAllRoomBindings.
const listed = getStoredRoomRowsFromDatabase(db).map((r) => r.chatJid);
expect(listed).toContain(jid);
});
it('room_mode is guarded by a schema CHECK, but mode_source is not — hence the loader coercion', () => {
const db = openInitializedInMemoryDatabase();
insertStoredRoomSettings(db, jid, 'single', 'explicit', snapshot);
// room_mode cannot be corrupted at all: the column CHECK rejects the write.
expect(() =>
db
.prepare('UPDATE room_settings SET room_mode = ? WHERE chat_jid = ?')
.run('bogus', jid),
).toThrow(/CHECK constraint/);
// mode_source has no such CHECK, so a bad value persists — which is exactly
// the corruption the loader now tolerates instead of dropping the room.
db.prepare('UPDATE room_settings SET mode_source = ? WHERE chat_jid = ?').run(
'room',
jid,
);
expect(getStoredRoomSettingsRowFromDatabase(db, jid)?.roomMode).toBe('single');
});
it('preserves a valid mode_source unchanged', () => {
const db = openInitializedInMemoryDatabase();
insertStoredRoomSettings(db, jid, 'tribunal', 'inferred', snapshot);
expect(getStoredRoomSettingsRowFromDatabase(db, jid)?.modeSource).toBe(
'inferred',
);
});
});

View File

@@ -182,8 +182,23 @@ export function getStoredRoomSettingsRowFromDatabase(
row?.room_mode === 'single' || row?.room_mode === 'tribunal' row?.room_mode === 'single' || row?.room_mode === 'tribunal'
? row.room_mode ? row.room_mode
: undefined; : undefined;
const source = normalizeRoomModeSource(row?.mode_source); if (!row || !roomMode) return undefined;
if (!row || !roomMode || !source) return undefined;
// A row with a valid room_mode but an unrecognized mode_source must NOT be
// silently dropped. Dropping it removes the room from every binding lookup,
// so the router ignores the channel, it disappears from the status list, and
// re-registration wedges on the UNIQUE(chat_jid) constraint (assignRoom's
// "existing" probe also relies on this function). Coerce the source to
// 'explicit' — which preserves the stored room_mode as-is — and warn so the
// underlying corruption stays visible and self-heals on the next assignRoom.
let source = normalizeRoomModeSource(row.mode_source);
if (!source) {
logger.warn(
{ jid: chatJid, modeSource: row.mode_source ?? null },
'room_settings row has invalid mode_source; coercing to explicit to keep room routable',
);
source = 'explicit';
}
return { return {
chatJid, chatJid,