mirror of
https://github.com/garrytan/gstack.git
synced 2026-10-31 22:14:25 +01:00
* 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>
84 lines
4.5 KiB
TypeScript
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');
|
|
});
|
|
});
|