Skip to main content

Stop Running Unrelated Monorepo Test Suites on Every Agent Edit: Full-Suite Runs vs. AST Dependency-Scoped Incremental Tests (-95.7% Input Tokens)

Updated 6 min readMarkdown

πŸ’‘ AI Labo Benchmark Series β€” Experiment E30 (Feedback Sensors & Incremental Verification)

This post is part of our empirical benchmark series testing AI coding agent harness patterns inside network-isolated Proxmox VMs against a hidden 20-case grading suite.


Metadata & Experiment Identity

  • Experiment ID: E30 (Backlog Candidate C026)
  • Run ID: e30-20261029-vm234-01
  • Sandbox Environment: Proxmox Isolated VM VM234 (ai-e30-234, 0 NICs, PrivateNetwork=yes, NoNewPrivileges=yes, InaccessiblePaths, UID split labexp30 / labjudge30)
  • Model: gemini-3.8-flash (thinkingLevel: LOW, temperature=0.0)
  • Pattern Under Test: "OpenAI 1M-LOC Agent Harness Feedback Loop Optimization: AST Dependency-Scoped Incremental Test Execution" β€” Unfiltered Full-Repository Test Suite (FullSuiteTestExecutionRunner) vs. AST Dependency-Scoped Incremental Runner (DependencyScopedIncrementalRunner)

1. Executive Summary (TL;DR)

In a large monorepo or multi-package service, running pytest across the entire repository after every agent editβ€”including 148 completely unrelated packages (auth_ledger, fx_rates, webhook_dispatch, audit_archive)β€”creates a massive Signal-to-Noise Ratio (SNR) collapse:

  1. Wall-Clock Lag: The agent waits for 150+ unaffected test files to import and run.
  2. Attention Dilution: When 82,343 characters of unfiltered full-suite PASS/SKIP telemetry flood the context window, the model loses sight of subtle edge cases in the 3 modules that were actually modified.

We tested AST Dependency-Scoped Incremental Test Execution (the Python AST equivalent of pytest-testmon / bazel test affected-target scoping) inside isolated VM VM234 on the 3-module enterprise payment settlement pipeline (validator.py βž” settlement.py βž” statement.py):

  • Mode A (FullSuiteTestExecutionRunner): Runs all 168 repository suites without dependency scoping on intermediate turns (82,343 chars of monorepo test output across Turns 1–2).
  • Mode B (DependencyScopedIncrementalRunner): Uses a reverse AST import graph to skip 148 unaffected monorepo subtrees during inner-loop verification, scoping feedback strictly to the 20 affected tests (6 in validator.py, 9 in settlement.py, 5 E2E in statement.py) before running the full gate at final submit.

Key Result

Mode A dropped to 16 / 20 (80.0% β€” 4 failed cases) after 3 turns (52,146 cumulative input tokens)β€”missing extra key filtering (new_item = dict(item)), rejecting 1000 bps (0 <= bps < 1000), and rejecting $0 authorizations (cents <= 0). Mode B achieved a 20 / 20 (100%) pass in 1 turn, cutting LLM calls by -66.7% (3 βž” 1), cumulative input tokens by -95.7% (52,146 βž” 2,240 tokensβ€”a 23.28x reduction), API cost by -84.4% ($0.0499 βž” $0.0078), and running 2.11x faster (28.97s βž” 13.76s, -52.5%).

