mirror of
https://github.com/garrytan/gstack.git
synced 2026-09-09 06:28:59 +02:00
spawnSync/execSync/Bun.spawnSync BLOCK the main thread, so bun's in-process per-test timeout can never fire while one waits — a hung child (stdin read, network probe, dead daemon) wedges the whole shard until the runner's external wall-clock SIGKILL. This exact class reached main: free-tests run 33262077256, test/gstack-memory-ingest.test.ts (normally 2.3s) held shard 2 at the 360s wall while its five siblings finished in ~65s. Mechanical sweep in two waves (12 + 4 fan-out agents, every edit verified against its call site): default timeout: 30_000 (matches the free runner's per-test budget), 120_000 for genuinely slow ops (installs, builds, playwright, provider CLIs), helper wrappers fixed ONCE where call sites route through them. Sites that only LOOK like calls (string fixtures, grep needles, comments) were skipped with reasons — the enforcement commit that follows marks them exempt. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
85 lines
3.0 KiB
TypeScript
85 lines
3.0 KiB
TypeScript
/**
|
|
* Locale-independent key validation tests for bin/gstack-config.
|
|
*
|
|
* POSIX bracket ranges such as a-z follow the active collation order. Under
|
|
* GNU grep with tr_TR.UTF-8, that excludes the ASCII letter i and silently
|
|
* breaks most stored preferences. macOS BSD grep does not reproduce the bug,
|
|
* so the source-level tripwire pins the C-locale boundary on every platform.
|
|
*/
|
|
import { afterEach, beforeEach, describe, expect, test } from "bun:test";
|
|
import { spawnSync } from "child_process";
|
|
import * as fs from "fs";
|
|
import * as os from "os";
|
|
import * as path from "path";
|
|
|
|
const ROOT = path.resolve(import.meta.dir, "..");
|
|
const CONFIG = path.join(ROOT, "bin", "gstack-config");
|
|
|
|
let stateRoot: string;
|
|
|
|
function run(args: string[]) {
|
|
const result = spawnSync(CONFIG, args, {
|
|
encoding: "utf8",
|
|
// GSTACK_SETUP_RUNNING suppresses `set skill_prefix`'s auto-relink side
|
|
// effect. Without it, this test invokes the REPO's gstack-config, whose
|
|
// auto-relink resolves the install dir from its own path — i.e. the repo —
|
|
// and gstack-patch-names rewrites all 52 tracked SKILL.md files to
|
|
// gstack-prefixed names, poisoning every downstream test that reads the
|
|
// live tree (observed in the free-tests CI job). Relink behavior itself is
|
|
// covered in isolation by test/relink.test.ts's mock install.
|
|
env: { ...process.env, GSTACK_STATE_ROOT: stateRoot, GSTACK_SETUP_RUNNING: "1" },
|
|
timeout: 30_000,
|
|
});
|
|
|
|
return {
|
|
status: result.status ?? -1,
|
|
stdout: result.stdout ?? "",
|
|
stderr: result.stderr ?? "",
|
|
};
|
|
}
|
|
|
|
beforeEach(() => {
|
|
stateRoot = fs.mkdtempSync(path.join(os.tmpdir(), "gstack-config-locale-"));
|
|
});
|
|
|
|
afterEach(() => {
|
|
fs.rmSync(stateRoot, { recursive: true, force: true });
|
|
});
|
|
|
|
describe("gstack-config key validation is locale-independent", () => {
|
|
test("get, has, and set all validate ASCII ranges under the C locale", () => {
|
|
const source = fs.readFileSync(CONFIG, "utf8");
|
|
const guardedValidators = source.match(
|
|
/LC_ALL=C grep -qE '\^\[a-zA-Z0-9_\]\+\(@\[a-zA-Z0-9\]\+\)\?\$'/g,
|
|
);
|
|
|
|
expect(guardedValidators).toHaveLength(3);
|
|
});
|
|
|
|
test("round-trips existing keys that contain i", () => {
|
|
expect(run(["set", "skill_prefix", "true"]).status).toBe(0);
|
|
|
|
const get = run(["get", "skill_prefix"]);
|
|
expect(get.status).toBe(0);
|
|
expect(get.stdout).toBe("true");
|
|
});
|
|
|
|
test("accepts an existing endpoint-scoped ASCII key", () => {
|
|
expect(run(["set", "brain_trust_policy@local", "personal"]).status).toBe(0);
|
|
|
|
const get = run(["get", "brain_trust_policy@local"]);
|
|
expect(get.status).toBe(0);
|
|
expect(get.stdout).toBe("personal");
|
|
});
|
|
|
|
test("continues to reject non-ASCII keys", () => {
|
|
const set = run(["set", "skïll_prefix", "true"]);
|
|
expect(set.status).toBe(1);
|
|
expect(set.stderr).toContain("key must contain only alphanumeric characters");
|
|
|
|
const get = run(["get", "skïll_prefix"]);
|
|
expect(get.status).toBe(1);
|
|
expect(get.stderr).toContain("key must contain only alphanumeric characters");
|
|
});
|
|
});
|