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
177 changes: 177 additions & 0 deletions .github/workflows/functional-tests.yml
Original file line number Diff line number Diff line change
@@ -0,0 +1,177 @@
name: Functional Tests

on:
push:
branches: [main]
paths:
- 'eval/**'

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

[low] trigger-coverage

These paths cover eval framework and scaffold changes, but not model updates. testing-agents.md (line 311) recommends "periodic re-evaluation on a schedule to catch drift from model updates." Worth noting in the ADR which layer handles drift detection and how a model change invalidates or refreshes layer 2 baselines.

Copy link
Copy Markdown
Member Author

Choose a reason for hiding this comment

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

Yes, but - let's leave reasoning about layer 2 tests to a future PR.

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

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

[medium] path-filter-gap

Paths trigger on eval/** and internal/scaffold/** but not internal/cli/run.go or internal/runtime/claude_progress.go — the Go code that emits metrics.json. A regression in metrics emission (wrong field names, missing values) wouldn't trigger this CI gate, so the max_turns and max_cost judges would silently fail.

Suggested fix: Add the metrics code paths:

paths:
  - 'eval/**'
  - 'internal/scaffold/**'
  - 'internal/cli/run.go'
  - 'internal/runtime/claude_progress*.go'

Or document that Go unit tests cover this gap if the omission is intentional.

Copy link
Copy Markdown
Member Author

Choose a reason for hiding this comment

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

Intentional for now — Go unit tests cover metrics emission. Can widen the trigger paths if that changes.

- 'internal/scaffold/**'
pull_request:
branches: [main]
paths:
- 'eval/**'
- 'internal/scaffold/**'
workflow_dispatch:

permissions:
contents: read
id-token: write

concurrency:
group: functional-tests-${{ github.ref }}
cancel-in-progress: true

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

[info] permissions

No environment: declaration on the job to gate secret access. An explicit environment declaration would be defense-in-depth.

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

[info] permissions

No environment: declaration on the job to gate secret access. Documentation states tests require the evals GitHub environment.

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

[info] permissions

No environment: declaration on the job to gate secret access. The documentation states tests require the evals GitHub environment, but the workflow does not declare it.

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

does this mean that if we cancel the test, there would be orphaned repos created that need manual deletion?

Copy link
Copy Markdown
Member Author

Choose a reason for hiding this comment

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

Oo, yes. That would happen.

Copy link
Copy Markdown
Member Author

Choose a reason for hiding this comment

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

But, fwiw, the repos get uuids in the name, so their existence wouldn't cause other tests to fail or back up.

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

well, maybe we should follow up with some cleanup cron jobs just in case


jobs:
functional-tests:
runs-on: ubuntu-latest
timeout-minutes: 45
steps:
- uses: actions/checkout@v6.0.2
with:
submodules: true

- uses: actions/setup-go@v5
with:
go-version-file: go.mod

- uses: actions/setup-python@v6.2.0
with:
python-version: "3.12"

- name: Install uv
uses: astral-sh/setup-uv@v7.6.0

- name: Install agent-eval-harness
# Installs from the git submodule checked out above (submodules: true)
run: uv pip install --system -e 'eval/.agent-eval-harness[anthropic]'

- name: Install yq
run: |
curl -sSfL "https://github.com/mikefarah/yq/releases/download/v4.47.1/yq_linux_amd64" -o /usr/local/bin/yq
chmod +x /usr/local/bin/yq

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

[high] supply-chain

The pip install of agent-eval-harness references a personal fork with a mutable branch name: git+https://github.com/ralphbean/agent-eval-harness.git@worktree-execution-hooks. Branch refs are mutable — a force-push to that branch silently changes the code installed into CI, which runs with access to EVAL_GH_TOKEN, GCP WIF credentials, and other secrets. The git submodule at eval/.agent-eval-harness is correctly pinned to commit 296c33f7f, but the pip install is a separate code path.

Suggested fix: Pin the pip install to a specific commit hash, e.g.: git+https://github.com/ralphbean/agent-eval-harness.git@296c33f7fc467a05c3cec6b12ab264bd04136ee6. Ideally match the submodule commit so only one pin needs updating.

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

[high] supply-chain

The pip install of agent-eval-harness references a personal fork with a mutable branch name: git+https://github.com/ralphbean/agent-eval-harness.git@worktree-execution-hooks. Branch refs are mutable — a force-push to that branch silently changes the code installed into CI, which runs with access to EVAL_GH_TOKEN, GCP WIF credentials, and other secrets.

Suggested fix: Pin the pip install to a specific commit hash, e.g.: git+https://github.com/ralphbean/agent-eval-harness.git@296c33f7fc467a05c3cec6b12ab264bd04136ee6. Ideally match the submodule commit so only one pin needs updating.

- name: Configure git identity
run: |
git config --global user.name "fullsend-eval[bot]"
git config --global user.email "fullsend-eval[bot]@users.noreply.github.com"
Comment thread
ralphbean marked this conversation as resolved.
Resolved
Comment thread
ralphbean marked this conversation as resolved.
Resolved
Comment thread
ralphbean marked this conversation as resolved.
Resolved

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

[medium] supply-chain

The yq binary is downloaded from GitHub Releases without checksum verification. A compromised release could inject a malicious binary that runs with full CI privileges and access to all workflow secrets. The Makefile bootstrap target verifies checksums for similar downloads (e.g., lychee), so this is inconsistent with existing practice.

Suggested fix: Add a sha256sum -c check after downloading yq, or use a published GitHub Action that handles checksums.

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

[medium] supply-chain

The yq binary is downloaded from GitHub Releases without checksum verification. A compromised release could inject a malicious binary that runs with full CI privileges and access to all workflow secrets.

Suggested fix: Add a sha256sum -c check after downloading yq, or use a published GitHub Action that handles checksums.

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

[medium] supply-chain

yq binary downloaded from GitHub Releases without checksum or signature verification. A compromised release could substitute a malicious binary executing with CI runner privileges.

Suggested fix: Add checksum verification after download.

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

[medium] supply-chain

The yq binary is downloaded from GitHub Releases via curl without checksum or signature verification.

Suggested fix: Add SHA-256 checksum verification after download.

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

[low] supply-chain

yq binary downloaded from GitHub Releases without checksum or signature verification.

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

[low] supply-chain

yq binary downloaded from GitHub Releases via curl without checksum or signature verification.


- name: Build fullsend
run: make go-build

- name: Add bin to PATH
run: echo "${{ github.workspace }}/bin" >> "$GITHUB_PATH"

# TODO: The openshell setup below (version, CLI, gateway, Podman,
# gateway start) is duplicated from action.yml. Extract into a
# shared script (e.g. .github/scripts/setup-openshell.sh) so the
# version and config stay in sync across both places.
- name: Set OpenShell version
run: echo "OPENSHELL_VERSION=0.0.38" >> "${GITHUB_ENV}"

- name: Install OpenShell CLI
run: |
uv tool install "openshell==${OPENSHELL_VERSION}"
openshell --version

- name: Download openshell-gateway
run: |
set -euo pipefail
arch="$(uname -m)"
case "${arch}" in
x86_64) ;;
aarch64|arm64) arch=aarch64 ;;
*) echo "::error::Unsupported architecture: ${arch}"; exit 1 ;;
esac
GATEWAY_ASSET="openshell-gateway-${arch}-unknown-linux-gnu.tar.gz"
GATEWAY_URL="https://github.com/NVIDIA/OpenShell/releases/download/v${OPENSHELL_VERSION}/${GATEWAY_ASSET}"
curl -fsSL "${GATEWAY_URL}" -o "/tmp/${GATEWAY_ASSET}"
tar xzf "/tmp/${GATEWAY_ASSET}" -C "${{ runner.temp }}"
rm -f "/tmp/${GATEWAY_ASSET}"

- name: Install Podman
run: |
sudo apt-get update
sudo apt-get install -y podman

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

[low] supply-chain

yq binary downloaded from GitHub Releases via curl without checksum or signature verification.

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

[low] supply-chain

yq binary downloaded via curl without checksum or signature verification.

- name: Configure rootless Podman
run: |
whoami_user="$(whoami)"
grep -q "^${whoami_user}:" /etc/subuid || sudo usermod --add-subuids 100000-165535 --add-subgids 100000-165535 "${whoami_user}"
podman system migrate

- name: Start Podman API service
run: |
SOCKET_PATH="${XDG_RUNTIME_DIR:-/run/user/$(id -u)}/podman/podman.sock"
if [ ! -S "${SOCKET_PATH}" ]; then
mkdir -p "$(dirname "${SOCKET_PATH}")"
podman system service --time=0 "unix://${SOCKET_PATH}" &
for _i in $(seq 1 30); do
[ -S "${SOCKET_PATH}" ] && podman --url "unix://${SOCKET_PATH}" info >/dev/null 2>&1 && break
sleep 1
done
[ -S "${SOCKET_PATH}" ] || { echo "::error::Podman socket not ready"; exit 1; }
fi

- name: Start openshell-gateway
run: |
set -euo pipefail
OPENSHELL_SSH_HANDSHAKE_SECRET="ci-$(openssl rand -hex 16)"
export OPENSHELL_SSH_HANDSHAKE_SECRET
echo "::add-mask::${OPENSHELL_SSH_HANDSHAKE_SECRET}"
export OPENSHELL_SUPERVISOR_IMAGE="ghcr.io/nvidia/openshell/supervisor:dfd47683e7da4f1a4a8fa5d77f92d3696e6a41f9"
"${{ runner.temp }}/openshell-gateway" \
--bind-address 0.0.0.0 \
--health-port 8081 \
--drivers podman \
--disable-tls \
--db-url "sqlite:/tmp/gateway.db?mode=rwc" \
>/tmp/gateway.log 2>&1 &
for _i in $(seq 1 30); do
curl -sf http://127.0.0.1:8081/healthz >/dev/null 2>&1 && break
sleep 2
done
curl -sf http://127.0.0.1:8081/healthz >/dev/null 2>&1 || {
echo "::error::Gateway health check failed"
cat /tmp/gateway.log 2>/dev/null || true
exit 1
}
openshell gateway add http://127.0.0.1:8080 --local --name local
openshell gateway select local

- name: Install validation dependencies
run: pip install --quiet "jsonschema>=4.18.0"

- name: Authenticate to GCP
uses: google-github-actions/auth@v2
with:
workload_identity_provider: ${{ secrets.E2E_GCP_WIF_PROVIDER }}
service_account: ${{ secrets.E2E_GCP_SERVICE_ACCOUNT }}

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

[low] supply-chain

pip install jsonschema>=4.18.0 is unpinned. Pin to a specific version for reproducibility.

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

[low] supply-chain

pip install jsonschema unpinned (>=4.18.0). Pin to a specific version for reproducibility.


- name: Prepare sandbox credentials
run: |
echo "HOST_GOOGLE_APPLICATION_CREDENTIALS=$GOOGLE_APPLICATION_CREDENTIALS" >> "$GITHUB_ENV"
bash internal/scaffold/fullsend-repo/scripts/prepare-sandbox-credentials.sh

- name: Run functional tests
env:
EVAL_ORG: ${{ vars.EVAL_ORG }}
GH_TOKEN: ${{ secrets.EVAL_GH_TOKEN }}
ANTHROPIC_VERTEX_PROJECT_ID: ${{ vars.EVALS_VERTEX_PROJECT_ID }}
GOOGLE_CLOUD_PROJECT: ${{ secrets.E2E_GCP_PROJECT_ID }}
CLOUD_ML_REGION: ${{ vars.EVALS_GCP_REGION }}
EVALS_HOST_CREDENTIALS: ${{ env.HOST_GOOGLE_APPLICATION_CREDENTIALS }}
run: make functional-tests

- name: Scrub secrets from eval results
if: always()
run: find eval/runs/ -name '.eval-env' -delete 2>/dev/null || true; find /tmp/agent-eval/ -name '.eval-env' -delete 2>/dev/null || true

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

[low] supply-chain

jsonschema package installed with floor pin (>=4.18.0) rather than exact version pin.

- name: Upload eval results
if: always()
uses: actions/upload-artifact@v4
with:
name: eval-results

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

[low] supply-chain

pip install jsonschema>=4.18.0 is unpinned. Pin to a specific version for reproducibility.

path: |
eval/runs/
!eval/runs/**/.eval-env
retention-days: 30
1 change: 1 addition & 0 deletions .gitignore
Original file line number Diff line number Diff line change
Expand Up @@ -21,3 +21,4 @@ bin/
.env.*
!.env.example
.transcripts/
eval/runs/
4 changes: 4 additions & 0 deletions .gitmodules
Original file line number Diff line number Diff line change
Expand Up @@ -2,3 +2,7 @@
path = experiments
url = git@github.com:fullsend-ai/experiments.git
branch = main
[submodule "eval/.agent-eval-harness"]

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

