Skip to content
Open
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
40 changes: 40 additions & 0 deletions eval/code/cases/002-dead-config-field/annotations.yaml
Original file line number Diff line number Diff line change
@@ -0,0 +1,40 @@
state: open

expected_files:
- config/config.go
- config/fields.go
- config/config_test.go

labels:
forbidden: []

# Consumed by eval.yaml's removed_symbols judge: each symbol must appear
# only in deletion lines of the captured PR diff. Both the Go identifier
# and its YAML key must vanish — a fix that drops the struct field but
# leaves the SetField case, a test assertion, or the raw YAML literal in
# TestLoad would keep one of these in a non-deletion line and fail.
removed_symbols:
- VerboseLogging
- verbose_logging

# Code agent budgets (sandbox work + retries). Unmeasured ceiling: this case

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 eval harness has never actually executed this case — every prior "Functional Tests" run was gated off as unauthorized

annotations.yaml's own comment (lines 11-16) already self-flags the max_turns/max_cost budget as an "Unmeasured ceiling... this case has no CI run of its own yet." Live verification at head 4ab7533 confirms why: gh pr checks 617 and the gate job log for the latest run (31710102745, 2026-08-13T14:25) show .github/scripts/check-e2e-authorization.sh printing authorized=false reason=unauthorized with the warning "Functional tests did not run (reason: unauthorized). External contributors need a maintainer to apply the ok-to-test label after the latest push." The "functional-tests" job itself reports DETECT_RESULT=skipped / TESTS_RESULT=skipped. Checking the full run history for this branch (11 pushes over 2026-08-03 through 2026-08-13), every single "Functional Tests" workflow run completed in 5-21 seconds and shows the identical unauthorized/skip pattern — the coding-agent harness (setup-fixture.sh's repo copy, the code agent solving the issue, and the expected_files/pr_created/max_turns/max_cost judges) has never actually run end-to-end for this case at any point in the PR's life, despite 4+ rounds of prior review iterating on the budget numbers as if a real baseline existed. This is a distinct, actionable fact beyond the already-posted "budget is an unmeasured guess" comments: it identifies the specific, currently-active CI gate blocking validation and the concrete remedy.

Suggestion: Have a maintainer apply the ok-to-test label (per CONTRIBUTING.md) to trigger a real functional-tests run before merge, so the budget/expected_files/harness wiring for this case is validated at least once rather than merged as an entirely unexercised configuration.

# has no CI run of its own yet — re-derive from its first run. Values mirror
# 001-fix-add's ceiling (60 turns / $4.00) and eval.yaml's max_budget_usd.
# For reference, 001 (a trivial fixture) observed 12 turns / $2.12 (CI run
# 29424512121) and 35 turns / $0.98 (CI run 30166455238); this cross-file
# task is harder, so its ceiling is set no lower.
max_turns: 60
max_cost_usd: 4.00

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 — Budget copied from 001-fix-add with no observed baseline, and case shipped without an end-to-end harness run

