Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
221 changes: 182 additions & 39 deletions .github/scripts/collect_harness_e2e_benchmarks.py

Large diffs are not rendered by default.

73 changes: 73 additions & 0 deletions .github/scripts/registry_stack_identity.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,73 @@
#!/usr/bin/env python3
"""Create a deterministic identity for a Registry-resolved iii.lock."""

from __future__ import annotations

import argparse
import hashlib
import json
import re
from pathlib import Path
from typing import Any

import yaml


WORKER_NAME = re.compile(r"^[a-z0-9][a-z0-9_-]*$")
EXACT_VERSION = re.compile(
r"^[0-9]+\.[0-9]+\.[0-9]+(-(experimental|alpha|beta))?$"
)


def stack_identity(lock_path: Path) -> dict[str, Any]:
try:
document = yaml.safe_load(lock_path.read_text()) or {}
except (OSError, yaml.YAMLError) as error:
raise SystemExit(f"invalid_lock: cannot read {lock_path}: {error}") from error

workers = document.get("workers") if isinstance(document, dict) else None
if not isinstance(workers, dict) or not workers:
raise SystemExit("invalid_lock: workers must be a non-empty mapping")

versions: dict[str, str] = {}
for worker, record in sorted(workers.items()):
if not isinstance(worker, str) or not WORKER_NAME.fullmatch(worker):
raise SystemExit(f"invalid_lock: invalid worker name {worker!r}")
if not isinstance(record, dict):
raise SystemExit(f"invalid_lock: {worker} must be a mapping")
version = record.get("version")
if not isinstance(version, str) or not EXACT_VERSION.fullmatch(version):
raise SystemExit(
f"invalid_lock: {worker} must have an exact published version"
)
versions[worker] = version

if "harness" not in versions:
raise SystemExit("invalid_lock: harness is required in the resolved stack")

return {
"schema_version": 1,
"lock_digest": hashlib.sha256(lock_path.read_bytes()).hexdigest(),
"stack_versions": versions,
}


def parse_args() -> argparse.Namespace:
parser = argparse.ArgumentParser()
parser.add_argument("--lock", type=Path, required=True)
parser.add_argument("--output", type=Path)
return parser.parse_args()


def main() -> None:
args = parse_args()
identity = stack_identity(args.lock)
rendered = json.dumps(identity, sort_keys=True)
if args.output:
args.output.parent.mkdir(parents=True, exist_ok=True)
args.output.write_text(rendered + "\n")
print(rendered)


if __name__ == "__main__":
main()
55 changes: 55 additions & 0 deletions .github/scripts/tests/test_collect_harness_e2e_benchmarks.py
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,7 @@
from __future__ import annotations

import json
from dataclasses import replace
from pathlib import Path

import pytest
Expand Down Expand Up @@ -57,6 +58,15 @@ def test_semantic_result_status_uses_blocking_precedence() -> None:
)
== "passed"
)
assert (
semantic_result_status(
passed=True,
hard_gate_failures=0,
technical_failures=0,
infra_failures=1,
)
== "infra_failed"
)


