Files
gstack/test/gstack-launch-probe.test.ts
T
Garry Tanandcapy-ai[bot] 4b8c03bea9 v1.91.294.0 fix(doctor): cut Git Bash process starts so the doctor tests fit Windows CI (GSTA-138) (#3540)
* perf(doctor): cut Git Bash process starts in doctor, hook-check and launch probe (GSTA-138)

Co-authored-by: capy-ai[bot] <230910855+capy-ai[bot]@users.noreply.github.com>

* fix: pre-landing review fixes

The doctor's in-process config read reselected the state root after the
doctor set GSTACK_STATE_ROOT, so --check and --json printed 'selected by
GSTACK_STATE_ROOT' whatever selected the root. cfg now keeps _gstack_sr_var.
Adds the regression test, a CRLF/blank/200-byte launch-probe detail test,
and corrects the clamp comment.

Co-authored-by: capy-ai[bot] <230910855+capy-ai[bot]@users.noreply.github.com>

* test: check the doctor cores row's test_backend by behavior, not the reader's spelling

test/owner-brief-contributor-banned.test.ts pinned the literal
'gstack-config" get test_backend' in bin/gstack-doctor-check.sh, which the
in-process cfg read removed (CI: quick-bun-default, free-suite 14). It now
runs gstack-doctor --check --require cores against a state root holding
test_backend: ubicloud and expects test_backend=ubicloud, with an empty
state root as the negative control (test_backend=local). The under-8-cores
wording pin stays.

Co-authored-by: capy-ai[bot] <230910855+capy-ai[bot]@users.noreply.github.com>

* chore: CHANGELOG [Unreleased] entry for the doctor's Windows process cuts

Co-authored-by: capy-ai[bot] <230910855+capy-ai[bot]@users.noreply.github.com>

* chore: stamp v1.91.294.0 for #3540

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>
2026-10-28 13:05:02 +00:00

84 lines
4.5 KiB
TypeScript

import { afterEach, 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 { runBashScript } from './helpers/bash-script';
// #2595/#2124: Smart App Control blocks unsigned binaries at launch and Git
// Bash reports it as "Permission denied" (exit 126). bin/gstack-launch-probe.sh
// runs each binary's --version and tells blocked from broken from healthy.
// The exact Windows text is unverified (no SAC machine); these cases use the
// shapes reporters captured in #2595 and #2124.
const ROOT = path.resolve(import.meta.dir, '..');
const PROBE = path.join(ROOT, 'bin/gstack-launch-probe.sh');
const tmps: string[] = [];
afterEach(() => { for (const t of tmps.splice(0)) fs.rmSync(t, { recursive: true, force: true }); });
function fakeBinary(body: string | null, mode = 0o755) {
const dir = fs.mkdtempSync(path.join(os.tmpdir(), 'gstack-launch-probe-'));
tmps.push(dir);
const file = path.join(dir, 'tool');
if (body !== null) fs.writeFileSync(file, `#!/bin/sh\n${body}\n`, { mode });
return file;
}
function probe(file: string) {
const r = runBashScript(`. ${JSON.stringify(PROBE)}\ngstack_launch_probe ${JSON.stringify(file)}\nprintf 'state=%s\\ndetail=%s\\n' "$_glp_state" "$_glp_detail"`, { timeout: 30_000 });
expect(r.status).toBe(0);
return { state: r.stdout.match(/^state=(.*)$/m)![1], detail: r.stdout.match(/^detail=(.*)$/m)![1] };
}
// POSIX execute bits stand in for a refused launch; Windows has no such bit,
// and a real SAC block cannot be produced in CI.
describe.skipIf(process.platform === 'win32')('gstack-launch-probe.sh classification', () => {
test('a binary that answers --version is native', () => {
expect(probe(fakeBinary('[ "$1" = --version ] && echo 1.2.3'))).toEqual({ state: 'native', detail: '1.2.3' });
});
test('a binary the OS refuses to execute is blocked (Git Bash "Permission denied", exit 126)', () => {
const r = probe(fakeBinary('echo never', 0o644));
expect(r.state).toBe('blocked');
expect(r.detail).toContain('Permission denied');
});
test('an application-control message is blocked even with another exit code', () => {
const r = probe(fakeBinary("echo \"Program 'browse.exe' failed to run: An Application Control policy has blocked this file\" >&2; exit 1"));
expect(r.state).toBe('blocked');
expect(r.detail).toContain('An Application Control policy has blocked this file');
});
test('a binary that launches and exits non-zero is broken, not blocked', () => {
expect(probe(fakeBinary('echo "Unknown argument: --version" >&2; exit 3'))).toEqual({ state: 'broken', detail: 'exit 3: Unknown argument: --version' });
});
// Value: protects=the detail is the first non-blank line with CRs dropped, cut to 200 bytes (Windows CRLF output);
// fails_when=the builtin line picker keeps a CR, returns a blank line or cuts at another length;
// why_new=every other case prints one clean LF line; seam=none; no_claim=real Git Bash output (a sh fake prints the bytes)
test('a broken binary\'s detail is its first non-blank line, CRs dropped, at most 200 bytes', () => {
const long = 'y'.repeat(250);
expect(probe(fakeBinary(`printf '\\r\\n \\n\\t\\r\\n${long}\\r\\nnext\\n'; exit 3`))).toEqual({ state: 'broken', detail: `exit 3: ${'y'.repeat(200)}` });
});
test('an absent binary is missing', () => {
expect(probe(fakeBinary(null)).state).toBe('missing');
});
test('probe_all groups blocked, broken and missing binaries by repo path', () => {
const root = fs.mkdtempSync(path.join(os.tmpdir(), 'gstack-launch-root-'));
tmps.push(root);
const write = (rel: string, body: string, mode = 0o755) => {
fs.mkdirSync(path.dirname(path.join(root, rel)), { recursive: true });
fs.writeFileSync(path.join(root, rel), `#!/bin/sh\n${body}\n`, { mode });
};
write('browse/dist/browse.exe', 'echo x', 0o644);
write('browse/dist/find-browse.exe', 'echo ok');
write('design/dist/design.exe', 'echo ok');
write('make-pdf/dist/pdf.exe', 'exit 2');
const r = runBashScript(`. ${JSON.stringify(PROBE)}\ngstack_launch_probe_all ${JSON.stringify(root)} .exe\nprintf 'blocked=[%s]\\nbroken=[%s]\\nmissing=[%s]\\n' "$_glp_blocked" "$_glp_broken" "$_glp_missing"`, { timeout: 30_000 });
expect(r.stdout).toContain('blocked=[browse/dist/browse\t');
expect(r.stdout).toContain('broken=[make-pdf/dist/pdf\texit 2');
expect(r.stdout).toContain('missing=[bin/gstack-global-discover]');
expect(r.stdout).not.toContain('design/dist/design');
});
});