Skip to content

Commit 262d7ca

Browse files
authored
Merge branch 'main' into codex/fix-cli-fixture-lifetime
2 parents 6f91a16 + 64c23fa commit 262d7ca

23 files changed

Lines changed: 1621 additions & 44 deletions

‎.github/workflows/smoke.yml‎

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,7 +1,7 @@
11
# Smoke invariants across the WIDEST set of GitHub-hosted runners. Builds the
22
# production source path with explicit fault-injection test seams and runs
33
# scripts/smoke-invariants.sh
4-
# (version/help, MCP initialize handshake [#513], all 14 tools invocable, malformed-
4+
# (version/help, MCP initialize handshake [#513], all registered tools invocable, malformed-
55
# input resilience, clean EOF exit, shared-lib resolution, install dry-run).
66
#
77
# Maximizing platforms is the point: ubuntu-22.04 (older glibc → AlmaLinux/#182

‎Makefile.cbm‎

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -449,6 +449,7 @@ PIPELINE_SRCS = \
449449
src/pipeline/pass_gitdiff.c \
450450
src/pipeline/pass_configures.c \
451451
src/pipeline/pass_configlink.c \
452+
src/pipeline/pass_doclinks.c \
452453
src/pipeline/pass_route_nodes.c \
453454
src/pipeline/pass_enrichment.c \
454455
src/pipeline/pass_envscan.c \
@@ -675,7 +676,7 @@ TEST_DISCOVER_SRCS = \
675676

676677
TEST_GRAPH_BUFFER_SRCS = tests/test_graph_buffer.c
677678

678-
TEST_PIPELINE_SRCS = tests/test_registry.c tests/test_pipeline.c tests/test_importance.c tests/test_cross_repo.c tests/test_fqn.c tests/test_route_canon.c tests/test_path_alias.c tests/test_configlink.c tests/test_infrascan.c tests/test_worker_pool.c tests/test_parallel.c tests/test_index_resilience.c tests/test_index_format.c tests/test_call_reference_contract.c tests/repro/repro_call_scope_usages.c tests/repro/repro_call_argument_usages.c tests/repro/repro_reference_precision.c tests/repro/repro_lexical_binding_precision.c tests/repro/repro_call_argument_matrix_a.c tests/repro/repro_call_argument_matrix_b.c tests/repro/repro_call_node_behaviors.c tests/repro/repro_language_registry.c tests/repro/repro_call_node_manifest.c tests/repro/repro_lsp_ordered_signatures.c tests/repro/repro_lsp_ordered_local.c tests/repro/repro_ts_overload_return_chains.c tests/repro/repro_harness_cleanup.c tests/repro/repro_runner_filter.c
679+
TEST_PIPELINE_SRCS = tests/test_registry.c tests/test_pipeline.c tests/test_importance.c tests/test_cross_repo.c tests/test_fqn.c tests/test_route_canon.c tests/test_path_alias.c tests/test_configlink.c tests/test_doclinks.c tests/test_infrascan.c tests/test_worker_pool.c tests/test_parallel.c tests/test_index_resilience.c tests/test_index_format.c tests/test_call_reference_contract.c tests/repro/repro_call_scope_usages.c tests/repro/repro_call_argument_usages.c tests/repro/repro_reference_precision.c tests/repro/repro_lexical_binding_precision.c tests/repro/repro_call_argument_matrix_a.c tests/repro/repro_call_argument_matrix_b.c tests/repro/repro_call_node_behaviors.c tests/repro/repro_language_registry.c tests/repro/repro_call_node_manifest.c tests/repro/repro_lsp_ordered_signatures.c tests/repro/repro_lsp_ordered_local.c tests/repro/repro_ts_overload_return_chains.c tests/repro/repro_harness_cleanup.c tests/repro/repro_runner_filter.c
679680

680681
TEST_WATCHER_SRCS = tests/test_watcher.c
681682

‎README.md‎

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -704,7 +704,7 @@ JSON arguments can also be piped on stdin, for tools that take arguments. A tool
704704

705705
### Edge Types
706706

