Skip to content
Merged
Show file tree
Hide file tree
Changes from 2 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
179 changes: 179 additions & 0 deletions .github/workflows/behaviour-tests.yml
Original file line number Diff line number Diff line change
@@ -0,0 +1,179 @@
name: Behaviour Tests
Comment thread
ifireball marked this conversation as resolved.
Resolved

# Live behaviour tests use pull_request_target so authorized PRs can receive
# the dev-environment secrets. The gate runs on the base checkout before this
# job checks out and executes the PR head.

permissions: {}

on:
push:
branches: [main]
paths:
- 'agents/**'
Comment thread
ifireball marked this conversation as resolved.
Resolved
- 'behaviour/**'
- 'config.yaml'
- 'env/**'
- 'harness/**'
- 'policies/**'
- 'schemas/**'
- 'scripts/**'
Comment thread
qodo-code-review[bot] marked this conversation as resolved.
Resolved
- 'go.mod'
- 'go.sum'
- 'Makefile'
- '.github/workflows/behaviour-tests.yml'
- '.github/actions/check-e2e-authorization/**'
- '.github/scripts/check-e2e-authorization.sh'
pull_request_target:
Comment thread
ifireball marked this conversation as resolved.
Resolved
types: [opened, synchronize, reopened, labeled]
merge_group:
workflow_dispatch:

concurrency:
group: >-
${{ github.event_name == 'pull_request_target'
&& format('behaviour-{0}', github.event.pull_request.number)
|| format('{0}-{1}', github.workflow, github.ref) }}
cancel-in-progress: >-
Comment thread
ifireball marked this conversation as resolved.
Resolved
${{ github.event_name == 'pull_request_target'
|| github.ref != 'refs/heads/main' }}

jobs:
gate:
if: >-
github.event_name == 'pull_request_target' &&
(github.event.action != 'labeled' || github.event.label.name == 'ok-to-test')
runs-on: ubuntu-24.04
timeout-minutes: 5
permissions:
contents: read
pull-requests: write
outputs:
authorized: ${{ steps.auth.outputs.authorized }}
steps:
- uses: actions/checkout@9c091bb21b7c1c1d1991bb908d89e4e9dddfe3e0 # v7.0.0
with:
ref: ${{ github.sha }}

- name: Check PR authorization
id: auth
uses: ./.github/actions/check-e2e-authorization
with:
pr_number: ${{ github.event.pull_request.number }}
repository: ${{ github.repository }}
pr_updated_at: ${{ github.event.pull_request.updated_at }}
event_action: ${{ github.event.action }}
pr_author_association: ${{ github.event.pull_request.author_association }}
pr_author_login: ${{ github.event.pull_request.user.login }}

behaviour:
needs: gate
if: >-
Comment thread
ifireball marked this conversation as resolved.
Resolved
!cancelled() &&
(
(github.event_name == 'push' && github.ref == 'refs/heads/main') ||
github.event_name == 'merge_group' ||
github.event_name == 'workflow_dispatch' ||
(github.event_name == 'pull_request_target' &&
needs.gate.outputs.authorized == 'true')
)
environment: dev
runs-on: ubuntu-24.04
timeout-minutes: 45
permissions:
contents: read
id-token: write
env:
ENVIRONMENT: dev
BEHAVIOUR_SCM: github
BEHAVIOUR_CI: githubactions
BEHAVIOUR_INSTALL_MODE: per-repo
BEHAVIOUR_APP_SET: fullsend-test
BEHAVIOUR_SUITE_NAME: agents-bt-${{ github.run_id }}
Comment thread
ifireball marked this conversation as resolved.
Resolved
steps:
- uses: actions/checkout@9c091bb21b7c1c1d1991bb908d89e4e9dddfe3e0 # v7.0.0
with:
ref: ${{ github.event_name == 'pull_request_target' && github.event.pull_request.head.sha || github.sha }}
persist-credentials: false
allow-unsafe-pr-checkout: ${{ github.event_name == 'pull_request_target' }}
Comment thread
ifireball marked this conversation as resolved.
Resolved

- uses: actions/setup-go@924ae3a1cded613372ab5595356fb5720e22ba16 # v6.5.0
with:
go-version-file: go.mod
cache-dependency-path: go.sum

- name: Authenticate to GCP
uses: google-github-actions/auth@7c6bc770dae815cd3e89ee6cdf493a5fab2cc093 # v3.0.0
with:
workload_identity_provider: ${{ secrets.AGENTS_BT_E2E_GCP_WIF_PROVIDER }}
service_account: ${{ secrets.AGENTS_BT_E2E_GCP_SERVICE_ACCOUNT }}

