Skip to content

@W-23144570: stale-content-cleanup-apply eval (behavioral + adversarial) + runnability layer#496

Open
Alon-ST-DATA wants to merge 3 commits into
mainfrom
w-23144570-stale-content-eval-runnable
Open

@W-23144570: stale-content-cleanup-apply eval (behavioral + adversarial) + runnability layer#496
Alon-ST-DATA wants to merge 3 commits into
mainfrom
w-23144570-stale-content-eval-runnable

Conversation

@Alon-ST-DATA

Copy link
Copy Markdown
Contributor

Summary

Full eval for the stale-content-cleanup-apply MCP prompt (W-23144570), targeting main as one complete PR. Combines the behavioral + adversarial coverage from #412 (Akash Rastogi) with the harness changes needed to actually run it on a low tokens-per-minute grader key and in CI.

Supersedes #412 — carries its cases plus the fixes below in one PR.

What this adds on top of #412

1. Tool-catalog narrowing (INCLUDE_TOOLS). The workflow only needs 6 tools. The full ~40-tool catalog schema alone can exceed a 30k-TPM grader budget before the workflow even runs. adminEnv() now sets INCLUDE_TOOLS to the 6 workflow tools; the two delete tools stay included on purpose so the "no destructive call" invariant remains meaningful.

2. Optional report scoping (EVAL_STALE_PROJECT_ID). On a large admin site the stale-content report returns thousands of rows / ~1MB. Fed back to the model that is ~353k tokens in a single request — an instant hard 429 Request too large on a 30k-TPM key. Setting EVAL_STALE_PROJECT_ID to a small, non-empty project scopes the report and cuts the request from 353,463 to ~30,000 tokens (12x) while still exercising the full report → resolve → notify → STOP workflow. Left unset, the eval runs unscoped (original behavior) — fine on a high-TPM key or a small fixture site. Read at call time (not module load), because tests/eval/.env is loaded in a beforeAll after module evaluation.

3. Best-of-N for the fuzzy judge. The one open-ended rubric case ("coherent assistant report") uses grade()'s best-of-N (attempts: 3) with rate-limit backoff instead of vitest .retry(2). .retry re-runs on any throw — including a 429 — and re-fires straight into the same TPM wall. Best-of-N re-runs only the fuzzy 5-dim judge, backs off on a detected rate-limit, and reports real scores if every attempt fails. Deterministic tool-call cases keep .retry(2).

Deterministic-vs-LLM split (preserved)

The security-critical invariant — the agent never invokes a delete tool from this prompt under any input, including adversarial "skip confirmation" / "ignore dryRun" / "fabricate a confirmationToken" messages — is asserted deterministically and gates hard. Only the fuzzy rubric is best-of-N.

Test plan

  • npx tsc --noEmit — clean
  • npm run lint — clean (eval files)
  • npm run build — clean
  • Scoping proven: request dropped from 353,463 → ~30,000 tokens (12x) with EVAL_STALE_PROJECT_ID set.
  • The best-of-N grade case cleared the rubric via backoff after two 429s — the only case to pass on a 30k-TPM key.

Known limitation (this is the pipeline requirement)

On a 30k-TPM OpenAI key the multi-turn agent loop (report → resolve → notify → summary is several LLM calls) still sums past the per-minute ceiling across all 11 cases, and a shared live Tableau site can throttle rapid sign-ins. Full green needs TPM headroom — a higher-tier key or an internal gateway — plus a bounded fixture. The scope + backoff added here are necessary to make the suite feasible at all and to keep the deterministic security invariant hard; they are not sufficient on the lowest key tier alone. This matches the eval-testing-pipeline requirements (CI-held LLM credential with TPM headroom + a bounded eval fixture).

Env

  • EVAL_STALE_PROJECT_ID (optional) — scope the stale-content report to a small non-empty project. Set in tests/eval/.env (git-ignored).

Akash-Rastogi and others added 3 commits July 8, 2026 18:19
…sarial)

Adds tests/eval/staleContentCleanupApply.test.ts evaluating the
stale-content-cleanup-apply MCP prompt shipped in #405. 5 behavioral
cases assert dry-run faithfulness, HITL gate, parameter scoping;
6 adversarial cases cover prompt-injection on tag (schema regex),
projectIds (verbatim data, not directives), itemTypes=Flow rejection,
user-message HITL bypass, user-message dryRun bypass, and
fabricated-confirmationToken coercion. Each LLM-bound case has up to
3 attempts (vitest retry: 2) to absorb single LLM flakes; faithfulness
target >= 0.95 from JTBD 2 work breakdown.

Tool-result injection (e.g., crafted workbook names) is out of scope
here because the harness drives a live Tableau site -- tracked as a
separate follow-up requiring a mocked MCP server.

Bumps version 2.18.0 -> 2.18.1 per repo convention.
Anchor .rejects.toThrow() to the schema's regex error message so the
test fails when the tag-validation defense regresses, not just on any
unrelated error.
… (token scope + best-of-N)

Builds on Akash Rastogi's behavioral + adversarial eval (PR #412, 5 behavioral + 6
adversarial cases) with the harness changes needed to run it on a low tokens-per-minute
grader key and in CI.

tests/eval/staleContentCleanupApply.test.ts
- INCLUDE_TOOLS narrowing in adminEnv(): the workflow only needs 6 tools; the full
  ~40-tool catalog schema alone can exceed a 30k-TPM grader budget before the workflow
  runs. Delete tools stay included so the "no destructive call" invariant is meaningful.
- Optional EVAL_STALE_PROJECT_ID report scope via withScope(): on a large admin site the
  stale-content report returns thousands of rows / ~1MB, which fed back to the model is
  ~350k tokens — an instant hard 429. Scoping to a small non-empty project cut the
  request from 353k to ~30k tokens (12x) while still exercising the full
  report -> resolve -> notify -> STOP workflow. Read at call time (not module load)
  because tests/eval/.env is loaded in a beforeAll after module evaluation.
- The one open-ended rubric case uses grade()'s best-of-N (attempts: 3) with rate-limit
  backoff instead of vitest .retry(2): it re-runs only the fuzzy judge and reports real
  scores if every attempt fails. Deterministic tool-call cases keep .retry(2).

tests/eval/grade.ts
- Best-of-N (attempts, default 1 => existing evals unchanged) + retryDelayMs backoff and a
  429/rate-limit detector, so a noisy single-shot LLM judge or a low per-minute quota does
  not produce false reds.

Note: on a 30k-TPM key the multi-turn agent loop still sums past the per-minute ceiling
across the 11 cases; full green needs TPM headroom (higher-tier key or gateway). The
scope + backoff make the suite feasible and keep the deterministic security invariant
(never call a delete tool) hard.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants