[Init] Initial commit - NetMesh terminal manager
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
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
This commit is contained in:
128
application/state/sessionPresentationStore.test.ts
Normal file
128
application/state/sessionPresentationStore.test.ts
Normal file
@@ -0,0 +1,128 @@
|
||||
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');
|
||||
});
|
||||
Reference in New Issue
Block a user