chore: import upstream snapshot with attribution
This commit is contained in:
@@ -0,0 +1,222 @@
|
||||
/*
|
||||
* repro_issue409.c — Reproduce-first case for OPEN bug #409.
|
||||
*
|
||||
* Issue #409: "v0.7.0 install/update wires the legacy blocking PreToolUse
|
||||
* gate, not hook_augment (regresses #214)"
|
||||
*
|
||||
* Root cause (as filed):
|
||||
* cbm_install_hook_gate_script wrote the legacy blocking shell gate
|
||||
* (keyed on $PPID, emitting `exit 2` to block tool calls) instead of the
|
||||
* non-blocking augmenter shim that delegates to `<binary> hook-augment`.
|
||||
* On an upgrade from a pre-v0.7.0 install the old gate script remained on
|
||||
* disk (or was rewritten with blocking content), so every Grep/Glob call
|
||||
* was blocked rather than being non-blocking augmented — the exact symptom
|
||||
* of #214 which was supposed to be fixed.
|
||||
*
|
||||
* Expected (correct) behaviour after cbm_upsert_claude_hooks +
|
||||
* cbm_install_hook_gate_script:
|
||||
* 1. The gate script written to
|
||||
* <home>/.claude/hooks/cbm-code-discovery-gate
|
||||
* MUST contain "hook-augment" (delegating to the compiled augmenter).
|
||||
* 2. The gate script MUST NOT contain "PPID" (the $PPID-keyed blocking
|
||||
* logic) or "exit 2" (the blocking exit code).
|
||||
* 3. The settings.json PreToolUse command must reference
|
||||
* "cbm-code-discovery-gate" (the shim), not an inline blocking script.
|
||||
*
|
||||
* Actual (buggy) behaviour (if bug is present):
|
||||
* The gate script still contains $PPID and exit 2; the assertions below
|
||||
* that check for absence of "PPID" and "exit 2" FAIL -> RED.
|
||||
*
|
||||
* Upgrade scenario tested here (NOT covered by existing tests):
|
||||
* This test simulates an upgrade from a pre-v0.7.0 install by:
|
||||
* a) Pre-seeding the gate-script path with the OLD blocking content
|
||||
* (containing $PPID and exit 2) — as would be present on disk after
|
||||
* a pre-v0.7.0 install.
|
||||
* b) Pre-seeding settings.json with a stale CMM hook entry using the
|
||||
* old "Grep|Glob|Read" matcher and an old command string.
|
||||
* Then running both cbm_upsert_claude_hooks + cbm_install_hook_gate_script
|
||||
* (the actual install/update code path) and asserting the CORRECT result.
|
||||
*
|
||||
* This is the critical gap: existing tests call cbm_install_hook_gate_script
|
||||
* into an EMPTY directory (no pre-existing script). The upgrade path
|
||||
* (old script on disk) was not verified to be overwritten correctly.
|
||||
*
|
||||
* Relationship to existing tests:
|
||||
* cli_hook_gate_script_no_predictable_tmp_issue384 (test_cli.c:2196):
|
||||
* Tests cbm_install_hook_gate_script in isolation on a fresh dir.
|
||||
* Does NOT test the upgrade/overwrite scenario.
|
||||
* cli_upsert_claude_hook_fresh (test_cli.c:2167):
|
||||
* Tests cbm_upsert_claude_hooks in isolation on fresh settings.json.
|
||||
* Does NOT test the integrated (both calls) upgrade path.
|
||||
*
|
||||
* NOTE (2026-06-26): Code review of the current codebase shows that
|
||||
* cbm_install_hook_gate_script already uses fopen(path, "w") (truncate)
|
||||
* and writes the non-blocking shim. If this test is GREEN it means the bug
|
||||
* is fixed on main and the issue can be closed (the test then acts as a
|
||||
* permanent regression guard for this upgrade scenario).
|
||||
*/
|
||||
|
||||
#include <foundation/compat.h>
|
||||
#include "test_framework.h"
|
||||
#include "test_helpers.h"
|
||||
#include <cli/cli.h>
|
||||
#include <string.h>
|
||||
#include <stdlib.h>
|
||||
#include <stdio.h>
|
||||
#include <sys/stat.h>
|
||||
#include <unistd.h>
|
||||
#include <errno.h>
|
||||
|
||||
/* ── Local helpers (mirror the helpers in test_cli.c) ──────────────── */
|
||||
|
||||
static int rp409_write_file(const char *path, const char *content) {
|
||||
FILE *f = fopen(path, "w");
|
||||
if (!f)
|
||||
return -1;
|
||||
fprintf(f, "%s", content);
|
||||
fclose(f);
|
||||
return 0;
|
||||
}
|
||||
|
||||
static const char *rp409_read_file(const char *path) {
|
||||
static char buf[16384];
|
||||
FILE *f = fopen(path, "r");
|
||||
if (!f)
|
||||
return NULL;
|
||||
size_t n = fread(buf, 1, sizeof(buf) - 1, f);
|
||||
fclose(f);
|
||||
buf[n] = '\0';
|
||||
return buf;
|
||||
}
|
||||
|
||||
/* Recursively create directory (simple two-level: parent + child). */
|
||||
static int rp409_mkdirp(const char *path) {
|
||||
char tmp[1024];
|
||||
snprintf(tmp, sizeof(tmp), "%s", path);
|
||||
for (char *p = tmp + 1; *p; p++) {
|
||||
if (*p == '/') {
|
||||
*p = '\0';
|
||||
cbm_mkdir(tmp);
|
||||
*p = '/';
|
||||
}
|
||||
}
|
||||
return cbm_mkdir(tmp) == 0 || errno == EEXIST ? 0 : -1;
|
||||
}
|
||||
|
||||
/* ── Test ──────────────────────────────────────────────────────────── */
|
||||
|
||||
/*
|
||||
* repro_issue409_install_wires_hook_augment_not_blocking_gate
|
||||
*
|
||||
* Simulates an upgrade from a pre-v0.7.0 install:
|
||||
* - The hooks dir already contains the OLD blocking gate script
|
||||
* (containing $PPID and exit 2).
|
||||
* - settings.json already contains a stale CMM hook with the old matcher
|
||||
* "Grep|Glob|Read" and an old inline command.
|
||||
*
|
||||
* After calling cbm_upsert_claude_hooks + cbm_install_hook_gate_script
|
||||
* (the actual install/update flow), asserts that:
|
||||
* 1. The gate script is OVERWRITTEN with the non-blocking shim
|
||||
* (contains "hook-augment", does NOT contain "PPID" or "exit 2").
|
||||
* 2. settings.json PreToolUse command references "cbm-code-discovery-gate"
|
||||
* (the shim path), not inline blocking code.
|
||||
* 3. settings.json uses the current non-blocking matcher "Grep|Glob"
|
||||
* (not the old "Grep|Glob|Read" that was silently upgrading Read-gating
|
||||
* behaviour).
|
||||
*
|
||||
* RED if:
|
||||
* - The gate script still contains "PPID" (old blocking logic not cleared)
|
||||
* - The gate script still contains "exit 2" (old blocking exit not cleared)
|
||||
* - The gate script does NOT contain "hook-augment" (shim not written)
|
||||
* - settings.json does NOT contain "cbm-code-discovery-gate" (wrong command)
|
||||
*
|
||||
* Oracle used: cbm_upsert_claude_hooks(settings_path) +
|
||||
* cbm_install_hook_gate_script(home, binary_path)
|
||||
* (the same two calls made by install_claude_code_config in cli.c).
|
||||
*/
|
||||
TEST(repro_issue409_install_wires_hook_augment_not_blocking_gate) {
|
||||
/* Create a temp HOME directory tree that simulates a pre-v0.7.0 install. */
|
||||
char tmpdir[256];
|
||||
snprintf(tmpdir, sizeof(tmpdir), "/tmp/rp409-XXXXXX");
|
||||
if (!cbm_mkdtemp(tmpdir))
|
||||
FAIL("cbm_mkdtemp failed");
|
||||
|
||||
/* Create <home>/.claude/hooks/ (mirrors real Claude Code layout). */
|
||||
char hooks_dir[512];
|
||||
snprintf(hooks_dir, sizeof(hooks_dir), "%s/.claude/hooks", tmpdir);
|
||||
if (rp409_mkdirp(hooks_dir) != 0)
|
||||
FAIL("mkdirp hooks_dir failed");
|
||||
|
||||
/* Pre-seed the gate script with the OLD blocking content that the issue
|
||||
* reporter observed on v0.7.0. This is the content that must be
|
||||
* overwritten (truncated) by cbm_install_hook_gate_script. */
|
||||
char script_path[512];
|
||||
snprintf(script_path, sizeof(script_path),
|
||||
"%s/cbm-code-discovery-gate", hooks_dir);
|
||||
rp409_write_file(script_path,
|
||||
"#!/bin/bash\n"
|
||||
"# Gate hook: nudges Claude toward codebase-memory-mcp for code discovery.\n"
|
||||
"# First Grep/Glob/Read per session -> block. Subsequent -> allow.\n"
|
||||
"# PPID = Claude Code process PID, unique per session.\n"
|
||||
"GATE=/tmp/cbm-code-discovery-gate-$PPID\n"
|
||||
"if [ -f \"$GATE\" ]; then exit 0; fi\n"
|
||||
"touch \"$GATE\"\n"
|
||||
"echo 'BLOCKED: use codebase-memory-mcp' >&2\n"
|
||||
"exit 2\n");
|
||||
|
||||
/* Pre-seed settings.json with a stale CMM hook entry (old matcher). */
|
||||
char settings_path[512];
|
||||
snprintf(settings_path, sizeof(settings_path),
|
||||
"%s/.claude/settings.json", tmpdir);
|
||||
rp409_write_file(settings_path,
|
||||
"{\"hooks\":{\"PreToolUse\":["
|
||||
"{\"matcher\":\"Grep|Glob|Read\","
|
||||
"\"hooks\":[{\"type\":\"command\","
|
||||
"\"command\":\"~/.claude/hooks/cbm-code-discovery-gate\"}]}]}}");
|
||||
|
||||
/* Run the actual install/update hook wiring (same two calls as
|
||||
* install_claude_code_config in src/cli/cli.c lines 3045-3046). */
|
||||
int rc = cbm_upsert_claude_hooks(settings_path);
|
||||
ASSERT_EQ(rc, 0);
|
||||
cbm_install_hook_gate_script(tmpdir, "/usr/local/bin/codebase-memory-mcp");
|
||||
|
||||
/* ── Assert the gate script was OVERWRITTEN with the non-blocking shim ── */
|
||||
const char *script_data = rp409_read_file(script_path);
|
||||
ASSERT_NOT_NULL(script_data);
|
||||
|
||||
/* MUST NOT contain $PPID: the old blocking gate used
|
||||
* /tmp/cbm-code-discovery-gate-$PPID as a per-invocation state file.
|
||||
* If present, the blocking gate was not overwritten -> RED for #409. */
|
||||
ASSERT(strstr(script_data, "PPID") == NULL);
|
||||
|
||||
/* MUST NOT contain "exit 2": the old gate blocked tool calls with exit 2.
|
||||
* If present, the installer still emits the blocking exit code -> RED. */
|
||||
ASSERT(strstr(script_data, "exit 2") == NULL);
|
||||
|
||||
/* MUST contain "hook-augment": the non-blocking shim delegates to the
|
||||
* compiled augmenter via `"$BIN" hook-augment 2>/dev/null`.
|
||||
* If absent, install did not write the correct shim -> RED for #409. */
|
||||
ASSERT(strstr(script_data, "hook-augment") != NULL);
|
||||
|
||||
/* ── Assert settings.json was updated to the correct non-blocking config ── */
|
||||
const char *settings_data = rp409_read_file(settings_path);
|
||||
ASSERT_NOT_NULL(settings_data);
|
||||
|
||||
/* The PreToolUse command must reference the shim (by its well-known name),
|
||||
* not an inline blocking script. */
|
||||
ASSERT(strstr(settings_data, "cbm-code-discovery-gate") != NULL);
|
||||
|
||||
/* The old "Grep|Glob|Read" matcher (which gated Read calls, breaking
|
||||
* the read-before-edit invariant per issue #362) must have been replaced
|
||||
* with the current "Grep|Glob" matcher. */
|
||||
ASSERT(strstr(settings_data, "\"Grep|Glob\"") != NULL);
|
||||
ASSERT(strstr(settings_data, "Glob|Read") == NULL);
|
||||
|
||||
th_rmtree(tmpdir);
|
||||
PASS();
|
||||
}
|
||||
|
||||
/* ── Suite ──────────────────────────────────────────────────────────── */
|
||||
SUITE(repro_issue409) {
|
||||
RUN_TEST(repro_issue409_install_wires_hook_augment_not_blocking_gate);
|
||||
}
|
||||
Reference in New Issue
Block a user