Files
Codeman/test/session-settings-recovery.test.ts
T
Devvyn 79fd2dc8e4 fix(sessions): restore a session's colour, pin and image watcher after a restart
restoreMuxSessions() re-applied name, auto-clear, auto-compact, auto-resume, nice and the
flicker filter from state.json but not color, pinned/pinnedAt or imageWatcherEnabled, which
the reboot-restore path (reapplyPersistedSessionState) already restores. After a restart or
crash a surviving pane came back uncoloured, unpinned and with its image watcher off, and the
next persist wrote that over the saved values. Found by killing -9 a server with a coloured,
pinned shell session and restarting it.
2026-10-10 13:50:05 +08:00

87 lines
3.7 KiB
TypeScript

/**
* @fileoverview The settings a user puts on a session survive a Codeman restart.
*
* `restoreMuxSessions()` (server.ts) rebuilds a Session around a surviving tmux pane from
* `state.json`, and `reapplyPersistedSessionState()` does the same for a reboot restore. A
* setting one of them forgets is silently reset on the next persist, which writes `toState()`
* wholesale. Found live: after a restart a session came back with its name, auto-clear and
* auto-compact but with its tab colour, pin and image watcher gone.
*
* `restoreMuxSessions()` cannot be reached under vitest (see test/session-model-recovery.test.ts),
* so the wiring is pinned by a source check and the setters it relies on are driven for real.
*/
import { readFileSync } from 'node:fs';
import { join } from 'node:path';
import { fileURLToPath } from 'node:url';
import { describe, expect, it } from 'vitest';
import { Session } from '../src/session.js';
const SRC = fileURLToPath(new URL('../src', import.meta.url));
const server = readFileSync(join(SRC, 'web', 'server.ts'), 'utf-8');
function functionBody(name: string): string {
const start = server.indexOf(`async ${name}(`);
expect(start, `${name} not found`).toBeGreaterThan(-1);
const next = server.indexOf('\n /**', start);
return server.slice(start, next === -1 ? undefined : next);
}
describe('restoreMuxSessions() hands back every persisted user setting', () => {
const body = functionBody('restoreMuxSessions');
it.each([
['name', /name:\s*sessionName/],
['name source', /nameSource:\s*savedState\?\.nameSource/],
['colour', /session\.setColor\(savedState\.color\)/],
['pin', /session\.restorePin\(true,\s*savedState\.pinnedAt\)/],
['image watcher', /session\.imageWatcherEnabled\s*=\s*savedState\.imageWatcherEnabled/],
['flicker filter', /session\.flickerFilterEnabled\s*=\s*savedState\.flickerFilterEnabled/],
['auto-clear', /session\.setAutoClear\(/],
['auto-compact', /session\.setAutoCompact\(/],
['auto-resume', /session\.restoreAutoResume\(/],
['nice priority', /session\.setNice\(/],
])('%s', (_label, pattern) => {
expect(body).toMatch(pattern);
});
it('sets the image-watcher flag before the listeners read it', () => {
expect(body.indexOf('session.imageWatcherEnabled = savedState.imageWatcherEnabled')).toBeLessThan(
body.indexOf('await this.setupSessionListeners(session)')
);
});
it('restores everything the reboot-restore path restores', () => {
const reapply = functionBody('reapplyPersistedSessionState');
// Each persisted setting that path re-applies must also be re-applied here.
for (const call of ['setColor(', 'restorePin(', 'imageWatcherEnabled =', 'flickerFilterEnabled =']) {
expect(reapply, `reapply lacks ${call}`).toContain(call);
expect(body, `restoreMuxSessions lacks ${call}`).toContain(call);
}
});
});
describe('the setters round-trip through toState()', () => {
it('keeps colour, pin, image watcher and name source', () => {
const original = new Session({ workingDir: '/tmp', mode: 'shell', name: 'custom name', nameSource: 'manual' });
original.setColor('red');
original.setPinned(true);
original.imageWatcherEnabled = true;
const state = original.toState();
const rebuilt = new Session({ workingDir: '/tmp', mode: 'shell', name: state.name, nameSource: state.nameSource });
rebuilt.setColor(state.color!);
rebuilt.restorePin(true, state.pinnedAt);
rebuilt.imageWatcherEnabled = state.imageWatcherEnabled!;
const after = rebuilt.toState();
expect(after).toMatchObject({
name: 'custom name',
nameSource: 'manual',
color: 'red',
pinned: true,
pinnedAt: state.pinnedAt,
imageWatcherEnabled: true,
});
});
});