mirror of
https://github.com/garrytan/gstack.git
synced 2026-11-12 19:44:48 +01:00
Step 0 probes with BROWSE_NO_AUTOSTART=1 $B status (no daemon is booted by the probe). No live daemon: clear Chromium profile locks and run plain $B connect, which reaps orphans and refuses to replace a busy daemon. Already headed: connect reports it. Live headless daemon: AskUserQuestion, and only an explicit yes runs $B connect --force-restart; spawned or headless sessions leave it running and print the stop line with the resolved browse binary. The hand-rolled kill / kill -9 of the recorded PID and the browse.json deletion are gone. Supersedes s10 M13. Tests: new open-gstack-browser-liveness.test.ts (no kill of the recorded PID, status probe before connect, --force-restart only after the yes branch, profile-lock cleanup kept, spawned/headless stop line). The state-extraction tripwire now covers only the port snippet, since the pid snippet is gone.
69 lines
3.2 KiB
TypeScript
69 lines
3.2 KiB
TypeScript
/**
|
|
* /open-gstack-browser must not kill a live browse daemon on its own.
|
|
*
|
|
* Step 0 probes with `$B status` (without auto-starting a daemon). No live
|
|
* daemon → plain `$B connect`, which reaps orphans and refuses to replace a
|
|
* busy daemon itself. A live daemon → AskUserQuestion, and only an explicit
|
|
* yes runs `$B connect --force-restart`. Spawned or headless sessions leave it
|
|
* running and print how to replace it. The skill never kills the PID recorded
|
|
* in .gstack/browse.json by hand.
|
|
*/
|
|
import { describe, expect, test } from 'bun:test';
|
|
import * as fs from 'fs';
|
|
import * as path from 'path';
|
|
|
|
const ROOT = path.resolve(import.meta.dir, '..');
|
|
const TMPL = fs.readFileSync(path.join(ROOT, 'open-gstack-browser', 'SKILL.md.tmpl'), 'utf-8');
|
|
// Printed with the resolved browse binary path in place of %s.
|
|
const STOP_LINE = 'Live browse daemon left running. Run %s stop, then re-run /open-gstack-browser to replace it.';
|
|
|
|
const section = (from: string, to: string) => {
|
|
const start = TMPL.indexOf(from);
|
|
const end = TMPL.indexOf(to, start + from.length);
|
|
expect(start).toBeGreaterThanOrEqual(0);
|
|
expect(end).toBeGreaterThan(start);
|
|
return TMPL.slice(start, end);
|
|
};
|
|
|
|
describe('/open-gstack-browser daemon liveness', () => {
|
|
test('no hand-rolled kill of the recorded daemon PID', () => {
|
|
expect(TMPL).not.toMatch(/\bkill\b[^\n]*(_OLD_PID|\$\w*PID)/);
|
|
expect(TMPL).not.toMatch(/kill -9/);
|
|
expect(TMPL).not.toMatch(/grep -o '"pid":/);
|
|
expect(TMPL).not.toMatch(/rm -f[^\n]*browse\.json/);
|
|
});
|
|
|
|
test('Step 0 probes with $B status without auto-starting a daemon, before any connect', () => {
|
|
const probeAt = TMPL.indexOf('BROWSE_NO_AUTOSTART=1 $B status');
|
|
expect(probeAt).toBeGreaterThanOrEqual(0);
|
|
expect(probeAt).toBeLessThan(TMPL.indexOf('$B connect'));
|
|
});
|
|
|
|
test('--force-restart appears only in the explicit-yes branch', () => {
|
|
const live = section('`DAEMON: live`', '## Step 2');
|
|
const ask = live.indexOf('AskUserQuestion');
|
|
expect(ask).toBeGreaterThanOrEqual(0);
|
|
const occurrences = [...TMPL.matchAll(/--force-restart/g)].map((m) => m.index!);
|
|
expect(occurrences.length).toBeGreaterThan(0);
|
|
const liveStart = TMPL.indexOf('`DAEMON: live`');
|
|
const askAbs = liveStart + ask;
|
|
for (const at of occurrences) expect(at).toBeGreaterThan(askAbs);
|
|
expect(live).toMatch(/only an explicit a runs[^\n]*--force-restart/i);
|
|
expect(TMPL).toMatch(/after an explicit a[^\n]*\n+```bash\n\$B connect --force-restart\n```/i);
|
|
});
|
|
|
|
test('the no-daemon branch runs plain connect and keeps the Chromium profile-lock cleanup', () => {
|
|
const none = section('`DAEMON: none`', '`DAEMON: headed`');
|
|
expect(none).toContain('plain `$B connect`');
|
|
expect(none).toContain('SingletonLock SingletonSocket SingletonCookie');
|
|
expect(none).not.toContain('--force-restart');
|
|
});
|
|
|
|
test('spawned or headless sessions leave the daemon running and print the resolved-binary stop line', () => {
|
|
const live = section('`DAEMON: live`', '## Step 1');
|
|
expect(live).toMatch(/spawned[\s\S]{0,40}headless[\s\S]{0,40}do not ask/i);
|
|
expect(live).toContain(`printf '${STOP_LINE}\\n' "$B"`);
|
|
expect(live.indexOf(STOP_LINE)).toBeLessThan(live.indexOf('AskUserQuestion'));
|
|
});
|
|
});
|