"""v0.3 PR #9 — User-scope persona/workflow directory tests. Covers: 1. `ensure_user_dirs_initialized` creates both directories (idempotent). 2. `load_combined_personas` returns seed + user, deduplicated by (name, version). 3. User entries override seed entries with the same key (last-wins). 4. Malformed user persona files are logged + skipped (don't kill the REPL). 5. `load_combined_workflows` mirrors the persona behaviour for workflow YAMLs. 6. Empty user dirs → seed-only. """ from __future__ import annotations from pathlib import Path from textwrap import dedent from my_deepagent.config import load_config from my_deepagent.user_dirs import ( ensure_user_dirs_initialized, load_combined_personas, load_combined_workflows, user_personas_dir, user_workflows_dir, ) def _write_persona_yaml( target: Path, *, name: str, version: int = 1, model: str = "openrouter:deepseek/deepseek-chat", backend: str = "openrouter", capabilities: list[str] | None = None, ) -> None: target.parent.mkdir(parents=True, exist_ok=True) caps = capabilities or ["code_edit"] cap_lines = "\n".join(f" - {c}" for c in caps) target.write_text( dedent( f"""\ name: {name} version: {version} backend: {backend} model: "{model}" provider_origin: "CN/DeepSeek" capabilities: {cap_lines} max_risk_level: medium system_prompt: | Test persona system prompt (must be ≥10 chars). allowed_tools: - read_file - write_file deepagents_backend: state """ ), encoding="utf-8", ) def _write_workflow_yaml(target: Path, *, name: str, version: int = 1) -> None: target.parent.mkdir(parents=True, exist_ok=True) target.write_text( dedent( f"""\ name: {name} version: {version} description: "test workflow {name}" roles: - id: writer required_capabilities: [code_edit] phases: - key: p1 title: "first phase" risk: medium role: writer gates: [] expected_artifact: path: artifacts/foo.md schema: text instructions: "do something useful in this phase" """ ), encoding="utf-8", ) # --------------------------------------------------------------------------- # Bootstrap # --------------------------------------------------------------------------- def test_ensure_user_dirs_creates_both(tmp_path: Path) -> None: cfg = load_config(workspace_root=tmp_path, data_dir=tmp_path / "data") ensure_user_dirs_initialized(cfg) assert user_personas_dir(cfg).is_dir() assert user_workflows_dir(cfg).is_dir() def test_ensure_user_dirs_is_idempotent(tmp_path: Path) -> None: cfg = load_config(workspace_root=tmp_path, data_dir=tmp_path / "data") ensure_user_dirs_initialized(cfg) # Drop a file to make sure repeat doesn't wipe it. _write_persona_yaml(user_personas_dir(cfg) / "p.yaml", name="custom-test") ensure_user_dirs_initialized(cfg) assert (user_personas_dir(cfg) / "p.yaml").is_file() # --------------------------------------------------------------------------- # load_combined_personas # --------------------------------------------------------------------------- def test_load_combined_personas_returns_seed_only_when_no_user(tmp_path: Path) -> None: cfg = load_config(workspace_root=tmp_path, data_dir=tmp_path / "data") seed = tmp_path / "seed" _write_persona_yaml(seed / "a.yaml", name="alpha") _write_persona_yaml(seed / "b.yaml", name="bravo") personas = load_combined_personas(cfg, seed) names = sorted(p.name for p in personas) assert names == ["alpha", "bravo"] def test_load_combined_personas_adds_user(tmp_path: Path) -> None: cfg = load_config(workspace_root=tmp_path, data_dir=tmp_path / "data") seed = tmp_path / "seed" _write_persona_yaml(seed / "a.yaml", name="alpha") _write_persona_yaml(user_personas_dir(cfg) / "user.yaml", name="my-custom") personas = load_combined_personas(cfg, seed) names = sorted(p.name for p in personas) assert names == ["alpha", "my-custom"] def test_load_combined_personas_user_overrides_seed(tmp_path: Path) -> None: cfg = load_config(workspace_root=tmp_path, data_dir=tmp_path / "data") seed = tmp_path / "seed" _write_persona_yaml(seed / "alpha.yaml", name="alpha", model="seed-model") _write_persona_yaml(user_personas_dir(cfg) / "alpha.yaml", name="alpha", model="user-model") personas = load_combined_personas(cfg, seed) assert len(personas) == 1 assert personas[0].name == "alpha" assert personas[0].model == "user-model" # user wins def test_load_combined_personas_skips_malformed_user_file(tmp_path: Path) -> None: cfg = load_config(workspace_root=tmp_path, data_dir=tmp_path / "data") seed = tmp_path / "seed" _write_persona_yaml(seed / "a.yaml", name="alpha") bad = user_personas_dir(cfg) / "broken.yaml" bad.parent.mkdir(parents=True, exist_ok=True) bad.write_text("not: a valid: persona:::", encoding="utf-8") # Should not raise — broken file is logged + skipped. personas = load_combined_personas(cfg, seed) # Seed alpha is still present. assert any(p.name == "alpha" for p in personas) # --------------------------------------------------------------------------- # load_combined_workflows # --------------------------------------------------------------------------- def test_load_combined_workflows_seed_only(tmp_path: Path) -> None: cfg = load_config(workspace_root=tmp_path, data_dir=tmp_path / "data") seed = tmp_path / "wf-seed" _write_workflow_yaml(seed / "a.yaml", name="wfa") workflows = load_combined_workflows(cfg, seed) names = sorted(t.name for (_p, t) in workflows) assert names == ["wfa"] def test_load_combined_workflows_user_overrides_seed(tmp_path: Path) -> None: cfg = load_config(workspace_root=tmp_path, data_dir=tmp_path / "data") seed = tmp_path / "wf-seed" _write_workflow_yaml(seed / "wfa.yaml", name="wfa", version=1) _write_workflow_yaml(user_workflows_dir(cfg) / "wfa.yaml", name="wfa", version=1) workflows = load_combined_workflows(cfg, seed) # Dedupe by (name, version) — only the user version remains. assert len(workflows) == 1 path, tpl = workflows[0] assert tpl.name == "wfa" assert path.parent == user_workflows_dir(cfg) def test_load_combined_workflows_user_adds_distinct(tmp_path: Path) -> None: cfg = load_config(workspace_root=tmp_path, data_dir=tmp_path / "data") seed = tmp_path / "wf-seed" _write_workflow_yaml(seed / "a.yaml", name="wfa") _write_workflow_yaml(user_workflows_dir(cfg) / "user.yaml", name="userwf") workflows = load_combined_workflows(cfg, seed) names = sorted(t.name for (_p, t) in workflows) assert names == ["userwf", "wfa"] def test_load_combined_workflows_skips_malformed(tmp_path: Path) -> None: cfg = load_config(workspace_root=tmp_path, data_dir=tmp_path / "data") seed = tmp_path / "wf-seed" _write_workflow_yaml(seed / "a.yaml", name="wfa") bad = user_workflows_dir(cfg) / "broken.yaml" bad.parent.mkdir(parents=True, exist_ok=True) bad.write_text("not: a workflow:::", encoding="utf-8") workflows = load_combined_workflows(cfg, seed) assert any(t.name == "wfa" for (_p, t) in workflows)