- name: Resolve pinned fullsend source
env:
GH_TOKEN: ${{ github.token }}
shell: bash
run: |
version="$(go list -m -f '{{.Version}}' github.com/fullsend-ai/fullsend)"
if [[ "$version" =~ -0\.[0-9]{14}-([0-9a-f]+)$ ]]; then
ref="${BASH_REMATCH[1]}"
else
ref="$version"
fi
sha="$(gh api "repos/fullsend-ai/fullsend/commits/${ref}" --jq .sha)"
checkout="${RUNNER_TEMP}/fullsend"
git clone --filter=blob:none --no-checkout \
https://github.com/fullsend-ai/fullsend.git "$checkout"
git -C "$checkout" \
-c "http.extraheader=AUTHORIZATION: bearer ${GH_TOKEN}" \
fetch --depth=1 origin "$sha"
git -C "$checkout" checkout --detach "$sha"
test "$(git -C "$checkout" rev-parse HEAD)" = "$sha"
grep -qx 'module github.com/fullsend-ai/fullsend' "$checkout/go.mod"
test -d "$checkout/cmd/fullsend"
modfile="${RUNNER_TEMP}/agents.mod"
cp go.mod "$modfile"
: > "${RUNNER_TEMP}/agents.sum"
go mod edit -modfile="$modfile" \
-replace="github.com/fullsend-ai/fullsend=${checkout}"
go mod edit -modfile="$modfile" \
-replace="github.com/fullsend-ai/fullsend/internal/mintcore=${checkout}/internal/mintcore"
go mod tidy -modfile="$modfile"
Comment thread
ifireball marked this conversation as resolved.
Resolved
Comment thread
ifireball marked this conversation as resolved.
Resolved
Comment thread
ifireball marked this conversation as resolved.
Resolved
{
echo "FULLSEND_CHECKOUT=$checkout"
echo "FULLSEND_SHA=$sha"
echo "BEHAVIOUR_GOFLAGS=-modfile=$modfile"
} >> "$GITHUB_ENV"
echo "Using fullsend source ${sha} for ${version}"

- name: Run behaviour tests
env:
E2E_GCP_WIF_PROVIDER: ${{ secrets.AGENTS_BT_E2E_GCP_WIF_PROVIDER }}
E2E_GCP_SERVICE_ACCOUNT: ${{ secrets.AGENTS_BT_E2E_GCP_SERVICE_ACCOUNT }}
E2E_GCP_PROJECT_ID: ${{ secrets.AGENTS_BT_E2E_GCP_PROJECT_ID }}
TEST_FULLSEND_PEM: ${{ secrets.AGENTS_BT_FULLSEND_PEM }}
TEST_TRIAGE_PEM: ${{ secrets.AGENTS_BT_TRIAGE_PEM }}
TEST_CODER_PEM: ${{ secrets.AGENTS_BT_CODER_PEM }}
TEST_REVIEW_PEM: ${{ secrets.AGENTS_BT_REVIEW_PEM }}
TEST_RETRO_PEM: ${{ secrets.AGENTS_BT_RETRO_PEM }}
TEST_PRIORITIZE_PEM: ${{ secrets.AGENTS_BT_PRIORITIZE_PEM }}
CLOUDFLARE_ACCOUNT_ID: ${{ secrets.AGENTS_BT_CLOUDFLARE_ACCOUNT_ID }}
CLOUDFLARE_API_TOKEN: ${{ secrets.AGENTS_BT_CLOUDFLARE_API_TOKEN }}
TEST_ACTOR_WRITE_PAT: ${{ secrets.AGENTS_BT_ACTOR_WRITE_PAT }}
TEST_ACTOR_TRIAGE_PAT: ${{ secrets.AGENTS_BT_ACTOR_TRIAGE_PAT }}
TEST_ACTOR_OUTSIDER_PAT: ${{ secrets.AGENTS_BT_ACTOR_OUTSIDER_PAT }}
shell: bash
run: |
required=(
E2E_GCP_WIF_PROVIDER E2E_GCP_SERVICE_ACCOUNT E2E_GCP_PROJECT_ID
TEST_FULLSEND_PEM TEST_TRIAGE_PEM TEST_CODER_PEM TEST_REVIEW_PEM
TEST_RETRO_PEM TEST_PRIORITIZE_PEM CLOUDFLARE_ACCOUNT_ID
CLOUDFLARE_API_TOKEN TEST_ACTOR_WRITE_PAT TEST_ACTOR_TRIAGE_PAT
TEST_ACTOR_OUTSIDER_PAT
Comment thread
ifireball marked this conversation as resolved.
Resolved
)
for name in "${required[@]}"; do
if [[ -z "${!name:-}" ]]; then
echo "missing behaviour credential: ${name}" >&2
exit 1
fi
done
make behaviour-test
6 changes: 3 additions & 3 deletions CONTRIBUTING.md
Original file line number Diff line number Diff line change
Expand Up @@ -58,9 +58,9 @@ Small adjustments in response to review feedback are normal iteration — this g

