Merge pull request #736 from DeusData/fix/deprecate-raw-json-cli

feat(cli): deprecation-warn raw-JSON args + full flag-ops smoke coverage
This commit is contained in:
Martin Vogel authored and GitHub committed 2026-07-01 17:21:27 +02:00
commit 67099fa249
2 files changed
+149 -25

No files matched your search

+142 -24
View File
@@ -119,8 +119,12 @@ with open(sys.argv[1], "w") as f:
f.write("}\n")
GENEOF
# Index
RESULT=$(cli index_repository "{\"repo_path\":\"$TMPDIR\"}")
# Index (flag form: --repo-path -> repo_path)
if ! RESULT=$(cli index_repository --repo-path "$TMPDIR"); then
echo "FAIL: index_repository (flag form) exited non-zero"
cat "$CLI_STDERR"
exit 1
fi
echo "$RESULT"
# Allocator-integrity guard: the prod binary overrides the global allocator with
@@ -166,7 +170,9 @@ echo "=== Phase 3: verify queries ==="
# 3a: search_graph — find the compute function
PROJECT=$(echo "$RESULT" | python3 -c "import json,sys; d=json.loads(sys.stdin.read()); print(d.get('project',''))" 2>/dev/null || echo "")
SEARCH=$(cli search_graph "{\"project\":\"$PROJECT\",\"name_pattern\":\"compute\"}")
if ! SEARCH=$(cli search_graph --project "$PROJECT" --name-pattern compute); then
echo "FAIL: search_graph (flag form) exited non-zero"; cat "$CLI_STDERR"; exit 1
fi
TOTAL=$(echo "$SEARCH" | python3 -c "import json,sys; d=json.loads(sys.stdin.read()); print(d.get('total',0))" 2>/dev/null || echo "0")
if [ "$TOTAL" -lt 1 ]; then
echo "FAIL: search_graph for 'compute' returned 0 results"
@@ -175,7 +181,9 @@ fi
echo "OK: search_graph found $TOTAL result(s) for 'compute'"
# 3b: trace_path — verify compute has callers
TRACE=$(cli trace_path "{\"project\":\"$PROJECT\",\"function_name\":\"compute\",\"direction\":\"inbound\",\"depth\":1}")
if ! TRACE=$(cli trace_path --project "$PROJECT" --function-name compute --direction inbound --depth 1); then
echo "FAIL: trace_path (flag form) exited non-zero"; cat "$CLI_STDERR"; exit 1
fi
CALLERS=$(echo "$TRACE" | python3 -c "import json,sys; d=json.loads(sys.stdin.read()); print(len(d.get('callers',[])))" 2>/dev/null || echo "0")
if [ "$CALLERS" -lt 1 ]; then
echo "FAIL: trace_path found 0 callers for 'compute'"
@@ -184,7 +192,9 @@ fi
echo "OK: trace_path found $CALLERS caller(s) for 'compute'"
# 3c: get_graph_schema — verify labels exist
SCHEMA=$(cli get_graph_schema "{\"project\":\"$PROJECT\"}")
if ! SCHEMA=$(cli get_graph_schema --project "$PROJECT"); then
echo "FAIL: get_graph_schema (flag form) exited non-zero"; cat "$CLI_STDERR"; exit 1
fi
LABELS=$(echo "$SCHEMA" | python3 -c "import json,sys; d=json.loads(sys.stdin.read()); print(len(d.get('node_labels',[])))" 2>/dev/null || echo "0")
if [ "$LABELS" -lt 3 ]; then
echo "FAIL: schema has fewer than 3 node labels"
@@ -193,7 +203,9 @@ fi
echo "OK: schema has $LABELS node labels"
# 3d: Verify __init__.py didn't clobber Folder node
FOLDERS=$(cli search_graph "{\"project\":\"$PROJECT\",\"label\":\"Folder\"}")
if ! FOLDERS=$(cli search_graph --project "$PROJECT" --label Folder); then
echo "FAIL: search_graph --label Folder exited non-zero"; cat "$CLI_STDERR"; exit 1
fi
FOLDER_COUNT=$(echo "$FOLDERS" | python3 -c "import json,sys; d=json.loads(sys.stdin.read()); print(d.get('total',0))" 2>/dev/null || echo "0")
if [ "$FOLDER_COUNT" -lt 2 ]; then
echo "FAIL: expected at least 2 Folder nodes (src, src/pkg), got $FOLDER_COUNT"
@@ -203,7 +215,7 @@ echo "OK: $FOLDER_COUNT Folder nodes (init.py didn't clobber them)"
# 3d-cypher: query_graph Cypher capabilities
# #238 WITH DISTINCT — all functions share label "Function" → collapses to 1 row.
CYPHER_WD=$(cli query_graph "{\"project\":\"$PROJECT\",\"query\":\"MATCH (f:Function) WITH DISTINCT f.label AS lbl RETURN lbl\"}")
CYPHER_WD=$(cli query_graph --project "$PROJECT" --query "MATCH (f:Function) WITH DISTINCT f.label AS lbl RETURN lbl")
WD_ROWS=$(echo "$CYPHER_WD" | python3 -c "import json,sys; d=json.loads(sys.stdin.read()); print(len(d.get('rows',[])))" 2>/dev/null || echo "0")
if [ "$WD_ROWS" -lt 1 ]; then
echo "FAIL: query_graph WITH DISTINCT returned 0 rows"
@@ -213,7 +225,7 @@ fi
echo "OK: query_graph WITH DISTINCT returned $WD_ROWS row(s)"
# #241 WHERE label test — f:Function is true for every Function node.
CYPHER_LBL=$(cli query_graph "{\"project\":\"$PROJECT\",\"query\":\"MATCH (f:Function) WHERE f:Function RETURN f.name\"}")
CYPHER_LBL=$(cli query_graph --project "$PROJECT" --query "MATCH (f:Function) WHERE f:Function RETURN f.name")
LBL_ROWS=$(echo "$CYPHER_LBL" | python3 -c "import json,sys; d=json.loads(sys.stdin.read()); print(len(d.get('rows',[])))" 2>/dev/null || echo "0")
if [ "$LBL_ROWS" -lt 1 ]; then
echo "FAIL: query_graph WHERE label-test returned 0 rows"
@@ -223,7 +235,7 @@ fi
echo "OK: query_graph WHERE f:Function returned $LBL_ROWS row(s)"
# #242 label alternation — (n:Function|Module) seeds either label.
CYPHER_ALT=$(cli query_graph "{\"project\":\"$PROJECT\",\"query\":\"MATCH (n:Function|Module) RETURN n.name\"}")
CYPHER_ALT=$(cli query_graph --project "$PROJECT" --query "MATCH (n:Function|Module) RETURN n.name")
ALT_ROWS=$(echo "$CYPHER_ALT" | python3 -c "import json,sys; d=json.loads(sys.stdin.read()); print(len(d.get('rows',[])))" 2>/dev/null || echo "0")
if [ "$ALT_ROWS" -lt 1 ]; then
echo "FAIL: query_graph label alternation returned 0 rows"
@@ -233,7 +245,7 @@ fi
echo "OK: query_graph (n:Function|Module) returned $ALT_ROWS row(s)"
# #239 count(DISTINCT) — must parse and return a single aggregate row.
CYPHER_CD=$(cli query_graph "{\"project\":\"$PROJECT\",\"query\":\"MATCH (f:Function) RETURN count(DISTINCT f.label)\"}")
CYPHER_CD=$(cli query_graph --project "$PROJECT" --query "MATCH (f:Function) RETURN count(DISTINCT f.label)")
CD_ROWS=$(echo "$CYPHER_CD" | python3 -c "import json,sys; d=json.loads(sys.stdin.read()); print(len(d.get('rows',[])))" 2>/dev/null || echo "0")
if [ "$CD_ROWS" -ne 1 ]; then
echo "FAIL: query_graph count(DISTINCT) expected 1 row, got $CD_ROWS"
@@ -244,11 +256,10 @@ echo "OK: query_graph count(DISTINCT f.label) returned 1 aggregate row"
# 3d-funcs: scalar / introspection functions (full Cypher suite, Tier 1)
cyp_first_cell() {
# $1 = query; echoes rows[0][0] (or empty)
# Escape embedded double-quotes so string-literal args (e.g. replace(x,"a","A"))
# don't break the JSON we build by interpolation.
local q="${1//\"/\\\"}"
cli query_graph "{\"project\":\"$PROJECT\",\"query\":\"$q\"}" |
# $1 = query; echoes rows[0][0] (or empty). Flag form passes the query as ONE
# argv token, so string-literal args (e.g. replace(f.name,"a","A")) and Cypher
# metacharacters {}|=~<>" need no JSON escaping.
cli query_graph --project "$PROJECT" --query "$1" |
python3 -c "import json,sys; d=json.loads(sys.stdin.read()); rows=d.get('rows',[]); print(rows[0][0] if rows and rows[0] else '')" 2>/dev/null || echo ""
}
@@ -303,7 +314,7 @@ if [ -z "$COALV" ]; then echo "FAIL: query_graph coalesce(...) returned empty";
echo "OK: query_graph coalesce(f.nonesuch, f.name) = $COALV"
# EXISTS { } pattern predicate (edge-type-specific existence)
CYPHER_EX=$(cli query_graph "{\"project\":\"$PROJECT\",\"query\":\"MATCH (f:Function) WHERE EXISTS { (f)-[:CALLS]->() } RETURN f.name\"}")
CYPHER_EX=$(cli query_graph --project "$PROJECT" --query "MATCH (f:Function) WHERE EXISTS { (f)-[:CALLS]->() } RETURN f.name")
EX_ROWS=$(echo "$CYPHER_EX" | python3 -c "import json,sys; d=json.loads(sys.stdin.read()); print(len(d.get('rows',[])))" 2>/dev/null || echo "0")
if [ "$EX_ROWS" -lt 1 ]; then
echo "FAIL: query_graph EXISTS{} predicate returned 0 rows"; echo "$CYPHER_EX"; exit 1
@@ -311,7 +322,7 @@ fi
echo "OK: query_graph EXISTS { (f)-[:CALLS]->() } returned $EX_ROWS row(s)"
# =~ regex match in WHERE
CYPHER_RX=$(cli query_graph "{\"project\":\"$PROJECT\",\"query\":\"MATCH (f:Function) WHERE f.name =~ \\\".+\\\" RETURN f.name\"}")
CYPHER_RX=$(cli query_graph --project "$PROJECT" --query 'MATCH (f:Function) WHERE f.name =~ ".+" RETURN f.name')
RX_ROWS=$(echo "$CYPHER_RX" | python3 -c "import json,sys; d=json.loads(sys.stdin.read()); print(len(d.get('rows',[])))" 2>/dev/null || echo "0")
if [ "$RX_ROWS" -lt 1 ]; then
echo "FAIL: query_graph WHERE =~ regex returned 0 rows"; echo "$CYPHER_RX"; exit 1
@@ -334,7 +345,7 @@ LEFTV=$(cyp_first_cell 'MATCH (f:Function) RETURN left(f.name, 3) AS l LIMIT 1')
[ -n "$LEFTV" ] && echo "OK: query_graph left(f.name,3) = $LEFTV" || { echo "FAIL: left empty"; exit 1; }
# NOT EXISTS dead-code query (functions with no caller)
CYPHER_NX=$(cli query_graph "{\"project\":\"$PROJECT\",\"query\":\"MATCH (f:Function) WHERE NOT EXISTS { (f)<-[:CALLS]-() } RETURN f.name\"}")
CYPHER_NX=$(cli query_graph --project "$PROJECT" --query "MATCH (f:Function) WHERE NOT EXISTS { (f)<-[:CALLS]-() } RETURN f.name")
NX_OK=$(echo "$CYPHER_NX" | python3 -c "import json,sys; d=json.loads(sys.stdin.read()); print('rows' in d)" 2>/dev/null || echo "False")
[ "$NX_OK" = "True" ] && echo "OK: query_graph NOT EXISTS dead-code query executed" || { echo "FAIL: NOT EXISTS query"; echo "$CYPHER_NX" | head -c 300; exit 1; }
@@ -345,7 +356,7 @@ CASEV=$(cyp_first_cell 'MATCH (f:Function) RETURN CASE WHEN f.name =~ ".+" THEN
# unsupported function must FAIL LOUDLY (not silently return empty). The CLI
# prints the parse error to stderr (captured by cli() into $CLI_STDERR) and exits
# non-zero, leaving stdout empty — so verify the loud failure on that channel.
if cli query_graph "{\"project\":\"$PROJECT\",\"query\":\"MATCH (f:Function) RETURN nosuchfn(f.name)\"}" >/dev/null; then
if cli query_graph --project "$PROJECT" --query "MATCH (f:Function) RETURN nosuchfn(f.name)" >/dev/null; then
echo "FAIL: unsupported function did not error (exit 0)"; exit 1
fi
ERROUT=$(cat "$CLI_STDERR" 2>/dev/null)
@@ -355,7 +366,9 @@ case "$ERROUT" in
esac
# 3f: get_architecture surfaces Leiden community clusters
ARCH=$(cli get_architecture "{\"project\":\"$PROJECT\",\"aspects\":[\"clusters\"]}")
if ! ARCH=$(cli get_architecture --project "$PROJECT" --aspects clusters); then
echo "FAIL: get_architecture (flag form) exited non-zero"; cat "$CLI_STDERR"; exit 1
fi
NCLUST=$(echo "$ARCH" | python3 -c "import json,sys; d=json.loads(sys.stdin.read()); print(len(d.get('clusters',[])))" 2>/dev/null || echo "0")
if [ "$NCLUST" -lt 1 ]; then
echo "FAIL: get_architecture returned 0 community clusters"; echo "$ARCH" | head -c 400; exit 1
@@ -363,22 +376,127 @@ fi
echo "OK: get_architecture returned $NCLUST community cluster(s)"
# 3g: search_code — basic search reports elapsed_ms + matches
SC=$(cli search_code "{\"project\":\"$PROJECT\",\"pattern\":\"cbm_\",\"mode\":\"compact\",\"limit\":5}")
SC=$(cli search_code --project "$PROJECT" --pattern cbm_ --mode compact --limit 5)
echo "$SC" | python3 -c "import json,sys; d=json.loads(sys.stdin.read()); assert 'elapsed_ms' in d; print('OK: search_code elapsed_ms='+str(d['elapsed_ms'])+' total_grep_matches='+str(d.get('total_grep_matches')))" 2>/dev/null || { echo "FAIL: search_code basic / no elapsed_ms"; echo "$SC" | head -c 400; exit 1; }
# 3g: search_code — literal '|' under regex=false must surface a warning (#282)
SCW=$(cli search_code "{\"project\":\"$PROJECT\",\"pattern\":\"cbm_init|cbm_nope\",\"regex\":false,\"limit\":5}")
SCW=$(cli search_code --project "$PROJECT" --pattern "cbm_init|cbm_nope" --regex false --limit 5)
echo "$SCW" | python3 -c "import json,sys; d=json.loads(sys.stdin.read()); w=' '.join(d.get('warnings',[])); assert 'regex=true' in w; print('OK: search_code literal-| warning surfaced')" 2>/dev/null || { echo "FAIL: search_code literal-| warning missing"; echo "$SCW" | head -c 400; exit 1; }
# 3g: search_code — '&' in file_pattern accepted, not rejected as invalid (#272)
SCA=$(cli search_code "{\"project\":\"$PROJECT\",\"pattern\":\"cbm_\",\"file_pattern\":\"*R&D*.c\",\"limit\":5}")
SCA=$(cli search_code --project "$PROJECT" --pattern cbm_ --file-pattern "*R&D*.c" --limit 5)
case "$SCA" in
*"invalid characters"*) echo "FAIL: search_code rejected '&' in file_pattern"; echo "$SCA" | head -c 300; exit 1 ;;
*) echo "OK: search_code accepts '&' in file_pattern" ;;
esac
echo ""
echo "=== Phase 3h: CLI input-mode guards (flags / stdin / --args-file / --help / deprecation) ==="
# Small helper: assert its stdin is a JSON object (exit non-zero otherwise).
assert_json_obj() { python3 -c "import json,sys; d=json.loads(sys.stdin.read()); sys.exit(0 if isinstance(d,dict) else 1)" 2>/dev/null; }
# B1: INTEGER flag — --limit is schema-typed integer; must parse to valid JSON.
if ! IM_INT=$(cli search_graph --project "$PROJECT" --name-pattern compute --limit 5); then
echo "FAIL B1: search_graph --limit 5 exited non-zero"; cat "$CLI_STDERR"; exit 1
fi
if echo "$IM_INT" | assert_json_obj; then
echo "OK B1: INTEGER flag (--limit 5) parsed → valid JSON"
else
echo "FAIL B1: --limit 5 did not produce valid JSON"; echo "$IM_INT" | head -c 300; exit 1
fi
# B2: BOOLEAN bare flag — --exclude-entry-points with no value → true; must succeed.
if ! IM_BOOL=$(cli search_graph --project "$PROJECT" --exclude-entry-points); then
echo "FAIL B2: search_graph --exclude-entry-points exited non-zero"; cat "$CLI_STDERR"; exit 1
fi
if echo "$IM_BOOL" | assert_json_obj; then
echo "OK B2: BOOLEAN bare flag (--exclude-entry-points) → success"
else
echo "FAIL B2: --exclude-entry-points did not produce valid JSON"; echo "$IM_BOOL" | head -c 300; exit 1
fi
# B3: ARRAY flag — repeated --semantic-query accumulates into a JSON array.
# semantic_results may be empty (index-mode dependent); only assert valid JSON.
if ! IM_ARR=$(cli search_graph --project "$PROJECT" --semantic-query send --semantic-query publish); then
echo "FAIL B3: search_graph repeated --semantic-query exited non-zero"; cat "$CLI_STDERR"; exit 1
fi
if echo "$IM_ARR" | assert_json_obj; then
echo "OK B3: ARRAY flag (repeated --semantic-query) → valid JSON"
else
echo "FAIL B3: repeated --semantic-query did not produce valid JSON"; echo "$IM_ARR" | head -c 300; exit 1
fi
# B4: STDIN — piped JSON resolves; this path must NOT emit a deprecation warning.
IM_STDIN=$(echo "{\"project\":\"$PROJECT\"}" | "$BINARY" cli get_graph_schema 2>"$CLI_STDERR")
if ! echo "$IM_STDIN" | python3 -c "import json,sys; d=json.loads(sys.stdin.read()); sys.exit(0 if 'node_labels' in d else 1)" 2>/dev/null; then
echo "FAIL B4: stdin get_graph_schema did not resolve"; echo "$IM_STDIN" | head -c 300; cat "$CLI_STDERR"; exit 1
fi
if grep -qi 'deprecated' "$CLI_STDERR"; then
echo "FAIL B4: stdin path wrongly emitted a deprecation warning"; cat "$CLI_STDERR"; exit 1
fi
echo "OK B4: STDIN input resolves, no deprecation warning"
# B5: --args-file — JSON read from a file resolves; must NOT warn deprecated.
IM_ARGS_FILE=$(mktemp)
echo "{\"project\":\"$PROJECT\"}" > "$IM_ARGS_FILE"
if ! IM_AF=$(cli get_graph_schema --args-file "$IM_ARGS_FILE"); then
echo "FAIL B5: get_graph_schema --args-file exited non-zero"; cat "$CLI_STDERR"; rm -f "$IM_ARGS_FILE"; exit 1
fi
if ! echo "$IM_AF" | python3 -c "import json,sys; d=json.loads(sys.stdin.read()); sys.exit(0 if 'node_labels' in d else 1)" 2>/dev/null; then
echo "FAIL B5: --args-file get_graph_schema did not resolve"; echo "$IM_AF" | head -c 300; rm -f "$IM_ARGS_FILE"; exit 1
fi
if grep -qi 'deprecated' "$CLI_STDERR"; then
echo "FAIL B5: --args-file path wrongly emitted a deprecation warning"; cat "$CLI_STDERR"; rm -f "$IM_ARGS_FILE"; exit 1
fi
rm -f "$IM_ARGS_FILE"
echo "OK B5: --args-file input resolves, no deprecation warning"
# B6: per-tool --help — RC0 with expected flags in stdout; unknown tool errors non-zero.
if ! H_SG=$(cli search_graph --help); then
echo "FAIL B6a: 'search_graph --help' exited non-zero"; exit 1
fi
if echo "$H_SG" | grep -q -- "--name-pattern"; then
echo "OK B6a: search_graph --help (RC0) lists --name-pattern"
else
echo "FAIL B6a: search_graph --help missing --name-pattern"; echo "$H_SG" | head -c 400; exit 1
fi
if ! H_IR=$(cli index_repository --help); then
echo "FAIL B6b: 'index_repository --help' exited non-zero"; exit 1
fi
if echo "$H_IR" | grep -q -- "--repo-path"; then
echo "OK B6b: index_repository --help (RC0) lists --repo-path"
else
echo "FAIL B6b: index_repository --help missing --repo-path"; echo "$H_IR" | head -c 400; exit 1
fi
# Unknown tool: must exit non-zero and report "unknown tool" (on stderr).
if cli notatool --help >/dev/null; then
echo "FAIL B6c: 'notatool --help' exited 0 (expected non-zero for unknown tool)"; exit 1
fi
if grep -qi 'unknown tool' "$CLI_STDERR"; then
echo "OK B6c: 'notatool --help' errors non-zero with 'unknown tool'"
else
echo "FAIL B6c: 'notatool --help' did not report 'unknown tool'"; cat "$CLI_STDERR"; exit 1
fi
# B7: DEPRECATION guard — one raw-JSON call MUST warn on stderr; flag form must NOT.
cli search_graph "{\"project\":\"$PROJECT\",\"name_pattern\":\"compute\"}" >/dev/null || true
if grep -qi 'deprecated' "$CLI_STDERR"; then
echo "OK B7a: raw-JSON cli emits deprecation warning on stderr"
else
echo "FAIL B7a: raw-JSON cli did NOT emit deprecation warning"; cat "$CLI_STDERR"; exit 1
fi
cli search_graph --project "$PROJECT" --name-pattern compute >/dev/null || true
if grep -qi 'deprecated' "$CLI_STDERR"; then
echo "FAIL B7b: flag-form cli wrongly emitted a deprecation warning"; cat "$CLI_STDERR"; exit 1
else
echo "OK B7b: flag-form cli emits no deprecation warning"
fi
# 3e: delete_project cleanup
cli delete_project "{\"project\":\"$PROJECT\"}" > /dev/null
if ! cli delete_project --project "$PROJECT" > /dev/null; then
echo "FAIL: delete_project (flag form) exited non-zero"; cat "$CLI_STDERR"; exit 1
fi
echo ""
echo "=== Phase 4: security checks ==="
+7 -1
View File
@@ -343,7 +343,13 @@ static int run_cli(int argc, char **argv) {
}
args_json = heap_args;
} else if (rem_argc >= SKIP_ONE && cli_first_nonspace_is_brace(rem_argv[0])) {
/* raw-JSON back-compat: cli <tool> '{"k":"v"}' */
/* raw-JSON back-compat: cli <tool> '{"k":"v"}' (deprecated path). Warn on
* STDERR only — stdout must stay clean JSON for piping. */
(void)fprintf(stderr,
"warning: passing raw JSON to 'cli %s' is deprecated and "
"will be removed in a future release; use flags (run 'cli "
"%s --help'), --args-file <path>, or piped stdin.\n",
tool_name, tool_name);
args_json = rem_argv[0];
} else if (rem_argc >= SKIP_ONE && strncmp(rem_argv[0], "--", 2) == 0) {
/* flag form: cli <tool> --flag value --bare-bool ... */