Some checks failed
build-packages / resolve bundled mosh-client (push) Has been cancelled
build-packages / resolve bundled et-client (push) Has been cancelled
build-packages / build-macos (push) Has been cancelled
build-packages / build-windows (push) Has been cancelled
build-packages / build-linux-x64 (push) Has been cancelled
build-packages / build-linux-arm64 (push) Has been cancelled
build-packages / release (push) Has been cancelled
build-packages / update Nix release metadata (push) Has been cancelled
build-packages / bump homebrew tap (push) Has been cancelled
test / lint-and-test (push) Has been cancelled
AI automation / Route event (push) Has been cancelled
AI automation / Hand reopened issue to maintainers (push) Has been cancelled
AI automation / Clean source issue state (push) Has been cancelled
AI automation / Reconcile handoffs (push) Has been cancelled
AI automation / Classify issue (push) Has been cancelled
AI automation / Claude Code smoke (push) Has been cancelled
AI automation / Review issue follow-up (push) Has been cancelled
AI automation / Publish issue follow-up (push) Has been cancelled
AI automation / Implement with Claude Code (push) Has been cancelled
AI automation / Publish implement PR (push) Has been cancelled
AI automation / Continue queued issue comments (push) Has been cancelled
AI automation / Codex review loop (push) Has been cancelled
AI automation / Publish Codex fix (push) Has been cancelled
AI automation / Clear Codex dispatch marker (push) Has been cancelled
AI automation / Own PR re-request Codex (push) Has been cancelled
AI automation / External PR re-request Codex (push) Has been cancelled
AI automation / Poll Codex reaction / retry (push) Has been cancelled
build-et-binaries / build-linux-x64 (push) Has been cancelled
build-et-binaries / build-linux-arm64 (push) Has been cancelled
build-et-binaries / build-macos-universal (push) Has been cancelled
build-et-binaries / build-windows-x64 (push) Has been cancelled
build-et-binaries / release (push) Has been cancelled
129 lines
5.8 KiB
TypeScript
129 lines
5.8 KiB
TypeScript
import assert from 'node:assert/strict';
|
|
import test from 'node:test';
|
|
|
|
import {
|
|
retainStableSessionsIgnoringPresentation,
|
|
terminalPaneSessionsEqual,
|
|
} from '../../domain/terminalPaneSessionsEqual.ts';
|
|
import { topTabsSessionsEqual } from '../../domain/topTabsSessionsEqual.ts';
|
|
import {
|
|
applySessionPresentation,
|
|
publishSessionCodingCliProvider,
|
|
publishSessionDynamicTitle,
|
|
sessionPresentationStore,
|
|
} from './sessionPresentationStore.ts';
|
|
|
|
const session = (overrides: Record<string, unknown> = {}) => ({
|
|
id: 's1',
|
|
hostId: 'h1',
|
|
hostLabel: 'host',
|
|
username: 'root',
|
|
hostname: 'example.test',
|
|
status: 'connected' as const,
|
|
...overrides,
|
|
});
|
|
|
|
test('presentation store notifies on title and provider updates', () => {
|
|
sessionPresentationStore.clearSession('s1');
|
|
const versions: number[] = [];
|
|
const unsub = sessionPresentationStore.subscribe(() => {
|
|
versions.push(sessionPresentationStore.getVersion());
|
|
});
|
|
const base = sessionPresentationStore.getVersion();
|
|
publishSessionDynamicTitle('s1', 'Claude Code');
|
|
assert.equal(sessionPresentationStore.getPresentation('s1')?.dynamicTitle, 'Claude Code');
|
|
publishSessionCodingCliProvider('s1', 'claude');
|
|
assert.equal(sessionPresentationStore.getPresentation('s1')?.codingCliProviderId, 'claude');
|
|
assert.ok(sessionPresentationStore.getVersion() > base);
|
|
assert.ok(versions.length >= 2);
|
|
unsub();
|
|
sessionPresentationStore.clearSession('s1');
|
|
});
|
|
|
|
test('title-only session changes stay equal for TopTabs structural compare', () => {
|
|
const prev = [session({ dynamicTitle: 'old' })];
|
|
const next = [session({ dynamicTitle: 'new', codingCliProviderId: 'claude' })];
|
|
assert.equal(topTabsSessionsEqual(prev, next), true);
|
|
assert.equal(terminalPaneSessionsEqual(prev, next), true);
|
|
assert.equal(
|
|
topTabsSessionsEqual(prev, [session({ status: 'disconnected' })]),
|
|
false,
|
|
);
|
|
});
|
|
|
|
test('applySessionPresentation overlays store onto orphan-style snapshots', () => {
|
|
sessionPresentationStore.clearSession('orphan-1');
|
|
publishSessionDynamicTitle('orphan-1', 'agent: refactor');
|
|
publishSessionCodingCliProvider('orphan-1', 'claude');
|
|
const base = session({ id: 'orphan-1', dynamicTitle: 'stale', codingCliProviderId: undefined });
|
|
const merged = applySessionPresentation(base);
|
|
assert.equal(merged.dynamicTitle, 'agent: refactor');
|
|
assert.equal(merged.codingCliProviderId, 'claude');
|
|
// Same store values → retain object identity when already up to date.
|
|
assert.equal(applySessionPresentation(merged), merged);
|
|
sessionPresentationStore.clearSession('orphan-1');
|
|
});
|
|
|
|
test('retainStableSessionsIgnoringPresentation keeps array identity on title-only churn', () => {
|
|
const prev = [session({ dynamicTitle: 'old' })];
|
|
const next = [session({ dynamicTitle: 'new', codingCliProviderId: 'codex' })];
|
|
const retained = retainStableSessionsIgnoringPresentation(prev, next);
|
|
assert.equal(retained, prev);
|
|
const structural = [session({ status: 'disconnected' })];
|
|
assert.notEqual(retainStableSessionsIgnoringPresentation(prev, structural), prev);
|
|
});
|
|
|
|
test('applySessionPresentation is the shared overlay for focus sidebar and panes', () => {
|
|
sessionPresentationStore.clearSession('focus-1');
|
|
publishSessionDynamicTitle('focus-1', 'agent: search me');
|
|
const base = session({ id: 'focus-1', dynamicTitle: undefined });
|
|
const merged = applySessionPresentation(base);
|
|
assert.equal(merged.dynamicTitle, 'agent: search me');
|
|
assert.ok(merged.dynamicTitle?.toLowerCase().includes('search'));
|
|
sessionPresentationStore.clearSession('focus-1');
|
|
});
|
|
|
|
test('per-session snapshot stays stable when a sibling session title changes', () => {
|
|
sessionPresentationStore.clearSession('a');
|
|
sessionPresentationStore.clearSession('b');
|
|
publishSessionDynamicTitle('a', 'title-a');
|
|
const snapBBefore = sessionPresentationStore.getSessionSnapshot('b');
|
|
publishSessionDynamicTitle('a', 'title-a-2');
|
|
assert.equal(sessionPresentationStore.getSessionSnapshot('b'), snapBBefore);
|
|
publishSessionDynamicTitle('b', 'title-b');
|
|
assert.notEqual(sessionPresentationStore.getSessionSnapshot('b'), snapBBefore);
|
|
sessionPresentationStore.clearSession('a');
|
|
sessionPresentationStore.clearSession('b');
|
|
});
|
|
|
|
test('null presentation tombstones clear stale snapshot titles', () => {
|
|
sessionPresentationStore.clearSession('tomb-1');
|
|
// No prior store entry; first clear must still be persisted.
|
|
publishSessionDynamicTitle('tomb-1', null);
|
|
assert.equal(sessionPresentationStore.getPresentation('tomb-1')?.dynamicTitle, null);
|
|
const base = session({ id: 'tomb-1', dynamicTitle: 'stale-from-snapshot' });
|
|
const merged = applySessionPresentation(base);
|
|
assert.equal(merged.dynamicTitle, undefined);
|
|
// Idempotent second clear does not thrash.
|
|
const version = sessionPresentationStore.getVersion();
|
|
publishSessionDynamicTitle('tomb-1', null);
|
|
assert.equal(sessionPresentationStore.getVersion(), version);
|
|
sessionPresentationStore.clearSession('tomb-1');
|
|
});
|
|
|
|
test('session snapshot distinguishes missing fields from null tombstones', () => {
|
|
sessionPresentationStore.clearSession('snap-1');
|
|
sessionPresentationStore.setPresentation('snap-1', { dynamicTitle: 'live', codingCliProviderId: 'claude' });
|
|
const bothLive = sessionPresentationStore.getSessionSnapshot('snap-1');
|
|
publishSessionDynamicTitle('snap-1', null);
|
|
const titleTombstoned = sessionPresentationStore.getSessionSnapshot('snap-1');
|
|
assert.notEqual(titleTombstoned, bothLive);
|
|
// Clearing provider after title tombstone must also change the snapshot.
|
|
publishSessionCodingCliProvider('snap-1', null);
|
|
const bothTombstoned = sessionPresentationStore.getSessionSnapshot('snap-1');
|
|
assert.notEqual(bothTombstoned, titleTombstoned);
|
|
// Empty entry vs explicit dual null tombstone are distinct from missing.
|
|
assert.notEqual(bothTombstoned, '');
|
|
sessionPresentationStore.clearSession('snap-1');
|
|
});
|