Files
gbrain/test/e2e/claw-test.test.ts
Garry TanandClaude Fable 5 7fdcd8bd2e v0.45.12.0 feat(claw-test): hermes e2e install harness — runner, real-binary door, CI job, friction diff (#4058)
* chore(guards): allow the public Hermes platform name in tests

The banned entry targeted conflating the public NousResearch agent with
private deployment names. gbrain now documents and tests against the
public platform (README hero, claw-test runner, install door e2e), so
the public name is legal in tests; private fork names remain banned.
Drops the three now-inert allowlist entries.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>

* feat(claw-test): hermes runner, live staging + success oracles, friction diff

HermesRunner (hermes -z one-shot, HERMES_BIN > which hermes, allowlist
env with HERMES_HOME + OPENROUTER_API_KEY delta). Live mode now stages
the scenario before the agent turn (fresh-install: brain + routing stub
+ init; upgrade: seed-first) and verifies outcomes after it: doctor
must parse and report healthy/warnings, scenario-declared query +
files_exist oracles are enforced for every kind, and upgrades use a
non-mutating schema-version probe that must reach LATEST_VERSION.
Missing upgrade seed dumps fail loudly in BOTH modes (a silent skip
false-greened the upgrade lane). Bare gbrain in live runs resolves
through a per-run PATH shim; when gbrain itself runs under the bun
runtime the harness synthesizes a launcher back into cli.ts instead of
handing children the bun binary.

gbrain friction diff --base/--compare: identity is (kind, phase,
digit-collapsed 80-char prefix); severity compares as a per-severity
distribution (integer proportion test) so redistribution and
delight-to-friction flips always surface; run start/end phase markers
carry agent + scenario for agent-name resolution.

Hardening from the adversarial gate: every harness child runs under a
wall-clock timeout with process-group kill + exit-fallback settle;
scenario names and declared brief/brain/seed paths are confined to the
scenario dir; child friction merges require a regular file, cap size,
and keep only valid JSONL lines; crashed runs stamp a non-zero end
marker; GBRAIN_* routing vars are scrubbed from child env; agent
stdin closes at spawn; argv agent/scenario values are charset-guarded.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>

* test(hermes): real-binary install door e2e + generic workspace compat

Door e2e registers this checkout's gbrain into a hermetic Hermes home
via the real CLI (single --env flag with multiple values, piped
confirm, enabled:true + mcp test as the success discriminators), the
direct-YAML surface, and a paid one-shot smoke turn proving MCP recall
of a seeded synthetic fact with a NO-GBRAIN-TOOL negative control.
Triple-gated (opt-in env + resolvable binary + non-empty anthropic key)
so it can never burn tokens by accident; anthropic-only auth because a
second visible provider key mis-routes hermes provider auto-detection.
Helpers copy exactly ONE provider key from the operator's env file,
never the whole file, and scrub all provider keys from child env.

workspace-generic-compat pins the documented any-repo-with-a-workspace
install flow (detection tier, scaffold additivity, resolver health) on
a generic fixture; the Hermes-behavior proof lives in the door test.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>

* ci: label-gated hermes-door job + e2e hermeticity scrub

hermes-door provisions a pinned Hermes release: installer digest
verified before execution, payload tag+commit flags ASSERTED post-
install via rev-parse (an installer that ignores unknown flags can
never run unpinned upstream code next to secrets), secretless install
step, loud-fail preconditions, zero-pass-refuses-green, evidence
scrubbed three ways before upload, and unconditional credential
cleanup for self-hosted-runner safety. real-agent-e2e gains the door
file + opt-in env. run-e2e.sh scrubs HERMES_* alongside OPENCLAW_*;
e2e-test-map narrows claw-test core changes to their e2e suite.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>

* docs: hermes + openclaw MCP guides, CLI pin notes, harness reference updates

Per-client docs for Hermes (observed-behavior guide incl. flag-order
and multi-key gotchas) and OpenClaw; HERMES-CLI-PIN records every
pinned CLI behavior + the CI pin posture. README MCP table rows,
INSTALL_FOR_AGENTS hermes block, TESTING/KEY_FILES current-state
rewrites (two runners, oracle semantics, diff identity), TODOS closure
(hermes runner done, friction diff shipped, follow-ups filed) and the
llms bundle regenerated in the same commit.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>

* chore: bump version and changelog (v0.45.10.0)

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>

* docs: release sync — verb-count drift, hermes link, discovery rows, harness knobs

Cross-referencing the diff against every .md surfaced drift beyond this
wave: the memory-verbs surface prose still said five verbs (the frozen
protocol grew context_pack + delta additively), docs/INSTALL.md linked
a wrong Hermes repo and missed the new HERMES/OPENCLAW per-client
guides, the door-suite doc pinned a tool COUNT that tracks the op
catalog, the friction protocol skill missed the diff subcommand, and
the claw-test KEY_FILES entry lacked the harness env knobs. Comment
counts in heavy-tests.yml corrected (three triggers; four door tests).
llms bundle regenerated in the same commit.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>

* ci: quote inner expansion in evidence-scrub path strip (shellcheck SC2295)

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>

* chore: re-bump to v0.45.12.0 (user-pinned past the contested 0.45.11.0 slot)

Two sibling PRs already claim 0.45.11.0; pinning one slot higher avoids
a second merge-race re-bump. All version locations move together:
VERSION, package.json, CHANGELOG entry header, openclaw.plugin.json,
bootstrap runbook stamp, regenerated template stamp, CLAUDE.md example
cell, llms bundle.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>

---------

Co-authored-by: Claude Fable 5 <noreply@anthropic.com>
2026-08-13 18:22:08 -07:00

429 lines
19 KiB
TypeScript

/**
* gbrain claw-test scripted-mode E2E.
*
* Invokes the harness via `bun run src/cli.ts` (NOT a compiled binary —
* `bun build --compile` doesn't bundle PGLite's runtime assets like
* pglite.data, so a compiled gbrain can't init a fresh PGLite brain).
* Uses a tiny shim script that the harness can spawn as if it were the
* gbrain binary.
*
* Asserts:
* - exit code 0 on a clean tree
* - the friction JSONL has zero error/blocker entries
* - the harness recorded progress events for the expected phases
*
* Tagged-skip env: CLAW_TEST_SKIP_E2E=1 to opt out (e.g. when PGLite
* WASM is broken on the host — the macOS 26.3 #223 bug class).
*/
import { describe, test, expect, beforeAll } from 'bun:test';
import { execFileSync, spawnSync } from 'child_process';
import { mkdirSync, existsSync, mkdtempSync, rmSync, readFileSync, readdirSync, writeFileSync, chmodSync } from 'fs';
import { tmpdir } from 'os';
import { join, resolve } from 'path';
const REPO_ROOT = resolve(import.meta.dir, '..', '..');
const BIN_CACHE = join(REPO_ROOT, 'test', '.cache');
const BIN_PATH = join(BIN_CACHE, 'gbrain.sh');
const SCENARIOS_DIR = join(REPO_ROOT, 'test', 'fixtures', 'claw-test-scenarios');
beforeAll(() => {
if (!existsSync(BIN_CACHE)) mkdirSync(BIN_CACHE, { recursive: true });
// Shim that delegates to `bun run src/cli.ts` so PGLite assets resolve from
// the source tree (bun --compile doesn't bundle them). Marked executable so
// child_process.spawn can run it directly.
const shim = `#!/bin/sh\nexec bun run "${join(REPO_ROOT, 'src', 'cli.ts')}" "$@"\n`;
writeFileSync(BIN_PATH, shim, 'utf-8');
chmodSync(BIN_PATH, 0o755);
}, 30_000);
describe('gbrain claw-test --scenario fresh-install (scripted)', () => {
test('runs end-to-end clean and produces zero error/blocker friction', () => {
const tmp = mkdtempSync(join(tmpdir(), 'claw-test-e2e-fresh-'));
try {
const result = spawnSync(BIN_PATH, ['claw-test', '--scenario', 'fresh-install', '--keep-tempdir'], {
cwd: REPO_ROOT,
env: {
...process.env,
GBRAIN_HOME: tmp,
GBRAIN_BIN_OVERRIDE: BIN_PATH,
GBRAIN_CLAW_SCENARIOS_DIR: join(REPO_ROOT, 'test', 'fixtures', 'claw-test-scenarios'),
},
encoding: 'utf-8',
timeout: 120_000,
});
if (result.status !== 0) {
console.error('STDOUT:', result.stdout);
console.error('STDERR:', result.stderr);
}
expect(result.status).toBe(0);
// Inspect the friction JSONL the harness wrote.
const frictionDir = join(tmp, '.gbrain', 'friction');
expect(existsSync(frictionDir)).toBe(true);
const files = readdirSync(frictionDir).filter(f => f.endsWith('.jsonl'));
expect(files.length).toBeGreaterThan(0);
const runFile = join(frictionDir, files[0]);
const lines = readFileSync(runFile, 'utf-8').split('\n').filter(l => l.trim());
const entries = lines.map(l => JSON.parse(l));
const blockers = entries.filter(e => e.kind === 'friction' && (e.severity === 'error' || e.severity === 'blocker'));
if (blockers.length > 0) {
console.error('unexpected friction entries:', blockers);
}
expect(blockers.length).toBe(0);
// REGRESSION pin: scripted runs stamp agent 'scripted' (previously the
// misleading parseArgs default 'openclaw'). friction diff's agent-name
// resolution reads this stamp off the start marker.
expect(entries[0].kind).toBe('phase-marker');
expect(entries[0].marker).toBe('start');
expect(entries[0].agent).toBe('scripted');
expect(entries[0].scenario).toBe('fresh-install');
} finally {
rmSync(tmp, { recursive: true, force: true });
}
}, 180_000);
test('break path: an invented command produces an error friction entry and exits non-zero', () => {
// We do this by setting GBRAIN_BIN_OVERRIDE to a script that pretends to be gbrain
// and rejects the `import` subcommand specifically.
const tmp = mkdtempSync(join(tmpdir(), 'claw-test-e2e-break-'));
const fakeBin = join(tmp, 'fake-gbrain');
try {
// Write a shim that delegates to real gbrain but rejects 'import' to simulate breakage.
const shimContent = `#!/bin/sh\nif [ "$1" = "import" ]; then echo "fake import error" >&2; exit 17; fi\nexec "${BIN_PATH}" "$@"\n`;
const { writeFileSync, chmodSync } = require('fs');
writeFileSync(fakeBin, shimContent, 'utf-8');
chmodSync(fakeBin, 0o755);
const result = spawnSync(BIN_PATH, ['claw-test', '--scenario', 'fresh-install', '--keep-tempdir'], {
cwd: REPO_ROOT,
env: {
...process.env,
GBRAIN_HOME: tmp,
GBRAIN_BIN_OVERRIDE: fakeBin,
GBRAIN_CLAW_SCENARIOS_DIR: join(REPO_ROOT, 'test', 'fixtures', 'claw-test-scenarios'),
},
encoding: 'utf-8',
timeout: 60_000,
});
expect(result.status).not.toBe(0);
// The friction log should have an error-severity entry for the 'import' phase.
const frictionDir = join(tmp, '.gbrain', 'friction');
const files = readdirSync(frictionDir).filter(f => f.endsWith('.jsonl'));
const lines = readFileSync(join(frictionDir, files[0]), 'utf-8').split('\n').filter(l => l.trim());
const entries = lines.map(l => JSON.parse(l));
const importErrors = entries.filter(e => e.phase === 'import' && e.severity === 'error');
expect(importErrors.length).toBeGreaterThan(0);
} finally {
rmSync(tmp, { recursive: true, force: true });
}
}, 90_000);
});
describe('gbrain claw-test --list-agents', () => {
test('reports both built-in runners (available or not — both valid states)', () => {
// HERMES_BIN/OPENCLAW_BIN point at a nonexistent path so the output shape
// is deterministic regardless of what's installed on the box (detect
// rejects a non-stat-able absolute path with a specific reason).
const result = spawnSync(BIN_PATH, ['claw-test', '--list-agents'], {
cwd: REPO_ROOT,
env: { ...process.env, HERMES_BIN: '/nonexistent/hermes', OPENCLAW_BIN: '/nonexistent/openclaw' },
encoding: 'utf-8',
timeout: 60_000,
});
expect(result.status).toBe(0);
expect(result.stdout).toMatch(/^hermes: unavailable: /m);
expect(result.stdout).toMatch(/^openclaw: unavailable: /m);
// Alphabetical print order (the awaited-detection fix pins this).
expect(result.stdout.indexOf('hermes:')).toBeLessThan(result.stdout.indexOf('openclaw:'));
}, 60_000);
});
describe('gbrain claw-test --live (shim agents; no real agent binary, no tokens)', () => {
// The OpenClaw runner honors an absolute $OPENCLAW_BIN pointing at any
// executable, so these tests drive live mode with tiny sh shims. That
// exercises the REAL live path: staging, the PATH shim, the agent turn,
// the success oracle, and the E0 friction merge.
function runLiveWithShim(shimBody: string, scenario: string, extraEnv: Record<string, string> = {}) {
const tmp = mkdtempSync(join(tmpdir(), 'claw-test-e2e-live-'));
const shim = join(tmp, 'agent-shim');
writeFileSync(shim, shimBody, 'utf-8');
chmodSync(shim, 0o755);
const result = spawnSync(BIN_PATH, ['claw-test', '--live', '--agent', 'openclaw', '--scenario', scenario], {
cwd: REPO_ROOT,
env: {
...process.env,
GBRAIN_HOME: tmp,
OPENCLAW_BIN: shim,
GBRAIN_BIN_OVERRIDE: BIN_PATH,
GBRAIN_CLAW_SCENARIOS_DIR: extraEnv.GBRAIN_CLAW_SCENARIOS_DIR ?? SCENARIOS_DIR,
...extraEnv,
},
encoding: 'utf-8',
timeout: 180_000,
});
const frictionDirPath = join(tmp, '.gbrain', 'friction');
const entries: any[] = [];
if (existsSync(frictionDirPath)) {
for (const f of readdirSync(frictionDirPath).filter(f => f.endsWith('.jsonl'))) {
for (const line of readFileSync(join(frictionDirPath, f), 'utf-8').split('\n')) {
if (line.trim()) entries.push(JSON.parse(line));
}
}
}
return { tmp, result, entries };
}
test('oracle break path: a do-nothing agent that exits 0 now FAILS the run', () => {
const { tmp, result, entries } = runLiveWithShim('#!/bin/sh\nexit 0\n', 'fresh-install');
try {
expect(result.status).not.toBe(0);
const verifyErrors = entries.filter(e => e.phase === 'verify' && e.severity === 'error');
expect(verifyErrors.length).toBeGreaterThan(0);
// The run-start meta record (agent-name resolution depends on it) is the
// FIRST line and carries agent + scenario.
expect(entries[0].kind).toBe('phase-marker');
expect(entries[0].marker).toBe('start');
expect(entries[0].agent).toBe('openclaw');
expect(entries[0].scenario).toBe('fresh-install');
} finally {
rmSync(tmp, { recursive: true, force: true });
}
}, 180_000);
test('working agent passes the oracle AND its child-side friction survives tempdir cleanup (E0 merge)', () => {
// The shim does the brief's work via BARE `gbrain` (proving the PATH shim
// resolves to this checkout) and logs one agent-side friction entry, which
// lands under the run's hermetic GBRAIN_HOME — the dir the harness deletes.
const shim = [
'#!/bin/sh',
'set -e',
'gbrain import ./brain --no-embed',
'gbrain skillpack scaffold query --workspace "$PWD"',
'gbrain friction log --phase agent-side --severity nit --message "child entry survives cleanup"',
'',
].join('\n');
const { tmp, result, entries } = runLiveWithShim(shim, 'fresh-install');
try {
if (result.status !== 0) {
console.error('STDOUT:', result.stdout);
console.error('STDERR:', result.stderr);
}
expect(result.status).toBe(0);
const blockers = entries.filter(e => e.kind === 'friction' && (e.severity === 'error' || e.severity === 'blocker'));
expect(blockers.length).toBe(0);
// E0: the child-side entry was written under the (deleted) runRoot but
// must appear in the parent's friction file.
const childSide = entries.filter(e => e.phase === 'agent-side' && e.message.includes('child entry survives cleanup'));
expect(childSide.length).toBe(1);
// Completion marker paired with the start marker.
const endMarkers = entries.filter(e => e.kind === 'phase-marker' && e.marker === 'end' && e.phase === 'harness');
expect(endMarkers.length).toBe(1);
} finally {
rmSync(tmp, { recursive: true, force: true });
}
}, 180_000);
test('REGRESSION (upgrade): staging seeds first and the schema-version oracle fails a do-nothing agent', () => {
// Synthetic upgrade scenario: a minimal "old brain" dump that records
// config.version = 1. The do-nothing agent never advances the migration
// chain, so the non-mutating pre/post probe must fail the run. (A
// doctor-based oracle would auto-migrate on connect and pass vacuously —
// the exact bug class this oracle exists to prevent.)
const scenRoot = mkdtempSync(join(tmpdir(), 'claw-test-e2e-upg-scen-'));
const scenDir = join(scenRoot, 'upgrade-synthetic');
mkdirSync(join(scenDir, 'seed'), { recursive: true });
writeFileSync(join(scenDir, 'scenario.json'), JSON.stringify({
kind: 'upgrade',
from_version: '0.0.1',
expected_phases: [],
seed: 'seed',
}), 'utf-8');
writeFileSync(join(scenDir, 'BRIEF.md'), '# Upgrade brief\n\nRun `gbrain doctor --json` to walk the migration chain forward.\n', 'utf-8');
writeFileSync(join(scenDir, 'seed', 'dump.sql'), [
"CREATE TABLE IF NOT EXISTS config (key TEXT PRIMARY KEY, value TEXT);",
"INSERT INTO config (key, value) VALUES ('version', '1');",
'',
].join('\n'), 'utf-8');
const { tmp, result, entries } = runLiveWithShim('#!/bin/sh\nexit 0\n', 'upgrade-synthetic', {
GBRAIN_CLAW_SCENARIOS_DIR: scenRoot,
});
try {
expect(result.status).not.toBe(0);
const verifyErrors = entries.filter(e => e.phase === 'verify' && e.severity === 'error');
expect(verifyErrors.length).toBe(1);
expect(verifyErrors[0].message).toContain('did not advance');
// pre=1 proves the seeded PGLite existed BEFORE the agent turn.
expect(verifyErrors[0].message).toContain('pre=1');
} finally {
rmSync(tmp, { recursive: true, force: true });
rmSync(scenRoot, { recursive: true, force: true });
}
}, 180_000);
});
describe('gbrain claw-test — adversarial-gate regression pins', () => {
test('P1: without GBRAIN_BIN_OVERRIDE the harness synthesizes a bun launcher (children run gbrain, not bun)', () => {
// Under `bun run src/cli.ts`, process.execPath is the Bun runtime; the
// pre-fix fallback handed children `bun init …`, which scaffolds a Bun
// project and fails the rest of the run. Exit 0 proves the synthesized
// launcher resolved children to real gbrain.
const tmp = mkdtempSync(join(tmpdir(), 'claw-test-e2e-noovr-'));
try {
const env: Record<string, string | undefined> = {
...process.env,
GBRAIN_HOME: tmp,
GBRAIN_CLAW_SCENARIOS_DIR: SCENARIOS_DIR,
};
delete env.GBRAIN_BIN_OVERRIDE;
const result = spawnSync(BIN_PATH, ['claw-test', '--scenario', 'fresh-install'], {
cwd: REPO_ROOT,
env: env as NodeJS.ProcessEnv,
encoding: 'utf-8',
timeout: 150_000,
});
if (result.status !== 0) {
console.error('STDOUT:', result.stdout);
console.error('STDERR:', result.stderr);
}
expect(result.status).toBe(0);
} finally {
rmSync(tmp, { recursive: true, force: true });
}
}, 180_000);
test('scripted upgrade with the shipped fixture fails LOUDLY (no false-green upgrade without a seed dump)', () => {
const tmp = mkdtempSync(join(tmpdir(), 'claw-test-e2e-upgloud-'));
try {
const result = spawnSync(BIN_PATH, ['claw-test', '--scenario', 'upgrade-from-v0.18'], {
cwd: REPO_ROOT,
env: {
...process.env,
GBRAIN_HOME: tmp,
GBRAIN_BIN_OVERRIDE: BIN_PATH,
GBRAIN_CLAW_SCENARIOS_DIR: SCENARIOS_DIR,
},
encoding: 'utf-8',
timeout: 60_000,
});
expect(result.status).toBe(1);
expect(result.stderr).toContain('no seed dump');
} finally {
rmSync(tmp, { recursive: true, force: true });
}
}, 90_000);
test('charset guard: traversal-shaped scenario and agent values are usage errors, exit 2', () => {
for (const argv of [
['claw-test', '--scenario', '../evil'],
['claw-test', '--agent', 'x/y', '--scenario', 'fresh-install'],
]) {
const result = spawnSync(BIN_PATH, argv, {
cwd: REPO_ROOT,
env: { ...process.env, GBRAIN_CLAW_SCENARIOS_DIR: SCENARIOS_DIR },
encoding: 'utf-8',
timeout: 30_000,
});
expect(result.status).toBe(2);
expect(result.stderr).toContain('invalid');
}
}, 60_000);
test('phase timeout: a hung gbrain child is killed at GBRAIN_CLAW_PHASE_TIMEOUT_MS instead of wedging the run', () => {
const tmp = mkdtempSync(join(tmpdir(), 'claw-test-e2e-hang-'));
const hangBin = join(tmp, 'hang-gbrain');
try {
writeFileSync(hangBin, `#!/bin/sh\nif [ "$1" = "init" ]; then sleep 45; fi\nexec "${BIN_PATH}" "$@"\n`, 'utf-8');
chmodSync(hangBin, 0o755);
const result = spawnSync(BIN_PATH, ['claw-test', '--scenario', 'fresh-install'], {
cwd: REPO_ROOT,
env: {
...process.env,
GBRAIN_HOME: tmp,
GBRAIN_BIN_OVERRIDE: hangBin,
GBRAIN_CLAW_SCENARIOS_DIR: SCENARIOS_DIR,
GBRAIN_CLAW_PHASE_TIMEOUT_MS: '2000',
},
encoding: 'utf-8',
timeout: 35_000,
});
// If the phase timeout were unwired, init would sleep past spawnSync's
// own kill and status would be null with signal SIGTERM.
expect(result.signal).toBeNull();
expect(result.status).toBe(1);
} finally {
rmSync(tmp, { recursive: true, force: true });
}
}, 60_000);
test('fail-closed doctor oracle: doctor exiting 0 with unparsable output FAILS the live run', () => {
// A wrapper gbrain that answers `doctor` with a non-JSON banner and exit 0
// — the pre-fix oracle only rejected literal "unhealthy" and passed this.
const scratch = mkdtempSync(join(tmpdir(), 'claw-test-e2e-docgarb-'));
const wrapper = join(scratch, 'doctor-garbage-gbrain');
writeFileSync(wrapper, `#!/bin/sh\nif [ "$1" = "doctor" ]; then echo "banner: everything is fine (not json)"; exit 0; fi\nexec "${BIN_PATH}" "$@"\n`, 'utf-8');
chmodSync(wrapper, 0o755);
const workShim = [
'#!/bin/sh',
'set -e',
'gbrain import ./brain --no-embed',
'gbrain skillpack scaffold query --workspace "$PWD"',
'',
].join('\n');
const tmp = mkdtempSync(join(tmpdir(), 'claw-test-e2e-live-docgarb-'));
const shim = join(tmp, 'agent-shim');
writeFileSync(shim, workShim, 'utf-8');
chmodSync(shim, 0o755);
try {
const result = spawnSync(BIN_PATH, ['claw-test', '--live', '--agent', 'openclaw', '--scenario', 'fresh-install'], {
cwd: REPO_ROOT,
env: {
...process.env,
GBRAIN_HOME: tmp,
OPENCLAW_BIN: shim,
GBRAIN_BIN_OVERRIDE: wrapper,
GBRAIN_CLAW_SCENARIOS_DIR: SCENARIOS_DIR,
},
encoding: 'utf-8',
timeout: 180_000,
});
expect(result.status).not.toBe(0);
const frictionDirPath = join(tmp, '.gbrain', 'friction');
const entries: any[] = [];
for (const f of readdirSync(frictionDirPath).filter(f => f.endsWith('.jsonl'))) {
for (const line of readFileSync(join(frictionDirPath, f), 'utf-8').split('\n')) {
if (line.trim()) entries.push(JSON.parse(line));
}
}
const unparsable = entries.filter(e => e.phase === 'verify' && typeof e.message === 'string' && e.message.includes('unparsable'));
expect(unparsable.length).toBe(1);
} finally {
rmSync(tmp, { recursive: true, force: true });
rmSync(scratch, { recursive: true, force: true });
}
}, 180_000);
});
describe('gbrain friction render integration', () => {
test('render produces a markdown report with the redact placeholder', () => {
const tmp = mkdtempSync(join(tmpdir(), 'claw-test-e2e-render-'));
try {
// Log a friction entry with $HOME embedded, then render --redact md
const home = process.env.HOME ?? '/tmp';
const env = { ...process.env, GBRAIN_HOME: tmp, GBRAIN_FRICTION_RUN_ID: 'render-e2e' };
execFileSync(BIN_PATH, ['friction', 'log', '--phase', 'p', '--message', `error at ${home}/.gbrain/x`], { env, encoding: 'utf-8' });
const out = execFileSync(BIN_PATH, ['friction', 'render', '--run-id', 'render-e2e'], { env, encoding: 'utf-8' });
expect(out).toContain('# Friction report');
expect(out).toContain('<HOME>');
// --redact is the default for md, so home itself should not appear.
expect(out).not.toContain(home + '/.gbrain');
} finally {
rmSync(tmp, { recursive: true, force: true });
}
}, 30_000);
});