Skip to content

feat(behaviour): add isolated dev behaviour tests - #1244

Open
ifireball wants to merge 2 commits into
fullsend-ai:mainfrom
ifireball:codex/issue-1242
Open

feat(behaviour): add isolated dev behaviour tests#1244
ifireball wants to merge 2 commits into
fullsend-ai:mainfrom
ifireball:codex/issue-1242

Conversation

@ifireball

Copy link
Copy Markdown
Member

Closes #1242

Summary

  • add the agents behaviour suite, fixtures, and make target
  • run only with ENVIRONMENT=dev
  • vendor the exact pinned fullsend source in CI so the test binary uses fullsend assets
  • use a unique suite name per workflow run
  • map isolated agents credentials into the existing fullsend dev driver interface

Provisioning

  • added a dedicated agents GitHub Actions WIF provider and service account in it-gcp-konflux-dev-fullsend
  • added isolated PEM, actor PAT, Cloudflare, and GCP secrets to the agents dev Environment
  • authorized fullsend-ai/agents in the dev pool organizations while preserving existing callers
  • left existing credentials and all stage configuration unchanged

Validation

  • actionlint passed
  • compile-only behaviour test passed against the exact pinned fullsend commit
  • git diff --check passed

Signed-off-by: Barak Korren <bkorren@redhat.com>
Assisted-by: Codex (gpt-5.6-sol)
@ifireball
ifireball requested a review from a team as a code owner September 10, 2026 11:03
@qodo-code-review

Copy link
Copy Markdown

PR Summary by Qodo

Add isolated dev behaviour tests for agents

🧪 Tests ✨ Enhancement ⚙️ Configuration changes 📝 Documentation 🕐 40+ Minutes

Grey Divider

AI Description

• Adds isolated triage behaviour coverage using fullsend’s shared live test runner.
• Gates secret-bearing CI runs and restricts execution to the dev environment.
• Pins fullsend source and maps dedicated agents credentials into its driver.
Diagram

graph TD
  A["Workflow event"] --> B{"PR event?"} -->|Yes| C["Authorization gate"] --> D["Dev environment"] --> E["GCP WIF"] --> F["Pinned fullsend"] --> G["Behaviour suite"] --> H["Dev driver"]
  B -->|No| D
Loading
High-Level Assessment

The following are alternative approaches to this PR:

1. Use the published module only
  • ➕ Avoids cloning fullsend and generating a temporary module file.
  • ➕ Simplifies CI setup and dependency resolution.
  • ➖ Does not provide the fullsend working-tree assets required by the live runner.
  • ➖ May compile against one revision while executing mismatched external assets.
2. Move scenarios into fullsend
  • ➕ Keeps runner, fixtures, and source assets in one repository.
  • ➕ Eliminates cross-repository checkout and fixture-linking logic.
  • ➖ Moves agents-specific behavior ownership out of the agents repository.
  • ➖ Makes agent changes dependent on coordinated fullsend updates.
3. Export fullsend test assets
  • ➕ Could provide assets through a stable package or configurable path.
  • ➕ Would eventually remove the checkout and symlink workaround.
  • ➖ Requires an upstream fullsend interface change.
  • ➖ Adds migration work before isolated agents tests can run.

Recommendation: Use the PR’s pinned-checkout approach now because it guarantees that the test runner and fullsend assets come from the exact dependency revision while keeping agents scenarios isolated. Exporting configurable test assets from fullsend would be a cleaner long-term option, but it requires upstream changes beyond this PR’s scope.

Files changed (9) +590 / -1

Tests (3) +61 / -0
triage.featureDefine ready-to-code triage scenario +13/-0

Define ready-to-code triage scenario

• Adds a Gherkin scenario that enrolls a repository, injects deterministic triage output, triggers triage, and verifies successful completion and the ready-to-code label.

behaviour/features/triage.feature

sufficient.jsonAdd sufficient-triage result fixture +24/-0

Add sufficient-triage result fixture

• Provides deterministic agent output representing a sufficiently specified issue, including clarity scores, summary data, and the expected comment.

behaviour/fixtures/triage/sufficient.json

suite_test.goRegister the agents behaviour suite +24/-0

Register the agents behaviour suite

• Adds a build-tagged Go test entry point that invokes fullsend’s shared behaviour runner with the agents feature and fixture locations.

behaviour/suite_test.go

Documentation (1) +77 / -0
behaviour-testing.mdDocument behaviour testing and provisioning +77/-0

Document behaviour testing and provisioning

• Documents local and live commands, pinned fullsend checkout requirements, isolated dev secrets, and dev-pool authorization. It explicitly excludes durable stage credentials and permissions.

docs/behaviour-testing.md

Other (5) +452 / -1
behaviour-tests.ymlAdd gated dev behaviour-test workflow +179/-0

Add gated dev behaviour-test workflow

• Adds live behaviour-test triggers for relevant pushes, merge groups, manual runs, and authorized pull requests. The workflow validates isolated credentials, authenticates through GCP WIF, checks out the exact pinned fullsend source, and runs the suite with a unique name.

.github/workflows/behaviour-tests.yml

MakefileAdd configurable behaviour-test target +8/-1

Add configurable behaviour-test target

• Adds a make target that runs the build-tagged behaviour package through the execution wrapper. Runner path, build tags, and Go module flags remain configurable for local and CI use.

Makefile

go.modInitialize Go module with pinned fullsend dependency +5/-0

Initialize Go module with pinned fullsend dependency

• Defines the agents Go module and pins the exact fullsend pseudo-version needed by the shared behaviour-test runner.