@pytest.mark.parametrize(
Expand Down Expand Up @@ -338,6 +348,51 @@ def test_missing_report_is_visible_and_totals_are_not_fabricated(
}


def test_registry_identity_is_recorded_and_registry_failure_is_infra_failed(
tmp_path: Path,
) -> None:
write_report(tmp_path, subject_id="glm", scenario_id="direct_answer")
directory = tmp_path / "harness-e2e-glm-security_review-results"
directory.mkdir()
(directory / "benchmark-context.json").write_text(
json.dumps({"subject_id": "glm", "scenario_id": "security_review"})
)
(directory / "deployment.json").write_text(
json.dumps(
{
"status": "infra_failed",
"actual_release_version": "1.8.0",
"stack_versions": {"harness": "1.8.0", "state": "0.22.0"},
"stack_lock_digest": "a" * 64,
}
)
)

_, efficiency, snapshot, execution = collect(
replace(
config(tmp_path, ["direct_answer", "security_review"]),
stack_mode="registry",
)
)
efficiency_by_name = by_name(efficiency)
security_extra = json.loads(
efficiency_by_name[
"reliability::glm::security_review::infra_failed"
]["extra"]
)

assert security_extra["status"] == "infra_failed"
assert security_extra["release"]["version"] == "1.8.0"
assert security_extra["stack"]["versions"] == {
"harness": "1.8.0",
"state": "0.22.0",
}
assert security_extra["stack"]["lock_digest"] == "a" * 64
assert snapshot["stack"] == security_extra["stack"]
assert snapshot["subjects"][0]["infra_failures"] == 1
assert execution["reports"][1]["deployment"]["status"] == "infra_failed"


def test_unknown_cost_is_omitted_instead_of_recorded_as_zero(
tmp_path: Path,
) -> None:
Expand Down
58 changes: 58 additions & 0 deletions .github/scripts/tests/test_registry_stack_identity.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,58 @@
from __future__ import annotations

import hashlib
import json
import subprocess
import sys
from pathlib import Path

import pytest
import yaml


SCRIPT = Path(__file__).parents[1] / "registry_stack_identity.py"


def run_identity(
tmp_path: Path, workers: dict[str, dict[str, str]]
) -> subprocess.CompletedProcess[str]:
lock = tmp_path / "iii.lock"
lock.write_text(yaml.safe_dump({"workers": workers}, sort_keys=True))
return subprocess.run(
[sys.executable, str(SCRIPT), "--lock", str(lock)],
text=True,
capture_output=True,
check=False,
)


def test_returns_exact_versions_and_lock_digest(tmp_path: Path) -> None:
workers = {
"harness": {"version": "1.8.0"},
"state": {"version": "0.22.0"},
}
result = run_identity(tmp_path, workers)

assert result.returncode == 0, result.stderr
identity = json.loads(result.stdout)
lock = tmp_path / "iii.lock"
assert identity["stack_versions"] == {"harness": "1.8.0", "state": "0.22.0"}
assert identity["lock_digest"] == hashlib.sha256(lock.read_bytes()).hexdigest()


@pytest.mark.parametrize(
("workers", "message"),
[
({"state": {"version": "0.22.0"}}, "harness is required"),
({"harness": {"version": "latest"}}, "exact published version"),
],
)
def test_rejects_non_identity_locks(
tmp_path: Path,
workers: dict[str, dict[str, str]],
message: str,
) -> None:
result = run_identity(tmp_path, workers)

assert result.returncode != 0
assert message in result.stderr
49 changes: 39 additions & 10 deletions .github/workflows/_harness-e2e.yml
Original file line number Diff line number Diff line change
Expand Up @@ -88,6 +88,16 @@ on:
required: false
type: string
default: '{}'
stack_digest:
description: SHA-256 digest of the resolved registry lock
required: false
type: string
default: ''
resolve_stack:
description: Resolve the live registry stack from latest and freeze its lock
required: false
type: boolean
default: false
validation_profile:
description: Scenario profile resolved for this execution
required: false
Expand Down Expand Up @@ -195,6 +205,7 @@ jobs:
RELEASE_WORKER: ${{ inputs.release_worker }}
RELEASE_VERSION: ${{ inputs.release_version }}
STACK_VERSIONS: ${{ inputs.stack_versions }}
RESOLVE_STACK: ${{ inputs.resolve_stack }}
COVERAGE: ${{ inputs.coverage }}
run: |
set -euo pipefail
Expand All @@ -215,16 +226,22 @@ jobs:
[[ "$CLI_CHANNEL" == "latest" || "$CLI_CHANNEL" == "next" ]]
[[ "$REGISTRY_TAG" =~ ^[A-Za-z0-9._-]+$ ]]
[[ "$RELEASE_WORKER" =~ ^[A-Za-z0-9_-]+$ ]]
[[ -n "$RELEASE_VERSION" ]]
jq -e --arg worker "$RELEASE_WORKER" --arg version "$RELEASE_VERSION" '
type == "object" and
all(to_entries[];
(.key | test("^[a-z0-9][a-z0-9_-]*$")) and
(.value | type == "string" and test("^[0-9]+\\.[0-9]+\\.[0-9]+(-(experimental|alpha|beta))?$"))
) and
.[$worker] == $version
' <<<"$STACK_VERSIONS" >/dev/null
[[ "$COVERAGE" == "false" ]]
if [[ "$RESOLVE_STACK" == "true" ]]; then
[[ "$RELEASE_WORKER" == "harness" ]]
[[ "$RELEASE_VERSION" == "latest" ]]
jq -e 'type == "object" and length == 0' <<<"$STACK_VERSIONS" >/dev/null
else
[[ -n "$RELEASE_VERSION" ]]
jq -e --arg worker "$RELEASE_WORKER" --arg version "$RELEASE_VERSION" '
type == "object" and
all(to_entries[];
(.key | test("^[a-z0-9][a-z0-9_-]*$")) and
(.value | type == "string" and test("^[0-9]+\\.[0-9]+\\.[0-9]+(-(experimental|alpha|beta))?$"))
) and
.[$worker] == $version
' <<<"$STACK_VERSIONS" >/dev/null
fi
;;
*)
echo "::error::stack_mode must be source or registry"
Expand Down Expand Up @@ -510,6 +527,8 @@ jobs:
HARNESS_E2E_RELEASE_RUN_ID: ${{ inputs.release_run_id }}
HARNESS_E2E_SMOKE_RUN_ID: ${{ inputs.smoke_run_id }}
HARNESS_E2E_STACK_VERSIONS: ${{ inputs.stack_versions }}
HARNESS_E2E_STACK_DIGEST: ${{ inputs.stack_digest }}
HARNESS_E2E_RESOLVE_STACK: ${{ inputs.resolve_stack }}
HARNESS_E2E_BIN: ${{ github.workspace }}/target/runner/harness-e2e
HARNESS_E2E_ARTIFACTS_DIR: ${{ github.workspace }}/target/harness-e2e
HARNESS_E2E_SCENARIO: ${{ matrix.scenario }}
Expand Down Expand Up @@ -574,7 +593,11 @@ jobs:
uses: actions/upload-artifact@v6
with:
name: harness-e2e-${{ matrix.subject.id }}-${{ matrix.scenario }}-results
path: target/harness-e2e/results/
path: |
target/harness-e2e/results/
target/harness-e2e/deployment.json
target/harness-e2e/cli-version.txt
target/harness-e2e/stack/
retention-days: 14
if-no-files-found: error

