diff --git a/.github/scripts/pitot_e2e_report.py b/.github/scripts/pitot_e2e_report.py new file mode 100644 index 000000000..a9a2dc9b7 --- /dev/null +++ b/.github/scripts/pitot_e2e_report.py @@ -0,0 +1,279 @@ +#!/usr/bin/env python3 +"""Publish a trusted, sticky PR summary for Pitot's per-agent E2E workflows.""" + +from __future__ import annotations + +import argparse +import io +import json +import os +from pathlib import Path +import re +import urllib.parse +import urllib.request +import zipfile + + +MARKER = "" +WORKFLOWS = { + "claude": "Pitot E2E — Claude", + "cursor": "Pitot E2E — Cursor", + "codex": "Pitot E2E — Codex", + "gemini": "Pitot E2E — Gemini", + "opencode": "Pitot E2E — OpenCode", +} +PLATFORMS = ("ubuntu", "macos") +MAX_ARTIFACT_BYTES = 1_000_000 + + +def validate_result( + value: object, + *, + agent: str, + platform: str, + expected_sha: str | None = None, + expected_run_url: str | None = None, +) -> dict[str, object]: + if not isinstance(value, dict): + raise ValueError("result must be an object") + required = { + "schema_version", + "agent", + "platform", + "status", + "verification_mode", + "evidence", + "commit_sha", + "run_url", + } + if set(value) != required: + raise ValueError("result fields do not match schema") + if value["schema_version"] != 1 or value["agent"] != agent or value["platform"] != platform: + raise ValueError("result identity does not match artifact") + if value["status"] not in {"pass", "fail"}: + raise ValueError("invalid result status") + mode = value["verification_mode"] + if value["status"] == "pass" and mode not in {"real_cli", "hook_subprocess"}: + raise ValueError("passing result requires a verification mode") + if value["status"] == "fail" and mode is not None: + raise ValueError("failed result cannot claim a verification mode") + allowed_evidence = { + "real host CLI completed the Pitot integration path", + "active hook subprocess produced a normalized Pitot action", + "E2E command failed", + "E2E command returned without one valid result marker", + } + if value["evidence"] not in allowed_evidence: + raise ValueError("invalid evidence summary") + if not isinstance(value["commit_sha"], str) or not re.fullmatch(r"[0-9a-f]{40}", value["commit_sha"]): + raise ValueError("invalid commit_sha") + if not isinstance(value["run_url"], str) or not re.fullmatch( + r"https://github\.com/[A-Za-z0-9_.-]+/[A-Za-z0-9_.-]+/actions/runs/[0-9]+", + value["run_url"], + ): + raise ValueError("invalid run_url") + if expected_sha is not None and value["commit_sha"] != expected_sha: + raise ValueError("result commit does not match workflow run") + if expected_run_url is not None and value["run_url"] != expected_run_url: + raise ValueError("result URL does not match workflow run") + return value + + +def aggregate(platform_results: dict[str, dict[str, object] | None]) -> str: + if any(platform not in platform_results or platform_results[platform] is None for platform in PLATFORMS): + return "pending" + return "passing" if all(platform_results[p]["status"] == "pass" for p in PLATFORMS) else "failing" + + +def result_cell(result: dict[str, object] | None) -> str: + if result is None: + return "⏳ Pending" + if result["status"] == "fail": + return "❌ Failed" + mode = "real CLI" if result["verification_mode"] == "real_cli" else "hook verified" + return f"✅ Pass · {mode}" + + +def failed_result(agent: str, platform: str, head_sha: str, run_url: str) -> dict[str, object]: + return { + "schema_version": 1, + "agent": agent, + "platform": platform, + "status": "fail", + "verification_mode": None, + "evidence": "E2E command failed", + "commit_sha": head_sha, + "run_url": run_url, + } + + +def render_comment( + results: dict[str, dict[str, dict[str, object] | None]], + run_urls: dict[str, str], + head_sha: str, +) -> str: + lines = [ + MARKER, + "## Pitot coding-agent E2E", + "", + "Intelligence Flow is the verification source; Pitot's public README carries the latest `main` status.", + "", + "| Agent | Ubuntu | macOS | Result | Evidence |", + "|---|---|---|---|---|", + ] + icons = {"passing": "✅ Passing", "failing": "❌ Failing", "pending": "⏳ Pending"} + for agent in WORKFLOWS: + platforms = results.get(agent, {}) + status = aggregate(platforms) + url = run_urls.get(agent) + label = agent.capitalize() if agent != "opencode" else "OpenCode" + linked_label = f"[{label}]({url})" if url else label + evidence = "Both platforms are required" if status != "pending" else "Waiting for both platform artifacts" + lines.append( + f"| {linked_label} | {result_cell(platforms.get('ubuntu'))} | " + f"{result_cell(platforms.get('macos'))} | {icons[status]} | {evidence} |" + ) + lines.extend(["", f"Source commit: `{head_sha[:12]}`", ""]) + return "\n".join(lines) + + +class GitHub: + def __init__(self, repository: str, token: str) -> None: + self.repository = repository + self.token = token + self.base = f"https://api.github.com/repos/{repository}" + + def request(self, method: str, path: str, payload: object | None = None) -> object: + data = json.dumps(payload).encode() if payload is not None else None + request = urllib.request.Request( + self.base + path, + data=data, + method=method, + headers={ + "Accept": "application/vnd.github+json", + "Authorization": f"Bearer {self.token}", + "Content-Type": "application/json", + "X-GitHub-Api-Version": "2022-11-28", + "User-Agent": "pitot-e2e-reporter", + }, + ) + with urllib.request.urlopen(request, timeout=30) as response: + body = response.read(MAX_ARTIFACT_BYTES) + return json.loads(body) if body else {} + + def download(self, url: str) -> bytes: + request = urllib.request.Request( + url, + headers={"Authorization": f"Bearer {self.token}", "User-Agent": "pitot-e2e-reporter"}, + ) + with urllib.request.urlopen(request, timeout=30) as response: + data = response.read(MAX_ARTIFACT_BYTES + 1) + if len(data) > MAX_ARTIFACT_BYTES: + raise ValueError("artifact exceeds size limit") + return data + + +def load_artifact( + github: GitHub, + artifact: dict[str, object], + *, + agent: str, + platform: str, + expected_sha: str, + expected_run_url: str, +) -> dict[str, object]: + archive = github.download(str(artifact["archive_download_url"])) + with zipfile.ZipFile(io.BytesIO(archive)) as bundle: + names = bundle.namelist() + if names != ["result.json"] or bundle.getinfo("result.json").file_size > MAX_ARTIFACT_BYTES: + raise ValueError("artifact must contain one bounded result.json") + value = json.loads(bundle.read("result.json")) + return validate_result( + value, + agent=agent, + platform=platform, + expected_sha=expected_sha, + expected_run_url=expected_run_url, + ) + + +def main() -> int: + parser = argparse.ArgumentParser(description=__doc__) + parser.add_argument("--event", type=Path, required=True) + parser.add_argument("--repository", required=True) + args = parser.parse_args() + event = json.loads(args.event.read_text(encoding="utf-8")) + workflow_run = event["workflow_run"] + token = os.environ.get("GITHUB_TOKEN") + if not token: + raise SystemExit("GITHUB_TOKEN is required") + github = GitHub(args.repository, token) + pull_requests = workflow_run.get("pull_requests", []) + if not pull_requests: + owner = workflow_run.get("head_repository", {}).get("owner", {}).get("login") + branch = workflow_run.get("head_branch") + if owner and branch: + head = urllib.parse.quote(f"{owner}:{branch}", safe=":") + candidates = github.request("GET", f"/pulls?state=open&head={head}&per_page=20") + pull_requests = [pr for pr in candidates if pr.get("head", {}).get("sha") == workflow_run["head_sha"]] + if not pull_requests: + print("No pull request is associated with this workflow run; nothing to report.") + return 0 + head_sha = workflow_run["head_sha"] + query = urllib.parse.urlencode({"head_sha": head_sha, "event": "pull_request", "per_page": 100}) + runs = github.request("GET", f"/actions/runs?{query}")["workflow_runs"] + + results: dict[str, dict[str, dict[str, object] | None]] = { + agent: {platform: None for platform in PLATFORMS} for agent in WORKFLOWS + } + run_urls: dict[str, str] = {} + for agent, workflow_name in WORKFLOWS.items(): + candidates = [run for run in runs if run.get("name") == workflow_name] + if not candidates: + continue + run = max(candidates, key=lambda value: value["id"]) + run_urls[agent] = run["html_url"] + if run["status"] != "completed": + continue + artifacts = github.request("GET", f"/actions/runs/{run['id']}/artifacts")["artifacts"] + by_name = {artifact["name"]: artifact for artifact in artifacts if not artifact.get("expired")} + for platform in PLATFORMS: + artifact = by_name.get(f"pitot-e2e-{agent}-{platform}") + if artifact is not None: + try: + results[agent][platform] = load_artifact( + github, + artifact, + agent=agent, + platform=platform, + expected_sha=run["head_sha"], + expected_run_url=run["html_url"], + ) + except (ValueError, json.JSONDecodeError, zipfile.BadZipFile): + results[agent][platform] = failed_result(agent, platform, head_sha, run["html_url"]) + elif run.get("conclusion") != "success": + results[agent][platform] = failed_result(agent, platform, head_sha, run["html_url"]) + + body = render_comment(results, run_urls, head_sha) + issue_number = pull_requests[0]["number"] + comments = github.request("GET", f"/issues/{issue_number}/comments?per_page=100") + existing = next( + ( + comment + for comment in comments + if MARKER in comment.get("body", "") + and comment.get("user", {}).get("login") == "github-actions[bot]" + ), + None, + ) + if existing: + github.request("PATCH", f"/issues/comments/{existing['id']}", {"body": body}) + print(f"Updated Pitot E2E report on PR #{issue_number}") + else: + github.request("POST", f"/issues/{issue_number}/comments", {"body": body}) + print(f"Created Pitot E2E report on PR #{issue_number}") + return 0 + + +if __name__ == "__main__": + raise SystemExit(main()) diff --git a/.github/workflows/pitot-e2e-agent.yml b/.github/workflows/pitot-e2e-agent.yml new file mode 100644 index 000000000..6d8b4bd01 --- /dev/null +++ b/.github/workflows/pitot-e2e-agent.yml @@ -0,0 +1,79 @@ +name: Pitot E2E reusable agent check + +on: + workflow_call: + inputs: + agent: + description: Supported Pitot coding-agent host + required: true + type: string + +permissions: + contents: read + +env: + CLAUDE_CLI_VERSION: 2.1.217 + +jobs: + e2e: + name: ${{ inputs.agent }} (${{ matrix.platform }}) + strategy: + fail-fast: false + matrix: + include: + - runner: ubuntu-latest + platform: ubuntu + - runner: macos-latest + platform: macos + runs-on: ${{ matrix.runner }} + steps: + - uses: actions/checkout@v7 + - uses: actions/setup-go@v7 + with: + go-version-file: labs/15-pitot/pitot/go.mod + cache-dependency-path: labs/15-pitot/pitot/go.mod + - uses: actions/setup-node@v4 + with: + node-version: "20" + - name: Install Claude CLI + if: inputs.agent == 'claude' + shell: bash + run: | + npm install -g "@anthropic-ai/claude-code@$CLAUDE_CLI_VERSION" + claude_bin="$(npm prefix -g)/bin/claude" + installed_version="$("$claude_bin" --version)" + echo "Using $claude_bin ($installed_version)" + [[ "$installed_version" == "$CLAUDE_CLI_VERSION"* ]] + echo "CLAUDE_PATH=$claude_bin" >> "$GITHUB_ENV" + - name: Install Codex CLI + if: inputs.agent == 'codex' + run: npm install -g @openai/codex + - name: Install Cursor CLI + if: inputs.agent == 'cursor' + run: curl https://cursor.com/install -fsS | bash + - name: Run ${{ inputs.agent }} E2E + id: e2e + continue-on-error: true + timeout-minutes: 10 + shell: bash + env: + PITOT_SOURCE_SHA: ${{ github.event.pull_request.head.sha || github.sha }} + run: >- + python3 labs/15-pitot/tests/run_e2e_report.py + --agent "${{ inputs.agent }}" + --platform "${{ matrix.platform }}" + --output "${{ runner.temp }}/pitot-e2e/result.json" + -- bash "labs/15-pitot/tests/e2e_${{ inputs.agent }}_cli_test.sh" + < /dev/null + - name: Upload structured E2E result + if: always() + uses: actions/upload-artifact@v4 + with: + name: pitot-e2e-${{ inputs.agent }}-${{ matrix.platform }} + path: ${{ runner.temp }}/pitot-e2e/result.json + if-no-files-found: error + retention-days: 14 + - name: Enforce E2E result + if: always() && steps.e2e.outcome != 'success' + shell: bash + run: exit 1 diff --git a/.github/workflows/pitot-e2e-claude.yml b/.github/workflows/pitot-e2e-claude.yml new file mode 100644 index 000000000..bb8f9989b --- /dev/null +++ b/.github/workflows/pitot-e2e-claude.yml @@ -0,0 +1,19 @@ +name: Pitot E2E — Claude + +on: + pull_request: + paths: &pitot_paths + - "labs/15-pitot/**" + - "go.work" + - ".github/workflows/pitot-e2e-*.yml" + - ".github/scripts/pitot_e2e_report.py" + push: + branches: [main] + paths: *pitot_paths + workflow_dispatch: + +jobs: + verify: + uses: ./.github/workflows/pitot-e2e-agent.yml + with: + agent: claude diff --git a/.github/workflows/pitot-e2e-codex.yml b/.github/workflows/pitot-e2e-codex.yml new file mode 100644 index 000000000..c38752f50 --- /dev/null +++ b/.github/workflows/pitot-e2e-codex.yml @@ -0,0 +1,19 @@ +name: Pitot E2E — Codex + +on: + pull_request: + paths: &pitot_paths + - "labs/15-pitot/**" + - "go.work" + - ".github/workflows/pitot-e2e-*.yml" + - ".github/scripts/pitot_e2e_report.py" + push: + branches: [main] + paths: *pitot_paths + workflow_dispatch: + +jobs: + verify: + uses: ./.github/workflows/pitot-e2e-agent.yml + with: + agent: codex diff --git a/.github/workflows/pitot-e2e-cursor.yml b/.github/workflows/pitot-e2e-cursor.yml new file mode 100644 index 000000000..b5db90d1a --- /dev/null +++ b/.github/workflows/pitot-e2e-cursor.yml @@ -0,0 +1,19 @@ +name: Pitot E2E — Cursor + +on: + pull_request: + paths: &pitot_paths + - "labs/15-pitot/**" + - "go.work" + - ".github/workflows/pitot-e2e-*.yml" + - ".github/scripts/pitot_e2e_report.py" + push: + branches: [main] + paths: *pitot_paths + workflow_dispatch: + +jobs: + verify: + uses: ./.github/workflows/pitot-e2e-agent.yml + with: + agent: cursor diff --git a/.github/workflows/pitot-e2e-gemini.yml b/.github/workflows/pitot-e2e-gemini.yml new file mode 100644 index 000000000..4afd38039 --- /dev/null +++ b/.github/workflows/pitot-e2e-gemini.yml @@ -0,0 +1,19 @@ +name: Pitot E2E — Gemini + +on: + pull_request: + paths: &pitot_paths + - "labs/15-pitot/**" + - "go.work" + - ".github/workflows/pitot-e2e-*.yml" + - ".github/scripts/pitot_e2e_report.py" + push: + branches: [main] + paths: *pitot_paths + workflow_dispatch: + +jobs: + verify: + uses: ./.github/workflows/pitot-e2e-agent.yml + with: + agent: gemini diff --git a/.github/workflows/pitot-e2e-opencode.yml b/.github/workflows/pitot-e2e-opencode.yml new file mode 100644 index 000000000..cf9f7ea96 --- /dev/null +++ b/.github/workflows/pitot-e2e-opencode.yml @@ -0,0 +1,19 @@ +name: Pitot E2E — OpenCode + +on: + pull_request: + paths: &pitot_paths + - "labs/15-pitot/**" + - "go.work" + - ".github/workflows/pitot-e2e-*.yml" + - ".github/scripts/pitot_e2e_report.py" + push: + branches: [main] + paths: *pitot_paths + workflow_dispatch: + +jobs: + verify: + uses: ./.github/workflows/pitot-e2e-agent.yml + with: + agent: opencode diff --git a/.github/workflows/pitot-e2e-report.yml b/.github/workflows/pitot-e2e-report.yml new file mode 100644 index 000000000..371269581 --- /dev/null +++ b/.github/workflows/pitot-e2e-report.yml @@ -0,0 +1,34 @@ +name: Report Pitot E2E results + +on: + workflow_run: + workflows: + - Pitot E2E — Claude + - Pitot E2E — Cursor + - Pitot E2E — Codex + - Pitot E2E — Gemini + - Pitot E2E — OpenCode + types: [completed] + +permissions: + actions: read + contents: read + pull-requests: write + +concurrency: + group: pitot-e2e-report-${{ github.event.workflow_run.head_sha }} + cancel-in-progress: true + +jobs: + report: + if: github.event.workflow_run.event == 'pull_request' + runs-on: ubuntu-latest + steps: + - uses: actions/checkout@v7 + - name: Update sticky PR report + env: + GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} + run: >- + python3 .github/scripts/pitot_e2e_report.py + --event "$GITHUB_EVENT_PATH" + --repository "$GITHUB_REPOSITORY" diff --git a/.github/workflows/pitot-lab.yml b/.github/workflows/pitot-lab.yml index 4fccae0d0..f98329025 100644 --- a/.github/workflows/pitot-lab.yml +++ b/.github/workflows/pitot-lab.yml @@ -46,21 +46,6 @@ jobs: env: GOWORK: "off" run: go test ./... - - name: Install Real Host CLIs - if: matrix.os != 'windows-latest' - run: | - npm install -g @anthropic-ai/claude-code - npm install -g @openai/codex - curl https://cursor.com/install -fsS | bash - - name: Run all E2E Integration Tests - if: matrix.os != 'windows-latest' - timeout-minutes: 10 - run: | - for script in labs/15-pitot/tests/e2e_*_cli_test.sh; do - echo "Running $script" - bash "$script" < /dev/null - done - working-directory: ${{ github.workspace }} - name: Build reference executable env: GOWORK: "off" diff --git a/labs/15-pitot/pitot-distribution/UPSTREAM.json b/labs/15-pitot/pitot-distribution/UPSTREAM.json index 643b97359..488c9bcd7 100644 --- a/labs/15-pitot/pitot-distribution/UPSTREAM.json +++ b/labs/15-pitot/pitot-distribution/UPSTREAM.json @@ -1,7 +1,7 @@ { "files": { "CONTRIBUTING.md": "0613b71aa497f8ca7d7296bf34ade87bfc7237a664d2e812d9b77b3b6befb0ad", - "README.md": "e54193fee53061889b3d5e82df7c76e72e440f265377d260b4a793475c216a7a", + "README.md": "ae604a6d27656532985838f9a988e163652c568bb3d2d110c413b86527f77f10", "adapters/adapters.go": "0d5b03d3e8fd1f302a7b92781038500c8df9a3e3b6eb9e08b243169cda1d81c4", "assets/pitot-boundary.png": "8a0ddb7d81831d94e14813f50ea4ca8670d77417f339ed2f91f0c653bf52f41d", "assets/pitot-boundary.svg": "0c3871d70c84748573f231842091deb38a6def2862403ca34e8cc4493b9c9ebf", diff --git a/labs/15-pitot/pitot-distribution/release-notes/2026-07-22-agent-e2e-status.md b/labs/15-pitot/pitot-distribution/release-notes/2026-07-22-agent-e2e-status.md new file mode 100644 index 000000000..597aac497 --- /dev/null +++ b/labs/15-pitot/pitot-distribution/release-notes/2026-07-22-agent-e2e-status.md @@ -0,0 +1,3 @@ +### Surface coding-agent E2E status + +Pitot's public README now carries per-agent E2E status from Intelligence Flow, while pull requests receive platform-specific evidence for Claude, Cursor, Codex, Gemini, and OpenCode verification. diff --git a/labs/15-pitot/public-readme-preview/README.md b/labs/15-pitot/public-readme-preview/README.md index 66a859541..c7c990042 100644 --- a/labs/15-pitot/public-readme-preview/README.md +++ b/labs/15-pitot/public-readme-preview/README.md @@ -7,7 +7,17 @@
- One language-neutral boundary for Claude Code, Cursor, Codex, and compatible runtimes.
+
+
+
+
+
+
Verified upstream in Intelligence Flow on Ubuntu and macOS.
+ ++ One language-neutral boundary for Claude Code, Cursor, Codex, Gemini, OpenCode, and compatible runtimes.
Pitot lets you build above coding agents without rebuilding every host diff --git a/labs/15-pitot/tests/e2e_unified_runner.sh b/labs/15-pitot/tests/e2e_unified_runner.sh index 6ea6401e0..20f2dcf8a 100755 --- a/labs/15-pitot/tests/e2e_unified_runner.sh +++ b/labs/15-pitot/tests/e2e_unified_runner.sh @@ -32,7 +32,7 @@ cleanup() { trap cleanup EXIT # 3. Path discovery for real host CLI binary -CLAUDE_PATH="/Users/apple/.local/bin/claude" +CLAUDE_PATH="${CLAUDE_PATH:-/Users/apple/.local/bin/claude}" if [ ! -f "$CLAUDE_PATH" ] && which claude &>/dev/null; then CLAUDE_PATH=$(which claude) fi @@ -77,8 +77,8 @@ SETTINGS_EOF # Check if real binary is installed if [ ! -f "$CLAUDE_PATH" ]; then - echo "WARNING: Real 'claude' CLI binary not found on this machine. Simulating success." - exit 0 + echo "===> [FAILURE] Real 'claude' CLI binary not found on this machine." + exit 1 fi echo "===> Launching real Claude CLI against mock API server..." @@ -94,6 +94,7 @@ SETTINGS_EOF if echo "$OUTPUT" | grep -q "E2E Verification Complete"; then echo "===> [SUCCESS] $HOST end-to-end integration test passed perfectly!" + echo "PITOT_E2E_RESULT mode=real_cli" exit 0 else echo "===> [FAILURE] $HOST end-to-end integration test failed." @@ -163,6 +164,7 @@ SETTINGS_EOF if [ "$RUN_REAL_E2E" = true ]; then echo "===> [SUCCESS] $HOST end-to-end integration test passed perfectly using real CLI!" + echo "PITOT_E2E_RESULT mode=real_cli" exit 0 else echo "===> [E2E] Running active Cursor subprocess hook verification..." @@ -174,6 +176,7 @@ SETTINGS_EOF if echo "$OUTPUT" | grep -q '"type":"action.requested"'; then echo "===> [SUCCESS] $HOST active subprocess hook verification passed perfectly!" + echo "PITOT_E2E_RESULT mode=hook_subprocess" exit 0 else echo "===> [FAILURE] $HOST active subprocess hook verification failed." @@ -248,6 +251,7 @@ CONFIG_EOF if [ "$RUN_REAL_E2E" = true ]; then echo "===> [SUCCESS] $HOST end-to-end integration test passed perfectly using real CLI!" + echo "PITOT_E2E_RESULT mode=real_cli" exit 0 else echo "===> [E2E] Running active Codex subprocess hook verification..." @@ -259,6 +263,7 @@ CONFIG_EOF if echo "$OUTPUT" | grep -q '"type":"action.requested"'; then echo "===> [SUCCESS] $HOST active subprocess hook verification passed perfectly!" + echo "PITOT_E2E_RESULT mode=hook_subprocess" exit 0 else echo "===> [FAILURE] $HOST active subprocess hook verification failed." @@ -278,6 +283,7 @@ CONFIG_EOF if echo "$OUTPUT" | grep -q '"type":"action.requested"'; then echo "===> [SUCCESS] $HOST active subprocess hook verification passed perfectly!" + echo "PITOT_E2E_RESULT mode=hook_subprocess" exit 0 else echo "===> [FAILURE] $HOST active subprocess hook verification failed." @@ -296,6 +302,7 @@ CONFIG_EOF if echo "$OUTPUT" | grep -q '"type":"action.requested"'; then echo "===> [SUCCESS] $HOST active subprocess hook verification passed perfectly!" + echo "PITOT_E2E_RESULT mode=hook_subprocess" exit 0 else echo "===> [FAILURE] $HOST active subprocess hook verification failed." diff --git a/labs/15-pitot/tests/mock_anthropic_protocol.js b/labs/15-pitot/tests/mock_anthropic_protocol.js new file mode 100644 index 000000000..b6828cae1 --- /dev/null +++ b/labs/15-pitot/tests/mock_anthropic_protocol.js @@ -0,0 +1,5 @@ +export function isStructuredMetadataRequest(payload) { + return Array.isArray(payload.tools) + && payload.tools.length === 0 + && payload.output_config?.format?.type === 'json_schema'; +} diff --git a/labs/15-pitot/tests/mock_anthropic_protocol_test.js b/labs/15-pitot/tests/mock_anthropic_protocol_test.js new file mode 100644 index 000000000..39803a54d --- /dev/null +++ b/labs/15-pitot/tests/mock_anthropic_protocol_test.js @@ -0,0 +1,19 @@ +import assert from 'node:assert/strict'; +import test from 'node:test'; + +import { isStructuredMetadataRequest } from './mock_anthropic_protocol.js'; + +test('recognizes Claude structured metadata requests without tools', () => { + assert.equal(isStructuredMetadataRequest({ + tools: [], + output_config: { format: { type: 'json_schema' } }, + }), true); +}); + +test('does not classify the tool-enabled E2E request as metadata', () => { + assert.equal(isStructuredMetadataRequest({ + tools: [{ name: 'Bash' }], + output_config: { format: { type: 'json_schema' } }, + }), false); + assert.equal(isStructuredMetadataRequest({ tools: [] }), false); +}); diff --git a/labs/15-pitot/tests/mock_anthropic_server.js b/labs/15-pitot/tests/mock_anthropic_server.js index 5d4602ace..391d7f370 100644 --- a/labs/15-pitot/tests/mock_anthropic_server.js +++ b/labs/15-pitot/tests/mock_anthropic_server.js @@ -1,5 +1,7 @@ import http from 'http'; +import { isStructuredMetadataRequest } from './mock_anthropic_protocol.js'; + const PORT = 8080; const server = http.createServer((req, res) => { @@ -33,6 +35,11 @@ const server = http.createServer((req, res) => { res.setHeader('Cache-Control', 'no-cache'); res.setHeader('Connection', 'keep-alive'); + if (isStructuredMetadataRequest(payload)) { + sendTextResponse(res, requestedModel, 'msg_metadata_42', '{"title":"List directory"}'); + return; + } + // If the last message contains a tool result, we have successfully run the tool! const hasToolResult = lastMessage.content && lastMessage.content.some(c => c.type === 'tool_result'); @@ -180,6 +187,27 @@ function sendSSEEvent(res, eventName, data) { res.write(`data: ${JSON.stringify(data)}\n\n`); } +function sendTextResponse(res, model, messageId, text) { + sendSSEEvent(res, 'message_start', { + type: 'message_start', + message: { id: messageId, type: 'message', role: 'assistant', content: [], model, stop_reason: null, stop_sequence: null, usage: { input_tokens: 10, output_tokens: 1 } } + }); + sendSSEEvent(res, 'content_block_start', { + type: 'content_block_start', + index: 0, + content_block: { type: 'text', text: '' } + }); + sendSSEEvent(res, 'content_block_delta', { + type: 'content_block_delta', + index: 0, + delta: { type: 'text_delta', text } + }); + sendSSEEvent(res, 'content_block_stop', { type: 'content_block_stop', index: 0 }); + sendSSEEvent(res, 'message_delta', { type: 'message_delta', delta: { stop_reason: 'end_turn', stop_sequence: null }, usage: { output_tokens: 10 } }); + sendSSEEvent(res, 'message_stop', { type: 'message_stop' }); + res.end(); +} + server.listen(PORT, () => { console.log(`Mock Anthropic Server running on http://localhost:${PORT}`); }); diff --git a/labs/15-pitot/tests/run_e2e_report.py b/labs/15-pitot/tests/run_e2e_report.py new file mode 100644 index 000000000..86364735d --- /dev/null +++ b/labs/15-pitot/tests/run_e2e_report.py @@ -0,0 +1,78 @@ +#!/usr/bin/env python3 +"""Run one Pitot host E2E script and emit a strict result artifact.""" + +from __future__ import annotations + +import argparse +import json +import os +from pathlib import Path +import re +import subprocess +import sys + + +AGENTS = {"claude", "cursor", "codex", "gemini", "opencode"} +PLATFORMS = {"ubuntu", "macos"} +RESULT_PATTERN = re.compile(r"^PITOT_E2E_RESULT mode=(real_cli|hook_subprocess)$", re.MULTILINE) + + +def result_for(agent: str, platform: str, returncode: int, output: str) -> dict[str, object]: + if agent not in AGENTS: + raise ValueError(f"unsupported agent: {agent}") + if platform not in PLATFORMS: + raise ValueError(f"unsupported platform: {platform}") + + modes = RESULT_PATTERN.findall(output) + passed = returncode == 0 and len(modes) == 1 + mode = modes[0] if passed else None + if passed and mode == "real_cli": + evidence = "real host CLI completed the Pitot integration path" + elif passed: + evidence = "active hook subprocess produced a normalized Pitot action" + elif returncode != 0: + evidence = "E2E command failed" + else: + evidence = "E2E command returned without one valid result marker" + + return { + "schema_version": 1, + "agent": agent, + "platform": platform, + "status": "pass" if passed else "fail", + "verification_mode": mode, + "evidence": evidence, + "commit_sha": os.environ.get("PITOT_SOURCE_SHA", os.environ.get("GITHUB_SHA", "local")), + "run_url": ( + f"{os.environ['GITHUB_SERVER_URL']}/{os.environ['GITHUB_REPOSITORY']}" + f"/actions/runs/{os.environ['GITHUB_RUN_ID']}" + if all( + key in os.environ + for key in ("GITHUB_SERVER_URL", "GITHUB_REPOSITORY", "GITHUB_RUN_ID") + ) + else "local" + ), + } + + +def main() -> int: + parser = argparse.ArgumentParser(description=__doc__) + parser.add_argument("--agent", required=True, choices=sorted(AGENTS)) + parser.add_argument("--platform", required=True, choices=sorted(PLATFORMS)) + parser.add_argument("--output", required=True, type=Path) + parser.add_argument("command", nargs=argparse.REMAINDER) + args = parser.parse_args() + command = args.command[1:] if args.command[:1] == ["--"] else args.command + if not command: + parser.error("a command is required after --") + + completed = subprocess.run(command, text=True, stdout=subprocess.PIPE, stderr=subprocess.STDOUT) + sys.stdout.write(completed.stdout) + result = result_for(args.agent, args.platform, completed.returncode, completed.stdout) + args.output.parent.mkdir(parents=True, exist_ok=True) + args.output.write_text(json.dumps(result, indent=2, sort_keys=True) + "\n", encoding="utf-8") + return 0 if result["status"] == "pass" else 1 + + +if __name__ == "__main__": + raise SystemExit(main()) diff --git a/labs/15-pitot/tests/test_e2e_reporting.py b/labs/15-pitot/tests/test_e2e_reporting.py new file mode 100644 index 000000000..5ece87435 --- /dev/null +++ b/labs/15-pitot/tests/test_e2e_reporting.py @@ -0,0 +1,139 @@ +from __future__ import annotations + +import importlib.util +from pathlib import Path +import subprocess +import unittest + + +ROOT = Path(__file__).resolve().parents[3] + + +def _load(name: str, path: Path): + spec = importlib.util.spec_from_file_location(name, path) + module = importlib.util.module_from_spec(spec) + assert spec.loader is not None + spec.loader.exec_module(module) + return module + + +runner = _load("run_e2e_report", ROOT / "labs/15-pitot/tests/run_e2e_report.py") +reporter = _load("pitot_e2e_report", ROOT / ".github/scripts/pitot_e2e_report.py") + + +def _result(agent="claude", platform="ubuntu", status="pass", mode="real_cli"): + evidence = { + "real_cli": "real host CLI completed the Pitot integration path", + "hook_subprocess": "active hook subprocess produced a normalized Pitot action", + None: "E2E command failed", + }[mode] + return { + "schema_version": 1, + "agent": agent, + "platform": platform, + "status": status, + "verification_mode": mode, + "evidence": evidence, + "commit_sha": "a" * 40, + "run_url": "https://github.com/operatorstack/intelligence-flow/actions/runs/1", + } + + +class ResultGenerationTests(unittest.TestCase): + def test_mock_anthropic_request_classifier(self): + subprocess.run( + ["node", "--test", str(ROOT / "labs/15-pitot/tests/mock_anthropic_protocol_test.js")], + check=True, + stdout=subprocess.PIPE, + stderr=subprocess.STDOUT, + text=True, + ) + + def test_real_cli_marker_passes(self): + result = runner.result_for("claude", "ubuntu", 0, "PITOT_E2E_RESULT mode=real_cli\n") + self.assertEqual(result["status"], "pass") + self.assertEqual(result["verification_mode"], "real_cli") + + def test_hook_marker_passes(self): + result = runner.result_for("gemini", "macos", 0, "PITOT_E2E_RESULT mode=hook_subprocess\n") + self.assertEqual(result["status"], "pass") + self.assertEqual(result["verification_mode"], "hook_subprocess") + + def test_missing_or_duplicate_marker_fails(self): + self.assertEqual(runner.result_for("codex", "ubuntu", 0, "ok\n")["status"], "fail") + duplicate = "PITOT_E2E_RESULT mode=real_cli\n" * 2 + self.assertEqual(runner.result_for("codex", "ubuntu", 0, duplicate)["status"], "fail") + + def test_nonzero_command_fails_even_with_marker(self): + result = runner.result_for("cursor", "ubuntu", 1, "PITOT_E2E_RESULT mode=real_cli\n") + self.assertEqual(result["status"], "fail") + self.assertIsNone(result["verification_mode"]) + + +class ReporterContractTests(unittest.TestCase): + def test_validates_exact_artifact_schema(self): + value = _result() + self.assertIs(reporter.validate_result(value, agent="claude", platform="ubuntu"), value) + value["unexpected"] = True + with self.assertRaises(ValueError): + reporter.validate_result(value, agent="claude", platform="ubuntu") + + def test_rejects_identity_and_evidence_injection(self): + with self.assertRaises(ValueError): + reporter.validate_result(_result(agent="cursor"), agent="claude", platform="ubuntu") + value = _result() + value["evidence"] = "@everyone | injected" + with self.assertRaises(ValueError): + reporter.validate_result(value, agent="claude", platform="ubuntu") + + def test_rejects_untrusted_commit_and_run_url(self): + value = _result() + with self.assertRaises(ValueError): + reporter.validate_result( + value, + agent="claude", + platform="ubuntu", + expected_sha="b" * 40, + ) + value["run_url"] = "https://example.test/injected" + with self.assertRaises(ValueError): + reporter.validate_result(value, agent="claude", platform="ubuntu") + + def test_aggregate_requires_both_platforms(self): + self.assertEqual(reporter.aggregate({"ubuntu": _result(), "macos": None}), "pending") + self.assertEqual( + reporter.aggregate( + { + "ubuntu": _result(), + "macos": _result(platform="macos", mode="hook_subprocess"), + } + ), + "passing", + ) + self.assertEqual( + reporter.aggregate( + { + "ubuntu": _result(), + "macos": _result(platform="macos", status="fail", mode=None), + } + ), + "failing", + ) + + def test_rendered_comment_is_sticky_and_shows_modes(self): + results = { + agent: {"ubuntu": None, "macos": None} for agent in reporter.WORKFLOWS + } + results["claude"] = { + "ubuntu": _result(), + "macos": _result(platform="macos", mode="hook_subprocess"), + } + body = reporter.render_comment(results, {"claude": "https://example.test/run"}, "b" * 40) + self.assertIn(reporter.MARKER, body) + self.assertIn("Pass · real CLI", body) + self.assertIn("Pass · hook verified", body) + self.assertIn("OpenCode", body) + + +if __name__ == "__main__": + unittest.main()