# ============================================================================== # test_deploy_layout.py — Deploy Script Layout, Refresh & Gitignore Test Suite # ============================================================================== import os import shutil import subprocess import tempfile import unittest class TestDeployLayout(unittest.TestCase): def setUp(self): self.repo_root = os.path.abspath(os.path.join(os.path.dirname(__file__), "..")) self.temp_dir = tempfile.mkdtemp(prefix="mam_deploy_layout_test_") self.test_dir = os.path.join(self.temp_dir, "workspace") os.makedirs(self.test_dir, exist_ok=True) self.env = os.environ.copy() self.env["MAM_REPO_URL"] = self.repo_root self.env["MAM_SKIP_VENV"] = "1" def tearDown(self): shutil.rmtree(self.temp_dir, ignore_errors=True) def test_td1_td2_td3_essential_markdowns_only(self): """T-D1~D3: Verify essential markdowns are shipped and internal reports/references are excluded.""" res = subprocess.run( ["bash", os.path.join(self.repo_root, "deploy/install.sh"), self.test_dir], env=self.env, capture_output=True, text=True ) self.assertEqual(res.returncode, 0, f"install.sh failed: {res.stderr}") # T-D1: No internal reports in .agents/reports reports_dir = os.path.join(self.test_dir, ".agents", "reports") if os.path.exists(reports_dir): reports_files = [f for root, _, files in os.walk(reports_dir) for f in files if f.endswith(".md")] self.assertEqual(len(reports_files), 0, f"Internal reports found: {reports_files}") # T-D2: Non-essential root docs omitted by default self.assertFalse(os.path.exists(os.path.join(self.test_dir, "MESSAGING.md"))) self.assertFalse(os.path.exists(os.path.join(self.test_dir, "BOOTSTRAP.md"))) # T-D3: Core essential docs present (B-3 gate) self.assertTrue(os.path.exists(os.path.join(self.test_dir, "AGENTS.md"))) self.assertTrue(os.path.exists(os.path.join(self.test_dir, ".agents", "MULTI_AGENT_RULES.md"))) self.assertTrue(os.path.exists(os.path.join(self.test_dir, ".agents", "INSTALL.md"))) self.assertTrue(os.path.exists(os.path.join(self.test_dir, ".agents", "skills", "lib.sh"))) def test_td6_td7_td8_mam_deploy_layout_and_removal(self): """T-D6~D8: Verify remove.sh/update.sh installed in .mam_deploy and work properly.""" subprocess.run( ["bash", os.path.join(self.repo_root, "deploy/install.sh"), self.test_dir], env=self.env, check=True ) # T-D6: Installed in .mam_deploy and executable remover = os.path.join(self.test_dir, ".mam_deploy", "remove.sh") updater = os.path.join(self.test_dir, ".mam_deploy", "update.sh") self.assertTrue(os.path.exists(remover)) self.assertTrue(os.path.exists(updater)) self.assertTrue(os.access(remover, os.X_OK)) # T-D8: Execution from inside .mam_deploy directory targets parent workspace res = subprocess.run( ["bash", "./remove.sh", "--force"], cwd=os.path.join(self.test_dir, ".mam_deploy"), capture_output=True, text=True ) self.assertEqual(res.returncode, 0, f"remove.sh failed: {res.stderr}") self.assertFalse(os.path.exists(os.path.join(self.test_dir, ".agents", "skills"))) self.assertFalse(os.path.exists(os.path.join(self.test_dir, ".mam_deploy"))) def test_td12_td13_td14_gitignore_managed_block(self): """T-D12~D14: Verify .gitignore managed block injection and manifest exclusion (B-1 gate).""" subprocess.run( ["bash", os.path.join(self.repo_root, "deploy/install.sh"), self.test_dir], env=self.env, check=True ) gi_path = os.path.join(self.test_dir, ".gitignore") self.assertTrue(os.path.exists(gi_path)) with open(gi_path) as f: content = f.read() self.assertIn("# >>> MAM managed block", content) self.assertIn("/.mam.env", content) self.assertIn("/.mam_deploy/", content) # T-D14 [MERGE BLOCKER B-1]: .gitignore MUST NOT be in manifest manifest_path = os.path.join(self.test_dir, ".mam", "install_manifest.txt") with open(manifest_path) as f: manifest = f.read() self.assertNotIn(".gitignore", manifest) def test_td21_td22_td23_safe_refresh_custom_skills(self): """T-D21~D23: Verify custom modifications to skills are preserved on refresh (B-4 gate).""" subprocess.run( ["bash", os.path.join(self.repo_root, "deploy/install.sh"), self.test_dir], env=self.env, check=True ) # Modify a framework skill file custom_skill = os.path.join(self.test_dir, ".agents", "skills", "lib.sh") with open(custom_skill, "a") as f: f.write("\n# CUSTOM_USER_MODIFICATION\n") # Run install.sh again (REFRESH=1) res = subprocess.run( ["bash", os.path.join(self.repo_root, "deploy/install.sh"), self.test_dir], env=self.env, capture_output=True, text=True ) self.assertEqual(res.returncode, 0) self.assertIn("Local modification detected", res.stderr) # Verify modification preserved with open(custom_skill) as f: self.assertIn("CUSTOM_USER_MODIFICATION", f.read()) # Verify backup created in .mam/skill-backups backups_dir = os.path.join(self.test_dir, ".mam", "skill-backups") self.assertTrue(os.path.exists(backups_dir)) def test_td27_td28_update_preserves_mam_state(self): """T-D27~D28: Verify update.sh preserves .mam state files across cycles (B-5 gate).""" subprocess.run( ["bash", os.path.join(self.repo_root, "deploy/install.sh"), self.test_dir], env=self.env, check=True ) # Run update.sh updater = os.path.join(self.test_dir, ".mam_deploy", "update.sh") res = subprocess.run(["bash", updater, "--force"], cwd=self.test_dir, env=self.env, capture_output=True, text=True) self.assertEqual(res.returncode, 0, f"update.sh failed: {res.stderr}") # Verify asset_hashes.txt and version.txt still exist self.assertTrue(os.path.exists(os.path.join(self.test_dir, ".mam", "asset_hashes.txt"))) self.assertTrue(os.path.exists(os.path.join(self.test_dir, ".mam", "version.txt"))) if __name__ == "__main__": unittest.main()