Files
smart-org/apps/api/tests/engine_fakes.py
T
mjalmoudy94 36564e3f2b feat(engine): Phase 4 — scheduler tick, gate chain, platform git, budget
The AI workforce engine's driver. One tick moves every ready task one step:
a turn, a gate, or a platform merge. The AI never runs git.

- scheduler.py: tick() over TURN_STAGES/MERGE_STAGES; selection caps
  (per-org 3, global 8); optimistic advance so two workers cannot both win;
  worktree created for code pipelines, released on completion/cancel/failure
  (kept during error_review); budget pause before any turn is spent; merge
  conflict back-off; transient-failure retries then the manager's error turn.
- gates.py: diff guard (empty/.git/CI/symlink/oversize), no-progress hash,
  tests in the sandbox, green gate that only trusts a green tests row or an
  explicit waiver. Every verdict is a gate_results row + a task.gate event.
  Fixed forbidden_paths: lstrip("./") stripped all leading dots, hiding
  .git and .github from the guard.
- container_engine.build_scheduler + worker drives the engine tick.
- ManagerOutcome.error_kind; TaskSpendRepository.spend_for_task.
- New settings engine_max_concurrent_turns, engine_max_turns_per_org,
  engine_tests_timeout_seconds, engine_max_merge_conflicts,
  engine_max_task_credits (+ .env.example).
- tests: test_engine_gates.py (13), test_engine_flow.py (11).

Checks: pytest 746 passed/1 skipped, ruff clean, mypy clean (160 files).
2026-10-14 19:02:58 +03:30

271 lines
8.8 KiB
Python

