Files

689 lines
34 KiB
TypeScript
Raw Permalink Normal View History

/**
* Which model a session runs (`SessionState.displayModel`), for the tile grid's and the
* split pane's headers: the pure resolver, the screen read with each CLI's registry
* pattern, and the session that feeds them.
*
* The pane fixtures are verbatim `capture-pane -p` rows (trailing blanks trimmed) from
* live panes on 2026-10-07: dsh-TUI 0.10.0-beta.1 on the owner's qwen route, and codex
* 0.147.0. The codex 0.154.0 footer is the one `session-watching.test.ts` pins.
* The opencode 1.3.0 composer rows are from 2026-10-09 captures.
*/
import { mkdirSync, mkdtempSync, rmSync, writeFileSync } from 'node:fs';
import { tmpdir } from 'node:os';
import { join } from 'node:path';
import { afterEach, describe, expect, it, vi } from 'vitest';
import { Session } from '../src/session.js';
import { getCli } from '../src/config/cli-registry/index.js';
import { compileVersionRegex, countCaptureGroups } from '../src/config/cli-registry/patterns.js';
import { CliEntrySchema } from '../src/config/cli-registry/schema.js';
import {
MAX_DISPLAY_MODEL_CHARS,
launchModelFor,
readScreenModel,
resolveDisplayModel,
restoredReportedModel,
sanitizeModelName,
} from '../src/session-display-model.js';
import { IDLE_SILENCE_MS } from '../src/session-activity.js';
import { CODEX_REASONING_EFFORTS } from '../src/types/session.js';
const detectOf = (mode: string) => getCli(mode)!.capabilities.modelDetect!;
const DSH = compileVersionRegex(detectOf('deepseek').screenLine)!;
const DSH_ROWS = detectOf('deepseek').screenLines;
const DSH_REJECT = detectOf('deepseek').rejectWords;
const CODEX = compileVersionRegex(detectOf('codex').screenLine)!;
const CODEX_ROWS = detectOf('codex').screenLines;
const PI = compileVersionRegex(detectOf('pi').screenLine)!;
const PI_ROWS = detectOf('pi').screenLines;
const PI_REJECT = detectOf('pi').rejectWords;
const DSH_BORDER_TOP = '╭' + '─'.repeat(95) + '╮';
const DSH_BORDER_BOTTOM = '╰' + '─'.repeat(95) + '╯';
const DSH_COMPOSER = '❯' + ' '.repeat(95) + '⛶';
/** The foot of a dsh-TUI pane: transcript, the composer's rounded box, the status line. */
function dshPane(statusLine: string | null, transcript: string[] = []): string {
return [
' Tip: Footer compact on = one merged line; off = metric…',
' Explore the uncharted!',
' ▶ (Ctrl+P to expand) Context loaded · System prompt 16 sections · Runtime context 2 items · …',
...transcript,
DSH_BORDER_TOP,
DSH_COMPOSER,
DSH_BORDER_BOTTOM,
...(statusLine === null ? [] : [statusLine]),
'',
].join('\n');
}
const DSH_LIVE = dshPane(' qwen3.8-27b · medium · th-scratch');
/**
* The foot of a codex pane: transcript, composer, then the status line, and from 0.162.0
* on a hint row under it at rest (`hint`; absent while a prompt is being typed).
*/
function codexPane(statusLine: string | null, transcript: string[] = [], hint: string | null = null): string {
return [
'│ directory: ~/codeman-cases/th-scratch │',
'│ permissions: YOLO mode │',
'╰───────────────────────────────────────────────╯',
' Tip: New For a limited time, Codex is included in your',
' plan for free – let’s build together.',
...transcript,
'› Explain this codebase',
'',
...(statusLine === null ? [] : [statusLine]),
...(hint === null ? [] : [hint]),
'',
].join('\n');
}
const CODEX_LIVE = codexPane(' gpt-5.6-terra default · ~/codeman-cases/th-scratch');
const CODEX_154 = codexPane(
' gpt-5.6-sol medium · Context 98% left · ~/codeman-cases/codex-probe · 5h 99% left · weekly 94% left'
);
/** 0.162.0 at rest, verbatim from the 1.36.0 beta pane whose tile showed no model. */
const CODEX_162 = codexPane(' GPT-6-Luna default · ~/codeman-cases/testcase', [], ' ← for agents · ? for shortcuts');
describe('the registry patterns', () => {
it('compile through compileVersionRegex() with exactly one capture group', () => {
for (const mode of ['deepseek', 'codex', 'pi']) {
const { screenLine } = detectOf(mode);
expect(compileVersionRegex(screenLine), mode).not.toBeNull();
expect(countCaptureGroups(screenLine), mode).toBe(1);
}
});
it('are declared only where a footer was measured (claude reports through its statusline)', () => {
expect(getCli('claude')!.capabilities.modelDetect).toBeUndefined();
expect(getCli('shell')!.capabilities.modelDetect).toBeUndefined();
});
it('the schema refuses a pattern with no capture group, two of them, or one it will not run', () => {
const codex = getCli('codex')!;
const withDetect = (modelDetect: unknown) =>
CliEntrySchema.safeParse({ ...codex, capabilities: { ...codex.capabilities, modelDetect } }).success;
expect(withDetect({ screenLine: '^ {2}([a-z]+) · ' })).toBe(true);
expect(withDetect({ screenLine: '^ {2}[a-z]+ · ' })).toBe(false);
expect(withDetect({ screenLine: '^ {2}([a-z]+) (high) · ' })).toBe(false);
expect(withDetect({ screenLine: '(a+)+$' })).toBe(false);
expect(withDetect({ screenLine: '^(x)', screenLines: 9 })).toBe(false);
});
it("dsh declares what its footer's first field can be when it is not the model", () => {
// Every effort id dsh's adapters offer, and the shipped mode ids.
expect(DSH_REJECT).toEqual(
expect.arrayContaining(['off', 'minimal', 'low', 'medium', 'high', 'xhigh', 'max', 'default', 'plan', 'full'])
);
expect(detectOf('codex').rejectWords).toBeUndefined();
});
it('the schema takes single-token reject words with a screenLine only, and bounds them', () => {
const codex = getCli('codex')!;
const withDetect = (modelDetect: unknown) =>
CliEntrySchema.safeParse({ ...codex, capabilities: { ...codex.capabilities, modelDetect } }).success;
const screenLine = '^ {2}([a-z]+) · ';
expect(withDetect({ screenLine, rejectWords: ['medium'] })).toBe(true);
expect(withDetect({ configResolver: 'deepseek-route', rejectWords: ['medium'] })).toBe(false);
expect(withDetect({ screenLine, rejectWords: ['two words'] })).toBe(false);
expect(withDetect({ screenLine, rejectWords: Array.from({ length: 33 }, (_, i) => `w${i}`) })).toBe(false);
});
it('dsh also names a config reader for while its screen names no model', () => {
expect(detectOf('deepseek').configResolver).toBe('deepseek-route');
expect(detectOf('codex').configResolver).toBeUndefined();
});
it('the schema takes a known config reader alone, and refuses an empty or unknown one', () => {
const codex = getCli('codex')!;
const withDetect = (modelDetect: unknown) =>
CliEntrySchema.safeParse({ ...codex, capabilities: { ...codex.capabilities, modelDetect } }).success;
expect(withDetect({ configResolver: 'deepseek-route' })).toBe(true);
expect(withDetect({ configResolver: 'read-anything' })).toBe(false);
expect(withDetect({})).toBe(false);
expect(withDetect({ configResolver: 'deepseek-route', screenLines: 2 })).toBe(false);
});
it('countCaptureGroups counts named groups and ignores non-capturing ones', () => {
expect(countCaptureGroups('a(?:b)(?<m>c)')).toBe(1);
expect(countCaptureGroups('(a)(b)')).toBe(2);
expect(countCaptureGroups('abc')).toBe(0);
expect(countCaptureGroups('(')).toBe(-1);
});
});
/**
* The foot of a pi pane (live pi 1.1.0 capture, rules shortened): transcript, the composer
* between two `─` rules, the cwd row, then the stats row with the model on the right.
*/
function piPane(statsRow: string | null, transcript: string[] = [], below: string[] = []): string {
const rule = '─'.repeat(60);
return [
' Pi can explain its own features and look up its docs.',
...transcript,
rule,
'',
rule,
'~/codeman-cases/testcase',
...(statsRow === null ? [] : [statsRow]),
...below,
'',
].join('\n');
}
const PI_LIVE = piPane('0.8%/253k (auto)' + ' '.repeat(40) + 'qwen3.8-27b-pi • xhigh');
describe('readScreenModel', () => {
it("reads dsh's model off the row under its composer", () => {
expect(readScreenModel(DSH_LIVE, DSH, DSH_ROWS)).toBe('qwen3.8-27b');
// A footer with the model alone, and one with a row below it (the activity line).
expect(readScreenModel(dshPane(' deepseek-v4-flash'), DSH, DSH_ROWS)).toBe('deepseek-v4-flash');
expect(readScreenModel(dshPane(' qwen3.8-27b · medium · th-scratch') + ' ⠋ Thinking… 3s\n', DSH, DSH_ROWS)).toBe(
'qwen3.8-27b'
);
});
it("never reads the field after a switched-off model as the model (dsh's effort, mode, folder)", () => {
// dsh-TUI with `statusBar.model: false` (live capture: ` medium · th-config`): the
// effort id comes first, then the mode, then the folder name.
const at = (cwdBasename: string) => ({ rejectWords: DSH_REJECT, cwdBasename });
for (const effort of ['off', 'minimal', 'low', 'medium', 'high', 'xhigh', 'max']) {
expect(readScreenModel(dshPane(` ${effort} · x`), DSH, DSH_ROWS, at('x')), effort).toBeUndefined();
}
expect(readScreenModel(dshPane(' default · x'), DSH, DSH_ROWS, at('x'))).toBeUndefined();
// The words compare ignoring case (the banner capitalizes effort; the footer does not).
expect(readScreenModel(dshPane(' Medium · x'), DSH, DSH_ROWS, at('x'))).toBeUndefined();
// A mode's drawn label is two words: never one field the pattern takes.
expect(readScreenModel(dshPane(' plan mode · x'), DSH, DSH_ROWS, at('x'))).toBeUndefined();
// The session's own folder name first, with and without a digit.
expect(readScreenModel(dshPane(' th-config'), DSH, DSH_ROWS, at('th-config'))).toBeUndefined();
expect(readScreenModel(dshPane(' project2 · main'), DSH, DSH_ROWS, at('project2'))).toBeUndefined();
// A field that is none of those IS read: the same `project2` in another folder.
expect(readScreenModel(dshPane(' project2'), DSH, DSH_ROWS, at('elsewhere'))).toBe('project2');
});
it('reads the official DeepSeek ids, which carry no digit, with the model field on', () => {
const at = { rejectWords: DSH_REJECT, cwdBasename: 'th-config' };
expect(readScreenModel(dshPane(' deepseek-chat · max · th-config'), DSH, DSH_ROWS, at)).toBe('deepseek-chat');
expect(readScreenModel(dshPane(' deepseek-reasoner · high · th-config'), DSH, DSH_ROWS, at)).toBe(
'deepseek-reasoner'
);
// The live qwen footer still reads.
expect(readScreenModel(dshPane(' qwen3.8-27b · medium · th-scratch'), DSH, DSH_ROWS, at)).toBe('qwen3.8-27b');
});
it("reads codex's model off its status line (0.147.0 and 0.154.0 layouts)", () => {
expect(readScreenModel(CODEX_LIVE, CODEX, CODEX_ROWS)).toBe('gpt-5.6-terra');
expect(readScreenModel(CODEX_154, CODEX, CODEX_ROWS)).toBe('gpt-5.6-sol');
});
it("reads codex's model above the 0.162.0 hint row, and with the hint gone while typing", () => {
expect(readScreenModel(CODEX_162, CODEX, CODEX_ROWS)).toBe('GPT-6-Luna');
const shortHint = codexPane(' GPT-6-Luna default · ~/codeman-cases/testcase', [], ' ? for shortcuts');
expect(readScreenModel(shortHint, CODEX, CODEX_ROWS)).toBe('GPT-6-Luna');
// Typing hides the hint, which puts the footer back on the last row.
const typing = codexPane(' GPT-6-Luna default · ~/codeman-cases/testcase');
expect(readScreenModel(typing, CODEX, CODEX_ROWS)).toBe('GPT-6-Luna');
// Neither hint row is ever read as a model.
expect(readScreenModel(codexPane(null, [], ' ← for agents · ? for shortcuts'), CODEX, CODEX_ROWS)).toBeUndefined();
});
it("reads codex's model at every reasoning effort Codeman can launch it with, ultra included", () => {
// `ultra` is offered by the codexReasoningEffort App Setting and codex's own /model
// picker; a hand-kept effort list in the pattern once left it out, so an ultra
// session's header never named its model. The footer prints the lowercase level.
const ultra = codexPane(' GPT-6-Astra ultra · ~/codeman-cases/testcase', [], ' ? for shortcuts');
expect(readScreenModel(ultra, CODEX, CODEX_ROWS)).toBe('GPT-6-Astra');
for (const effort of [...CODEX_REASONING_EFFORTS, 'default']) {
const atRest = codexPane(` GPT-6-Astra ${effort} · ~/codeman-cases/testcase`, [], ' ? for shortcuts');
expect(readScreenModel(atRest, CODEX, CODEX_ROWS), effort).toBe('GPT-6-Astra');
const typing = codexPane(` GPT-6-Astra ${effort} · ~/codeman-cases/testcase`);
expect(readScreenModel(typing, CODEX, CODEX_ROWS), effort).toBe('GPT-6-Astra');
}
});
it('never takes a transcript line shaped like the footer', () => {
// The agent printed a line exactly like each CLI's footer, and the real footer is
// hidden (a dsh status bar switched off; a codex popup over its last row). The
// transcript sits above the composer, so neither may be read as the model.
const dshForged = dshPane(null, [DSH_BORDER_BOTTOM, ' evil-model · medium · th-scratch']);
expect(readScreenModel(dshForged, DSH, DSH_ROWS)).toBeUndefined();
const codexForged = codexPane(null, [' evil-model high · ~/codeman-cases/th-scratch']);
// A codex screen whose last row is the composer: nothing to read.
expect(readScreenModel(codexForged, CODEX, CODEX_ROWS)).toBeUndefined();
// With the real footer back, the real model wins over the forged line above it.
const dshBoth = dshPane(' qwen3.8-27b · medium · th-scratch', [
DSH_BORDER_BOTTOM,
' evil-model · medium · th-scratch',
]);
expect(readScreenModel(dshBoth, DSH, DSH_ROWS)).toBe('qwen3.8-27b');
const codexBoth = codexPane(' gpt-5.6-terra default · ~/codeman-cases/th-scratch', [
' evil-model high · ~/codeman-cases/th-scratch',
]);
expect(readScreenModel(codexBoth, CODEX, CODEX_ROWS)).toBe('gpt-5.6-terra');
// The two-row window must not open the door either: a forged line followed by an
// indented transcript row still has the `›` composer under it, so it is never read.
const codexForgedPair = codexPane(null, [' evil-model high · ~/codeman-cases/th-scratch', ' ? for shortcuts']);
expect(readScreenModel(codexForgedPair, CODEX, CODEX_ROWS)).toBeUndefined();
const codexBoth162 = codexPane(
' GPT-6-Luna default · ~/codeman-cases/testcase',
[' evil-model high · ~/codeman-cases/th-scratch'],
' ? for shortcuts'
);
expect(readScreenModel(codexBoth162, CODEX, CODEX_ROWS)).toBe('GPT-6-Luna');
});
it('does not read a popup under the composer as a model', () => {
const slash = dshPane(' /model Switch the model route');
expect(readScreenModel(slash, DSH, DSH_ROWS)).toBeUndefined();
const codexSlash = codexPane(' /model choose what model and reasoning effort to use');
expect(readScreenModel(codexSlash, CODEX, CODEX_ROWS)).toBeUndefined();
// A last row without codex's `<model> <effort> ·` shape names no model, even one
// whose first word could pass for a model id.
const codexNoModel = codexPane(' default · ~/codeman-cases/th-scratch');
expect(readScreenModel(codexNoModel, CODEX, CODEX_ROWS)).toBeUndefined();
});
it("reads pi's model off its footer stats row, in every shape pi's footer code draws", () => {
const read = (row: string, below: string[] = []) =>
readScreenModel(piPane(row, [], below), PI, PI_ROWS, { rejectWords: PI_REJECT, cwdBasename: 'testcase' });
expect(readScreenModel(PI_LIVE, PI, PI_ROWS)).toBe('qwen3.8-27b-pi');
// More than one provider configured: `(provider)` in front of the model.
expect(read('↑12k ↓3.4k $0.123 12.3%/200k (auto) (anthropic) claude-sonnet-4-5 • medium')).toBe(
'claude-sonnet-4-5'
);
expect(read('?/128k gpt-5 • thinking off')).toBe('gpt-5');
// A routed model: the selected one is named first.
expect(read('3.0%/1.0M (auto) auto • high → qwen/qwen3-coder • low')).toBe('auto');
// A model without reasoning ends the row.
expect(read('0.8%/253k (auto) llama3.3:70b')).toBe('llama3.3:70b');
// An extension's status row under the stats row.
expect(read('0.8%/253k (auto) • xp qwen3.8-27b-pi • xhigh', ['my-ext: ready'])).toBe('qwen3.8-27b-pi');
// pi's placeholder when no model is selected.
expect(read('0.8%/253k (auto) no-model')).toBeUndefined();
});
it('never reads a model pi truncated to fit a narrow pane', () => {
// pi cuts the right side with no ellipsis and leaves exactly two spaces in front.
const read = (row: string) => readScreenModel(piPane(row), PI, PI_ROWS);
expect(read('↑12k ↓3.4k R45k $0.123 12.3%/253k (auto) qwen3.8-27b-pi •')).toBe('qwen3.8-27b-pi');
expect(read('↑12k ↓3.4k R45k $0.123 12.3%/253k (auto) qwen3.8-2')).toBeUndefined();
expect(read('0.8%/253k (auto) llama3.3:70')).toBeUndefined();
});
it('never takes a pi transcript line shaped like the footer', () => {
const forged = piPane(null, ['0.8%/253k (auto) evil-model • high']);
expect(readScreenModel(forged, PI, PI_ROWS)).toBeUndefined();
});
it('says nothing about an empty or unreadable frame', () => {
expect(readScreenModel('', DSH, DSH_ROWS)).toBeUndefined();
expect(readScreenModel(null, CODEX, CODEX_ROWS)).toBeUndefined();
});
});
/**
* opencode 1.3.0's composer agent row, verbatim (trailing blanks trimmed, bars shortened):
* the owner's home screen on the 1.36.0 beta, one with a tip, an in-session pane at 200
* columns whose sidebar shares the row, and a 40-column pane mid-turn.
*/
const OC_EDGE = '╹' + '▀'.repeat(56);
const OC_HOME = [
' ┃',
' ┃ Ask anything... "Fix a TODO in the codebase"',
' ┃',
' ┃ Build Big Pickle OpenCode Zen',
` ${OC_EDGE}`,
' ctrl+t variants tab agents ctrl+p commands',
' ~/codeman-cases/testcase 1.3.0',
'',
].join('\n');
const OC_HOME_TIP = [
' ┃ Ask anything... "Fix a TODO in the codebase"',
' ┃',
' ┃ Build Big Pickle OpenCode Zen',
` ${OC_EDGE}`,
' ctrl+t variants tab agents ctrl+p commands',
' ● Tip Use {env:VAR_NAME} syntax to reference environment variables in',
' /tmp/claude-1000/-home-arkon-default-claudeman/6dec47be/scratchpad/oc-repro 1.3.0',
'',
].join('\n');
const OC_WIDE = [
' ▣ Build · qwen3.8-27b · 11.9s' + ' '.repeat(40) + '[•] summarize',
' ┃' + ' '.repeat(157) + '/tmp/claude-1000/-home-arkon-default-',
' ┃' + ' '.repeat(157) + 'claudeman/6dec47be-0153-4e13-a229-',
' ┃ Build qwen3.8-27b Qwen 5090' + ' '.repeat(127) + 'fc335c2c76c8/scratchpad/oc-turn',
` ${OC_EDGE}`,
' '.repeat(129) + 'tab agents ctrl+p commands • OpenCode 1.3.0',
'',
].join('\n');
const OC_PHONE_MID = [
' ┃',
' ┃ Build qwen3.8-27b Qwen 5090',
' ╹' + '▀'.repeat(35),
' ⬝⬝⬝⬝⬝⬝⬝⬝ esc tab ctrl+p',
' interragents commands',
' upt',
'',
].join('\n');
const OC = compileVersionRegex(detectOf('opencode').screenLine)!;
const OC_ROWS = detectOf('opencode').screenLines;
describe("opencode's composer agent row", () => {
it('compiles through compileVersionRegex() with exactly one capture group', () => {
expect(OC).not.toBeNull();
expect(countCaptureGroups(detectOf('opencode').screenLine!)).toBe(1);
});
it('reads the model and provider opencode shows, at every width and on the home screen', () => {
expect(readScreenModel(OC_HOME, OC, OC_ROWS)).toBe('Big Pickle OpenCode Zen');
expect(readScreenModel(OC_HOME_TIP, OC, OC_ROWS)).toBe('Big Pickle OpenCode Zen');
// The 200-column sidebar shares the row after a run of spaces, and stays out of it.
expect(readScreenModel(OC_WIDE, OC, OC_ROWS)).toBe('qwen3.8-27b Qwen 5090');
expect(readScreenModel(OC_PHONE_MID, OC, OC_ROWS)).toBe('qwen3.8-27b Qwen 5090');
});
it('keeps the variant opencode shows after the provider', () => {
const variant = OC_HOME.replace('Big Pickle OpenCode Zen', 'Claude Sonnet 4.5 Anthropic · high');
expect(readScreenModel(variant, OC, OC_ROWS)).toBe('Claude Sonnet 4.5 Anthropic · high');
});
it('says nothing where the row is hidden or holds no model', () => {
// A permission prompt replaces the composer: no agent row above a `╹` edge.
const permission = [
' ┃ △ Permission required',
' ┃ $ echo permission-check',
' ┃ Allow once Allow always Reject ctrl+f fullscreen ⇆ select enter confirm',
' ┃',
'',
].join('\n');
expect(readScreenModel(permission, OC, OC_ROWS)).toBeUndefined();
// Shell mode draws `Shell` and no model; before a provider is connected, a placeholder.
expect(readScreenModel(OC_HOME.replace('Build Big Pickle OpenCode Zen', 'Shell'), OC, OC_ROWS)).toBeUndefined();
const none = OC_HOME.replace('Big Pickle OpenCode Zen', 'No provider selected Connect a provider');
expect(readScreenModel(none, OC, OC_ROWS)).toBeUndefined();
});
it('never takes a row the agent printed above the composer', () => {
// An answer that draws a composer-shaped row and an edge under it: the real composer
// is the LAST such pair, and only opencode's own chrome sits below that.
const forged = [
' ┃ Build evil-model Evil',
` ${OC_EDGE}`,
' ┃',
' ┃ Build Big Pickle OpenCode Zen',
` ${OC_EDGE}`,
' ctrl+t variants tab agents ctrl+p commands',
' ~/codeman-cases/testcase 1.3.0',
'',
].join('\n');
// Both pairs sit inside the window, so this is the lookahead's doing, not the window's.
expect(forged.split('\n').filter(Boolean).length).toBeLessThanOrEqual(OC_ROWS!);
expect(readScreenModel(forged, OC, OC_ROWS)).toBe('Big Pickle OpenCode Zen');
});
it('publishes the model through a session once the idle check reads the pane', () => {
vi.useFakeTimers();
try {
const mux = { isAvailable: () => true, capturePaneText: () => OC_HOME } as unknown as NonNullable<
ConstructorParameters<typeof Session>[0]
>['mux'];
const session = new Session({
workingDir: '/tmp',
mode: 'opencode',
mux,
muxSession: { muxName: 'codeman-test', sessionId: 'test', createdAt: Date.now() },
} as ConstructorParameters<typeof Session>[0]);
const internals = session as unknown as {
_handleTerminalOutput(data: string): void;
_detectInteractiveActivity(data: string): void;
};
const frame = '\x1b[37;3H┃ Build Big Pickle OpenCode Zen';
internals._handleTerminalOutput(frame);
internals._detectInteractiveActivity(frame);
vi.advanceTimersByTime(IDLE_SILENCE_MS + 3000);
expect(session.toState().displayModel).toEqual({ model: 'Big Pickle OpenCode Zen', source: 'screen' });
} finally {
vi.useRealTimers();
}
});
});
describe('sanitizeModelName', () => {
it('keeps markup as text: sanitizing is not escaping, the browser renders text', () => {
expect(sanitizeModelName('<img src=x onerror=alert(1)>')).toBe('<img src=x onerror=alert(1)>');
});
it('drops escape sequences and control characters, and collapses whitespace', () => {
expect(sanitizeModelName('\x1b[31mOpus\x1b[0m\t 4.8‮\n')).toBe('Opus 4.8');
expect(sanitizeModelName('\x00\x07')).toBeUndefined();
expect(sanitizeModelName(' ')).toBeUndefined();
expect(sanitizeModelName(42)).toBeUndefined();
});
it(`caps the name at ${MAX_DISPLAY_MODEL_CHARS} characters`, () => {
expect(sanitizeModelName('m'.repeat(500))).toHaveLength(MAX_DISPLAY_MODEL_CHARS);
});
});
describe('resolveDisplayModel', () => {
const reported = { model: 'Sonnet 4.6', source: 'statusline' as const };
it('the custom endpoint wins, then the newest report, then the launch model', () => {
expect(resolveDisplayModel({ customModelId: 'qwen3.8-27b', reported, launchModel: 'opus' })).toEqual({
model: 'qwen3.8-27b',
source: 'custom-endpoint',
});
expect(resolveDisplayModel({ reported, launchModel: 'opus' })).toEqual(reported);
expect(resolveDisplayModel({ launchModel: 'opus' })).toEqual({ model: 'opus', source: 'launch' });
});
it("the config ranks below the CLI's own report and above the launch model", () => {
expect(resolveDisplayModel({ reported, configModel: 'qwen3.8-27b', launchModel: 'opus' })).toEqual(reported);
expect(resolveDisplayModel({ configModel: 'qwen3.8-27b', launchModel: 'opus' })).toEqual({
model: 'qwen3.8-27b',
source: 'config',
});
expect(resolveDisplayModel({ customModelId: 'm', configModel: 'qwen3.8-27b' })?.source).toBe('custom-endpoint');
expect(resolveDisplayModel({ configModel: null, launchModel: 'opus' })?.source).toBe('launch');
});
it('knows nothing when nothing is known: no placeholder', () => {
expect(resolveDisplayModel({})).toBeUndefined();
expect(resolveDisplayModel({ customModelId: ' ', reported: null, launchModel: '' })).toBeUndefined();
});
});
describe('restoredReportedModel', () => {
it('restores what the CLI reported, never a derived answer', () => {
expect(restoredReportedModel({ model: 'qwen3.8-27b', source: 'screen' })).toEqual({
model: 'qwen3.8-27b',
source: 'screen',
});
expect(restoredReportedModel({ model: 'Opus 4.8', source: 'statusline' })?.source).toBe('statusline');
expect(restoredReportedModel({ model: 'opus', source: 'launch' })).toBeUndefined();
expect(restoredReportedModel({ model: 'qwen3.8-27b', source: 'config' })).toBeUndefined();
expect(restoredReportedModel({ model: 'x', source: 'custom-endpoint' })).toBeUndefined();
expect(restoredReportedModel({ model: '', source: 'screen' })).toBeUndefined();
expect(restoredReportedModel('screen')).toBeUndefined();
expect(restoredReportedModel(undefined)).toBeUndefined();
});
});
describe('launchModelFor', () => {
it("reads the model param where each CLI's registry entry keeps it", () => {
expect(launchModelFor('claude', { model: 'haiku' })).toBe('haiku');
expect(launchModelFor('codex', { codexConfig: { model: 'gpt-5.5' } })).toBe('gpt-5.5');
expect(launchModelFor('grok', { grokConfig: { model: 'grok-code-fast' } })).toBe('grok-code-fast');
// Another CLI's config, or a top-level model, is not this CLI's launch model.
expect(launchModelFor('codex', { model: 'opus', grokConfig: { model: 'x' } })).toBeUndefined();
});
it('has none for a CLI whose model is not a launch param', () => {
expect(launchModelFor('deepseek', { model: 'opus', deepSeekConfig: { profile: 'dsh-tui' } })).toBeUndefined();
expect(launchModelFor('shell', { model: 'opus' })).toBeUndefined();
expect(launchModelFor('no-such-cli', { model: 'opus' })).toBeUndefined();
});
});
describe('a session', () => {
afterEach(() => {
vi.useRealTimers();
});
type Internals = {
_handleTerminalOutput(data: string): void;
_detectInteractiveActivity(data: string): void;
_customModel?: { endpointId: string; modelId: string; envKeys: string[] };
};
function withFakePane(mode: string, screen: () => string | null, extra: Record<string, unknown> = {}): Session {
const mux = { isAvailable: () => true, capturePaneText: () => screen() } as unknown as NonNullable<
ConstructorParameters<typeof Session>[0]
>['mux'];
return new Session({
workingDir: '/tmp',
mode,
mux,
muxSession: { muxName: 'codeman-test', sessionId: 'test', createdAt: Date.now() },
...extra,
} as ConstructorParameters<typeof Session>[0]);
}
/** A composer repaint (arms the idle confirmation), then quiet: the probe reads the screen. */
function settle(session: Session, glyph: string): void {
const internals = session as unknown as Internals;
for (let i = 0; i < 3; i++) {
const frame = `\x1b[31;1H${glyph}\xa0`;
internals._handleTerminalOutput(frame);
internals._detectInteractiveActivity(frame);
vi.advanceTimersByTime(1000);
}
vi.advanceTimersByTime(IDLE_SILENCE_MS + 2000);
}
it('publishes the model its footer names, and follows the footer when it changes', () => {
vi.useFakeTimers();
let screen = DSH_LIVE;
const session = withFakePane('deepseek', () => screen);
const changed = vi.fn();
session.on('displayModelChanged', changed);
expect(session.toState().displayModel).toBeUndefined();
settle(session, '❯');
expect(session.toState().displayModel).toEqual({ model: 'qwen3.8-27b', source: 'screen' });
expect(changed).toHaveBeenCalledTimes(1);
// The same footer again: nothing new to say.
settle(session, '❯');
expect(changed).toHaveBeenCalledTimes(1);
// An in-session switch redraws the footer.
screen = dshPane(' deepseek-v4-flash · high · th-scratch');
settle(session, '❯');
expect(session.toState().displayModel).toEqual({ model: 'deepseek-v4-flash', source: 'screen' });
expect(changed).toHaveBeenCalledTimes(2);
});
it('publishes the model a pi footer names once the pane settles', () => {
vi.useFakeTimers();
const session = withFakePane('pi', () => PI_LIVE);
expect(session.toState().displayModel).toBeUndefined();
// pi's composer rule is the glyph that arms the confirmation, whose probe reads the footer.
settle(session, '─');
expect(session.toState().displayModel).toEqual({ model: 'qwen3.8-27b-pi', source: 'screen' });
});
it("a footer field equal to the session's folder is not its model; the official ids are", () => {
vi.useFakeTimers();
let screen = dshPane(' th-config');
const session = withFakePane('deepseek', () => screen, { workingDir: '/w/th-config' });
settle(session, '❯');
expect(session.toState().displayModel).toBeUndefined();
screen = dshPane(' medium · th-config');
settle(session, '❯');
expect(session.toState().displayModel).toBeUndefined();
screen = dshPane(' deepseek-chat · max · th-config');
settle(session, '❯');
expect(session.toState().displayModel).toEqual({ model: 'deepseek-chat', source: 'screen' });
});
it('keeps the last model when the footer cannot be read', () => {
vi.useFakeTimers();
let screen: string | null = CODEX_LIVE;
const session = withFakePane('codex', () => screen);
settle(session, '›');
expect(session.toState().displayModel?.model).toBe('gpt-5.6-terra');
screen = codexPane(' /model choose what model and reasoning effort to use');
settle(session, '›');
screen = null;
settle(session, '›');
expect(session.toState().displayModel).toEqual({ model: 'gpt-5.6-terra', source: 'screen' });
});
it('a CLI without a footer pattern never reads one (a claude pane says nothing)', () => {
vi.useFakeTimers();
const session = withFakePane('claude', () => '❯ \n Haiku 4.5 in:0 out:0\n');
settle(session, '❯');
expect(session.toState().displayModel).toBeUndefined();
});
it('shows the launch model until the CLI reports, and a custom endpoint over both', () => {
const session = withFakePane('claude', () => null, { model: 'haiku' });
expect(session.toState().displayModel).toEqual({ model: 'haiku', source: 'launch' });
expect(session.noteReportedModel('statusline', 'Haiku 4.5')).toBe(true);
expect(session.toState().displayModel).toEqual({ model: 'Haiku 4.5', source: 'statusline' });
expect(session.noteReportedModel('statusline', 'Haiku 4.5')).toBe(false);
expect(session.noteReportedModel('statusline', '')).toBe(false);
(session as unknown as Internals)._customModel = { endpointId: 'e', modelId: 'qwen3.8-27b', envKeys: [] };
expect(session.toState().displayModel).toEqual({ model: 'qwen3.8-27b', source: 'custom-endpoint' });
(session as unknown as Internals)._customModel = undefined;
expect(session.toState().displayModel?.model).toBe('Haiku 4.5');
});
it("an external CLI's launch model is its own config's, never the inert top-level one", () => {
const codex = withFakePane('codex', () => null, { model: 'opus', codexConfig: { model: 'gpt-5.5' } });
expect(codex.toState().displayModel).toEqual({ model: 'gpt-5.5', source: 'launch' });
const dsh = withFakePane('deepseek', () => null, { model: 'opus', deepSeekConfig: { profile: 'dsh-tui' } });
expect(dsh.toState().displayModel).toBeUndefined();
});
it('restores a reported model after a restart, until the next report replaces it', () => {
const session = withFakePane('deepseek', () => null, {
displayModel: { model: 'qwen3.8-27b', source: 'screen' },
});
expect(session.toState().displayModel).toEqual({ model: 'qwen3.8-27b', source: 'screen' });
session.noteReportedModel('screen', 'deepseek-v4-flash');
expect(session.toState().displayModel?.model).toBe('deepseek-v4-flash');
// A launch answer from the previous run is derived again, not restored.
const claude = withFakePane('claude', () => null, { displayModel: { model: 'opus', source: 'launch' } });
expect(claude.toState().displayModel).toBeUndefined();
});
});
describe('a dsh session over a fixture dsh home (end to end, no mocks)', () => {
it('names the route its profile pins until the screen names one, then the screen', async () => {
const root = mkdtempSync(join(tmpdir(), 'dsh-e2e-'));
try {
const home = join(root, 'dsh');
mkdirSync(join(home, 'profiles', 'dsh-tui'), { recursive: true });
writeFileSync(
join(home, 'profiles', 'dsh-tui', 'package.json'),
JSON.stringify({ dsh: { profile: { bundles: ['@deepseek-ai/dsh-base', '@deepseek-harness-tui/dsh-tui'] } } })
);
writeFileSync(
join(home, 'profiles', 'dsh-tui', 'cordis.patch.yml'),
'- id: dsh-tui\n config:\n provider: qwen5090\n model: qwen3.8-27b\n'
);
const session = new Session({
workingDir: '/tmp',
mode: 'deepseek',
deepSeekConfig: { profile: 'dsh-tui' },
envOverrides: { DSH_HOME: home },
} as ConstructorParameters<typeof Session>[0]);
await (session as unknown as { _withPaneLifecycle(op: () => Promise<void>): Promise<void> })._withPaneLifecycle(
async () => {}
);
await vi.waitFor(() =>
expect(session.toState().displayModel).toEqual({ model: 'qwen3.8-27b', source: 'config' })
);
session.noteReportedModel('screen', 'deepseek-v4-flash');
expect(session.toState().displayModel).toEqual({ model: 'deepseek-v4-flash', source: 'screen' });
} finally {
rmSync(root, { recursive: true, force: true });
}
});
});