Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
4 changes: 4 additions & 0 deletions docs/performance/2026-10-26-integration.md
Original file line number Diff line number Diff line change
Expand Up @@ -46,3 +46,7 @@ Functional built-CLI PTY stress separately verified all numbered Unicode output,
## Physical QA and service lifetime

Ghostty/Zed physical QA is **NOT RUN**. Follow the [performance integration QA handoff](../testing/performance-integration-physical-qa.md). Keep the performance issues open until human results are recorded. Existing services retain the code with which they started until their sessions naturally end; do not terminate existing sessions to force an upgrade.

## Final push stabilization

The test/documentation follow-up merged as `c690de055daeb7b33ed8b5e302ef01133b3e55b4`. Its [exact dev push](https://github.com/raiseCatError/notMyShell/actions/runs/40512364215) failed the existing macOS cross-profile completion test; Fedora and both timing jobs passed. See [stabilization evidence and synchronization regression](../testing/host-profile-stabilization.md). The historical failure is not dismissed as harmless; final proposed-head CI, merged-source measurements and remaining uncertainty are recorded in the focused PR/#473 handoff. Physical Ghostty/Zed QA remains NOT RUN.
40 changes: 40 additions & 0 deletions docs/testing/host-profile-stabilization.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,40 @@
# Host-profile completion stabilization

Refs [#473](https://github.com/raiseCatError/notMyShell/issues/473). Source base: merged dev `c690de055daeb7b33ed8b5e302ef01133b3e55b4`. All five performance integration PRs are already merged; this work changes test synchronization only.

## Hosted failure and what is known

[Final integration push CI 40512364215](https://github.com/raiseCatError/notMyShell/actions/runs/40512364215) failed macOS shard 2/3 in `hostProfiles.test.ts`: `timed out waiting for completed journal for echo SIZE-$(stty size)`. Other platform shards, Node 22, Fedora and both timing jobs passed. This was a test timeout, not the earlier Node 22 workflow-budget cancellation. Identical test source on old/new dev does not establish runtime correctness.

The original test submitted a bracketed STATE command, waited only for matching screen text, then queried size. Its outer `until` awaits each `app.run` sequentially: a completed wrong-width probe can cause another submission, but exceptions escape rather than being retried. Each run waits for exact-command journal count growth. The outer deadline is checked between callbacks, not while an inner command completion wait is pending.

Two observation defects are established:

- A rendered STATE row does not acknowledge that this frontend has processed the command's completion. The helper's independent service-idle query can be ahead of frontend event delivery. The existing input-ownership rules correctly hold input entered while a command still owns the reader; such a held probe has no execution/completion record.
- A resized/redrawn viewport can repeat an older SIZE row. An observed natural trace finalized a new probe at 60 columns, then advanced to the next profile despite expecting 50. A fresh output suffix therefore did not identify which command produced the matched geometry.

A controlled completion-batch gate reproduced the exact hosted journal-timeout message on unchanged original dev `be3f6abe26a112a39f3a23c800954b8981ae5b0e`. The size command was held for approval, never delivered/executed, so no completion marker, finalized record or journal entry was missing. Corrected synchronization passes the same event-driven observer gate. Other controlled runs failed earlier at output waiting; they are not presented as exact-symptom reproductions.

The historical hosted failure had no lifecycle trace, so its precise cause and natural frequency remain unconfirmed. Ungated isolated, bounded two-worker and representative full-shard local controls passed. This establishes a reachable pre-existing harness mechanism and a concrete false pass, not proof that all #473 macOS failures share it or that production cannot regress.

## Focused correction and regression

The STATE submission remains bracketed paste, with a unique profile tag in its single printf command. Before resizing, the test requires exactly one durable completed record, successful status and that record's actual expected environment output. Every sequential size attempt has a unique tag; its own completed journal record must have successful status, the matching tag and width 50. All six hosts, mode assertions, persistent shell, existing deadlines and detach checks remain. No production/helper behavior, dependencies, test skips or expected geometry changed; no timing grace was added.

`hostProfileSynchronization.test.ts` invokes the real profile test through the existing private-home test runner with `hostProfileCompletionGate.mjs`. The explicit fixture delays a reattached STATE command's done/prompt delivery until the test polls its completion, or records premature SIZE input before releasing it. No fake output or shell completion is generated. The test requires every reattachment to exercise/release the gate and no premature probe. The child budget is shorter than the outer canonical budget to retain teardown margin; other existing test budgets are unchanged. Watchers close on release/stop; existing sandbox disposal owns its files/services. Failures print only compact fixture event metadata before removing their private trace.

Explicit reproduction:

```sh
node --import=tsx --import=./tests/fixtures/hostProfileCompletionGate.mjs --test --test-name-pattern='reattaching across profiles' tests/hostProfiles.test.ts
```

For detailed instrumentation, retain only isolated fixture metadata: attachment/profile/PIDs, hashed command identity, submission and actual PTY delivery, exec/data/done/prompt events, record finalization, journal save/durability and expected/observed count. Never enable arbitrary private output logging. Local ordered traces and all failed diagnostic attempts remain preserved on T7. The production marker path is unchanged: shell protocol → ShellSession → service sequence/backlog → frontend completion → OutputBuffer record → serialized atomic SessionJournal/TranscriptStore save → helper readback.

Independent review found no blocker in the final synchronization design. Its concrete readback-diagnostic and child-budget/failure-trace findings were addressed. Local/hosted final validation and measurements are recorded in the focused PR and #473 handoff; do not equate proposed-head passage with a green push on a later dev commit.

## Performance and human QA

Runtime code remains identical to the merged performance source. Final measurements must use built original dev versus actual mergeddev under equivalent conditions, with no local verification/profiling running concurrently. Report output/history/input median and nearest-rankp95, sampled frontend RSS and CPU separately from startup and idle observations. Retain the prior +6.7% large-history typing p95 and +2.3% deletion p95 fixed-versus-pre-fix differences without attributing causation. Do not pool absolute values across experiments or call PTY observations physical pixel latency. See the [integration report](../performance/2026-10-26-integration.md).

Physical Ghostty/Zed QA remains **NOT RUN**. #473 and performance issues remain open; this test correction does not resolve the other queue attribution, read/input or physical-validation risks. Existing live user services/sessions must remain alive. Follow the [physical QA guide](performance-integration-physical-qa.md).
79 changes: 79 additions & 0 deletions tests/fixtures/hostProfileCompletionGate.mjs
Original file line number Diff line number Diff line change
@@ -0,0 +1,79 @@
/**
* Explicit diagnostic import, not a production hook or canonical test preload:
* node --import=tsx --import=./tests/fixtures/hostProfileCompletionGate.mjs --test tests/hostProfiles.test.ts
* Delay a reattached STATE command's completion batch until the test observes its journal,
* or until premature SIZE input exposes the old harness ordering. No timing sleeps.
*/
import {appendFileSync, existsSync, watch, writeFileSync} from 'node:fs';
import {fileURLToPath} from 'node:url';
const entry = fileURLToPath(import.meta.url);
const repo = fileURLToPath(new URL('../../', import.meta.url));
function trace(event, detail = {}) {
const path = process.env.NMSH_HOST_COMPLETION_TRACE;
if (path) appendFileSync(path, JSON.stringify({ns: process.hrtime.bigint().toString(), pid: process.pid, event, ...detail}) + '\n');
}

if (process.argv[1] === entry) {
const {TerminalApp} = await import(repo + 'src/app/TerminalApp.ts');
const prompt = TerminalApp.prototype.onShellPrompt;
const input = TerminalApp.prototype.onInputState;
const route = TerminalApp.prototype.routeComposer;
const stop = TerminalApp.prototype.stop;
const states = new WeakMap();
const signal = process.env.HOME + '/host-completion-' + process.pid;
const held = app => {
let state = states.get(app);
if (!state) { state = {}; states.set(app, state); }
return state;
};
const eligible = app => app.attachedSession && app.running?.command.startsWith('printf "STATE-');
function release(app) {
const state = held(app);
if (!state.prompt || !existsSync(signal)) return;
state.watcher?.close();
const args = state.prompt;
state.prompt = undefined;
trace('completion-batch.released');
if (state.done) { input.call(app, state.done); state.done = undefined; }
prompt.apply(app, args);
}
TerminalApp.prototype.onInputState = function (value) {
if (eligible(this) && value.done) { held(this).done = value; trace('done.held'); return; }
return input.call(this, value);
};
TerminalApp.prototype.onShellPrompt = function (...args) {
if (!eligible(this)) return prompt.apply(this, args);
const state = held(this);
state.prompt = args;
state.watcher = watch(process.env.HOME, () => release(this));
trace('prompt.held');
release(this);
};
TerminalApp.prototype.routeComposer = function (...args) {
const premature = held(this).prompt && this.editor.text.startsWith('echo SIZE-$(stty size)');
const result = route.apply(this, args);
if (premature) { trace('size.entered.before.completion', {done: Boolean(this.running?.done)}); writeFileSync(signal, 'release'); }
return result;
};
TerminalApp.prototype.stop = function (...args) { held(this).watcher?.close(); return stop.apply(this, args); };
await import(repo + 'src/index.ts');
} else if (process.argv.some(arg => arg.endsWith('/hostProfiles.test.ts'))) {
const {LiveSandbox, Frontend} = await import(repo + 'tests/helpers/liveFrontend.ts');
const launch = LiveSandbox.prototype.launch;
const completions = Frontend.prototype.completions;
const homes = new WeakMap();
let generation = 0;
LiveSandbox.prototype.launch = function (args, size, env) {
const frontend = launch.call(this, args, size, env, entry);
homes.set(frontend, this.home);
trace('attachment', {generation: ++generation, profile: env?.TERM_PROGRAM, frontendPid: frontend.pty.pid});
return frontend;
};
Frontend.prototype.completions = function (command) {
if (command.startsWith('printf "STATE-')) {
trace('test.observes.state.completion', {frontendPid: this.pty.pid});
writeFileSync(homes.get(this) + '/host-completion-' + this.pty.pid, 'release');
}
return completions.call(this, command);
};
}
40 changes: 40 additions & 0 deletions tests/hostProfileSynchronization.test.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,40 @@
import test from 'node:test';
import assert from 'node:assert/strict';
import {mkdtemp, readFile, rm} from 'node:fs/promises';
import {tmpdir} from 'node:os';
import {join} from 'node:path';

// Run the real profile test with an event-driven completion gate. Without its STATE
// completion wait, SIZE is correctly held for approval and the child test fails.
test('host profile probes wait for frontend completion and verify their own journal geometry', async () => {
const root = await mkdtemp(join(tmpdir(), 'host-sync-'));
const tracePath = join(root, 'trace.jsonl');
const previous = process.env.NMSH_HOST_COMPLETION_TRACE;
process.env.NMSH_HOST_COMPLETION_TRACE = tracePath;
try {
const {runTestFiles} = await import(new URL('../scripts/test.mjs', import.meta.url).href);
const result = await runTestFiles(['tests/hostProfiles.test.ts'], [
'--import=./tests/fixtures/hostProfileCompletionGate.mjs',
'--test-name-pattern=reattaching across profiles',
// Keep teardown margin below the outer canonical test's existing 120-second budget.
'--test-timeout=60000',
]);
assert.equal(result.code, 0, 'the real profile test passes with delayed completion delivery');
const rows = (await readFile(tracePath, 'utf8')).trim().split('\n')
.map(line => JSON.parse(line) as {event: string; profile?: string});
assert.deepEqual(rows.filter(row => row.event === 'attachment').map(row => row.profile),
['ghostty', 'Apple_Terminal', 'kitty', 'iTerm.app', 'WezTerm', 'zed']);
assert.equal(rows.filter(row => row.event === 'prompt.held').length, 5, 'each reattachment exercised the gate');
assert.equal(rows.filter(row => row.event === 'completion-batch.released').length, 5);
assert.equal(rows.some(row => row.event === 'size.entered.before.completion'), false,
'no geometry probe was submitted before the prior frontend completion');
} catch (error) {
// Preserve only fixture event metadata in hosted logs before removing our private trace.
console.error('Host completion gate trace:\n' + await readFile(tracePath, 'utf8').catch(() => '(unavailable)'));
throw error;
} finally {
if (previous === undefined) delete process.env.NMSH_HOST_COMPLETION_TRACE;
else process.env.NMSH_HOST_COMPLETION_TRACE = previous;
await rm(root, {recursive: true, force: true});
}
});
29 changes: 26 additions & 3 deletions tests/hostProfiles.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -108,14 +108,37 @@ test('reattaching across profiles updates input modes while keeping the real she
id = (await sandbox.sessions())[0]!.id;
}
const mark = app.mark;
app.pty.write('\u001b[200~printf "STATE-%s-HOST-%s\\n" "$PROFILE_STATE" "$TERM_PROGRAM"\u001b[201~\r');
const stateCommand = `printf "STATE-%s-HOST-%s-${TERM_PROGRAM}\\n" "$PROFILE_STATE" "$TERM_PROGRAM"`;
app.pty.write(`\u001b[200~${stateCommand}\u001b[201~\r`);
await app.waitFor(/STATE-kept-HOST-ghostty/, mark);
// A rendered row (including restored history) is not command completion. Starting the size
// probe before this prompt reaches the frontend can correctly hold it for queue approval.
await until(async () => await app.completions(stateCommand) > 0, 15000, `${TERM_PROGRAM}: state command completed`);
assert.equal(await app.completions(stateCommand), 1, 'the state command completed exactly once');
const stateRecord = (await sandbox.transcripts().list()).flatMap(session => session.transcript.records)
.find(record => record.command === stateCommand);
assert.ok(stateRecord, `${TERM_PROGRAM}: completed state record is readable`);
assert.equal(stateRecord.exitCode, 0);
assert.ok(stateRecord.output.split('\n').includes(`STATE-kept-HOST-ghostty-${TERM_PROGRAM}`));
app.pty.resize(50, 18);
let probe = 0;
await until(async () => {
const sizeMark = app.mark;
await app.run('echo SIZE-$(stty size)', /SIZE-\d+ \d+/);
// Retries are sequential, and each must finish. Unique command identities prevent an older
// journal entry or a redrawn size row from satisfying this probe's lifecycle/geometry check.
const tag = `${TERM_PROGRAM}:${++probe}`;
const sizeCommand = `echo SIZE-$(stty size) '${tag}'`;
await app.run(sizeCommand, /SIZE-\d+ \d+/);
await app.waitFor(/Completed/, sizeMark);
return /SIZE-\d+ 50/u.test(app.output.slice(sizeMark));
assert.equal(await app.completions(sizeCommand), 1, 'the size probe completed exactly once');
const sizeRecord = (await sandbox.transcripts().list()).flatMap(session => session.transcript.records)
.find(record => record.command === sizeCommand);
assert.ok(sizeRecord, `${sizeCommand}: completed probe record is readable`);
assert.equal(sizeRecord.exitCode, 0);
const size = /^SIZE-(\d+) (\d+) ([^\n]+)$/mu.exec(sizeRecord.output);
assert.ok(size, `${tag}: the completed probe contains its size`);
assert.equal(size[3], tag, 'the size belongs to this probe');
return Number(size[2]) === 50;
}, 15000, 'resize reached persistent shell');
await app.waitFor(/Completed/, mark);
app.pty.kill('SIGKILL');
Expand Down
Loading