go.mod

go.sumLock behaviour runner dependency graph +229/-0

Lock behaviour runner dependency graph

• Records checksums for fullsend and its transitive dependencies so behaviour-test builds resolve reproducibly.

go.sum

run-behaviour-test-exec.shRun tests from the pinned fullsend checkout +31/-0

Run tests from the pinned fullsend checkout

• Adds a go test execution wrapper that resolves the test binary before changing directories. When a fullsend checkout is configured, it temporarily links agents fixtures into that checkout, runs there, and removes the link on exit.

scripts/run-behaviour-test-exec.sh

@fullsend-ai-review

fullsend-ai-review Bot commented Sep 10, 2026

Copy link
Copy Markdown

🤖 Finished Review · ✅ Success · Started 11:05 AM UTC · Completed 11:28 AM UTC

Commit: e8efba6 · View workflow run →

Runtime: pi · Model: sonnet → claude-sonnet-5 · Effort: high · Cost: $5.87

@qodo-code-review

qodo-code-review Bot commented Sep 10, 2026

Copy link
Copy Markdown

Code Review by Qodo

🐞 Bugs (0) 📘 Rule violations (0) 🔗 Cross-repo conflicts (0) 📜 Skill insights (2)

Grey Divider


Action required

1. Fork code receives development secrets ✗ Dismissed 📜 Skill insight ⛨ Security
Description
The behaviour job checks out github.event.pull_request.head.sha during a pull_request_target
run and then executes the checked-out repository through make behaviour-test. When the
authorization gate accepts a fork PR, its head-controlled Makefile and scripts run with the GCP,
Cloudflare, private-key, and actor-token secrets mapped into the job environment.
Code

.github/workflows/behaviour-tests.yml[R109-111]

+          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' }}
Relevance

●●● Strong

The workflow executes fork-controlled code with secrets, matching the repository’s accepted
privileged CI security concerns.

PR-#89
PR-#776

ⓘ Recommendations generated based on similar findings in past PRs

Evidence
Rule 1538384 prohibits pull_request_target workflows from running fork code with repository
secrets. The workflow enables that event, exposes numerous environment secrets, performs an
explicitly unsafe checkout of the PR head, and subsequently runs code from that checkout.

.github/workflows/behaviour-tests.yml[26-27]
.github/workflows/behaviour-tests.yml[92-111]
.github/workflows/behaviour-tests.yml[178-179]
Skill: pr-review

Agent prompt
The issue below was found during a code review. Follow the provided context and guidance below and implement a solution

## Issue description
The `pull_request_target` workflow executes fork-controlled PR code while development credentials are present in the job environment.

## Fix Focus Areas
- .github/workflows/behaviour-tests.yml[26-27]
- .github/workflows/behaviour-tests.yml[92-111]
- .github/workflows/behaviour-tests.yml[178-179]

## Recommended Fix
Do not execute PR-head code in a secret-bearing `pull_request_target` job. Restrict live secret-dependent tests to trusted push or merge-group revisions, and use a secret-free `pull_request` job for fork validation.

ⓘ Copy this prompt and use it to remediate the issue with your preferred AI generation tools


2. Workflow error values stay unsanitized ⊘ Outdated 📜 Skill insight ⛨ Security
Description
Validate behaviour credentials interpolates ${name} into an ::error:: workflow command without
applying a workflow-command sanitizer. Every missing credential reaches this branch, so future
entries containing command delimiters, encoded newlines, ANSI escapes, or control characters would
be emitted without escaping.
Code

.github/workflows/behaviour-tests.yml[136]

+              echo "::error::missing behaviour credential: ${name}"
Relevance

●● Moderate

Sanitization findings have mixed outcomes: similar interpolations were both accepted and rejected
recently.

PR-#90
PR-#148
PR-#776

ⓘ Recommendations generated based on similar findings in past PRs

Evidence
Rules 1538312 and 1538382 require every interpolated GitHub Actions workflow-command value to be
sanitized individually regardless of perceived risk. Line 136 places ${name} directly in the
command body without any sanitization.

.github/workflows/behaviour-tests.yml[127-139]
Skill: code-review
Skill: pr-review

Agent prompt
The issue below was found during a code review. Follow the provided context and guidance below and implement a solution

## Issue description
The credential-validation error command directly interpolates its loop value without sanitizing every workflow-command-sensitive sequence.

## Fix Focus Areas
- .github/workflows/behaviour-tests.yml[127-139]

## Recommended Fix
Add a workflow-command sanitizer that escapes percent signs and newlines and removes command delimiters, ANSI escapes, and control characters. Apply it to `${name}` before constructing the `::error::` command.

ⓘ Copy this prompt and use it to remediate the issue with your preferred AI generation tools


3. Test scenarios embed agent instructions 📜 Skill insight ⛨ Security
Description
triage.feature defines dummy-agent behavior through the instruction-like phrase `a dummy agent
that would and the imperative write_fixture` operation. Loading the scenario feeds this newly
committed instruction pattern into the behavior runner's agent configuration surface.
Code

behaviour/features/triage.feature[R5-7]

+    And a dummy agent that would:
+      | description      | op            | args                                                      |
+      | Emit triage JSON | write_fixture | output/agent-result.json, fixtures/triage/sufficient.json |
Relevance

●● Moderate

No scoped history exists for behaviour scenarios, making this policy interpretation uncertain
despite the explicit instruction-like wording.

ⓘ Recommendations generated based on similar findings in past PRs

Evidence
Rule 1538322 prohibits agent instruction patterns in configuration values and string-like test
content. The Gherkin scenario explicitly introduces what a dummy agent should do and supplies an
operation for it to perform.