707-
`CONTAINS_PACKAGE`, `CONTAINS_FOLDER`, `CONTAINS_FILE`, `DEFINES`, `DEFINES_METHOD`, `IMPORTS`, `CALLS`, `CALL_REFERENCE`, `HTTP_CALLS`, `ASYNC_CALLS`, `IMPLEMENTS`, `HANDLES`, `USAGE`, `CONFIGURES`, `WRITES`, `MEMBER_OF`, `TESTS`, `USES_TYPE`, `FILE_CHANGES_WITH`
707+
`CONTAINS_PACKAGE`, `CONTAINS_FOLDER`, `CONTAINS_FILE`, `DEFINES`, `DEFINES_METHOD`, `IMPORTS`, `CALLS`, `CALL_REFERENCE`, `HTTP_CALLS`, `ASYNC_CALLS`, `IMPLEMENTS`, `HANDLES`, `USAGE`, `CONFIGURES`, `REFERENCES_FILE`, `WRITES`, `MEMBER_OF`, `TESTS`, `USES_TYPE`, `FILE_CHANGES_WITH`
708708

709709
### Qualified Names
710710

‎scripts/smoke-invariants.sh‎

Lines changed: 21 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -520,10 +520,10 @@ inv_mcp_initialize() {
520520
return 0
521521
}
522522

523-
# ── Invariant 4: tools/list returns all expected tools ─────────────────────
524-
# Cross-check against the canonical 14-tool list (TOOLS[] in src/mcp/mcp.c).
525-
EXPECTED_TOOLS="index_repository search_graph query_graph trace_path get_code_snippet get_graph_schema get_architecture search_code list_projects delete_project index_status detect_changes manage_adr ingest_traces"
526-
EXPECTED_TOOL_COUNT=14
523+
# ── Invariant 4: tools/list returns exactly the expected tools ─────────────
524+
# Cross-check against the canonical registry (TOOLS[] in src/mcp/mcp.c).
525+
EXPECTED_TOOLS="index_repository search_graph query_graph trace_path get_code_snippet get_file_outline get_graph_schema compare_graphs get_architecture search_code list_projects delete_project index_status check_index_coverage detect_changes manage_adr ingest_traces"
526+
EXPECTED_TOOL_COUNT="$(printf '%s\n' $EXPECTED_TOOLS | grep -c .)"
527527
inv_tools_list() {
528528
if ! mcp_alive; then
529529
fail "tools-list" "server not alive"
@@ -573,18 +573,29 @@ print(r.get("nextCursor") or "")' 2>/dev/null)"
573573
fail "tools-list" "got $got_count tools across $page page(s), expected $EXPECTED_TOOL_COUNT; names=[$got_names]"
574574
return
575575
fi
576-
local missing=""
576+
local missing="" extra="" duplicates=""
577577
local t
578578
for t in $EXPECTED_TOOLS; do
579579
case " $got_names " in
580580
*" $t "*) ;;
581581
*) missing="$missing $t" ;;
582582
esac
583583
done
584+
for t in $got_names; do
585+
case " $EXPECTED_TOOLS " in
586+
*" $t "*) ;;
587+
*) extra="$extra $t" ;;
588+
esac
589+
done
590+
duplicates="$(printf '%s\n' $got_names | sort | uniq -d | tr '\n' ' ')"
584591
if [ -n "$missing" ]; then
585592
fail "tools-list" "missing tools:$missing"
593+
elif [ -n "$extra" ]; then
594+
fail "tools-list" "unexpected tools:$extra"
595+
elif [ -n "$duplicates" ]; then
596+
fail "tools-list" "duplicate tools: $duplicates"
586597
else
587-
pass "tools-list (all $EXPECTED_TOOL_COUNT tools present)"
598+
pass "tools-list (exactly $EXPECTED_TOOL_COUNT unique tools present)"
588599
fi
589600
}
590601

