Files
aios/tests/test_canonical_task_projection_recovery.py
T

261 lines
8.6 KiB
Python

from __future__ import annotations
import json
from pathlib import Path
import sqlite3
import sys
import pytest
ROOT = Path(__file__).resolve().parents[1]
sys.path.insert(0, str(ROOT / "active-runtime" / "app"))
from aios_agent_task_projection_recovery import (
CanonicalTaskProjectionIntent,
CanonicalTaskProjectionStore,
ProjectionRecoveryError,
ProjectionState,
sha256_json,
)
from aios_agent_task_projection_recovery_http import recover_from_json
TASK_ID = "TASK-CONV-V2R2C2-RECONCILE-SNAPSHOT-20260826"
JOB_ID = "job-9eee1142f2fc8bb7b571d065c01f40e0"
OID = "2afc2cf41fc9090cf01e3882d1b8db8d54cf1844"
RUNBOOK_SHA = "a500504a76fb99b26541eaf4edd842a942992ae170fc0f3838530189ca672320"
def make_db(tmp_path: Path) -> Path:
path = tmp_path / "state.sqlite3"
con = sqlite3.connect(path)
con.executescript(
"""
CREATE TABLE tasks(
task_id TEXT PRIMARY KEY,
goal TEXT NOT NULL,
instructions TEXT NOT NULL,
success_criteria_json TEXT NOT NULL,
required_capabilities_json TEXT NOT NULL,
constraints_json TEXT NOT NULL,
metadata_json TEXT NOT NULL,
deadline TEXT,
priority INTEGER NOT NULL,
max_attempts INTEGER NOT NULL,
attempt INTEGER NOT NULL DEFAULT 0,
status TEXT NOT NULL,
claimed_by TEXT,
lease_token_hash TEXT,
lease_expires_at TEXT,
result_json TEXT,
result_digest TEXT,
result_idempotency_key TEXT,
verification_json TEXT,
revision INTEGER NOT NULL DEFAULT 1,
created_at TEXT NOT NULL,
updated_at TEXT NOT NULL
);
CREATE TABLE events(
event_id INTEGER PRIMARY KEY AUTOINCREMENT,
task_id TEXT,
agent_id TEXT,
event_type TEXT NOT NULL,
payload_json TEXT NOT NULL,
created_at TEXT NOT NULL
);
"""
)
con.commit()
con.close()
return path
def manifest() -> dict:
task = {
"task_id": TASK_ID,
"goal": "Complete V2R2C2 staging convergence",
"instructions": "Resume the immutable runbook and close the parent goal.",
"success_criteria": [{"id": "closure", "required": True, "expected": "PASS"}],
"constraints": {
"staging_only": True,
"production_write": False,
"main_direct_write": False,
},
"required_capabilities": [],
"metadata": {
"governed_task_id": TASK_ID,
"bridge_job_id": JOB_ID,
"runbook_sha256": RUNBOOK_SHA,
},
"priority": 100,
"max_attempts": 20,
"deadline": None,
}
return {
"schema": "aios.canonical-task-projection-intent.v1",
"projection_id": "projection-v2r2c2-parent-recovery-20260826",
"projection_generation": 1,
"idempotency_key": "projection-v2r2c2-parent-recovery-20260826-v1",
"task": task,
"governed_binding": {
"governed_task_id": TASK_ID,
"bridge_job_id": JOB_ID,
"repository": "aios/aios",
"branch": "agent/v2r2c2-convergence-integration-20260825",
"generation": 2,
"base_oid": OID,
"expected_branch_oid": OID,
},
"source_artifact": {
"ref": f"git:{OID}:runbooks/agent-v2r2c2-control-plane-convergence-20260825.md",
"sha256": RUNBOOK_SHA,
},
"task_payload_sha256": sha256_json(task),
}
def test_missing_task_is_recovered_with_same_identity(tmp_path):
db = make_db(tmp_path)
intent = CanonicalTaskProjectionIntent.from_mapping(manifest())
result = CanonicalTaskProjectionStore(db).reconcile(intent)
assert result["state"] == ProjectionState.CONVERGED.value
assert result["canonical_task_id"] == TASK_ID
assert result["bridge_job_id"] == JOB_ID
assert result["created_new_row"] is True
con = sqlite3.connect(db)
row = con.execute(
"SELECT task_id,status,attempt,revision,claimed_by,"
"required_capabilities_json,metadata_json FROM tasks WHERE task_id=?",
(TASK_ID,),
).fetchone()
assert row[:5] == (TASK_ID, "QUEUED", 0, 1, None)
assert json.loads(row[5]) == []
metadata = json.loads(row[6])
assert "execution" not in metadata
assert "execution_projection" not in metadata
events = con.execute(
"SELECT event_type FROM events WHERE task_id=? ORDER BY event_id",
(TASK_ID,),
).fetchall()
assert events == [("TASK_CREATED",)]
def test_replay_is_idempotent_and_creates_no_duplicate_event(tmp_path):
db = make_db(tmp_path)
intent = CanonicalTaskProjectionIntent.from_mapping(manifest())
store = CanonicalTaskProjectionStore(db)
first = store.reconcile(intent)
second = store.reconcile(intent)
assert first["idempotent"] is False
assert second["idempotent"] is True
con = sqlite3.connect(db)
assert con.execute("SELECT COUNT(*) FROM tasks").fetchone()[0] == 1
assert con.execute("SELECT COUNT(*) FROM events").fetchone()[0] == 1
assert con.execute(
"SELECT COUNT(*) FROM task_projection_receipts"
).fetchone()[0] == 1
def test_existing_matching_row_converges_without_second_task_created_event(tmp_path):
db = make_db(tmp_path)
intent = CanonicalTaskProjectionIntent.from_mapping(manifest())
store = CanonicalTaskProjectionStore(db)
store.reconcile(intent)
con = sqlite3.connect(db)
con.execute("DELETE FROM task_projection_receipts")
con.execute("DELETE FROM task_projection_intents")
con.commit()
con.close()
result = store.reconcile(intent)
assert result["created_new_row"] is False
con = sqlite3.connect(db)
assert con.execute("SELECT COUNT(*) FROM tasks").fetchone()[0] == 1
assert con.execute("SELECT COUNT(*) FROM events").fetchone()[0] == 1
def test_existing_different_task_payload_fails_closed(tmp_path):
db = make_db(tmp_path)
intent = CanonicalTaskProjectionIntent.from_mapping(manifest())
store = CanonicalTaskProjectionStore(db)
store.reconcile(intent)
con = sqlite3.connect(db)
con.execute("DELETE FROM task_projection_receipts")
con.execute("DELETE FROM task_projection_intents")
con.execute("UPDATE tasks SET goal='drifted'")
con.commit()
con.close()
with pytest.raises(
ProjectionRecoveryError,
match="CANONICAL_TASK_PROJECTION_IDENTITY_CONFLICT",
):
store.reconcile(intent)
def test_intent_replay_with_same_generation_but_different_request_conflicts(tmp_path):
db = make_db(tmp_path)
first = manifest()
intent = CanonicalTaskProjectionIntent.from_mapping(first)
store = CanonicalTaskProjectionStore(db)
store.persist_intent(intent)
other = manifest()
other["task"]["instructions"] = "different"
other["task_payload_sha256"] = sha256_json(other["task"])
other_intent = CanonicalTaskProjectionIntent.from_mapping(other)
with pytest.raises(
ProjectionRecoveryError,
match="PROJECTION_INTENT_IDEMPOTENCY_CONFLICT",
):
store.persist_intent(other_intent)
def test_recovery_refuses_non_pristine_execution_metadata(tmp_path):
value = manifest()
value["task"]["metadata"]["execution"] = {"action": "agent_runtime"}
value["task_payload_sha256"] = sha256_json(value["task"])
with pytest.raises(
ProjectionRecoveryError,
match="TASK_RECOVERY_REQUIRES_PRISTINE_METADATA",
):
CanonicalTaskProjectionIntent.from_mapping(value)
def test_recovery_refuses_runtime_capability_in_pristine_row(tmp_path):
value = manifest()
value["task"]["required_capabilities"] = ["agent.runtime.openclaw"]
value["task_payload_sha256"] = sha256_json(value["task"])
with pytest.raises(
ProjectionRecoveryError,
match="TASK_RECOVERY_REQUIRES_PRISTINE_CAPABILITIES",
):
CanonicalTaskProjectionIntent.from_mapping(value)
def test_http_adapter_rejects_path_body_task_mismatch(tmp_path):
db = make_db(tmp_path)
status, body = recover_from_json(db, "TASK-other-123", manifest())
assert status == 409
assert body["error"] == "PROJECTION_TASK_IDENTITY_MISMATCH"
def test_projection_readback_contains_durable_mapping_and_receipt(tmp_path):
db = make_db(tmp_path)
intent = CanonicalTaskProjectionIntent.from_mapping(manifest())
store = CanonicalTaskProjectionStore(db)
store.reconcile(intent)
projection = store.get_projection(TASK_ID)
assert projection is not None
assert projection["task_id"] == TASK_ID
assert projection["bridge_job_id"] == JOB_ID
assert projection["state"] == "CONVERGED"
assert projection["receipt"]["canonical_task_id"] == TASK_ID