behaviour/features/triage.feature[5-7]
Skill: code-review

Agent prompt
The issue below was found during a code review. Follow the provided context and guidance below and implement a solution

## Issue description
The new behavior scenario contains an explicit agent-instruction pattern prohibited in comments, strings, and configuration content.

## Fix Focus Areas
- behaviour/features/triage.feature[5-7]

## Recommended Fix
Represent the dummy output through a non-instructional fixture or structured test configuration supported by the behavior runner. Preserve the asserted triage outcome without embedding role-play or agent-directed wording.

ⓘ Copy this prompt and use it to remediate the issue with your preferred AI generation tools


View high (1)
4. Protected changes require human review 📜 Skill insight § Compliance
Description
The PR adds .github/workflows/behaviour-tests.yml and scripts/run-behaviour-test-exec.sh, both
paths that the governance rule classifies as protected. Issue #1242 and the PR description justify
the behavior-test infrastructure, but these additions still require human approval rather than
automated acceptance.
Code

.github/workflows/behaviour-tests.yml[1]

+name: Behaviour Tests
Relevance

●● Moderate

Protected-path precedent rejects adding approval comments, but does not clearly establish rejecting
the governance finding itself.

PR-#754

ⓘ Recommendations generated based on similar findings in past PRs

Evidence
Rule 1538392 mandates a finding whenever protected governance or infrastructure paths are modified.
This PR adds both a workflow under .github/ and an executable under scripts/; the linked issue
supplies justification but does not remove the human-review requirement.

.github/workflows/behaviour-tests.yml[1-1]
scripts/run-behaviour-test-exec.sh[1-2]
Skill: pr-review



Remediation recommended

