@junghanacs/entwurf 0.12.5 → 0.12.7
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/AGENTS.md +31 -17
- package/BASELINE.md +42 -8
- package/CHANGELOG.md +44 -0
- package/DELIVERY.md +71 -14
- package/README.md +77 -45
- package/VERIFY.md +33 -38
- package/docs/setup-clean-host.md +80 -22
- package/mcp/entwurf-bridge/dist/mcp/entwurf-bridge/src/index.js +90 -66
- package/mcp/entwurf-bridge/dist/pi-extensions/lib/acp/acp-client.js +54 -0
- package/mcp/entwurf-bridge/dist/pi-extensions/lib/acp/backend-adapter.js +153 -0
- package/mcp/entwurf-bridge/dist/pi-extensions/lib/acp/config.js +436 -0
- package/mcp/entwurf-bridge/dist/pi-extensions/lib/acp/context.js +157 -0
- package/mcp/entwurf-bridge/dist/pi-extensions/lib/acp/engraving.js +105 -0
- package/mcp/entwurf-bridge/dist/pi-extensions/lib/acp/models.js +90 -0
- package/mcp/entwurf-bridge/dist/pi-extensions/lib/acp/overlay.js +194 -0
- package/mcp/entwurf-bridge/dist/pi-extensions/lib/acp/tool-surface.js +153 -0
- package/mcp/entwurf-bridge/dist/pi-extensions/lib/entwurf-deliverability.js +42 -9
- package/mcp/entwurf-bridge/dist/pi-extensions/lib/entwurf-self-address.js +49 -13
- package/mcp/entwurf-bridge/dist/pi-extensions/lib/entwurf-v2-contract.js +104 -11
- package/mcp/entwurf-bridge/dist/pi-extensions/lib/entwurf-v2-decider.js +30 -1
- package/mcp/entwurf-bridge/dist/pi-extensions/lib/entwurf-v2-native-push.js +57 -0
- package/mcp/entwurf-bridge/dist/pi-extensions/lib/entwurf-v2-production.js +10 -0
- package/mcp/entwurf-bridge/dist/pi-extensions/lib/entwurf-v2-release.js +9 -0
- package/mcp/entwurf-bridge/dist/pi-extensions/lib/entwurf-v2-runner.js +21 -0
- package/mcp/entwurf-bridge/dist/pi-extensions/lib/entwurf-v2-send.js +5 -0
- package/mcp/entwurf-bridge/dist/pi-extensions/lib/entwurf-v2-surface.js +17 -0
- package/mcp/entwurf-bridge/dist/pi-extensions/lib/meta-sender-identity.js +125 -0
- package/mcp/entwurf-bridge/dist/pi-extensions/lib/native-push/adapter.js +158 -0
- package/mcp/entwurf-bridge/dist/pi-extensions/lib/native-push/register.js +61 -0
- package/mcp/entwurf-bridge/dist/scripts/agy-imprint.js +166 -0
- package/mcp/entwurf-bridge/dist/scripts/doctor-pi-provider.js +130 -0
- package/mcp/entwurf-bridge/dist/scripts/meta-bridge-prune.js +178 -0
- package/mcp/entwurf-bridge/dist/scripts/new-session-id.js +24 -0
- package/mcp/entwurf-bridge/src/index.ts +101 -67
- package/mcp/entwurf-bridge/test.sh +1 -1
- package/mcp/entwurf-bridge/tsconfig.build.json +23 -3
- package/package.json +10 -6
- package/pi-extensions/lib/entwurf-deliverability.ts +62 -9
- package/pi-extensions/lib/entwurf-self-address.ts +58 -15
- package/pi-extensions/lib/entwurf-v2-contract.ts +120 -12
- package/pi-extensions/lib/entwurf-v2-decider.ts +60 -0
- package/pi-extensions/lib/entwurf-v2-native-push.ts +86 -0
- package/pi-extensions/lib/entwurf-v2-production.ts +20 -0
- package/pi-extensions/lib/entwurf-v2-release.ts +9 -0
- package/pi-extensions/lib/entwurf-v2-runner.ts +29 -1
- package/pi-extensions/lib/entwurf-v2-send.ts +7 -0
- package/pi-extensions/lib/entwurf-v2-surface.ts +17 -0
- package/pi-extensions/lib/meta-sender-identity.ts +154 -0
- package/pi-extensions/lib/native-push/adapter.ts +255 -0
- package/pi-extensions/lib/native-push/register.ts +99 -0
- package/run.sh +974 -248
- package/scripts/agy-bridge-config.py +446 -0
- package/scripts/agy-bridge.sh +359 -0
- package/scripts/agy-hooks-bridge.sh +193 -0
- package/scripts/agy-hooks-config.py +257 -0
- package/scripts/agy-imprint.sh +28 -0
- package/scripts/agy-imprint.ts +193 -0
- package/scripts/agy-statusline-bridge.sh +176 -0
- package/scripts/agy-statusline-config.py +213 -0
- package/scripts/agy-statusline.sh +256 -0
- package/scripts/build-bridge.sh +20 -0
- package/scripts/check-agy-sender-identity.ts +364 -0
- package/scripts/check-entwurf-bridge-boot.ts +8 -2
- package/scripts/check-entwurf-deliverability.ts +34 -0
- package/scripts/check-entwurf-self-address.ts +78 -11
- package/scripts/check-entwurf-v2-contract.ts +136 -1
- package/scripts/check-entwurf-v2-decider.ts +95 -1
- package/scripts/check-entwurf-v2-matrix.ts +14 -3
- package/scripts/check-entwurf-v2-native-push.ts +193 -0
- package/scripts/check-entwurf-v2-production.ts +68 -1
- package/scripts/check-entwurf-v2-runner.ts +58 -0
- package/scripts/check-entwurf-v2-surface.ts +35 -0
- package/scripts/check-install-surface.ts +357 -0
- package/scripts/check-native-push-adapter.ts +319 -0
- package/scripts/check-native-push-register.ts +130 -0
- package/scripts/dev-bin.sh +195 -0
- package/scripts/doctor-pi-provider.ts +140 -0
- package/scripts/meta-bridge-doctor.sh +47 -9
- package/scripts/meta-bridge-install.sh +20 -17
- package/scripts/meta-bridge-state.py +40 -2
- package/scripts/meta-bridge-uninstall.sh +23 -12
- package/scripts/register-pi-package.py +183 -0
- package/scripts/register-pi-provider.py +287 -0
- package/scripts/smoke-agy-hooks-state.sh +172 -0
- package/scripts/smoke-agy-install-state.sh +660 -0
- package/scripts/smoke-agy-native-push-live.ts +243 -0
- package/scripts/smoke-agy-statusline-state.sh +300 -0
- package/scripts/smoke-meta-async-drift.sh +9 -2
- package/scripts/smoke-meta-install-state.sh +178 -8
- package/scripts/smoke-pi-provider-state.sh +182 -0
- package/scripts/smoke-user-scope-citizen.sh +191 -0
- package/scripts/with-dist-lock.sh +81 -0
- package/scripts/__pycache__/meta-bridge-state.cpython-312.pyc +0 -0
|
@@ -22,10 +22,58 @@ TMP="$(mktemp -d -t psa-meta-install-state.XXXXXX)"
|
|
|
22
22
|
cleanup() { rm -rf "$TMP"; }
|
|
23
23
|
trap cleanup EXIT
|
|
24
24
|
|
|
25
|
+
# ⓪ 설치 경계 봉쇄 (2026-07-03 `?` 사건의 구조적 소멸): source origin(repo/npm)과
|
|
26
|
+
# live artifact(XDG)를 분리한다. dev clone과 installed 소비자는 같은 user-scope
|
|
27
|
+
# install이고, 둘 다 marketplace source를 XDG data dir에 조립한다 — 절대 checkout
|
|
28
|
+
# 내부가 아니다. 이 smoke의 어떤 단계도 repo 안에 live source를 만들지 않음을
|
|
29
|
+
# top/bottom fingerprint로 감싸 증명하고, 아래 fake-install로 "install → XDG"를 직접 건다.
|
|
30
|
+
# present/absent만 보면 present→present 덮어쓰기를 놓치므로 내용 해시까지 비교한다.
|
|
31
|
+
asm_fingerprint() {
|
|
32
|
+
if [ -e "$REPO/pi/meta-bridge/.assembled" ]; then (cd "$REPO/pi/meta-bridge/.assembled" && find . -type f -exec sha256sum {} + 2>/dev/null | sort); else echo ABSENT; fi
|
|
33
|
+
}
|
|
34
|
+
REPO_ASM_FP_START="$(asm_fingerprint)"
|
|
35
|
+
|
|
36
|
+
# 실제 meta-bridge-install.sh를 격리된 HOME + XDG_DATA_HOME + fake claude로 돌려
|
|
37
|
+
# live marketplace source가 XDG 아래에 조립되고 `plugin marketplace add`가 그 XDG
|
|
38
|
+
# 경로를 받는지 직접 증명한다(오프라인: 실제 claude 없음).
|
|
39
|
+
INS_HOME="$TMP/ins-home"; INS_XDG="$TMP/ins-xdg"; INS_BIN="$TMP/ins-bin"
|
|
40
|
+
mkdir -p "$INS_HOME/.claude" "$INS_BIN"
|
|
41
|
+
echo '{}' > "$INS_HOME/.claude/settings.json"
|
|
42
|
+
echo '{}' > "$INS_HOME/.claude.json"
|
|
43
|
+
INS_LOG="$TMP/ins-claude.log"
|
|
44
|
+
cat > "$INS_BIN/claude" <<'SH'
|
|
45
|
+
#!/usr/bin/env bash
|
|
46
|
+
printf '%s\n' "$*" >> "$FAKE_CLAUDE_LOG"
|
|
47
|
+
case "$1${2:+ $2}" in
|
|
48
|
+
"plugin list") printf '%s\n' "entwurf-meta-receive@meta-bridge-local" " Status: enabled" ;;
|
|
49
|
+
"mcp get") printf '%s\n' "Scope: User config" "Status: Connected" ;;
|
|
50
|
+
*) : ;;
|
|
51
|
+
esac
|
|
52
|
+
exit 0
|
|
53
|
+
SH
|
|
54
|
+
chmod +x "$INS_BIN/claude"
|
|
55
|
+
if env HOME="$INS_HOME" CLAUDE_CONFIG_DIR="$INS_HOME/.claude" XDG_DATA_HOME="$INS_XDG" \
|
|
56
|
+
FAKE_CLAUDE_LOG="$INS_LOG" PATH="$INS_BIN:$PATH" \
|
|
57
|
+
bash "$REPO/scripts/meta-bridge-install.sh" >/dev/null 2>&1; then
|
|
58
|
+
if [ -e "$INS_XDG/entwurf/meta-bridge/.assembled/entwurf-meta-receive" ]; then ok "dev install assembles the live marketplace source under XDG (not the checkout)"; else bad "dev install did not assemble under XDG_DATA_HOME"; fi
|
|
59
|
+
MKT_ADD="$(grep '^plugin marketplace add ' "$INS_LOG" | head -1 || true)"
|
|
60
|
+
if printf '%s' "$MKT_ADD" | grep -Fq "$INS_XDG/entwurf/meta-bridge/.assembled"; then ok "claude plugin marketplace add received the XDG artifact path"; else bad "marketplace add did not point at XDG: $MKT_ADD"; fi
|
|
61
|
+
else
|
|
62
|
+
bad "real install-meta-bridge failed under isolated HOME/XDG/fake-claude:"$'\n'"$(cat "$INS_LOG" 2>/dev/null)"
|
|
63
|
+
fi
|
|
64
|
+
|
|
25
65
|
export HOME="$TMP/home"
|
|
66
|
+
# The wrapper-uninstall below removes ${XDG_DATA_HOME:-$HOME/.local/share}/entwurf/
|
|
67
|
+
# meta-bridge. Pin XDG_DATA_HOME into the sandbox too, so a developer who runs this
|
|
68
|
+
# smoke with XDG_DATA_HOME set in their shell can never have their real live
|
|
69
|
+
# artifact removed — the boundary that keeps check/smoke off global user wiring.
|
|
70
|
+
export XDG_DATA_HOME="$TMP/xdg"
|
|
26
71
|
export CLAUDE_CONFIG_DIR="$HOME/.claude"
|
|
27
72
|
mkdir -p "$CLAUDE_CONFIG_DIR"
|
|
28
|
-
|
|
73
|
+
# Canonical-suffix asm: production install ALWAYS records a path ending in
|
|
74
|
+
# …/entwurf/meta-bridge/.assembled, and state.py check now shape-validates that
|
|
75
|
+
# suffix — so the survival harness must use a realistic path, not a bare stub.
|
|
76
|
+
ASM="$TMP/asm-xdg/entwurf/meta-bridge/.assembled"
|
|
29
77
|
export ASM
|
|
30
78
|
mkdir -p "$ASM"
|
|
31
79
|
|
|
@@ -213,8 +261,10 @@ then ok "uninstall restores scalars/maps and removes only managed array addition
|
|
|
213
261
|
|
|
214
262
|
if py uninstall >/dev/null 2>&1; then bad "state manager uninstall without state should not guess"; else ok "state manager uninstall without state fails instead of guessing"; fi
|
|
215
263
|
|
|
216
|
-
#
|
|
217
|
-
#
|
|
264
|
+
# ⓪ 봉쇄: wrapper-uninstall을 실제 $REPO 스크립트로 직접 돌린다. 이제 uninstall이
|
|
265
|
+
# 지우는 live artifact는 ${XDG_DATA_HOME}/entwurf/meta-bridge 하나뿐이고, 위에서
|
|
266
|
+
# HOME + XDG_DATA_HOME를 샌드박스로 고정했으므로 실제 개발자 배선/체크아웃에는
|
|
267
|
+
# 닿을 수 없다(2026-07-03 `?` 사건의 구조적 소멸 — 사본 repo 우회가 더는 불필요).
|
|
218
268
|
FAKE_BIN="$TMP/fake-bin"; mkdir -p "$FAKE_BIN"
|
|
219
269
|
cat > "$FAKE_BIN/claude" <<'SH'
|
|
220
270
|
#!/usr/bin/env bash
|
|
@@ -222,6 +272,9 @@ printf '%s\n' "$*" >> "$FAKE_CLAUDE_LOG"
|
|
|
222
272
|
exit 0
|
|
223
273
|
SH
|
|
224
274
|
chmod +x "$FAKE_BIN/claude"
|
|
275
|
+
|
|
276
|
+
# The wrapper must refuse before side effects. A fake claude records whether
|
|
277
|
+
# plugin/MCP removals were attempted; no state means the log must stay empty.
|
|
225
278
|
FAKE_CLAUDE_LOG="$TMP/fake-claude-nostate.log"
|
|
226
279
|
if PATH="$FAKE_BIN:$PATH" FAKE_CLAUDE_LOG="$FAKE_CLAUDE_LOG" bash "$REPO/scripts/meta-bridge-uninstall.sh" >/dev/null 2>&1; then
|
|
227
280
|
bad "wrapper uninstall without state should fail"
|
|
@@ -229,10 +282,14 @@ else
|
|
|
229
282
|
if [ ! -s "$FAKE_CLAUDE_LOG" ]; then ok "wrapper uninstall without state has zero Claude side effects"; else bad "wrapper uninstall without state touched Claude: $(cat "$FAKE_CLAUDE_LOG")"; fi
|
|
230
283
|
fi
|
|
231
284
|
|
|
232
|
-
# With valid state, the wrapper
|
|
233
|
-
#
|
|
234
|
-
|
|
235
|
-
|
|
285
|
+
# With valid state, the wrapper removes Claude registrations, restores JSON state,
|
|
286
|
+
# AND removes the XDG live artifact RECORDED in state — never the checkout, never a
|
|
287
|
+
# path recomputed from the env. Record an XDG-structured assembled path and seed the
|
|
288
|
+
# sentinel artifact so we can prove uninstall drops exactly the recorded tree.
|
|
289
|
+
WASM="$XDG_DATA_HOME/entwurf/meta-bridge/.assembled"
|
|
290
|
+
python3 "$STATE" prepare --repo "$REPO" --asm "$WASM" >/dev/null
|
|
291
|
+
python3 "$STATE" apply --repo "$REPO" --asm "$WASM" >/dev/null
|
|
292
|
+
mkdir -p "$WASM"; : > "$WASM/.sentinel"
|
|
236
293
|
FAKE_CLAUDE_LOG="$TMP/fake-claude-state.log"
|
|
237
294
|
if PATH="$FAKE_BIN:$PATH" FAKE_CLAUDE_LOG="$FAKE_CLAUDE_LOG" bash "$REPO/scripts/meta-bridge-uninstall.sh" >/dev/null 2>&1; then
|
|
238
295
|
if grep -q 'plugin uninstall entwurf-meta-receive@meta-bridge-local' "$FAKE_CLAUDE_LOG" && grep -q 'mcp remove entwurf-bridge -s user' "$FAKE_CLAUDE_LOG" && [ ! -f "$STATE_FILE" ]; then
|
|
@@ -240,10 +297,97 @@ if PATH="$FAKE_BIN:$PATH" FAKE_CLAUDE_LOG="$FAKE_CLAUDE_LOG" bash "$REPO/scripts
|
|
|
240
297
|
else
|
|
241
298
|
bad "wrapper uninstall with state missed expected side effects/state removal"
|
|
242
299
|
fi
|
|
300
|
+
# 봉쇄가 회피가 아니라 구조임을 증명: uninstall이 지우는 건 recorded XDG artifact뿐.
|
|
301
|
+
if [ ! -e "$XDG_DATA_HOME/entwurf/meta-bridge" ]; then ok "wrapper removes the recorded XDG live artifact tree, not the checkout"; else bad "wrapper did not remove the recorded XDG live artifact"; fi
|
|
243
302
|
else
|
|
244
303
|
bad "wrapper uninstall with valid state failed"
|
|
245
304
|
fi
|
|
246
305
|
|
|
306
|
+
# Honest inverse under a CHANGED XDG_DATA_HOME (GPT hardening): uninstall must remove
|
|
307
|
+
# the RECORDED artifact path, never one recomputed from the current env. Record A,
|
|
308
|
+
# run uninstall with the env pointing at B, and prove A is gone while B is untouched.
|
|
309
|
+
XDGA="$TMP/xdgA"; XDGB="$TMP/xdgB"
|
|
310
|
+
ASM_A="$XDGA/entwurf/meta-bridge/.assembled"
|
|
311
|
+
python3 "$STATE" prepare --repo "$REPO" --asm "$ASM_A" >/dev/null
|
|
312
|
+
python3 "$STATE" apply --repo "$REPO" --asm "$ASM_A" >/dev/null
|
|
313
|
+
mkdir -p "$ASM_A"; : > "$ASM_A/.sentinel"
|
|
314
|
+
mkdir -p "$XDGB/entwurf/meta-bridge/.assembled"; : > "$XDGB/entwurf/meta-bridge/.assembled/.sentinel"
|
|
315
|
+
if XDG_DATA_HOME="$XDGB" PATH="$FAKE_BIN:$PATH" FAKE_CLAUDE_LOG="$TMP/fake-claude-mismatch.log" bash "$REPO/scripts/meta-bridge-uninstall.sh" >/dev/null 2>&1; then
|
|
316
|
+
if [ ! -e "$XDGA/entwurf/meta-bridge" ]; then ok "uninstall removes the RECORDED artifact (XDG A) even when env XDG_DATA_HOME differs"; else bad "uninstall did not remove the recorded artifact A — recomputed from env instead"; fi
|
|
317
|
+
if [ -e "$XDGB/entwurf/meta-bridge/.assembled/.sentinel" ]; then ok "uninstall leaves the current-env XDG (B) untouched (no env recompute)"; else bad "uninstall wrongly removed the current-env XDG B"; fi
|
|
318
|
+
else
|
|
319
|
+
bad "wrapper uninstall (recorded-path mismatch case) failed"
|
|
320
|
+
fi
|
|
321
|
+
|
|
322
|
+
# GPT hardening: a corrupt recorded assembledMarketplacePath must fail the wrapper
|
|
323
|
+
# uninstall LOUD and BEFORE any side effect — no Claude removal, state file intact
|
|
324
|
+
# (honest inverse never guesses / never partially uninstalls / never WARN-then-DONE).
|
|
325
|
+
# Two corrupt shapes: a trivially-bad "/" AND the basename-corrupt case that a
|
|
326
|
+
# PARENT-ONLY guard would silently pass (…/entwurf/meta-bridge/not-assembled) — the
|
|
327
|
+
# rm targets the parent meta-bridge dir, so that shape must be refused by the FULL
|
|
328
|
+
# suffix guard or it nukes the real artifact + Claude registrations (2026-07-03).
|
|
329
|
+
for CORRUPT in "/" "$XDG_DATA_HOME/entwurf/meta-bridge/not-assembled"; do
|
|
330
|
+
python3 "$STATE" prepare --repo "$REPO" --asm "$XDG_DATA_HOME/entwurf/meta-bridge/.assembled" >/dev/null
|
|
331
|
+
python3 "$STATE" apply --repo "$REPO" --asm "$XDG_DATA_HOME/entwurf/meta-bridge/.assembled" >/dev/null
|
|
332
|
+
# seed the real artifact so an over-broad rm would be observable as its removal
|
|
333
|
+
mkdir -p "$XDG_DATA_HOME/entwurf/meta-bridge/.assembled"; : > "$XDG_DATA_HOME/entwurf/meta-bridge/.assembled/.sentinel"
|
|
334
|
+
CORRUPT="$CORRUPT" python3 - <<'PY'
|
|
335
|
+
import json, os
|
|
336
|
+
p = os.environ['CLAUDE_CONFIG_DIR'] + '/entwurf.install-state.json'
|
|
337
|
+
s = json.load(open(p)); s['assembledMarketplacePath'] = os.environ['CORRUPT']
|
|
338
|
+
json.dump(s, open(p, 'w'), indent=2)
|
|
339
|
+
PY
|
|
340
|
+
FAKE_CLAUDE_LOG="$TMP/fake-claude-corrupt.log"; : > "$FAKE_CLAUDE_LOG"
|
|
341
|
+
if PATH="$FAKE_BIN:$PATH" FAKE_CLAUDE_LOG="$FAKE_CLAUDE_LOG" bash "$REPO/scripts/meta-bridge-uninstall.sh" >/dev/null 2>&1; then
|
|
342
|
+
bad "wrapper uninstall with corrupt recorded path '$CORRUPT' should fail loud"
|
|
343
|
+
elif [ -s "$FAKE_CLAUDE_LOG" ] || [ ! -f "$STATE_FILE" ] || [ ! -e "$XDG_DATA_HOME/entwurf/meta-bridge/.assembled/.sentinel" ]; then
|
|
344
|
+
bad "corrupt-path uninstall '$CORRUPT' leaked side effects/removed artifact: claude_log=[$(cat "$FAKE_CLAUDE_LOG")] state_exists=$([ -f "$STATE_FILE" ] && echo yes || echo no) artifact=$([ -e "$XDG_DATA_HOME/entwurf/meta-bridge/.assembled/.sentinel" ] && echo intact || echo REMOVED)"
|
|
345
|
+
else
|
|
346
|
+
ok "corrupt recorded path '$CORRUPT' → uninstall fails BEFORE side effects (no Claude removal, state + artifact intact)"
|
|
347
|
+
fi
|
|
348
|
+
rm -f "$STATE_FILE"; rm -rf "$XDG_DATA_HOME/entwurf/meta-bridge"
|
|
349
|
+
done
|
|
350
|
+
|
|
351
|
+
# GPT hardening: state.py check must compare the RECORDED marketplace path, not the
|
|
352
|
+
# --asm handed in. Install/apply with A, then check with a DIFFERENT --asm B: it must
|
|
353
|
+
# still PASS (Claude settings + state both hold A; only the caller's XDG differs).
|
|
354
|
+
CKA="$TMP/ck-xdgA/entwurf/meta-bridge/.assembled"
|
|
355
|
+
CKB="$TMP/ck-xdgB/entwurf/meta-bridge/.assembled"
|
|
356
|
+
python3 "$STATE" prepare --repo "$REPO" --asm "$CKA" >/dev/null
|
|
357
|
+
python3 "$STATE" apply --repo "$REPO" --asm "$CKA" >/dev/null
|
|
358
|
+
if python3 "$STATE" check --repo "$REPO" --asm "$CKB" >/dev/null 2>&1; then ok "state.py check passes when --asm differs from the recorded path (compares recorded, no XDG false-fail)"; else bad "state.py check false-FAILed on a recorded≠--asm mismatch"; fi
|
|
359
|
+
rm -f "$STATE_FILE"
|
|
360
|
+
|
|
361
|
+
# GPT hardening: check must shape-validate the recorded path, not just compare it to
|
|
362
|
+
# settings. Corrupt BOTH state + settings to the same basename-bad value: a pure
|
|
363
|
+
# consistency compare would PASS (both agree) and greenlight a bogus marketplace
|
|
364
|
+
# source. The suffix guard must FAIL it instead.
|
|
365
|
+
python3 "$STATE" prepare --repo "$REPO" --asm "$XDG_DATA_HOME/entwurf/meta-bridge/.assembled" >/dev/null
|
|
366
|
+
python3 "$STATE" apply --repo "$REPO" --asm "$XDG_DATA_HOME/entwurf/meta-bridge/.assembled" >/dev/null
|
|
367
|
+
python3 - <<'PY'
|
|
368
|
+
import json, os
|
|
369
|
+
xdg = os.environ['XDG_DATA_HOME']; corrupt = xdg + '/entwurf/meta-bridge/not-assembled'
|
|
370
|
+
sp = os.environ['CLAUDE_CONFIG_DIR'] + '/entwurf.install-state.json'
|
|
371
|
+
s = json.load(open(sp)); s['assembledMarketplacePath'] = corrupt; json.dump(s, open(sp, 'w'), indent=2)
|
|
372
|
+
cp = os.environ['CLAUDE_CONFIG_DIR'] + '/settings.json'
|
|
373
|
+
c = json.load(open(cp)); c['extraKnownMarketplaces']['meta-bridge-local'] = {'source': {'source': 'directory', 'path': corrupt}}; json.dump(c, open(cp, 'w'), indent=2)
|
|
374
|
+
PY
|
|
375
|
+
if python3 "$STATE" check --repo "$REPO" --asm "$XDG_DATA_HOME/entwurf/meta-bridge/.assembled" >/dev/null 2>&1; then bad "state.py check greenlit a both-corrupt (state+settings) malformed marketplace path"; else ok "state.py check fails a malformed recorded path even when settings agree (shape guard, not just consistency)"; fi
|
|
376
|
+
rm -f "$STATE_FILE"
|
|
377
|
+
|
|
378
|
+
# GPT hardening: a MISSING/empty recorded path is corruption too — every state our
|
|
379
|
+
# code writes carries the field, so check must NOT fall back to --asm and PASS.
|
|
380
|
+
# Delete the field with settings still valid; the shape guard must FAIL, not greenlight.
|
|
381
|
+
python3 "$STATE" prepare --repo "$REPO" --asm "$XDG_DATA_HOME/entwurf/meta-bridge/.assembled" >/dev/null
|
|
382
|
+
python3 "$STATE" apply --repo "$REPO" --asm "$XDG_DATA_HOME/entwurf/meta-bridge/.assembled" >/dev/null
|
|
383
|
+
python3 - <<'PY'
|
|
384
|
+
import json, os
|
|
385
|
+
sp = os.environ['CLAUDE_CONFIG_DIR'] + '/entwurf.install-state.json'
|
|
386
|
+
s = json.load(open(sp)); s.pop('assembledMarketplacePath', None); json.dump(s, open(sp, 'w'), indent=2)
|
|
387
|
+
PY
|
|
388
|
+
if python3 "$STATE" check --repo "$REPO" --asm "$XDG_DATA_HOME/entwurf/meta-bridge/.assembled" >/dev/null 2>&1; then bad "state.py check PASSed with a MISSING recorded assembledMarketplacePath (fell back to --asm)"; else ok "state.py check fails a missing/empty recorded path (no --asm fallback greenlight)"; fi
|
|
389
|
+
rm -f "$STATE_FILE"
|
|
390
|
+
|
|
247
391
|
# Doctor hook-log recovery predicate: only a later `INFO armed watch` clears an
|
|
248
392
|
# ERROR. A UserPromptSubmit `INFO attach record` is degraded backfill, not wake
|
|
249
393
|
# recovery.
|
|
@@ -365,7 +509,7 @@ if node --experimental-strip-types "$STORE_DOCTOR" "$STORE" >/dev/null 2>&1; the
|
|
|
365
509
|
# chain to its final summary line — i.e. no early set -e death anywhere.
|
|
366
510
|
DOC_HOME="$TMP/doctor-home"; DOC_CFG="$DOC_HOME/.claude"
|
|
367
511
|
DOC_AGENT="$TMP/doctor-agent"; DOC_STORE="$DOC_AGENT/meta-sessions"
|
|
368
|
-
DOC_BIN="$TMP/doctor-bin"; DOC_ASM="$
|
|
512
|
+
DOC_BIN="$TMP/doctor-bin"; DOC_ASM="$XDG_DATA_HOME/entwurf/meta-bridge/.assembled" # SAME asm the doctor computes (XDG), so only the MCP drifts
|
|
369
513
|
mkdir -p "$DOC_CFG" "$DOC_STORE" "$DOC_BIN"
|
|
370
514
|
echo '{}' > "$DOC_CFG/settings.json"
|
|
371
515
|
echo '{}' > "$DOC_HOME/.claude.json"
|
|
@@ -375,6 +519,14 @@ valid_record "20260606T120000-eeeeee" "doctor-native" > "$DOC_STORE/20260606T120
|
|
|
375
519
|
# Fake claude: a healthy toolchain so the doctor sails past every other section.
|
|
376
520
|
cat > "$DOC_BIN/claude" <<'SH'
|
|
377
521
|
#!/usr/bin/env bash
|
|
522
|
+
if [ "${1:-} ${2:-} ${3:-}" = "plugin list --json" ]; then
|
|
523
|
+
if [ "${FAKE_PLUGIN_CACHE_MISS:-0}" = 1 ]; then
|
|
524
|
+
printf '%s\n' '[{"id":"entwurf-meta-receive@meta-bridge-local","enabled":true,"errors":["Marketplace meta-bridge-local failed to load: cache-miss"]}]'
|
|
525
|
+
else
|
|
526
|
+
printf '%s\n' '[{"id":"entwurf-meta-receive@meta-bridge-local","enabled":true}]'
|
|
527
|
+
fi
|
|
528
|
+
exit 0
|
|
529
|
+
fi
|
|
378
530
|
case "$1${2:+ $2}" in
|
|
379
531
|
"--version") echo "2.1.167 (Claude Code)" ;;
|
|
380
532
|
"plugin list") printf '%s\n' "entwurf-meta-receive@meta-bridge-local" " Status: enabled" ;;
|
|
@@ -405,5 +557,23 @@ if printf '%s\n' "$DOC_OUT" | grep -q 'user MCP entwurf-bridge'; then ok "doctor
|
|
|
405
557
|
if printf '%s\n' "$DOC_OUT" | grep -q '\[plugin install'; then ok "doctor continues to a later section after the drift (no early set -e death)"; else bad "doctor did not reach a later section header after the drift:"$'\n'"$DOC_OUT"; fi
|
|
406
558
|
if printf '%s\n' "$DOC_OUT" | grep -q 'meta-bridge doctor: FAIL'; then ok "doctor runs the whole chain to its final summary line"; else bad "doctor did not reach its final summary line (mid-run death):"$'\n'"$DOC_OUT"; fi
|
|
407
559
|
|
|
560
|
+
# A plugin can be configured enabled while Claude refuses to load its missing marketplace
|
|
561
|
+
# source. Text-only `grep enabled` called that healthy and let `🪛 ? cc` look mysterious.
|
|
562
|
+
set +e
|
|
563
|
+
DOC_CACHE_OUT="$(env HOME="$DOC_HOME" CLAUDE_CONFIG_DIR="$DOC_CFG" PI_CODING_AGENT_DIR="$DOC_AGENT" ENTWURF_META_SESSIONS_DIR="$DOC_STORE" FAKE_PLUGIN_CACHE_MISS=1 PATH="$DOC_BIN:$PATH" bash "$REPO/scripts/meta-bridge-doctor.sh" 2>&1)"
|
|
564
|
+
DOC_CACHE_CODE=$?
|
|
565
|
+
set -e
|
|
566
|
+
if [ "$DOC_CACHE_CODE" -eq 1 ] && printf '%s\n' "$DOC_CACHE_OUT" | grep -q 'enabled=true but FAILED TO LOAD:.*cache-miss'; then
|
|
567
|
+
ok "doctor distinguishes configured-enabled from loadable (cache-miss is a hard failure)"
|
|
568
|
+
else
|
|
569
|
+
bad "doctor did not surface enabled-but-cache-miss as a hard load failure:"$'\n'"$DOC_CACHE_OUT"
|
|
570
|
+
fi
|
|
571
|
+
|
|
572
|
+
# ⓪ 경계 종료 단언: 이 smoke 전체(install 조립 + state + wrapper-uninstall + doctor)가
|
|
573
|
+
# 끝난 뒤에도 checkout 안에는 live marketplace source가 생기지 않았다 — source
|
|
574
|
+
# origin(repo)과 live artifact(XDG)가 구조적으로 분리됐다는 최종 증명. 미래 회귀가
|
|
575
|
+
# repo 내부 ASM을 되살리거나 덮어쓰면(내용 해시 변화 포함) 여기서 잡힌다.
|
|
576
|
+
if [ "$(asm_fingerprint)" = "$REPO_ASM_FP_START" ]; then ok "checkout-internal marketplace source is byte-identical before/after — no op created or mutated \$REPO/.assembled (source origin ≠ live artifact)"; else bad "a meta-bridge operation created/mutated \$REPO/pi/meta-bridge/.assembled — repo boundary breached"; fi
|
|
577
|
+
|
|
408
578
|
echo
|
|
409
579
|
if [ "$fail" -eq 0 ]; then echo "smoke-meta-install-state: PASS"; else echo "smoke-meta-install-state: FAIL (see above)"; exit 1; fi
|
|
@@ -0,0 +1,182 @@
|
|
|
1
|
+
#!/usr/bin/env bash
|
|
2
|
+
# smoke-pi-provider-state — regression gate for the pi provider install adapter (#46 Task 2):
|
|
3
|
+
# register-pi-provider.py (ownership-classified install/remove, user+project scopes) + the
|
|
4
|
+
# read-only doctor-pi-provider.ts (effective shadow view). Runs in an ISOLATED HOME + XDG with a
|
|
5
|
+
# fake stable bin and isolated settings files (no real ~/.pi). Asserts:
|
|
6
|
+
# - user install ownership matrix: absent→create+state; managed-legacy(repo start.sh)→bare bin +
|
|
7
|
+
# state(preimage audit, NOT restored) + siblings preserved; managed-current→idempotent;
|
|
8
|
+
# user-override→NOT overwritten, NO state (unowned).
|
|
9
|
+
# - user remove: state-based honest inverse (managed-* → remove OUR key, a legacy repo path is
|
|
10
|
+
# NOT restored; siblings kept; parent tidied); user-override → no state → nothing to undo.
|
|
11
|
+
# - legacy bundle prune (session-bridge/pi-tools-bridge repo path) alongside, user my-own kept.
|
|
12
|
+
# - project scope: NO state; install normalizes the bare bin, remove strips our-managed shapes
|
|
13
|
+
# (bare bin AND legacy repo path), a user override left in place.
|
|
14
|
+
# - doctor: effective = project shadows user; user-only/both-bare → ok green; project-STALE
|
|
15
|
+
# (user bare, project legacy) → effective legacy note (the "doctor green runtime red" guard);
|
|
16
|
+
# state-owned DRIFT → FAIL; bare-but-dangling → FAIL; malformed settings → FAIL; symlink refuse.
|
|
17
|
+
# - checkout stays byte-identical (nothing under $REPO).
|
|
18
|
+
# Offline + deterministic (deps: bash + python3 + node --experimental-strip-types).
|
|
19
|
+
set -euo pipefail
|
|
20
|
+
|
|
21
|
+
REPO_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")/.." && pwd)"
|
|
22
|
+
REG="$REPO_DIR/scripts/register-pi-provider.py"
|
|
23
|
+
DOCTOR="$REPO_DIR/scripts/doctor-pi-provider.ts"
|
|
24
|
+
|
|
25
|
+
pass=0
|
|
26
|
+
ok() { printf ' ok %s\n' "$1"; pass=$((pass + 1)); }
|
|
27
|
+
die() { printf 'FAIL: %s\n' "$1" >&2; exit 1; }
|
|
28
|
+
want() { eval "$2" && ok "$1" || die "$1"; }
|
|
29
|
+
|
|
30
|
+
REPO_BEFORE="$(cd "$REPO_DIR" && git status --porcelain)"
|
|
31
|
+
|
|
32
|
+
SB="$(mktemp -d)"
|
|
33
|
+
trap 'rm -rf "$SB"' EXIT
|
|
34
|
+
export HOME="$SB/home"
|
|
35
|
+
export XDG_DATA_HOME="$SB/xdg"
|
|
36
|
+
GLOBAL="$SB/global.json"
|
|
37
|
+
PROJECT="$SB/proj/.pi/settings.json"
|
|
38
|
+
STATE="$XDG_DATA_HOME/entwurf/pi-provider/install-state.json"
|
|
39
|
+
mkdir -p "$SB/proj/.pi" "$SB/bin" "$SB/home"
|
|
40
|
+
|
|
41
|
+
# A fake repo anchor for the managed-legacy predicate (endswith /entwurf/mcp/entwurf-bridge/start.sh).
|
|
42
|
+
FAKE_REPO="$SB/checkout/entwurf"
|
|
43
|
+
LEGACY_CMD="$FAKE_REPO/mcp/entwurf-bridge/start.sh"
|
|
44
|
+
|
|
45
|
+
# fake stable bin on PATH so the bare command RESOLVES in the doctor. Keep THIS fake
|
|
46
|
+
# authoritative: drop the dir that holds a REAL entwurf-bridge (this dev host exposes one at
|
|
47
|
+
# ~/.local/bin) so the dangling test is not masked by it.
|
|
48
|
+
printf '#!/usr/bin/env bash\necho fake-bridge\n' > "$SB/bin/entwurf-bridge"
|
|
49
|
+
chmod +x "$SB/bin/entwurf-bridge"
|
|
50
|
+
_real_bridge="$(command -v entwurf-bridge 2>/dev/null || true)"
|
|
51
|
+
_real_dir="${_real_bridge%/*}"
|
|
52
|
+
if [ -n "$_real_dir" ]; then
|
|
53
|
+
PATH="$(printf '%s' "$PATH" | tr ':' '\n' | grep -vFx "$_real_dir" | paste -sd: -)"
|
|
54
|
+
fi
|
|
55
|
+
export PATH="$SB/bin:$PATH"
|
|
56
|
+
|
|
57
|
+
reg() { python3 "$REG" "$@"; }
|
|
58
|
+
doctor() { PI_PROVIDER_GLOBAL_SETTINGS="$GLOBAL" PI_PROVIDER_PROJECT_SETTINGS="$PROJECT" PI_PROVIDER_STATE="$STATE" \
|
|
59
|
+
node --experimental-strip-types "$DOCTOR"; }
|
|
60
|
+
cmd_of() { python3 -c 'import json,sys
|
|
61
|
+
try:
|
|
62
|
+
d=json.load(open(sys.argv[1])); print(d["entwurfProvider"]["mcpServers"]["entwurf-bridge"]["command"])
|
|
63
|
+
except Exception: print("<none>")' "$1"; }
|
|
64
|
+
|
|
65
|
+
# ── A: user install — absent → create + state(ownership=absent) ───────────────
|
|
66
|
+
printf '{"defaultProvider":"x"}\n' > "$GLOBAL"
|
|
67
|
+
reg install "$GLOBAL" "$FAKE_REPO" --scope user --state "$STATE" >/dev/null
|
|
68
|
+
want "user/absent: command is bare stable bin" "[ \"\$(cmd_of '$GLOBAL')\" = entwurf-bridge ]"
|
|
69
|
+
want "user/absent: state ownership=absent" \
|
|
70
|
+
"python3 -c 'import json,sys; print(json.load(open(sys.argv[1]))[\"ownership\"])' '$STATE' | grep -qx absent"
|
|
71
|
+
|
|
72
|
+
# ── B: user install — managed-legacy(repo start.sh) → bare bin + preimage + siblings kept ──
|
|
73
|
+
rm -f "$STATE"
|
|
74
|
+
python3 -c 'import json,sys; json.dump({"entwurfProvider":{"appendSystemPrompt":False,"skillPlugins":["/p"],"mcpServers":{"entwurf-bridge":{"command":sys.argv[2]}}},"packages":["y"]}, open(sys.argv[1],"w"))' "$GLOBAL" "$LEGACY_CMD"
|
|
75
|
+
OUT="$(reg install "$GLOBAL" "$FAKE_REPO" --scope user --state "$STATE")"
|
|
76
|
+
want "user/managed-legacy: reported as managed-legacy" "printf '%s' \"\$OUT\" | grep -q 'managed-legacy'"
|
|
77
|
+
want "user/managed-legacy: normalized to bare bin" "[ \"\$(cmd_of '$GLOBAL')\" = entwurf-bridge ]"
|
|
78
|
+
want "user/managed-legacy: siblings preserved (skillPlugins/appendSystemPrompt)" \
|
|
79
|
+
"python3 -c 'import json,sys; ep=json.load(open(sys.argv[1]))[\"entwurfProvider\"]; sys.exit(0 if ep.get(\"skillPlugins\")==[\"/p\"] and ep.get(\"appendSystemPrompt\") is False else 1)' '$GLOBAL'"
|
|
80
|
+
want "user/managed-legacy: state ownership=managed-legacy" \
|
|
81
|
+
"python3 -c 'import json,sys; print(json.load(open(sys.argv[1]))[\"ownership\"])' '$STATE' | grep -qx managed-legacy"
|
|
82
|
+
want "user/managed-legacy: preimage records the repo path (audit, not restored)" \
|
|
83
|
+
"python3 -c 'import json,sys; p=json.load(open(sys.argv[1]))[\"preimage\"]; sys.exit(0 if p and p.get(\"command\")==sys.argv[2] else 1)' '$STATE' '$LEGACY_CMD'"
|
|
84
|
+
want "user/managed-legacy: timing honesty label emitted" \
|
|
85
|
+
"printf '%s' \"\$OUT\" | grep -q 'existing pi sessions unaffected until restart'"
|
|
86
|
+
|
|
87
|
+
# ── C: user install — managed-current(bare) → idempotent ──────────────────────
|
|
88
|
+
OUT="$(reg install "$GLOBAL" "$FAKE_REPO" --scope user --state "$STATE")"
|
|
89
|
+
want "user/managed-current: idempotent (stays bare)" "[ \"\$(cmd_of '$GLOBAL')\" = entwurf-bridge ]"
|
|
90
|
+
want "user/managed-current: reported managed-current" "printf '%s' \"\$OUT\" | grep -q 'managed-current'"
|
|
91
|
+
|
|
92
|
+
# ── D: user install — user-override → NOT overwritten, NO state ───────────────
|
|
93
|
+
rm -f "$STATE"
|
|
94
|
+
printf '{"entwurfProvider":{"mcpServers":{"entwurf-bridge":{"command":"/my/own/thing"}}}}\n' > "$GLOBAL"
|
|
95
|
+
OUT="$(reg install "$GLOBAL" "$FAKE_REPO" --scope user --state "$STATE")"
|
|
96
|
+
want "user/override: command left untouched" "[ \"\$(cmd_of '$GLOBAL')\" = /my/own/thing ]"
|
|
97
|
+
want "user/override: NO state written (unowned)" "[ ! -f '$STATE' ]"
|
|
98
|
+
want "user/override: reported preserved user override" "printf '%s' \"\$OUT\" | grep -qi 'user override'"
|
|
99
|
+
|
|
100
|
+
# ── E: user remove — state-based honest inverse (key removed, siblings kept) ──
|
|
101
|
+
python3 -c 'import json,sys; json.dump({"entwurfProvider":{"skillPlugins":["/p"],"mcpServers":{"entwurf-bridge":{"command":sys.argv[2]}}}}, open(sys.argv[1],"w"))' "$GLOBAL" "$LEGACY_CMD"
|
|
102
|
+
reg install "$GLOBAL" "$FAKE_REPO" --scope user --state "$STATE" >/dev/null
|
|
103
|
+
reg remove "$GLOBAL" "$FAKE_REPO" --scope user --state "$STATE" >/dev/null
|
|
104
|
+
want "user/remove: our key removed (NOT restored to repo path)" "[ \"\$(cmd_of '$GLOBAL')\" = '<none>' ]"
|
|
105
|
+
want "user/remove: sibling skillPlugins preserved" \
|
|
106
|
+
"python3 -c 'import json,sys; sys.exit(0 if json.load(open(sys.argv[1]))[\"entwurfProvider\"].get(\"skillPlugins\")==[\"/p\"] else 1)' '$GLOBAL'"
|
|
107
|
+
want "user/remove: state removed" "[ ! -f '$STATE' ]"
|
|
108
|
+
reg remove "$GLOBAL" "$FAKE_REPO" --scope user --state "$STATE" >/dev/null 2>&1
|
|
109
|
+
ok "user/remove: no-state is idempotent (nothing to undo)"
|
|
110
|
+
|
|
111
|
+
# ── F: legacy bundle prune alongside, user-authored server kept ───────────────
|
|
112
|
+
python3 -c 'import json,sys; json.dump({"entwurfProvider":{"mcpServers":{"entwurf-bridge":{"command":sys.argv[2]},"session-bridge":{"command":sys.argv[3]},"my-own":{"command":"/custom"}}}}, open(sys.argv[1],"w"))' "$GLOBAL" "$LEGACY_CMD" "$FAKE_REPO/mcp/session-bridge/start.sh"
|
|
113
|
+
reg install "$GLOBAL" "$FAKE_REPO" --scope user --state "$STATE" >/dev/null
|
|
114
|
+
want "legacy-prune: session-bridge removed" \
|
|
115
|
+
"python3 -c 'import json,sys; sys.exit(0 if \"session-bridge\" not in json.load(open(sys.argv[1]))[\"entwurfProvider\"][\"mcpServers\"] else 1)' '$GLOBAL'"
|
|
116
|
+
want "legacy-prune: user-authored my-own kept" \
|
|
117
|
+
"python3 -c 'import json,sys; sys.exit(0 if \"my-own\" in json.load(open(sys.argv[1]))[\"entwurfProvider\"][\"mcpServers\"] else 1)' '$GLOBAL'"
|
|
118
|
+
reg remove "$GLOBAL" "$FAKE_REPO" --scope user --state "$STATE" >/dev/null; rm -f "$STATE"
|
|
119
|
+
|
|
120
|
+
# ── G: project scope — NO state; install normalizes, remove strips managed shapes ──
|
|
121
|
+
python3 -c 'import json,sys; json.dump({"entwurfProvider":{"mcpServers":{"entwurf-bridge":{"command":sys.argv[2]}}}}, open(sys.argv[1],"w"))' "$PROJECT" "$LEGACY_CMD"
|
|
122
|
+
reg install "$PROJECT" "$FAKE_REPO" --scope project >/dev/null
|
|
123
|
+
want "project/install: normalized to bare bin" "[ \"\$(cmd_of '$PROJECT')\" = entwurf-bridge ]"
|
|
124
|
+
want "project/install: NO user-scope state created" "[ ! -f '$STATE' ]"
|
|
125
|
+
reg remove "$PROJECT" "$FAKE_REPO" --scope project >/dev/null
|
|
126
|
+
want "project/remove: strips the bare bin (our-managed)" "[ \"\$(cmd_of '$PROJECT')\" = '<none>' ]"
|
|
127
|
+
|
|
128
|
+
# ── H: doctor — effective shadow view + verdicts ──────────────────────────────
|
|
129
|
+
# H-1: user-only bare → effective user, ok green
|
|
130
|
+
printf '{"entwurfProvider":{"mcpServers":{"entwurf-bridge":{"command":"entwurf-bridge"}}}}\n' > "$GLOBAL"
|
|
131
|
+
printf '{}\n' > "$PROJECT"; rm -f "$STATE"
|
|
132
|
+
OUT="$(doctor)"; RC=$?
|
|
133
|
+
want "doctor(user-only bare): exits 0" "[ '$RC' -eq 0 ]"
|
|
134
|
+
want "doctor(user-only bare): ok — bare bin resolves" "printf '%s' \"\$OUT\" | grep -q 'bare stable bin'"
|
|
135
|
+
|
|
136
|
+
# H-2: project STALE (user bare, project legacy) → effective=project=legacy note (green-runtime-red guard)
|
|
137
|
+
printf '{"entwurfProvider":{"mcpServers":{"entwurf-bridge":{"command":"entwurf-bridge"}}}}\n' > "$GLOBAL"
|
|
138
|
+
python3 -c 'import json,sys; json.dump({"entwurfProvider":{"mcpServers":{"entwurf-bridge":{"command":sys.argv[2]}}}}, open(sys.argv[1],"w"))' "$PROJECT" "$LEGACY_CMD"
|
|
139
|
+
OUT="$(doctor)"; RC=$?
|
|
140
|
+
want "doctor(project-stale): exits 0 (honest note, not a crash)" "[ '$RC' -eq 0 ]"
|
|
141
|
+
want "doctor(project-stale): EFFECTIVE is project (shadows user)" "printf '%s' \"\$OUT\" | grep -q 'EFFECTIVE (project)'"
|
|
142
|
+
want "doctor(project-stale): flags legacy managed path (not adopted)" "printf '%s' \"\$OUT\" | grep -q 'LEGACY managed repo path'"
|
|
143
|
+
|
|
144
|
+
# H-3: state-owned DRIFT → FAIL (state says managed but effective is foreign)
|
|
145
|
+
printf '{"entwurfProvider":{"mcpServers":{"entwurf-bridge":{"command":"/foreign/thing"}}}}\n' > "$GLOBAL"
|
|
146
|
+
printf '{}\n' > "$PROJECT"
|
|
147
|
+
python3 -c 'import json,sys; json.dump({"schemaVersion":1,"managedSettingsPath":sys.argv[1],"scope":"user","ownership":"managed-legacy","command":"entwurf-bridge","preimage":None}, open(sys.argv[2],"w"))' "$GLOBAL" "$STATE"
|
|
148
|
+
if doctor >/dev/null 2>&1; then die "doctor(drift): should FAIL (state owns but effective drifted)"; fi
|
|
149
|
+
ok "doctor(drift): FAILS on state-owned + effective-drifted"
|
|
150
|
+
rm -f "$STATE"
|
|
151
|
+
|
|
152
|
+
# H-4: bare-but-dangling (bin not on PATH) → FAIL
|
|
153
|
+
printf '{"entwurfProvider":{"mcpServers":{"entwurf-bridge":{"command":"entwurf-bridge"}}}}\n' > "$GLOBAL"
|
|
154
|
+
printf '{}\n' > "$PROJECT"
|
|
155
|
+
mv "$SB/bin/entwurf-bridge" "$SB/bin/entwurf-bridge.hidden"
|
|
156
|
+
if doctor >/dev/null 2>&1; then die "doctor(dangling): should FAIL (bare bin not resolvable)"; fi
|
|
157
|
+
ok "doctor(dangling): FAILS when the bare bin does not resolve"
|
|
158
|
+
mv "$SB/bin/entwurf-bridge.hidden" "$SB/bin/entwurf-bridge"
|
|
159
|
+
|
|
160
|
+
# H-5: malformed settings → FAIL (SSOT read throws)
|
|
161
|
+
printf 'not json{{{' > "$GLOBAL"; printf '{}\n' > "$PROJECT"
|
|
162
|
+
if doctor >/dev/null 2>&1; then die "doctor(malformed): should FAIL"; fi
|
|
163
|
+
ok "doctor(malformed): FAILS on malformed settings (SSOT read throws)"
|
|
164
|
+
|
|
165
|
+
# H-6: none configured → honest '?' note, exits 0
|
|
166
|
+
printf '{}\n' > "$GLOBAL"; printf '{}\n' > "$PROJECT"
|
|
167
|
+
OUT="$(doctor)"; RC=$?
|
|
168
|
+
want "doctor(none): exits 0" "[ '$RC' -eq 0 ]"
|
|
169
|
+
want "doctor(none): honest never-installed '?' note" "printf '%s' \"\$OUT\" | grep -q 'never installed'"
|
|
170
|
+
|
|
171
|
+
# ── I: symlink target → install REFUSES ───────────────────────────────────────
|
|
172
|
+
printf '{}\n' > "$SB/real.json"; rm -f "$GLOBAL"; ln -s "$SB/real.json" "$GLOBAL"
|
|
173
|
+
if reg install "$GLOBAL" "$FAKE_REPO" --scope user --state "$STATE" >/dev/null 2>&1; then die "symlink: install should REFUSE"; fi
|
|
174
|
+
ok "symlink: install refused (someone else's SSOT)"
|
|
175
|
+
rm -f "$GLOBAL"
|
|
176
|
+
|
|
177
|
+
# ── checkout purity ───────────────────────────────────────────────────────────
|
|
178
|
+
REPO_AFTER="$(cd "$REPO_DIR" && git status --porcelain)"
|
|
179
|
+
want "purity: checkout unchanged (all writes stayed in the sandbox HOME+XDG)" \
|
|
180
|
+
"[ \"\$REPO_BEFORE\" = \"\$REPO_AFTER\" ]"
|
|
181
|
+
|
|
182
|
+
printf '\nsmoke-pi-provider-state: %d checks passed\n' "$pass"
|