Expand Down Expand Up @@ -676,6 +699,9 @@ jobs:
RELEASE_VERSION: ${{ inputs.release_version }}
RELEASE_URL: ${{ inputs.release_url }}
REGISTRY_TAG: ${{ inputs.registry_tag }}
STACK_MODE: ${{ inputs.stack_mode }}
STACK_VERSIONS: ${{ inputs.stack_versions }}
STACK_DIGEST: ${{ inputs.stack_digest }}
JUDGE_MODEL: ${{ inputs.judge_model }}
JUDGE_PROVIDER: ${{ inputs.judge_provider }}
EXECUTION_RUN_ID: ${{ github.run_id }}
Expand All @@ -700,6 +726,9 @@ jobs:
--release-version "$RELEASE_VERSION" \
--release-url "$RELEASE_URL" \
--registry-tag "$REGISTRY_TAG" \
--stack-mode "$STACK_MODE" \
--stack-versions "$STACK_VERSIONS" \
--stack-digest "$STACK_DIGEST" \
--judge-model "$JUDGE_MODEL" \
--judge-provider "$JUDGE_PROVIDER" \
--execution-run-id "$EXECUTION_RUN_ID" \
Expand Down
26 changes: 7 additions & 19 deletions .github/workflows/harness-e2e-daily.yml
Original file line number Diff line number Diff line change
@@ -1,5 +1,5 @@
name: Harness E2E Daily
run-name: Test · harness_source · ${{ inputs.operation_id || github.event_name }}
run-name: Test · harness_registry · ${{ inputs.operation_id || github.event_name }}

on:
schedule:
Expand Down Expand Up @@ -79,13 +79,15 @@ jobs:
runs: 3
max_parallel: 2
source_ref: ${{ needs.context.outputs.source_sha }}
stack_mode: registry
resolve_stack: true
benchmark_lane: daily
release_tag: daily/${{ needs.context.outputs.benchmark_day }}
release_worker: main
release_version: ${{ needs.context.outputs.benchmark_day }}
release_worker: harness
release_version: latest
release_url: ${{ needs.context.outputs.source_url }}
registry_tag: daily
coverage: true
registry_tag: latest
coverage: false
Comment on lines +82 to +90

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🗄️ Data Integrity & Integration | 🟠 Major | 🏗️ Heavy lift

Each matrix job resolves latest independently, so the daily data point can mix stack versions.

resolve_stack: true makes every subject/scenario job run its own harness@latest resolution. If a release publishes while the matrix runs, jobs resolve different versions.

collect_harness_e2e_benchmarks.py then records more than one entry in stack_observations, and lines 704-708 and 850-854 fall back to the configured stack. Because the daily lane passes no stack_versions and no stack_digest, that fallback is empty. The suite and snapshot metadata for that day would then carry no resolved stack version.

