Files
CASAN/packages/casan-devkit/tests/project-scaffold-tests.py

124 lines
6.0 KiB
Python

#!/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()