Initial commit: a0_software_orchestrator v1.0

- Auto-Registration-Bug behoben (register_project/get_project_id/resolve_project Trennung)
- 25 Tests gruen (Pytest)
- block_compactor-Tool refactored (Option B: Soft-Check statt Hard-Block)
- 4 Restbaustellen gefixt
- DB-Schema: plugin_settings-Tabelle hinzugefuegt
- 3 Schattenprojekte aus DB geloescht
- Plan v3 + Refactor-Plan + Worklog dokumentiert
This commit is contained in:
Software Orchestrator
2026-06-16 22:13:06 +00:00
commit 5769c1cd22
236 changed files with 17825 additions and 0 deletions
View File
+24
View File
@@ -0,0 +1,24 @@
"""conftest.py für helpers/tests/ mockt helpers.tool, um plan_mode_guard-Tests
zu ermöglichen, ohne das Agent-Framework (helpers.tool → helpers.history → langchain.prompts)
laden zu müssen.
Wir mocken nur das minimale Tool/Response-Interface, das plan_mode_guard braucht.
"""
import sys
from types import SimpleNamespace
from unittest.mock import MagicMock
# Minimal-Stubs für helpers.tool: Tool als leere Klasse, Response als SimpleNamespace
class _FakeResponse:
def __init__(self, message="", break_loop=False):
self.message = message
self.break_loop = break_loop
class _FakeTool:
pass
_fake_tool_module = SimpleNamespace(Tool=_FakeTool, Response=_FakeResponse)
sys.modules["helpers.tool"] = _fake_tool_module
+630
View File
@@ -0,0 +1,630 @@
"""Tests für helpers/db_state_store.py Auto-Registration Bugfix (Plan v3 §4).
Verwendet tmp-DB (NICHT die echte patterns.db). PatternDB ist Singleton,
daher wird pro Test resettet.
"""
from __future__ import annotations
import json
import os
import sqlite3
import sys
import time
from pathlib import Path
from unittest.mock import patch
import pytest
# --- Fixture: tmp-DB pro Test -------------------------------------------------
@pytest.fixture
def tmp_db(tmp_path, monkeypatch):
"""Erstellt eine frische in-memory-ähnliche Test-DB und patched die Singleton.
PatternDB ist Singleton; wir resetten _instance pro Test und instanziieren
mit einem tmp_path, damit die echte patterns.db nicht berührt wird.
"""
from usr.plugins.a0_software_orchestrator.helpers.library.db import PatternDB
from usr.plugins.a0_software_orchestrator.helpers import db_state_store
db_path = tmp_path / "test_patterns.db"
PatternDB._instance = None # Singleton reset
db = PatternDB(db_path) # Erstellt + initialisiert + migrations
yield db, db_path
# Cleanup
try:
db.close()
except Exception:
pass
PatternDB._instance = None
# =============================================================================
# Validator (4 Cases)
# =============================================================================
class TestValidateProjectName:
def test_validate_project_name_ok(self):
from usr.plugins.a0_software_orchestrator.helpers.db_state_store import (
_validate_project_name,
)
for name in ("crm-system", "web-cad", "rentman-clone", "a1b"):
assert _validate_project_name(name) == name, f"'{name}' should pass"
def test_validate_project_name_pattern_fail(self):
from usr.plugins.a0_software_orchestrator.helpers.db_state_store import (
_validate_project_name,
)
for name in ("A0", "Crm System", "a", "", "my.app", "cool_app", "müller"):
with pytest.raises(ValueError):
_validate_project_name(name)
def test_validate_project_name_blacklist(self):
from usr.plugins.a0_software_orchestrator.helpers.db_state_store import (
_validate_project_name,
)
for name in ("a0", "a0-development", "workdir", "dev-projects"):
with pytest.raises(ValueError):
_validate_project_name(name)
def test_validate_project_name_whitespace_strip(self):
from usr.plugins.a0_software_orchestrator.helpers.db_state_store import (
_validate_project_name,
)
assert _validate_project_name(" crm ") == "crm"
# =============================================================================
# register_project (4 Cases)
# =============================================================================
class TestRegisterProject:
def test_register_project_new(self, tmp_db):
db, _ = tmp_db
from usr.plugins.a0_software_orchestrator.helpers.db_state_store import (
register_project,
)
pid = register_project("crm-system", git_url="https://x.com/y.git",
description="Test")
assert isinstance(pid, int) and pid > 0
row = db.conn.execute(
"SELECT name, git_url, description FROM projects WHERE id = ?", (pid,)
).fetchone()
assert row["name"] == "crm-system"
assert row["git_url"] == "https://x.com/y.git"
assert row["description"] == "Test"
def test_register_project_idempotent(self, tmp_db):
db, _ = tmp_db
from usr.plugins.a0_software_orchestrator.helpers.db_state_store import (
register_project,
)
pid1 = register_project("crm-system")
pid2 = register_project("crm-system")
assert pid1 == pid2
# Nur ein Eintrag
n = db.conn.execute(
"SELECT COUNT(*) as c FROM projects WHERE name = 'crm-system'"
).fetchone()["c"]
assert n == 1
def test_register_project_value_error_wraps_validator(self, tmp_db):
from usr.plugins.a0_software_orchestrator.helpers.db_state_store import (
register_project,
)
with pytest.raises(ValueError):
register_project("a0") # Blacklist
with pytest.raises(ValueError):
register_project("Cool_App") # Pattern-Fail
def test_register_project_idempotent_overwrite_semantics(self, tmp_db):
"""Zweiter Aufruf mit leerem git_url darf den ersten NICHT überschreiben."""
db, _ = tmp_db
from usr.plugins.a0_software_orchestrator.helpers.db_state_store import (
register_project,
)
register_project("crm-system", git_url="https://first.git")
register_project("crm-system", git_url="")
row = db.conn.execute(
"SELECT git_url FROM projects WHERE name = 'crm-system'"
).fetchone()
# COALESCE(NULLIF('', ''), projects.git_url) → behält first.git
assert row["git_url"] == "https://first.git"
# =============================================================================
# resolve_project (4 Cases)
# =============================================================================
class TestResolveProject:
def test_resolve_project_existing(self, tmp_db):
from usr.plugins.a0_software_orchestrator.helpers.db_state_store import (
register_project, resolve_project,
)
pid = register_project("crm-system")
assert resolve_project("crm-system") == pid
def test_resolve_project_missing_raises(self, tmp_db):
"""Bug 1 Fix: KEIN Auto-Register mehr! Unbekannter Name → LookupError."""
from usr.plugins.a0_software_orchestrator.helpers.db_state_store import (
resolve_project,
)
with pytest.raises(LookupError):
resolve_project("never-existed")
# Bestätige: kein Eintrag in DB angelegt
from usr.plugins.a0_software_orchestrator.helpers.library.db import PatternDB
db = PatternDB()
n = db.conn.execute(
"SELECT COUNT(*) as c FROM projects WHERE name = 'never-existed'"
).fetchone()["c"]
assert n == 0
def test_resolve_project_invalid_name_raises(self, tmp_db):
"""Blacklist wirft VOR SELECT → ValueError (nicht LookupError)."""
from usr.plugins.a0_software_orchestrator.helpers.db_state_store import (
resolve_project,
)
with pytest.raises(ValueError):
resolve_project("a0")
with pytest.raises(ValueError):
resolve_project("workdir")
def test_resolve_project_touches_last_active_at(self, tmp_db):
db, _ = tmp_db
from usr.plugins.a0_software_orchestrator.helpers.db_state_store import (
register_project, resolve_project,
)
pid = register_project("crm-system")
ts1 = db.conn.execute(
"SELECT last_active_at FROM project_state WHERE project_id = ?", (pid,)
).fetchone()["last_active_at"]
time.sleep(1.1) # SQLite datetime() hat Sekundengranularität
resolve_project("crm-system")
ts2 = db.conn.execute(
"SELECT last_active_at FROM project_state WHERE project_id = ?", (pid,)
).fetchone()["last_active_at"]
assert ts2 is not None and ts1 is not None
assert ts2 > ts1, f"last_active_at should advance: ts1={ts1} ts2={ts2}"
# =============================================================================
# get_project_id (1 Case)
# =============================================================================
class TestGetProjectId:
def test_get_project_id_missing_returns_none(self, tmp_db):
from usr.plugins.a0_software_orchestrator.helpers.db_state_store import (
get_project_id,
)
assert get_project_id("never-existed") is None
# Kein Side-Effect: kein Eintrag angelegt
from usr.plugins.a0_software_orchestrator.helpers.library.db import PatternDB
db = PatternDB()
n = db.conn.execute(
"SELECT COUNT(*) as c FROM projects WHERE name = 'never-existed'"
).fetchone()["c"]
assert n == 0
# =============================================================================
# Legacy-Migration (1 Case)
# =============================================================================
class TestLegacyMigration:
def test_legacy_row_with_invalid_name_is_archived(self, tmp_db):
"""Projekt mit ungültigem Namen (UPPER, Punkt) wird durch Migrations-Lauf archiviert."""
db, _ = tmp_db
# 1. Direkt eine Row mit ungültigem Namen einfügen (simuliert Alt-Daten)
cur = db.conn.execute(
"INSERT INTO projects (name, description) VALUES (?, ?)",
("OLD-PROJECT", "legacy"),
)
legacy_pid = int(cur.lastrowid)
db.conn.execute(
"""INSERT INTO project_state (project_id, status, phase)
VALUES (?, 'active', 'intake')""",
(legacy_pid,),
)
db.conn.commit()
# 2. Migrations-Run: rufe _archive_invalids-Logik aus utils/migrate_legacy_project_names
# Wir importieren die Helfer-Funktionen direkt.
from usr.plugins.a0_software_orchestrator.helpers.db_state_store import (
_validate_project_name,
)
# Iteriere projects und archiviere ungültige
rows = db.conn.execute(
"SELECT p.id, p.name, COALESCE(ps.status, '<none>') as status "
"FROM projects p LEFT JOIN project_state ps ON ps.project_id = p.id"
).fetchall()
for r in rows:
name = r["name"]
try:
_validate_project_name(name)
except ValueError:
if r["status"] != "archived":
db.conn.execute(
"""UPDATE project_state SET status = 'archived',
phase = 'archived', last_active_at = datetime('now'),
updated_at = datetime('now') WHERE project_id = ?""",
(r["id"],),
)
db.conn.commit()
# 3. Prüfe: legacy_pid ist jetzt archived
status = db.conn.execute(
"SELECT status FROM project_state WHERE project_id = ?", (legacy_pid,)
).fetchone()["status"]
assert status == "archived", f"expected archived, got {status}"
# =============================================================================
# End-to-End (1 Case)
# =============================================================================
class TestE2ENextStep:
def test_e2e_next_step_with_unknown_project_raises(self, tmp_db):
"""NextStep-Tool mit unbekanntem Projekt wirft LookupError,
ohne Phantom-Eintrag in DB zu erzeugen."""
from usr.plugins.a0_software_orchestrator.helpers.library.db import PatternDB
from usr.plugins.a0_software_orchestrator.helpers.db_state_store import (
get_project_id,
)
# Vorbedingung: Projekt existiert nicht
assert get_project_id("phantom-app") is None
with pytest.raises(LookupError):
get_project_id("phantom-app") # get_project_id wirft nicht → None
# Stattdessen: simulate Tool-Verhalten: get_project_id+None+raise
pid = get_project_id("phantom-app")
if pid is None:
raise LookupError(f"project 'phantom-app' is not registered")
# Kein DB-Eintrag
db = PatternDB()
n = db.conn.execute(
"SELECT COUNT(*) as c FROM projects WHERE name = 'phantom-app'"
).fetchone()["c"]
assert n == 0
# =============================================================================
# plan_mode_guard global (2 Cases)
# =============================================================================
class TestPlanModeGuardGlobal:
def test_plan_mode_guard_set_get(self, tmp_db):
"""Set implementation_allowed, lese via _get_plugin_mode zurück."""
from usr.plugins.a0_software_orchestrator.tools.plan_mode_guard import (
_get_plugin_mode, _set_plugin_mode, _DEFAULT_MODE,
)
# Init: default
mode = _get_plugin_mode()
assert mode["mode"] == _DEFAULT_MODE["mode"]
# Set
new_mode = {
"mode": "implementation_allowed",
"allowed_actions": ["code", "test", "commit"],
"blocked_actions": ["deploy", "destroy", "push_to_main"],
}
_set_plugin_mode(new_mode)
# Read back
mode2 = _get_plugin_mode()
assert mode2["mode"] == "implementation_allowed"
assert "code" in mode2["allowed_actions"]
# Nutzt plugin_settings.key (kein project_id, kein project_name nötig).
# Migration 5 muss die Tabelle plugin_settings angelegt haben.
from usr.plugins.a0_software_orchestrator.helpers.library.db import PatternDB
db = PatternDB()
# plugin_settings-Tabelle existiert?
tbl = db.conn.execute(
"SELECT name FROM sqlite_master WHERE type='table' AND name='plugin_settings'"
).fetchone()
assert tbl is not None, "plugin_settings-Tabelle fehlt Migration 5 nicht gelaufen"
# Wert vorhanden unter 'orchestrator_mode'?
row = db.conn.execute(
"SELECT value_json FROM plugin_settings WHERE key = 'orchestrator_mode'"
).fetchone()
assert row is not None
assert json.loads(row[0])["mode"] == "implementation_allowed"
def test_plan_mode_guard_no_resolve_project_called(self, tmp_db):
"""plan_mode_guard ruft KEIN resolve_project() auf (Bug 7 Fix)."""
from usr.plugins.a0_software_orchestrator.tools import plan_mode_guard as pmg
with patch(
"usr.plugins.a0_software_orchestrator.helpers.db_state_store.resolve_project"
) as mock_resolve:
# Mode lesen
pmg._get_plugin_mode()
# Mode setzen
pmg._set_plugin_mode({
"mode": "implementation_allowed",
"allowed_actions": ["code"],
"blocked_actions": ["destroy"],
})
# Tool.execute aufrufen (verschiedene Pfade)
import asyncio
tool = pmg.PlanModeGuard()
asyncio.run(tool.execute(requested_action="code"))
asyncio.run(tool.execute(new_mode="planning_only"))
asyncio.run(tool.execute(requested_action="deploy"))
# KEIN Aufruf von resolve_project
assert mock_resolve.call_count == 0, (
f"resolve_project wurde {mock_resolve.call_count}x aufgerufen, "
f"erwartet 0"
)
# =============================================================================
# check_mandatory_artifacts 'todo' Bug-Fix (2 Cases)
# =============================================================================
class TestCheckMandatoryArtifacts:
def test_check_mandatory_artifacts_session_done_with_no_open_todos(self, tmp_db):
"""'Session beendet, 0 open todos' → todo: True (Bug-Fix).
Vorher wurde 'mind. 1 open todo' verlangt → blockierte
Block-Compact fälschlich im Session-Ende-Zustand.
Jetzt: Todo-Mechanismus verfügbar (Tabelle + Schema) reicht.
"""
from usr.plugins.a0_software_orchestrator.helpers.compact_protocol import (
check_mandatory_artifacts,
)
from usr.plugins.a0_software_orchestrator.helpers.db_state_store import (
register_project,
)
db, _ = tmp_db
# 1. Projekt registrieren (in tmp_db mit Migration 4 = orch_todos existiert)
register_project("crm-system")
# 2. Sicherstellen: 0 open todos (Session beendet)
n = db.conn.execute(
"SELECT COUNT(*) as c FROM orch_todos "
"WHERE project_id = (SELECT id FROM projects WHERE name='crm-system') "
"AND status='open'"
).fetchone()["c"]
assert n == 0, f"Test-Precondition: 0 open todos, got {n}"
# 3. check_mandatory_artifacts aufrufen
result = check_mandatory_artifacts("crm-system")
# 4. 'todo' MUSS True sein (Bug-Fix)
assert result["todo"] is True, (
f"'todo' sollte True sein wenn orch_todos-Tabelle mit Schema "
f"existiert (auch bei 0 open todos). Got: {result}"
)
# Sanity: andere Artefakte können False sein (kein worklog etc.)
# aber das ist nicht der Punkt dieses Tests.
def test_check_mandatory_artifacts_no_orch_todos_table(self, tmp_db):
"""Wenn orch_todos-Tabelle fehlt → todo: False (negativ-Test)."""
from usr.plugins.a0_software_orchestrator.helpers.compact_protocol import (
check_mandatory_artifacts,
)
from usr.plugins.a0_software_orchestrator.helpers.db_state_store import (
register_project,
)
db, _ = tmp_db
register_project("crm-system")
# orch_todos-Tabelle droppen (simuliert fehlende/alte DB)
db.conn.execute("DROP TABLE orch_todos")
db.conn.commit()
# check_mandatory_artifacts aufrufen
result = check_mandatory_artifacts("crm-system")
# 'todo' MUSS False sein (Tabelle fehlt)
assert result["todo"] is False, (
f"'todo' sollte False sein wenn orch_todos-Tabelle fehlt. "
f"Got: {result}"
)
# =============================================================================
# block_compactor.py entfernte lokale Helper (3 Cases)
# =============================================================================
class TestBlockCompactorCleanup:
def test_block_compactor_no_local_resolve_name(self):
"""_resolve_name wurde aus tools/block_compactor.py entfernt
(basename-Fallback erzeugte potenziell ungültige Namen)."""
import usr.plugins.a0_software_orchestrator.tools.block_compactor as bc
assert not hasattr(bc, "_resolve_name"), (
"_resolve_name sollte NICHT mehr in block_compactor definiert sein. "
"Bug 4-7-3 Fix: basename-Fallback raus, project_name ist Pflicht."
)
def test_block_compactor_uses_imported_check_mandatory_artifacts(self):
"""check_mandatory_artifacts in block_compactor muss die importierte
Version aus compact_protocol sein (gleiche Funktion-ID), nicht lokal
überschrieben."""
import usr.plugins.a0_software_orchestrator.tools.block_compactor as bc
from usr.plugins.a0_software_orchestrator.helpers import compact_protocol as cp
# bc.check_mandatory_artifacts MUSS cp.check_mandatory_artifacts sein
assert bc.check_mandatory_artifacts is cp.check_mandatory_artifacts, (
"check_mandatory_artifacts in block_compactor ist nicht die "
"importierte compact_protocol-Version. Lokale Definition würde "
"den 'todo'-Fix aus compact_protocol zunichte machen."
)
def test_block_compactor_mandatory_artifacts_pass_with_full_setup(self, tmp_db):
"""End-to-End: Projekt mit allen 5 mandatory artifacts →
check_mandatory_artifacts liefert alles True (in tmp_db mit
Migration 4 hat 'todo' nach Fix automatisch True)."""
from usr.plugins.a0_software_orchestrator.helpers.compact_protocol import (
check_mandatory_artifacts,
)
from usr.plugins.a0_software_orchestrator.helpers.db_state_store import (
register_project, append_worklog, set_kv, set_status, add_next_step,
)
# 1. Projekt + alle 5 Artefakte anlegen
register_project("crm-system")
# tmp_db: get_project_id('crm-system') aufrufen für pid
from usr.plugins.a0_software_orchestrator.helpers.library.db import PatternDB
db = PatternDB()
pid_row = db.conn.execute(
"SELECT id FROM projects WHERE name='crm-system'"
).fetchone()
pid = int(pid_row["id"])
# worklog
append_worklog(pid, "phase", "block-1", "agent", "summary", "details")
# project_state (KV-Key)
set_kv(pid, "project_state", {"phase": "intake"})
# current_status
set_status(pid, "working on it")
# next_steps (pending)
add_next_step(pid, "task 1")
db.conn.commit()
# 2. check_mandatory_artifacts aufrufen
result = check_mandatory_artifacts("crm-system")
# 3. Alle 5 True
missing = [k for k, v in result.items() if not v]
assert not missing, f"Erwartet alle 5 True, aber fehlen: {missing} (Full result: {result})"
assert result["todo"] is True, (
"'todo' muss True sein (Bug-Fix in compact_protocol: "
"Tabelle+Schema verfügbar, nicht 'mind. 1 open todo')"
)
# =============================================================================
# block_compactor Option B Soft-Check (3 Cases)
# =============================================================================
class TestBlockCompactorSoftCheck:
def test_block_compactor_warns_but_writes_with_missing_artifacts(self, tmp_db):
"""Refactor Option B: mandatory artifacts fehlen → WARN, nicht BLOCK.
Tool schreibt trotzdem (DB-Eintrag in orch_block_compact + worklog),
message enthält 'mandatory_artifacts: 0/5 ⚠️' + 'mandatory artifacts incomplete'.
"""
import asyncio
from usr.plugins.a0_software_orchestrator.helpers.db_state_store import (
register_project, add_next_step,
)
from usr.plugins.a0_software_orchestrator.tools.block_compactor import (
BlockCompactor,
)
from usr.plugins.a0_software_orchestrator.helpers.library.db import PatternDB
db, _ = tmp_db
register_project("crm-system")
pid = int(db.conn.execute(
"SELECT id FROM projects WHERE name='crm-system'"
).fetchone()["id"])
# next_steps anlegen, sonst triggert should_block_compact Z.6
# (nicht Teil der Soft-Check-Refactor-Scope, bleibt BLOCK).
add_next_step(pid, "dummy next step")
db.conn.commit()
# KEINE mandatory artifacts gefüllt (worklog, project_state-KV,
# current_status fehlen; nur next_steps ist da)
# block_compactor.execute() aufrufen
bc = BlockCompactor()
result = asyncio.run(bc.execute(
project_name="crm-system",
block_id="B-test-soft-check",
block_summary="Test: fehlende mandatory artifacts sollen WARN sein",
next_block="siehe worklog",
decisions=[], open_questions=[], key_findings=[],
estimated_context_tokens=7000, max_context_tokens=8000, # ratio=0.875 OK
open_subagent_calls=0, quality_gate_passed=True,
))
msg = result.message
# 1. NICHT 'BLOCK COMPACT BLOCKED'
assert "BLOCK COMPACT BLOCKED" not in msg, (
f"Soft-Check Bug: Tool hat BLOCKED statt WARN. message={msg!r}"
)
# 2. 'BLOCK COMPACT READY' (Erfolg)
assert "BLOCK COMPACT READY" in msg, f"Erwartet READY, got: {msg!r}"
# 3. 'mandatory artifacts incomplete' Warnung
assert "mandatory artifacts incomplete" in msg, (
f"Erwartet warning 'mandatory artifacts incomplete', got: {msg!r}"
)
# 4. mandatory_artifacts: <n>/5 + ⚠️ + missing-Liste
# Setup hat: next_steps (add_next_step), todo (Tabelle via Migration),
# project_state (register_project triggert phase-KV) = 3/5 True.
# Fehlen: worklog, current_status.
assert "⚠️" in msg and "missing:" in msg, (
f"Erwartet '⚠️ missing: [...]', got: {msg!r}"
)
assert "worklog" in msg and "current_status" in msg, (
f"Erwartet 'worklog' und 'current_status' in missing-Liste, got: {msg!r}"
)
# 5. DB-Eintrag in orch_block_compact wurde geschrieben
row = db.conn.execute(
"SELECT last_block_id FROM orch_block_compact WHERE project_id = ?",
(pid,),
).fetchone()
assert row is not None, (
"Tool hat NICHT geschrieben trotz Option-B-Soft-Check. "
"Erwartet orch_block_compact-Eintrag."
)
assert row["last_block_id"] == "B-test-soft-check"
def test_block_compactor_blocks_on_open_subagents(self, tmp_db):
"""Hard-Block bleibt für open_subagent_calls > 0 (echter Datenverlust-Schutz)."""
import asyncio
from usr.plugins.a0_software_orchestrator.helpers.db_state_store import (
register_project, append_worklog, set_kv, set_status, add_next_step,
)
from usr.plugins.a0_software_orchestrator.tools.block_compactor import (
BlockCompactor,
)
db, _ = tmp_db
register_project("crm-system")
pid = int(db.conn.execute(
"SELECT id FROM projects WHERE name='crm-system'"
).fetchone()["id"])
# ALLE mandatory artifacts füllen
append_worklog(pid, "phase", "b1", "agent", "s", "d")
set_kv(pid, "project_state", {"phase": "intake"})
set_status(pid, "ok")
add_next_step(pid, "task 1")
db.conn.commit()
# BlockCompactor mit open_subagent_calls=1
bc = BlockCompactor()
result = asyncio.run(bc.execute(
project_name="crm-system",
block_id="B-test-subagent",
block_summary="subagent test",
next_block="x",
open_subagent_calls=1, quality_gate_passed=True,
estimated_context_tokens=7000, max_context_tokens=8000,
))
msg = result.message
assert "BLOCK COMPACT BLOCKED" in msg, f"Erwartet BLOCKED, got: {msg!r}"
# Grund: subagent / open_subagent / pending subagent
assert ("subagent" in msg.lower() or "subagent_calls" in msg.lower()), (
f"Erwartet Hinweis auf subagent, got: {msg!r}"
)
def test_block_compactor_blocks_on_quality_gate_fail(self, tmp_db):
"""Hard-Block bleibt für quality_gate_passed=False (persistiert-Bug-Schutz)."""
import asyncio
from usr.plugins.a0_software_orchestrator.helpers.db_state_store import (
register_project, append_worklog, set_kv, set_status, add_next_step,
)
from usr.plugins.a0_software_orchestrator.tools.block_compactor import (
BlockCompactor,
)
db, _ = tmp_db
register_project("crm-system")
pid = int(db.conn.execute(
"SELECT id FROM projects WHERE name='crm-system'"
).fetchone()["id"])
# ALLE mandatory artifacts füllen
append_worklog(pid, "phase", "b1", "agent", "s", "d")
set_kv(pid, "project_state", {"phase": "intake"})
set_status(pid, "ok")
add_next_step(pid, "task 1")
db.conn.commit()
# BlockCompactor mit quality_gate_passed=False
bc = BlockCompactor()
result = asyncio.run(bc.execute(
project_name="crm-system",
block_id="B-test-quality",
block_summary="quality test",
next_block="x",
open_subagent_calls=0, quality_gate_passed=False,
estimated_context_tokens=7000, max_context_tokens=8000,
))
msg = result.message
assert "BLOCK COMPACT BLOCKED" in msg, f"Erwartet BLOCKED, got: {msg!r}"
assert "quality" in msg.lower(), (
f"Erwartet Hinweis auf quality, got: {msg!r}"
)