[low] supply-chain

The git submodule points to a personal fork (ralphbean/agent-eval-harness) rather than the upstream org (opendatahub-io/agent-eval-harness). While the submodule commit is pinned, using a personal fork means the PR author retains control over the repository including the ability to rewrite history or delete it.

Suggested fix: Point the submodule URL to the upstream repo once the worktree-execution-hooks branch is merged upstream, or fork under the fullsend-ai organization.

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

[high] supply-chain

The agent-eval-harness git submodule points to a personal fork (ralphbean/agent-eval-harness) on branch worktree-execution-hooks, not the upstream (opendatahub-io/agent-eval-harness). CI installs this fork as a Python package, executing arbitrary code with access to secrets. ADR 0047 documents the upstream, creating a discrepancy.

Suggested fix: Point the submodule URL to the upstream repository or an organization fork. Merge required features upstream first.

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

[high] supply-chain

The agent-eval-harness git submodule points to a personal fork (ralphbean/agent-eval-harness) on branch worktree-execution-hooks, not the upstream repository. A personal fork can be force-pushed, deleted, or transferred. ADR 0047 documents upstream as opendatahub-io/agent-eval-harness, creating a discrepancy.

Suggested fix: Point the submodule URL to the upstream repository or an organization fork (fullsend-ai/agent-eval-harness).

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