max_turns: 60 / max_cost_usd: 4.00 are byte-identical to 001-fix-add's values, but 001-fix-add/annotations.yaml documents its numbers with actual observed CI baselines ("12 turns / $2.12 (CI run 29424512121), then 35 turns / $0.98 (CI run 30166455238)") and explains the chosen headroom multiplier. This annotations.yaml has no such comment at all. This PR's own test plan leaves "End-to-end fullsend eval code run (requires CI)" unchecked — the case has only been validated by go test ./... and eval/lint-cases.sh code, never by actually driving an agent through the real fullsend eval code harness — yet the PR justifies its design partly by citing an unrelated prior benchmark (fullsend#5808, N=13 pairs on a structurally different, single-file fixture) as if it generalizes to this new cross-file case. Given the PR itself describes 002 as meaningfully harder than 001 (multi-file tracing across 3 files vs. a one-line arithmetic fix), reusing 001's untouched budget without any baseline or real run is an unverified guess presented as final configuration.

Suggestion: Run the case at least once through the real fullsend eval code harness (or CI) before merging, and add an annotations.yaml comment documenting the observed turns/cost baseline and chosen headroom multiplier, matching the convention in 001-fix-add/annotations.yaml. If CI access is genuinely blocking pre-merge, say so explicitly and treat the unchecked test-plan item as a required follow-up rather than optional.


# Human reference only; not consumed by judges (unlike triage/review quality).
code_expectations: |
The repo has a dead config field: Config.VerboseLogging is declared, defaulted,
parsed, and tested, but never read by any consumer (runner/runner.go doesn't
use it). A successful run creates a PR that removes VerboseLogging from: the
struct definition (config.go), the Defaults() return value (config.go), the
SetField() switch case (fields.go), and all test assertions (config_test.go).
Tests must still pass after removal. This case tests cross-file dead-code
removal — tracing symbol references across multiple files to determine what
to change, not just fixing a single line. The raw YAML literal in TestLoad
(verbose_logging: true) must also be removed — SetField rejects unknown keys,
so leaving it breaks Load().
25 changes: 25 additions & 0 deletions eval/code/cases/002-dead-config-field/input.yaml
Original file line number Diff line number Diff line change
@@ -0,0 +1,25 @@
forge: github
fixture:
type: issue
title: "config: VerboseLogging field appears unused"
body: |
## Bug Report

**What happened:**
We have a `VerboseLogging` field in the config that I can't find being used anywhere
in the actual runner logic. I added it a while back thinking we'd need it for debug
output, but I'm not sure we ever wired it up to anything.

**Expected behavior:**
If it's truly unused, we should remove it to keep the config clean. But I want to
make sure I'm not missing something — maybe there's some code path that reads it
that I didn't spot?

**Additional context:**
- The field exists in `config/config.go`
- YAML key is `verbose_logging`
- I see tests for it, but those might just be testing the config parsing itself
- Haven't found where the runner actually checks this value, but I might be wrong

Can someone verify whether this is actually used? If not, please clean it up.
Make sure tests still pass after any changes.
1 change: 1 addition & 0 deletions eval/code/cases/002-dead-config-field/repo
81 changes: 74 additions & 7 deletions eval/code/eval.yaml
Original file line number Diff line number Diff line change
Expand Up @@ -2,10 +2,13 @@ name: code-eval
description: >
Functional test of the fullsend code agent pipeline (pre → sandbox → post).
Validates that the post-script opens a PR touching the expected files for
a small issue — an end-to-end pipeline guard, not a correctness check.
No judge inspects the PR's diff content or runs the fixture's tests
against it (see annotations.yaml: "Primary signal is pr_created"), so a PR
that opens but contains a cosmetic or outright wrong fix still passes.
a small issue — an end-to-end pipeline guard, not a full correctness
check. Cases may declare removed_symbols in annotations.yaml; the
removed_symbols judge then verifies those symbols appear only in deletion
lines of the captured PR diff. Beyond that, no judge runs the fixture's
tests against the PR, so a fix that compiles but misbehaves can still
pass — and cases with no removed_symbols get file-touch checking only
(see annotations.yaml: "Primary signal is pr_created").
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).
Expand Down Expand Up @@ -65,9 +68,15 @@ execution:
# timeout envelope; matches eval/fix/eval.yaml's value instead (see
# execution.timeout above).
#
# This fixture is a 2-line arithmetic bug and completes in well under a
# minute in practice, so this number essentially never fires. If a
# genuine hang did occur: a single-iteration hang gets caught here and
# Two cases now share this window: 001-fix-add (a 2-line arithmetic bug
# that completes in well under a minute) and 002-dead-config-field (a
# cross-file dead-config removal with a larger 60-turn / $4.00 budget).
# Neither is expected to approach 1700s in practice: these small fixtures
# finish far under it. Note the 1700s outer backstop — not the larger
# 2100s per-iteration agent budget — is the binding limit, so a genuine
# hang is caught by 1700s first. 002 has no observed CI runtime yet, so
# treat its headroom as an open risk to revisit once a real run exists.
# If a genuine hang did occur: a single-iteration hang gets caught here and
# fullsend still writes a partial metrics.json before returning (see
# writeMetricsJSON in run.go's error path), giving an inconclusive but
# readable "metrics.json not found"-adjacent judge failure; a
Expand Down Expand Up @@ -177,6 +186,62 @@ judges:
return False, f"Expected files missing from PRs: {missing} (changed: {sorted(changed)})"
return True, f"All expected files present: {expected}"

- name: removed_symbols

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 — removed_symbols judge is schema-incompatible with the pull_request fixture-type branch if ever reused

