feat: harden CASAN production install lifecycle
This commit is contained in:
@@ -36,6 +36,9 @@ assert 'CASAN_PROJECT_ID: "sample-project"' in workflow
|
||||
assert "__PROJECT_ID__" not in workflow
|
||||
assert (root / "bin/casan-chat").exists()
|
||||
assert (root / "bin/casan-chat.ps1").exists()
|
||||
assert not (root / "packages/casan-harness/tests").exists()
|
||||
assert not (root / "packages/casan-harness/level5").exists()
|
||||
assert not (root / "packages/casan-harness/scripts/bash/ci-harness-gate.sh").exists()
|
||||
for relative in ("docs/casan/CASAN_ADOPTION_WINDOWS.md", "docs/casan/CASAN_PROMPT_ENFORCEMENT.md"):
|
||||
text = (root / relative).read_text(encoding="utf-8")
|
||||
assert "sample-project" in text
|
||||
|
||||
@@ -27,6 +27,57 @@ echo "===== ① global install ====="
|
||||
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", "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 (
|
||||
"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",
|
||||
):
|
||||
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 "client selector accepts menu numbers, aliases, repeats, and all" \
|
||||
@@ -39,6 +90,33 @@ 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/config/tool-registry.yaml",
|
||||
"packages/casan-harness/scripts/bash/casan-harness.sh",
|
||||
"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 ) \
|
||||
@@ -160,7 +238,7 @@ L2="$WORK/l2"; mkdir -p "$L2"; ( cd "$L2" && "$DKC" init --level 2 --project l2
|
||||
( [ -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 ) | python3 -c 'import json,sys;print(json.load(sys.stdin)["project_target_level"])' 2>/dev/null)
|
||||
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)"
|
||||
|
||||
echo "===== ⑨ SAFETY: init refuses to adopt a CASAN source hub into itself ====="
|
||||
@@ -269,6 +347,48 @@ CASAN_HOME="$DK_HOME" node "$REPO_ROOT/packages/casan-devkit/tests/vscode-extens
|
||||
( 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 --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)"
|
||||
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"' \
|
||||
&& pass "doctor --json preserves the machine-readable contract" \
|
||||
|| fail "doctor --json is not valid/ready"
|
||||
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
|
||||
[ -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"
|
||||
|
||||
echo ""
|
||||
echo "===== HYBRID INSTALL SUMMARY: PASS=$PASS FAIL=$FAIL ====="
|
||||
[[ "$FAIL" -eq 0 ]] || exit 1
|
||||
|
||||
@@ -75,10 +75,13 @@ class ProjectScaffoldTests(unittest.TestCase):
|
||||
args = options(directory, with_harness=True)
|
||||
first = SCAFFOLD.scaffold(args)
|
||||
second = SCAFFOLD.scaffold(args)
|
||||
registry = json.loads((Path(directory) / "packages/casan-harness/level5/project-registry.json").read_text(encoding="utf-8"))
|
||||
registry = json.loads((Path(directory) / "packages/casan-harness/config/project-registry.json").read_text(encoding="utf-8"))
|
||||
self.assertEqual([item["project_id"] for item in registry["projects"]], ["inventory-app"])
|
||||
self.assertGreater(first["created"], 100)
|
||||
self.assertEqual(second["created"], 0)
|
||||
self.assertFalse((Path(directory) / "packages/casan-harness/tests").exists())
|
||||
self.assertFalse((Path(directory) / "packages/casan-harness/level5").exists())
|
||||
self.assertFalse((Path(directory) / "packages/casan-harness/scripts/bash/ci-harness-gate.sh").exists())
|
||||
|
||||
def test_invalid_identifiers_and_broad_target_fail_closed(self):
|
||||
with tempfile.TemporaryDirectory() as directory:
|
||||
|
||||
Reference in New Issue
Block a user