From 106a0291a4dc227b07b8333ec33d924c5b43c9ad Mon Sep 17 00:00:00 2001 From: NW Date: Fri, 8 May 2026 22:49:02 +0100 Subject: [PATCH] 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 --- scripts/e2e-gns2-test.py | 189 +++++++++++++++++++++++++++++++++++++++ 1 file changed, 189 insertions(+) create mode 100644 scripts/e2e-gns2-test.py diff --git a/scripts/e2e-gns2-test.py b/scripts/e2e-gns2-test.py new file mode 100644 index 0000000..1898a87 --- /dev/null +++ b/scripts/e2e-gns2-test.py @@ -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"") + 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'', 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())