Files
thanhnv 06c404084d
CASAN Supply Chain and Provenance / verify-scan-attest (push) Canceled after 0s
CASAN CI Gate / Frontend Tests (H3 gate) (push) Canceled after 0s
CASAN CI Gate / CASAN Security Gate + Vault KMS (H4/H5) (push) Canceled after 0s
CASAN CI Gate / Build & Deploy OKR → /opt/webapps/okr (push) Canceled after 0s
CASAN Harness CI / harness (push) Canceled after 0s
feat(casan): add native plugin facade
2026-08-18 16:26:27 +07:00

617 lines
35 KiB
Bash
Executable File

#!/usr/bin/env bash
set -uo pipefail
# CASAN Plan-21 — hybrid install + `casan init` acceptance tests.
#
# Proves the codegraph-style flow: global install once, then per-project
# `casan init` that writes CONFIG ONLY (no harness copy), with a pin+verify
# integrity guarantee on the shared harness. Deterministic, offline.
SCRIPT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)"
REPO_ROOT="$(cd "$SCRIPT_DIR/../../.." && pwd)"
PASS=0; FAIL=0
pass() { echo "PASS: $1"; PASS=$((PASS + 1)); }
fail() { echo "FAIL: $1"; FAIL=$((FAIL + 1)); }
WORK="$(mktemp -d)"
export CASAN_HOME="$WORK/home/.casan"
export CASAN_SRC="$REPO_ROOT"
export CASAN_NO_PATH_LINK=1
cleanup() { rm -rf "$WORK"; }
trap cleanup EXIT
CASAN="$CASAN_HOME/bin/casan"
echo "===== ① global install ====="
python3 "$REPO_ROOT/packages/casan-devkit/tests/casan-init-tests.py" >/dev/null \
&& pass "casan init/doctor unit contracts pass" \
|| fail "casan init/doctor unit contracts failed"
if sh "$REPO_ROOT/install.sh" >/dev/null 2>&1; then pass "install.sh completes"; else fail "install.sh failed"; fi
[[ -x "$CASAN" ]] && pass "global launcher created" || fail "launcher missing"
[[ -f "$CASAN_HOME/current/.harness-hash" ]] && pass "integrity hash recorded at install" || fail "no .harness-hash"
python3 - "$CASAN_HOME/current" <<'PY' \
&& pass "global install has the exact production directory layout" \
|| fail "global install contains missing or source-only paths"
from pathlib import Path
import json
import sys
root = Path(sys.argv[1])
assert {path.name for path in root.iterdir()} == {
".casan-level", ".harness-hash", "VERSION", "bin", "packages", "packaging",
}
assert {path.name for path in (root / "packages").iterdir()} == {
"casan-harness", "casan-devkit",
}
assert {path.name for path in (root / "packages/casan-harness").iterdir()} == {
"adapters", "agentops", "assets", "config", "governance", "memory", "prompts",
"schemas", "scripts", "security", "templates",
}
assert {path.name for path in (root / "packages/casan-devkit").iterdir()} == {
"casan-init.py", "package-vscode-extension.py", "project-scaffold.py",
"quality-profiles", "schemas", "templates",
}
assert {path.name for path in (root / "packaging").iterdir()} == {
"runtime-layout.json",
}
for forbidden in (
"packages/casan-harness/tests",
"packages/casan-harness/level5",
"packages/casan-harness/scripts/bash/ci-harness-gate.sh",
"packages/casan-harness/scripts/bash/security-gate.sh",
"packages/casan-harness/scripts/bash/test-integrity.py",
"packages/casan-harness/scripts/bash/dashboard-server.py",
"packages/casan-devkit/tests",
"packages/casan-devkit/install.sh",
"install.sh",
"install.ps1",
"docs",
):
assert not (root / forbidden).exists(), forbidden
for required in (
".codex-plugin/plugin.json",
".claude-plugin/plugin.json",
".claude-plugin/marketplace.json",
"skills/casan/SKILL.md",
"packages/casan-harness/assets/local-viewer/index.html",
"packages/casan-harness/assets/local-viewer/app.css",
"packages/casan-harness/assets/local-viewer/app.js",
"packages/casan-harness/config/project-registry.json",
"packages/casan-harness/config/tool-registry.yaml",
"packages/casan-harness/scripts/bash/project-gate.sh",
"packages/casan-harness/scripts/python/agentic_bridge.py",
"packages/casan-harness/scripts/python/local_report.py",
"packages/casan-harness/scripts/python/local_viewer.py",
"packages/casan-harness/scripts/python/report_cli.py",
"packages/casan-harness/scripts/python/readiness.py",
):
assert (root / required).is_file(), required
registry = json.loads(
(root / "packages/casan-harness/config/project-registry.json").read_text(
encoding="utf-8"))
assert registry["projects"] == []
PY
"$CASAN" version >/dev/null 2>&1 && pass "casan version works via launcher" || fail "casan version failed"
python3 - "$CASAN_HOME/current/packages/casan-devkit/casan-init.py" <<'PY' \
&& pass "init wizard resolves runtime and client choices safely" \
|| fail "init wizard selection failed"
import contextlib
import importlib.util
import io
import sys
spec=importlib.util.spec_from_file_location("casan_init",sys.argv[1])
m=importlib.util.module_from_spec(spec); spec.loader.exec_module(m)
# Explicit flags and existing project configuration never open a prompt.
assert m.select_runtime_mode("vendored",None,True)=="vendored"
assert m.select_runtime_mode(None,"vendored",True)=="vendored"
assert m.select_runtime_mode(None,None,False)=="managed"
# Interactive defaults, aliases, validation, and retry behavior.
old_stdin=m.sys.stdin
try:
m.sys.stdin=io.StringIO("\n")
with contextlib.redirect_stderr(io.StringIO()):
assert m.select_runtime_mode(None,None,True)=="managed"
m.sys.stdin=io.StringIO("9\n2\n")
with contextlib.redirect_stderr(io.StringIO()) as errors:
assert m.select_runtime_mode(None,None,True)=="vendored"
assert "Invalid selection" in errors.getvalue()
m.sys.stdin=io.StringIO("bogus\n3\n")
with contextlib.redirect_stderr(io.StringIO()) as errors:
assert m.select_clients(None,True)==["vscode-copilot"]
assert "Invalid selection" in errors.getvalue()
finally:
m.sys.stdin=old_stdin
assert m.select_clients(["1,3"],False)==["claude","vscode-copilot"]
assert m.select_clients(["codex","copilot"],False)==["codex","vscode-copilot"]
assert m.select_clients(["all"],False)==["claude","codex","vscode-copilot"]
PY
RELEASE_DIST="$WORK/release-dist"
CASAN_DIST_DIR="$RELEASE_DIST" bash "$REPO_ROOT/scripts/package-release.sh" core >/dev/null
python3 - "$RELEASE_DIST/casan-core-v$(cat "$REPO_ROOT/VERSION").tar.gz" <<'PY' \
&& pass "core release artifact contains production runtime only" \
|| fail "core release artifact leaked source-only content"
import sys
import tarfile
with tarfile.open(sys.argv[1], "r:gz") as archive:
names = [name.split("/", 1)[1] for name in archive.getnames() if "/" in name]
for forbidden in (
"packages/casan-harness/tests",
"packages/casan-harness/level5",
"packages/casan-harness/scripts/bash/ci-harness-gate.sh",
"packages/casan-harness/scripts/bash/test-integrity.py",
):
assert not any(name == forbidden or name.startswith(forbidden + "/") for name in names), forbidden
for required in (
"packages/casan-harness/assets/local-viewer/index.html",
"packages/casan-harness/config/tool-registry.yaml",
"packages/casan-harness/scripts/bash/casan-harness.sh",
"packages/casan-harness/scripts/python/local_viewer.py",
"packages/casan-harness/scripts/python/readiness.py",
"packaging/runtime-layout.json",
"scripts/copy-runtime.py",
"install.sh",
):
assert required in names, required
PY
echo "===== ② casan init (config only, no harness copy) ====="
PROJ="$WORK/proj/my-app"; mkdir -p "$PROJ"; echo '{"name":"x"}' > "$PROJ/package.json"
( cd "$PROJ" && "$CASAN" init --project my-app --mode enforce >/dev/null 2>&1 ) \
&& pass "casan init completes" || fail "casan init failed"
for f in .casan/config.json .casan/version.lock .casan/agentic.env .casan/casan-hook.py .casan/discovery.json .casan/readiness.json .claude/settings.json .codex/hooks.json .vscode/extensions.json .specify/.gitignore; do
[[ -f "$PROJ/$f" ]] && pass "init wrote $f" || fail "init missing $f"
done
python3 - "$PROJ/.casan/readiness.json" <<'PY' \
&& pass "Core readiness is independent from optional Domain/Provider states" \
|| fail "commercial readiness contract is invalid"
import json
import sys
d = json.load(open(sys.argv[1], encoding="utf-8"))
assert d["dimensions"]["core"]["status"] in {"ready", "ready_with_attention"}
assert d["dimensions"]["domain_pipeline"]["status"] == "not_configured"
assert d["dimensions"]["provider_telemetry"]["status"] == "optional_unavailable"
PY
CASAN_APP_ROOT="$PROJ" "$CASAN" pipeline --help 2>&1 \
| grep -q "optional Domain Pack" \
&& pass "pipeline help describes the optional capability without executing it" \
|| fail "pipeline help executed or hid the Core/Domain boundary"
set +e
PIPELINE_MISSING="$(CASAN_APP_ROOT="$PROJ" "$CASAN" pipeline 2>&1)"
PIPELINE_RC=$?
set -e
[[ "$PIPELINE_RC" -eq 2 && "$PIPELINE_MISSING" == *"CASAN_PIPELINE_NOT_CONFIGURED"* ]] \
&& pass "missing Domain Pipeline is explicit and does not impersonate a Core failure" \
|| fail "missing Domain Pipeline capability was not reported truthfully"
python3 - "$PROJ/.casan/config.json" <<'PY' \
&& pass "config declares supported and excluded client surfaces" \
|| fail "config client surface contract is incomplete"
import json
import sys
d = json.load(open(sys.argv[1], encoding="utf-8"))
assert d["schema_version"] == "21.3"
assert d["client_surfaces"]["claude"]["supported_local"] == [
"claude-code-cli", "claude-code-vscode", "claude-code-jetbrains",
]
assert d["client_surfaces"]["codex"]["supported_local"] == [
"codex-desktop-local", "codex-cli", "codex-ide-extension-local",
]
assert "claude-desktop" in d["client_surfaces"]["claude"]["not_covered"]
assert "codex-cloud" in d["client_surfaces"]["codex"]["not_covered"]
PY
if [[ -d "$PROJ/packages/casan-harness" ]]; then fail "harness was copied into the repo (should not be)"; else pass "harness NOT copied into repo (hybrid model)"; fi
if grep -R -q 'packages/casan-harness/adapters' "$PROJ/.claude/settings.json" "$PROJ/.codex/hooks.json"; then fail "generated hooks still target a repo-local harness"; else pass "generated hooks target the stable project bootstrap"; fi
echo "===== ②b generated hooks execute through the GLOBAL harness ====="
CLAUDE_BEGIN=$(printf '%s' '{"session_id":"hybrid-claude","prompt_id":"t1","prompt":"add a safe helper","cwd":"'"$PROJ"'"}' |
CASAN_APP_ROOT="$PROJ" python3 "$PROJ/.casan/casan-hook.py" --client claude --event UserPromptSubmit)
echo "$CLAUDE_BEGIN" | grep -q '"additionalContext"' && pass "Claude generated hook opens admission" || fail "Claude generated hook failed ($CLAUDE_BEGIN)"
CLAUDE_PRE=$(printf '%s' '{"session_id":"hybrid-claude","tool_name":"Bash","tool_input":{"command":"printf ok"},"cwd":"'"$PROJ"'"}' |
CASAN_APP_ROOT="$PROJ" python3 "$PROJ/.casan/casan-hook.py" --client claude --event PreToolUse)
echo "$CLAUDE_PRE" | grep -q '"permissionDecision": "allow"' && pass "Claude generated hook gates an admitted tool" || fail "Claude pre-tool failed ($CLAUDE_PRE)"
CODEX_BEGIN=$(printf '%s' '{"session_id":"hybrid-codex","turn_id":"t2","prompt":"review this project","cwd":"'"$PROJ"'"}' |
CASAN_APP_ROOT="$PROJ" python3 "$PROJ/.casan/casan-hook.py" --client codex --event UserPromptSubmit)
echo "$CODEX_BEGIN" | grep -q '"continue": true' && pass "Codex generated hook opens admission" || fail "Codex generated hook failed ($CODEX_BEGIN)"
python3 - "$PROJ/.codex/hooks.json" <<'PY' && pass "Codex hooks use the current schema and project-relative bootstrap" || fail "Codex hook contract is stale"
import json,sys
d=json.load(open(sys.argv[1]))
assert set(d) == {"hooks"}
handlers=[h for groups in d["hooks"].values() for group in groups for h in group["hooks"]]
assert handlers and all(h.get("type") == "command" for h in handlers)
assert all(isinstance(h.get("command"), str) for h in handlers)
assert all(isinstance(h.get("timeout"), int) and "timeout_ms" not in h for h in handlers)
assert all('python3 ".casan/casan-hook.py"' in h["command"] for h in handlers)
assert all("git rev-parse" not in h["command"] for h in handlers)
PY
echo "===== ③ version.lock pins the installed harness ====="
LOCK_HASH="$(python3 -c 'import json;print(json.load(open("'"$PROJ"'/.casan/version.lock"))["harness_hash"])')"
REC_HASH="$(cat "$CASAN_HOME/current/.harness-hash")"
[[ -n "$LOCK_HASH" && "$LOCK_HASH" == "$REC_HASH" ]] && pass "version.lock pins the installed gate-code hash" || fail "lock hash mismatch ($LOCK_HASH vs $REC_HASH)"
echo "===== ④ verify-harness: ok before tamper, drift after ====="
( cd "$PROJ" && "$CASAN" verify-harness >/dev/null 2>&1 ) && pass "verify-harness OK on a clean install" || fail "verify-harness reported drift on clean install"
cp "$CASAN_HOME/current/packages/casan-harness/scripts/bash/security-check.sh" "$WORK/security-check.clean"
echo "# tampered $(date)" >> "$CASAN_HOME/current/packages/casan-harness/scripts/bash/security-check.sh"
VRC=0; ( cd "$PROJ" && "$CASAN" verify-harness >/dev/null 2>"$WORK/vh.err" ) || VRC=$?
[[ "$VRC" -eq 3 ]] && pass "verify-harness detects tamper (rc=3)" || fail "tamper not detected (rc=$VRC)"
grep -q "HARNESS_INTEGRITY_DRIFT" "$WORK/vh.err" && pass "drift message emitted" || fail "no drift message"
cp "$WORK/security-check.clean" "$CASAN_HOME/current/packages/casan-harness/scripts/bash/security-check.sh"
cp "$CASAN_HOME/current/packages/casan-harness/adapters/codex/codex_hook.py" "$WORK/codex-hook.clean"
echo "# adapter tamper" >> "$CASAN_HOME/current/packages/casan-harness/adapters/codex/codex_hook.py"
ARC=0; ( cd "$PROJ" && "$CASAN" verify-harness >/dev/null 2>&1 ) || ARC=$?
[[ "$ARC" -eq 3 ]] && pass "integrity pin includes client adapters" || fail "adapter tamper was not detected"
cp "$WORK/codex-hook.clean" "$CASAN_HOME/current/packages/casan-harness/adapters/codex/codex_hook.py"
echo "===== ⑤ agentic bridge runs against the PROJECT state via GLOBAL harness ====="
BR="$CASAN_HOME/current/packages/casan-harness/scripts/python/agentic_bridge.py"
B=$(CASAN_APP_ROOT="$PROJ" CASAN_AGENTIC_ENFORCEMENT_MODE=enforce \
bash -c 'echo '\''{"op":"begin","client":"claude-code","project":"'"$PROJ"'","session":"s","prompt":"add fn","integration_mode":"project_hook"}'\'' | python3 "'"$BR"'" run')
echo "$B" | grep -q '"decision": "allow"' && pass "bridge admits a normal turn via the global harness" || fail "bridge begin failed ($B)"
if find "$PROJ/.specify" -name 'turn-*.json' | grep -q .; then pass "admission state lands in the PROJECT .specify"; else fail "no admission state in project"; fi
if [[ -d "$CASAN_HOME/current/.specify" ]]; then fail "runtime state leaked into the global install"; else pass "no runtime state in the global install"; fi
echo "===== ⑥ init defaults project id from dir name + is re-runnable ====="
PROJ2="$WORK/proj2/Some_App"; mkdir -p "$PROJ2"
( cd "$PROJ2" && "$CASAN" init >/dev/null 2>&1 ) && pass "init works with a defaulted project id" || fail "init default id failed"
PID=$(python3 -c 'import json;print(json.load(open("'"$PROJ2"'/.casan/config.json"))["project_id"])' 2>/dev/null)
[[ "$PID" =~ ^[a-z][a-z0-9-]{1,62}$ ]] && pass "defaulted project id is sanitized ($PID)" || fail "bad default project id ($PID)"
MODE=$(python3 -c 'import json;print(json.load(open("'"$PROJ2"'/.casan/config.json"))["enforcement_mode"])' 2>/dev/null)
[[ "$MODE" == "enforce" ]] && pass "production init defaults to enforce mode" || fail "default mode is not enforce ($MODE)"
DEFAULT_LEVEL=$(python3 -c 'import json;print(json.load(open("'"$PROJ2"'/.casan/config.json"))["target_level"])' 2>/dev/null)
DEFAULT_RUNTIME=$(python3 -c 'import json;print(json.load(open("'"$PROJ2"'/.casan/config.json"))["runtime_mode"])' 2>/dev/null)
[[ "$DEFAULT_LEVEL" == "1" ]] \
&& [[ "$DEFAULT_RUNTIME" == "managed" ]] \
&& [ ! -d "$PROJ2/.casan/runtime" ] \
&& [ ! -d "$PROJ2/.gitea" ] \
&& [ ! -d "$PROJ2/apps" ] \
&& pass "existing-project init defaults to managed Core without vendoring" \
|| fail "default init mode is unclear (level=$DEFAULT_LEVEL runtime=$DEFAULT_RUNTIME)"
echo "===== ⑦ init MERGES into an existing shell (agents/skills/hooks preserved) ====="
EXP="$WORK/existing"; mkdir -p "$EXP/.claude/agents" "$EXP/.claude/skills" "$EXP/.codex"
cat > "$EXP/.claude/settings.json" <<'EOF'
{ "model": "claude-opus-4-8", "hooks": { "PreToolUse": [ {"matcher":"*","hooks":[{"type":"command","command":"my-existing-hook.sh"}]} ] } }
EOF
cat > "$EXP/.codex/hooks.json" <<'EOF'
{
"description": "CASAN Plan-20 lifecycle hooks. Review with /hooks; the project bootstrap resolves and verifies the pinned global harness.",
"hooks": {
"PreToolUse": [
{"matcher":"*","hooks":[{"type":"command","command":"my-existing-codex-hook.sh"}]}
]
}
}
EOF
echo "agent" > "$EXP/.claude/agents/reviewer.md"; echo "skill" > "$EXP/.claude/skills/deploy.md"
printf '[hooks]\nenabled = true\n\n[mytool]\nfoo = 1\n' > "$EXP/.codex/config.toml"
( cd "$EXP" && "$CASAN" init --project existing-app >/dev/null 2>&1 )
py_check() { python3 - "$1" "$2" <<'PY'
import json,sys
d=json.load(open(sys.argv[1]))
cmds=[h["command"] for ev in d.get("hooks",{}).values() for g in ev for h in g["hooks"]]
sys.exit(0 if any(sys.argv[2] in c for c in cmds) else 1)
PY
}
[ -f "$EXP/.claude/agents/reviewer.md" ] && [ -f "$EXP/.claude/skills/deploy.md" ] && pass "existing agents/skills preserved" || fail "agents/skills lost"
py_check "$EXP/.claude/settings.json" "my-existing-hook" && pass "existing Claude hook preserved (not clobbered)" || fail "existing hook clobbered"
py_check "$EXP/.claude/settings.json" "casan-hook.py" && pass "CASAN Claude hook merged in" || fail "CASAN hook not merged"
[ "$(python3 -c 'import json;print(json.load(open("'"$EXP"'/.claude/settings.json")).get("model"))')" = "claude-opus-4-8" ] && pass "unrelated settings key (model) preserved" || fail "model key lost"
py_check "$EXP/.codex/hooks.json" "my-existing-codex-hook" && pass "existing Codex hook preserved (not clobbered)" || fail "existing Codex hook clobbered"
python3 - "$EXP/.codex/hooks.json" <<'PY' \
&& pass "obsolete CASAN description migrated for Codex 0.142.5 compatibility" \
|| fail "obsolete Codex description was retained"
import json
import sys
d = json.load(open(sys.argv[1], encoding="utf-8"))
assert "description" not in d
assert set(d) == {"hooks"}
PY
grep -q '\[mytool\]' "$EXP/.codex/config.toml" && pass "existing codex [mytool] preserved" || fail "mytool lost"
[ "$(grep -c '^\[hooks\]' "$EXP/.codex/config.toml")" = "1" ] && pass "codex config.toml has no duplicate [hooks] table" || fail "duplicate [hooks]"
# idempotent
( cd "$EXP" && "$CASAN" init --project existing-app >/dev/null 2>&1 )
N=$(python3 -c 'import json;d=json.load(open("'"$EXP"'/.claude/settings.json"));print(sum(1 for g in d["hooks"]["PreToolUse"] for h in g["hooks"] if "casan-hook.py" in h["command"]))')
[ "$N" = "1" ] && pass "re-running init is idempotent (no duplicate CASAN hook)" || fail "init duplicated CASAN hook (n=$N)"
( cd "$EXP" && "$CASAN" init --project existing-app --client claude >/dev/null 2>&1 )
if py_check "$EXP/.codex/hooks.json" "casan-hook.py"; then fail "re-selecting Claude left the CASAN Codex hook enabled"; else pass "re-running init synchronizes disabled clients"; fi
py_check "$EXP/.claude/settings.json" "my-existing-hook" && pass "client re-selection still preserves user hooks" || fail "client re-selection removed user hooks"
echo "===== ⑧ level-aware install + init (packaging/levels.json) ====="
# core install: no devkit, casan init unavailable
CORE_HOME="$WORK/home-core/.casan"
CASAN_HOME="$CORE_HOME" sh "$REPO_ROOT/install.sh" --level core >/dev/null 2>&1
[ -d "$CORE_HOME/current/packages/casan-devkit" ] && fail "core install pulled in devkit" || pass "core level excludes devkit"
[ "$(cat "$CORE_HOME/current/.casan-level" 2>/dev/null)" = "core" ] && pass "install records level=core" || fail "no .casan-level=core"
# Always run init inside a THROWAWAY dir — never the ambient cwd.
CINIT="$WORK/core-init-proj"; mkdir -p "$CINIT"
CI_OUT=$( ( cd "$CINIT" && "$CORE_HOME/bin/casan" init 2>&1 | head -1 ) || true )
echo "$CI_OUT" | grep -q "requires the casan-devkit" && pass "casan init unavailable on core (clear message)" || fail "core init did not fail clearly ($CI_OUT)"
# enterprise refused, platform refused-with-guidance
ERC=0; CASAN_HOME="$WORK/home-e/.casan" sh "$REPO_ROOT/install.sh" --level enterprise >/dev/null 2>&1 || ERC=$?
[ "$ERC" -eq 3 ] && pass "install --level enterprise refused (rc=3, no fake-complete)" || fail "enterprise not refused (rc=$ERC)"
PRC=0; CASAN_HOME="$WORK/home-p/.casan" sh "$REPO_ROOT/install.sh" --level platform >/dev/null 2>&1 || PRC=$?
[ "$PRC" -ne 0 ] && pass "install --level platform refused (preview service, rc=$PRC)" || fail "platform install pretended to succeed"
# devkit install: init --level differentiation
DK_HOME="$WORK/home-dk/.casan"
CASAN_HOME="$DK_HOME" sh "$REPO_ROOT/install.sh" --level devkit >/dev/null 2>&1
DKC="$DK_HOME/bin/casan"
L1="$WORK/l1"; mkdir -p "$L1"; ( cd "$L1" && "$DKC" init --level 1 --project l1 >/dev/null 2>&1 )
( [ ! -d "$L1/.gitea" ] && [ ! -d "$L1/apps" ] ) && pass "init --level 1 = gate/hooks only (no CI/domain)" || fail "L1 wrote devkit extras"
[ -f "$L1/.claude/settings.json" ] && pass "init --level 1 still writes Plan-20 hooks (core feature)" || fail "L1 missing hooks"
L2="$WORK/l2"; mkdir -p "$L2"; ( cd "$L2" && "$DKC" init --level 2 --project l2 >/dev/null 2>&1 )
( [ -f "$L2/.gitea/workflows/casan-ci.yml" ] && [ -d "$L2/apps/l2/domain" ] ) && pass "init --level 2 adds CI + domain-pack" || fail "L2 missing devkit extras"
L4RC=0; L4="$WORK/l4"; mkdir -p "$L4"; ( cd "$L4" && "$DKC" init --level 4 --project l4 >/dev/null 2>&1 ) || L4RC=$?
[ "$L4RC" -eq 3 ] && pass "init --level 4 (enterprise) refused (rc=3)" || fail "L4 init not refused (rc=$L4RC)"
LVL=$( ( cd "$L2" && "$DKC" level show --json ) | python3 -c 'import json,sys;print(json.load(sys.stdin)["project_target_level"])' 2>/dev/null)
[ "$LVL" = "2" ] && pass "casan level show reports project target level" || fail "level show wrong ($LVL)"
( cd "$L2" && "$DKC" init --project l2 --client none --non-interactive >/dev/null )
LVL_DOWN=$( ( cd "$L2" && "$DKC" level show --json ) | python3 -c 'import json,sys;print(json.load(sys.stdin)["project_target_level"])' 2>/dev/null)
[ "$LVL_DOWN" = "1" ] \
&& [ ! -d "$L2/.gitea" ] \
&& [ ! -d "$L2/apps" ] \
&& pass "default init downgrades existing DevKit adoption to clean core" \
|| fail "default init left Level 2 artifacts after core downgrade"
VENDORED="$WORK/vendored-core"; mkdir -p "$VENDORED"
VENDORED_OUT=$(cd "$VENDORED" && "$DKC" init --runtime vendored --project vendored-core --client claude --non-interactive)
VENDORED_ROOT="$VENDORED/.casan/runtime/casan-core"
echo "$VENDORED_OUT" | grep -q "Runtime.*Vendored" \
&& pass "vendored init clearly reports runtime placement" \
|| fail "vendored init output does not explain runtime mode ($VENDORED_OUT)"
[ -x "$VENDORED_ROOT/bin/casan" ] \
&& [ -f "$VENDORED_ROOT/packages/casan-harness/scripts/bash/casan-harness.sh" ] \
&& [ ! -d "$VENDORED_ROOT/packages/casan-harness/tests" ] \
&& [ ! -d "$VENDORED_ROOT/packages/casan-harness/level5" ] \
&& pass "vendored mode installs only production Core inside the project" \
|| fail "vendored Core layout is incomplete or contains source-only files"
( cd "$VENDORED" && "$VENDORED_ROOT/bin/casan" version >/dev/null ) \
&& pass "project-vendored Core includes a working local CLI" \
|| fail "project-vendored CLI cannot resolve its Core runtime"
printf 'vendored consumer smoke\n' > "$VENDORED/input.txt"
( cd "$VENDORED" && "$VENDORED_ROOT/bin/casan" run input.txt output.txt agent_step -- printf 'ok\n' >/dev/null ) \
&& [ -s "$VENDORED/.specify/logs/cost/metrics.jsonl" ] \
&& [ ! -d "$VENDORED_ROOT/.specify" ] \
&& pass "vendored CLI exports the consumer project root to harness commands" \
|| fail "vendored CLI wrote governed state inside the runtime bundle"
( cd "$VENDORED" && "$DKC" verify-harness >/dev/null ) \
&& pass "global launcher resolves and verifies the project-vendored Core" \
|| fail "vendored Core is not honored by the global launcher"
( cd "$VENDORED" && "$DKC" doctor --client claude >/dev/null ) \
&& pass "project hooks execute against the verified vendored Core" \
|| fail "project hook cannot execute against vendored Core"
mv "$VENDORED_ROOT" "$VENDORED_ROOT.missing"
VENDORED_MISSING_RC=0
( cd "$VENDORED" && "$DKC" version >/dev/null 2>&1 ) || VENDORED_MISSING_RC=$?
mv "$VENDORED_ROOT.missing" "$VENDORED_ROOT"
[ "$VENDORED_MISSING_RC" -ne 0 ] \
&& pass "vendored contract fails closed instead of falling back to global Core" \
|| fail "missing vendored Core silently fell back to the global runtime"
( cd "$VENDORED" && "$DKC" init --project vendored-core --client none --non-interactive >/dev/null )
[ -d "$VENDORED_ROOT/packages/casan-harness" ] \
&& pass "re-init preserves the project's selected vendored runtime mode" \
|| fail "re-init silently changed the project's runtime mode"
( cd "$VENDORED" && "$DKC" init --runtime managed --project vendored-core --client none --non-interactive >/dev/null )
[ ! -d "$VENDORED/.casan/runtime" ] \
&& pass "switching back to managed mode removes the old vendored runtime" \
|| fail "managed re-init left a stale vendored runtime"
VENDORED_ESCAPE="$WORK/vendored-escape"; VENDORED_OUTSIDE="$WORK/vendored-outside"
mkdir -p "$VENDORED_ESCAPE/.casan" "$VENDORED_OUTSIDE"
ln -s "$VENDORED_OUTSIDE" "$VENDORED_ESCAPE/.casan/runtime"
VENDORED_ESCAPE_RC=0
( cd "$VENDORED_ESCAPE" && "$DKC" init --runtime vendored --project vendored-escape --client none --non-interactive >/dev/null 2>&1 ) || VENDORED_ESCAPE_RC=$?
[ "$VENDORED_ESCAPE_RC" -ne 0 ] \
&& [ ! -e "$VENDORED_OUTSIDE/casan-core" ] \
&& pass "vendored install rejects a runtime path escaping through symlink" \
|| fail "vendored install followed an unsafe project symlink"
echo "===== ⑨ SAFETY: init refuses to adopt a CASAN source hub into itself ====="
HUB="$WORK/fakehub"
mkdir -p "$HUB/packages/casan-harness/scripts/bash" \
"$HUB/packages/casan-devkit" "$HUB/packaging"
echo '#casan' > "$HUB/packages/casan-harness/scripts/bash/casan-harness.sh"
echo '#devkit' > "$HUB/packages/casan-devkit/casan-init.py"
echo '{}' > "$HUB/packaging/levels.json"
echo '#!/bin/sh' > "$HUB/install.sh"
GRC=0; ( cd "$HUB" && "$DKC" init --project hub >/dev/null 2>"$WORK/hub.err" ) || GRC=$?
[ "$GRC" -eq 65 ] && pass "init refuses a source hub (rc=65)" || fail "source-hub not refused (rc=$GRC)"
[ ! -f "$HUB/.claude/settings.json" ] && pass "no hooks written into the refused hub" || fail "hooks written into source hub"
( cd "$HUB" && "$DKC" init --project hub --force >/dev/null 2>&1 ) && pass "--force overrides the source-hub guard" || fail "--force did not override guard"
echo "===== ⑨b MIGRATION: existing vendored project shell is preserved ====="
LEGACY="$WORK/legacy-vendored"
mkdir -p "$LEGACY/packages/casan-harness/scripts/bash" \
"$LEGACY/.claude/commands/bd" "$LEGACY/.github/skills/domain" \
"$LEGACY/.gitea/workflows" "$LEGACY/.casan"
echo '#legacy harness' > "$LEGACY/packages/casan-harness/scripts/bash/casan-harness.sh"
echo 'existing command' > "$LEGACY/.claude/commands/bd/generate.md"
echo 'existing skill' > "$LEGACY/.github/skills/domain/SKILL.md"
echo 'existing workflow' > "$LEGACY/.gitea/workflows/existing.yml"
cat > "$LEGACY/AGENTS.md" <<'EOF'
Project-owned instructions.
<!-- CASAN_PROMPT_ENFORCEMENT_START -->
## CASAN prompt enforcement — mandatory
- The supported prompt boundary is `bin/casan-chat`.
<!-- CASAN_PROMPT_ENFORCEMENT_END -->
EOF
cat > "$LEGACY/.casan/prompt-policy.json" <<'EOF'
{"schema_version":1,"mode":"enforced","project_id":"legacy-vendored","domain_root":"apps/legacy-vendored/domain","direct_external_ui":"not_interceptable_not_certified"}
EOF
chmod 0555 "$LEGACY/.claude"
( cd "$LEGACY" && "$DKC" init --project legacy-vendored --client claude,codex >/dev/null 2>&1 ) \
&& pass "vendored application migrates without unsafe --force" \
|| fail "vendored application was mistaken for a source hub"
[ -f "$LEGACY/.claude/commands/bd/generate.md" ] \
&& [ -f "$LEGACY/.github/skills/domain/SKILL.md" ] \
&& [ -f "$LEGACY/.gitea/workflows/existing.yml" ] \
&& pass "existing commands, GitHub skills, and CI are preserved" \
|| fail "existing project shell content was changed"
grep -q 'CASAN IDE governance' "$LEGACY/AGENTS.md" \
&& grep -q 'Project-owned instructions.' "$LEGACY/AGENTS.md" \
&& pass "only the CASAN-managed legacy instruction block is migrated" \
|| fail "managed prompt-boundary migration was not precise"
python3 - "$LEGACY/.casan/prompt-policy.json" <<'PY' \
&& pass "legacy prompt policy records hybrid client-dependent routing" \
|| fail "legacy prompt policy was not migrated"
import json,sys
d=json.load(open(sys.argv[1]))
assert d["direct_external_ui"] == "client_dependent_see_config"
assert d["agentic_clients"] == ["claude","codex"]
assert d["hybrid_adoption"] is True
PY
[ -f "$LEGACY/packages/casan-harness/scripts/bash/casan-harness.sh" ] \
&& pass "vendored harness is retained for compatibility, not deleted" \
|| fail "migration deleted the existing vendored harness"
python3 - "$LEGACY/.claude" <<'PY' \
&& pass "owner-controlled read-only config directory mode is restored" \
|| fail "migration did not restore the original directory permissions"
import os,stat,sys
assert stat.S_IMODE(os.stat(sys.argv[1]).st_mode) == 0o555
PY
# Restore fixture writability so the suite's temporary-directory cleanup works.
chmod 0755 "$LEGACY/.claude"
echo "===== ⑩ VS Code/Copilot @casan packaging + install + doctor ====="
FAKE_BIN="$WORK/fake-bin"; mkdir -p "$FAKE_BIN"
cat > "$FAKE_BIN/code" <<'EOF'
#!/usr/bin/env bash
if [[ "${1:-}" == "--list-extensions" ]]; then
printf '%s\n' 'fpt-casan.casan-governed-chat' 'GitHub.copilot' 'GitHub.copilot-chat'
exit 0
fi
printf '%s\n' "$*" >> "$CASAN_FAKE_CODE_LOG"
exit 0
EOF
chmod +x "$FAKE_BIN/code"
VSP="$WORK/vscode-project"; mkdir -p "$VSP"
export CASAN_FAKE_CODE_LOG="$WORK/code.log"
( cd "$VSP" && PATH="$FAKE_BIN:$PATH" "$DKC" init --project vscode-project --client vscode-copilot --mode enforce --vscode-install yes >/dev/null 2>&1 ) \
&& pass "init enables the selected VS Code/Copilot integration" || fail "VS Code/Copilot init failed"
grep -q -- '--install-extension .*casan-governed-chat.vsix --force' "$CASAN_FAKE_CODE_LOG" \
&& pass "init installs the packaged CASAN VSIX through code CLI" || fail "VSIX install was not invoked"
VSIX1="$WORK/one.vsix"; VSIX2="$WORK/two.vsix"
CASAN_HARNESS_ROOT="$DK_HOME/current/packages/casan-harness" python3 "$DK_HOME/current/packages/casan-devkit/package-vscode-extension.py" --output "$VSIX1" >/dev/null
CASAN_HARNESS_ROOT="$DK_HOME/current/packages/casan-harness" python3 "$DK_HOME/current/packages/casan-devkit/package-vscode-extension.py" --output "$VSIX2" >/dev/null
cmp -s "$VSIX1" "$VSIX2" && pass "VSIX packaging is deterministic" || fail "VSIX package bytes drift between builds"
python3 - "$VSIX1" <<'PY' && pass "VSIX contains the required production extension assets" || fail "VSIX structure is invalid"
import zipfile,sys
with zipfile.ZipFile(sys.argv[1]) as z:
names=set(z.namelist())
assert {"[Content_Types].xml","extension.vsixmanifest","extension/package.json","extension/extension.js"} <= names
PY
python3 - "$VSP/.vscode/extensions.json" <<'PY' && pass "VS Code recommendations include Copilot + CASAN" || fail "VS Code recommendations incomplete"
import json,sys
r=set(json.load(open(sys.argv[1]))["recommendations"])
assert {"GitHub.copilot","GitHub.copilot-chat","fpt-casan.casan-governed-chat"} <= r
PY
CASAN_HOME="$DK_HOME" node "$REPO_ROOT/packages/casan-devkit/tests/vscode-extension-tests.js" \
"$VSP" "$REPO_ROOT/packages/casan-harness/adapters/vscode/extension/extension.js" \
&& pass "VS Code @casan handler completes the real governed lifecycle" \
|| fail "VS Code @casan handler contract failed"
( cd "$VSP" && PATH="$FAKE_BIN:$PATH" "$DKC" doctor --client vscode-copilot >/dev/null 2>&1 ) \
&& pass "doctor proves the VS Code/Copilot adapter and installed extension" || fail "VS Code doctor failed"
echo "===== ⑪ professional output + safe project uninstall ====="
UN="$WORK/uninstall-project"
mkdir -p "$UN/.claude"
cat > "$UN/.claude/settings.json" <<'EOF'
{"model":"claude-opus-4-8","hooks":{"PreToolUse":[{"matcher":"*","hooks":[{"type":"command","command":"my-user-hook.sh"}]}]}}
EOF
INIT_OUT=$(cd "$UN" && "$DKC" init --level devkit --project uninstall-project --client claude --non-interactive)
echo "$INIT_OUT" | grep -q "CASAN initialized" \
&& pass "init defaults to concise human-readable output" \
|| fail "init human output missing ($INIT_OUT)"
echo "$INIT_OUT" | grep -q "Runtime.*Managed" \
&& pass "init output clearly identifies managed runtime and path" \
|| fail "init output hides runtime placement ($INIT_OUT)"
if echo "$INIT_OUT" | grep -q "✗"; then
fail "init output shows a false integration failure ($INIT_OUT)"
else
pass "optional integrations use a neutral status instead of a false failure"
fi
if echo "$INIT_OUT" | head -1 | grep -q '^[[:space:]]*{'; then
fail "init still defaults to raw JSON"
else
pass "init no longer dumps raw JSON by default"
fi
( cd "$UN" && "$DKC" doctor --json ) | python3 -c \
'import json,sys; d=json.load(sys.stdin); assert d["status"] == "ready"; assert d["operational_status"] == "ready"' \
&& pass "doctor --json preserves the machine-readable contract" \
|| fail "doctor --json is not valid/ready"
echo "project-owned requirement" > "$UN/apps/uninstall-project/domain/input/requirement.md"
UN_OUT=$(cd "$UN" && "$DKC" uninstall)
echo "$UN_OUT" | grep -q "CASAN removed from project" \
&& pass "uninstall emits a concise completion summary" \
|| fail "uninstall summary missing ($UN_OUT)"
[ ! -f "$UN/.casan/config.json" ] \
&& pass "uninstall removes CASAN-owned project config" \
|| fail "uninstall left CASAN config enabled"
py_check "$UN/.claude/settings.json" "my-user-hook" \
&& pass "uninstall preserves user-authored hooks" \
|| fail "uninstall removed a user hook"
if py_check "$UN/.claude/settings.json" "casan-hook.py"; then
fail "uninstall left the CASAN Claude hook enabled"
else
pass "uninstall removes only the CASAN hook"
fi
[ ! -e "$UN/.gitea/workflows/casan-ci.yml" ] \
&& [ ! -d "$UN/.gitea" ] \
&& pass "uninstall removes CASAN Gitea workflow and prunes empty .gitea" \
|| fail "uninstall left CASAN-owned .gitea content"
[ -f "$UN/apps/uninstall-project/domain/input/requirement.md" ] \
&& [ ! -f "$UN/apps/uninstall-project/domain/input/architecture.md" ] \
&& pass "uninstall removes unchanged scaffold files but preserves modified project content" \
|| fail "uninstall ownership checksum handling is unsafe"
[ -d "$UN/.specify" ] \
&& pass "uninstall retains runtime evidence unless --purge is explicit" \
|| fail "uninstall removed evidence without --purge"
( cd "$UN" && "$DKC" uninstall --purge >/dev/null ) \
&& [ ! -d "$UN/.specify" ] \
&& pass "a follow-up uninstall --purge removes retained evidence" \
|| fail "uninstall --purge did not remove retained evidence"
UN_KEEP="$WORK/uninstall-user-workflow"
mkdir -p "$UN_KEEP/.gitea/workflows"
echo "name: user-workflow" > "$UN_KEEP/.gitea/workflows/user-ci.yml"
( cd "$UN_KEEP" && "$DKC" init --level devkit --project uninstall-user-workflow --client none --non-interactive >/dev/null )
python3 - "$UN_KEEP/.casan/init-manifest.json" <<'PY'
import json
import sys
path = sys.argv[1]
data = json.load(open(path, encoding="utf-8"))
data.pop("manifest_version", None)
data.pop("owned_file_hashes", None)
with open(path, "w", encoding="utf-8") as handle:
json.dump(data, handle, indent=2)
handle.write("\n")
PY
( cd "$UN_KEEP" && "$DKC" uninstall >/dev/null )
[ -f "$UN_KEEP/.gitea/workflows/user-ci.yml" ] \
&& [ ! -f "$UN_KEEP/.gitea/workflows/casan-ci.yml" ] \
&& pass "legacy uninstall preserves user Gitea workflow and removes CASAN workflow" \
|| fail "uninstall removed a user workflow or retained the CASAN workflow"
UN_VENDORED="$WORK/uninstall-vendored"; mkdir -p "$UN_VENDORED"
( cd "$UN_VENDORED" && "$DKC" init --runtime vendored --project uninstall-vendored --client none --non-interactive >/dev/null )
( cd "$UN_VENDORED" && "$DKC" uninstall >/dev/null )
[ ! -d "$UN_VENDORED/.casan/runtime" ] \
&& [ ! -f "$UN_VENDORED/.casan/config.json" ] \
&& pass "uninstall removes the complete project-vendored Core" \
|| fail "uninstall left vendored Core or active project config"
echo ""
echo "===== HYBRID INSTALL SUMMARY: PASS=$PASS FAIL=$FAIL ====="
[[ "$FAIL" -eq 0 ]] || exit 1