@@ -392,7 +392,7 @@ if [ "$TOTAL" -lt 1 ]; then
392392fi
393393
394394echo " "
395- echo " === Phase 3z: no tool duplicates its payload into structuredContent (#1375) ==="
395+ echo " === Phase 3z: structuredContent carries structure or is absent — never {} and never a copy (#1375, #1522 ) ==="
396396# Asserts on the SHIPPED artifact what the unit suite asserts from source: a
397397# non-JSON payload must travel ONCE. It used to appear twice — content[0].text
398398# plus an identical structuredContent.text — costing 2.05x the bytes on a large
@@ -421,21 +421,32 @@ if d.get("isError"):
421421 print("skip"); raise SystemExit
422422content = d.get("content") or []
423423text = content[0].get("text", "") if content else ""
424- sc = d.get("structuredContent")
425- if not isinstance(sc, dict):
426- print("no-structured"); raise SystemExit
424+ sc = d.get("structuredContent", "ABSENT")
427425try:
428426 payload_is_object = isinstance(json.loads(text), dict)
429427except Exception:
430428 payload_is_object = False
431429if payload_is_object:
432- print("skip"); raise SystemExit
433- print("dup" if sc.get("text") == text and text else "ok")
430+ # Object payloads must carry the parsed, NON-EMPTY object.
431+ print("ok" if isinstance(sc, dict) and sc else "object-lost"); raise SystemExit
432+ # Text payloads: no structuredContent key at all. {} rendered as the whole
433+ # result in outputSchema-honoring clients (#1522); {"text": ...} duplicated
434+ # the wire (#1375).
435+ if sc == "ABSENT":
436+ print("ok")
437+ elif isinstance(sc, dict) and not sc:
438+ print("empty-lie")
439+ elif isinstance(sc, dict) and sc.get("text") == text and text:
440+ print("dup")
441+ else:
442+ print("unexpected")
434443' )
435444 case " $VERDICT " in
436445 dup) echo " FAIL: $( echo " $TOOL_ARGS " | cut -d" " -f1) repeats its payload in structuredContent (#1375)" ; DUP_TOOLS=$(( DUP_TOOLS+ 1 )) ;;
446+ empty-lie) echo " FAIL: $( echo " $TOOL_ARGS " | cut -d" " -f1) ships structuredContent {} beside a non-empty payload (#1522)" ; DUP_TOOLS=$(( DUP_TOOLS+ 1 )) ;;
447+ object-lost) echo " FAIL: $( echo " $TOOL_ARGS " | cut -d" " -f1) dropped the parsed object from structuredContent" ; DUP_TOOLS=$(( DUP_TOOLS+ 1 )) ;;
448+ unexpected) echo " FAIL: $( echo " $TOOL_ARGS " | cut -d" " -f1) has an unexpected structuredContent shape" ; DUP_TOOLS=$(( DUP_TOOLS+ 1 )) ;;
437449 ok) DUP_CHECKED=$(( DUP_CHECKED+ 1 )) ;;
438- no-structured) echo " FAIL: $( echo " $TOOL_ARGS " | cut -d" " -f1) has no structuredContent object (outputSchema requires one)" ; DUP_TOOLS=$(( DUP_TOOLS+ 1 )) ;;
439450 esac
440451done
441452if [ " $DUP_TOOLS " -ne 0 ]; then
@@ -448,6 +459,105 @@ if [ "$DUP_CHECKED" -eq 0 ]; then
448459fi
449460echo " OK: $DUP_CHECKED tool(s) deliver their payload exactly once"
450461
462+ echo " === Phase 3z1: default-format MCP replies are usable in schema-honoring clients (#1522) ==="
463+ # The exact end-to-end failure shape of #1522: a spec-compliant MCP client
464+ # (Claude Code) reads structuredContent as THE result when a tool declares an
465+ # outputSchema. Drive the REAL stdio server the way such a client does and
466+ # assert a default-format search_graph reply cannot render as "{}": either
467+ # structuredContent is absent (client falls back to content text) or it is a
468+ # non-empty object. Also assert no tool declares an outputSchema anymore.
469+ MCP_1522=$( python3 - " $BINARY " " $PROJECT " << 'PYMCP '
470+ import json, subprocess, sys
471+ BIN, PROJECT = sys.argv[1], sys.argv[2]
472+ def rpc(i, m, p): return json.dumps({"jsonrpc": "2.0", "id": i, "method": m, "params": p})
473+ reqs = "\n".join([
474+ rpc(1, "initialize", {"protocolVersion": "2025-06-18", "capabilities": {},
475+ "clientInfo": {"name": "smoke-1522", "version": "0"}}),
476+ json.dumps({"jsonrpc": "2.0", "method": "notifications/initialized"}),
477+ rpc(2, "tools/list", {}),
478+ rpc(3, "tools/call", {"name": "search_graph", "arguments":
479+ {"project": PROJECT, "name_pattern": "compute"}}),
480+ ]) + "\n"
481+ out = subprocess.run([BIN], input=reqs, capture_output=True, text=True, timeout=180).stdout
482+ verdict = []
483+ for line in out.splitlines():
484+ try:
485+ d = json.loads(line)
486+ except Exception:
487+ continue
488+ if d.get("id") == 2:
489+ schemas = [t["name"] for t in d.get("result", {}).get("tools", []) if "outputSchema" in t]
490+ if schemas:
491+ verdict.append("FAIL: outputSchema declared by: " + ",".join(schemas))
492+ if d.get("id") == 3:
493+ r = d.get("result", {})
494+ text = (r.get("content") or [{}])[0].get("text", "")
495+ sc = r.get("structuredContent", "ABSENT")
496+ if not text:
497+ verdict.append("FAIL: search_graph content text is empty")
498+ if isinstance(sc, dict) and not sc:
499+ verdict.append("FAIL: search_graph ships structuredContent {} (#1522)")
500+ if not verdict:
501+ verdict.append("OK")
502+ print("; ".join(verdict))
503+ PYMCP
504+ )
505+ case " $MCP_1522 " in
506+ OK) echo " OK: default-format MCP reply is client-usable, no outputSchema declared" ;;
507+ * ) echo " $MCP_1522 " ; exit 1 ;;
508+ esac
509+
510+ echo " === Phase 3z2: pipelined requests beyond queue capacity all get answers ==="
511+ # Any 7+ requests written in one stdin burst used to kill the server with
512+ # rc=1 and ZERO bytes of output — the frontend queue (capacity 8 frames,
513+ # initialize + notification included) failed the whole session at overflow
514+ # instead of backpressuring the reader. Agent clients issuing parallel tool
515+ # calls pipeline exactly like this.
516+ PIPELINE=$( python3 - " $BINARY " << 'PYPIPE '
517+ import json, subprocess, sys
518+ BIN = sys.argv[1]
519+ N = 24
520+ def rpc(i, m, p): return json.dumps({"jsonrpc": "2.0", "id": i, "method": m, "params": p})
521+ lines = [rpc(1, "initialize", {"protocolVersion": "2025-06-18", "capabilities": {},
522+ "clientInfo": {"name": "smoke-pipe", "version": "0"}}),
523+ json.dumps({"jsonrpc": "2.0", "method": "notifications/initialized"})]
524+ for i in range(N):
525+ lines.append(rpc(100 + i, "tools/call", {"name": "list_projects", "arguments": {}}))
526+ r = subprocess.run([BIN], input="\n".join(lines) + "\n",
527+ capture_output=True, text=True, timeout=300)
528+ answered = set()
529+ for line in r.stdout.splitlines():
530+ try:
531+ d = json.loads(line)
532+ except Exception:
533+ continue
534+ if isinstance(d.get("id"), int) and d["id"] >= 100:
535+ answered.add(d["id"])
536+ if r.returncode == 0 and len(answered) == N:
537+ print("OK")
538+ else:
539+ print(f"FAIL: rc={r.returncode} answered={len(answered)}/{N} stdout_bytes={len(r.stdout)}")
540+ PYPIPE
541+ )
542+ case " $PIPELINE " in
543+ OK) echo " OK: 24 pipelined tool calls all answered, clean exit" ;;
544+ * ) echo " $PIPELINE " ; exit 1 ;;
545+ esac
546+
547+ echo " === Phase 3z3: config get prints real defaults and rejects unknown keys (#1522) ==="
548+ CFG_HOME=$( smoke_mktemp_dir)
549+ CFG_WATCH=$( CBM_CACHE_DIR=" $CFG_HOME " " $BINARY " config get auto_watch 2> /dev/null || true)
550+ if [ " $CFG_WATCH " != " true" ] && [ " $CFG_WATCH " != " false" ]; then
551+ echo " FAIL: config get auto_watch printed '$CFG_WATCH ' (expected the stored value or the default 'true')"
552+ rm -rf " $CFG_HOME " ; exit 1
553+ fi
554+ if CBM_CACHE_DIR=" $CFG_HOME " " $BINARY " config get totally_bogus_key > /dev/null 2>&1 ; then
555+ echo " FAIL: config get of an unknown key exited 0"
556+ rm -rf " $CFG_HOME " ; exit 1
557+ fi
558+ rm -rf " $CFG_HOME "
559+ echo " OK: config get returns defaults and errors on unknown keys"
560+
451561echo " OK: search_graph found $TOTAL result(s) for 'compute'"
452562
453563# 3b: trace_path — verify compute has callers
0 commit comments