mirror of
https://github.com/Ark0N/Codeman.git
synced 2026-10-02 13:39:41 +02:00
fix(sessions): route antigravity launches through the ownership helpers
runAntigravity() landed on master after this branch was cut, so it kept the exact pattern the rest of this PR removes: terminal.clear() plus direct writeln into whatever session happened to be active. Merging master in surfaced it, leaving one of six run modes still wiping the active session's xterm on launch. Also adds regression coverage that can actually see the bug. The existing test drives the three helpers directly, so it stays green even when a run*() function is reverted to writing at the terminal itself: reverting runClaude()'s call site keeps all 16 tests passing. The new static guard scans session-ui.js and fails if any run*() body touches this.terminal.clear/writeln, which catches a regressed call site and would have caught runAntigravity on its own. A second unit test covers the home-screen path that nothing exercised: with no active session, launch progress must still clear and render in the terminal. Verified in a browser against a live instance. With a session active, runShell() and runAntigravity() leave its terminal untouched (clear() calls: 0, writes: 0) and emit one info toast; on master the same run wipes the session's marker text. The session-less home screen still clears and writes exactly as before. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
This commit is contained in:
@@ -1120,9 +1120,7 @@ Object.assign(CodemanApp.prototype, {
|
||||
const _runLoc = (this.cases || []).find(c => c.name === caseName)?.location;
|
||||
const isRemote = _runLoc === 'remote' || _runLoc === 'docker';
|
||||
|
||||
this.terminal.clear();
|
||||
this.terminal.writeln(`\x1b[1;32m Starting Antigravity session in ${caseName}...\x1b[0m`);
|
||||
this.terminal.writeln('');
|
||||
const ownsLaunchTerminal = this._beginSessionLaunchStatus(`Starting Antigravity session in ${caseName}...`);
|
||||
this.terminal.focus();
|
||||
|
||||
try {
|
||||
@@ -1130,8 +1128,10 @@ Object.assign(CodemanApp.prototype, {
|
||||
const statusRes = await fetch('/api/antigravity/status');
|
||||
const status = (await statusRes.json()).data;
|
||||
if (!status.available) {
|
||||
this.terminal.writeln('\x1b[1;31m Antigravity CLI not found.\x1b[0m');
|
||||
this.terminal.writeln('\x1b[90m Install with: curl -fsSL https://antigravity.google/cli/install.sh | bash\x1b[0m');
|
||||
this._reportSessionLaunchError(
|
||||
ownsLaunchTerminal,
|
||||
'Antigravity CLI not found. Install with: curl -fsSL https://antigravity.google/cli/install.sh | bash'
|
||||
);
|
||||
return;
|
||||
}
|
||||
}
|
||||
@@ -1160,7 +1160,7 @@ Object.assign(CodemanApp.prototype, {
|
||||
|
||||
this.terminal.focus();
|
||||
} catch (err) {
|
||||
this.terminal.writeln(`\x1b[1;31m Error: ${err.message}\x1b[0m`);
|
||||
this._reportSessionLaunchError(ownsLaunchTerminal, err.message);
|
||||
}
|
||||
},
|
||||
|
||||
|
||||
@@ -114,6 +114,71 @@ describe('Run launch synchronization', () => {
|
||||
expect(app.showToast).toHaveBeenNthCalledWith(2, 'Launch failed', 'error');
|
||||
});
|
||||
|
||||
it('still renders launch progress in the terminal on the session-less home screen', () => {
|
||||
const CodemanApp = function CodemanApp(this: any) {};
|
||||
const context = vm.createContext({
|
||||
CodemanApp,
|
||||
localStorage: { getItem: () => null, setItem: () => {} },
|
||||
document: { getElementById: () => null },
|
||||
console,
|
||||
});
|
||||
const sessionUi = readFileSync(resolve(import.meta.dirname, '../src/web/public/session-ui.js'), 'utf8');
|
||||
vm.runInContext(sessionUi, context, { filename: 'session-ui.js' });
|
||||
|
||||
const app = new (CodemanApp as any)();
|
||||
app.activeSessionId = null; // home screen: nothing else owns the terminal
|
||||
app.terminal = { clear: vi.fn(), writeln: vi.fn() };
|
||||
app.showToast = vi.fn();
|
||||
|
||||
const ownsTerminal = app._beginSessionLaunchStatus('Starting Codex session', '1;32');
|
||||
app._appendSessionLaunchStatus(ownsTerminal, 'Creating session');
|
||||
app._reportSessionLaunchError(ownsTerminal, 'Launch failed');
|
||||
|
||||
expect(ownsTerminal).toBe(true);
|
||||
expect(app.terminal.clear).toHaveBeenCalledTimes(1);
|
||||
expect(app.terminal.writeln.mock.calls.map((c: string[]) => c[0]).join('\n')).toContain('Starting Codex session');
|
||||
expect(app.terminal.writeln.mock.calls.map((c: string[]) => c[0]).join('\n')).toContain('Creating session');
|
||||
expect(app.terminal.writeln.mock.calls.map((c: string[]) => c[0]).join('\n')).toContain('Error: Launch failed');
|
||||
expect(app.showToast).not.toHaveBeenCalled();
|
||||
});
|
||||
|
||||
/**
|
||||
* Static guard over session-ui.js itself. The helpers above can be perfectly
|
||||
* correct while a run*() entry point still writes to the shared xterm
|
||||
* directly, which is the actual bug: a launch started while another session
|
||||
* is active wipes that session's terminal, and _cleanupPreviousSession()
|
||||
* then serializes the wiped view into its restore snapshot. Asserting on the
|
||||
* helpers alone cannot see that, so pin the call sites here. This also
|
||||
* covers run modes added later, which is how runAntigravity was caught.
|
||||
*/
|
||||
it('routes every run mode through the ownership helpers, never the terminal directly', () => {
|
||||
const src = readFileSync(resolve(import.meta.dirname, '../src/web/public/session-ui.js'), 'utf8');
|
||||
|
||||
// Methods live in one Object.assign(prototype, {...}) block at a fixed
|
||||
// 2-space indent, so `\n },` reliably closes the one we are inside.
|
||||
const bodies = new Map<string, string>();
|
||||
const header = /^ {2}async (run[A-Za-z]*)\(\) \{$/gm;
|
||||
for (let m = header.exec(src); m; m = header.exec(src)) {
|
||||
const start = m.index + m[0].length;
|
||||
const end = src.indexOf('\n },', start);
|
||||
expect(end, `could not find the end of ${m[1]}()`).toBeGreaterThan(start);
|
||||
bodies.set(m[1], src.slice(start, end));
|
||||
}
|
||||
|
||||
// Fail loudly if the scan matched nothing: a silently empty scan would make
|
||||
// every assertion below vacuously true.
|
||||
expect([...bodies.keys()]).toEqual(
|
||||
expect.arrayContaining(['runClaude', 'runShell', 'runOpenCode', 'runCodex', 'runGemini', 'runAntigravity'])
|
||||
);
|
||||
|
||||
for (const [name, body] of bodies) {
|
||||
expect(body, `${name}() must not clear a terminal it may not own`).not.toContain('this.terminal.clear(');
|
||||
expect(body, `${name}() must not write launch status straight to the terminal`).not.toContain(
|
||||
'this.terminal.writeln('
|
||||
);
|
||||
}
|
||||
});
|
||||
|
||||
it('coalesces overlapping Run activations and disables the button while the request is active', async () => {
|
||||
const runBtn = {
|
||||
disabled: false,
|
||||
|
||||
Reference in New Issue
Block a user