Verified against current capture-fixture.sh (head 14cd11d): the removed_symbols judge reads state.get("pull_requests"), a nested array populated only by capture-fixture.sh's issue-fixture-type branch (lines 145-214). The pull_request fixture-type branch (lines 216-315, used by human-authored-PR fixtures like eval/fix's case type) writes the PR's fields flat at the top level of fixture-state.json, never sets diff_fetch_failed, and never calls fetch_pr_diff/writes output/pr-<num>.diff at all (fetch_pr_diff is only invoked inside the issue case's while-loop at line 176). If removed_symbols is ever copied into a pull_request-type eval suite (e.g. eval/fix or eval/review) as-is, it will silently return "No open/merged PR to inspect" for every run regardless of what the PR actually contains.

Suggestion: Either document in the judge description that removed_symbols only supports issue-type fixtures today, or extend capture-fixture.sh's pull_request branch to also populate an equivalent diff artifact / diff_fetch_failed flag before this pattern is copy-pasted into another suite.

description: >
Content-level check for removal cases: every symbol listed in
annotations.removed_symbols must appear in at least one deletion line
of the captured PR diff (output/pr-<num>.diff, written by
capture-fixture.sh) and in no added or context line — i.e. the symbol
is gone from every hunk the fix touched. Passes trivially when a case
declares no removed_symbols. Diff-scoped only: a symbol surviving in
a file the PR never touched is invisible here (expected_files covers
the known declaration sites), and no judge runs the fixture's tests.
check: |
import json
symbols = outputs.get("annotations", {}).get("removed_symbols") or []
if not symbols:
return True, "No removed_symbols declared"
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 = [p for p in (state.get("pull_requests") or [])
if str(p.get("state", "")).upper() in ("OPEN", "MERGED")]
if not prs:
return False, "No open/merged PR to inspect"
failed = [p.get("number") for p in prs if p.get("diff_fetch_failed")]
if failed:
return False, f"Could not fetch diff for PR(s): {failed}"
diff_lines = []
for pr in prs:
chunk = outputs["files"].get(f"output/pr-{pr.get('number')}.diff")
if chunk is None:
return False, f"output/pr-{pr.get('number')}.diff not captured"
diff_lines.extend(chunk.splitlines())
# Skip diff metadata: file headers, hunk headers (whose trailing
# function context can legitimately mention the symbol), and index
# lines. Everything else is a deletion ("-"), addition ("+"), or
# unchanged context line — the symbol may only appear in deletions.
meta = ("--- ", "+++ ", "diff ", "index ", "@@")

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 — removed_symbols judge's diff-metadata skip-list omits rename/mode-change header lines

Verified: meta = ("--- ", "+++ ", "diff ", "index ", "@@") is the only skip-list for non-content diff lines. Unified/git diff output also emits rename from <path>, rename to <path>, old mode <mode>, new mode <mode>, similarity index NN%, and Binary files ... differ lines for renamed/mode-changed/binary files, none of which are in the skip list. A future removal case whose PR renames a file to a path containing the removed symbol (e.g. rename to config/VerboseLoggingHandler.go) would have that line misclassified as a non-deletion "survivor" line and fail the case even though the symbol was legitimately removed elsewhere.

Suggestion: Add the rename/mode-change/binary line prefixes to the meta tuple, or restrict the content-line check to only lines starting with ' ', '+', or '-' rather than trying to enumerate every non-content prefix.

problems = []
for sym in symbols:

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 — removed_symbols judge aggregates deletions/survivors across the whole diff, so a partial/incomplete removal can still pass

Verified against current eval.yaml (head 14cd11d): the removed_symbols check counts deleted/survivors per symbol across ALL diff lines from every touched hunk, with no correlation to which file/occurrence was deleted. It only requires "at least one deletion line across the whole diff" and "zero non-deletion lines mentioning the symbol in any touched hunk". Because VerboseLogging/verbose_logging is legitimately deleted in config.go's struct field+yaml tag and fields.go's SetField case regardless, deleted is already non-zero from those alone. If the agent misses the third required edit — the raw verbose_logging: true literal in config_test.go's TestLoad string (which annotations.yaml's own code_expectations now explicitly calls out: "SetField rejects unknown keys, so leaving it breaks Load()") — that untouched line never appears in the diff at all (unified diff only shows lines inside changed hunks/context windows), so it contributes neither a deletion nor a survivor. The judge would report "All declared symbols removed cleanly" and expected_files would also pass (config_test.go was touched elsewhere), so a PR that ships a broken Load() could pass the entire eval at min_pass_rate 1.0. No judge runs the fixture's tests to catch this.

