mirror of
https://github.com/garrytan/gstack.git
synced 2026-05-08 21:49:45 +08:00
merge: origin/main into garrytan/multi-checkpoint-resume
Catches up commit822e843afrom main: - fix: headed browser auto-shutdown + disconnect cleanup (v0.18.1.0) (#1025) Version collision resolution: Both main (822e843a) and my branch (6d879cd4) bumped to v0.18.1.0. Per CLAUDE.md's CHANGELOG rule ("Merging main does NOT mean adopting main's version. Your branch still needs its OWN version bump on top"), I preserved main's v0.18.1.0 entry (headed browser fix) as-is and bumped this branch to v0.18.2.0. VERSION + package.json updated to match. CHANGELOG.md conflict resolved: main's v0.18.1.0 entry stays (it landed first); my context-rot-defense entry moves to v0.18.2.0 at the top. TODOS.md auto-merged cleanly (main added a new Browse TODO about scoping sidebar-agent kill to session PID — that stays). Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
This commit is contained in:
16
CHANGELOG.md
16
CHANGELOG.md
@@ -1,6 +1,6 @@
|
||||
# Changelog
|
||||
|
||||
## [0.18.1.0] - 2026-04-17
|
||||
## [0.18.2.0] - 2026-04-17
|
||||
|
||||
### Fixed
|
||||
- **`/ship` stops skipping `/document-release` ~80% of the time.** The old Step 8.5 told Claude to `cat` a 2500-line external skill file *after* the PR URL was already output, at which point the model had 500-1,750 lines of intermediate tool output in context and was at its least intelligent. Now `/ship` dispatches `/document-release` as a subagent that runs in a fresh context window, *before* creating the PR, so the `## Documentation` section gets baked into the initial PR body instead of a create-then-re-edit dance. The result: documentation actually syncs on every ship.
|
||||
@@ -14,6 +14,20 @@
|
||||
- New regression guards in `test/skill-validation.test.ts` prevent drift back to fractional step numbers and catch cross-contamination between `/ship` and `/review` resolver conditionals.
|
||||
- Ship template restructure: old Step 8.5 (post-PR doc sync with `cat` delegation) replaced by new Step 18 (pre-PR subagent dispatch that invokes full `/document-release` skill with its CHANGELOG clobber protections, doc exclusions, risky-change gates, and race-safe PR body editing). Codex caught that the original plan's reimplementation dropped those protections; this version reuses the real `/document-release`.
|
||||
|
||||
## [0.18.1.0] - 2026-04-16
|
||||
|
||||
### Fixed
|
||||
- **`/open-gstack-browser` actually stays open now.** If you ran `/open-gstack-browser` or `$B connect` and your browser vanished roughly 15 seconds later, this was why: a watchdog inside the browse server was polling the CLI process that spawned it, and when the CLI exited (which it does, immediately, right after launching the browser), the watchdog said "orphan!" and killed everything. The fix disables that watchdog for headed mode, both in the CLI (always set `BROWSE_PARENT_PID=0` for headed launches) and in the server (skip the watchdog entirely when `BROWSE_HEADED=1`). Two layers of defense in case a future launcher forgets to pass the env var. Thanks to @rocke2020 (#1020), @sanghyuk-seo-nexcube (#1018), @rodbland2021 (#1012), and @jbetala7 (#986) for independently diagnosing this and sending in clean, well-documented fixes.
|
||||
- **Closing the headed browser window now cleans up properly.** Before this release, clicking the X on the GStack Browser window skipped the server's cleanup routine and exited the process directly. That left behind stale sidebar-agent processes polling a dead server, unsaved chat session state, leftover Chromium profile locks (which cause "profile in use" errors on the next `$B connect`), and a stale `browse.json` state file. Now the disconnect handler routes through the full `shutdown()` path first, cleans everything, and then exits with code 2 (which still distinguishes user-close from crash).
|
||||
- **CI/Claude Code Bash calls can now share a persistent headless server.** The headless spawn path used to hardcode the CLI's own PID as the watchdog target, ignoring `BROWSE_PARENT_PID=0` even if you set it in your environment. Now `BROWSE_PARENT_PID=0 $B goto https://...` keeps the server alive across short-lived CLI invocations, which is what multi-step workflows (CI matrices, Claude Code's Bash tool, cookie picker flows) actually want.
|
||||
- **`SIGTERM` / `SIGINT` shutdown now exits with code 0 instead of 1.** Regression caught during /ship's adversarial review: when `shutdown()` started accepting an `exitCode` argument, Node's signal listeners silently passed the signal name (`'SIGTERM'`) as the exit code, which got coerced to `NaN` and used `1`. Wrapped the listeners so they call `shutdown()` with no args. Your `Ctrl+C` now exits clean again.
|
||||
|
||||
### For contributors
|
||||
- `test/relink.test.ts` no longer flakes under parallel test load. The 23 tests in that file each shell out to `gstack-config` + `gstack-relink` (bash subprocess work), and under `bun test` with other suites running, each test drifted ~200ms past Bun's 5s default. Wrapped `test` to default the per-test timeout to 15s with `Object.assign` preserving `.only`/`.skip`/`.each` sub-APIs.
|
||||
- `BrowserManager` gained an `onDisconnect` callback (wired by `server.ts` to `shutdown(2)`), replacing the direct `process.exit(2)` in the disconnect handler. The callback is wrapped with try/catch + Promise rejection handling so a rejecting cleanup path still exits the process instead of leaving a live server attached to a dead browser.
|
||||
- `shutdown()` now accepts an optional `exitCode: number = 0` parameter, used by the disconnect path (exit 2) and the signal path (default 0). Same cleanup code, two call sites, distinct exit codes.
|
||||
- `BROWSE_PARENT_PID` parsing in `cli.ts` now matches `server.ts`: `parseInt` instead of strict string equality, so `BROWSE_PARENT_PID=0\n` (common from shell `export`) is honored.
|
||||
|
||||
## [0.18.0.1] - 2026-04-16
|
||||
|
||||
### Fixed
|
||||
|
||||
14
TODOS.md
14
TODOS.md
@@ -1,5 +1,19 @@
|
||||
# TODOS
|
||||
|
||||
## Browse
|
||||
|
||||
### Scope sidebar-agent kill to session PID, not `pkill -f sidebar-agent\.ts`
|
||||
|
||||
**What:** `shutdown()` in `browse/src/server.ts:1193` uses `pkill -f sidebar-agent\.ts` to kill the sidebar-agent daemon, which matches every sidebar-agent on the machine, not just the one this server spawned. Replace with PID tracking: store the sidebar-agent PID when `cli.ts` spawns it (via state file or env), then `process.kill(pid, 'SIGTERM')` in `shutdown()`.
|
||||
|
||||
**Why:** A user running two Conductor worktrees (or any multi-session setup), each with its own `$B connect`, closes one browser window ... and the other worktree's sidebar-agent gets killed too. The blast radius was there before, but the v0.18.1.0 disconnect-cleanup fix makes it more reachable: every user-close now runs the full `shutdown()` path, whereas before user-close bypassed it.
|
||||
|
||||
**Context:** Surfaced by /ship's adversarial review on v0.18.1.0. Pre-existing code, not introduced by the fix. Fix requires propagating the sidebar-agent PID from `cli.ts` spawn site (~line 885) into the server's state file so `shutdown()` can target just this session's agent. Related: `browse/src/cli.ts` spawns with `Bun.spawn(...).unref()` and already captures `agentProc.pid`.
|
||||
|
||||
**Effort:** S (human: ~2h / CC: ~15min)
|
||||
**Priority:** P2
|
||||
**Depends on:** None
|
||||
|
||||
## Sidebar Security
|
||||
|
||||
### ML Prompt Injection Classifier
|
||||
|
||||
@@ -72,6 +72,12 @@ export class BrowserManager {
|
||||
private connectionMode: 'launched' | 'headed' = 'launched';
|
||||
private intentionalDisconnect = false;
|
||||
|
||||
// Called when the headed browser disconnects without intentional teardown
|
||||
// (user closed the window). Wired up by server.ts to run full cleanup
|
||||
// (sidebar-agent, state file, profile locks) before exiting with code 2.
|
||||
// Returns void or a Promise; rejections are caught and fall back to exit(2).
|
||||
public onDisconnect: (() => void | Promise<void>) | null = null;
|
||||
|
||||
getConnectionMode(): 'launched' | 'headed' { return this.connectionMode; }
|
||||
|
||||
// ─── Watch Mode Methods ─────────────────────────────────
|
||||
@@ -467,13 +473,32 @@ export class BrowserManager {
|
||||
await this.newTab();
|
||||
}
|
||||
|
||||
// Browser disconnect handler — exit code 2 distinguishes from crashes (1)
|
||||
// Browser disconnect handler — exit code 2 distinguishes from crashes (1).
|
||||
// Calls onDisconnect() to trigger full shutdown (kill sidebar-agent, save
|
||||
// session, clean profile locks + state file) before exit. Falls back to
|
||||
// direct process.exit(2) if no callback is wired up, or if the callback
|
||||
// throws/rejects — never leave the process running with a dead browser.
|
||||
if (this.browser) {
|
||||
this.browser.on('disconnected', () => {
|
||||
if (this.intentionalDisconnect) return;
|
||||
console.error('[browse] Real browser disconnected (user closed or crashed).');
|
||||
console.error('[browse] Run `$B connect` to reconnect.');
|
||||
process.exit(2);
|
||||
if (!this.onDisconnect) {
|
||||
process.exit(2);
|
||||
return;
|
||||
}
|
||||
try {
|
||||
const result = this.onDisconnect();
|
||||
if (result && typeof (result as Promise<void>).catch === 'function') {
|
||||
(result as Promise<void>).catch((err) => {
|
||||
console.error('[browse] onDisconnect rejected:', err);
|
||||
process.exit(2);
|
||||
});
|
||||
}
|
||||
} catch (err) {
|
||||
console.error('[browse] onDisconnect threw:', err);
|
||||
process.exit(2);
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
|
||||
@@ -210,12 +210,20 @@ async function startServer(extraEnv?: Record<string, string>): Promise<ServerSta
|
||||
|
||||
let proc: any = null;
|
||||
|
||||
// Allow the caller to opt out of the parent-process watchdog by setting
|
||||
// BROWSE_PARENT_PID=0 in the environment. Useful for CI, non-interactive
|
||||
// shells, and short-lived Bash invocations that need the server to outlive
|
||||
// the spawning CLI. Defaults to the current process PID (watchdog active).
|
||||
// Parse as int so stray whitespace ("0\n") still opts out — matches the
|
||||
// server's own parseInt at server.ts:760.
|
||||
const parentPid = parseInt(process.env.BROWSE_PARENT_PID || '', 10) === 0 ? '0' : String(process.pid);
|
||||
|
||||
if (IS_WINDOWS && NODE_SERVER_SCRIPT) {
|
||||
// Windows: Bun.spawn() + proc.unref() doesn't truly detach on Windows —
|
||||
// when the CLI exits, the server dies with it. Use Node's child_process.spawn
|
||||
// with { detached: true } instead, which is the gold standard for Windows
|
||||
// process independence. Credit: PR #191 by @fqueiro.
|
||||
const extraEnvStr = JSON.stringify({ BROWSE_STATE_FILE: config.stateFile, BROWSE_PARENT_PID: String(process.pid), ...(extraEnv || {}) });
|
||||
const extraEnvStr = JSON.stringify({ BROWSE_STATE_FILE: config.stateFile, BROWSE_PARENT_PID: parentPid, ...(extraEnv || {}) });
|
||||
const launcherCode =
|
||||
`const{spawn}=require('child_process');` +
|
||||
`spawn(process.execPath,[${JSON.stringify(NODE_SERVER_SCRIPT)}],` +
|
||||
@@ -226,7 +234,7 @@ async function startServer(extraEnv?: Record<string, string>): Promise<ServerSta
|
||||
// macOS/Linux: Bun.spawn + unref works correctly
|
||||
proc = Bun.spawn(['bun', 'run', SERVER_SCRIPT], {
|
||||
stdio: ['ignore', 'pipe', 'pipe'],
|
||||
env: { ...process.env, BROWSE_STATE_FILE: config.stateFile, BROWSE_PARENT_PID: String(process.pid), ...extraEnv },
|
||||
env: { ...process.env, BROWSE_STATE_FILE: config.stateFile, BROWSE_PARENT_PID: parentPid, ...extraEnv },
|
||||
});
|
||||
proc.unref();
|
||||
}
|
||||
@@ -826,12 +834,12 @@ Refs: After 'snapshot', use @e1, @e2... as selectors:
|
||||
BROWSE_HEADED: '1',
|
||||
BROWSE_PORT: '34567',
|
||||
BROWSE_SIDEBAR_CHAT: '1',
|
||||
// Disable parent-process watchdog: the user controls the headed browser
|
||||
// window lifecycle. The CLI exits immediately after connect, so watching
|
||||
// it would kill the server ~15s later. Cleanup happens via browser
|
||||
// disconnect event or $B disconnect.
|
||||
BROWSE_PARENT_PID: '0',
|
||||
};
|
||||
// If parent explicitly set BROWSE_PARENT_PID=0 (pair-agent disabling
|
||||
// self-termination), pass it through so startServer doesn't override it.
|
||||
if (process.env.BROWSE_PARENT_PID === '0') {
|
||||
serverEnv.BROWSE_PARENT_PID = '0';
|
||||
}
|
||||
const newState = await startServer(serverEnv);
|
||||
|
||||
// Print connected status
|
||||
|
||||
@@ -757,8 +757,16 @@ const idleCheckInterval = setInterval(() => {
|
||||
// server can become an orphan — keeping chrome-headless-shell alive and
|
||||
// causing console-window flicker on Windows. Poll the parent PID every 15s
|
||||
// and self-terminate if it is gone.
|
||||
//
|
||||
// Headed mode (BROWSE_HEADED=1 or BROWSE_PARENT_PID=0): The user controls
|
||||
// the browser window lifecycle. The CLI exits immediately after connect,
|
||||
// so the watchdog would kill the server prematurely. Disabled in both cases
|
||||
// as defense-in-depth — the CLI sets PID=0 for headed mode, and the server
|
||||
// also checks BROWSE_HEADED in case a future launcher forgets.
|
||||
// Cleanup happens via browser disconnect event or $B disconnect.
|
||||
const BROWSE_PARENT_PID = parseInt(process.env.BROWSE_PARENT_PID || '0', 10);
|
||||
if (BROWSE_PARENT_PID > 0) {
|
||||
const IS_HEADED_WATCHDOG = process.env.BROWSE_HEADED === '1';
|
||||
if (BROWSE_PARENT_PID > 0 && !IS_HEADED_WATCHDOG) {
|
||||
setInterval(() => {
|
||||
try {
|
||||
process.kill(BROWSE_PARENT_PID, 0); // signal 0 = existence check only, no signal sent
|
||||
@@ -767,6 +775,10 @@ if (BROWSE_PARENT_PID > 0) {
|
||||
shutdown();
|
||||
}
|
||||
}, 15_000);
|
||||
} else if (IS_HEADED_WATCHDOG) {
|
||||
console.log('[browse] Parent-process watchdog disabled (headed mode)');
|
||||
} else if (BROWSE_PARENT_PID === 0) {
|
||||
console.log('[browse] Parent-process watchdog disabled (BROWSE_PARENT_PID=0)');
|
||||
}
|
||||
|
||||
// ─── Command Sets (from commands.ts — single source of truth) ───
|
||||
@@ -793,6 +805,10 @@ function emitInspectorEvent(event: any): void {
|
||||
|
||||
// ─── Server ────────────────────────────────────────────────────
|
||||
const browserManager = new BrowserManager();
|
||||
// When the user closes the headed browser window, run full cleanup
|
||||
// (kill sidebar-agent, save session, remove profile locks, delete state file)
|
||||
// before exiting with code 2. Exit code 2 distinguishes user-close from crashes (1).
|
||||
browserManager.onDisconnect = () => shutdown(2);
|
||||
let isShuttingDown = false;
|
||||
|
||||
// Test if a port is available by binding and immediately releasing.
|
||||
@@ -1180,7 +1196,7 @@ async function handleCommand(body: any, tokenInfo?: TokenInfo | null): Promise<R
|
||||
});
|
||||
}
|
||||
|
||||
async function shutdown() {
|
||||
async function shutdown(exitCode: number = 0) {
|
||||
if (isShuttingDown) return;
|
||||
isShuttingDown = true;
|
||||
|
||||
@@ -1221,12 +1237,15 @@ async function shutdown() {
|
||||
// Clean up state file
|
||||
safeUnlinkQuiet(config.stateFile);
|
||||
|
||||
process.exit(0);
|
||||
process.exit(exitCode);
|
||||
}
|
||||
|
||||
// Handle signals
|
||||
process.on('SIGTERM', shutdown);
|
||||
process.on('SIGINT', shutdown);
|
||||
// Node passes the signal name (e.g. 'SIGTERM') as the first arg to listeners.
|
||||
// Wrap so shutdown() receives no args — otherwise the string gets passed as
|
||||
// exitCode and process.exit() coerces it to NaN, exiting with code 1 instead of 0.
|
||||
process.on('SIGTERM', () => shutdown());
|
||||
process.on('SIGINT', () => shutdown());
|
||||
// Windows: taskkill /F bypasses SIGTERM, but 'exit' fires for some shutdown paths.
|
||||
// Defense-in-depth — primary cleanup is the CLI's stale-state detection via health check.
|
||||
if (process.platform === 'win32') {
|
||||
|
||||
147
browse/test/watchdog.test.ts
Normal file
147
browse/test/watchdog.test.ts
Normal file
@@ -0,0 +1,147 @@
|
||||
import { describe, test, expect, afterEach } from 'bun:test';
|
||||
import { spawn, type Subprocess } from 'bun';
|
||||
import * as path from 'path';
|
||||
import * as fs from 'fs';
|
||||
import * as os from 'os';
|
||||
|
||||
// End-to-end regression tests for the parent-process watchdog in server.ts.
|
||||
// Proves three invariants that the v0.18.1.0 fix depends on:
|
||||
//
|
||||
// 1. BROWSE_PARENT_PID=0 disables the watchdog (opt-in used by CI and pair-agent).
|
||||
// 2. BROWSE_HEADED=1 disables the watchdog (server-side defense-in-depth).
|
||||
// 3. Default headless mode still kills the server when its parent dies
|
||||
// (the original orphan-prevention must keep working).
|
||||
//
|
||||
// Each test spawns the real server.ts, not a mock. Tests 1 and 2 verify the
|
||||
// code path via stdout log line (fast). Test 3 waits for the watchdog's 15s
|
||||
// poll cycle to actually fire (slow — ~25s).
|
||||
|
||||
const ROOT = path.resolve(import.meta.dir, '..');
|
||||
const SERVER_SCRIPT = path.join(ROOT, 'src', 'server.ts');
|
||||
|
||||
let tmpDir: string;
|
||||
let serverProc: Subprocess | null = null;
|
||||
let parentProc: Subprocess | null = null;
|
||||
|
||||
afterEach(async () => {
|
||||
// Kill any survivors so subsequent tests get a clean slate.
|
||||
try { parentProc?.kill('SIGKILL'); } catch {}
|
||||
try { serverProc?.kill('SIGKILL'); } catch {}
|
||||
// Give processes a moment to exit before tmpDir cleanup.
|
||||
await Bun.sleep(100);
|
||||
try { fs.rmSync(tmpDir, { recursive: true, force: true }); } catch {}
|
||||
parentProc = null;
|
||||
serverProc = null;
|
||||
});
|
||||
|
||||
function spawnServer(env: Record<string, string>, port: number): Subprocess {
|
||||
const stateFile = path.join(tmpDir, 'browse-state.json');
|
||||
return spawn(['bun', 'run', SERVER_SCRIPT], {
|
||||
env: {
|
||||
...process.env,
|
||||
BROWSE_STATE_FILE: stateFile,
|
||||
BROWSE_PORT: String(port),
|
||||
...env,
|
||||
},
|
||||
stdio: ['ignore', 'pipe', 'pipe'],
|
||||
});
|
||||
}
|
||||
|
||||
function isProcessAlive(pid: number): boolean {
|
||||
try {
|
||||
process.kill(pid, 0); // signal 0 = existence check, no signal sent
|
||||
return true;
|
||||
} catch {
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
// Read stdout until we see the expected marker or timeout. Returns the captured
|
||||
// text. Used to verify the watchdog code path ran as expected at startup.
|
||||
async function readStdoutUntil(
|
||||
proc: Subprocess,
|
||||
marker: string,
|
||||
timeoutMs: number,
|
||||
): Promise<string> {
|
||||
const deadline = Date.now() + timeoutMs;
|
||||
const decoder = new TextDecoder();
|
||||
let captured = '';
|
||||
const reader = (proc.stdout as ReadableStream<Uint8Array>).getReader();
|
||||
try {
|
||||
while (Date.now() < deadline) {
|
||||
const readPromise = reader.read();
|
||||
const timed = Bun.sleep(Math.max(0, deadline - Date.now()));
|
||||
const result = await Promise.race([readPromise, timed.then(() => null)]);
|
||||
if (!result || result.done) break;
|
||||
captured += decoder.decode(result.value);
|
||||
if (captured.includes(marker)) return captured;
|
||||
}
|
||||
} finally {
|
||||
try { reader.releaseLock(); } catch {}
|
||||
}
|
||||
return captured;
|
||||
}
|
||||
|
||||
describe('parent-process watchdog (v0.18.1.0)', () => {
|
||||
test('BROWSE_PARENT_PID=0 disables the watchdog', async () => {
|
||||
tmpDir = fs.mkdtempSync(path.join(os.tmpdir(), 'watchdog-pid0-'));
|
||||
serverProc = spawnServer({ BROWSE_PARENT_PID: '0' }, 34901);
|
||||
|
||||
const out = await readStdoutUntil(
|
||||
serverProc,
|
||||
'Parent-process watchdog disabled (BROWSE_PARENT_PID=0)',
|
||||
5000,
|
||||
);
|
||||
expect(out).toContain('Parent-process watchdog disabled (BROWSE_PARENT_PID=0)');
|
||||
// Control: the "parent exited, shutting down" line must NOT appear —
|
||||
// that would mean the watchdog ran after we said to skip it.
|
||||
expect(out).not.toContain('Parent process');
|
||||
}, 15_000);
|
||||
|
||||
test('BROWSE_HEADED=1 disables the watchdog (server-side guard)', async () => {
|
||||
tmpDir = fs.mkdtempSync(path.join(os.tmpdir(), 'watchdog-headed-'));
|
||||
// Pass a bogus parent PID to prove BROWSE_HEADED takes precedence.
|
||||
// If the server-side guard regresses, the watchdog would try to poll
|
||||
// this PID and eventually fire on the "dead parent."
|
||||
serverProc = spawnServer(
|
||||
{ BROWSE_HEADED: '1', BROWSE_PARENT_PID: '999999' },
|
||||
34902,
|
||||
);
|
||||
|
||||
const out = await readStdoutUntil(
|
||||
serverProc,
|
||||
'Parent-process watchdog disabled (headed mode)',
|
||||
5000,
|
||||
);
|
||||
expect(out).toContain('Parent-process watchdog disabled (headed mode)');
|
||||
expect(out).not.toContain('Parent process 999999 exited');
|
||||
}, 15_000);
|
||||
|
||||
test('default headless mode: watchdog fires when parent dies', async () => {
|
||||
tmpDir = fs.mkdtempSync(path.join(os.tmpdir(), 'watchdog-default-'));
|
||||
|
||||
// Spawn a real, short-lived "parent" that the watchdog will poll.
|
||||
parentProc = spawn(['sleep', '60'], { stdio: ['ignore', 'ignore', 'ignore'] });
|
||||
const parentPid = parentProc.pid!;
|
||||
|
||||
// Default headless: no BROWSE_HEADED, real parent PID — watchdog active.
|
||||
serverProc = spawnServer({ BROWSE_PARENT_PID: String(parentPid) }, 34903);
|
||||
const serverPid = serverProc.pid!;
|
||||
|
||||
// Give the server a moment to start and register the watchdog interval.
|
||||
await Bun.sleep(2000);
|
||||
expect(isProcessAlive(serverPid)).toBe(true);
|
||||
|
||||
// Kill the parent. The watchdog polls every 15s, so first tick after
|
||||
// parent death lands within ~15s, plus shutdown() cleanup time.
|
||||
parentProc.kill('SIGKILL');
|
||||
|
||||
// Poll for up to 25s for the server to exit.
|
||||
const deadline = Date.now() + 25_000;
|
||||
while (Date.now() < deadline) {
|
||||
if (!isProcessAlive(serverPid)) break;
|
||||
await Bun.sleep(500);
|
||||
}
|
||||
expect(isProcessAlive(serverPid)).toBe(false);
|
||||
}, 45_000);
|
||||
});
|
||||
@@ -1,6 +1,6 @@
|
||||
{
|
||||
"name": "gstack",
|
||||
"version": "0.18.1.0",
|
||||
"version": "0.18.2.0",
|
||||
"description": "Garry's Stack — Claude Code skills + fast headless browser. One repo, one install, entire AI engineering workflow.",
|
||||
"license": "MIT",
|
||||
"type": "module",
|
||||
|
||||
@@ -1,9 +1,19 @@
|
||||
import { describe, test, expect, beforeEach, afterEach } from 'bun:test';
|
||||
import { describe, test as _bunTest, expect, beforeEach, afterEach } from 'bun:test';
|
||||
import { execSync } from 'child_process';
|
||||
import * as fs from 'fs';
|
||||
import * as path from 'path';
|
||||
import * as os from 'os';
|
||||
|
||||
// Every test in this file shells out to gstack-config + gstack-relink (bash scripts
|
||||
// invoking subprocess work). Under parallel bun test load, subprocess spawn contends
|
||||
// with other suites and each test can drift ~200ms past the 5s default. Bump to 15s.
|
||||
// Object.assign preserves test.only / test.skip / test.each / test.todo sub-APIs.
|
||||
const test = Object.assign(
|
||||
((name: any, fn: any, timeout?: number) =>
|
||||
_bunTest(name, fn, timeout ?? 15_000)) as typeof _bunTest,
|
||||
_bunTest,
|
||||
);
|
||||
|
||||
const ROOT = path.resolve(import.meta.dir, '..');
|
||||
const BIN = path.join(ROOT, 'bin');
|
||||
|
||||
|
||||
Reference in New Issue
Block a user