5. Schema changes skip live tests ✓ Resolved 🐞 Bug ≡ Correctness
Description
The push.paths filter omits schemas/** even though harness/triage.yaml validates the
scenario's generated result against schemas/triage-result.schema.json. A schema-only push to
main therefore changes whether the triage fixture is accepted without running the new live
behaviour test.
Code

.github/workflows/behaviour-tests.yml[R16-19]

+      - 'env/**'
+      - 'harness/**'
+      - 'policies/**'
+      - 'scripts/**'
Relevance

●●● Strong

PR #78 accepted the same workflow path-filter regression involving omitted runtime inputs.

PR-#78

ⓘ Recommendations generated based on similar findings in past PRs

Evidence
The new workflow lists its main-push triggers without schemas/**, while the triage harness
explicitly uses the triage result schema and the scenario supplies output that must pass that
validation. Past PR #78 documents the same class of regression caused by omitting a runtime input
from a functional workflow's path filter.

.github/workflows/behaviour-tests.yml[12-25]
harness/triage.yaml[33-39]
behaviour/features/triage.feature[5-13]
PR-#78

Agent prompt
The issue below was found during a code review. Follow the provided context and guidance below and implement a solution

## Issue description
The behaviour workflow does not run on main-branch pushes that only change schemas, although the triage harness directly uses the triage result schema during this scenario.

## Fix Focus Areas
- .github/workflows/behaviour-tests.yml[12-25]

## Recommended Fix
Add `schemas/**` to the workflow's `on.push.paths` list so schema-only changes trigger the live behaviour suite.

ⓘ Copy this prompt and use it to remediate the issue with your preferred AI generation tools


Grey Divider

Context sources
✅ Compliance rules (platform): 57 rules
✅ Skills: 4 invoked
  code-review
  code-implementation
  pr-review
  docs-review
✅ Cross-repo context — repo relationships
  Explored: repo: fullsend-ai/fullsend (sha: e57fcc2e)
Review mode: ⚖️ Balanced: Downgraded extended -> standard: change is below the extended eligibility bar (hunks 11/18, lines 591/200; both must reach the floor). Router rationale: This introduces security-sensitive GitHub Actions secret handling and cloud authorization plus several independent workflow, test harness, dependency, and shell-script paths, creating a dense set of easy-to-miss defects.

Grey Divider

Tip of the day
💡 Did you know, you can turn these tips off under Display preferences

More tips ↗ | Customize Qodo ↗ | Qodo docs ↗

Grey Divider

Qodo Logo

Comment thread .github/workflows/behaviour-tests.yml
Comment thread .github/workflows/behaviour-tests.yml Outdated
Comment thread behaviour/features/triage.feature
Comment thread .github/workflows/behaviour-tests.yml
Comment thread .github/workflows/behaviour-tests.yml
@fullsend-ai-review

fullsend-ai-review Bot commented Sep 10, 2026

Copy link
Copy Markdown

Review

Findings

Medium

  • [logic-error] .github/workflows/behaviour-tests.yml:140 — In the "Resolve pinned fullsend source" step, go mod tidy -modfile="$modfile" still runs without -tags behaviour. The only Go file in this module, behaviour/suite_test.go, is guarded by //go:build behaviour, so a tag-less tidy does not see the github.com/fullsend-ai/fullsend import and will drop that require from the temporary modfile (the step also truncates ${RUNNER_TEMP}/agents.sum beforehand, so the temp module graph has nothing to fall back on). go.mod's declared Go version defaults to -mod=readonly, so the subsequent go test -tags behaviour -modfile=... step is likely to fail with an "updates to go.mod needed" error rather than compiling against the pinned checkout. This is the same defect flagged in the prior review at commit e8efba62bfba11fbfc249c1e9eb505c7e629cf45; the workflow changed (secrets were re-scoped) but this tidy invocation did not.
    Remediation: Run go mod tidy -modfile="$modfile" -tags behaviour so tidy resolves the build-tagged import correctly.

  • [protected-path] .github/workflows/behaviour-tests.yml — This PR modifies files under protected paths that require human approval: .github/workflows/behaviour-tests.yml and scripts/run-behaviour-test-exec.sh (both match entries in the active protected-paths list: .github/, scripts/). The PR links to issue feat(behaviour): integrate behaviour test runner in fullsend-ai/agents #1242 and its description explains the rationale (adding the CI workflow and test-exec wrapper the issue's merge criteria call for). Human approval is always required for protected-path changes, regardless of context.

Low

  • [architectural-coherence] .github/workflows/behaviour-tests.yml — Unchanged since the prior review: this workflow copies functional-tests.yml's gate/checkout/authorize pattern (pull_request_target, base-only gate, head checkout only after authorization) but still has only two jobs (gate, behaviour) and omits the stable-named roll-up job (functional-tests-complete in the sibling workflow). That roll-up exists because a skipped job still posts a check run that can trivially satisfy branch protection — see functional-tests.yml's inline reference to issue functional-tests-complete goes red when a label is added to a PR whose tests already passed #954, including the labeled-event case where gate is skipped for a non-ok-to-test label. This is only a problem if the behaviour job is ever added to required branch-protection checks; as written it would reproduce that hole.
    Remediation: If this workflow is intended to gate merges, add a roll-up job mirroring functional-tests-complete/check-rollup-result.sh; otherwise document that it is advisory-only and must not be added to required checks.

Fixed since the prior review (verified against the current PR head):

  • The behaviour-test Makefile target now runs go test -count=1 -timeout 40m -tags "$(BEHAVIOUR_TEST_TAGS)" $(BEHAVIOUR_GOFLAGS) -exec "$(BEHAVIOUR_TEST_EXEC)" ./behaviour.
  • The behaviour job's secrets (AGENTS_BT_* PEMs, PATs, Cloudflare credentials) are now scoped to only the final "Run behaviour tests" step instead of the whole job.
  • docs/README.md's Guides table, README.md's Repository structure and Testing sections, and CONTRIBUTING.md's ok-to-test gate description have all been updated to cover the new behaviour-testing surface.
  • The behaviour-test target and its BEHAVIOUR_TEST_* variables were relocated next to the other test targets in the Makefile.
  • scripts/run-behaviour-test-exec.sh now has a descriptive header comment.

Next steps:

  • /fs-fix — agent addresses review findings automatically
  • /fs-fix <your instruction> — agent fixes with your specific guidance
  • Push commits directly — review re-runs automatically on push
  • /fs-fix-stop — disable automatic fix runs for this PR
Previous run

Review

Findings

Medium

  • [logic-error] Makefile:46 — The behaviour-test target runs go test with neither -count=1 nor an explicit -timeout. The default Go test timeout is 10 minutes while the CI job budget (.github/workflows/behaviour-tests.yml, behaviour job) is 45 minutes, so a live run legitimately waiting on the dev environment can be killed with most of the job budget unused, and a genuine hang is indistinguishable from that. Without -count=1, a cached pass from an unrelated prior run is also possible.
    Remediation: Change the target to go test -count=1 -timeout 40m -tags "$(BEHAVIOUR_TEST_TAGS)" $(BEHAVIOUR_GOFLAGS) -exec "$(BEHAVIOUR_TEST_EXEC)" ./behaviour.

  • [logic-error] .github/workflows/behaviour-tests.yml:170 — In the "Resolve pinned fullsend source" step, go mod tidy -modfile="$modfile" runs without -tags behaviour. The only Go file in this module, behaviour/suite_test.go, is guarded by //go:build behaviour, so a tag-less go mod tidy does not see the github.com/fullsend-ai/fullsend import. Since go.mod declares go 1.26.5, -mod=readonly applies by default, so the subsequent go test -tags behaviour step is likely to fail with an "updates to go.mod needed" error rather than silently succeeding.
    Remediation: Run go mod tidy -modfile="$modfile" -tags behaviour so tidy resolves the build-tagged import correctly.

  • [secret-exposure] .github/workflows/behaviour-tests.yml:92 — The behaviour job's job-level env: block (lines 85-105) injects six GitHub App private keys (TEST_FULLSEND_PEM, TEST_TRIAGE_PEM, TEST_CODER_PEM, TEST_REVIEW_PEM, TEST_RETRO_PEM, TEST_PRIORITIZE_PEM), three actor PATs, a Cloudflare account id/API token, and GCP WIF identifiers into every step of the job — including checkout, setup-go, and "Resolve pinned fullsend source", none of which need these secrets. Only the final "Run behaviour tests" step (which executes make behaviour-test, compiling and running PR-controlled Go code under ./behaviour) actually needs them. The sibling functional-tests.yml scopes its secrets (EVAL_GH_TOKEN, GCP identifiers) to only the step that uses them. Widening the exposure window increases blast radius unnecessarily, even though these are described as isolated dev-environment credentials and the job is already gated by check-e2e-authorization.
    Remediation: Move the AGENTS_BT_* secret mappings from the job-level env: block to the env: block of only the final "Run behaviour tests" step; keep non-secret BEHAVIOUR_* configuration knobs at job level.

  • [documentation-currency] docs/README.md:15 — docs/README.md's "Guides" table previously indexed every guide file under docs/ (network-policy.md was the only entry, and it exists). This PR adds docs/behaviour-testing.md but does not add a corresponding row, breaking that previously-exhaustive coverage of guide files.
    Remediation: Add | [Behaviour testing](behaviour-testing.md) | Live behaviour test suite for agents | under the Guides table.

  • [protected-path] .github/workflows/behaviour-tests.yml — This PR modifies files under protected paths that require human approval: .github/workflows/behaviour-tests.yml and scripts/run-behaviour-test-exec.sh (both match entries in the active protected-paths list: .github/, scripts/). The PR links to issue feat(behaviour): integrate behaviour test runner in fullsend-ai/agents #1242 and its description explains the rationale (adding the CI workflow and test-exec wrapper the issue's merge criteria call for). Human approval is always required for protected-path changes, regardless of context.

Low

  • [architectural-coherence] .github/workflows/behaviour-tests.yml — This workflow explicitly models itself on functional-tests.yml's gate/checkout/authorize pattern (per its own header comment) but omits the stable-named roll-up job (functional-tests-complete in the sibling workflow) that exists specifically because a skipped gate job still posts a check run that can trivially satisfy branch protection (see functional-tests.yml's inline reference to issue functional-tests-complete goes red when a label is added to a PR whose tests already passed #954). This is only a problem if the behaviour job is ever added to required branch-protection checks, but as written it would reproduce the exact hole functional-tests-complete goes red when a label is added to a PR whose tests already passed #954 fixed.
    Remediation: If this workflow is intended to gate merges, add a roll-up job mirroring functional-tests-complete; otherwise document that it is advisory-only and must not be added to required checks.

  • [code-organization] Makefile:43 — The new behaviour-test target (lines 45-46) is inserted between the script-build target (line 43) and its prerequisite pattern rule scripts/%.sh: ... (line 48), splitting a cohesive script-bundling unit. The other test targets (script-test, test) are grouped together later in the file.
    Remediation: Move the BEHAVIOUR_TEST_* variables and the behaviour-test target next to the other test targets, keeping script-build and scripts/%.sh contiguous.

  • [documentation-comment-format] scripts/run-behaviour-test-exec.sh:1 — The script has no top-level header comment. Other scripts in scripts/ consistently start with a descriptive header documenting purpose, invocation contract, and expected environment variables.
    Remediation: Add a header comment describing the script's role as a go test -exec wrapper and the FULLSEND_CHECKOUT behavior.

  • [documentation-currency] README.md:24 — README.md's "Repository structure" block documents top-level product/test directories (including eval/, the functional eval harness) but does not list the new top-level behaviour/ directory this PR adds.
    Remediation: Add behaviour/ Live behaviour test suite (BDD feature specs and runner) to the Repository structure block.

  • [documentation-currency] README.md:48 — README.md's "Testing" section documents make test/make script-test and make lint but does not mention the new make behaviour-test target, even though make help now lists it alongside the others.
    Remediation: Add a short paragraph and make behaviour-test example to the Testing section, linking to docs/behaviour-testing.md.

  • [documentation-currency] CONTRIBUTING.md:61 — CONTRIBUTING.md describes the ok-to-test label gate only in terms of the Functional Tests workflow. The new .github/workflows/behaviour-tests.yml reuses the identical pull_request_target + check-e2e-authorization + ok-to-test gate, but CONTRIBUTING.md is not updated to mention it.
    Remediation: Update the paragraph to note that both Functional Tests and Behaviour Tests require the ok-to-test label for external contributors.


Next steps:

  • /fs-fix — agent addresses review findings automatically
  • /fs-fix <your instruction> — agent fixes with your specific guidance
  • Push commits directly — review re-runs automatically on push
  • /fs-fix-stop — disable automatic fix runs for this PR

fullsend-ai-review[bot]

This comment was marked as outdated.

Scope behaviour-test credentials to the execution step, cover schema changes, and document the new suite. Make live tests uncached with an explicit timeout and keep the execution wrapper and Makefile organized.

Signed-off-by: Barak Korren <bkorren@redhat.com>
Assisted-by: Codex (gpt-5.6-sol)
@fullsend-ai-review

fullsend-ai-review Bot commented Sep 10, 2026

Copy link
Copy Markdown

🤖 Finished Review · ✅ Success · Started 12:03 PM UTC · Completed 12:20 PM UTC

Commit: 1593b59 · View workflow run →

Runtime: pi · Model: sonnet → claude-sonnet-5 · Effort: high · Cost: $4.46

@fullsend-ai-review fullsend-ai-review Bot added the risk/elevated PR risk: elevated label Sep 10, 2026
@fullsend-ai-review

Copy link
Copy Markdown

Risk Assessment: elevated (3/5)

Details

Large blast-radius PR (12 files, 610 lines) touching two protected paths (CI workflow, CONTRIBUTING.md) with a new dependency-affecting go.mod/go.sum and low test-file ratio, but the shared docs/config files it modifies show high recent churn and multi-author activity; issue linkage shows scope aligned with acceptance criteria and an additive, dev-gated, easily revertible design, yielding an elevated but not high composite score.

@fullsend-ai-review fullsend-ai-review Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

See the review comment for full details.

Comment thread .github/workflows/behaviour-tests.yml

@waynesun09 waynesun09 left a comment

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.

Review-only pass on the behaviour-test infrastructure, verified against the pinned fullsend commit c85577b87895 and reproduced locally with go1.26.5 where a command is quoted. 8 inline findings (2 high, 6 medium); nothing here duplicates an open or already-addressed thread. No approval or change request implied — flagging for your judgement.

The two high findings are the ones I would weigh before merge: the suite resolves agent content from agents@main rather than the PR head (so harness/**, scripts/**, schemas/** and policies/** are not actually under test at PR time, despite the path filter and the new CONTRIBUTING wording), and the committed module cannot be built or tidied outside the CI-generated modfile, which Renovate's gomod manager will hit with patch automerge enabled.

push:
branches: [main]
paths:
- 'agents/**'

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.

[HIGH] Behaviour runs exercise agents@main, not the PR head — harness/scripts/schemas/policies are never under test at PR time

Verified end-to-end against the pinned fullsend commit c85577b87895:

  1. pkg/behaviourtest/run.go:61 builds the CLI via e2etest.BuildModuleBinary, which is buildCLIBinary in pkg/e2etest/build.go:42 — a plain go build -o <bin> ./cmd/fullsend/ with NO ldflags.
  2. internal/cli/root.go:10-11 therefore leaves version = "dev" and commitSHA = "dev", so resolveBuildVersion() returns ("", "").
  3. internal/cli/run.go:5303 resolveAgentsRef() returns ("main", "heads/main") for any non-release build, and fetchPinnedAgentsRepoFile (run.go:5360) resolves heads/main on fullsend-ai/agents with no env, flag, or config override anywhere in the tree (git grep AGENTS_REF finds nothing).
  4. pkg/behaviourtest/drivers/install/common/setup.go:26 — the dev/preview driver uses vendored mode by default (Vendor: true, FullsendRef: ""), so this dev-stamped binary is what runs fullsend run inside the pool repo.

Consequence: the vendored binary fetches harness/triage.yaml, the pre/post scripts, and the result schema from fullsend-ai/agents@main, NOT from the PR head. Only behaviour/features (absolute path via runtime.Caller in behaviour/suite_test.go:19-22) and behaviour/fixtures (via the exec wrapper's symlink + findModuleSubdir) come from the PR.

This contradicts what the PR advertises: the push paths: filter lists harness/**, policies/**, scripts/**, schemas/** (lines 13-24), and CONTRIBUTING.md now tells external contributors that Behaviour Tests is a gate on their change. A PR that breaks scripts/post-triage.sh or harness/triage.yaml still passes this suite green (the scenario's ready-to-code assertion is satisfied by main's content) and only breaks after merge. The sibling .github/workflows/functional-tests.yml explicitly solves this by threading needs.detect.outputs.agents_sha into its checkout (lines 110, 225-230); this workflow has no equivalent.

Suggestion: Either (a) add an agents-ref override upstream in fullsend that resolveAgentsRef honours for dev builds and thread the PR head SHA through the driver (e.g. FULLSEND_AGENTS_REF), or (b) if that is out of scope for this PR, state plainly in docs/behaviour-testing.md and the workflow header that PR/merge-group runs validate the feature specs and fixtures only, that agent content is resolved from agents@main and is exercised solely by the post-merge push run, and drop the Behaviour Tests claim from the CONTRIBUTING external-contributor gate wording so the coverage promise matches reality.

Comment thread go.mod

go 1.26.5

require github.com/fullsend-ai/fullsend v0.43.1-0.20260909221251-c85577b87895

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.

[HIGH] Committed go.mod/go.sum is not a buildable or tidy-able module — Renovate (with automerge on patch) will break on it

Distinct from the go mod tidy -tags thread at behaviour-tests.yml:140, and that dismissal was correct: my run confirms tidy IS tag-agnostic (it did see ./behaviour). The failure here is the nested mintcore module, not build tags.

Reproduced with go1.26.5 against the exact committed files at head 1593b59:

  • go test -tags behaviour -run '^$' ./behaviour -> go: updates to go.mod needed; to update it: go mod tidy
  • go mod tidy -> reading github.com/fullsend-ai/fullsend/internal/mintcore/go.mod at revision internal/mintcore/v0.0.0: unknown revision (two chains: pkg/behaviourtest -> pkg/e2etest -> internal/mintcore, and -> internal/cli -> internal/mintcore/mintconsts)
  • The committed go.sum is 229 lines with ZERO mintcore entries, and go.mod (5 lines) declares no indirect requires.

Root cause is upstream: internal/mintcore is an unpublished nested module whose replace in fullsend's own go.mod does not propagate to consumers. Only the CI step at behaviour-tests.yml:133-140 makes it build, by copying go.mod to a temp modfile, truncating the sum file, and adding two -replace directives pointing at the cloned checkout.

This matters concretely because the repo has renovate.json extending config:recommended, which enables the gomod manager. Its only disable rule is scoped to matchManagers: ["github-actions"], so nothing exempts gomod. Renovate will now open github.com/fullsend-ai/fullsend bumps whose go.sum artifact regeneration (go mod tidy) hits the failure above — and the config sets automerge: true for patch/pin updates with platformAutomerge, so a bump with a stale/unregenerated go.sum can land unreviewed.

Suggestion: File and reference the upstream root cause (publish or fold internal/mintcore, or break the pkg/e2etest -> internal/cli import). In the meantime: (1) add a gomod disable/ignore rule to renovate.json for github.com/fullsend-ai/fullsend so the bot cannot automerge a module it cannot tidy; (2) state in docs/behaviour-testing.md that this module builds only via the CI-generated modfile and that plain go build/go mod tidy will fail; (3) consider a CI guard that regenerates the modfile and diffs it against a committed artefact so drift is caught.

This also blocks adding a cheap credential-free go test -tags behaviour -run '^$' compile lane to default CI, which is the only way a PR that breaks behaviour/suite_test.go compilation would be caught outside the 45-minute secretful job.

BEHAVIOUR_CI: githubactions
BEHAVIOUR_INSTALL_MODE: per-repo
BEHAVIOUR_APP_SET: fullsend-test
BEHAVIOUR_SUITE_NAME: agents-bt-${{ github.run_id }}

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] Per-run BEHAVIOUR_SUITE_NAME leaks one durable Cloudflare Worker script per workflow run, forever

Verified against pinned fullsend c85577b87895:

  • repopool_cfmint_previews.go:311 envSuiteName() reads BEHAVIOUR_SUITE_NAME and defaults to "bt". Upstream .github/workflows/e2e.yml does NOT set this variable at all — every upstream run shares the single Worker bt-mint.
  • CFMintWorkerName(suiteName) = suiteName + "-mint" (repopool_cfmint_previews.go, ~line 176).
  • internal/dispatch/cf/provisioner.go (preview branch, ~lines 249-268): when the Worker script does not exist, it performs a one-time bootstrap DURABLE deploy so wrangler versions upload can target it.
  • Provisioner.Teardown (provisioner.go, ~lines 336-345): for DeployPreview it abandons the alias and explicitly does NOT delete the durable Worker script — return nil.
  • pkg/e2etest/cleanup.go CleanupStaleResources is forge-org scoped (repos, org secrets, locks); it contains no Cloudflare Worker enumeration or prefix cleanup.

So BEHAVIOUR_SUITE_NAME: agents-bt-${{ github.run_id }} (line 92) yields a brand-new agents-bt-<run_id>-mint Worker script on every single run, each of which triggers a fresh bootstrap durable deploy and is never deleted by anything. Per-run isolation is already provided by the random preview alias bt-<8hex> from GenerateCFMintPreviewAlias() (repopool_cfmint_previews.go:206), so the unique suite name buys no isolation while accumulating Cloudflare Worker scripts without bound against the account.

Suggestion: Use a stable per-consumer suite name — BEHAVIOUR_SUITE_NAME: agents-bt — so all runs share one bootstrapped agents-bt-mint script and rely on the random preview alias for run isolation, exactly as upstream does with the default bt.

${{ github.event_name == 'pull_request_target'
&& format('behaviour-{0}', github.event.pull_request.number)
|| format('{0}-{1}', github.workflow, github.ref) }}
cancel-in-progress: >-

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] Any label event cancels the in-flight 45-minute run, and the replacement run does nothing — skipping all driver teardown

pull_request_target fires on types: [opened, synchronize, reopened, labeled] (line 28); the concurrency group for that event is keyed only on format('behaviour-{0}', github.event.pull_request.number) (line 35) with cancel-in-progress: true for every pull_request_target action (lines 37-39). Adding ANY label to a PR (e.g. documentation, size/L) therefore lands in the same group and cancels a running suite.

The replacement run then does nothing: gate is skipped by its own if (line 45, the label is not ok-to-test), so needs.gate.outputs.authorized is empty and the behaviour job's condition (line 78) is false — the PR is left with a cancelled 45-minute run and no completed result.

Worse, cancellation skips t.Cleanup, so the preview alias, org pool lock, and repo-level FULLSEND_MINT_URL are abandoned mid-run (recovered only opportunistically by the next run's CleanupStaleResources), and with the per-run Worker naming above the bootstrapped Worker script is orphaned too.

Upstream fullsend .github/workflows/e2e.yml:57-63 — the direct template for this workflow — guards exactly this, with the inline comment "Label events are auth, not a code change — never cancel an in-progress run":

cancel-in-progress: ${{ (github.event_name == 'pull_request_target' && github.event.action != 'labeled') || (github.event_name != 'pull_request_target' && github.ref != 'refs/heads/main') }}

This PR copied e2e.yml's gate/checkout pattern but not this guard. (The sibling functional-tests.yml has the same unguarded form, so this is also a pre-existing repo convention — but the cost is far higher for a 45-minute live suite holding shared dev-pool resources.)

Suggestion: Adopt upstream e2e.yml's form:

cancel-in-progress: >-
  ${{ (github.event_name == 'pull_request_target' && github.event.action != 'labeled')
      || (github.event_name != 'pull_request_target' && github.ref != 'refs/heads/main') }}

and consider the same fix in functional-tests.yml.

Comment thread docs/behaviour-testing.md
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

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] The documented local compile command fails as written — wrong -exec path resolution and no modfile

Two independent failures, both reproduced with go1.26.5.

  1. -exec ./scripts/run-behaviour-test-exec.shgo test resolves the -exec program relative to the PACKAGE directory, not the module root. Reproduced with a minimal module: go test -run '^$' -exec ./scripts/wrap.sh ./sub -> fork/exec ./scripts/wrap.sh: no such file or directory, while -exec "$PWD/scripts/wrap.sh" ./sub -> ok. The repo's own Makefile:59 already avoids this with BEHAVIOUR_TEST_EXEC ?= $(CURDIR)/scripts/run-behaviour-test-exec.sh; only this doc uses the broken relative form.

  2. The command omits -modfile, so even with the path fixed it hits the go.mod failure: running it verbatim on the committed module gives go: updates to go.mod needed; to update it: go mod tidy. The preceding sentence — "The CI workflow prepares a temporary module file... With that same setup, a credential-free compile is:" — points at a setup it never shows, and go mod tidy (the obvious next step a reader takes) fails too, so a reader cannot recover.

This is the only local-check instruction in the new doc, so as written no contributor can run the credential-free compile.

Suggestion: Use an absolute path — -exec "$PWD/scripts/run-behaviour-test-exec.sh" — or better, document make behaviour-test BEHAVIOUR_GOFLAGS=-modfile=<modfile> so the Makefile's $(CURDIR) handles it. Then show the modfile preparation explicitly (the go mod edit -replace pair for github.com/fullsend-ai/fullsend and its nested internal/mintcore) as a numbered prerequisite instead of "with that same setup", and note that plain go mod tidy will fail.

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

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] Three actor PATs are hard-required (job exits 1) but nothing in the pinned framework consumes them

TEST_ACTOR_WRITE_PAT, TEST_ACTOR_TRIAGE_PAT, and TEST_ACTOR_OUTSIDER_PAT are in the required=(...) list at lines 170-171 and cause exit 1 when unset (lines 173-177).

At the pinned fullsend commit c85577b87895, git grep -l TEST_ACTOR returns only .github/workflows/e2e.yml, docs/guides/dev/{behaviour,e2e}-testing.md, hack/setup-new-e2e-org.sh, and scripts/redact-behaviour-artifacts.sh — ZERO hits under pkg/behaviourtest, pkg/e2etest, internal/, or cmd/. The redaction script is the only real consumer, and this workflow never runs it. Upstream e2e.yml says so in its own inline comment at lines 290-291: "Human-like actor PATs (write / triage / outsider) for behaviour SCM ops. Env names match repo secret names; consumption is driver/step work (#6027)." — i.e. upstream knows they are not yet consumed, and upstream merely passes them, it does not hard-require them.

By contrast the PEMs ARE consumed (repopool_cfmint_previews.go:353-354 maps app names to TEST_FULLSEND_PEM / TEST_TRIAGE_PEM / ...), so the requirement list looks copied wholesale from e2e.yml rather than derived from what the pinned driver reads. Net effect: the job blocks on minting three unused PATs, and injects three unused credentials into the one step that executes PR-head-controlled make under pull_request_target — needlessly widening the blast radius that the (accepted) fork-code-with-secrets tradeoff is already carrying. E2E_GCP_SERVICE_ACCOUNT is likewise only read by the google-github-actions/auth step, not the test process.

Suggestion: Require and inject only what the pinned framework actually reads (the six PEMs, the two Cloudflare values, E2E_GCP_PROJECT_ID, E2E_GCP_WIF_PROVIDER). If you want to pre-provision the actor PATs for future scenarios, pass them but drop them from the required list and add the same inline comment upstream uses (referencing fullsend#6027) so the hard gate is not blocking on secrets nothing consumes.

- '.github/workflows/behaviour-tests.yml'
- '.github/actions/check-e2e-authorization/**'
- '.github/scripts/check-e2e-authorization.sh'
pull_request_target:

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] No relevance filter on pull_request_target/merge_group — a docs-only PR burns a dev-pool org, a preview mint, and 45 minutes

push is path-filtered (lines 12-26) but pull_request_target (line 27), merge_group (line 29), and workflow_dispatch (line 30) have no path filter and no change-detection step, so every authorized PR — including one touching only docs/** or README.md — acquires a dev pool org lock, deploys a Cloudflare preview mint, and runs up to the 45-minute timeout-minutes budget.

Both reference workflows do gate this:

  • Upstream fullsend .github/workflows/e2e.yml:208-243 has a "Check for behaviour-relevant changes" step for pull_request_target/merge_group, marked # SYNC-WITH: push.paths behaviour entries above, that compares the API file list against the same regex and sets relevant=false to skip (failing open with a warning if the API call fails or the compare truncates at 300 files).
  • The sibling .github/workflows/functional-tests.yml:98-135 uses a lightweight detect job over changed files for the same purpose.

The path set is already written down at lines 13-26 of this file, so the filter has no new information to derive.

Suggestion: Port e2e.yml's relevance step: for pull_request_target/merge_group, fetch the changed-file list and grep it against the same path set already in push.paths (add a # SYNC-WITH: push.paths above comment), gate the checkout and subsequent steps on steps.changes.outputs.relevant != 'false', and fail open with a ::warning:: on API error or a truncated compare, exactly as upstream does.


behaviour:
needs: gate
if: >-

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] Behaviour job omits the github.repository_owner == 'fullsend-ai' guard that the sibling secretful workflow has

Distinct from the (dismissed) fork-PR-secrets thread: that one is about fork PRs into this repo, which the gate covers. This is about forks of the repository running the privileged path on their own events.

The behaviour job's if: (lines 71-79) checks event name and gate authorization but never the repository owner. The sibling .github/workflows/functional-tests.yml applies github.repository_owner == 'fullsend-ai' && to BOTH of its secretful jobs (line 102 on detect, line 209 on functional-tests) — verified at this head SHA.

Without it, a fork of this repo runs the full privileged path on its own pushes and PRs: environment: dev, id-token: write, GCP WIF auth, and the make behaviour-test step with all fourteen mapped secrets. In practice a fork without those secrets fails at the credential-validation loop (lines 173-177), so this is defence-in-depth and wasted-run prevention rather than a live bypass — but it is an unexplained divergence from the security posture the repo's other pull_request_target workflow establishes, in a job whose blast radius (six App private keys, Cloudflare API token, three PATs, GCP WIF) is larger than functional-tests'.

Suggestion: Add github.repository_owner == 'fullsend-ai' && to the behaviour job's if: condition, matching functional-tests.yml lines 102 and 209.

@ifireball ifireball self-assigned this Sep 11, 2026
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

risk/elevated PR risk: elevated

Projects

None yet

Development

Successfully merging this pull request may close these issues.

feat(behaviour): integrate behaviour test runner in fullsend-ai/agents

2 participants