Suggestion: Correlate each removed_symbols deletion requirement with the specific expected_files declaration site(s) (e.g., parse the diff per-file and require a deletion in each file expected to contain the symbol), or diff the final file contents against a known-good post-fix reference instead of relying on a floating "at least one deletion anywhere" check.

deleted = 0
survivors = 0
for line in diff_lines:
if sym not in line or line.startswith(meta):

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 — removed_symbols uses unanchored substring matching, risking false-fails on comments/renamed identifiers

Verified: if sym not in line or line.startswith(meta): continue is a plain substring test with no word-boundary or comment-awareness. This means: (1) a legitimate explanatory comment/commit-message-style line like // Removed unused VerboseLogging in an added or context line would count as a "survivor" and fail an otherwise-correct removal; (2) renaming the field to something like VerboseLoggingEnabled would false-fail because VerboseLogging is a substring of the new identifier; (3) any unchanged context line in a touched hunk that happens to mention the symbol name (e.g. in a docstring) also counts as a survivor. This is stricter than the actual intent and can penalize correct fixes.

Suggestion: Use word-boundary-aware matching (e.g. regex \bVerboseLogging\b) and/or restrict the survivor check to added lines only (line.startswith('+')), letting unchanged context lines mention the symbol without failing the case.

continue
if line.startswith("-"):
deleted += 1
else:
survivors += 1
if survivors:
problems.append(f"{sym}: present in {survivors} non-deletion diff line(s)")
elif not deleted:
problems.append(f"{sym}: no deletion lines in diff")
if problems:
return False, "Symbols not fully removed: " + "; ".join(problems)
return True, f"All declared symbols removed cleanly: {symbols}"

# forbidden_labels / max_turns / max_cost below are shared verbatim with
# eval/fix/eval.yaml and eval/review/eval.yaml — update all three if changing.
- name: forbidden_labels
Expand Down Expand Up @@ -237,6 +302,8 @@ thresholds:
min_pass_rate: 1.0
expected_files:
min_pass_rate: 1.0
removed_symbols:
min_pass_rate: 1.0
forbidden_labels:
min_pass_rate: 1.0
max_turns:
Expand Down
3 changes: 3 additions & 0 deletions eval/code/repos/taskrunner/README.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,3 @@
# taskrunner

A minimal task runner that reads a YAML config and executes registered tasks.
47 changes: 47 additions & 0 deletions eval/code/repos/taskrunner/config/config.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,47 @@
package config

import (
"fmt"
"os"

"github.com/eval-org/taskrunner/config/internal/yaml"
)

// Config holds the task runner configuration.
type Config struct {
// MaxRetries controls how many times a failed task is retried.
MaxRetries int `yaml:"max_retries"`

// Timeout is the per-task timeout in seconds.
Timeout int `yaml:"timeout"`

// VerboseLogging enables detailed debug output.
VerboseLogging bool `yaml:"verbose_logging"`

// Workers is the number of concurrent task workers.
Workers int `yaml:"workers"`
}

// Defaults returns a Config with sensible default values.
func Defaults() Config {
return Config{
MaxRetries: 3,
Timeout: 60,
VerboseLogging: false,
Workers: 4,
}
}

// Load reads a YAML config file and returns a Config.
// Missing fields are filled with defaults.
func Load(path string) (Config, error) {
data, err := os.ReadFile(path)
if err != nil {
return Config{}, fmt.Errorf("reading config %s: %w", path, err)
}
cfg := Defaults()
if err := yaml.Unmarshal(data, &cfg); err != nil {
return Config{}, fmt.Errorf("parsing config %s: %w", path, err)
}
return cfg, nil
}
103 changes: 103 additions & 0 deletions eval/code/repos/taskrunner/config/config_test.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,103 @@
package config

import (
"os"
"path/filepath"
"testing"
)

func TestDefaults(t *testing.T) {
cfg := Defaults()
if cfg.MaxRetries != 3 {
t.Errorf("MaxRetries = %d, want 3", cfg.MaxRetries)
}
if cfg.Timeout != 60 {
t.Errorf("Timeout = %d, want 60", cfg.Timeout)
}
if cfg.VerboseLogging != false {
t.Errorf("VerboseLogging = %v, want false", cfg.VerboseLogging)
}
if cfg.Workers != 4 {
t.Errorf("Workers = %d, want 4", cfg.Workers)
}
}