"""Fakes for the engine tests: a scripted model and a scripted sandbox.
Two rules the engine tests insist on: the model never answers by calling a
real provider, and nothing ever executes on the host. Both fakes also *record*
what they were given, because half of what the engine must guarantee is about
prompts (no secret ever reaches one) and commands (git never runs).
``ScriptedModel.complete`` returns the queued responses in order and raises
the queued exceptions, so a test can script "read a file, run the tests, report"
exactly the way a real agent would behave — including the failures.
"""
from __future__ import annotations
from dataclasses import dataclass
from app.application.ports import ChatMessage, CommandResult, ModelHealth, ModelResponse
from app.application.ports.model_grants import GrantedModel
from app.application.ports.sandbox_provider import (
SessionInfo,
SnapshotInfo,
WorkspaceInfo,
)
@dataclass(slots=True, kw_only=True)
class ScriptedReply:
"""One queued model answer: text, or an exception to raise instead."""
content: str = ""
raises: BaseException | None = None
input_tokens: int = 10
output_tokens: int = 5
class ScriptedModel:
"""A ``ModelProviderPort`` that replays a script.
``seen`` holds every call it received (system prompt + messages), so a test
can assert what the model was actually told — the no-secrets rule is only
testable this way.
"""
def __init__(self, replies: list[ScriptedReply] | None = None) -> None:
self.replies: list[ScriptedReply] = list(replies or [])
self.seen: list[dict] = []
self.calls = 0
def queue(self, content: str, **kwargs) -> None:
self.replies.append(ScriptedReply(content=content, **kwargs))
async def complete(
self,
*,
model,
messages: list[ChatMessage],
system: str = "",
temperature: float = 0.2,
max_tokens: int = 2048,
credential=None,
credentials=None,
) -> ModelResponse:
self.calls += 1
self.seen.append(
{
"model": model,
"system": system,
"messages": list(messages),
"credential": credential,
}
)
if not self.replies:
return ModelResponse(
content="[[WORK_REPORT]]پاسخ پیش‌فرض[[/WORK_REPORT]]",
model_internal_name=getattr(model, "internal_name", ""),
input_tokens=1,
output_tokens=1,
)
reply = self.replies.pop(0)
if reply.raises is not None:
raise reply.raises
return ModelResponse(
content=reply.content,
model_internal_name=getattr(model, "internal_name", ""),
input_tokens=reply.input_tokens,
output_tokens=reply.output_tokens,
)
async def health(self) -> ModelHealth:
return ModelHealth(ok=True, detail="scripted")
@property
def prompts(self) -> list[str]:
"""Every system prompt the model was given, in order."""
return [call["system"] for call in self.seen]
def all_text(self) -> str:
"""Everything the model saw, for the "no secret" assertion."""
parts = [call["system"] for call in self.seen]
for call in self.seen:
parts.extend(message.content for message in call["messages"])
return "\n".join(parts)
class FakeSandbox:
"""A ``SandboxProviderPort`` over a dict filesystem and scripted commands.
``commands`` maps the command *prefix* to a queue of results, so a test can
make the tests red once and green afterwards: the engine must notice.
"""
def __init__(
self,
*,
files: dict[str, str] | None = None,
commands: dict[str, list[CommandResult]] | None = None,
) -> None:
self.files: dict[str, str] = dict(files or {})
self.commands: dict[str, list[CommandResult]] = {
key: list(value) for key, value in (commands or {}).items()
}
self.executed: list[dict] = []
self.writes: list[dict] = []
self.reads: list[str] = []
# -- SandboxProviderPort ------------------------------------------------
async def create_workspace(
self, *, organization_id: str, project_id: str, template_id: str, image: str
) -> WorkspaceInfo:
return WorkspaceInfo(external_id="ws-1", status="ready")
async def start_session(self, *, workspace_external_id: str) -> SessionInfo:
return SessionInfo(external_id="sess-1", status="running")
async def run_command(
self,
*,
session_external_id: str,
command: str,
timeout_seconds: int,
max_output_bytes: int,
env: dict[str, str] | None = None,
) -> CommandResult:
self.executed.append(
{
"command": command,
"timeout_seconds": timeout_seconds,
"max_output_bytes": max_output_bytes,
"session": session_external_id,
}
)
for prefix, queue in self.commands.items():
if command.startswith(prefix) and queue:
return queue.pop(0)
return CommandResult(exit_code=0, stdout="ok")
async def read_file(self, *, session_external_id: str, path: str) -> str:
self.reads.append(path)
if path not in self.files:
raise FileNotFoundError(path)
return self.files[path]
async def write_file(self, *, session_external_id: str, path: str, content: str) -> None:
self.files[path] = content
self.writes.append({"path": path, "content": content})
async def snapshot(self, *, session_external_id: str, name: str) -> SnapshotInfo:
return SnapshotInfo(external_id="snap-1", name=name)
async def pause(self, *, session_external_id: str) -> None:
return None
async def resume(self, *, session_external_id: str) -> SessionInfo:
return SessionInfo(external_id="sess-1", status="running")
async def destroy(self, *, session_external_id: str) -> None:
return None
async def get_preview_url(self, *, session_external_id: str, port: int) -> str:
return f"http://127.0.0.1:{port}"
class FakeGrants:
"""A ``ModelGrantPort`` with an explicit allow-list.
``granted`` is either a list of model ids (all ``balanced``) or a mapping
of id → neutral tier, so a test can have a "strong" and a "fast" model and
assert the engine picked the right one.
"""
def __init__(self, granted: list[str] | dict[str, str] | None = None) -> None:
if granted is None:
self._tiers: dict[str, str] = {
"model-strong": "strong",
"model-fast": "fast",
}
elif isinstance(granted, dict):
self._tiers = dict(granted)
else:
self._tiers = dict.fromkeys(granted, "balanced")
self.checks: list[str] = []
@property
def granted(self) -> list[str]:
return list(self._tiers)
async def granted_models(self, *, organization_id: str) -> list[GrantedModel]:
return [
GrantedModel(ai_model_id=model_id, tier=tier)
for model_id, tier in self._tiers.items()
]
async def is_granted(self, *, organization_id: str, ai_model_id: str) -> bool:
self.checks.append(ai_model_id)
return ai_model_id in self._tiers
def grant(self, model_id: str, tier: str = "balanced") -> None:
self._tiers[model_id] = tier
class RecordingMemory:
"""A ``MemoryPort`` that records its queries and returns fixed context."""
def __init__(self, context: str = "") -> None:
self.context = context
self.queries: list[dict] = []
self.recorded: list[dict] = []
self.finished: list[dict] = []
async def build_context(
self, *, organization_id: str, role: str, query: str, budget_chars: int = 2500
) -> str:
self.queries.append(
{"organization_id": organization_id, "role": role, "query": query}
)
return self.context
async def record(
self,
*,
organization_id: str,
kind: str,
content: str,
source: str,
evidence_task_id: str | None = None,
key: str | None = None,
) -> str | None:
self.recorded.append(
{
"organization_id": organization_id,
"kind": kind,
"content": content,
"source": source,
}
)
return None
async def on_task_finished(self, *, organization_id: str, task) -> None:
self.finished.append({"organization_id": organization_id, "task": task})
return None
__all__ = [
"FakeGrants",
"FakeSandbox",
"RecordingMemory",
"ScriptedModel",
"ScriptedReply",
]