210 lines
5.9 KiB
Python
210 lines
5.9 KiB
Python
from __future__ import annotations
|
|
|
|
from pathlib import Path
|
|
import sys
|
|
|
|
import pytest
|
|
|
|
ROOT=Path(__file__).resolve().parents[1]
|
|
sys.path.insert(0,str(ROOT/"active-runtime/app"))
|
|
|
|
import aios_online_agent_worker as worker
|
|
from aios_online_agent_execution import (
|
|
GovernedExecutionBinding,
|
|
project_execution_envelope,
|
|
)
|
|
|
|
|
|
|
|
|
|
@pytest.fixture(autouse=True)
|
|
def _state_paths(monkeypatch, tmp_path):
|
|
root=tmp_path/"worker-state"
|
|
monkeypatch.setattr(worker,"STATE_ROOT",root)
|
|
monkeypatch.setattr(worker,"WORKER_LIVENESS_PATH",root/"worker-liveness.json")
|
|
|
|
BASE="a"*40
|
|
TASK_ID="task-worker-availability-placeholder"
|
|
JOB_ID="job-worker-availability-placeholder"
|
|
BRANCH="agent/task-worker-availability-placeholder"
|
|
IDEMPOTENCY="task-worker-availability-placeholder-binding-v1"
|
|
|
|
|
|
def binding():
|
|
return GovernedExecutionBinding.from_mapping({
|
|
"task_id":TASK_ID,
|
|
"job_id":JOB_ID,
|
|
"repository":"aios/aios",
|
|
"base_oid":BASE,
|
|
"expected_branch_oid":BASE,
|
|
"target_branch":BRANCH,
|
|
"branch_mode":"CREATE_NEW",
|
|
"generation":1,
|
|
"idempotency_key":IDEMPOTENCY,
|
|
})
|
|
|
|
|
|
def placeholder():
|
|
return project_execution_envelope(
|
|
binding(),
|
|
{
|
|
"action":"source_capture",
|
|
"adapter":"source_capture",
|
|
"capability_scope":[
|
|
"git.read",
|
|
"git.test",
|
|
"governed.write.branch",
|
|
"governed.write.file",
|
|
],
|
|
},
|
|
)
|
|
|
|
|
|
def readonly_source_capture():
|
|
return project_execution_envelope(
|
|
binding(),
|
|
{
|
|
"action":"source_capture",
|
|
"adapter":"source_capture",
|
|
"capability_scope":["git.read","git.test"],
|
|
"source_signals":["worker availability"],
|
|
},
|
|
)
|
|
|
|
|
|
def deterministic():
|
|
return project_execution_envelope(
|
|
binding(),
|
|
{
|
|
"action":"deterministic",
|
|
"adapter":"deterministic",
|
|
"capability_scope":[
|
|
"git.read",
|
|
"git.test",
|
|
"governed.write.branch",
|
|
"governed.write.file",
|
|
],
|
|
"change_set":[{
|
|
"path":"active-runtime/app/example.py",
|
|
"operation":"write_file",
|
|
"content":"VALUE=1\\n",
|
|
}],
|
|
"verification":[{
|
|
"label":"unit",
|
|
"argv":["/usr/bin/python3","-m","pytest","-q"],
|
|
"cwd":".",
|
|
"timeout_seconds":120,
|
|
}],
|
|
"governance":{
|
|
"commit_idempotency_key":"task-worker-availability-placeholder-commit-v1",
|
|
"allowed_files":["active-runtime/app/example.py"],
|
|
},
|
|
},
|
|
)
|
|
|
|
|
|
def task(execution):
|
|
return {
|
|
"task_id":TASK_ID,
|
|
"required_capabilities":list(execution["capability_scope"]),
|
|
"metadata":{
|
|
"execution":execution,
|
|
"execution_projection":{
|
|
"schema":"aios.online-agent.task-projection.v1",
|
|
"task_id":TASK_ID,
|
|
"job_id":JOB_ID,
|
|
"execution_digest":execution["execution_digest"],
|
|
},
|
|
},
|
|
}
|
|
|
|
|
|
class FakeBridge:
|
|
def __init__(self,status,execution):
|
|
self.status=status
|
|
self.execution=execution
|
|
self.calls=[]
|
|
|
|
def project_execution(self,task_value,execution=None):
|
|
self.calls.append(execution)
|
|
return self.status,self.execution
|
|
|
|
|
|
class FakeAgent:
|
|
def __init__(self,tasks):
|
|
self.tasks=tasks
|
|
self.claim_calls=[]
|
|
|
|
def poll(self):
|
|
return list(self.tasks)
|
|
|
|
def claim(self,task_id):
|
|
self.claim_calls.append(task_id)
|
|
return 500,{"error":"SHOULD_NOT_CLAIM"}
|
|
|
|
|
|
def supervisor(bridge, agent=None):
|
|
value=object.__new__(worker.WorkerSupervisor)
|
|
value.bridge=bridge
|
|
value.agent=agent
|
|
return value
|
|
|
|
|
|
def test_write_capable_source_capture_is_classified_as_placeholder():
|
|
envelope=worker.ExecutionEnvelope.from_task(
|
|
task(placeholder()),
|
|
worker_capabilities=tuple(placeholder()["capability_scope"]),
|
|
)
|
|
assert worker.source_capture_requires_deterministic_plan(envelope) is True
|
|
|
|
|
|
def test_readonly_source_capture_is_not_placeholder():
|
|
envelope=worker.ExecutionEnvelope.from_task(
|
|
task(readonly_source_capture()),
|
|
worker_capabilities=tuple(readonly_source_capture()["capability_scope"]),
|
|
)
|
|
assert worker.source_capture_requires_deterministic_plan(envelope) is False
|
|
|
|
|
|
def test_placeholder_without_registered_plan_is_skipped_before_claim():
|
|
persisted=placeholder()
|
|
bridge=FakeBridge(200,persisted)
|
|
agent=FakeAgent([task(persisted)])
|
|
sup=supervisor(bridge,agent)
|
|
assert sup.once()==0
|
|
assert bridge.calls==[None]
|
|
assert agent.claim_calls==[]
|
|
|
|
|
|
def test_placeholder_with_missing_projection_is_skipped_before_claim():
|
|
persisted=placeholder()
|
|
bridge=FakeBridge(404,{"error":"TASK_EXECUTION_PROJECTION_MISSING"})
|
|
agent=FakeAgent([task(persisted)])
|
|
sup=supervisor(bridge,agent)
|
|
assert sup.once()==0
|
|
assert bridge.calls==[None]
|
|
assert agent.claim_calls==[]
|
|
|
|
|
|
def test_registered_deterministic_plan_replaces_placeholder():
|
|
persisted=placeholder()
|
|
resolved=deterministic()
|
|
bridge=FakeBridge(200,resolved)
|
|
sup=supervisor(bridge)
|
|
envelope,source=sup.resolve_envelope(task(persisted))
|
|
assert source=="plan_registry_verified"
|
|
assert envelope.action=="deterministic"
|
|
assert envelope.digest==resolved["execution_digest"]
|
|
assert bridge.calls==[None]
|
|
|
|
|
|
def test_real_readonly_source_capture_replays_exact_payload():
|
|
persisted=readonly_source_capture()
|
|
bridge=FakeBridge(200,persisted)
|
|
sup=supervisor(bridge)
|
|
envelope,source=sup.resolve_envelope(task(persisted))
|
|
assert source=="persisted_verified"
|
|
assert envelope.action=="source_capture"
|
|
assert envelope.digest==persisted["execution_digest"]
|
|
assert bridge.calls==[persisted]
|