init: llm-wiki-haness 하네스 설계
This commit is contained in:
@@ -0,0 +1,365 @@
|
||||
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()
|
||||
Reference in New Issue
Block a user