diff --git a/.github/scripts/pitot_e2e_report.py b/.github/scripts/pitot_e2e_report.py index a9a2dc9b7..0fd008ff1 100644 --- a/.github/scripts/pitot_e2e_report.py +++ b/.github/scripts/pitot_e2e_report.py @@ -22,10 +22,24 @@ "gemini": "Pitot E2E — Gemini", "opencode": "Pitot E2E — OpenCode", } -PLATFORMS = ("ubuntu", "macos") +PLATFORMS = ("ubuntu", "macos", "windows") MAX_ARTIFACT_BYTES = 1_000_000 +class ArtifactRedirectHandler(urllib.request.HTTPRedirectHandler): + def redirect_request(self, request, file_pointer, code, message, headers, new_url): + redirected = super().redirect_request(request, file_pointer, code, message, headers, new_url) + if redirected is None: + return None + source = urllib.parse.urlparse(request.full_url) + target = urllib.parse.urlparse(new_url) + if target.scheme != "https": + raise ValueError("artifact redirect must use HTTPS") + if source.netloc != target.netloc: + redirected.remove_header("Authorization") + return redirected + + def validate_result( value: object, *, @@ -118,8 +132,8 @@ def render_comment( "", "Intelligence Flow is the verification source; Pitot's public README carries the latest `main` status.", "", - "| Agent | Ubuntu | macOS | Result | Evidence |", - "|---|---|---|---|---|", + "| Agent | Ubuntu | macOS | Windows | Result | Evidence |", + "|---|---|---|---|---|---|", ] icons = {"passing": "✅ Passing", "failing": "❌ Failing", "pending": "⏳ Pending"} for agent in WORKFLOWS: @@ -128,10 +142,11 @@ def render_comment( 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" + evidence = "All platforms are required" if status != "pending" else "Waiting for all platform artifacts" lines.append( f"| {linked_label} | {result_cell(platforms.get('ubuntu'))} | " - f"{result_cell(platforms.get('macos'))} | {icons[status]} | {evidence} |" + f"{result_cell(platforms.get('macos'))} | {result_cell(platforms.get('windows'))} | " + f"{icons[status]} | {evidence} |" ) lines.extend(["", f"Source commit: `{head_sha[:12]}`", ""]) return "\n".join(lines) @@ -166,7 +181,8 @@ def download(self, url: str) -> bytes: url, headers={"Authorization": f"Bearer {self.token}", "User-Agent": "pitot-e2e-reporter"}, ) - with urllib.request.urlopen(request, timeout=30) as response: + opener = urllib.request.build_opener(ArtifactRedirectHandler()) + with opener.open(request, timeout=30) as response: data = response.read(MAX_ARTIFACT_BYTES + 1) if len(data) > MAX_ARTIFACT_BYTES: raise ValueError("artifact exceeds size limit") diff --git a/.github/workflows/pitot-e2e-agent.yml b/.github/workflows/pitot-e2e-agent.yml index 6d8b4bd01..0ba379853 100644 --- a/.github/workflows/pitot-e2e-agent.yml +++ b/.github/workflows/pitot-e2e-agent.yml @@ -25,6 +25,8 @@ jobs: platform: ubuntu - runner: macos-latest platform: macos + - runner: windows-latest + platform: windows runs-on: ${{ matrix.runner }} steps: - uses: actions/checkout@v7 @@ -35,12 +37,24 @@ jobs: - uses: actions/setup-node@v4 with: node-version: "20" + - uses: actions/setup-python@v6 + with: + python-version: "3.11" + - name: Resolve Bash runtime + shell: bash + run: | + bash_path="$(command -v bash)" + if [[ "${{ runner.os }}" == "Windows" ]]; then + bash_path="$(cygpath -m "$bash_path")" + fi + echo "PITOT_BASH=$bash_path" >> "$GITHUB_ENV" - 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" + claude_bin="$(command -v claude)" + [[ -n "$claude_bin" ]] installed_version="$("$claude_bin" --version)" echo "Using $claude_bin ($installed_version)" [[ "$installed_version" == "$CLAUDE_CLI_VERSION"* ]] @@ -49,7 +63,7 @@ jobs: if: inputs.agent == 'codex' run: npm install -g @openai/codex - name: Install Cursor CLI - if: inputs.agent == 'cursor' + if: inputs.agent == 'cursor' && runner.os != 'Windows' run: curl https://cursor.com/install -fsS | bash - name: Run ${{ inputs.agent }} E2E id: e2e @@ -59,11 +73,11 @@ jobs: env: PITOT_SOURCE_SHA: ${{ github.event.pull_request.head.sha || github.sha }} run: >- - python3 labs/15-pitot/tests/run_e2e_report.py + python 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" + -- "$PITOT_BASH" "labs/15-pitot/tests/e2e_${{ inputs.agent }}_cli_test.sh" < /dev/null - name: Upload structured E2E result if: always() diff --git a/labs/15-pitot/pitot-distribution/UPSTREAM.json b/labs/15-pitot/pitot-distribution/UPSTREAM.json index 488c9bcd7..d0e55c282 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": "ae604a6d27656532985838f9a988e163652c568bb3d2d110c413b86527f77f10", + "README.md": "20cacdc35bd17d34c8451f8eb87b8bb7a788ca3d3c6c1283e36bfc901f0a4479", "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-windows-agent-e2e.md b/labs/15-pitot/pitot-distribution/release-notes/2026-07-22-windows-agent-e2e.md new file mode 100644 index 000000000..61129844c --- /dev/null +++ b/labs/15-pitot/pitot-distribution/release-notes/2026-07-22-windows-agent-e2e.md @@ -0,0 +1,3 @@ +### Verify every coding-agent integration on Windows + +Pitot's per-agent E2E matrix and pull-request report now require Windows alongside Ubuntu and macOS. Windows results use the same structured, commit-bound evidence artifacts, while host setup preserves real CLI verification where supported and the existing active-hook subprocess boundary where a Windows CLI installer is unavailable. diff --git a/labs/15-pitot/public-readme-preview/README.md b/labs/15-pitot/public-readme-preview/README.md index c7c990042..ae4306338 100644 --- a/labs/15-pitot/public-readme-preview/README.md +++ b/labs/15-pitot/public-readme-preview/README.md @@ -14,7 +14,7 @@ OpenCode E2E

-

Verified upstream in Intelligence Flow on Ubuntu and macOS.

+

Verified upstream in Intelligence Flow on Ubuntu, macOS, and Windows.

One language-neutral boundary for Claude Code, Cursor, Codex, Gemini, OpenCode, and compatible runtimes. diff --git a/labs/15-pitot/tests/e2e_unified_runner.sh b/labs/15-pitot/tests/e2e_unified_runner.sh index 20f2dcf8a..ce0575ed6 100755 --- a/labs/15-pitot/tests/e2e_unified_runner.sh +++ b/labs/15-pitot/tests/e2e_unified_runner.sh @@ -12,7 +12,11 @@ echo "===> [E2E] Starting $HOST + Pitot Hook Integration Test" # 1. Compile the local Go 'pitot' binary echo "===> Compiling pitot binary..." -go build -o labs/15-pitot/tests/pitot labs/15-pitot/pitot/cmd/pitot/main.go +PITOT_BINARY="labs/15-pitot/tests/pitot" +if [ "${RUNNER_OS:-}" = "Windows" ]; then + PITOT_BINARY="${PITOT_BINARY}.exe" +fi +go build -o "$PITOT_BINARY" labs/15-pitot/pitot/cmd/pitot/main.go # 2. Host-specific setup and mocking SERVER_PID="" @@ -24,7 +28,7 @@ cleanup() { if [ -n "$SERVER_PID" ]; then kill "$SERVER_PID" 2>/dev/null || true fi - rm -f labs/15-pitot/tests/pitot + rm -f "$PITOT_BINARY" if [ -n "${MOCK_HOME:-}" ]; then rm -rf "$MOCK_HOME" fi @@ -44,8 +48,11 @@ fi CODEX_PATH="" # Codex CLI path placeholder -# Get absolute path of the compiled pitot binary -PITOT_ABS_PATH=$(pwd)/labs/15-pitot/tests/pitot +# Get an absolute path that the host process can execute on each runner OS. +PITOT_ABS_PATH="$(pwd)/$PITOT_BINARY" +if [ "${RUNNER_OS:-}" = "Windows" ] && command -v cygpath &>/dev/null; then + PITOT_ABS_PATH="$(cygpath -m "$PITOT_ABS_PATH")" +fi # Spin up mock API server on port 8080 for all tests echo "===> Starting local mock API server..." diff --git a/labs/15-pitot/tests/run_e2e_report.py b/labs/15-pitot/tests/run_e2e_report.py index 86364735d..157d7901b 100644 --- a/labs/15-pitot/tests/run_e2e_report.py +++ b/labs/15-pitot/tests/run_e2e_report.py @@ -13,7 +13,7 @@ AGENTS = {"claude", "cursor", "codex", "gemini", "opencode"} -PLATFORMS = {"ubuntu", "macos"} +PLATFORMS = {"ubuntu", "macos", "windows"} RESULT_PATTERN = re.compile(r"^PITOT_E2E_RESULT mode=(real_cli|hook_subprocess)$", re.MULTILINE) @@ -66,8 +66,15 @@ def main() -> int: 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) + completed = subprocess.run( + command, + text=True, + encoding="utf-8", + errors="replace", + stdout=subprocess.PIPE, + stderr=subprocess.STDOUT, + ) + sys.stdout.buffer.write(completed.stdout.encode("utf-8", errors="replace")) 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") diff --git a/labs/15-pitot/tests/test_e2e_reporting.py b/labs/15-pitot/tests/test_e2e_reporting.py index 5ece87435..f64d204b0 100644 --- a/labs/15-pitot/tests/test_e2e_reporting.py +++ b/labs/15-pitot/tests/test_e2e_reporting.py @@ -3,7 +3,10 @@ import importlib.util from pathlib import Path import subprocess +import sys +import tempfile import unittest +import urllib.request ROOT = Path(__file__).resolve().parents[3] @@ -59,6 +62,11 @@ def test_hook_marker_passes(self): self.assertEqual(result["status"], "pass") self.assertEqual(result["verification_mode"], "hook_subprocess") + def test_windows_result_is_supported(self): + result = runner.result_for("opencode", "windows", 0, "PITOT_E2E_RESULT mode=hook_subprocess\n") + self.assertEqual(result["platform"], "windows") + self.assertEqual(result["status"], "pass") + 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 @@ -69,8 +77,66 @@ def test_nonzero_command_fails_even_with_marker(self): self.assertEqual(result["status"], "fail") self.assertIsNone(result["verification_mode"]) + def test_runner_replaces_non_utf8_command_output(self): + with tempfile.TemporaryDirectory() as temp: + output = Path(temp) / "result.json" + completed = subprocess.run( + [ + sys.executable, + str(ROOT / "labs/15-pitot/tests/run_e2e_report.py"), + "--agent", "claude", + "--platform", "windows", + "--output", str(output), + "--", + sys.executable, + "-c", "import sys; sys.stdout.buffer.write(b'\\x96')", + ], + stdout=subprocess.PIPE, + stderr=subprocess.STDOUT, + ) + self.assertNotEqual(completed.returncode, 0) + self.assertTrue(output.is_file()) + class ReporterContractTests(unittest.TestCase): + def test_artifact_redirect_strips_cross_host_credentials(self): + request = urllib.request.Request( + "https://api.github.com/repos/operatorstack/intelligence-flow/actions/artifacts/1/zip", + headers={"Authorization": "Bearer secret"}, + ) + redirected = reporter.ArtifactRedirectHandler().redirect_request( + request, + None, + 302, + "Found", + {}, + "https://artifact.example.test/signed.zip", + ) + self.assertIsNotNone(redirected) + self.assertIsNone(redirected.get_header("Authorization")) + + def test_artifact_redirect_rejects_http_downgrade(self): + request = urllib.request.Request("https://api.github.com/artifact") + with self.assertRaisesRegex(ValueError, "must use HTTPS"): + reporter.ArtifactRedirectHandler().redirect_request( + request, + None, + 302, + "Found", + {}, + "http://artifact.example.test/signed.zip", + ) + + def test_workflow_matrix_covers_every_reported_platform(self): + workflow = (ROOT / ".github/workflows/pitot-e2e-agent.yml").read_text() + runners = {"ubuntu": "ubuntu-latest", "macos": "macos-latest", "windows": "windows-latest"} + self.assertEqual(set(reporter.PLATFORMS), set(runner.PLATFORMS)) + for platform, image in runners.items(): + self.assertIn(f"runner: {image}\n platform: {platform}", workflow) + self.assertIn('bash_path="$(cygpath -m "$bash_path")"', workflow) + self.assertIn('-- "$PITOT_BASH"', workflow) + self.assertNotIn('-- bash "labs/15-pitot/tests/e2e_', workflow) + def test_validates_exact_artifact_schema(self): value = _result() self.assertIs(reporter.validate_result(value, agent="claude", platform="ubuntu"), value) @@ -99,13 +165,24 @@ def test_rejects_untrusted_commit_and_run_url(self): with self.assertRaises(ValueError): reporter.validate_result(value, agent="claude", platform="ubuntu") - def test_aggregate_requires_both_platforms(self): + def test_aggregate_requires_all_platforms(self): self.assertEqual(reporter.aggregate({"ubuntu": _result(), "macos": None}), "pending") self.assertEqual( reporter.aggregate( { "ubuntu": _result(), "macos": _result(platform="macos", mode="hook_subprocess"), + "windows": None, + } + ), + "pending", + ) + self.assertEqual( + reporter.aggregate( + { + "ubuntu": _result(), + "macos": _result(platform="macos", mode="hook_subprocess"), + "windows": _result(platform="windows", mode="hook_subprocess"), } ), "passing", @@ -115,6 +192,7 @@ def test_aggregate_requires_both_platforms(self): { "ubuntu": _result(), "macos": _result(platform="macos", status="fail", mode=None), + "windows": _result(platform="windows", mode="hook_subprocess"), } ), "failing", @@ -122,16 +200,20 @@ def test_aggregate_requires_both_platforms(self): def test_rendered_comment_is_sticky_and_shows_modes(self): results = { - agent: {"ubuntu": None, "macos": None} for agent in reporter.WORKFLOWS + agent: {platform: None for platform in reporter.PLATFORMS} + for agent in reporter.WORKFLOWS } results["claude"] = { "ubuntu": _result(), "macos": _result(platform="macos", mode="hook_subprocess"), + "windows": _result(platform="windows", 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("Windows", body) + self.assertIn("All platforms are required", body) self.assertIn("OpenCode", body)