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:
@@ -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
|
||||
@@ -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}"
|
||||
)
|
||||
Reference in New Issue
Block a user