[high] supply-chain

The agent-eval-harness git submodule points to a personal fork (ralphbean/agent-eval-harness.git) on branch worktree-execution-hooks, not the upstream repository (opendatahub-io/agent-eval-harness). A personal fork can be force-pushed, deleted, or transferred at any time. ADR 0047 documents the upstream as opendatahub-io/agent-eval-harness, creating a discrepancy.

Suggested fix: Point the submodule URL to the upstream repository or an organization fork (fullsend-ai/agent-eval-harness). If the worktree-execution-hooks branch has unreleased features, merge them upstream first.

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

[high] supply-chain

The agent-eval-harness git submodule points to a personal fork (ralphbean/agent-eval-harness) on branch worktree-execution-hooks, not the upstream repository (opendatahub-io/agent-eval-harness). A personal fork can be force-pushed, deleted, or transferred at any time. ADR 0047 documents the upstream as opendatahub-io/agent-eval-harness, creating a discrepancy.

Suggested fix: Point the submodule URL to the upstream repository or an organization fork (fullsend-ai/agent-eval-harness). If the worktree-execution-hooks branch has unreleased features, merge them upstream first.

Copy link
Copy Markdown
Member Author

Choose a reason for hiding this comment

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

This was fixed — .gitmodules already points to opendatahub-io/agent-eval-harness on main.

