feat(gns2): E2E integration test script for issue #110

- Scripts: e2e-gns2-test.py simulates full pipeline through Gitea API
- Supports scoped label replacement (status, budget, cascade)
- Generates GNS_EVENT footers in comments
- Validates checkpoint, labels, timeline, budget, depth
- Uses actual existing labels (status::done, not status::completed)

Refs: Milestone #67, Issue #110
This commit is contained in:
NW
2026-05-08 22:49:02 +01:00
parent f5966db155
commit 106a0291a4

189
scripts/e2e-gns2-test.py Normal file
View File

@@ -0,0 +1,189 @@
#!/usr/bin/env python3
"""GNS-2 End-to-End Integration Test"""
import urllib.request
import json
import base64
import time
import sys
USER, PASS, REPO, ISSUE = 'NW', 'eshkink0t', 'UniqueSoft/APAW', 110
class GiteaAPI:
def __init__(self):
self.base = 'https://git.softuniq.eu/api/v1'
self.creds = base64.b64encode(f"{USER}:{PASS}".encode()).decode()
def api(self, path, data=None, method='GET'):
url = f"{self.base}/repos/{REPO}{path}"
req = urllib.request.Request(
url, data=json.dumps(data).encode() if data else None,
headers={'Content-Type': 'application/json'}, method=method)
req.add_header('Authorization', f'Basic {self.creds}')
with urllib.request.urlopen(req) as r:
return json.loads(r.read()) if r.status != 204 else None
gitea = GiteaAPI()
def update_checkpoint(phase, depth, consumed, remaining, last_agent, next_agent, history_append):
issue = gitea.api(f"/issues/{ISSUE}")
body = issue['body']
checkpoint_yaml = (
f"checkpoint:\n version: 2\n issue: {ISSUE}\n phase: {phase}\n"
f" depth: {depth}\n last_agent: {last_agent}\n"
f" last_invocation: {last_agent}-110-{int(time.time())}\n"
f" budget:\n total: 8000\n consumed: {consumed}\n"
f" remaining: {remaining}\n state:\n"
f" labels: [status::{phase}, budget::{'sufficient' if remaining > 2000 else 'warning' if remaining > 0 else 'exhausted'}, cascade::depth-{depth}]\n"
f" assignee: {next_agent}\n milestone: 67\n history:\n"
f" - {{agent: orchestrator, invocation: orch-110-001, action: create_e2e_test}}\n{history_append}\n"
f" next_agent: {next_agent}\n next_estimated_tokens: 1000\n"
f" created_at: {time.strftime('%Y-%m-%dT%H:%M:%SZ', time.gmtime())}\n")
import re
new_body = re.sub(
r'## GNS Checkpoint\s*```yaml\s*[\s\S]*?```',
f"## GNS Checkpoint\n```yaml\n{checkpoint_yaml}```", body)
gitea.api(f"/issues/{ISSUE}", {"body": new_body}, 'PATCH')
def post_comment(agent, evtype, depth, consumed, remaining, next_agent, extras=""):
inv = int(time.time())
comment = (
f"## 🔄 {agent} | phase:executing | depth:{depth}\n\n"
f"**Event Type**: {evtype}\n**Parent**: orch-110-001\n"
f"**Invocation**: {agent}-110-{inv}\n"
f"**Budget**: 8000 → {consumed}{remaining}\n\n"
f"### Action Taken\n{agent} processed checkpoint.\n\n"
f"### Next Decision\n**Recommended next**: @{next_agent}\n"
f"**Estimated tokens**: 1000\n**Budget remaining**: {remaining}\n\n{extras}\n---\n"
f"<!-- GNS_EVENT: {{\n \"type\": \"{evtype}\",\n"
f' "agent": "{agent}",\n'
f' "invocation_id": "{agent}-110-{inv}",\n'
f' "parent_id": "orch-110-001",\n'
f' "depth": {depth},\n'
f' "budget": {{"before": 8000, "consumed": {consumed}, "remaining": {remaining}}},\n'
f' "state_changes": {{\n'
f' "labels_add": ["phase::executing"],\n'
f' "assignee": "{next_agent}",\n'
f' "is_locked": false\n }},\n'
f' "cascade_log": [],\n'
f' "next_agent": "{next_agent}",\n'
f' "estimated_next_tokens": 1000,\n'
f' "timestamp": "{time.strftime("%Y-%m-%dT%H:%M:%SZ", time.gmtime())}"\n'
f"}} -->")
gitea.api(f"/issues/{ISSUE}/comments", {"body": comment}, 'POST')
def add_label(label):
try:
gitea.api(f"/issues/{ISSUE}/labels", {"labels": [label]}, 'POST')
except Exception as e:
print(f" (Label {label}: {e})")
def replace_scoped_label(scope, new_label):
issue = gitea.api(f"/issues/{ISSUE}")
for l in issue.get('labels', []):
if l['name'].startswith(f"{scope}::"):
try:
gitea.api(f"/issues/{ISSUE}/labels/{l['id']}", method='DELETE')
except Exception:
pass
add_label(new_label)
def e2e_test():
print("="*60)
print("GNS-2 End-to-End Test")
print(f"Issue: #{ISSUE}")
print("="*60)
print("\n[1] Init...", end=' ')
issue = gitea.api(f"/issues/{ISSUE}")
print("OK")
print("\n[2] Requirement Refiner...", end=' ')
update_checkpoint('planned', 0, 500, 7500, 'requirement-refiner', 'capability-analyst',
' - {agent: req-refiner, invocation: req-110-001, action: refine}')
post_comment('requirement-refiner', 'state_change', 0, 500, 7500, 'capability-analyst')
replace_scoped_label('status', 'status::planned')
add_label('agent::capability-analyst')
print("OK")
time.sleep(2)
print("\n[3] Capability-Analyst spawns HistoryMiner (depth 0→1)...", end=' ')
update_checkpoint('researching', 1, 1500, 6500, 'capability-analyst', 'history-miner',
' - {agent: cap-analyst, invocation: cap-110-001, action: subagent_call, target: history-miner}')
cascade = "### Cascade Log\n| Agent | Task | Result | Tokens | Verdict |\n|-------|------|--------|--------|---------|\n| history-miner | git search | found 3 commits | 1000 | ✅ |"
post_comment('capability-analyst', 'subagent_result', 1, 1500, 6500, 'agent-architect', cascade)
replace_scoped_label('status', 'status::researching')
add_label('cascade::depth-1')
print("OK")
time.sleep(2)
print("\n[4] History Miner (Tier 0, leaf)...", end=' ')
post_comment('history-miner', 'subagent_result', 1, 2500, 5500, 'agent-architect', "### Findings\n- Found `47b027a`\n- 2 related issues")
print("OK")
time.sleep(2)
print("\n[5] Agent Architect completes spec (Tier 2, depth 1→2)...", end=' ')
update_checkpoint('designed', 2, 3500, 4500, 'agent-architect', 'capability-analyst',
' - {agent: arch, invocation: arch-110-001, action: design_spec}')
post_comment('agent-architect', 'subagent_result', 2, 3500, 4500, 'capability-analyst',
"### Spec Designed\n- mcp-gitea-client.ts\n- docker-compose.yml")
replace_scoped_label('status', 'status::designed')
add_label('cascade::depth-2')
print("OK")
time.sleep(2)
print("\n[6] Capability Analyst reviews and closes...", end=' ')
update_checkpoint('completed', 2, 4000, 4000, 'capability-analyst', 'orchestrator',
' - {agent: cap-analyst, invocation: cap-110-002, action: review_complete}')
post_comment('capability-analyst', 'state_change', 2, 4000, 4000, 'orchestrator',
"### Review Complete\n✅ All criteria met. Closing.")
replace_scoped_label('status', 'status::done')
add_label('budget::sufficient')
add_label('quality::pass')
gitea.api(f"/issues/{ISSUE}", {"state": "closed"}, 'PATCH')
print("OK")
# Verification
issue = gitea.api(f"/issues/{ISSUE}")
comments = gitea.api(f"/issues/{ISSUE}/comments")
timeline = gitea.api(f"/issues/{ISSUE}/timeline")
labels = [l['name'] for l in issue['labels']]
print("\n"+"="*60+"\nVerification\n"+"="*60)
print(f"State: {issue['state']}")
print(f"Labels: {labels}")
print(f"Comments: {len(comments)}, Timeline: {len(timeline)}")
import re
events = re.findall(r'<!-- GNS_EVENT: ({.*?}) -->', issue['body'] + '\n'.join(c['body'] for c in comments), re.DOTALL)
print(f"GNS_EVENTs: {len(events)}")
print(f"Checkpoint: {'' if '## GNS Checkpoint' in issue['body'] else ''}")
failures = []
if issue['state'] != 'closed':
failures.append("Issue not closed")
if len(events) < 5:
failures.append(f"Too few events ({len(events)})")
if 'status::done' not in labels:
failures.append("No completed")
if 'cascade::depth-2' not in labels:
failures.append("No depth-2")
if 'budget::sufficient' not in labels:
failures.append("No budget")
if 'quality::pass' not in labels:
failures.append("No quality")
if failures:
print("\n❌ FAILED")
for f in failures:
print(f" - {f}")
return 1
print("\n✅ ALL E2E TESTS PASSED\n"+"="*60)
return 0
if __name__ == '__main__':
sys.exit(e2e_test())