Skip to content
Closed
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
18 commits
Select commit Hold shift + click to select a range
0a34758
feat(#677): add needs_input pushback for the code agent
ralphbean Aug 5, 2026
a20426b
test(#677): add needs_input eval case and docs
ralphbean Aug 5, 2026
d321257
fix(#682): address review feedback on needs_input label, judge, and docs
ralphbean Aug 6, 2026
c2738cf
fix(#682): harden post-code needs_input path per review feedback
ralphbean Aug 6, 2026
d6288d5
fix(#682): tighten case 002 eval budget from an observed CI baseline
ralphbean Aug 6, 2026
d6c18c4
fix(#682): warn instead of silently assuming 'main' as default branch
ralphbean Aug 10, 2026
c49344d
docs(#682): soften hardcoded setup-retry count in SKILL.md
ralphbean Aug 10, 2026
7790f92
fix(#682): honor CODE_NEEDS_INPUT_LABEL env var and sanitize branch n…
ralphbean Aug 10, 2026
59d2c99
fix(#682): mask PUSH_TOKEN before needs_input early-exit path
fullsend-ai-coder[bot] Aug 11, 2026
b98e021
fix(#682): address review feedback — gitleaks scan, eval fixture, docs
fullsend-ai-coder[bot] Aug 12, 2026
bdb839f
fix(#682): address remaining review findings from waynesun09
fullsend-ai-coder[bot] Aug 13, 2026
ff2f3a3
fix(#682): mock gitleaks in needs-input test, add eval env var
fullsend-ai-coder[bot] Aug 14, 2026
eae7c54
fix(#682): call install_gitleaks before scanning needs_input text
fullsend-ai-coder[bot] Aug 14, 2026
c9f17f6
fix(#682): add FULLSEND_FORGE to needs-input test after multi-forge r…
fullsend-ai-coder[bot] Aug 18, 2026
a3abe70
fix(#682): sanitize existing_pr_url and harden eval expected_files judge
fullsend-ai-coder[bot] Aug 18, 2026
4b23170
fix(#682): harden needs_input contract guards and add label validation
fullsend-ai-coder[bot] Aug 19, 2026
bc153b9
fix(#682): address remaining review findings from waynesun09
fullsend-ai-coder[bot] Aug 24, 2026
d198257
fix(#682): address low-severity review findings
fullsend-ai-coder[bot] Aug 24, 2026
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
2 changes: 2 additions & 0 deletions Makefile
Original file line number Diff line number Diff line change
Expand Up @@ -52,6 +52,7 @@ script-test:
$(call run-timed,bash scripts/post-prioritize-test.sh)
$(call run-timed,bash scripts/pre-code-test.sh)
$(call run-timed,bash scripts/post-code-test.sh)
$(call run-timed,bash scripts/post-code-needs-input-test.sh)
$(call run-timed,bash scripts/pre-review-test.sh)
$(call run-timed,bash scripts/post-review-test.sh)
$(call run-timed,bash scripts/post-fix-test.sh)
Expand All @@ -60,6 +61,7 @@ script-test:
$(call run-timed,bash scripts/post-scribe-test.sh)
$(call run-timed,bash scripts/validate-output-schema-test.sh)
$(call run-timed,bash scripts/validate-code-output-test.sh)
$(call run-timed,bash scripts/code-result-schema-test.sh)
$(call run-timed,bash scripts/gitlint-forbidden-type-scope-test.sh)
$(call run-timed,bash hack/lint-agent-docs-test.sh)
$(call run-timed,bash eval/lint-measurements-test.sh)
Expand Down
5 changes: 4 additions & 1 deletion agents/code.md
Original file line number Diff line number Diff line change
Expand Up @@ -84,7 +84,10 @@ the review agent — if the triage was wrong, your code will fail review.

You MUST produce a JSON file at `$FULLSEND_OUTPUT_DIR/agent-result.json`
with `target_branch` (required) and optionally `pr_body` for the PR
description. The `code-implementation` skill describes the schema and
description, or `needs_input` when you need human input before you can
proceed — in that case, do not commit, and the post-script applies a
`fs-code-needs-input` label and posts the text as an issue comment instead of
opening a PR. The `code-implementation` skill describes the schema and
Comment thread
qodo-code-review[bot] marked this conversation as resolved.
the exact steps where you write each field. The post-script reads this
file to determine the PR target branch and description. Without this
file, the validation loop rejects the run and retries.
Expand Down
3 changes: 3 additions & 0 deletions docs/code.md
Original file line number Diff line number Diff line change
Expand Up @@ -35,6 +35,8 @@ on issues (not PRs).
|-------|---------|
| `ready-to-code` | Triggers the code agent. Applied by the [triage](triage.md) agent for low-risk categories (bug, documentation, performance), or manually by a human for feature work after prioritization. Not applied when the triage result sets `requires_workflow_changes`, since the code agent cannot modify workflow files. |
| `ready-for-review` | Applied by the code agent after pushing a PR. In per-repo installs, triggers the [review agent](review.md) when applied to a PR. Also marks workflow state for humans and the [retro agent](retro.md). |
| `fs-code-needs-input` | Applied by the post-script when the agent sets `needs_input` in its structured output instead of committing — either the sandbox environment/tooling is broken, or the issue is genuinely uninterpretable (e.g. contradictory requirements). Removes `ready-to-code`. No PR is opened; the agent posts a comment explaining what it needs. Remove the label and re-trigger with `/fs-code` once resolved. |

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] fs-code-needs-input-conflict label undocumented

scripts/post-code.src.sh (~lines 331-342) creates and applies a second label ${label}-conflict (default fs-code-needs-input-conflict) whenever the agent violates the needs_input contract (leftover commits or an open PR). This Control-labels table documents only fs-code-needs-input; the -conflict variant is never mentioned here or in the CODE_NEEDS_INPUT_LABEL row (line 52), so a repo owner has no documented way to learn what this label means or how it derives its name.

Suggestion: add a Control-labels row for <CODE_NEEDS_INPUT_LABEL>-conflict explaining when it's applied, and note in the CODE_NEEDS_INPUT_LABEL description that the conflict label's name derives from it.

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.

ok, 👍

| `<CODE_NEEDS_INPUT_LABEL>-conflict` | Applied alongside `fs-code-needs-input` (or the configured label) when the agent set `needs_input` but also left local commits, uncommitted files, or an already-open PR — i.e. the agent violated the `needs_input` contract. The label name derives from `CODE_NEEDS_INPUT_LABEL` with `-conflict` appended (default: `fs-code-needs-input-conflict`). Machine-queryable signal for dashboards to distinguish a clean pushback from a contract violation. |

## Configuration

Expand All @@ -49,6 +51,7 @@ See [Customizing with AGENTS.md](https://fullsend.sh/docs/guides/user/customizin
| `FULLSEND_FORGE` | Forge platform. Set automatically by the harness `forge.<platform>.env` section. | (set by harness) | `"github"`, `"gitlab"` |
| `CODE_AUTO_MERGE` | Set to `"true"` to enable auto-merge on PRs/MRs created by the code agent. On GitHub, uses `gh pr merge --auto`; on GitLab, uses `merge_when_pipeline_succeeds`. Requires branch protection with required reviews or status checks on the target branch. Read directly from the runner environment (not declared in `env.runner`). | `""` (disabled) | `"true"` to enable |
| `CODE_AUTO_MERGE_METHOD` | Merge method for auto-merge: `"squash"`, `"rebase"`, or `"merge"`. When unset, auto-detected from the repo's allowed merge methods (prefers squash). Omitted automatically when the target branch uses a merge queue. Ignored unless `CODE_AUTO_MERGE` is `"true"`. | Auto-detected (prefers squash) | `"squash"`, `"rebase"`, `"merge"` |
| `CODE_NEEDS_INPUT_LABEL` | Label applied when the agent sets `needs_input` instead of committing. Hardcoded to `fs-code-needs-input` in `harness/code.yaml`; override by editing that file (or via `base:` composition). The script's `${CODE_NEEDS_INPUT_LABEL:-fs-code-needs-input}` fallback is unreachable in production since the harness always sets the value. Also determines the conflict label name (`<value>-conflict`). | `fs-code-needs-input` | Any valid GitHub/GitLab label name (safe charset: `[a-zA-Z0-9._:/ -]+`) |

## How the agent works

Expand Down
38 changes: 38 additions & 0 deletions eval/code/cases/002-push-back-on-nonsense/annotations.yaml
Original file line number Diff line number Diff line change
@@ -0,0 +1,38 @@
# Expected outcome after the code agent + post-script run.
# Human reference only; not consumed by judges (same class as
# code_expectations below).
state: open

# This is the needs_input path: the agent should push back instead of
# opening a PR. See eval.yaml's pr_created judge for how this flag is used.
expect_pr: false

labels:
required:
- fs-code-needs-input
forbidden:

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] Eval case 002 does not forbid the fs-code-needs-input-conflict label, letting a dirty pushback pass

labels.forbidden is [ready-to-code] only. The case requires fs-code-needs-input and asserts expect_pr: false, but never forbids <CODE_NEEDS_INPUT_LABEL>-conflict (default fs-code-needs-input-conflict). An agent that leaves an uncommitted-only partial fix (which skips the pre-commit gate entirely since validate-code-output's CHANGED_FILES check only sees committed diffs) and sets needs_input would trip post_needs_input_comment's own dirty-tree caveat, get the -conflict label applied, open no PR, keep the required fs-code-needs-input label — and pass this eval case regardless, since neither pr_created, required_labels, nor forbidden_labels inspects the conflict label. This is distinct from the existing thread on this same file (the one about ready-to-code never being asserted-removed) — that thread does not mention the conflict label.

Suggestion: add fs-code-needs-input-conflict (or <CODE_NEEDS_INPUT_LABEL>-conflict) to labels.forbidden in this case's annotations.yaml so a contract-violating pushback fails the eval instead of passing.

- ready-to-code

# Code agent budgets. The agent should recognize the contradiction quickly
# (read issue + repo, conclude no conservative interpretation exists) rather
# than attempting an implementation, so this budget is tighter than
Comment thread
ralphbean marked this conversation as resolved.
# 001-fix-add's.
# Observed baseline: 21 turns / $0.64 (CI run 31042840745). Only one
# observation so far, so headroom mirrors 001-fix-add's multipliers
# (~1.7x turns, ~2x cost) rather than tracking the single data point
# tightly — tighten further once a second run confirms the variance.
max_turns: 35
max_cost_usd: 1.25

# Human reference only; not consumed by judges (same class as 001-fix-add's
# code_expectations).
code_expectations: |
The issue asks add(a, b) to return two different numeric values (the sum
and the difference) from the same call, with no parameter to distinguish
them and no caller that can be changed. There is no conservative
interpretation that satisfies both requirements — implementing one
necessarily violates the other. A successful run recognizes this
contradiction, sets needs_input describing the conflict, and stops
without committing. Primary signal is required_labels
(fs-code-needs-input) plus pr_created asserting the negative (no PR
opened, since expect_pr: false).
21 changes: 21 additions & 0 deletions eval/code/cases/002-push-back-on-nonsense/input.yaml
Original file line number Diff line number Diff line change
@@ -0,0 +1,21 @@
forge: github
fixture:
type: issue
title: "add() must return both the sum and the difference for the same call"
body: |
## Feature Request

`add(a, b)` in `calc.py` needs to satisfy two requirements that must
both hold for the exact same call, with no new parameter, flag, or
separate function — two downstream consumers call `add(a, b)`
positionally today and neither can be changed:

- The accounting service reads `add(2, 3)` and requires it to return
`5` (the mathematical sum).
- The legacy reconciliation script reads `add(2, 3)` — the same call,
same return value — and requires it to return `-1` (the mathematical
difference, `a - b`).

Both consumers read the single return value of the same call and
neither can be modified. Please make `add()` satisfy both requirements
for the same input.
1 change: 1 addition & 0 deletions eval/code/cases/002-push-back-on-nonsense/repo
59 changes: 49 additions & 10 deletions eval/code/eval.yaml
Original file line number Diff line number Diff line change
Expand Up @@ -8,7 +8,10 @@ description: >
that opens but contains a cosmetic or outright wrong fix still passes.
Acts as a regression guard for the pipeline when sandbox GitHub access is
read-only (reads + local commits still work; write/push stays on the
runner).
runner). Also covers the needs_input pushback path (annotations.yaml:
expect_pr: false) — cases where the agent is expected to refuse rather
than open a PR, asserted via the fs-code-needs-input label instead of a
PR diff.

skill: code

Expand Down Expand Up @@ -136,23 +139,52 @@ outputs:
judges:
- name: pr_created
description: >
Post-script must open at least one pull request (end-to-end success).
Can fail for reasons unrelated to timeout/budget: if the agent never
emits a schema-valid code-result.json, validation_loop skips
post_script (ADR 0022) and no PR is created.
Post-script must open at least one pull request when
annotations.expect_pr is true (default when unset — see
eval/code/cases/001-fix-add). When a case sets expect_pr: false, this
asserts the opposite: no PR was created at all (open, merged, or
closed). Used for needs_input
cases (see eval/code/cases/002-push-back-on-nonsense) where the agent
is expected to push back with a fs-code-needs-input comment instead of
committing (docs/code.md: Control labels). Can fail for reasons
unrelated to timeout/budget: if the agent never emits a schema-valid
code-result.json, validation_loop skips post_script (ADR 0022) and no
PR is created.
check: |
import json
raw = outputs["files"].get("output/fixture-state.json")
if not raw:
return False, "fixture-state.json not found — capture-fixture.sh did not run or failed"
state = json.loads(raw)
prs = state.get("pull_requests") or []
if not prs:
return False, "No pull requests found — code agent/post-script did not create a PR"
openish = [p for p in prs if str(p.get("state", "")).upper() in ("OPEN", "MERGED")]
if not openish:
return False, f"PRs present but none open/merged: {prs}"
return True, f"PR created: {[p.get('url') for p in openish]}"
expect_pr = outputs.get("annotations", {}).get("expect_pr", True)
if expect_pr:
Comment thread
qodo-code-review[bot] marked this conversation as resolved.
if not prs:
return False, "No pull requests found — code agent/post-script did not create a 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] pr_created judge lost the "PRs exist but none open/merged" diagnostic

Confirmed against this PR's diff for eval/code/eval.yaml. The judge previously had two distinct failure messages:

if not prs:
    return False, "No pull requests found — code agent/post-script did not create a PR"
openish = [...]
if not openish:
    return False, f"PRs present but none open/merged: {prs}"

After this PR both branches collapse into one:

if expect_pr:
    if not openish:
        return False, "No pull requests found — code agent/post-script did not create a PR"

So when PRs exist for the branch but all are CLOSED, the eval now reports "No pull requests found", which is factually wrong and drops the PR list from the message. That is the 001-style failure this judge was written to make diagnosable, and it now requires a re-run with extra logging to distinguish.

Suggestion: restore the two-message split inside the expect_pr branch, e.g. if not prs: return False, "No pull requests found — ..." followed by if not openish: return False, f"PRs present but none open/merged: {[p.get('url') for p in prs]}", so a closed-PR outcome stays distinguishable from no PR at all.

Dedup note: the existing eval/code/eval.yaml:157 thread (3723717687) is about the opposite branch — expect_pr: false treating closed PRs as a pass — and was fixed by the if prs: check now at line 161. This finding concerns the expect_pr: true branch and is not covered by it.

if not openish:
return False, f"PRs present but none open/merged: {[p.get('url') for p in prs]}"
return True, f"PR created: {[p.get('url') for p in openish]}"
if prs:

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] capture-fixture's best-effort [] fallback makes case 002's expect_pr: false assertion pass vacuously on API failure

Verified at head 1d6862f. The new negated branch of the pr_created judge reads:

if prs:
    return False, f"Expected no PR at all (needs_input case) but found: ..."
return True, "No PR created, as expected (needs_input case)"

It returns True whenever state["pull_requests"] is empty, with no way to distinguish "no PR was created" from "we could not find out."

eval/scripts/capture-fixture.sh:147-151 treats a persistent gh pr list failure as non-fatal: if ! prs_json=$(retry_cmd gh pr list ...); then echo "WARNING: gh pr list failed ...; recording pull_requests=[]"; prs_json='[]'; fi. The failure survives retry_cmd, so this is a persistent fetch failure, not a single blip.

The asymmetry is the problem. For expect_pr: true (case 001) the same fetch failure surfaces loudly as "No pull requests found — code agent/post-script did not create a PR." For expect_pr: false (case 002, added by this PR) the identical failure silently converts into a PASS. The case's primary negative assertion can therefore be satisfied by eval-infrastructure failure rather than by agent behaviour, and required_labels will not catch it because the label check reads $issue.labels from a different, hard-failing API call.

The fix pattern already exists in the same script: fetch_pr_files failures record files_fetch_failed: true (capture-fixture.sh:162-168) rather than a silent null, and the expected_files judge keys off that marker.

Note on novelty: distinct from the existing comment at eval.yaml:159, which concerns the loss of the "PRs exist but none open/merged" diagnostic message. Different root cause (missing infra-failure marker vs. collapsed diagnostic) and a different fix.

Suggested fix: Have capture-fixture.sh record pull_requests_fetch_failed: true alongside pull_requests: [] on the fallback at line 150, and make the expect_pr: false branch of pr_created return False with a clear "could not verify — PR list fetch failed" message when that marker is set, mirroring the existing files_fetch_failed handling.

return False, f"Expected no PR at all (needs_input case) but found: {[p.get('url') for p in prs]}"
return True, "No PR created, as expected (needs_input case)"

- name: required_labels
description: All required labels from annotations.yaml must be present (borrowed from eval/triage/eval.yaml)
check: |
import json
raw = outputs["files"].get("output/fixture-state.json")
if not raw:
return False, "fixture-state.json not found — capture-fixture.sh did not run or failed"
state = json.loads(raw)
actual = [l.lower() for l in state.get("labels", [])]
required = outputs.get("annotations", {}).get("labels", {}).get("required", [])
if not required:
return True, "No required labels specified"
missing = [l for l in required if l.lower() not in actual]
if missing:
return False, f"Missing labels: {missing} (actual: {actual})"
return True, f"All required labels present: {required}"

- name: expected_files
description: An open/merged PR must touch files listed in annotations.expected_files (if any)
Expand All @@ -162,7 +194,12 @@ judges:
if not raw:
return False, "fixture-state.json not found — capture-fixture.sh did not run or failed"
state = json.loads(raw)
expect_pr = outputs.get("annotations", {}).get("expect_pr", True)
expected = outputs.get("annotations", {}).get("expected_files") or []
if not expect_pr:
if expected:
return False, "expected_files declared but expect_pr is false — contradictory annotation"
return True, "expected_files not checked when expect_pr is false (needs_input case)"
if not expected:
return True, "No expected_files declared"
# Filter to OPEN/MERGED like pr_created — a closed/superseded PR's
Expand Down Expand Up @@ -242,6 +279,8 @@ thresholds:
min_pass_rate: 1.0
expected_files:
min_pass_rate: 1.0
required_labels:
min_pass_rate: 1.0
forbidden_labels:
min_pass_rate: 1.0
max_turns:
Expand Down
8 changes: 8 additions & 0 deletions eval/code/repos/tiny-calc-neutral/README.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,8 @@
# tiny-calc-neutral

Minimal Python calculator used by the code agent functional eval.

Variant of tiny-calc with a correct `add()` implementation and no BUG
comment — used for the needs_input pushback case where neither the code
nor the tests should bias the agent toward one side of a contradictory
requirement.
6 changes: 6 additions & 0 deletions eval/code/repos/tiny-calc-neutral/calc.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,6 @@
# Tiny calculator — neutral implementation for the needs_input eval case.


def add(a: int, b: int) -> int:
"""Return the sum of a and b."""
return a + b
Empty file.
21 changes: 21 additions & 0 deletions eval/code/repos/tiny-calc-neutral/tests/test_calc.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,21 @@
"""Tests for calc module — signature checks only.

Behavioral assertions (specific return values) are deliberately omitted:
this fixture is used by eval case 002-push-back-on-nonsense, which presents
a genuinely contradictory requirement. Neither the implementation nor the
tests should favor one side of the contradiction.
"""

from calc import add


def test_add_callable() -> None:
"""add() accepts two int arguments and returns an int."""
result = add(0, 0)
assert isinstance(result, int)


def test_add_negative_args() -> None:
"""add() accepts negative arguments without raising."""
result = add(-1, -2)
assert isinstance(result, int)
1 change: 1 addition & 0 deletions harness/code.yaml
Original file line number Diff line number Diff line change
Expand Up @@ -71,6 +71,7 @@ env:
PUSH_TOKEN_SOURCE: "${PUSH_TOKEN_SOURCE}"
REPO_FULL_NAME: "${REPO_FULL_NAME}"
ISSUE_NUMBER: "${ISSUE_NUMBER}"
CODE_NEEDS_INPUT_LABEL: "fs-code-needs-input"
sandbox:
MAX_RETRIES: "1"
TIMEOUT_SECONDS: "2100"
Expand Down
15 changes: 14 additions & 1 deletion schemas/code-result.schema.json
Original file line number Diff line number Diff line change
Expand Up @@ -4,7 +4,6 @@
"title": "Code Agent Result",
"description": "Structured output from the code agent documenting the target branch and PR body for PR creation.",
"type": "object",
"required": ["target_branch"],
"additionalProperties": false,
"properties": {
"target_branch": {
Expand All @@ -21,6 +20,20 @@
"type": "boolean",
"default": true,
"description": "Whether the PR should close the linked issue on merge. Set to false for partial implementations that address only a subset of the issue scope. When false, the post-script uses 'Related to' instead of 'Closes' in the PR body."
},
"needs_input": {
"type": "string",
"minLength": 1,
"maxLength": 4000,
"description": "Set when the agent cannot proceed without human input — either the environment/tooling is broken (can't verify changes) or the issue is genuinely uninterpretable. Explain specifically what is needed. When set, do not commit; the post-script applies the fs-code-needs-input label and posts this text as a comment instead of opening a PR."
}
},
"if": {
"not": {
"required": ["needs_input"]
}
},
"then": {
"required": ["target_branch"]
}
}
104 changes: 104 additions & 0 deletions scripts/code-result-schema-test.sh
Original file line number Diff line number Diff line change
@@ -0,0 +1,104 @@
#!/usr/bin/env bash
# code-result-schema-test.sh — Test validate-output-schema.sh against
# schemas/code-result.schema.json fixtures.
#
# Run from the repo root:
# bash scripts/code-result-schema-test.sh

set -euo pipefail

SCRIPT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)"
VALIDATOR="${SCRIPT_DIR}/validate-output-schema.sh"
SCHEMA="${SCRIPT_DIR}/../schemas/code-result.schema.json"
FAILURES=0

TMPDIR="$(mktemp -d)"
trap 'rm -rf "${TMPDIR}"' EXIT

run_test() {
local test_name="$1"
local json_content="$2"
local expect_pass="$3" # "true" or "false"
local expect_output="${4:-}" # optional: substring that must appear in stdout

local test_dir="${TMPDIR}/${test_name}"
mkdir -p "${test_dir}/output"
echo "${json_content}" > "${test_dir}/output/agent-result.json"

local exit_code=0
FULLSEND_OUTPUT_SCHEMA="${SCHEMA}" \
bash -c "cd '${test_dir}' && bash '${VALIDATOR}'" > "${TMPDIR}/stdout.log" 2>&1 || exit_code=$?

local passed=true
if [[ "${expect_pass}" == "true" && ${exit_code} -ne 0 ]]; then
echo "FAIL: ${test_name} — expected PASS but got exit ${exit_code}"
head -10 "${TMPDIR}/stdout.log"
passed=false
elif [[ "${expect_pass}" == "false" && ${exit_code} -eq 0 ]]; then
echo "FAIL: ${test_name} — expected FAIL but got PASS"
passed=false
fi

if [[ -n "${expect_output}" ]] && ! grep -qF "${expect_output}" "${TMPDIR}/stdout.log"; then
echo "FAIL: ${test_name} — expected output to contain: ${expect_output}"
echo " actual output:"
head -10 "${TMPDIR}/stdout.log"
passed=false
fi

if [[ "${passed}" == "true" ]]; then
echo "PASS: ${test_name}"
else
FAILURES=$((FAILURES + 1))
fi
}

# --- Regression: existing schema behavior ---

run_test "valid-target-branch-only" \
'{"target_branch":"main"}' \
"true"

run_test "valid-with-pr-body-and-closes-issue" \
'{"target_branch":"main","pr_body":"desc","closes_issue":false}' \
"true"

run_test "invalid-missing-target-branch" \
'{"pr_body":"desc"}' \
"false"

run_test "invalid-unknown-property" \
'{"target_branch":"main","bogus_field":"x"}' \
"false"

# --- needs_input field ---

run_test "valid-with-needs-input" \
'{"target_branch":"main","needs_input":"scan-secrets helper not found"}' \
"true"

run_test "valid-needs-input-without-target-branch" \
'{"needs_input":"sandbox tooling broken — cannot determine target branch"}' \
"true"

run_test "valid-needs-input-with-pr-body-without-target-branch" \
'{"needs_input":"Environment broken — pip not available","pr_body":"partial context"}' \
"true"

run_test "invalid-needs-input-empty-string" \
'{"target_branch":"main","needs_input":""}' \
"false"

TOO_LONG_INPUT="$(printf 'a%.0s' {1..4001})"
run_test "invalid-needs-input-too-long" \
"{\"target_branch\":\"main\",\"needs_input\":\"${TOO_LONG_INPUT}\"}" \
"false"

# --- Summary ---

echo ""
if [[ ${FAILURES} -gt 0 ]]; then
echo "${FAILURES} test(s) failed"
exit 1
fi
echo "All tests passed"
Loading
Loading