### Functional tests for external contributors

Functional tests (the `Functional Tests` workflow) run automatically for
org/repo members and collaborators. For other contributors, a maintainer
must add the `ok-to-test` label **after** the latest push.
Functional tests (the `Functional Tests` and `Behaviour Tests` workflows) run
automatically for org/repo members and collaborators. For other contributors,
a maintainer must add the `ok-to-test` label **after** the latest push.

This is a separate gate from GitHub's own first-time-contributor
workflow-approval prompt (the "Approve and run workflows" button). Approving
Expand Down
10 changes: 9 additions & 1 deletion Makefile
Original file line number Diff line number Diff line change
@@ -1,5 +1,5 @@
.DEFAULT_GOAL := help
.PHONY: help script-build check-bundle script-test test lint lint-fix lint-baseline
.PHONY: help script-build check-bundle script-test behaviour-test test lint lint-fix lint-baseline

BUNDLE_SRCS := scripts/pre-code.src.sh scripts/pre-code-jira.src.sh scripts/post-code.src.sh scripts/pre-fix.src.sh scripts/post-fix.src.sh scripts/pre-prioritize.src.sh scripts/post-prioritize.src.sh scripts/pre-retro.src.sh scripts/post-retro.src.sh scripts/pre-review.src.sh scripts/post-review.src.sh scripts/pre-scribe.src.sh scripts/post-scribe.src.sh scripts/pre-triage.src.sh scripts/post-triage.src.sh scripts/validate-code-output.src.sh
BUNDLE_OUTS := $(BUNDLE_SRCS:.src.sh=.sh)
Expand All @@ -14,6 +14,7 @@ help:
@echo " script-build - Bundle .src.sh scripts into committed .sh artifacts"
@echo " check-bundle - Verify committed bundles match script-build output"
@echo " script-test - Run agent shell script unit tests"
@echo " behaviour-test - Run live behaviour tests"
@echo " test - Alias for script-test"
@echo " lint - Lint skills/agents/instructions with skillsaw"
@echo " lint-fix - Apply skillsaw's automatic lint fixes"
Expand Down Expand Up @@ -55,6 +56,10 @@ check-bundle:
SCRIPT_TEST_TARGET ?= source
export SCRIPT_TEST_TARGET

BEHAVIOUR_TEST_EXEC ?= $(CURDIR)/scripts/run-behaviour-test-exec.sh
BEHAVIOUR_TEST_TAGS ?= behaviour
BEHAVIOUR_GOFLAGS ?=

script-test:
$(call run-timed,bash scripts/bundle-sh-test.sh)
$(call run-timed,bash scripts/gitleaks-install-test.sh)
Expand Down Expand Up @@ -90,3 +95,6 @@ script-test:
$(call run-timed,bash .github/scripts/check-rollup-result-test.sh)

test: script-test