path = eval/.agent-eval-harness
url = https://github.com/ralphbean/agent-eval-harness.git
branch = worktree-execution-hooks
23 changes: 21 additions & 2 deletions Makefile
Original file line number Diff line number Diff line change
Expand Up @@ -2,7 +2,8 @@
.PHONY: help bootstrap lint lint-all check fmt \
mindmap go-build go-test go-lint go-fmt go-vet go-tidy \
lint-md-links script-test test \
e2e-test e2e-playwright e2e-export-session e2e-upload-session
e2e-test e2e-playwright e2e-export-session e2e-upload-session \
lint-eval-cases functional-tests

# Let Go automatically download the toolchain version required by go.mod.
# This ensures local builds use the right version without manual intervention.
Expand Down Expand Up @@ -30,6 +31,8 @@ help:
@echo " e2e-test - Run admin e2e tests (requires E2E_GITHUB_SESSION_FILE or E2E_GITHUB_USERNAME + E2E_GITHUB_PASSWORD)"
@echo " e2e-export-session - Login to GitHub and export a Playwright session file"
@echo " e2e-upload-session - Export session and upload it as a GitHub repo secret"
@echo " lint-eval-cases - Lint eval case definitions (annotations.yaml completeness)"
@echo " functional-tests - Run functional agent tests (requires EVAL_ORG, FULLSEND_DIR, GH_TOKEN, GCP creds)"

