Files
aios/tests/task_plane/test_schedulability_r2.py
T
AIOS Governed Continuation 7e1649808d
autonomy-smoke-tests autonomy smoke tests PASS (A18-A23 staged-run authorized by Owner; G7 staging checks green)
autonomy-unit-tests R2 290/0 + G7 66/0 unit suites PASS
campaign-approval-gate OWNER APPROVAL — campaign approval gate PASS (Owner-authorized agent)
git-diff-check git diff --check clean (only patch-content artifact)
py-compile py_compile 71/71 PASS
reconciliation-contract reconciliation contract consistent (no drops, ChangeEnvelope verified)
release-state-machine release state machine advances to STAGING (authorized by Owner)
secret-scan secret scan 0 secrets PASS
signer-validator-contract signer-validator contract PASS (Owner-authorized)
feat(r2+g5): R2+R2.1 unified platform convergence candidate + G5 import/execution-mode fixes
R2+R2.1: task_plane lineage/auto-claim/autonomous-next-child/mission/legacy-attachment,
task_lineage + hooks/integration/taskplane-adapter, terminal proof, mission authority server,
job controller/admission live adapters (53 new + 7 modified).
G5 dependency/import closure: tests/conftest.py inject active-runtime root (app namespace)
alongside active-runtime/app (legacy aios_* imports); fix 3 real NameErrors in
test_autonomous_next_child.py (mod-qualified refs).
Working-tree subset 290 passed / 0 failed in supported run; overlay/stub + S3 live-binding pending env.
2026-09-14 22:33:41 +08:00

406 lines
14 KiB
Python

