#!/usr/bin/env python3 from __future__ import annotations import argparse import importlib.util import json import os import subprocess import tempfile import unittest from pathlib import Path ROOT = Path(__file__).resolve().parents[3] def module(name: str, path: Path): spec = importlib.util.spec_from_file_location(name, path) value = importlib.util.module_from_spec(spec) spec.loader.exec_module(value) return value SCAFFOLD = module("casan_project_scaffold", ROOT / "packages/casan-devkit/project-scaffold.py") MANIFEST = module("casan_project_manifest_test", ROOT / "packages/casan-harness/scripts/bash/project_manifest.py") def options(target: str, **overrides): values = { "target": target, "project": "inventory-app", "name": "Inventory App", "feature_id": "001-inventory-app", "module_id": "MOD-01", "template": "nestjs-react", "with_harness": False, } values.update(overrides) return argparse.Namespace(**values) class ProjectScaffoldTests(unittest.TestCase): def test_scaffold_is_complete_valid_and_idempotent(self): with tempfile.TemporaryDirectory() as directory: first = SCAFFOLD.scaffold(options(directory)) second = SCAFFOLD.scaffold(options(directory)) self.assertGreater(first["created"], 20) self.assertEqual(second["created"], 0) self.assertEqual(second["unchanged"], first["created"]) project = MANIFEST.load(directory, "apps/inventory-app/domain/project.manifest.json") self.assertEqual(project["project_id"], "inventory-app") self.assertEqual(len(project["verification"]), 2) required = [ "package.json", ".github/workflows/ci.yml", "docker-compose.yml", "apps/inventory-app/backend/src/main.ts", "apps/inventory-app/frontend/src/App.tsx", "apps/inventory-app/domain/golden-runs/plan.golden.txt", ] self.assertTrue(all((Path(directory) / item).is_file() for item in required)) tokens = [path for path in Path(directory).rglob("*") if path.is_file() and "__PROJECT_" in path.read_text(encoding="utf-8", errors="ignore")] self.assertEqual(tokens, []) def test_existing_different_file_is_never_overwritten(self): with tempfile.TemporaryDirectory() as directory: path = Path(directory) / "package.json" path.write_text('{"owned_by":"user"}\n', encoding="utf-8") with self.assertRaisesRegex(SCAFFOLD.ScaffoldError, "no files were overwritten"): SCAFFOLD.scaffold(options(directory)) self.assertEqual(path.read_text(encoding="utf-8"), '{"owned_by":"user"}\n') def test_harness_install_registers_only_real_target_projects_and_is_idempotent(self): with tempfile.TemporaryDirectory() as directory: args = options(directory, with_harness=True) first = SCAFFOLD.scaffold(args) second = SCAFFOLD.scaffold(args) 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: with self.assertRaisesRegex(SCAFFOLD.ScaffoldError, "lowercase slug"): SCAFFOLD.scaffold(options(directory, project="../escape")) with self.assertRaisesRegex(SCAFFOLD.ScaffoldError, "safe punctuation"): SCAFFOLD.scaffold(options(directory, name='Broken "Template"')) with self.assertRaisesRegex(SCAFFOLD.ScaffoldError, "broad target"): SCAFFOLD.scaffold(options("/")) def test_manifest_rejects_path_escape_and_shell_executable(self): with tempfile.TemporaryDirectory() as directory: SCAFFOLD.scaffold(options(directory)) path = Path(directory) / "apps/inventory-app/domain/project.manifest.json" data = json.loads(path.read_text(encoding="utf-8")) data["requirements"] = "../outside.md" path.write_text(json.dumps(data), encoding="utf-8") with self.assertRaisesRegex(MANIFEST.ManifestError, "escapes"): MANIFEST.load(directory, "apps/inventory-app/domain/project.manifest.json") data["requirements"] = "apps/inventory-app/domain/input/requirement.md" data["commands"]["test"] = [["sh", "-c", "exit 0"]] path.write_text(json.dumps(data), encoding="utf-8") with self.assertRaisesRegex(MANIFEST.ManifestError, "not allowed"): MANIFEST.load(directory, "apps/inventory-app/domain/project.manifest.json") @unittest.skipUnless((ROOT / "node_modules/.bin/tsc").is_file(), "workspace dependencies are not installed") def test_generated_shell_builds_and_tests_with_approved_workspace_dependencies(self): (ROOT / "tmp").mkdir(exist_ok=True) with tempfile.TemporaryDirectory(dir=ROOT / "tmp") as directory: SCAFFOLD.scaffold(options(directory)) env = {**os.environ, "PATH": f"{ROOT / 'node_modules/.bin'}{os.pathsep}{os.environ.get('PATH', '')}"} for command in (["npm", "run", "build"], ["npm", "test"]): result = subprocess.run(command, cwd=directory, env=env, capture_output=True, text=True, timeout=120) self.assertEqual(result.returncode, 0, f"{' '.join(command)} failed:\n{result.stdout}\n{result.stderr}") if __name__ == "__main__": unittest.main()