# Install all development tools needed for linting, formatting, and pre-commit hooks.
# Prerequisites: uv (https://docs.astral.sh/uv/) and go (https://go.dev/)
Expand Down Expand Up @@ -118,7 +121,7 @@ script-test:
python3 internal/scaffold/fullsend-repo/scripts/process-fix-result-test.py
python3 skills/topissues/scripts/topissues_test.py

test: lint-all go-test script-test
test: lint-all go-test script-test lint-eval-cases

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

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

[medium] yq-dependency-gap

test now depends on lint-eval-cases, which shells out to yq. But yq is not installed by make bootstrap. Any developer running make test locally gets a cryptic failure. The CI workflow installs yq explicitly (line 49 of functional-tests.yml), but the local dev path is broken.

Suggested fix: Either add yq to make bootstrap, or have lint-eval-cases fail fast with a clear message:

lint-eval-cases:
	@command -v yq >/dev/null 2>&1 || { echo "ERROR: yq required — install from https://github.com/mikefarah/yq"; exit 1; }
	@for agent in $(EVAL_AGENTS); do \
		./eval/lint-cases.sh "$$agent"; \
	done

Copy link
Copy Markdown
Member Author

Choose a reason for hiding this comment

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

Added fail-fast yq check in 994bb1a.


E2E_SESSION_FILE ?= $(CURDIR)/.playwright/session.json

Expand Down Expand Up @@ -149,3 +152,19 @@ e2e-playwright:
echo "==> Installing Playwright Chromium..."; \
go run github.com/playwright-community/playwright-go/cmd/playwright install chromium; \
fi

# Functional agent evals — run agents against ephemeral GitHub repos and judge results.
# Required env: EVAL_ORG (GitHub org for ephemeral repos), plus GCP creds for Vertex AI.
# GH_TOKEN defaults to `gh auth token` if not set.
FULLSEND_DIR ?= $(CURDIR)/internal/scaffold/fullsend-repo
EVAL_AGENTS ?= triage

lint-eval-cases:
@for agent in $(EVAL_AGENTS); do \
./eval/lint-cases.sh "$$agent"; \
done

functional-tests: lint-eval-cases

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

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

[medium] for-loop-error-swallowing

Both functional-tests and lint-eval-cases iterate agents in a shell for loop without set -e. The loop's exit code is the exit code of the last iteration — a failing agent is silently skipped if a subsequent one succeeds. Latent today (only triage in EVAL_AGENTS) but the loop exists to support multiple agents.

Suggested fix:

functional-tests: lint-eval-cases
	@set -e; for agent in $(EVAL_AGENTS); do \
		FULLSEND_DIR="$(FULLSEND_DIR)" ./eval/run-functional.sh "$$agent"; \
	done