behaviour-test:
go test -count=1 -timeout 40m -tags "$(BEHAVIOUR_TEST_TAGS)" $(BEHAVIOUR_GOFLAGS) -exec "$(BEHAVIOUR_TEST_EXEC)" ./behaviour
8 changes: 8 additions & 0 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -33,6 +33,7 @@ scripts/ Pre-scripts (input validation) and post-scripts (forge mutations)
skills/ Reusable skill definitions loaded by agents at runtime
plugins/ Sandbox plugins (e.g. gopls LSP for the code agent)
eval/ Functional eval harness and default online-scoring manifests
behaviour/ Live behaviour test suite (BDD feature specs and runner)
```

## Architecture
Expand All @@ -55,6 +56,13 @@ make test

This is an alias for `make script-test`, which runs the `scripts/*-test.sh` suites. CI also runs `make check-bundle` and executes `make script-test` twice (source and bundled modes) via `.github/workflows/script-test.yml`.

The live behaviour suite runs the isolated dev scenarios described in
[`docs/behaviour-testing.md`](docs/behaviour-testing.md):

```bash
make behaviour-test
```

Lint skills, agents, and instructions with [skillsaw](https://github.com/stbenjam/skillsaw):

```bash
Expand Down
13 changes: 13 additions & 0 deletions behaviour/features/triage.feature
Original file line number Diff line number Diff line change
@@ -0,0 +1,13 @@
Feature: Agents triage behaviour

Scenario: Triage applies the ready-to-code label
Given the enrolled test repository
And a dummy agent that would:
| description | op | args |
| Emit triage JSON | write_fixture | output/agent-result.json, fixtures/triage/sufficient.json |
Comment thread
ifireball marked this conversation as resolved.
Resolved
And an issue
When the issue is labeled "ready-for-triage"
Then the triage workflow completes successfully
And the run selected the "dummy" runtime
And the agent will succeed to Emit triage JSON
And the issue has label "ready-to-code"
24 changes: 24 additions & 0 deletions behaviour/fixtures/triage/sufficient.json
Original file line number Diff line number Diff line change
@@ -0,0 +1,24 @@
{
"action": "sufficient",
"reasoning": "Issue includes clear reproduction steps and environment details.",
"clarity_scores": {
"symptom": 0.9,
"cause": 0.85,
"reproduction": 0.9,
"impact": 0.8,
"overall": 0.87
},
"triage_summary": {
"title": "Login fails",
"severity": "high",
"category": "bug",
"problem": "Application crashes on login",
"root_cause_hypothesis": "Session handling regression",
"reproduction_steps": ["Open app", "Enter credentials", "Click login"],
"environment": "Linux",
"impact": "Users cannot authenticate",
"recommended_fix": "Fix session token validation",
"proposed_test_case": "test_login_success"
},
"comment": "## Triage Summary\n\nThis issue is ready for implementation."
}
24 changes: 24 additions & 0 deletions behaviour/suite_test.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,24 @@
//go:build behaviour

package behaviour_test

import (
"path/filepath"
"runtime"
"testing"

"github.com/fullsend-ai/fullsend/pkg/behaviourtest"
)

func TestBehaviour(t *testing.T) {
_, sourceFile, _, ok := runtime.Caller(0)
if !ok {
t.Fatal("finding behaviour suite source path")
}

root := filepath.Dir(sourceFile)
behaviourtest.RunSuite(t, behaviourtest.SuiteOptions{
FeaturePaths: []string{filepath.Join(root, "features")},
FixturesRoot: "behaviour",
})
}
1 change: 1 addition & 0 deletions docs/README.md
Original file line number Diff line number Diff line change
Expand Up @@ -16,6 +16,7 @@ Reference documentation for the default agents shipped by fullsend.

| Guide | Summary |
|-------|---------|
| [Behaviour testing](behaviour-testing.md) | Live behaviour test suite for agents |
| [Custom network policy](network-policy.md) | Configure sandbox network access for hosts beyond the default allowlist |

For configuration, customization, and building your own agents, see the
Expand Down
77 changes: 77 additions & 0 deletions docs/behaviour-testing.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,77 @@
# Behaviour testing

The live behaviour suite runs only against the fullsend `dev` preview driver.
It never uses the durable stage mint.

## Local checks

The suite is build-tagged and imports fullsend's live runner. The CI workflow
prepares a temporary module file that points at the exact fullsend checkout
before running it. With that same setup, a credential-free compile is:

```bash
go test -tags behaviour -run '^$' -exec ./scripts/run-behaviour-test-exec.sh ./behaviour
Comment thread
ifireball marked this conversation as resolved.
Resolved
```

The live command is:

```bash
make behaviour-test
```

When `FULLSEND_CHECKOUT` is set, the test process runs from that checked-out
fullsend source tree. This is required for vendoring when the suite is run from
the agents module.

## CI credentials

The `dev` GitHub Environment on `fullsend-ai/agents` must contain these
agent-specific secrets:

```text
AGENTS_BT_E2E_GCP_WIF_PROVIDER
AGENTS_BT_E2E_GCP_SERVICE_ACCOUNT
AGENTS_BT_E2E_GCP_PROJECT_ID
AGENTS_BT_CLOUDFLARE_ACCOUNT_ID
AGENTS_BT_CLOUDFLARE_API_TOKEN
AGENTS_BT_FULLSEND_PEM
AGENTS_BT_TRIAGE_PEM
AGENTS_BT_CODER_PEM
AGENTS_BT_REVIEW_PEM
AGENTS_BT_RETRO_PEM
AGENTS_BT_PRIORITIZE_PEM
AGENTS_BT_ACTOR_WRITE_PAT
AGENTS_BT_ACTOR_TRIAGE_PAT
AGENTS_BT_ACTOR_OUTSIDER_PAT
```

These are deliberately separate from existing fullsend `TEST_*`, Cloudflare,
GCP, PEM, and PAT credentials. Do not rotate or overwrite an existing value.
Private keys and PATs should be newly created alongside existing credentials;
the workflow maps the new names to the environment variable names consumed by
the shared fullsend driver.

## Dev pool authorization

The cross-org e2e role must authorize the agents repository in every dev pool
organization while retaining the existing fullsend caller:

```bash
# Run from an exact fullsend checkout with the operator credentials required
# by the fullsend admin command.
for org in halfsend-{01..12}; do
go run ./cmd/fullsend admin foreign allow \
--org "$org" \
--role e2e \
--caller fullsend-ai/agents
done
```

Verify each organization with:

```bash
go run ./cmd/fullsend admin foreign list --org halfsend-01
```

The test Apps and actor permissions must also be present on the dev pool
organizations. Stage organization permissions are intentionally out of scope.
Loading
Loading