func TestLoad(t *testing.T) {
content := `max_retries: 5
timeout: 120
verbose_logging: true

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 — code_expectations omits that config_test.go's embedded raw YAML literal must also be edited, not just Go-level assertions

annotations.yaml's code_expectations says the agent must remove VerboseLogging from "the struct definition, the Defaults() return value, the SetField() switch case, and all test assertions" but never mentions that the raw embedded YAML string in TestLoad's content variable (here: verbose_logging: true) also needs to be edited. This matters given the concurrent fix in yaml.go/fields.go: since SetField now has a default: case returning fmt.Errorf("unknown config key: %s", key), leaving verbose_logging: true in the raw content while removing the Go-level field/case/assertions would make Load() fail with "unknown config key: verbose_logging" at test time. Because no judge actually runs go test against the resulting PR, an agent could plausibly ship this exact incomplete removal, touch all three expected_files, and still pass the eval.

Suggestion: Extend code_expectations to explicitly call out that the fixture's embedded raw YAML in config_test.go must be updated too (not just Go-level struct/assertions), and/or treat this as supporting evidence for adding a real correctness judge rather than relying solely on file-touch matching.

workers: 8
`
dir := t.TempDir()
path := filepath.Join(dir, "config.yaml")
if err := os.WriteFile(path, []byte(content), 0644); err != nil {
t.Fatal(err)
}

cfg, err := Load(path)
if err != nil {
t.Fatal(err)
}

if cfg.MaxRetries != 5 {
t.Errorf("MaxRetries = %d, want 5", cfg.MaxRetries)
}
if cfg.Timeout != 120 {
t.Errorf("Timeout = %d, want 120", cfg.Timeout)
}
if cfg.VerboseLogging != true {
t.Errorf("VerboseLogging = %v, want true", cfg.VerboseLogging)
}
if cfg.Workers != 8 {
t.Errorf("Workers = %d, want 8", cfg.Workers)
}
}

func TestLoadPartial(t *testing.T) {
content := `timeout: 30
`
dir := t.TempDir()
path := filepath.Join(dir, "config.yaml")
if err := os.WriteFile(path, []byte(content), 0644); err != nil {
t.Fatal(err)
}

cfg, err := Load(path)
if err != nil {
t.Fatal(err)
}

if cfg.MaxRetries != 3 {
t.Errorf("MaxRetries = %d, want 3 (default)", cfg.MaxRetries)
}
if cfg.Timeout != 30 {
t.Errorf("Timeout = %d, want 30", cfg.Timeout)
}
if cfg.VerboseLogging != false {
t.Errorf("VerboseLogging = %v, want false (default)", cfg.VerboseLogging)
}
}

func TestLoadMalformedLine(t *testing.T) {
dir := t.TempDir()
path := filepath.Join(dir, "config.yaml")
if err := os.WriteFile(path, []byte("not a valid line\n"), 0644); err != nil {
t.Fatal(err)
}
_, err := Load(path)
if err == nil {
t.Error("Load() = nil error for malformed YAML, want error")
}
}

func TestLoadUnknownKey(t *testing.T) {
dir := t.TempDir()
path := filepath.Join(dir, "config.yaml")
if err := os.WriteFile(path, []byte("bogus_key: 1\n"), 0644); err != nil {
t.Fatal(err)
}
_, err := Load(path)
if err == nil {
t.Error("Load() = nil error for unknown key, want error")
}
}
40 changes: 40 additions & 0 deletions eval/code/repos/taskrunner/config/fields.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,40 @@
package config

import (
"fmt"

"github.com/eval-org/taskrunner/config/internal/yaml"
)

// SetField implements the configFields interface for the minimal YAML parser.
func (c *Config) SetField(key, value string) error {
switch key {
case "max_retries":
v, err := yaml.ParseInt(value)
if err != nil {
return err
}
c.MaxRetries = v
case "timeout":
v, err := yaml.ParseInt(value)
if err != nil {
return err
}
c.Timeout = v
case "verbose_logging":
v, err := yaml.ParseBool(value)
if err != nil {
return err
}
c.VerboseLogging = v
case "workers":
v, err := yaml.ParseInt(value)
if err != nil {
return err
}
c.Workers = v
default:
return fmt.Errorf("unknown config key: %s", key)
}
return nil
}
Comment thread
qodo-code-review[bot] marked this conversation as resolved.
Loading
Loading