The reusable workflow already accepts stack_digest. Resolving latest once in the build job and passing the resolved digest to the matrix would pin the whole day to one stack.

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In @.github/workflows/harness-e2e-daily.yml around lines 82 - 90, Update the
daily workflow so the build job resolves harness@latest once, captures its
digest, and passes that value as stack_digest to every matrix job. Disable
per-job latest resolution by changing the matrix configuration around
resolve_stack, and ensure the collected benchmark metadata uses the pinned
digest instead of relying on empty stack_versions or stack_observations
fallbacks.

subjects: ${{ inputs.subjects || vars.HARNESS_E2E_SUBJECTS || '[{"id":"anthropic-sonnet","model":"claude-sonnet-4-6","provider":"anthropic"}]' }}
judge_model: ${{ inputs.judge_model || vars.HARNESS_E2E_JUDGE_MODEL || 'claude-sonnet-4-6' }}
judge_provider: ${{ inputs.judge_provider || vars.HARNESS_E2E_JUDGE_PROVIDER || 'anthropic' }}
Expand All @@ -94,17 +96,3 @@ jobs:
openai_api_key: ${{ secrets.OPENAI_API_KEY }}
zai_api_key: ${{ secrets.ZAI_API_KEY }}
deepseek_api_key: ${{ secrets.DEEPSEEK_API_KEY }}

integration:
needs: context
if: github.ref == 'refs/heads/main'
permissions:
actions: read
contents: read
uses: ./.github/workflows/_harness-integration.yml
with:
coverage: true
# rust-cache saving is driven by the coverage input; the pinned engine
# cache key stays owned by cache-warm.
save-cache: false
source_ref: ${{ needs.context.outputs.source_sha }}
1 change: 1 addition & 0 deletions .github/workflows/harness-e2e-main.yml
Original file line number Diff line number Diff line change
Expand Up @@ -44,6 +44,7 @@ jobs:
runs: 1
max_parallel: 2
benchmark_lane: main
coverage: true
subjects: ${{ vars.HARNESS_E2E_SUBJECTS || '[{"id":"anthropic-sonnet","model":"claude-sonnet-4-6","provider":"anthropic"}]' }}
judge_model: ${{ vars.HARNESS_E2E_JUDGE_MODEL || 'claude-sonnet-4-6' }}
judge_provider: ${{ vars.HARNESS_E2E_JUDGE_PROVIDER || 'anthropic' }}
Expand Down
7 changes: 7 additions & 0 deletions harness/tests/e2e/README.md
Original file line number Diff line number Diff line change
Expand Up @@ -231,6 +231,13 @@ fresh stack, and repetitions run sequentially inside that job with unique table,
session, and state namespaces. At most two matrix jobs make live-model calls
concurrently.

The daily lane uses the registry mode to measure the currently published live
stack. It resolves `latest` once per matrix job, records the exact versions and
SHA-256 digest of the resulting `iii.lock`, and treats Registry resolution
failures as `infra_failed`; it does not fall back to a source build. The main
lane keeps the source build and LLVM coverage so operational daily metrics stay
separate from checkout regression coverage.

Comment on lines +234 to +240

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

📐 Maintainability & Code Quality | 🟡 Minor | ⚡ Quick win

A later paragraph now contradicts this one.

Lines 329-331 still state that the daily lane evaluates repository binaries built from the resolved default branch commit, does not install registry artifacts, and leaves registry installation to the quickstart validator. The new paragraph and harness-e2e-daily.yml make the daily lane registry-based. Update lines 329-331.

The table row for Harness E2E Daily at line 222 also omits the new infra_failed classification.

📝 Proposed replacement for lines 329-331
-The daily lane evaluates repository binaries built from the resolved default
-branch commit. It does not install registry artifacts; registry installation
-remains the responsibility of the quickstart validator.
+The daily lane installs the published registry stack and evaluates it against
+the resolved default branch commit. The main lane evaluates repository
+binaries built from the checkout. The quickstart validator remains the
+coverage for the `iii worker add` CLI path.
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@harness/tests/e2e/README.md` around lines 234 - 240, Update the later
daily-lane description in the README to state that it installs and evaluates
registry artifacts resolved from latest, without falling back to source-built
repository binaries; keep the quickstart validator distinction accurate. Also
update the “Harness E2E Daily” table row to include the infra_failed
classification.

The deployed lane is a separate workflow run dispatched by the release smoke
workflow. The release publishes first, the smoke validates the published
installation and exact released version, and only a successful smoke dispatches
Expand Down
Loading
Loading