mirror of
https://github.com/garrytan/gstack.git
synced 2026-11-01 14:34:42 +01:00
v1.91.297.0 fix(browse): read Windows process start times in-process so the terminal agent boots without PowerShell (GSTA-139 flaky pass) (#3545)
* fix(browse): read Windows process start times in-process; drop PowerShell from two flaky test paths (GSTA-139 S5)
The terminal agent's identity probe launched powershell.exe Get-CimInstance
with a 2 s timeout on every boot (twice when started standalone). On loaded
Windows runners a launch takes ~0.94 s at p50 and up to 10.8 s, so the agent
missed the watchdog test's 5 s window and a stalled launch returned an empty
identity. Under Bun the start time now comes from GetProcessTimes in the
same /Date(ms)/ form PowerShell's CreationDate serialises to, so records,
the Node daemon (which keeps the PowerShell reader) and earlier releases
agree. The kill-time command-line check still uses PowerShell.
path-security-realpath builds its 8.3 alias with GetShortPathNameW instead
of a PowerShell launch that stalled for 30 s and was reported as '8.3
disabled'. cookie-import-native-job prints the Restart Manager receipt when
its owner assertion fails.
Co-authored-by: capy-ai[bot] <230910855+capy-ai[bot]@users.noreply.github.com>
* fix(browse): scope test 5's PowerShell assertion to the owner; identify a win32 agent by generation when readers disagree
Review of d4f0ea3c (specialist + outside voice, confirmed by Windows Free
Tests 40699973746 shard 3): test 5's fake child pid cannot be opened, so its
start time falls back to the PowerShell query; only the owner must skip it.
Adversarial note: the Bun reader and the Node daemon's PowerShell reader can
disagree for a process created in a DST-ambiguous hour; on win32 a live
process carrying the record's --agent-gen is still the agent, so it is
stopped, never treated as gone beside a second agent.
Co-authored-by: capy-ai[bot] <230910855+capy-ai[bot]@users.noreply.github.com>
* fix(browse): a win32 start-time mismatch with an unreadable command line is unknown, not gone
Fix-check (adversarial reviewer): when the Bun reader and a Node-written
record disagree and the PowerShell command-line query also fails, the agent
was read as gone and a second agent could start beside it. Base treated a
failed query as unknown; this restores that. CHANGELOG [Unreleased] entry.
Co-authored-by: capy-ai[bot] <230910855+capy-ai[bot]@users.noreply.github.com>
* fix(browse): keep agentStatus's zombie check before the generation return (terminal-agent-watchdog source pin)
Co-authored-by: capy-ai[bot] <230910855+capy-ai[bot]@users.noreply.github.com>
---------
Co-authored-by: capy-ai[bot] <230910855+capy-ai[bot]@users.noreply.github.com>
This commit is contained in:
1 parent
98dd72ffc3
commit
fb30ed1047
8 files changed
+228
-19
No files matched your search
@@ -1,5 +1,20 @@
|
||||
# Changelog
|
||||
|
||||
## [1.91.297.0] - 2026-10-28
|
||||
|
||||
**The terminal agent on Windows no longer waits on PowerShell to learn its own start time, so it boots in about a second and a slow PowerShell can no longer leave it without an identity.** Every agent boot ran `powershell.exe Get-CimInstance` with a 2 s timeout, twice when started standalone. On a loaded `windows-latest` runner one launch took 0.94 s at the median and up to 10.8 s, so agent boot took 2.4 to 2.8 s at the median and 5.1 s on a runner's first boot, past the owner-watchdog test's 5 s window. Measured again after the change under the same load: 0.86 to 0.91 s at the median, 1.18 s at most, 120 of 120 boots inside the window.
|
||||
|
||||
### Fixed
|
||||
- **`browse/src/terminal-agent-control.ts`**: under Bun the Windows start time comes from `GetProcessTimes` in-process and is written as the same `/Date(<ms>)/` value PowerShell's `CreationDate` gives (147 of 147 processes matched), so records from earlier releases and from the Node daemon, which keeps the PowerShell reader, still compare. The kill-time check still reads the command line through PowerShell, and a process is stopped only when it carries the record's `--agent-gen`. On Windows a start time that disagrees between the two readers no longer reads as gone when the live process carries that generation or its command line cannot be read, so no second agent starts beside it.
|
||||
|
||||
### Tests
|
||||
- `browse/test/process-liveness-windows.test.ts`: the in-process reader, the Node fallback, the kill-time command-line check, a reader disagreement and the unreadable path (never signalled, never gone, blocks a second agent), plus two Windows-only cases: the in-process value equals PowerShell's `CreationDate`, and a live agent whose identity cannot be read blocks a second one.
|
||||
- `browse/test/path-security-realpath.test.ts` builds its 8.3 alias with `GetShortPathNameW` instead of a PowerShell launch that stalled for 30 s in Windows Free Tests 40624015029 and was reported as "8.3 short names are disabled". A failed lookup now fails the test instead of skipping it.
|
||||
- `browse/test/cookie-import-native-job.test.ts` prints the Restart Manager receipt when the owner assertion fails, so the next red names its cause.
|
||||
|
||||
### Gate integrity
|
||||
- Test 5 in `process-liveness-windows.test.ts` expected the PowerShell mock's `fixture-owner-start` as the owner start time on every platform. On Windows that expectation followed the old implementation, so it moved with the product. It now requires the in-process value, equality with a real read, and no PowerShell query for the owner. The Linux and macOS assertion is unchanged.
|
||||
|
||||
## [1.91.296.0] - 2026-10-28
|
||||
|
||||
**The docsync gate stops failing ship-docsync-current and ship-docsync-store when the agent asks the docs helper for its usage in the middle of a line of reads.** Five red paid trials since 2026-10-27 each failed on a single refused line. In four, the agent ended a line of reads with `gstack-docs-candidate --help` (screen 40558235014, gate 40600731931, and screen 40627127462 twice). In the fifth, gate 40643690306, the line included `git tag`. v1.91.185.0 tried to stop the first slip by moving the "own Bash call" rule into the sentence that names `--help`, and the slip came back four times. The helper's `--help` prints its usage and returns before it touches Git or any file, so refusing it protected nothing. The 2026-10-27 backstop red (40515158924, `toContain`) ran on a tree from before #3516, which had already fixed it.
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
# gstack digest v1.91.296.0 — regenerate/re-copy after upgrading gstack
|
||||
# gstack digest v1.91.297.0 — regenerate/re-copy after upgrading gstack
|
||||
|
||||
Behavioral rules from gstack (https://github.com/garrytan/gstack), compressed
|
||||
for agent hosts without a full skill install. The full skills add workflows,
|
||||
|
||||
@@ -24,9 +24,10 @@ import { atomicWriteSync } from '../../lib/fs-atomic';
|
||||
import { readPidCmdline, readPidStartTime } from './xvfb';
|
||||
import { emptyBunConfig, findWindowsBunExe, type BunExeLookup } from './bun-child-config';
|
||||
|
||||
function agentProcessInfo(pid: number): { startTime: string; commandLine: string } {
|
||||
if (!Number.isSafeInteger(pid) || pid <= 0) return { startTime: '', commandLine: '' };
|
||||
if (process.platform !== 'win32') return { startTime: readPidStartTime(pid), commandLine: readPidCmdline(pid) };
|
||||
const FILETIME_UNIX_EPOCH_MS = 11_644_473_600_000n;
|
||||
let win32Kernel: any;
|
||||
|
||||
function windowsProcessQuery(pid: number): { startTime: string; commandLine: string } {
|
||||
const script = `(Get-CimInstance Win32_Process -Filter 'ProcessId = ${pid}' | Select-Object CreationDate,CommandLine | ConvertTo-Json -Compress)`;
|
||||
try {
|
||||
const result = spawnSync('powershell.exe', ['-NoProfile', '-NonInteractive', '-Command', script], { encoding: 'utf8', windowsHide: true, timeout: 2000 });
|
||||
@@ -36,8 +37,40 @@ function agentProcessInfo(pid: number): { startTime: string; commandLine: string
|
||||
} catch { return { startTime: '', commandLine: '' }; }
|
||||
}
|
||||
|
||||
export const win32CreationTimeSource = {
|
||||
read(pid: number): string | undefined {
|
||||
if (!process.versions.bun) return;
|
||||
try {
|
||||
const { dlopen, FFIType, ptr } = require('bun:ffi');
|
||||
win32Kernel ??= dlopen('kernel32.dll', {
|
||||
OpenProcess: { args: [FFIType.u32, FFIType.i32, FFIType.u32], returns: FFIType.u64 },
|
||||
GetProcessTimes: { args: [FFIType.u64, FFIType.ptr, FFIType.ptr, FFIType.ptr, FFIType.ptr], returns: FFIType.i32 },
|
||||
CloseHandle: { args: [FFIType.u64], returns: FFIType.i32 },
|
||||
}).symbols;
|
||||
const handle = win32Kernel.OpenProcess(0x1000, 0, pid);
|
||||
if (!handle) return;
|
||||
try {
|
||||
const times = new BigUint64Array(4);
|
||||
if (!win32Kernel.GetProcessTimes(handle, ptr(times, 0), ptr(times, 8), ptr(times, 16), ptr(times, 24)) || times[0] === 0n) return;
|
||||
return `/Date(${times[0] / 10_000n - FILETIME_UNIX_EPOCH_MS})/`;
|
||||
} finally {
|
||||
win32Kernel.CloseHandle(handle);
|
||||
}
|
||||
} catch { return; }
|
||||
},
|
||||
};
|
||||
|
||||
function agentProcessInfo(pid: number): { startTime: string; commandLine: string } {
|
||||
if (!Number.isSafeInteger(pid) || pid <= 0) return { startTime: '', commandLine: '' };
|
||||
if (process.platform !== 'win32') return { startTime: readPidStartTime(pid), commandLine: readPidCmdline(pid) };
|
||||
const query = windowsProcessQuery(pid);
|
||||
return { startTime: win32CreationTimeSource.read(pid) ?? query.startTime, commandLine: query.commandLine };
|
||||
}
|
||||
|
||||
export function readAgentStartTime(pid: number): string {
|
||||
return agentProcessInfo(pid).startTime;
|
||||
if (!Number.isSafeInteger(pid) || pid <= 0) return '';
|
||||
if (process.platform !== 'win32') return readPidStartTime(pid);
|
||||
return win32CreationTimeSource.read(pid) ?? windowsProcessQuery(pid).startTime;
|
||||
}
|
||||
|
||||
const pendingAgentExits = new Set<any>();
|
||||
@@ -335,7 +368,8 @@ function agentStatus(record: AgentRecord, ownerPid?: number): 'owned' | 'gone' |
|
||||
if (ownerPid !== undefined && (record.ownerPid !== ownerPid || record.ownerStartTime !== readAgentStartTime(ownerPid))) return 'unknown';
|
||||
const actual = agentProcessInfo(record.pid);
|
||||
if (!actual.startTime) return isAgentRecordLive(record) ? 'unknown' : 'gone';
|
||||
if (actual.startTime !== record.startTime) return 'gone';
|
||||
if (actual.startTime !== record.startTime && process.platform !== 'win32') return 'gone';
|
||||
if (actual.startTime !== record.startTime && !actual.commandLine) return 'unknown';
|
||||
try {
|
||||
let state: string | undefined;
|
||||
if (process.platform === 'linux') {
|
||||
@@ -348,7 +382,7 @@ function agentStatus(record: AgentRecord, ownerPid?: number): 'owned' | 'gone' |
|
||||
} catch {}
|
||||
if (!isAgentRecordLive(record)) return 'gone';
|
||||
return actual.commandLine.split(/\s+/).some(arg => arg.replace(/^['"]|['"]$/g, '') === `--agent-gen=${record.gen}`)
|
||||
? 'owned' : 'unknown';
|
||||
? 'owned' : actual.startTime === record.startTime ? 'unknown' : 'gone';
|
||||
}
|
||||
|
||||
export function isOurAgent(record: AgentRecord, ownerPid?: number): boolean {
|
||||
|
||||
@@ -999,9 +999,9 @@ function safeNativeEnvelope(output: string): object {
|
||||
|
||||
function assertOwnerReceipt(receipt: { available: boolean; owners: { pid: number; creationMatched: boolean; isTestHost: boolean }[] }, pid: number) {
|
||||
const before = JSON.stringify(receipt);
|
||||
expect(receipt.available).toBe(true);
|
||||
expect(Array.isArray(receipt.owners)).toBe(true);
|
||||
expect(receipt.owners.some(owner => owner.pid === pid && owner.creationMatched && owner.isTestHost)).toBe(true);
|
||||
expect(receipt.available, before).toBe(true);
|
||||
expect(Array.isArray(receipt.owners), before).toBe(true);
|
||||
expect(receipt.owners.some(owner => owner.pid === pid && owner.creationMatched && owner.isTestHost), before).toBe(true);
|
||||
expect(JSON.stringify(receipt)).toBe(before);
|
||||
}
|
||||
|
||||
|
||||
@@ -11,6 +11,7 @@ import { afterAll, beforeAll, describe, expect, test } from 'bun:test';
|
||||
import * as fs from 'node:fs';
|
||||
import * as os from 'node:os';
|
||||
import * as path from 'node:path';
|
||||
import { dlopen, FFIType, ptr } from 'bun:ffi';
|
||||
|
||||
const VALIDATOR = path.resolve(import.meta.dir, '../src/path-security.ts');
|
||||
const WIN = process.platform === 'win32';
|
||||
@@ -43,11 +44,16 @@ function validate(tempDir: string, checks: Array<['output' | 'read', string]>):
|
||||
}
|
||||
|
||||
function shortPath(dir: string): string {
|
||||
const result = Bun.spawnSync(['powershell', '-NoProfile', '-NonInteractive', '-Command',
|
||||
`(New-Object -ComObject Scripting.FileSystemObject).GetFolder($env:GSTACK_LONG_DIR).ShortPath`], {
|
||||
env: { ...process.env, GSTACK_LONG_DIR: dir }, stdout: 'pipe', stderr: 'pipe', timeout: 30_000,
|
||||
});
|
||||
return result.stdout.toString().trim();
|
||||
const kernel = dlopen('kernel32.dll', { GetShortPathNameW: { args: [FFIType.ptr, FFIType.ptr, FFIType.u32], returns: FFIType.u32 } });
|
||||
try {
|
||||
const input = Buffer.from(dir + '\0', 'utf16le');
|
||||
const output = Buffer.alloc(65536);
|
||||
const length = kernel.symbols.GetShortPathNameW(ptr(input), ptr(output), 32768);
|
||||
if (length === 0 || length >= 32768) throw new Error(`GetShortPathNameW failed for ${dir} (returned ${length})`);
|
||||
return output.subarray(0, length * 2).toString('utf16le');
|
||||
} finally {
|
||||
kernel.close();
|
||||
}
|
||||
}
|
||||
|
||||
describe('#3104 one realpath for safe dirs and targets', () => {
|
||||
|
||||
@@ -4,7 +4,10 @@ import * as fs from 'fs';
|
||||
import * as os from 'os';
|
||||
import * as path from 'path';
|
||||
import { isProcessAlive } from '../src/error-handling';
|
||||
import { spawnTerminalAgent } from '../src/terminal-agent-control';
|
||||
import {
|
||||
isAgentRecordGone, isOurAgent, readAgentStartTime, spawnTerminalAgent, stopAgentByRecord, win32CreationTimeSource, writeAgentRecord,
|
||||
type AgentRecord,
|
||||
} from '../src/terminal-agent-control';
|
||||
|
||||
// REGRESSION TEST for the Windows terminal-agent leak.
|
||||
//
|
||||
@@ -117,8 +120,10 @@ describe('process liveness probe (Windows terminal-agent leak)', () => {
|
||||
}
|
||||
return originalProbe(command, options);
|
||||
}) as typeof Bun.spawnSync);
|
||||
const powershellCalls: string[] = [];
|
||||
const windowsProbe = spyOn(childProcess, 'spawnSync').mockImplementation(((command: string, args: string[], options: any) => {
|
||||
if (command === 'powershell.exe') {
|
||||
powershellCalls.push(args.join(' '));
|
||||
const owner = args.join(' ').includes(`ProcessId = ${process.pid}'`);
|
||||
return { status: 0, stdout: JSON.stringify(owner ? { CreationDate: 'fixture-owner-start', CommandLine: 'test-owner' } : null), stderr: '' };
|
||||
}
|
||||
@@ -143,7 +148,14 @@ describe('process liveness probe (Windows terminal-agent leak)', () => {
|
||||
// Owner-PID lifetime tie (#2019): the agent polls this and exits when
|
||||
// its owning browse server dies, so it can't be adopted by PID 1.
|
||||
expect(captured.env.BROWSE_OWNER_PID).toBe(String(process.pid));
|
||||
expect(captured.env.BROWSE_OWNER_START_TIME).toBe('fixture-owner-start');
|
||||
if (process.platform === 'win32') {
|
||||
expect(captured.env.BROWSE_OWNER_START_TIME).toMatch(/^\/Date\(\d+\)\/$/);
|
||||
expect(captured.env.BROWSE_OWNER_START_TIME).toBe(readAgentStartTime(process.pid));
|
||||
expect(powershellCalls.filter(call => call.includes(`ProcessId = ${process.pid}'`))).toEqual([]);
|
||||
expect(powershellCalls.every(call => call.includes("ProcessId = 2147483647'"))).toBe(true);
|
||||
} else {
|
||||
expect(captured.env.BROWSE_OWNER_START_TIME).toBe('fixture-owner-start');
|
||||
}
|
||||
// Detached background daemon — must not inherit a terminal either.
|
||||
expect(captured.stdio).toEqual(['ignore', 'ignore', 'ignore']);
|
||||
await exited;
|
||||
@@ -174,3 +186,145 @@ describe('process liveness probe (Windows terminal-agent leak)', () => {
|
||||
expect(windowMs).toBeGreaterThanOrEqual(tick * guardMax);
|
||||
});
|
||||
});
|
||||
|
||||
describe('win32 terminal-agent identity (GSTA-139)', () => {
|
||||
const pid = 2147483645;
|
||||
const gen = 'win32-identity-generation';
|
||||
const birth = '/Date(1761600000123)/';
|
||||
const ownedCommand = `"C:\\bun\\bun.exe" run terminal-agent.ts --agent-gen=${gen}`;
|
||||
const record = (startTime = birth): AgentRecord => ({ pid, gen, startTime, startedAt: 0, ownerPid: pid, ownerStartTime: birth });
|
||||
type PowerShell = { status: number | null; creationDate?: string; commandLine?: string };
|
||||
|
||||
function onWin32(scenario: { ffi: string | undefined; powershell: PowerShell }, run: (calls: { powershell: number; signals: unknown[] }) => void): void {
|
||||
const platform = Object.getOwnPropertyDescriptor(process, 'platform')!;
|
||||
const calls = { powershell: 0, signals: [] as unknown[] };
|
||||
let live = true;
|
||||
const kill = spyOn(process, 'kill').mockImplementation(((target: number, signal: unknown) => {
|
||||
expect(target).toBe(pid);
|
||||
if (!live) throw Object.assign(new Error('gone'), { code: 'ESRCH' });
|
||||
if (signal !== 0) { calls.signals.push(signal); live = false; }
|
||||
return true;
|
||||
}) as typeof process.kill);
|
||||
const read = spyOn(win32CreationTimeSource, 'read').mockImplementation((target: number) => (target === pid && live ? scenario.ffi : undefined));
|
||||
const probe = spyOn(childProcess, 'spawnSync').mockImplementation(((command: string) => {
|
||||
expect(command).toBe('powershell.exe');
|
||||
calls.powershell++;
|
||||
const { status, creationDate, commandLine } = scenario.powershell;
|
||||
return status === 0
|
||||
? { status, stdout: JSON.stringify({ CreationDate: creationDate, CommandLine: commandLine }), stderr: '' }
|
||||
: { status, stdout: '', stderr: '', error: Object.assign(new Error('spawnSync powershell.exe ETIMEDOUT'), { code: 'ETIMEDOUT' }) };
|
||||
}) as typeof childProcess.spawnSync);
|
||||
Object.defineProperty(process, 'platform', { ...platform, value: 'win32' });
|
||||
try {
|
||||
run(calls);
|
||||
} finally {
|
||||
Object.defineProperty(process, 'platform', platform);
|
||||
probe.mockRestore();
|
||||
read.mockRestore();
|
||||
kill.mockRestore();
|
||||
}
|
||||
}
|
||||
|
||||
test('the start time is read in-process, so a PowerShell launch killed at its timeout cannot empty it', () => {
|
||||
onWin32({ ffi: birth, powershell: { status: null } }, calls => {
|
||||
expect(readAgentStartTime(pid)).toBe(birth);
|
||||
expect(calls.powershell).toBe(0);
|
||||
});
|
||||
});
|
||||
|
||||
test('without the in-process reader (Node daemon) the start time falls back to the PowerShell CreationDate', () => {
|
||||
onWin32({ ffi: undefined, powershell: { status: 0, creationDate: birth, commandLine: ownedCommand } }, calls => {
|
||||
expect(readAgentStartTime(pid)).toBe(birth);
|
||||
expect(calls.powershell).toBe(1);
|
||||
});
|
||||
});
|
||||
|
||||
test('kill-time ownership still requires the generation in the PowerShell command line', () => {
|
||||
onWin32({ ffi: birth, powershell: { status: 0, creationDate: birth, commandLine: ownedCommand } }, calls => {
|
||||
expect(isOurAgent(record())).toBe(true);
|
||||
expect(stopAgentByRecord(record(), 0)).toBe(true);
|
||||
expect(calls.signals).toEqual(['SIGTERM']);
|
||||
});
|
||||
for (const powershell of [{ status: 0, creationDate: birth, commandLine: 'bun run unrelated.ts' }, { status: null }]) {
|
||||
onWin32({ ffi: birth, powershell }, calls => {
|
||||
expect(isOurAgent(record())).toBe(false);
|
||||
expect(isAgentRecordGone(record())).toBe(false);
|
||||
expect(stopAgentByRecord(record(), 0)).toBe(false);
|
||||
expect(calls.signals).toEqual([]);
|
||||
});
|
||||
}
|
||||
onWin32({ ffi: '/Date(1761600000124)/', powershell: { status: 0, creationDate: birth, commandLine: 'bun run unrelated.ts' } }, calls => {
|
||||
expect(isAgentRecordGone(record())).toBe(true);
|
||||
expect(calls.signals).toEqual([]);
|
||||
});
|
||||
});
|
||||
|
||||
test('a start time that disagrees across readers still identifies the live agent by its generation, so no second agent starts', () => {
|
||||
const daemonRecorded = '/Date(1761596400123)/';
|
||||
onWin32({ ffi: birth, powershell: { status: 0, creationDate: daemonRecorded, commandLine: ownedCommand } }, calls => {
|
||||
expect(isAgentRecordGone(record(daemonRecorded))).toBe(false);
|
||||
expect(isOurAgent(record(daemonRecorded))).toBe(true);
|
||||
expect(stopAgentByRecord(record(daemonRecorded), 0)).toBe(true);
|
||||
expect(calls.signals).toEqual(['SIGTERM']);
|
||||
});
|
||||
onWin32({ ffi: birth, powershell: { status: null } }, calls => {
|
||||
expect(isAgentRecordGone(record(daemonRecorded))).toBe(false);
|
||||
expect(stopAgentByRecord(record(daemonRecorded), 0)).toBe(false);
|
||||
expect(calls.signals).toEqual([]);
|
||||
});
|
||||
});
|
||||
|
||||
test('an unreadable start time is unknown: never signalled and never treated as gone', () => {
|
||||
onWin32({ ffi: undefined, powershell: { status: null } }, calls => {
|
||||
expect(readAgentStartTime(pid)).toBe('');
|
||||
expect(isOurAgent(record())).toBe(false);
|
||||
expect(isAgentRecordGone(record())).toBe(false);
|
||||
expect(stopAgentByRecord(record(), 0)).toBe(false);
|
||||
expect(calls.signals).toEqual([]);
|
||||
});
|
||||
});
|
||||
|
||||
test.skipIf(process.platform !== 'win32')('the in-process start time equals the CreationDate earlier releases recorded', () => {
|
||||
const child = Bun.spawn([process.execPath, '-e', 'setInterval(() => {}, 1000)'], { stdio: ['ignore', 'ignore', 'ignore'] });
|
||||
try {
|
||||
for (const target of [process.pid, child.pid]) {
|
||||
const script = `(Get-CimInstance Win32_Process -Filter 'ProcessId = ${target}' | Select-Object CreationDate | ConvertTo-Json -Compress)`;
|
||||
const legacy = childProcess.spawnSync('powershell.exe', ['-NoProfile', '-NonInteractive', '-Command', script], { encoding: 'utf8', windowsHide: true, timeout: 60_000 });
|
||||
expect(legacy.status).toBe(0);
|
||||
const recorded = JSON.parse(legacy.stdout).CreationDate;
|
||||
expect(recorded).toMatch(/^\/Date\(\d+\)\/$/);
|
||||
expect(win32CreationTimeSource.read(target)).toBe(recorded);
|
||||
expect(readAgentStartTime(target)).toBe(recorded);
|
||||
}
|
||||
} finally {
|
||||
child.kill();
|
||||
}
|
||||
}, 150_000);
|
||||
|
||||
test.skipIf(process.platform !== 'win32')('a live agent whose identity cannot be read blocks a second agent', () => {
|
||||
const tmpDir = fs.mkdtempSync(path.join(os.tmpdir(), 'gstack-unknown-'));
|
||||
const script = path.join(tmpDir, 'fake-agent.ts');
|
||||
fs.writeFileSync(script, '// no-op\n');
|
||||
const live = Bun.spawn([process.execPath, '-e', 'setInterval(() => {}, 1000)'], { stdio: ['ignore', 'ignore', 'ignore'] });
|
||||
writeAgentRecord(tmpDir, { pid: live.pid, gen, startTime: birth, startedAt: 0, ownerPid: process.pid, ownerStartTime: birth });
|
||||
const original = childProcess.spawnSync;
|
||||
const probe = spyOn(childProcess, 'spawnSync').mockImplementation(((command: string, args: string[], options: any) => (
|
||||
command === 'powershell.exe' ? { status: null, stdout: '', stderr: '' } : original(command, args, options)
|
||||
)) as typeof childProcess.spawnSync);
|
||||
const read = spyOn(win32CreationTimeSource, 'read').mockImplementation(() => undefined);
|
||||
const origSpawn = (Bun as any).spawn;
|
||||
let spawned = 0;
|
||||
(Bun as any).spawn = () => { spawned++; throw new Error('a second agent must not start'); };
|
||||
try {
|
||||
expect(spawnTerminalAgent({ stateFile: path.join(tmpDir, 'state.json'), serverPort: 12345, ownerPid: process.pid, cwd: tmpDir, scriptPath: script })).toBeNull();
|
||||
expect(spawned).toBe(0);
|
||||
expect(isProcessAlive(live.pid)).toBe(true);
|
||||
} finally {
|
||||
(Bun as any).spawn = origSpawn;
|
||||
read.mockRestore();
|
||||
probe.mockRestore();
|
||||
live.kill();
|
||||
fs.rmSync(tmpDir, { recursive: true, force: true, maxRetries: 5 });
|
||||
}
|
||||
});
|
||||
});
|
||||
+1
-1
@@ -1,6 +1,6 @@
|
||||
{
|
||||
"name": "gstack",
|
||||
"version": "1.91.296",
|
||||
"version": "1.91.297",
|
||||
"description": "Garry's Stack — Claude Code skills + fast headless browser. One repo, one install, entire AI engineering workflow.",
|
||||
"license": "MIT",
|
||||
"type": "module",
|
||||
|
||||
Reference in new issue
Block a user