from __future__ import annotations import json from pathlib import Path import jira_mock.state as state_mod from jira_mock.state import ( load_state, resolve_bundle_output_path, resolve_bundle_state_path, resolve_bundle_state_paths, set_agent_workspace, state_to_json, ) def _flat_site(account_id: str) -> dict: """A minimal valid flat single-site seed keyed on a distinct user.""" return { "currentUserAccountId": account_id, "users": { account_id: { "accountId": account_id, "accountType": "atlassian", "emailAddress": f"{account_id}@example.com", "displayName": f"User {account_id}", "active": True, "timeZone": "America/New_York", } }, } def test_resolve_bundle_state_path_prefers_state_json(tmp_path: Path, monkeypatch) -> None: """state.json wins over a bare *.json sibling in the per-service subdir.""" service_dir = tmp_path / "services" / "jira" service_dir.mkdir(parents=True) (service_dir / "state.json").write_text("{}", encoding="utf-8") (service_dir / "issues.json").write_text("{}", encoding="utf-8") monkeypatch.setenv("BUNDLEDIR", str(tmp_path)) assert resolve_bundle_state_path() == service_dir / "state.json" def test_resolve_bundle_state_path_globs_when_no_state_json(tmp_path: Path, monkeypatch) -> None: """The singular back-compat accessor returns the first *.json when there's no state.json. (The loader itself reads the whole folder — see resolve_bundle_state_paths.)""" service_dir = tmp_path / "services" / "jira" service_dir.mkdir(parents=True) (service_dir / "b.json").write_text("{}", encoding="utf-8") (service_dir / "a.json").write_text("{}", encoding="utf-8") monkeypatch.setenv("BUNDLEDIR", str(tmp_path)) assert resolve_bundle_state_path() == service_dir / "a.json" def test_resolve_bundle_state_path_missing_subdir(tmp_path: Path, monkeypatch) -> None: """A partial bundle without this service's subdir resolves to None so the loader falls back to INPUTDIR.""" (tmp_path / "services").mkdir() monkeypatch.setenv("BUNDLEDIR", str(tmp_path)) assert resolve_bundle_state_path() is None monkeypatch.delenv("BUNDLEDIR") assert resolve_bundle_state_path() is None def test_resolve_bundle_output_path(monkeypatch) -> None: monkeypatch.setenv("BUNDLE_OUTPUT_DIR", "/some/output/services/jira") assert resolve_bundle_output_path() == Path("/some/output/services/jira/state.json") monkeypatch.delenv("BUNDLE_OUTPUT_DIR") assert resolve_bundle_output_path() is None def _reset_loader(workspace: Path) -> None: """Force a clean reload from env the way load_state() guards on globals.""" state_mod._current_state = None state_mod._sites.clear() set_agent_workspace(str(workspace / "agent_workspace")) def test_bundle_state_json_matches_inputdir(tmp_path: Path, monkeypatch) -> None: """Loading the same seed from /services/jira/state.json yields the same canonical state as loading it from INPUTDIR.""" for var in ("BUNDLEDIR", "INPUTDIR", "OUTPUTDIR", "BUNDLE_OUTPUT_DIR"): monkeypatch.delenv(var, raising=False) seed = { "currentUserAccountId": "user-1", "users": { "user-1": { "accountId": "user-1", "accountType": "atlassian", "emailAddress": "user-1@example.com", "displayName": "User 1", "active": True, "timeZone": "America/New_York", } }, } seed_text = json.dumps(seed) bundle_dir = tmp_path / "bundle" bundle_state = bundle_dir / "services" / "jira" / "state.json" bundle_state.parent.mkdir(parents=True) bundle_state.write_text(seed_text, encoding="utf-8") input_dir = tmp_path / "input" input_dir.mkdir() (input_dir / "jira.json").write_text(seed_text, encoding="utf-8") # Load from bundle (INPUTDIR unset). _reset_loader(tmp_path / "bundle_ws") monkeypatch.setenv("BUNDLEDIR", str(bundle_dir)) monkeypatch.delenv("INPUTDIR", raising=False) load_state() from_bundle = state_to_json() # Load from INPUTDIR (BUNDLEDIR unset). _reset_loader(tmp_path / "input_ws") monkeypatch.delenv("BUNDLEDIR", raising=False) monkeypatch.setenv("INPUTDIR", str(input_dir)) load_state() from_inputdir = state_to_json() assert from_bundle == from_inputdir def test_resolve_bundle_state_paths_returns_whole_folder(tmp_path: Path, monkeypatch) -> None: """The plural resolver returns ALL sorted *.json when there's no state.json, and just [state.json] when one is present.""" service_dir = tmp_path / "services" / "jira" service_dir.mkdir(parents=True) (service_dir / "b.json").write_text("{}", encoding="utf-8") (service_dir / "a.json").write_text("{}", encoding="utf-8") monkeypatch.setenv("BUNDLEDIR", str(tmp_path)) assert resolve_bundle_state_paths() == [service_dir / "a.json", service_dir / "b.json"] (service_dir / "state.json").write_text("{}", encoding="utf-8") assert resolve_bundle_state_paths() == [service_dir / "state.json"] def test_bundle_multifile_folder_matches_consolidated_state(tmp_path: Path, monkeypatch) -> None: """A split multi-file bundle folder coalesces to the same canonical state as a single consolidated multi-site state.json.""" for var in ("BUNDLEDIR", "INPUTDIR", "OUTPUTDIR", "BUNDLE_OUTPUT_DIR"): monkeypatch.delenv(var, raising=False) site_a = _flat_site("user-a") site_b = _flat_site("user-b") # Make site B distinguishable beyond its current user via an extra user. site_b["users"]["user-b-extra"] = { "accountId": "user-b-extra", "accountType": "atlassian", "emailAddress": "user-b-extra@example.com", "displayName": "User B Extra", "active": True, "timeZone": "America/New_York", } # (a) consolidated: one state.json with both sites. consolidated = tmp_path / "consolidated" consolidated_state = consolidated / "services" / "jira" / "state.json" consolidated_state.parent.mkdir(parents=True) consolidated_state.write_text( json.dumps({"sites": {"default": site_a, "extra": site_b}}), encoding="utf-8", ) # (b) split: two {sites} wrapper files, no state.json — two distinct named # sites across files (vs. the flat-file case, which merges into one site). split = tmp_path / "split" split_dir = split / "services" / "jira" split_dir.mkdir(parents=True) (split_dir / "a.json").write_text(json.dumps({"sites": {"default": site_a}}), encoding="utf-8") (split_dir / "b.json").write_text(json.dumps({"sites": {"extra": site_b}}), encoding="utf-8") # Load consolidated. _reset_loader(tmp_path / "consolidated_ws") monkeypatch.setenv("BUNDLEDIR", str(consolidated)) load_state() from_consolidated = state_to_json() # Load split. _reset_loader(tmp_path / "split_ws") monkeypatch.setenv("BUNDLEDIR", str(split)) load_state() from_split = state_to_json() assert from_consolidated == from_split assert set(from_consolidated["sites"]) == {"default", "extra"} assert set(from_split["sites"]) == {"default", "extra"} def test_bundle_flat_files_merge_into_one_site(tmp_path: Path, monkeypatch) -> None: """the raw entities layout splits ONE site across per-entity files (no {sites} wrapper). Flat files must merge into a single default site, not fragment into a phantom site per file the server never activates.""" for var in ("BUNDLEDIR", "INPUTDIR", "OUTPUTDIR", "BUNDLE_OUTPUT_DIR"): monkeypatch.delenv(var, raising=False) service_dir = tmp_path / "bundle" / "services" / "jira" service_dir.mkdir(parents=True) # Two halves of the SAME site: identity/users in one file, an extra user in # another. Both must land in the single active (default) site. (service_dir / "a_users.json").write_text(json.dumps(_flat_site("user-a")), encoding="utf-8") (service_dir / "b_users.json").write_text( json.dumps( { "users": { "user-b": { "accountId": "user-b", "accountType": "atlassian", "emailAddress": "user-b@example.com", "displayName": "User B", "active": True, "timeZone": "America/New_York", } } } ), encoding="utf-8", ) _reset_loader(tmp_path / "flat_ws") monkeypatch.setenv("BUNDLEDIR", str(tmp_path / "bundle")) load_state() merged = state_to_json() # One site, and both per-file users present in it — nothing fragmented away. assert set(merged.get("sites", {"default": merged}).keys()) == {"default"} or "users" in merged state = merged["sites"]["default"] if "sites" in merged else merged assert "user-a" in state["users"] assert "user-b" in state["users"]