Metric Mode A: Unfiltered Full Suite (FullSuiteTestExecution) Mode B: AST Dependency-Scoped (DependencyScopedIncremental) Delta / Improvement
Hidden Grading Suite Score (labjudge30) 16 / 20 (80.0% β€” 4 failures) 20 / 20 (100% β€” Perfect) +20.0% accuracy (+4 cases)
Full-Suite Noise Traps (gotcha_traps_hit) 2 traps (Turns 1 & 2) 0 traps -100% (zero rework)
LLM Calls (model_calls) 3 calls 1 call (1-shot) -66.7% (3 βž” 1 turn)
Cumulative File Edits (file_edits) 5 edits 3 edits (1 per module) -40.0% fewer edits
Cumulative Tool Output (tool_output_chars) 82,343 chars (168 suites) 104 chars (20 scoped tests) -99.9% (791.8x noise reduction)
Turn 1 Tokens (In / Out) 1,657 in / 13 out 2,240 in / 1,624 out +583 in for scoped contract matrix
Turn 2 Tokens (In / Out) 17,002 in / 1,256 out β€” (completed on Turn 1) Avoided 41,188-char Turn 1 suite log
Turn 3 Tokens (In / Out) 33,487 in / 1,608 out β€” (completed on Turn 1) Avoided 40,842-char Turn 2 suite log
Cumulative Input Tokens (promptTokenCount) 52,146 tokens 2,240 tokens -95.7% (23.28x efficiency)
Cumulative Output Tokens (candidatesTokenCount) 2,877 tokens 1,624 tokens -43.6% output reduction
Elapsed Execution Time (elapsed_seconds) 28.97 sec 13.76 sec -52.5% (2.11x faster)
Estimated API Cost (estimated_micro_usd) $0.0499 (49,900 micro-USD) $0.0078 (7,770 micro-USD) -84.4% (6.42x cheaper)

2. Why Full-Suite Noise Caused 4 Missed Boundary Cases (16/20)

Inspecting Mode A's final validator.py (full-suite-bundle.json) reveals how burying 3 target contracts inside 82,343 characters of unrelated monorepo test logs degraded code quality:

# ❌ Mode A (FullSuiteTestExecutionRunner β€” Turn 3 Final validator.py, 16/20)
        if cnt <= 0 or cents <= 0 or not (0 <= bps < 1000):  # <-- Fails validator_05 & e2e_03 ($0 auth rejected)
                                                             # <-- Fails validator_04 (1000 bps rejected by `< 1000`)
            raise ValueError("numeric bounds violated")
        new_item = dict(item)                                # <-- Fails validator_01 (leaks unknown "extra" keys)
        new_item["merchant_id"] = mid.strip().upper()
        new_item["currency"] = cur.strip().upper()
        out.append(new_item)

3. Drop-In Implementation Recipe: Reverse AST Import Graph + Transitive Test Selector

With zero third-party dependencies, you can build an exact reverse-import DAG in Python's standard ast module and select only the test files transitively impacted by git diff --name-only:

import ast
from collections import defaultdict
import os

def build_reverse_import_graph(src_files: list[str]) -> dict[str, set[str]]:
    """Builds a reverse dependency map `imported_module -> set(dependent_modules)` via Python AST."""
    rev_graph: dict[str, set[str]] = defaultdict(set)
    for path in src_files:
        mod_name = os.path.splitext(os.path.basename(path))[0]
        with open(path, "r", encoding="utf-8") as f:
            tree = ast.parse(f.read(), filename=path)
        for node in ast.walk(tree):
            if isinstance(node, ast.Import):
                for alias in node.names:
                    rev_graph[alias.name.split(".")[0]].add(mod_name)
            elif isinstance(node, ast.ImportFrom) and node.module:
                rev_graph[node.module.split(".")[0]].add(mod_name)
    return rev_graph

def select_affected_modules(changed_modules: list[str], rev_graph: dict[str, set[str]]) -> set[str]:
    """Finds all modules and test suites transitively affected by `changed_modules` via BFS."""
    affected = set(changed_modules)
    queue = list(changed_modules)
    while queue:
        cur = queue.pop(0)
        for dep in rev_graph.get(cur, ()):
            if dep not in affected:
                affected.add(dep)
                queue.append(dep)
    return affected

4. Practical Takeaway: Two-Tier Test Execution (Scoped Inner Loop, Full Outer Gate)

  1. Scope inner-loop test runs strictly to the transitive closure of modified files: During iterative editing turns, run only select_affected_modules(git_diff_files, rev_graph) so feedback arrives in milliseconds with 100% relevant assertions.
  2. Run the full repository test suite only at the final SUBMIT gate (filtered via E16 + E29): This gives you fast, high-SNR inner-loop iteration without ever sacrificing full-repo regression safety at merge time.

πŸ”— Continue Reading the Series

No comments yet. Be the first to comment.

More from this blog