366 lines
16 KiB
Python
366 lines
16 KiB
Python
from __future__ import annotations
|
|
|
|
import contextlib
|
|
import hashlib
|
|
import io
|
|
import json
|
|
import shutil
|
|
import sys
|
|
import tempfile
|
|
import unittest
|
|
from unittest import mock
|
|
from collections import Counter
|
|
from pathlib import Path
|
|
|
|
|
|
REPO_ROOT = Path(__file__).resolve().parents[2]
|
|
ADAPTER_DIR = REPO_ROOT / "harness/adapters"
|
|
if str(ADAPTER_DIR) not in sys.path:
|
|
sys.path.insert(0, str(ADAPTER_DIR))
|
|
|
|
import generate as adapters # noqa: E402
|
|
import generate_workflows as compatibility # noqa: E402
|
|
import fs_transaction # noqa: E402
|
|
|
|
|
|
class GenerateAdaptersTest(unittest.TestCase):
|
|
maxDiff = None
|
|
|
|
def setUp(self) -> None:
|
|
self.tempdir = tempfile.TemporaryDirectory()
|
|
self.root = Path(self.tempdir.name)
|
|
shutil.copytree(REPO_ROOT / "harness/source", self.root / "harness/source")
|
|
(self.root / "harness/adapters").mkdir(parents=True)
|
|
shutil.copyfile(
|
|
REPO_ROOT / adapters.PLATFORM_METADATA_REL,
|
|
self.root / adapters.PLATFORM_METADATA_REL,
|
|
)
|
|
entrypoint = self.root / "harness/runtime/branch_from_project.py"
|
|
entrypoint.parent.mkdir(parents=True)
|
|
shutil.copyfile(REPO_ROOT / "harness/runtime/branch_from_project.py", entrypoint)
|
|
|
|
def tearDown(self) -> None:
|
|
self.tempdir.cleanup()
|
|
|
|
def _write_all(self) -> adapters.GenerationResult:
|
|
result = adapters.generate(self.root, check=False)
|
|
self.assertEqual(len(result.written), 113)
|
|
return result
|
|
|
|
def _main(self, *args: str) -> int:
|
|
with contextlib.redirect_stdout(io.StringIO()), contextlib.redirect_stderr(io.StringIO()):
|
|
return adapters.main([*args, "--root", str(self.root)])
|
|
|
|
def test_manifest_covers_all_113_targets_exactly_once(self) -> None:
|
|
catalog = adapters.load_catalog(REPO_ROOT)
|
|
paths = [target.path for _, target in catalog.targets]
|
|
self.assertEqual(len(paths), 113)
|
|
self.assertEqual(len(set(paths)), 113)
|
|
self.assertEqual(
|
|
Counter(target.kind for _, target in catalog.targets),
|
|
{
|
|
"agent-skill": 16,
|
|
"agent-workflow": 16,
|
|
"claude-command": 24,
|
|
"plugin-agent-md": 11,
|
|
"claude-agent-md": 13,
|
|
"codex-agent-md": 11,
|
|
"codex-agent-toml": 11,
|
|
"antigravity-agent-json": 11,
|
|
},
|
|
)
|
|
self.assertEqual(set(paths), adapters._target_inventory(catalog))
|
|
|
|
def test_neutral_sources_match_pre_migration_semantic_goldens(self) -> None:
|
|
fixture = json.loads(
|
|
(REPO_ROOT / "harness/tests/fixtures/legacy-semantic-sha256.json").read_text(
|
|
encoding="utf-8"
|
|
)
|
|
)["sources"]
|
|
catalog = adapters.load_catalog(REPO_ROOT)
|
|
self.assertEqual(len(fixture), 37)
|
|
self.assertEqual(
|
|
set(fixture),
|
|
{f"{source.kind}:{source.identifier}" for source in catalog.sources},
|
|
)
|
|
for source in catalog.sources:
|
|
golden = fixture[f"{source.kind}:{source.identifier}"]
|
|
self.assertTrue((REPO_ROOT / golden["origin_target"]).is_file())
|
|
self.assertEqual(
|
|
hashlib.sha256(source.body.encode("utf-8")).hexdigest(),
|
|
golden["body_sha256"],
|
|
)
|
|
|
|
def test_adapter_snapshots_and_current_targets_are_fresh(self) -> None:
|
|
snapshots = json.loads(
|
|
(REPO_ROOT / "harness/tests/fixtures/adapter-snapshots.json").read_text(
|
|
encoding="utf-8"
|
|
)
|
|
)["targets"]
|
|
catalog = adapters.load_catalog(REPO_ROOT)
|
|
self.assertEqual(len(snapshots), 113)
|
|
for source, target in catalog.targets:
|
|
rendered = adapters.render_target(catalog, source, target)
|
|
digest = hashlib.sha256(rendered.encode("utf-8")).hexdigest()
|
|
self.assertEqual(digest, snapshots[target.path], target.path)
|
|
self.assertEqual(rendered, (REPO_ROOT / target.path).read_text(encoding="utf-8"))
|
|
self.assertTrue(adapters.generate(REPO_ROOT, check=True).ok)
|
|
|
|
def test_markers_cover_markdown_toml_and_json_targets(self) -> None:
|
|
catalog = adapters.load_catalog(REPO_ROOT)
|
|
for source, target in catalog.targets:
|
|
rendered = adapters.render_target(catalog, source, target)
|
|
expected_fragment = f"{source.metadata_path} sha256:{source.digest}; DO NOT EDIT"
|
|
self.assertIn(expected_fragment, rendered, target.path)
|
|
if target.kind == "antigravity-agent-json":
|
|
self.assertIn("_generated", json.loads(rendered))
|
|
|
|
def test_write_then_check_and_compatibility_wrapper(self) -> None:
|
|
self._write_all()
|
|
self.assertTrue(adapters.generate(self.root, check=True).ok)
|
|
self.assertEqual(compatibility.generate(self.root, check=True), [])
|
|
|
|
def test_direct_edit_drift_is_nonzero_and_not_overwritten_by_check(self) -> None:
|
|
self._write_all()
|
|
target = self.root / ".claude/commands/branch.md"
|
|
target.write_text("manual drift\n", encoding="utf-8")
|
|
self.assertEqual(self._main("--check"), 1)
|
|
self.assertEqual(target.read_text(encoding="utf-8"), "manual drift\n")
|
|
self.assertIn(".claude/commands/branch.md", adapters.generate(self.root).stale)
|
|
|
|
def test_missing_target_is_nonzero_and_write_recreates_it(self) -> None:
|
|
self._write_all()
|
|
target = self.root / ".claude/commands/branch.md"
|
|
target.unlink()
|
|
self.assertEqual(self._main("--check"), 1)
|
|
self.assertEqual(adapters.generate(self.root).missing, (".claude/commands/branch.md",))
|
|
self.assertEqual(self._main("--write"), 0)
|
|
self.assertTrue(target.is_file())
|
|
|
|
def test_extra_target_is_nonzero_and_write_refuses_to_delete_it(self) -> None:
|
|
self._write_all()
|
|
extra = self.root / ".claude/commands/unmapped.md"
|
|
extra.write_text("unmapped\n", encoding="utf-8")
|
|
self.assertEqual(self._main("--check"), 1)
|
|
self.assertEqual(self._main("--write"), 1)
|
|
self.assertTrue(extra.is_file())
|
|
|
|
def test_missing_and_extra_sources_are_nonzero(self) -> None:
|
|
missing = self.root / "harness/source/skills/branch.md"
|
|
missing.unlink()
|
|
self.assertEqual(self._main("--check"), 2)
|
|
shutil.copyfile(REPO_ROOT / "harness/source/skills/branch.md", missing)
|
|
(self.root / "harness/source/skills/unmapped.md").write_text("extra\n", encoding="utf-8")
|
|
self.assertEqual(self._main("--check"), 2)
|
|
|
|
def test_duplicate_source_and_target_mappings_are_nonzero(self) -> None:
|
|
manifest_path = self.root / adapters.MANIFEST_REL
|
|
manifest = json.loads(manifest_path.read_text(encoding="utf-8"))
|
|
manifest["sources"].append(dict(manifest["sources"][0]))
|
|
manifest_path.write_text(json.dumps(manifest), encoding="utf-8")
|
|
self.assertEqual(self._main("--check"), 2)
|
|
|
|
shutil.copyfile(REPO_ROOT / adapters.MANIFEST_REL, manifest_path)
|
|
first = json.loads((self.root / "harness/source/workflows/blogify.json").read_text())
|
|
second_path = self.root / "harness/source/workflows/branch.json"
|
|
second = json.loads(second_path.read_text())
|
|
second["targets"].append(dict(first["targets"][0]))
|
|
second_path.write_text(json.dumps(second), encoding="utf-8")
|
|
self.assertEqual(self._main("--check"), 2)
|
|
|
|
def test_heading_locale_is_machine_metadata_not_adapter_frontmatter(self) -> None:
|
|
catalog = adapters.load_catalog(REPO_ROOT)
|
|
self.assertTrue(all(source.heading_locale in {"ko-KR", "en", "mixed"} for source in catalog.sources))
|
|
for source, target in catalog.targets:
|
|
rendered = adapters.render_target(catalog, source, target)
|
|
self.assertNotIn("heading_locale", rendered, target.path)
|
|
|
|
def test_every_workflow_has_nested_profile_and_risk_contract(self) -> None:
|
|
catalog = adapters.load_catalog(REPO_ROOT)
|
|
workflows = {source.identifier: source for source in catalog.sources if source.kind == "workflow"}
|
|
self.assertEqual(len(workflows), 24)
|
|
for source in workflows.values():
|
|
contract = source.execution_contract
|
|
self.assertIsInstance(contract, dict, source.identifier)
|
|
self.assertIn(contract["profile"], adapters.EXECUTION_PROFILES)
|
|
self.assertIn(contract["default_risk"], adapters.EXECUTION_RISKS)
|
|
self.assertIsInstance(contract["design_bearing"], bool)
|
|
for target in source.targets:
|
|
rendered = adapters.render_target(catalog, source, target)
|
|
self.assertIn("execution_contract:", rendered, target.path)
|
|
self.assertIn("harness/runtime/workflow_dispatch.py", rendered, target.path)
|
|
self.assertIn("--phase baseline", rendered, target.path)
|
|
self.assertIn("--phase final", rendered, target.path)
|
|
|
|
self.assertEqual(workflows["branch-from-project"].execution_contract, {
|
|
"kind": "deterministic",
|
|
"profile": "capture",
|
|
"default_risk": "low",
|
|
"design_bearing": False,
|
|
"entrypoint": "harness/runtime/branch_from_project.py",
|
|
"dry_run_first": True,
|
|
"result_schema": "branch-from-project-result/v1",
|
|
})
|
|
expected_profiles = {
|
|
"branch-from-project": "capture",
|
|
"branch": "capture",
|
|
"daily": "capture",
|
|
"branch-spec": "design",
|
|
"project-spec": "design",
|
|
"depth": "audit",
|
|
"coverage": "audit",
|
|
"sync": "audit",
|
|
"lint": "audit",
|
|
"interviewize": "publish",
|
|
"blogify": "publish",
|
|
}
|
|
self.assertEqual(
|
|
{name: workflows[name].execution_contract["profile"] for name in expected_profiles},
|
|
expected_profiles,
|
|
)
|
|
|
|
def test_document_writers_and_reporters_keep_runtime_connections_in_every_adapter(self) -> None:
|
|
catalog = adapters.load_catalog(REPO_ROOT)
|
|
sources = {(source.kind, source.identifier): source for source in catalog.sources}
|
|
writer_required = (
|
|
"harness/runtime/document_commit.py",
|
|
"document-commit/v1",
|
|
"document-commit-result/v1",
|
|
"--dry-run",
|
|
"plan_sha256",
|
|
"--expected-plan-sha256",
|
|
"--apply",
|
|
)
|
|
writer_forbidden = (
|
|
"자동 rollback 미구현",
|
|
"**C4. Parent hub Cluster 갱신**",
|
|
"**M5. Parent hub Cluster 점검**",
|
|
"### Step 6: Parent hub Cluster 갱신",
|
|
)
|
|
for identifier in ("wiki-doc-author", "wiki-source-summarizer"):
|
|
source = sources[("agent", identifier)]
|
|
self.assertNotIn("replace_file_content", source.body, identifier)
|
|
documents = [source.body, *(adapters.render_target(catalog, source, target) for target in source.targets)]
|
|
for document in documents:
|
|
for token in writer_required:
|
|
self.assertIn(token, document, f"{identifier}: {token}")
|
|
for token in writer_forbidden:
|
|
self.assertNotIn(token, document, f"{identifier}: {token}")
|
|
|
|
proof_required = (
|
|
"proof-request/v1",
|
|
"harness/runtime/proof_runner.py",
|
|
"proof-runner-result/v1",
|
|
"proof-manifest/v1",
|
|
"manifest_path",
|
|
"manifest_sha256",
|
|
"proof_count",
|
|
"pass_count",
|
|
"fail_count",
|
|
"1~3",
|
|
)
|
|
reporter_agents = (
|
|
"branch-depth-auditor",
|
|
"coverage-auditor",
|
|
"extraction-broker",
|
|
"project-readiness-auditor",
|
|
"wiki-adversarial-reviewer",
|
|
"wiki-consistency-auditor",
|
|
"wiki-decision-researcher",
|
|
"wiki-diagram-reviewer",
|
|
"wiki-link-verifier",
|
|
"wiki-research-lane",
|
|
"wiki-source-summarizer",
|
|
)
|
|
reporter_workflows = (
|
|
"branch-spec",
|
|
"coverage",
|
|
"depth",
|
|
"ingest",
|
|
"invest-research",
|
|
"lint",
|
|
"migrate-claims",
|
|
"project-spec",
|
|
"sync",
|
|
)
|
|
for kind, identifiers in (("agent", reporter_agents), ("workflow", reporter_workflows)):
|
|
for identifier in identifiers:
|
|
source = sources[(kind, identifier)]
|
|
documents = [source.body, *(adapters.render_target(catalog, source, target) for target in source.targets)]
|
|
for document in documents:
|
|
self.assertNotIn("harness/runtime/proof_manifest.py", document, f"{kind}:{identifier}")
|
|
for token in proof_required:
|
|
self.assertIn(token, document, f"{kind}:{identifier}: {token}")
|
|
|
|
global_workflow = (REPO_ROOT / ".agents/plugins/wiki-superpowers/skills/wiki-workflow/SKILL.md").read_text(encoding="utf-8")
|
|
for token in proof_required:
|
|
self.assertIn(token, global_workflow, token)
|
|
self.assertNotIn("harness/runtime/proof_manifest.py", global_workflow)
|
|
self.assertNotIn("중립 adapter 전면 migration은 후속 TODO", global_workflow)
|
|
|
|
def test_deterministic_workflow_without_entrypoint_fails_closed(self) -> None:
|
|
metadata = self.root / "harness/source/workflows/branch-from-project.json"
|
|
document = json.loads(metadata.read_text(encoding="utf-8"))
|
|
del document["execution_contract"]["entrypoint"]
|
|
metadata.write_text(json.dumps(document), encoding="utf-8")
|
|
self.assertEqual(self._main("--check"), 2)
|
|
|
|
def test_write_uses_one_global_replace_many_call(self) -> None:
|
|
self._write_all()
|
|
source = self.root / "harness/source/skills/branch.md"
|
|
source.write_text(source.read_text(encoding="utf-8") + "\n", encoding="utf-8")
|
|
with mock.patch.object(adapters, "replace_many") as replace:
|
|
result = adapters.generate(self.root, check=False)
|
|
replace.assert_called_once()
|
|
self.assertEqual(len(replace.call_args.args[0]), 113)
|
|
self.assertEqual(len(result.written), 113)
|
|
|
|
def test_global_write_rolls_back_every_target_after_mid_commit_failure(self) -> None:
|
|
self._write_all()
|
|
source = self.root / "harness/source/skills/branch.md"
|
|
source.write_text(source.read_text(encoding="utf-8") + "\n", encoding="utf-8")
|
|
catalog = adapters.load_catalog(self.root)
|
|
targets = [self.root / target.path for _, target in catalog.targets]
|
|
before = {target: target.read_bytes() for target in targets}
|
|
original_replace = fs_transaction.os.replace
|
|
calls = 0
|
|
|
|
def fail_second_replace(source_path: object, target_path: object) -> None:
|
|
nonlocal calls
|
|
calls += 1
|
|
if calls == 2:
|
|
raise OSError("injected adapter commit failure")
|
|
original_replace(source_path, target_path)
|
|
|
|
with mock.patch.object(fs_transaction.os, "replace", side_effect=fail_second_replace):
|
|
with self.assertRaises(fs_transaction.TransactionError):
|
|
adapters.generate(self.root, check=False)
|
|
self.assertEqual({target: target.read_bytes() for target in targets}, before)
|
|
|
|
def test_global_write_cleans_staged_bytes_when_staging_fails(self) -> None:
|
|
self._write_all()
|
|
source = self.root / "harness/source/skills/branch.md"
|
|
source.write_text(source.read_text(encoding="utf-8") + "\n", encoding="utf-8")
|
|
original_temporary = fs_transaction._temporary_bytes
|
|
staged: list[Path] = []
|
|
calls = 0
|
|
|
|
def fail_second_stage(target: Path, data: bytes, mode: int | None = None) -> Path:
|
|
nonlocal calls
|
|
calls += 1
|
|
if calls == 2:
|
|
raise OSError("injected adapter staging failure")
|
|
temporary = original_temporary(target, data, mode)
|
|
staged.append(temporary)
|
|
return temporary
|
|
|
|
with mock.patch.object(fs_transaction, "_temporary_bytes", side_effect=fail_second_stage):
|
|
with self.assertRaises(OSError):
|
|
adapters.generate(self.root, check=False)
|
|
self.assertTrue(staged)
|
|
self.assertTrue(all(not temporary.exists() for temporary in staged))
|
|
|
|
|
|
if __name__ == "__main__":
|
|
unittest.main()
|