@@ -626,7 +637,7 @@ inv_every_tool() {
626637
return
627638
fi
628639

629-
# name|minimal-args (JSON object) for the remaining 13 tools.
640+
# name|minimal-args (JSON object) for the remaining 16 tools.
630641
# Args chosen to be minimally valid per TOOLS[] required fields.
631642
local p="$PROJ_NAME"
632643
local -a CALLS
@@ -635,11 +646,14 @@ inv_every_tool() {
635646
"query_graph|{\"project\":\"$p\",\"query\":\"MATCH (n) RETURN n.name LIMIT 5\"}"
636647
"trace_path|{\"project\":\"$p\",\"function_name\":\"compute\",\"direction\":\"both\"}"
637648
"get_code_snippet|{\"project\":\"$p\",\"qualified_name\":\"compute\"}"
649+
"get_file_outline|{\"project\":\"$p\",\"file_path\":\"src/main.py\"}"
638650
"get_graph_schema|{\"project\":\"$p\"}"
651+
"compare_graphs|{\"base_project\":\"$p\",\"target_project\":\"$p\"}"
639652
"get_architecture|{\"project\":\"$p\"}"
640653
"search_code|{\"project\":\"$p\",\"pattern\":\"def \"}"
641654
"list_projects|{}"
642655
"index_status|{\"project\":\"$p\"}"
656+
"check_index_coverage|{\"project\":\"$p\",\"paths\":[\"src/main.py\"]}"
643657
"detect_changes|{\"project\":\"$p\"}"
644658
"manage_adr|{\"project\":\"$p\",\"mode\":\"get\"}"
645659
"ingest_traces|{\"project\":\"$p\",\"traces\":[]}"

‎scripts/smoke-test.sh‎

Lines changed: 23 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -22,6 +22,8 @@ they stage the release fixture, start the fixture server, and sandbox
2222
HOME/TEMP/agent-config destinations. Called bare, the download/checksum/
2323
install-script phases (12-13) SKIP for lack of a fixture server, and the run
2424
mutates the REAL profile — the venue-parity contract forbids that in any venue.
25+
The daemon runtime and cache are private to the run either way: every product
26+
process is started under a CBM_RUNTIME_DIR/CBM_CACHE_DIR this harness owns.
2527
2628
Arguments:
2729
<binary-path> product binary to smoke
@@ -41,6 +43,20 @@ if [ -n "$SMOKE_MODE" ] && [ "$SMOKE_MODE" != "--agent-config-only" ]; then
4143
fi
4244
REPO_ROOT="$(cd "$(dirname "${BASH_SOURCE[0]:-$0}")/.." && pwd)"
4345

46+
# Every product process below — the phases, the install/update E2E and the
47+
# daemon retirements — must reach a daemon rendezvous this run owns. Only
48+
# CBM_RUNTIME_DIR moves that rendezvous; the wrappers' HOME/TMPDIR/CBM_CACHE_DIR
49+
# sandbox does not, so without this the retirements land on the operator's live
50+
# account daemon (#1691, #1696).
51+
# shellcheck source=test-runtime.sh
52+
source "$REPO_ROOT/scripts/test-runtime.sh"
53+
cbm_test_runtime_init
54+
# Armed here rather than only with the fixture trap below: the fixture mktemp
55+
# and its cygpath conversion sit between the two, and under `set -e` a failure
56+
# there would otherwise leave the private root behind. The fixture trap
57+
# replaces this one and keeps the same cleanup as its first step.
58+
trap 'cbm_test_runtime_cleanup "$BINARY"' EXIT
59+
4460
smoke_mktemp_file() {
4561
if [ -n "${SMOKE_TEMP_ROOT:-}" ]; then
4662
mktemp "$SMOKE_TEMP_ROOT/cbm-smoke.XXXXXX"
@@ -100,8 +116,8 @@ copy_smoke_binary() {
100116
cp "$BINARY" "$destination"
101117
}
102118

103-
# Retire the shared account daemon (if one is running) and wait until it
104-
# reports not-running. Install/uninstall flows leave an ephemeral daemon
119+
# Retire this run's private account daemon (if one is running) and wait until
120+
# it reports not-running. Install/uninstall flows leave an ephemeral daemon
105121
# draining asynchronously whose mapped generation backing and open logs
106122
# block rm on Windows (POSIX rm doesn't care) — so every cleanup of a
107123
# fixture HOME that received an install, and the final cache removal, must
@@ -149,7 +165,11 @@ CODEX_LIFECYCLE_HOME=""
149165
if command -v cygpath &>/dev/null; then
150166
TMPDIR=$(cygpath -m "$TMPDIR")
151167
fi
152-
trap 'smoke_rmtree "$TMPDIR" "${DRYRUN_HOME:-}" "${CODEX_LIFECYCLE_HOME:-}"' EXIT
168+
# Runtime cleanup first, so no earlier cleanup step stands between the exit
169+
# and the private daemon's retirement; on Windows that retirement is also what
170+
# unblocks the fixture rm (mapped binary, open logs). smoke_rmtree never fails,
171+
# so the fixture removal still runs after it.
172+
trap 'cbm_test_runtime_cleanup "$BINARY"; smoke_rmtree "$TMPDIR" "${DRYRUN_HOME:-}" "${CODEX_LIFECYCLE_HOME:-}"' EXIT
153173

154174
CLI_STDERR=$(smoke_mktemp_file)
155175
# 10 of the cli call sites assign directly (VAR=$(cli ...)). Under

‎scripts/test.sh‎

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -272,6 +272,9 @@ bash "$ROOT/tests/test_release_gate_chain_contract.sh"
272272
echo "=== Step 0t: test runtime isolation contract (#1691) ==="
273273
bash "$ROOT/tests/test_runtime_isolation_contract.sh"
274274

275+
echo "=== Step 0t2: smoke harness runtime isolation contract (#1696) ==="
276+
bash "$ROOT/tests/test_smoke_runtime_isolation_contract.sh"
277+
275278
echo "=== Step 0u: shell line-ending contract ==="
276279
bash "$ROOT/tests/test_shell_line_endings.sh"
277280

‎src/cli/cli.c‎

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1513,8 +1513,8 @@ static const char skill_content[] =
15131513
"\n"
15141514
"## Edge Types\n"
15151515
"CALLS, HTTP_CALLS, ASYNC_CALLS, DATA_FLOWS, IMPORTS, DEFINES, DEFINES_METHOD,\n"
1516-
"HANDLES, IMPLEMENTS, OVERRIDE, USAGE, CALL_REFERENCE, CONFIGURES, FILE_CHANGES_WITH,\n"
1517-
"SIMILAR_TO, SEMANTICALLY_RELATED, CONTAINS_FILE, CONTAINS_FOLDER,\n"
1516+
"HANDLES, IMPLEMENTS, OVERRIDE, USAGE, CALL_REFERENCE, CONFIGURES, REFERENCES_FILE,\n"
1517+
"FILE_CHANGES_WITH, SIMILAR_TO, SEMANTICALLY_RELATED, CONTAINS_FILE, CONTAINS_FOLDER,\n"
15181518
"CONTAINS_PACKAGE\n"
15191519
"\n"
15201520
"## Cypher Examples (for query_graph)\n"

‎src/foundation/compat.c‎

Lines changed: 35 additions & 17 deletions
Original file line numberDiff line numberDiff line change
@@ -269,36 +269,54 @@ int cbm_mkstemp(char *tmpl) {
269269
errno = ENAMETOOLONG;
270270
return CBM_NOT_FOUND;
271271
}
272-
/* Wide-API expansion and open: worker staging files land inside
273-
* CBM_CACHE_DIR, which users may place at non-ASCII paths; the ANSI CRT
274-
* (_mktemp/_open) mangles those bytes in the local codepage. */
272+
/* Keep the six-character mkstemp contract, but do not use _wmktemp:
273+
* that CRT helper has a tiny name space on Windows and retained worker
274+
* logs can exhaust it during recovery. The exclusive open closes races
275+
* with other processes; collisions simply draw another random name. */
275276
wchar_t *wide_template = cbm_utf8_to_wide(buf);
276-
if (!wide_template || !_wmktemp(wide_template)) {
277-
free(wide_template);
277+
if (!wide_template) {
278+
errno = EINVAL;
278279
return CBM_NOT_FOUND;
279280
}
280-
char *expanded_for_open = cbm_wide_to_utf8(wide_template);
281-
wchar_t *wide_open = expanded_for_open ? cbm_path_to_wide(expanded_for_open) : NULL;
282-
free(expanded_for_open);
283-
if (!wide_open) {
281+
size_t wide_len = wcslen(wide_template);
282+
if (wide_len < 6 || wcscmp(wide_template + wide_len - 6, L"XXXXXX") != 0) {
284283
free(wide_template);
284+
errno = EINVAL;
285285
return CBM_NOT_FOUND;
286286
}
287-
int fd = _wopen(wide_open, _O_CREAT | _O_EXCL | _O_RDWR | _O_BINARY, _S_IREAD | _S_IWRITE);
288-
free(wide_open);
289-
if (fd >= 0) {
287+
static const wchar_t hex[] = L"0123456789abcdef";
288+
for (int attempt = 0; attempt < 128; attempt++) {
289+
unsigned int random_bits = 0;
290+
if (!cbm_secure_random(&random_bits, sizeof(random_bits))) {
291+
errno = EIO;
292+
break;
293+
}
294+
for (int digit = 0; digit < 6; digit++) {
295+
wide_template[wide_len - 6 + digit] = hex[(random_bits >> (digit * 4)) & 0xf];
296+
}
290297
char *expanded = cbm_wide_to_utf8(wide_template);
291-
if (!expanded || strlen(expanded) >= sizeof(buf)) {
298+
wchar_t *wide_open = expanded ? cbm_path_to_wide(expanded) : NULL;
299+
if (!expanded || !wide_open || strlen(expanded) >= sizeof(buf)) {
300+
free(expanded);
301+
free(wide_open);
302+
errno = ENAMETOOLONG;
303+
break;
304+
}
305+
int fd = _wopen(wide_open, _O_CREAT | _O_EXCL | _O_RDWR | _O_BINARY, _S_IREAD | _S_IWRITE);
306+
free(wide_open);
307+
if (fd >= 0) {
308+
strcpy(tmpl, expanded);
292309
free(expanded);
293310
free(wide_template);
294-
(void)_close(fd);
295-
return CBM_NOT_FOUND;
311+
return fd;
296312
}
297-
strcpy(tmpl, expanded);
298313
free(expanded);
314+
if (errno != EEXIST) {
315+
break;
316+
}
299317
}
300318
free(wide_template);
301-
return fd;
319+
return CBM_NOT_FOUND;
302320
}
303321
#endif
304322

‎src/mcp/index_supervisor.c‎

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -13,6 +13,7 @@
1313
#include "ui/http_server.h" /* cbm_http_server_resolve_binary_path */
1414

1515
#include <limits.h>
16+
#include <errno.h>
1617
#include <stdio.h>
1718
#include <stdint.h>
1819
#include <stdlib.h>
@@ -705,6 +706,11 @@ int cbm_index_worker_start_with_log(const char *args_json, size_t memory_budget_
705706
worker_result_init(&handle->result);
706707
if (!worker_unique_file(handle->response_path, sizeof(handle->response_path), "response") ||
707708
!worker_unique_file(handle->log_path, sizeof(handle->log_path), "log")) {
709+
int saved_errno = errno;
710+
char error_text[CBM_SZ_32];
711+
(void)snprintf(error_text, sizeof(error_text), "%d", saved_errno);
712+
cbm_log_error("index.supervisor.artifact_create_failed", "artifact",
713+
handle->response_path[0] ? "log" : "response", "errno", error_text);
708714
(void)cbm_unlink(handle->response_path);
709715
(void)cbm_unlink(handle->log_path);
710716
free(handle);

‎src/mcp/mcp.c‎

Lines changed: 4 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -10808,7 +10808,10 @@ static char *index_run_supervised(cbm_mcp_server_t *srv, const char *args) {
1080810808
cbm_mcp_supervised_result_disposition_t recovery_disposition =
1080910809
cbm_mcp_supervised_result_disposition(rc2, &wr2);
1081010810
if (recovery_disposition == CBM_MCP_SUPERVISED_RESULT_FALLBACK) {
10811-
last_outcome = wr2.outcome;
10811+
/* Keep the original worker failure if recovery setup fails. */
10812+
cbm_log_error("index.supervisor.recovery_start_failed", "original_outcome",
10813+
cbm_proc_outcome_str(last_outcome), "recovery_outcome",
10814+
cbm_proc_outcome_str(wr2.outcome));
1081210815
cbm_index_worker_result_free(&wr2);
1081310816
break; /* spawn failed mid-recovery — give up */
1081410817
}

0 commit comments

Comments
 (0)