Same for lint-eval-cases.

Copy link
Copy Markdown
Member Author

Choose a reason for hiding this comment

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

Added set -e to both loops in 994bb1a.

@for agent in $(EVAL_AGENTS); do \
FULLSEND_DIR="$(FULLSEND_DIR)" ./eval/run-functional.sh "$$agent"; \
done
72 changes: 72 additions & 0 deletions docs/ADRs/0051-agent-eval-harness-for-test-infrastructure.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,72 @@
---

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

[medium] internal-inconsistency

Title/heading numbers (47, 48) do not match filename numbers (0051, 0052).

Suggested fix: Align the title/heading numbers with the filename numbers (51 and 52).

title: "47. agent-eval-harness for test infrastructure"
status: Accepted

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

[medium] internal-inconsistency

The YAML frontmatter title says 47. agent-eval-harness and the H1 heading says # 47. but the filename is 0051-. Similarly, 0052-functional-tests-for-agent-pipelines.md has title/heading number 48 but filename 0052. Cross-references by number are ambiguous.

Suggested fix: Update the YAML title and H1 heading in 0051-.md to 51. and in 0052-.md to 52. to match the filenames.

relates_to:
- testing-agents
topics:
- testing
---

# 47. agent-eval-harness for test infrastructure

Date: 2026-05-29

## Status

Accepted

<!-- Once this ADR is Accepted, its content is frozen. Do not edit the Context,
Decision, or Consequences sections. If circumstances change, write a new
ADR that supersedes this one. Only status changes and links to superseding
ADRs should be added after acceptance. -->

## Context

[ADR 0052](0052-functional-tests-for-agent-pipelines.md) establishes
functional tests as a test category for agent pipelines. That decision is
silent on which framework orchestrates them — it could be custom scripts,
Inspect AI, or something else.

Building test infrastructure from scratch is expensive and tangential to our
core problem. We need test case management, judge orchestration, scoring,
threshold gating, and regression detection. We do not need to build any of
these ourselves.

[agent-eval-harness](https://github.com/opendatahub-io/agent-eval-harness)
is a generic evaluation framework for agents and skills. It provides dataset
management, LLM-graded and deterministic judges, scoring pipelines, MLflow
integration, and an
[opaque CLI runner contract](https://github.com/opendatahub-io/agent-eval-harness/blob/main/docs/opaque-cli-runner-contract.md)
that delegates execution to an external command. The CLI runner was added in
[issue #59](https://github.com/opendatahub-io/agent-eval-harness/issues/59),
which we filed specifically to make `fullsend run` testable without forking
or extending the harness with fullsend-specific code.

## Decision

We adopt agent-eval-harness as the framework for fullsend functional tests.
Fullsend's `eval/scripts/run-fullsend.sh` implements the opaque CLI runner
contract — it accepts a workspace and output directory, runs `fullsend run`
inside a sandbox, and writes captured fixture state to the output directory.
Everything upstream (case iteration, judge invocation, scoring, thresholds)
is handled by agent-eval-harness.

When adding new test capabilities, prefer extending or contributing to
agent-eval-harness over building fullsend-specific tooling.

## Consequences

- Fullsend functional tests inherit agent-eval-harness capabilities (MLflow
logging, pairwise comparison, dataset generation) without building them.
- The opaque CLI runner contract is the integration boundary. Fullsend owns
execution; the harness owns everything else.
- agent-eval-harness becomes a runtime dependency for test execution, adding
a Python dependency alongside the Go codebase.
- Bugs or gaps in agent-eval-harness may require upstream contributions. We
have already done this once (issue #59).
- Future prompt evals (layer 2 in the test pyramid) can reuse the same
harness with a different runner, keeping test infrastructure unified.
- This decision, like any ADR, can be reversed or superseded. If we find a
better framework or discover that agent-eval-harness limits us in practice,
we can switch. The purpose of this ADR is to drive consistency for the
foreseeable future, not to lock us in permanently.
Loading
Loading