Files
handbook/docker/packages/google_calendar/tests/test_bundle_datadir.py
T
2026-06-24 14:16:58 -07:00

240 lines
8.4 KiB
Python

"""Tests for the nested bundle-input layout: <BUNDLEDIR>/services/<name>/state.json."""
import importlib
import json
import pytest
def _get_state():
return importlib.import_module("google_calendar.state")
# A minimal, valid CalendarState seed. CalendarState has all-default fields, so
# an empty events dict is valid; one event keeps the round-trip meaningful.
SEED = {
"events": {
"evt-1": {
"id": "evt-1",
"summary": "Seeded Event",
"start": {"dateTime": "2025-06-01T10:00:00Z"},
"end": {"dateTime": "2025-06-01T11:00:00Z"},
}
}
}
@pytest.fixture(autouse=True)
def _reset_state():
"""Keep state in-memory and reset the account registry around each test."""
state = _get_state()
state.set_agent_workspace(None)
state._accounts.clear()
state._active_account_id = "default"
yield
state.set_agent_workspace(None)
state._accounts.clear()
state._active_account_id = "default"
def test_resolve_bundle_state_path_prefers_state_json(tmp_path, monkeypatch):
state = _get_state()
service_dir = tmp_path / "services" / "google_calendar"
service_dir.mkdir(parents=True)
state_json = service_dir / "state.json"
state_json.write_text(json.dumps(SEED))
(service_dir / "events.json").write_text(json.dumps(SEED))
monkeypatch.setenv("BUNDLEDIR", str(tmp_path))
assert state.resolve_bundle_state_path() == state_json
def test_resolve_bundle_state_path_globs_when_no_state_json(tmp_path, monkeypatch):
state = _get_state()
service_dir = tmp_path / "services" / "google_calendar"
service_dir.mkdir(parents=True)
a_json = service_dir / "a.json"
a_json.write_text(json.dumps(SEED))
(service_dir / "b.json").write_text(json.dumps(SEED))
monkeypatch.setenv("BUNDLEDIR", str(tmp_path))
assert state.resolve_bundle_state_path() == a_json
def test_resolve_bundle_state_path_missing_subdir(tmp_path, monkeypatch):
state = _get_state()
(tmp_path / "services").mkdir()
monkeypatch.setenv("BUNDLEDIR", str(tmp_path))
assert state.resolve_bundle_state_path() is None
monkeypatch.delenv("BUNDLEDIR", raising=False)
assert state.resolve_bundle_state_path() is None
def test_resolve_bundle_output_path(tmp_path, monkeypatch):
state = _get_state()
output_dir = tmp_path / "services" / "google_calendar"
monkeypatch.setenv("BUNDLE_OUTPUT_DIR", str(output_dir))
assert state.resolve_bundle_output_path() == output_dir / "state.json"
monkeypatch.delenv("BUNDLE_OUTPUT_DIR", raising=False)
assert state.resolve_bundle_output_path() is None
def test_bundle_state_json_matches_inputdir(tmp_path, monkeypatch):
state = _get_state()
bundle_dir = tmp_path / "bundle"
service_dir = bundle_dir / "services" / "google_calendar"
service_dir.mkdir(parents=True)
(service_dir / "state.json").write_text(json.dumps(SEED))
inputdir = tmp_path / "input"
inputdir.mkdir()
(inputdir / "calendar.json").write_text(json.dumps(SEED))
# Load from the nested bundle layout.
state.set_agent_workspace(None)
state._accounts.clear()
state._active_account_id = "default"
monkeypatch.setenv("BUNDLEDIR", str(bundle_dir))
monkeypatch.delenv("INPUTDIR", raising=False)
state.load_seed_state_from_env()
bundle_state = state.state_to_json()
# Reset and load the same seed from the INPUTDIR fallback.
state._accounts.clear()
state._active_account_id = "default"
state.set_agent_workspace(None)
monkeypatch.delenv("BUNDLEDIR", raising=False)
monkeypatch.setenv("INPUTDIR", str(inputdir))
state.load_seed_state_from_env()
inputdir_state = state.state_to_json()
assert bundle_state == inputdir_state
# Two distinguishable accounts: different seeded event so a swap would be caught.
ACCT_A = {
"events": {
"evt-a": {
"id": "evt-a",
"summary": "Account A Event",
"start": {"dateTime": "2025-06-01T10:00:00Z"},
"end": {"dateTime": "2025-06-01T11:00:00Z"},
}
}
}
ACCT_B = {
"events": {
"evt-b": {
"id": "evt-b",
"summary": "Account B Event",
"start": {"dateTime": "2025-07-02T14:00:00Z"},
"end": {"dateTime": "2025-07-02T15:00:00Z"},
}
}
}
def _load_from_bundle(state, monkeypatch, bundle_dir):
"""Reset the registry and load seed state from *bundle_dir* in isolation."""
state.set_agent_workspace(None)
state._accounts.clear()
state._active_account_id = "default"
monkeypatch.setenv("BUNDLEDIR", str(bundle_dir))
monkeypatch.delenv("INPUTDIR", raising=False)
monkeypatch.delenv("OUTPUTDIR", raising=False)
monkeypatch.delenv("BUNDLE_OUTPUT_DIR", raising=False)
state.load_seed_state_from_env()
return state.state_to_json()
def test_bundle_multifile_folder_matches_consolidated_state(tmp_path, monkeypatch):
"""A multi-file bundle folder coalesces to the same state as a single
consolidated state.json with the same accounts."""
state = _get_state()
# (a) Consolidated: one state.json with both accounts.
consolidated = tmp_path / "consolidated"
consolidated_dir = consolidated / "services" / "google_calendar"
consolidated_dir.mkdir(parents=True)
(consolidated_dir / "state.json").write_text(json.dumps({"accounts": {"default": ACCT_A, "work": ACCT_B}}))
# (b) Split: two wrapper files, no state.json.
split = tmp_path / "split"
split_dir = split / "services" / "google_calendar"
split_dir.mkdir(parents=True)
(split_dir / "a.json").write_text(json.dumps({"accounts": {"default": ACCT_A}}))
(split_dir / "b.json").write_text(json.dumps({"accounts": {"work": ACCT_B}}))
consolidated_state = _load_from_bundle(state, monkeypatch, consolidated)
split_state = _load_from_bundle(state, monkeypatch, split)
assert consolidated_state == split_state
assert set(consolidated_state["accounts"]) == {"default", "work"}
assert set(split_state["accounts"]) == {"default", "work"}
def test_resolve_bundle_state_paths_returns_whole_folder(tmp_path, monkeypatch):
"""The plural resolver returns ALL sorted *.json when there's no state.json,
and exactly [state.json] when one is present."""
state = _get_state()
service_dir = tmp_path / "services" / "google_calendar"
service_dir.mkdir(parents=True)
a_json = service_dir / "a.json"
b_json = service_dir / "b.json"
a_json.write_text(json.dumps({"accounts": {"default": ACCT_A}}))
b_json.write_text(json.dumps({"accounts": {"work": ACCT_B}}))
monkeypatch.setenv("BUNDLEDIR", str(tmp_path))
assert state.resolve_bundle_state_paths() == [a_json, b_json]
state_json = service_dir / "state.json"
state_json.write_text(json.dumps({"accounts": {"default": ACCT_A}}))
assert state.resolve_bundle_state_paths() == [state_json]
def test_bundle_flat_files_merge_into_one_account(tmp_path, monkeypatch):
"""the raw entities layout splits ONE account across per-entity files
(e.g. events.json + calendars.json, no {accounts} wrapper). Flat files must
merge into a single default account, not fragment into a phantom account
per file that the server never activates."""
state = _get_state()
service_dir = tmp_path / "bundle" / "services" / "google_calendar"
service_dir.mkdir(parents=True)
# One account split into its two collection halves.
(service_dir / "events.json").write_text(json.dumps({"events": ACCT_A["events"]}))
(service_dir / "calendars.json").write_text(
json.dumps(
{
"calendars": {
"team": {
"summary": "Team",
"events": {
"evt-team": {
"id": "evt-team",
"summary": "Team Event",
"start": {"dateTime": "2025-08-03T09:00:00Z"},
"end": {"dateTime": "2025-08-03T10:00:00Z"},
}
},
}
}
}
)
)
merged = _load_from_bundle(state, monkeypatch, tmp_path / "bundle")
# Single (default) account holding BOTH halves — events and the calendar.
assert "accounts" not in merged, "flat per-entity files must merge into ONE account"
assert "evt-a" in merged["events"]
assert "team" in merged["calendars"]