from __future__ import annotations
import pytest
from app.task_plane.schedulability import (
ClaimConditions,
MemoryProofRepository,
SchedulabilityChanged,
SchedulabilityService,
)
from app.task_plane.auto_claim import AutoClaimCoordinator
NOW = 2_000_000_000.0
ACTOR = "principal:owner"
WORKER = "worker-a"
TASK = "task-a"
class Authority:
def __init__(self):
self.task = {
"task_id": TASK,
"revision": 7,
"generation": 3,
"status": "QUEUED",
"required_capabilities": ["task.execute", "writer.lease"],
"priority": 9,
"created_at": "2026-09-14T00:00:00Z",
}
self.worker = {
"worker_id": WORKER,
"registration_revision": 11,
"identity": "worker-principal-a",
"status": "ACTIVE",
"disabled": False,
"last_heartbeat": NOW - 5,
"heartbeat_interval_seconds": 10,
"capabilities": ["task.execute", "writer.lease"],
"max_concurrency": 2,
}
self.auth = {
"allowed": True,
"authorization_ref": "authz-1",
"revision": "4",
}
self.scope_ok = True
self.scope_digest = "scope:" + "a" * 32
self.active_claims = 0
self.deps = True
self.freeze = False
self.lease = False
self.claim_calls = []
self.candidates = [self.task]
self.before_atomic_claim = None
self._fence = 40
def read_task(self, task_id):
if task_id != self.task["task_id"]:
raise KeyError(task_id)
return dict(self.task)
def list_candidate_tasks(self):
return [dict(v) for v in self.candidates]
def read_worker(self, worker_id):
if worker_id != self.worker["worker_id"]:
raise KeyError(worker_id)
return dict(self.worker)
def read_authorization(self, **_kwargs):
return dict(self.auth)
def active_claim_count(self, worker_id):
assert worker_id == WORKER
return self.active_claims
def dependencies_satisfied(self, task):
assert task.get("task_id")
return self.deps
def freeze_blocked(self, task):
assert task.get("task_id")
return self.freeze
def conflicting_lease(self, task_id):
assert task_id
return self.lease
def exact_scope_digest(self, **_kwargs):
return self.scope_ok, self.scope_digest
def claim_atomically(
self,
*,
task_id,
worker_id,
actor_identity,
expected: ClaimConditions,
lease_seconds,
schedulability_proof_digest,
):
if self.before_atomic_claim:
callback = self.before_atomic_claim
self.before_atomic_claim = None
callback(self)
# This fake models the *contract* required of the real TaskPlane claim
# transaction. It does not prove a production adapter is atomic.
task = self.read_task(task_id)
worker = self.read_worker(worker_id)
auth = self.read_authorization(task=task, worker=worker, actor_identity=actor_identity)
scope_ok, scope_digest = self.exact_scope_digest(
task=task, worker=worker, actor_identity=actor_identity
)
checks = [
(task["revision"] == expected.task_revision, "task revision"),
(task["generation"] == expected.task_generation, "task generation"),
(task["status"] == expected.task_status and task["status"] in {"CREATED","QUEUED","FAILED_RECOVERABLE"}, "task status"),
(worker["registration_revision"] == expected.worker_generation, "worker generation"),
(worker["identity"] == expected.worker_identity, "worker identity"),
(worker["status"] == "ACTIVE" and not worker["disabled"], "worker state"),
(auth["allowed"] is True, "authorization allowed"),
(auth["authorization_ref"] == expected.authorization_ref, "authorization ref"),
(str(auth["revision"]) == expected.authorization_revision, "authorization revision"),
(scope_ok and scope_digest == expected.scope_digest, "scope"),
(self.active_claims < expected.capacity_limit, "capacity"),
(self.deps, "dependencies"),
(not self.freeze, "freeze"),
(not self.lease, "lease"),
]
failed = [name for ok, name in checks if not ok]
if failed:
raise SchedulabilityChanged("atomic claim condition changed: " + ",".join(failed))
self.active_claims += 1
self._fence += 1
call = {
"task_id": task_id,
"worker_id": worker_id,
"actor_identity": actor_identity,
"expected": expected,
"lease_seconds": lease_seconds,
"schedulability_proof_digest": schedulability_proof_digest,
}
self.claim_calls.append(call)
return {
"status": "CLAIMED",
"claim_id": f"claim-{len(self.claim_calls)}",
"lease_id": f"lease-{len(self.claim_calls)}",
"fencing_generation": self._fence,
"receipt_revision": task["revision"] + 1,
}
@pytest.fixture()
def env():
authority = Authority()
proofs = MemoryProofRepository()
service = SchedulabilityService(authority, proofs, clock=lambda: NOW, proof_ttl_seconds=30)
return authority, proofs, service
def codes(proof):
return {b.code for b in proof.blockers}
def test_a01_preflight_aggregates_multiple_blockers(env):
authority, _, service = env
authority.worker["status"] = "INACTIVE"
authority.worker["capabilities"] = []
authority.scope_ok = False
authority.auth["allowed"] = False
authority.active_claims = 2
authority.deps = False
authority.freeze = True
authority.lease = True
proof = service.preflight(task_id=TASK, worker_id=WORKER, actor_identity=ACTOR)
assert codes(proof) == {
"STALE_OR_INACTIVE_WORKER", "MISSING_CAPABILITY", "EXACT_SCOPE_MISMATCH",
"AUTHORIZATION_BLOCKED", "CAPACITY_EXHAUSTED", "DEPENDENCY_BLOCKED",
"FREEZE_BLOCKED", "CONFLICTING_LEASE",
}
assert not proof.claimable
def test_a02_healthy_worker_is_claimable_and_proof_is_non_bearer(env):
_, proofs, service = env
proof = service.preflight(
task_id=TASK, worker_id=WORKER, actor_identity=ACTOR, persist=False
)
assert proof.claimable
assert proofs.get(proof.proof_id) is None
def test_a03_explicit_zero_capacity_is_exhausted(env):
authority, _, service = env
authority.worker["max_concurrency"] = 0
proof = service.preflight(task_id=TASK, worker_id=WORKER, actor_identity=ACTOR)
assert proof.observation.capacity_limit == 0
assert "CAPACITY_EXHAUSTED" in codes(proof)
assert not proof.claimable
@pytest.mark.parametrize("value", [-1, True, "2", 1.5])
def test_invalid_capacity_is_structured_blocker(env, value):
authority, _, service = env
authority.worker["max_concurrency"] = value
proof = service.preflight(task_id=TASK, worker_id=WORKER, actor_identity=ACTOR)
assert "INVALID_AUTHORITY_VALUE" in codes(proof)
assert not proof.claimable
@pytest.mark.parametrize("status", ["COMPLETED", "SUCCEEDED", "FAILED_FINAL", "CANCELLED", "RUNNING"])
def test_terminal_or_running_task_is_not_claimable(env, status):
authority, _, service = env
authority.task["status"] = status
proof = service.preflight(task_id=TASK, worker_id=WORKER, actor_identity=ACTOR)
assert "TASK_STATE_NOT_CLAIMABLE" in codes(proof)
assert not proof.claimable
@pytest.mark.parametrize(
("mutation", "expected"),
[
(lambda a: a.worker.__setitem__("last_heartbeat", NOW - 999), "STALE_OR_INACTIVE_WORKER"),
(lambda a: a.worker.__setitem__("capabilities", ["task.execute"]), "MISSING_CAPABILITY"),
(lambda a: setattr(a, "scope_ok", False), "EXACT_SCOPE_MISMATCH"),
(lambda a: a.auth.__setitem__("allowed", False), "AUTHORIZATION_BLOCKED"),
(lambda a: setattr(a, "active_claims", 2), "CAPACITY_EXHAUSTED"),
(lambda a: setattr(a, "deps", False), "DEPENDENCY_BLOCKED"),
(lambda a: setattr(a, "freeze", True), "FREEZE_BLOCKED"),
(lambda a: setattr(a, "lease", True), "CONFLICTING_LEASE"),
],
)
def test_preflight_specific_blockers(env, mutation, expected):
authority, _, service = env
mutation(authority)
proof = service.preflight(task_id=TASK, worker_id=WORKER, actor_identity=ACTOR)
assert expected in codes(proof)
assert not proof.claimable
def test_cross_actor_proof_replay_is_rejected(env):
_, _, service = env
proof = service.preflight(
task_id=TASK, worker_id=WORKER, actor_identity=ACTOR, persist=True
)
with pytest.raises(SchedulabilityChanged, match="identity mismatch"):
service.claim_with_recheck(
proof_id=proof.proof_id, worker_id=WORKER, actor_identity="other"
)
def test_cross_worker_proof_replay_is_rejected(env):
_, _, service = env
proof = service.preflight(
task_id=TASK, worker_id=WORKER, actor_identity=ACTOR, persist=True
)
with pytest.raises(SchedulabilityChanged, match="identity mismatch"):
service.claim_with_recheck(
proof_id=proof.proof_id, worker_id="worker-b", actor_identity=ACTOR
)
def test_expired_proof_is_rejected(env):
authority, proofs, _ = env
clock = [NOW]
service = SchedulabilityService(authority, proofs, clock=lambda: clock[0], proof_ttl_seconds=1)
proof = service.preflight(
task_id=TASK, worker_id=WORKER, actor_identity=ACTOR, persist=True
)
clock[0] += 2
with pytest.raises(SchedulabilityChanged, match="expired"):
service.claim_with_recheck(
proof_id=proof.proof_id, worker_id=WORKER, actor_identity=ACTOR
)
@pytest.mark.parametrize(
"mutation",
[
lambda a: a.worker.__setitem__("registration_revision", 12),
lambda a: a.task.__setitem__("revision", 8),
lambda a: a.task.__setitem__("generation", 4),
lambda a: a.auth.__setitem__("revision", "5"),
lambda a: setattr(a, "active_claims", 1),
lambda a: setattr(a, "scope_digest", "scope:" + "b" * 32),
lambda a: a.task.__setitem__("status", "COMPLETED"),
],
)
def test_claim_time_stable_authority_change_invalidates_proof(env, mutation):
authority, _, service = env
proof = service.preflight(
task_id=TASK, worker_id=WORKER, actor_identity=ACTOR, persist=True
)
mutation(authority)
with pytest.raises(SchedulabilityChanged):
service.claim_with_recheck(
proof_id=proof.proof_id, worker_id=WORKER, actor_identity=ACTOR
)
def test_atomic_linearization_rechecks_capacity_after_read_recheck(env):
authority, _, service = env
authority.worker["max_concurrency"] = 1
proof = service.preflight(
task_id=TASK, worker_id=WORKER, actor_identity=ACTOR, persist=True
)
authority.before_atomic_claim = lambda a: setattr(a, "active_claims", 1)
with pytest.raises(SchedulabilityChanged, match="capacity"):
service.claim_with_recheck(
proof_id=proof.proof_id, worker_id=WORKER, actor_identity=ACTOR
)
assert authority.claim_calls == []
def test_atomic_linearization_rechecks_authorization_after_read_recheck(env):
authority, _, service = env
proof = service.preflight(
task_id=TASK, worker_id=WORKER, actor_identity=ACTOR, persist=True
)
authority.before_atomic_claim = lambda a: a.auth.__setitem__("allowed", False)
with pytest.raises(SchedulabilityChanged, match="authorization"):
service.claim_with_recheck(
proof_id=proof.proof_id, worker_id=WORKER, actor_identity=ACTOR
)
def test_atomic_linearization_rechecks_freeze_after_read_recheck(env):
authority, _, service = env
proof = service.preflight(
task_id=TASK, worker_id=WORKER, actor_identity=ACTOR, persist=True
)
authority.before_atomic_claim = lambda a: setattr(a, "freeze", True)
with pytest.raises(SchedulabilityChanged, match="freeze"):
service.claim_with_recheck(
proof_id=proof.proof_id, worker_id=WORKER, actor_identity=ACTOR
)
def test_normal_claim_requires_lease_and_fence_receipt(env):
authority, _, service = env
proof = service.preflight(
task_id=TASK, worker_id=WORKER, actor_identity=ACTOR, persist=True
)
out = service.claim_with_recheck(
proof_id=proof.proof_id, worker_id=WORKER, actor_identity=ACTOR, lease_seconds=45
)
assert out["status"] == "CLAIMED"
assert out["claim_id"].startswith("claim-")
assert out["lease_id"].startswith("lease-")
assert out["fencing_generation"] > 0
assert out["receipt_revision"] == 8
assert len(authority.claim_calls) == 1
def test_discovery_skips_blocked_high_priority_and_selects_next_claimable(env):
authority, proofs, service = env
blocked = dict(authority.task)
blocked["task_id"] = "blocked"
blocked["priority"] = 100
good = dict(authority.task)
good["task_id"] = TASK
authority.candidates = [blocked, good]
original_read = authority.read_task
def read_task(task_id):
if task_id == "blocked":
value = dict(blocked)
value["required_capabilities"] = ["missing-cap"]
return value
return original_read(task_id)
authority.read_task = read_task
proof = service.discover(worker_id=WORKER, actor_identity=ACTOR)
assert proof is not None
assert proof.task_id == TASK
assert proofs.get(proof.proof_id) is proof
def test_auto_claim_diagnostics_do_not_mask_authorization(env):
authority, _, service = env
coordinator = AutoClaimCoordinator(service)
authority.auth["allowed"] = False
waiting = coordinator.discover(worker_id=WORKER, actor_identity=ACTOR)
assert waiting.status == "WAITING_AUTHORIZATION"
assert "AUTHORIZATION_BLOCKED" in waiting.blocker_codes
def test_auto_claim_reports_no_candidates(env):
authority, _, service = env
authority.candidates = []
out = AutoClaimCoordinator(service).discover(worker_id=WORKER, actor_identity=ACTOR)
assert out.status == "NO_CANDIDATES"
assert out.candidate_count == 0
def test_auto_claim_capacity_wait_is_precise(env):
authority, _, service = env
authority.active_claims = 2
out = AutoClaimCoordinator(service).discover(worker_id=WORKER, actor_identity=ACTOR)
assert out.status == "WAITING_CAPACITY"
assert out.blocker_codes == ("CAPACITY_EXHAUSTED",)