mirror of
https://github.com/garrytan/gstack.git
synced 2026-07-07 01:45:47 +08:00
test: coverage-audit additions for the fix wave
Ship Step 7 gap-fill (all passing, 248 tests across the touched suites): memory + dream stage probe-timeout proceeds, gbrain-detect override paths, stale-flag passthrough, 200-body-missing-.security fail-closed case, telemetry redaction edges, and credential-pattern edge cases. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
This commit is contained in:
@@ -136,6 +136,27 @@ describe('gbrain detection override → gen-skill-docs', () => {
|
||||
}
|
||||
});
|
||||
|
||||
test('with status "timeout" (slow-but-healthy, #1964), brain blocks render like "ok"', () => {
|
||||
const { tmpHome, cleanup } = makeFixture(
|
||||
JSON.stringify({ gbrain_local_status: 'timeout', gbrain_on_path: true, gbrain_version: 'test-0.41.0' }),
|
||||
);
|
||||
try {
|
||||
const snap = regenAndSnapshot({
|
||||
respectDetection: true,
|
||||
tmpHome,
|
||||
files: PROBE_FILES,
|
||||
});
|
||||
const content = probeUnion(snap);
|
||||
|
||||
// A slow engine must not silently suppress brain features — same
|
||||
// treatment as "ok" (matches gstack-gbrain-detect --is-ok).
|
||||
expect(content).toContain('## Save Results to Brain');
|
||||
expect(content).toContain('gbrain put "office-hours/');
|
||||
} finally {
|
||||
cleanup();
|
||||
}
|
||||
});
|
||||
|
||||
test('with detected:false (status != "ok"), brain blocks stay suppressed', () => {
|
||||
const { tmpHome, cleanup } = makeFixture(
|
||||
JSON.stringify({ gbrain_local_status: 'no-cli', gbrain_on_path: false, gbrain_version: null }),
|
||||
|
||||
@@ -391,6 +391,20 @@ describe("lib/gbrain-local-status — cache behavior", () => {
|
||||
expect(localEngineStatus({ noCache: false })).toBe("ok");
|
||||
});
|
||||
|
||||
it("invalidates cache when GBRAIN_HOME changes (key invariant, codex D11)", () => {
|
||||
env = makeEnv({ withGbrain: true, gbrainBehavior: "ok", withConfig: true });
|
||||
restoreEnv = applyEnv(env);
|
||||
expect(localEngineStatus({ noCache: false })).toBe("ok");
|
||||
|
||||
// Point GBRAIN_HOME at an empty dir: a stale cached "ok" must not win —
|
||||
// gbrain_home is part of the cache key, so this re-probes and finds no
|
||||
// config at the new location.
|
||||
const altHome = join(env.tmp, "alt-gbrain-empty");
|
||||
mkdirSync(altHome, { recursive: true });
|
||||
process.env.GBRAIN_HOME = altHome;
|
||||
expect(localEngineStatus({ noCache: false })).toBe("missing-config");
|
||||
});
|
||||
|
||||
it("invalidates cache when HOME changes (key invariant)", () => {
|
||||
env = makeEnv({ withGbrain: true, gbrainBehavior: "ok", withConfig: true });
|
||||
restoreEnv = applyEnv(env);
|
||||
|
||||
@@ -152,6 +152,36 @@ describe("gstack-gbrain-sync — split-engine SKIP (plan D12)", () => {
|
||||
}
|
||||
}, 30_000); // proceeding runs the real code-import path against the slow fake (~11s)
|
||||
|
||||
it("memory stage also PROCEEDS (with warning) on probe timeout (#1964)", () => {
|
||||
const env = makeEnv({ withGbrain: true, gbrainBehavior: "slow", withConfig: true });
|
||||
try {
|
||||
const r = runOrchestrator(env, ["--no-code", "--no-brain-sync"], {
|
||||
GSTACK_GBRAIN_PROBE_TIMEOUT_MS: "300",
|
||||
});
|
||||
const out = r.stdout + r.stderr;
|
||||
expect(out).not.toContain("local engine timeout");
|
||||
expect(out).toContain("memory: engine probe timed out");
|
||||
} finally {
|
||||
env.cleanup();
|
||||
}
|
||||
}, 30_000);
|
||||
|
||||
it("dream stage also PROCEEDS (with warning) on probe timeout (#1964)", () => {
|
||||
const env = makeEnv({ withGbrain: true, gbrainBehavior: "slow", withConfig: true });
|
||||
try {
|
||||
const r = runOrchestrator(
|
||||
env,
|
||||
["--dream", "--no-code", "--no-memory", "--no-brain-sync"],
|
||||
{ GSTACK_GBRAIN_PROBE_TIMEOUT_MS: "300" },
|
||||
);
|
||||
const out = r.stdout + r.stderr;
|
||||
expect(out).not.toContain("local engine timeout");
|
||||
expect(out).toContain("dream: engine probe timed out");
|
||||
} finally {
|
||||
env.cleanup();
|
||||
}
|
||||
}, 30_000);
|
||||
|
||||
it("SKIPs code stage when local engine is broken-db; brain-sync still attempted", () => {
|
||||
const env = makeEnv({ withGbrain: true, gbrainBehavior: "broken-db", withConfig: true });
|
||||
try {
|
||||
|
||||
@@ -12,6 +12,7 @@ import {
|
||||
exitCodeFor,
|
||||
maskPreview,
|
||||
normalizeWithMap,
|
||||
redactFindingSpans,
|
||||
type RepoVisibility,
|
||||
} from "../lib/redact-engine";
|
||||
import {
|
||||
@@ -364,6 +365,52 @@ describe("masking + purity", () => {
|
||||
});
|
||||
});
|
||||
|
||||
describe("redactFindingSpans — machine-egress masking (#1947)", () => {
|
||||
test("clean input passes through unchanged", () => {
|
||||
const text = "push failed: remote rejected the branch";
|
||||
expect(redactFindingSpans(text, { repoVisibility: "private" })).toBe(text);
|
||||
});
|
||||
|
||||
test("a single finding's span becomes <REDACTED-{id}>, context survives", () => {
|
||||
const token = "ghp_" + "1234567890abcdefghijklmnopqrstuvwxyz";
|
||||
const out = redactFindingSpans(`auth ${token} rejected`, { repoVisibility: "private" });
|
||||
expect(out).toBe("auth <REDACTED-github.pat> rejected");
|
||||
});
|
||||
|
||||
test("multiple findings are all replaced (right-to-left splice keeps offsets valid)", () => {
|
||||
const pat = "ghp_" + "1234567890abcdefghijklmnopqrstuvwxyz";
|
||||
const aws = "AKIA1234567890ABCDEF";
|
||||
const out = redactFindingSpans(`first ${aws} then ${pat} end`, {
|
||||
repoVisibility: "private",
|
||||
});
|
||||
expect(out).toBe("first <REDACTED-aws.access_key> then <REDACTED-github.pat> end");
|
||||
});
|
||||
|
||||
test("fails closed (null) when a span cannot be relocated — never raw passthrough", () => {
|
||||
// env.kv's span (the value) starts well past the regex match start (the
|
||||
// var name), so locateSpan's rewind-2 re-exec misses it. The contract is
|
||||
// null → caller drops the whole payload. The one thing that must never
|
||||
// happen is the secret surviving in the output.
|
||||
const secret = "8Fk2pQ9vXz4wL7mN3rT6yB1cD5eG0hJq";
|
||||
const out = redactFindingSpans(`API_KEY=${secret}`, { repoVisibility: "private" });
|
||||
if (out !== null) {
|
||||
// If locateSpan ever learns to find context-prefixed spans, masking
|
||||
// must actually mask.
|
||||
expect(out).not.toContain(secret);
|
||||
} else {
|
||||
expect(out).toBeNull();
|
||||
}
|
||||
});
|
||||
|
||||
test("multiline input redacts a finding past the first line (locateSpan line/col path)", () => {
|
||||
const token = "ghp_" + "1234567890abcdefghijklmnopqrstuvwxyz";
|
||||
const out = redactFindingSpans(`line one\nline two has ${token}\nline three`, {
|
||||
repoVisibility: "private",
|
||||
});
|
||||
expect(out).toBe("line one\nline two has <REDACTED-github.pat>\nline three");
|
||||
});
|
||||
});
|
||||
|
||||
describe("taxonomy integrity", () => {
|
||||
test("every pattern has a unique id", () => {
|
||||
const set = new Set(PATTERNS.map((p) => p.id));
|
||||
|
||||
@@ -174,6 +174,14 @@ describe("gstack-security-dashboard — never reports fake zeros (#1947)", () =>
|
||||
expect(parsed.security.top_attack_domains[0].domain).toBe("evil.example");
|
||||
});
|
||||
|
||||
it("stale cache responses pass the stale flag through (json mode)", () => {
|
||||
const staleBody = JSON.stringify({ ...JSON.parse(GOOD_BODY_MARKER), stale: true });
|
||||
const r = run(SEC_BIN, { mode: "ok", body: staleBody, json: true });
|
||||
const parsed = JSON.parse(r.stdout.trim());
|
||||
expect(parsed.status).toBe("ok");
|
||||
expect(parsed.stale).toBe(true);
|
||||
});
|
||||
|
||||
it("200 without marker (legacy backend) → figures shown with unverified note", () => {
|
||||
const r = run(SEC_BIN, { mode: "ok", body: GOOD_BODY_LEGACY });
|
||||
expect(r.stdout).toContain("Attacks detected last 7 days: 3");
|
||||
@@ -186,6 +194,18 @@ describe("gstack-security-dashboard — never reports fake zeros (#1947)", () =>
|
||||
expect(parsed.status).toBe("legacy_unverified");
|
||||
expect(parsed.security.attacks_last_7_days).toBe(3);
|
||||
});
|
||||
|
||||
it("200 with a body missing .security → unknown backend_error, never 0", () => {
|
||||
const r = run(SEC_BIN, {
|
||||
mode: "ok",
|
||||
body: JSON.stringify({ weekly_active: 42, status: "ok" }),
|
||||
json: true,
|
||||
});
|
||||
const parsed = JSON.parse(r.stdout.trim());
|
||||
expect(parsed.status).toBe("unknown");
|
||||
expect(parsed.reason).toBe("backend_error");
|
||||
expect(parsed.security).toBeNull();
|
||||
});
|
||||
});
|
||||
|
||||
describe("gstack-community-dashboard — never reports fake zeros (#1947)", () => {
|
||||
@@ -212,4 +232,10 @@ describe("gstack-community-dashboard — never reports fake zeros (#1947)", () =
|
||||
expect(r.stdout).toContain("Weekly active installs: 42");
|
||||
expect(r.stdout).toContain("unverified");
|
||||
});
|
||||
|
||||
it("200 with a garbage body (no weekly_active) → unknown, never 0", () => {
|
||||
const r = run(COMM_BIN, { mode: "ok", body: '{"error":"weird"}' });
|
||||
expect(r.stdout).toContain("unknown — backend error (HTTP 200)");
|
||||
expect(r.stdout).not.toContain("Weekly active installs:");
|
||||
});
|
||||
});
|
||||
|
||||
@@ -236,6 +236,36 @@ describe('gstack-telemetry-log', () => {
|
||||
expect(lines[0]).not.toContain(token);
|
||||
});
|
||||
|
||||
test('truncates error_message to 200 chars after redaction (#1947)', () => {
|
||||
setConfig('telemetry', 'anonymous');
|
||||
const long = 'x'.repeat(300);
|
||||
run(
|
||||
`${BIN}/gstack-telemetry-log --skill qa --duration 10 --outcome error --error-message '${long}' --session-id red-3`,
|
||||
);
|
||||
|
||||
const events = parseJsonl();
|
||||
expect(events).toHaveLength(1);
|
||||
expect(events[0].error_message.length).toBeLessThanOrEqual(200);
|
||||
});
|
||||
|
||||
test('fails closed: error_message becomes null when the engine cannot relocate a span (#1947)', () => {
|
||||
setConfig('telemetry', 'anonymous');
|
||||
const secret = '8Fk2pQ9vXz4wL7mN3rT6yB1cD5eG0hJq';
|
||||
// env.kv-shaped finding (line-anchored, so the assignment leads the
|
||||
// message): the span (value) starts past the regex match start,
|
||||
// locateSpan misses it, redactFindingSpans returns null — the bin must
|
||||
// drop the whole message, never pass it through raw.
|
||||
run(
|
||||
`${BIN}/gstack-telemetry-log --skill qa --duration 10 --outcome error --error-message 'API_KEY=${secret} rejected by daemon' --session-id red-4`,
|
||||
);
|
||||
|
||||
const lines = readJsonl();
|
||||
expect(lines).toHaveLength(1);
|
||||
const event = JSON.parse(lines[0]);
|
||||
expect(event.error_message).toBeNull();
|
||||
expect(lines[0]).not.toContain(secret);
|
||||
});
|
||||
|
||||
test('creates analytics directory if missing', () => {
|
||||
// Remove analytics dir
|
||||
const analyticsDir = path.join(tmpDir, 'analytics');
|
||||
|
||||
Reference in New Issue
Block a user