afv-library/skills/investigating-agentforce-architecture/scripts/tests/test_finalize.py
2026-06-12 10:55:46 +00:00

279 lines
11 KiB
Python

"""Tests for ``finalize`` — declared-action-tree finalization + cache write.
Covers:
- ``sort_tree_in_place`` pure tree mutation (TOPIC-first, alphabetical-by-api_name)
- ``main`` WORK_DIR / DATA_DIR / CACHE_DIR orchestration
The pure-function tests build small dict trees inline. ``main`` tests
construct a minimal WORK_DIR layout under ``tmp_path`` so the orchestration
runs end-to-end without any external metadata fixtures.
"""
from __future__ import annotations
import json
import os
import unittest
from pathlib import Path
from tempfile import TemporaryDirectory
from . import _bootstrap # noqa: F401 — sys.path setup
import finalize # type: ignore
# -----------------------------------------------------------------------------
# sort_tree_in_place — pure tree mutation
# -----------------------------------------------------------------------------
class SortTreeInPlaceTests(unittest.TestCase):
"""Per docstring: TOPIC nodes first, then non-topic plannerActions.
Within each tier, sorted by kind then api_name (case-insensitive).
Each TOPIC's own children: alphabetical by api_name (case-insensitive).
FLOW children are NOT sorted.
"""
def test_empty_tree_is_noop(self):
root = {}
finalize.sort_tree_in_place(root)
self.assertEqual(root, {})
def test_root_with_no_children_is_noop(self):
root = {"children": []}
finalize.sort_tree_in_place(root)
self.assertEqual(root["children"], [])
def test_non_dict_root_is_safe(self):
# Non-dict input shouldn't blow up.
finalize.sort_tree_in_place(None) # type: ignore[arg-type]
finalize.sort_tree_in_place([]) # type: ignore[arg-type]
def test_topics_come_before_non_topics_at_root(self):
root = {"children": [
{"kind": "STANDARD_ACTION", "api_name": "actA"},
{"kind": "TOPIC", "api_name": "topicZ"},
{"kind": "APEX", "api_name": "apexA"},
{"kind": "TOPIC", "api_name": "topicA"},
]}
finalize.sort_tree_in_place(root)
kinds = [c["kind"] for c in root["children"]]
# Two TOPICs first, then non-topics
self.assertEqual(kinds[:2], ["TOPIC", "TOPIC"])
self.assertNotIn("TOPIC", kinds[2:])
def test_topics_sort_alphabetical_case_insensitive(self):
root = {"children": [
{"kind": "TOPIC", "api_name": "Zeta"},
{"kind": "TOPIC", "api_name": "alpha"},
{"kind": "TOPIC", "api_name": "Beta"},
]}
finalize.sort_tree_in_place(root)
names = [c["api_name"] for c in root["children"]]
self.assertEqual(names, ["alpha", "Beta", "Zeta"])
def test_non_topics_sort_by_kind_then_name(self):
root = {"children": [
{"kind": "STANDARD_ACTION", "api_name": "z_act"},
{"kind": "APEX", "api_name": "z_apex"},
{"kind": "STANDARD_ACTION", "api_name": "a_act"},
{"kind": "APEX", "api_name": "a_apex"},
]}
finalize.sort_tree_in_place(root)
# Tier 1 sorted by (kind, api_name): APEX rows before STANDARD_ACTION
kinds = [c["kind"] for c in root["children"]]
self.assertEqual(kinds, ["APEX", "APEX", "STANDARD_ACTION", "STANDARD_ACTION"])
names_apex = [c["api_name"] for c in root["children"][:2]]
self.assertEqual(names_apex, ["a_apex", "z_apex"])
def test_topic_children_sorted_alphabetical_case_insensitive(self):
root = {"children": [{
"kind": "TOPIC",
"api_name": "T1",
"children": [
{"kind": "STANDARD_ACTION", "api_name": "Zebra"},
{"kind": "STANDARD_ACTION", "api_name": "apple"},
{"kind": "STANDARD_ACTION", "api_name": "Banana"},
],
}]}
finalize.sort_tree_in_place(root)
names = [c["api_name"] for c in root["children"][0]["children"]]
self.assertEqual(names, ["apple", "Banana", "Zebra"])
def test_topic_with_no_children_is_safe(self):
root = {"children": [{"kind": "TOPIC", "api_name": "T1"}]}
finalize.sort_tree_in_place(root)
self.assertEqual(root["children"][0]["api_name"], "T1")
def test_missing_api_name_treated_as_empty(self):
# Should sort to the front under casefold of empty string.
root = {"children": [
{"kind": "TOPIC", "api_name": "Beta"},
{"kind": "TOPIC"}, # no api_name
]}
finalize.sort_tree_in_place(root)
names = [c.get("api_name", "") for c in root["children"]]
self.assertEqual(names[0], "") # empty/missing comes first
# -----------------------------------------------------------------------------
# main — orchestration via env vars + WORK_DIR
# -----------------------------------------------------------------------------
def _build_workdir(tmp: Path, *, planner_name: str = "MyPlanner",
pending: dict | None = None,
unresolved: list | None = None) -> tuple[Path, Path, Path]:
"""Construct a minimal WORK_DIR + DATA_DIR + CACHE_DIR triple.
Returns (work_dir, data_dir, cache_dir). Caller passes these as env
vars before invoking finalize.main().
"""
work_dir = tmp / "work"
data_dir = tmp / "data" / "ALPHA0000000000" / "MyAgent__v1"
cache_dir = tmp / "cache" / "ALPHA0000000000" / "MyAgent__v1"
work_dir.mkdir(parents=True)
tree = {
"agent": {"api_name": "MyAgent", "version": "v1"},
"node_count": 1,
"depth": 1,
"_schema_version": "3.1",
"_kind_counts": {"TOPIC": 1},
"_pending_fetches": pending or {},
"_unresolved": unresolved or [],
"root": {"children": [{"kind": "TOPIC", "api_name": "Greetings"}]},
}
(work_dir / "declared_action_tree.json").write_text(json.dumps(tree))
return work_dir, data_dir, cache_dir
class MainTests(unittest.TestCase):
def _run_main(self, work_dir: Path, data_dir: Path, cache_dir: Path,
*, planner_name: str = "MyPlanner") -> int:
old = dict(os.environ)
os.environ.update({
"WORK_DIR": str(work_dir),
"DATA_DIR": str(data_dir),
"CACHE_DIR": str(cache_dir),
"AGENT_API_NAME": "MyAgent",
"AGENT_VERSION": "v1",
"PLANNER_NAME": planner_name,
})
try:
return finalize.main()
finally:
os.environ.clear()
os.environ.update(old)
def test_main_writes_tree_and_manifest_and_returns_zero(self):
with TemporaryDirectory() as t:
tmp = Path(t)
work_dir, data_dir, cache_dir = _build_workdir(tmp)
rc = self._run_main(work_dir, data_dir, cache_dir)
self.assertEqual(rc, 0)
tree_file = data_dir / "MyAgent_v1_metadata_tree.json"
self.assertTrue(tree_file.exists())
self.assertTrue((data_dir / "last_built_at.txt").exists())
manifest = json.loads((cache_dir / "manifest.json").read_text())
self.assertEqual(manifest["agent"]["api_name"], "MyAgent")
self.assertEqual(manifest["partial"], False)
self.assertEqual(manifest["unresolved_count"], 0)
def test_main_marks_partial_when_planner_name_empty(self):
# Empty PLANNER_NAME → _partial=True, _partial_reason="no-planner".
with TemporaryDirectory() as t:
tmp = Path(t)
work_dir, data_dir, cache_dir = _build_workdir(tmp)
rc = self._run_main(work_dir, data_dir, cache_dir, planner_name="")
self.assertEqual(rc, 0)
tree = json.loads(
(data_dir / "MyAgent_v1_metadata_tree.json").read_text()
)
self.assertTrue(tree["_partial"])
self.assertEqual(tree["_partial_reason"], "no-planner")
def test_main_marks_partial_when_unresolved_present(self):
# Planner OK, _pending empty, but _unresolved non-empty
# → _partial=True, _partial_reason="unresolved-refs".
with TemporaryDirectory() as t:
tmp = Path(t)
work_dir, data_dir, cache_dir = _build_workdir(
tmp, unresolved=[{"reason": "404"}]
)
rc = self._run_main(work_dir, data_dir, cache_dir)
self.assertEqual(rc, 0)
tree = json.loads(
(data_dir / "MyAgent_v1_metadata_tree.json").read_text()
)
self.assertTrue(tree["_partial"])
self.assertEqual(tree["_partial_reason"], "unresolved-refs")
def test_main_marks_partial_when_pending_present(self):
# Planner OK, _unresolved empty, but _pending non-empty
# → _partial=True, _partial_reason="pending-refs".
with TemporaryDirectory() as t:
tmp = Path(t)
work_dir, data_dir, cache_dir = _build_workdir(
tmp, pending={"flows": ["FlowA"]}
)
rc = self._run_main(work_dir, data_dir, cache_dir)
self.assertEqual(rc, 0)
tree = json.loads(
(data_dir / "MyAgent_v1_metadata_tree.json").read_text()
)
self.assertTrue(tree["_partial"])
self.assertEqual(tree["_partial_reason"], "pending-refs")
def test_main_returns_one_when_env_missing(self):
old = dict(os.environ)
# Wipe required envs. Set only one.
for k in ("WORK_DIR", "DATA_DIR", "CACHE_DIR",
"AGENT_API_NAME", "AGENT_VERSION"):
os.environ.pop(k, None)
try:
rc = finalize.main()
finally:
os.environ.clear()
os.environ.update(old)
self.assertEqual(rc, 1)
def test_main_returns_one_when_tree_json_missing(self):
with TemporaryDirectory() as t:
tmp = Path(t)
work_dir = tmp / "work"
data_dir = tmp / "data"
cache_dir = tmp / "cache"
work_dir.mkdir()
# No declared_action_tree.json — finalize should bail.
rc = self._run_main(work_dir, data_dir, cache_dir)
self.assertEqual(rc, 1)
def test_main_strips_visited_from_durable_artifact(self):
with TemporaryDirectory() as t:
tmp = Path(t)
work_dir, data_dir, cache_dir = _build_workdir(tmp)
tree_path = work_dir / "declared_action_tree.json"
tree = json.loads(tree_path.read_text())
tree["_visited"] = {"some": "internal-state"}
tree_path.write_text(json.dumps(tree))
rc = self._run_main(work_dir, data_dir, cache_dir)
self.assertEqual(rc, 0)
written = json.loads(
(data_dir / "MyAgent_v1_metadata_tree.json").read_text()
)
self.assertNotIn("_visited", written)
def test_main_seeds_gitignore_on_data_and_cache_roots(self):
with TemporaryDirectory() as t:
tmp = Path(t)
work_dir, data_dir, cache_dir = _build_workdir(tmp)
self._run_main(work_dir, data_dir, cache_dir)
# data_dir.parent.parent == tmp/data; cache_dir.parent.parent == tmp/cache
self.assertTrue((data_dir.parent.parent / ".gitignore").exists())
self.assertTrue((cache_dir.parent.parent / ".gitignore").exists())
if __name__ == "__main__":
unittest.main()