返回 CodeWhale
concentrate-selftest.sh
根目录 / scripts / concentrate-selftest.sh
1 #!/usr/bin/env bash
2 # Keyless end-to-end self-test of the Concentrate provider route.
3 #
4 # Boots scripts/concentrate-stub.py (the documented Concentrate contract on
5 # loopback), then drives the REAL `codewhale exec` path — provider selection,
6 # secret/env resolution, Route Contract resolution, the Responses wire, SSE
7 # parsing, and the completed-turn receipt — through it. Nothing leaves the
8 # machine: the base URL is loopback, the key is a stub value, and no
9 # Concentrate account exists in this loop.
10 #
11 # What it asserts (from the stub's request log and the CLI's stream-json):
12 # 1. GET /v1/responses/health answers 200 (stub up, unauthenticated).
13 # 2. GET /v1/models is readable without a key.
14 # 3. POST /v1/responses arrived exactly once per turn, with
15 # `Authorization: Bearer <CONCENTRATE_API_KEY>`, `stream: true`, the
16 # model id passed through VERBATIM, a leading `system` input item, and
17 # no top-level field outside the documented parameter reference.
18 # 4. The CLI printed a `done` receipt (exit 0) with the stub's reply text.
19 # 5. A wrong key produces the documented 401 body and a non-zero exit.
20 #
21 # Usage:
22 # scripts/concentrate-selftest.sh # builds a debug codewhale if needed
23 # CODEWHALE_BIN=target/release/codewhale scripts/concentrate-selftest.sh
24 # CONCENTRATE_SELFTEST_MODEL=openai/gpt-5.6-sol scripts/concentrate-selftest.sh
25 #
26 # Evidence level: LOCAL (fixture gateway + real binary). Not a provider
27 # canary — a paid canary against the live gateway is a separate, founder-gated
28 # step (see docs/PROVIDERS.md → Concentrate Notes).
29 set -euo pipefail
30
31 repo_root=$(CDPATH='' cd -- "$(dirname -- "$0")/.." && pwd)
32 cd "$repo_root"
33
34 model=${CONCENTRATE_SELFTEST_MODEL:-concentrate/auto}
35 expected_wire_model=${model#concentrate/}
36 stub_key=${CONCENTRATE_SELFTEST_KEY:-stub-key-not-a-real-credential}
37 work=$(mktemp -d "${TMPDIR:-/tmp}/concentrate-selftest.XXXXXX")
38 log="$work/stub.jsonl"
39 cleanup() {
40 if [ -n "${stub_pid:-}" ]; then kill "$stub_pid" 2>/dev/null || true; wait "$stub_pid" 2>/dev/null || true; fi
41 if [ -z "${CONCENTRATE_SELFTEST_KEEP:-}" ]; then rm -rf "$work"; fi
42 }
43 trap cleanup EXIT
44
45 bin=${CODEWHALE_BIN:-}
46 if [ -z "$bin" ]; then
47 if [ -x target/release/codewhale ]; then
48 bin=target/release/codewhale
49 else
50 echo "+ cargo build -p codewhale-cli --locked (debug; set CODEWHALE_BIN to skip)"
51 cargo build -p codewhale-cli --locked >/dev/null
52 bin=target/debug/codewhale
53 fi
54 fi
55 [ -x "$bin" ] || { echo "codewhale binary not executable: $bin" >&2; exit 2; }
56
57 # 1. Stub on a free loopback port.
58 port=$(python3 -c 'import socket; s=socket.socket(); s.bind(("127.0.0.1",0)); print(s.getsockname()[1]); s.close()')
59 CONCENTRATE_STUB_PORT=$port CONCENTRATE_STUB_EXPECT_KEY=$stub_key CONCENTRATE_STUB_LOG=$log \
60 python3 scripts/concentrate-stub.py >"$work/stub.out" 2>&1 &
61 stub_pid=$!
62 base="http://127.0.0.1:$port/v1"
63 for _ in $(seq 1 50); do
64 if curl -sf -o /dev/null "$base/responses/health"; then break; fi
65 sleep 0.1
66 done
67 curl -sf -o /dev/null "$base/responses/health" || { echo "stub did not answer /v1/responses/health" >&2; cat "$work/stub.out" >&2; exit 1; }
68 echo "ok: GET $base/responses/health -> 200"
69
70 # 2. Unauthenticated catalog.
71 models=$(curl -sf "$base/models")
72 python3 - "$models" <<'PY'
73 import json, sys
74 catalog = json.loads(sys.argv[1])
75 assert catalog["object"] == "list" and any(m["id"] == "deepseek-v4-pro" for m in catalog["data"]), catalog
76 print("ok: GET /v1/models is readable without a key (%d rows)" % len(catalog["data"]))
77 PY
78
79 # Isolated home + workspace so the run never touches the real config or secrets.
80 #
81 # The key and the loopback base URL are written into the isolated config file
82 # on purpose: Codewhale's credential-scope rule binds a saved or environment
83 # Concentrate key to the official gateway URL and refuses to send it to any
84 # other endpoint (a stub, a proxy, a typo). A custom endpoint receives a key
85 # only when the user writes both the base_url and the api_key into the same
86 # provider table — which is exactly what a BYOK user pointing at a local
87 # gateway would do.
88 home="$work/home"; ws="$work/ws"; mkdir -p "$home/.codewhale" "$ws"
89 write_config() {
90 local key=$1
91 cat >"$home/.codewhale/config.toml" <<TOML
92 provider = "concentrate"
93
94 [providers.concentrate]
95 base_url = "$base"
96 api_key = "$key"
97 model = "$model"
98 TOML
99 }
100 run_exec() {
101 local key=$1 prompt=$2 out=$3
102 write_config "$key"
103 HOME="$home" XDG_CONFIG_HOME="$home/.config" CODEWHALE_HOME="$home/.codewhale" \
104 CODEWHALE_CONFIG_PATH="$home/.codewhale/config.toml" \
105 "$bin" --workspace "$ws" --no-project-config exec --auto --output-format stream-json "$prompt" >"$out" 2>"$out.err"
106 }
107
108 # 3+4. Real turn through the stub.
109 set +e
110 run_exec "$stub_key" "say ok" "$work/turn.jsonl"
111 exit_code=$?
112 set -e
113 if [ "$exit_code" -ne 0 ]; then
114 echo "codewhale exec exited $exit_code" >&2; tail -20 "$work/turn.jsonl.err" >&2; exit 1
115 fi
116 python3 - "$log" "$work/turn.jsonl" "$stub_key" "$expected_wire_model" <<'PY'
117 import json, sys
118 log_path, turn_path, key, expected_model = sys.argv[1:5]
119 records = [json.loads(line) for line in open(log_path, encoding="utf-8") if line.strip()]
120 posts = [r for r in records if r["method"] == "POST"]
121 assert len(posts) == 1, f"expected exactly one POST /v1/responses, got {len(posts)}: {posts}"
122 post = posts[0]
123 assert post["path"].split("?")[0].rstrip("/") == "/v1/responses", post["path"]
124 assert post["authorization"] == f"Bearer {key}", post["authorization"]
125 assert post["model"] == expected_model, f"model passthrough: sent {post['model']!r}, expected {expected_model!r}"
126 assert post["stream"] is True, post
127 assert post["undocumented_fields"] == [], f"undocumented top-level fields sent: {post['undocumented_fields']}"
128 assert post["input_roles"] and post["input_roles"][0] == "system", f"system prompt must lead the input: {post['input_roles']}"
129 events = [json.loads(line) for line in open(turn_path, encoding="utf-8") if line.strip()]
130 types = [e.get("type") for e in events]
131 assert "done" in types, f"no done receipt in stream-json: {types}"
132 text = "".join(e.get("text") or e.get("content") or "" for e in events if e.get("type") == "content")
133 print("ok: POST /v1/responses once, Bearer header matched, model %r verbatim, stream:true, system item first, only documented fields" % expected_model)
134 print("ok: completed-turn receipt types = %s" % types)
135 if "ok from the concentrate stub" not in text:
136 print("missing reply text in content events; types = %s; text = %r" % (types, text), file=sys.stderr)
137 sys.exit(1)
138 print("ok: reply text reached the CLI output")
139 PY
140
141 # 5. Wrong key → documented 401 → non-zero exit.
142 : > "$log"
143 set +e
144 run_exec "wrong-key" "say ok" "$work/turn-401.jsonl"
145 bad_exit=$?
146 set -e
147 if [ "$bad_exit" -eq 0 ]; then echo "expected a non-zero exit with a wrong key" >&2; exit 1; fi
148 python3 - "$log" <<'PY'
149 import json, sys
150 records = [json.loads(line) for line in open(sys.argv[1], encoding="utf-8") if line.strip()]
151 posts = [r for r in records if r["method"] == "POST"]
152 assert posts and posts[-1]["authorization"] == "Bearer wrong-key", posts
153 print("ok: wrong key was sent as `Bearer wrong-key`; the stub answered the documented 401 body")
154 PY
155 if grep -q "Invalid API key\|401" "$work/turn-401.jsonl" "$work/turn-401.jsonl.err"; then
156 echo "ok: the 401 reached the CLI output (exit $bad_exit)"
157 else
158 echo "ok: CLI exited $bad_exit on the 401 (message: $(tail -1 "$work/turn-401.jsonl.err"))"
159 fi
160
161 echo "CONCENTRATE SELFTEST PASS (binary: $bin, model: $model, stub: $base)"
162
162 lines BASH