@malloy-publisher/skills 0.1.20 → 0.1.21

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (79) hide show
  1. package/package.json +1 -1
  2. package/skills/eval-answer/SKILL.md +45 -5
  3. package/skills/eval-answer/reference/definition-ledger.md +305 -0
  4. package/skills/eval-answer/reference/ledger-schema.md +36 -7
  5. package/skills/eval-answer/scripts/check_coverage.py +12 -21
  6. package/skills/eval-answer/scripts/check_coverage_test.py +22 -0
  7. package/skills/eval-answer/scripts/check_findable.py +386 -0
  8. package/skills/eval-answer/scripts/check_findable_test.py +326 -0
  9. package/skills/eval-answer/scripts/json_scan.py +43 -0
  10. package/skills/eval-answer/scripts/ledger.py +63 -7
  11. package/skills/eval-answer/scripts/ledger_test.py +7 -0
  12. package/skills/eval-answer/scripts/mcp_payload.py +111 -2
  13. package/skills/eval-answer/scripts/mcp_payload_test.py +73 -2
  14. package/skills/eval-answer/scripts/publisher_rest.py +10 -4
  15. package/skills/eval-answer/scripts/publisher_rest_test.py +56 -0
  16. package/skills/eval-answer/scripts/score_retrieval.py +326 -41
  17. package/skills/eval-answer/scripts/score_retrieval_test.py +537 -15
  18. package/skills/eval-answer/scripts/verify_definitions.py +752 -0
  19. package/skills/eval-answer/scripts/verify_definitions_test.py +820 -0
  20. package/skills/eval-answer/scripts/verify_goldens.py +336 -22
  21. package/skills/eval-answer/scripts/verify_goldens_test.py +261 -2
  22. package/skills/eval-diagnose/SKILL.md +47 -2
  23. package/skills/eval-diagnose/scripts/cluster_failures.py +57 -14
  24. package/skills/eval-diagnose/scripts/cluster_failures_test.py +127 -0
  25. package/skills/eval-diagnose/scripts/diagnose.py +471 -42
  26. package/skills/eval-diagnose/scripts/diagnose_test.py +421 -0
  27. package/skills/eval-import/SKILL.md +180 -1
  28. package/skills/eval-import/reference/case-format.md +1 -1
  29. package/skills/eval-improve/SKILL.md +1 -1
  30. package/skills/eval-improve/scripts/improve.py +29 -3
  31. package/skills/eval-improve/scripts/improve_test.py +51 -0
  32. package/skills/eval-judge/SKILL.md +1 -1
  33. package/skills/eval-loop/SKILL.md +112 -3
  34. package/skills/eval-loop/reference/measurement.md +44 -4
  35. package/skills/eval-loop/reference/running-a-run.md +5 -1
  36. package/skills/eval-loop/scripts/agent_harness.py +21 -20
  37. package/skills/eval-loop/scripts/agent_harness_test.py +136 -0
  38. package/skills/eval-loop/scripts/build_run_package.py +9 -3
  39. package/skills/eval-loop/scripts/flip_table.py +107 -2
  40. package/skills/eval-loop/scripts/flip_table_test.py +165 -0
  41. package/skills/eval-loop/scripts/run_baseline.py +956 -97
  42. package/skills/eval-loop/scripts/run_baseline_test.py +799 -1
  43. package/skills/eval-loop/templates/eval-run-package/README.md +19 -5
  44. package/skills/eval-loop/templates/eval-run-package/eval_run.malloy +48 -19
  45. package/skills/eval-loop/templates/eval-run-package/eval_run.malloynb +11 -4
  46. package/skills/eval-loop/templates/eval-run-package/public/app.js +12 -4
  47. package/skills/eval-report/SKILL.md +362 -0
  48. package/skills/malloy/SKILL.md +2 -1
  49. package/skills/malloy-analysis/SKILL.md +1 -1
  50. package/skills/malloy-analysis-pitfalls/SKILL.md +13 -1
  51. package/skills/malloy-analysis-report/SKILL.md +1 -1
  52. package/skills/malloy-analyze/SKILL.md +1 -1
  53. package/skills/malloy-charts/SKILL.md +1 -1
  54. package/skills/malloy-dashboards/SKILL.md +1 -1
  55. package/skills/malloy-debug/SKILL.md +1 -1
  56. package/skills/malloy-define/SKILL.md +1 -1
  57. package/skills/malloy-discover/SKILL.md +1 -1
  58. package/skills/malloy-document/SKILL.md +1 -1
  59. package/skills/malloy-getting-started/SKILL.md +1 -1
  60. package/skills/malloy-gotchas-modeling/SKILL.md +1 -1
  61. package/skills/malloy-gotchas-queries/SKILL.md +1 -1
  62. package/skills/malloy-gotchas-rendering/SKILL.md +1 -1
  63. package/skills/malloy-html-data-app-embedding/SKILL.md +1 -1
  64. package/skills/malloy-html-data-app-runtime/SKILL.md +1 -1
  65. package/skills/malloy-html-data-apps/SKILL.md +1 -1
  66. package/skills/malloy-lookml-review/SKILL.md +1 -1
  67. package/skills/malloy-materialization/SKILL.md +1 -1
  68. package/skills/malloy-materialization-tuning/SKILL.md +1 -1
  69. package/skills/malloy-model/SKILL.md +1 -1
  70. package/skills/malloy-model-as-you-go/SKILL.md +1 -1
  71. package/skills/malloy-modeling/SKILL.md +1 -1
  72. package/skills/malloy-notebook-chat/SKILL.md +1 -1
  73. package/skills/malloy-notebooks/SKILL.md +1 -1
  74. package/skills/malloy-patterns/SKILL.md +1 -1
  75. package/skills/malloy-phrase-detection/SKILL.md +3 -2
  76. package/skills/malloy-publish/SKILL.md +1 -1
  77. package/skills/malloy-queries/SKILL.md +1 -1
  78. package/skills/malloy-review/SKILL.md +1 -1
  79. package/skills/malloy-scope/SKILL.md +1 -1
@@ -4,11 +4,14 @@ credited with, which retriever answered, what was actually re-executed, and
4
4
  when an attempt is unjudgeable."""
5
5
  import argparse
6
6
  import json
7
+ import os
7
8
  import pathlib
8
9
  import shutil
10
+ import subprocess
9
11
  import sys
10
12
  import tempfile
11
13
  import unittest
14
+ from unittest import mock
12
15
 
13
16
  sys.path.insert(0, str(pathlib.Path(__file__).resolve().parent))
14
17
  import run_baseline as rb # noqa: E402
@@ -260,6 +263,264 @@ class JudgeGate(unittest.TestCase):
260
263
  self.assertEqual(v["reason"], "no_saved_verdict")
261
264
 
262
265
 
266
+ class VerdictObject(unittest.TestCase):
267
+ """Finding the judge's verdict in a reply that holds other braces.
268
+
269
+ The old read spanned from the FIRST brace in the document to the last, so a
270
+ judge that quoted a Malloy snippet on its way to deciding handed
271
+ `json.loads` a blob starting mid-query. The case then counted in no bucket
272
+ at all and left the denominator the pass rate is printed over.
273
+ """
274
+
275
+ GOOD = '{"why": "the figures agree", "verdict": "match", "confidence": 9}'
276
+
277
+ def test_a_quoted_malloy_query_does_not_break_the_read(self):
278
+ # The reproduction: a fenced query, then the verdict.
279
+ text = ("```malloy\nrun: orders -> { aggregate: n is count() }\n```\n"
280
+ f"Looks right.\n{self.GOOD}")
281
+ self.assertEqual(rb.parse_verdict(text)["verdict"], "match")
282
+
283
+ def test_the_last_verdict_object_wins(self):
284
+ # The judge is told to end with the object, and prose reasoning toward
285
+ # it may quote a draft on the way.
286
+ text = ('{"why": "draft", "verdict": "near_match", "confidence": 6}\n'
287
+ f"On reflection:\n{self.GOOD}")
288
+ v = rb.parse_verdict(text)
289
+ self.assertEqual(v["verdict"], "match")
290
+ self.assertEqual(v["reason"], "the figures agree")
291
+
292
+ def test_a_nested_object_still_parses(self):
293
+ text = ('{"why": "a", "verdict": "match", "confidence": 9, '
294
+ '"column_pairing": {"gold": "pred"}}')
295
+ self.assertEqual(rb.parse_verdict(text)["column_pairing"],
296
+ {"gold": "pred"})
297
+
298
+ def test_prose_around_the_object_is_fine(self):
299
+ self.assertEqual(
300
+ rb.parse_verdict(f"Here you go:\n{self.GOOD}\nHope that helps"
301
+ )["verdict"], "match")
302
+
303
+ def test_a_reply_with_no_object_is_unparseable(self):
304
+ # The shape five cases across three runs actually produced: the judge
305
+ # reasons to the point of deciding and the process ends.
306
+ v = rb.parse_verdict("I have what I need to decide.")
307
+ self.assertIsNone(v["verdict"])
308
+ self.assertEqual(v["reason"], "judge_unparseable")
309
+
310
+ def test_braces_without_a_verdict_are_not_a_verdict(self):
311
+ v = rb.parse_verdict('{"note": "no verdict here"}')
312
+ self.assertEqual(v["reason"], "judge_unparseable")
313
+
314
+ def test_the_retry_predicate_agrees_with_the_parser(self):
315
+ # `judge_unusable` reuses `parse_verdict` rather than re-deciding what
316
+ # parseable means, so a reply this now reads must not be re-rolled.
317
+ text = f"```malloy\nrun: x -> {{ aggregate: n is count() }}\n```\n{self.GOOD}"
318
+ self.assertFalse(rb.judge_unusable([{"type": "assistant"}], text))
319
+
320
+
321
+ class JudgePrompt(unittest.TestCase):
322
+ """What the judge is SHOWN, and whether a run can say which version of it.
323
+
324
+ The tests cover the code paths; the defect that ran for three arms was in
325
+ the text the model reads. These are about that text.
326
+ """
327
+
328
+ def test_the_prompt_is_pinned_separately_from_the_skill(self):
329
+ # `rubricSha` hashes eval-judge/SKILL.md. The prompt template lives
330
+ # here and carries the golden rendering and the tie-breaking rules, so
331
+ # a run that names only the skill cannot say which judge it ran.
332
+ import ledger
333
+ self.assertIn("judgePromptSha", ledger.RUN_OPTIONAL)
334
+
335
+ def test_a_stale_rubric_figure_loses_to_the_golden(self):
336
+ # An answer holding the golden's own figures was failed against a
337
+ # rubric still quoting the previous morning's. Stated as a rule the
338
+ # judge applies at any precision, rather than a regex over prose.
339
+ self.assertIn("Where the CASE RUBRIC quotes a figure that the GOLDEN "
340
+ "contradicts", rb.JUDGE_PROMPT)
341
+ self.assertIn("the rubric's figure is stale prose", rb.JUDGE_PROMPT)
342
+
343
+
344
+ class JudgeCostAcrossARebuild(unittest.TestCase):
345
+ """A rebuild spends nothing on judging and must not erase what judging cost.
346
+
347
+ Found by writing a report off a rebuilt run: its cost line read $0.00 judge
348
+ for verdicts the original arm had paid $0.78 for.
349
+ """
350
+
351
+ def test_a_run_that_judged_records_what_it_spent(self):
352
+ self.assertEqual(rb.carry_judge_cost(0.78, None), (0.78, False))
353
+
354
+ def test_a_rebuild_keeps_the_earlier_figure_and_marks_it(self):
355
+ self.assertEqual(rb.carry_judge_cost(0.0, 0.78), (0.78, True))
356
+
357
+ def test_a_rejudge_wins_over_the_earlier_figure(self):
358
+ # It really did judge again, so the new spend is this run's own.
359
+ self.assertEqual(rb.carry_judge_cost(0.91, 0.78), (0.91, False))
360
+
361
+ def test_a_first_run_with_nothing_before_it_records_zero(self):
362
+ self.assertEqual(rb.carry_judge_cost(0.0, None), (0.0, False))
363
+
364
+ def test_a_prior_of_zero_is_not_carried(self):
365
+ # --no-judge then rebuild: there is no earlier spend to protect, and
366
+ # marking it carried would claim a judging that never happened.
367
+ self.assertEqual(rb.carry_judge_cost(0.0, 0.0), (0.0, False))
368
+
369
+ def test_the_prior_is_read_before_run_json_is_rewritten(self):
370
+ # The whole defect: read after `run_config` writes, it is always None
371
+ # and the fix silently does nothing.
372
+ src = pathlib.Path(rb.__file__).read_text()
373
+ self.assertLess(src.index("prior_judge = prior_judge_cost(a.out)"),
374
+ src.index('(a.out / "run.json").write_text'))
375
+
376
+ def test_a_missing_or_unreadable_run_json_is_none(self):
377
+ tmp = pathlib.Path(tempfile.mkdtemp())
378
+ try:
379
+ self.assertIsNone(rb.prior_judge_cost(tmp))
380
+ (tmp / "run.json").write_text("{not json")
381
+ self.assertIsNone(rb.prior_judge_cost(tmp))
382
+ finally:
383
+ shutil.rmtree(tmp, ignore_errors=True)
384
+
385
+
386
+ class TruncatedAttempt(unittest.TestCase):
387
+ """An attempt the turn cap cut off is not judged.
388
+
389
+ Measured on a 29-case customer arm: four attempts ended at exactly 31 turns
390
+ with `error_max_turns`, and each went to the judge as a complete answer.
391
+ One of them was a planning fragment -- "Let me build the correct monthly
392
+ trend query" -- which the judge scored `no_match` at confidence 9, where it
393
+ counted in the pass rate as if the model had got the question wrong.
394
+ """
395
+
396
+ def setUp(self):
397
+ self.tmp = pathlib.Path(tempfile.mkdtemp())
398
+
399
+ def tearDown(self):
400
+ shutil.rmtree(self.tmp, ignore_errors=True)
401
+
402
+ def judge(self, att, *, rebuild=True, rejudge=False, golden=None):
403
+ a = argparse.Namespace(rebuild=rebuild, rejudge=rejudge)
404
+ return rb.run_judge(
405
+ {"qid": "q", "question": "?",
406
+ "golden": golden if golden is not None else {"status": "verified",
407
+ "value": 1}},
408
+ att, a, self.tmp, "", "", False)
409
+
410
+ def test_a_truncated_attempt_is_refused_and_says_so(self):
411
+ v = self.judge({"answer_text": "Let me build the correct query.",
412
+ "submitted": True, "error": "error_max_turns"})
413
+ self.assertIsNone(v["verdict"])
414
+ self.assertEqual(v["reason"], "answerer_truncated")
415
+
416
+ def test_it_is_decided_before_a_saved_verdict_is_reused(self):
417
+ # The point of putting it first: `--rebuild` over the runs that
418
+ # motivated this re-derives the right ledger from their transcripts,
419
+ # rather than restoring the verdict the truncated attempt was given.
420
+ (self.tmp / "q").mkdir(parents=True)
421
+ (self.tmp / "q" / "judge.md").write_text(
422
+ '{"why": "trails off", "verdict": "no_match", "confidence": 9}')
423
+ v = self.judge({"answer_text": "frag", "submitted": True,
424
+ "error": "error_max_turns"})
425
+ self.assertEqual(v["reason"], "answerer_truncated")
426
+ self.assertIsNone(v["verdict"])
427
+
428
+ def test_it_is_decided_before_the_golden_gate(self):
429
+ # Order matters the other way too: a truncated attempt against a
430
+ # provisional key is truncated. Both refuse, and the one that names
431
+ # the harness's own setting is the one a conductor can act on.
432
+ v = self.judge({"answer_text": "frag", "submitted": True,
433
+ "error": "error_max_turns"},
434
+ golden={"status": "provisional", "value": 1})
435
+ self.assertEqual(v["reason"], "answerer_truncated")
436
+
437
+ def test_any_other_harness_failure_is_also_refused(self):
438
+ # The four-strikes abort does NOT cover this: it stops the arm and
439
+ # still judges what it collected. Measured on a live run, four attempts
440
+ # whose text was the CLI's own "model not found" message were scored
441
+ # `no_match` for $0.29, and the run printed `passed 0 of 4 decided
442
+ # (0%)` about a model no answerer had reached.
443
+ v = self.judge({"answer_text": "Error: model not found",
444
+ "submitted": False, "error": "error_during_execution"})
445
+ self.assertIsNone(v["verdict"])
446
+ self.assertTrue(v["reason"].startswith("environment_failure"))
447
+ # The cap keeps its own word, because the remedy differs: re-run those
448
+ # cases at a higher cap, versus fix the environment and re-run the arm.
449
+ self.assertNotIn("truncated", v["reason"])
450
+
451
+ def test_the_cli_reporting_subtype_success_on_an_error_still_refuses(self):
452
+ # Observed exactly: `is_error` true with subtype "success", so the
453
+ # attempt carried `run_error: "success"`. Testing the VALUE would have
454
+ # let every one of those through; the test is that an error was
455
+ # reported at all.
456
+ v = self.judge({"answer_text": "Error: model not found",
457
+ "submitted": False, "error": "success"})
458
+ self.assertIsNone(v["verdict"])
459
+ self.assertTrue(v["reason"].startswith("environment_failure"))
460
+
461
+ def test_a_clean_attempt_is_unaffected(self):
462
+ v = self.judge({"answer_text": "4.2M", "submitted": True,
463
+ "error": None})
464
+ self.assertEqual(v["reason"], "no_saved_verdict")
465
+
466
+
467
+ class ContaminatedVerdict(unittest.TestCase):
468
+ """A voided verdict says why, and keeps what the judge said.
469
+
470
+ Nulling in silence left a score event carrying a correct `reason` and
471
+ `confidence` beside `verdict: null`, which is indistinguishable from the
472
+ field being dropped on the write path -- and was read as exactly that in a
473
+ real run report, where nine "lost" verdicts were recovered by re-parsing
474
+ the stored judge replies. They had been voided on purpose.
475
+ """
476
+
477
+ def void(self, verdict, breaches, *, vetoed=False):
478
+ """The real nulling, not a copy of it: `main` calls this same
479
+ function, so a change there cannot pass these tests by drifting."""
480
+ v = dict(verdict)
481
+ if vetoed:
482
+ # What the mustNotUse veto does just above the call site.
483
+ v["judge_verdict"] = v["verdict"]
484
+ v["verdict"] = "no_match"
485
+ return rb.void_contaminated(v, breaches)
486
+
487
+ def test_a_voided_verdict_names_contamination(self):
488
+ v = self.void({"verdict": "match", "reason": "the figures agree",
489
+ "confidence": 9},
490
+ ["host tool available to the answerer: Bash"])
491
+ self.assertIsNone(v["verdict"])
492
+ self.assertEqual(v["reason"], "contaminated")
493
+ # The schema documents this reason; before, the judge's own prose sat
494
+ # here and the null looked like a bug.
495
+ self.assertEqual(v["judge_verdict"], "match")
496
+
497
+ def test_a_veto_that_is_also_contaminated_keeps_the_judges_read(self):
498
+ # Two overrides on one case. `judge_verdict` is for what the JUDGE
499
+ # said, so the veto's own `no_match` must not displace it.
500
+ v = self.void({"verdict": "match", "reason": "ok", "confidence": 8},
501
+ ["mcp server 'malloy' failed"], vetoed=True)
502
+ self.assertIsNone(v["verdict"])
503
+ self.assertEqual(v["judge_verdict"], "match")
504
+
505
+ def test_a_clean_attempt_keeps_its_verdict_and_reason(self):
506
+ v = self.void({"verdict": "match", "reason": "the figures agree",
507
+ "confidence": 9}, [])
508
+ self.assertEqual(v["verdict"], "match")
509
+ self.assertEqual(v["reason"], "the figures agree")
510
+
511
+ def test_every_null_verdict_reason_is_in_the_documented_set(self):
512
+ # The generalisation of the report's own ask ("a score event with a
513
+ # reason also has a verdict, or an explicit unreadable marker"): a null
514
+ # verdict must always name which of the known causes it was, so no
515
+ # future nulling can be silent the way contamination was.
516
+ schema = (pathlib.Path(rb.__file__).parent.parent.parent
517
+ / "eval-answer" / "reference" / "ledger-schema.md").read_text()
518
+ for reason in ("not_submitted", "contaminated", "answerer_truncated",
519
+ "judge_unparseable", "no_saved_verdict"):
520
+ with self.subTest(reason):
521
+ self.assertIn(reason, schema)
522
+
523
+
263
524
  class GoldenStatusGate(unittest.TestCase):
264
525
  """A verdict is refused against a key nobody has established.
265
526
 
@@ -320,10 +581,14 @@ class GoldenStatusGate(unittest.TestCase):
320
581
  # `criteria` holds no value by design; its clauses are the comparison.
321
582
  # Refusing a value-less golden would drop every such case, and every
322
583
  # deliberately-unanswerable case with it.
584
+ # The clauses live in `rubric`, which is the documented field
585
+ # (reference/case-format.md); this fixture said `criteria` and nothing
586
+ # read it, so the drift was invisible until a renderer had to find them.
323
587
  v = self.judge({"status": "verified", "kind": "criteria",
324
- "criteria": ["breaks the total out by region"]})
588
+ "rubric": "breaks the total out by region"})
325
589
  self.assertEqual(v["reason"], "no_saved_verdict")
326
590
 
591
+
327
592
  def test_refusal_is_decided_before_a_saved_verdict_is_reused(self):
328
593
  # `--rebuild` without `--rejudge` returns the saved judge.md. A saved
329
594
  # verdict against an unestablished key is not evidence either.
@@ -341,6 +606,242 @@ class GoldenStatusGate(unittest.TestCase):
341
606
  ("provisional", "invalid", "ambiguous"))
342
607
 
343
608
 
609
+ class GoldenForJudge(unittest.TestCase):
610
+ """What the GOLDEN line of the judge prompt says, per kind.
611
+
612
+ A `criteria` golden rendered as "unanswerable" is a contradiction the judge
613
+ can see, and one called it rather than scoring the case.
614
+ """
615
+
616
+ def test_criteria_never_says_unanswerable(self):
617
+ line = rb.golden_for_judge(
618
+ {"kind": "criteria", "rubric": "names the channels"})
619
+ self.assertNotIn("unanswerable", line)
620
+ self.assertIn("CASE RUBRIC", line)
621
+
622
+ def test_unanswerable_says_so(self):
623
+ self.assertIn("unanswerable",
624
+ rb.golden_for_judge({"kind": "unanswerable"}))
625
+
626
+ def test_a_scalar_renders_its_value(self):
627
+ self.assertEqual(
628
+ rb.golden_for_judge({"kind": "scalar", "value": {"total": 12.5}}),
629
+ '{"total": 12.5}')
630
+
631
+ def test_rows_render_the_list(self):
632
+ self.assertEqual(
633
+ rb.golden_for_judge({"kind": "rows", "value": [{"a": 1}]}),
634
+ '[{"a": 1}]')
635
+
636
+ def test_an_explicit_null_value_is_a_refusal_case(self):
637
+ # How the ecommerce set's four refusal cases say there is no number.
638
+ self.assertIn("unanswerable",
639
+ rb.golden_for_judge({"kind": "scalar", "value": None}))
640
+
641
+ def test_a_golden_with_no_kind_is_unchanged(self):
642
+ # Sets predating the field must read exactly as they did.
643
+ self.assertEqual(rb.golden_for_judge({"value": 7}), "7")
644
+ self.assertIn("unanswerable", rb.golden_for_judge({}))
645
+ self.assertIn("unanswerable", rb.golden_for_judge(None))
646
+
647
+
648
+ class AnUnreadableVerdictIsNotLost(unittest.TestCase):
649
+ """A judge reply that will not parse carries no verdict, so it counts in
650
+ none of match, no_match, near_match or needs_human. It therefore left the
651
+ denominator without appearing anywhere, and the pass rate was printed over
652
+ the remainder. Two cases went that way in one hosted run."""
653
+
654
+ GOOD = '{"why":"y","verdict":"match","confidence":9}'
655
+
656
+ def test_the_retry_predicate_reaches_run_cli(self):
657
+ # `claude` accepted `retry_when` and then passed `no_events` literally,
658
+ # so the judge's predicate never arrived and the judge never retried.
659
+ with mock.patch.object(rb, "run_cli",
660
+ return_value=([], "", "", 1, 0.0)) as run:
661
+ rb.claude("q", str(pathlib.Path(__file__).parent), "sonnet",
662
+ mcp=None, retry_when=rb.judge_unusable)
663
+ self.assertIs(run.call_args.kwargs["retry_when"], rb.judge_unusable)
664
+
665
+ def test_the_answerer_still_defaults_to_no_events(self):
666
+ # Re-rolling a bad ANSWER would put a second sample where the run
667
+ # records one. Only the default was ever meant to be the answerer's.
668
+ with mock.patch.object(rb, "run_cli",
669
+ return_value=([], "", "", 1, 0.0)) as run:
670
+ rb.claude("q", str(pathlib.Path(__file__).parent), "sonnet",
671
+ mcp=None)
672
+ self.assertIs(run.call_args.kwargs["retry_when"], rb.no_events)
673
+
674
+ def test_unparseable_prose_is_retried(self):
675
+ self.assertTrue(rb.judge_unusable([{"type": "assistant"}],
676
+ "The answer looks right to me."))
677
+
678
+ def test_no_text_at_all_is_retried(self):
679
+ self.assertTrue(rb.judge_unusable([], ""))
680
+
681
+ def test_a_good_verdict_is_not_retried(self):
682
+ self.assertFalse(rb.judge_unusable([{"type": "assistant"}], self.GOOD))
683
+
684
+ def test_a_verdict_wrapped_in_prose_is_not_retried(self):
685
+ # `parse_verdict` finds the object inside prose, so this is readable
686
+ # and re-rolling it would just cost a judge call.
687
+ self.assertFalse(rb.judge_unusable(
688
+ [{"type": "assistant"}], f"Here you go:\n{self.GOOD}\nHope that helps"))
689
+
690
+ def summary(self, **kw):
691
+ base = dict(out=pathlib.Path("r"), set_dir=pathlib.Path("s"),
692
+ events_n=0, attempted=10, decided=8, passed=4, near=0,
693
+ human=0, doubted=[], vetoed=[], alt_path=0, unscorable=0,
694
+ retrieval_mode="semantic",
695
+ tally={"semantic": 1, "lexical": 0, "unreported": 0},
696
+ rs={}, answerer_cost=0.0, judge_cost=0.0, publisher="",
697
+ environment="e")
698
+ return "\n".join(rb.summary_lines(**{**base, **kw}))
699
+
700
+ def test_the_summary_names_the_cases_it_could_not_read(self):
701
+ body = self.summary(unparseable=["cq-07", "cq-02"])
702
+ self.assertIn("unreadable 2", body)
703
+ self.assertIn("cq-02, cq-07", body) # sorted, so a diff is stable
704
+
705
+ def test_a_clean_run_still_says_zero(self):
706
+ # Printed even at zero, for the reason `unscorable` is: a reader must
707
+ # not have to know the line exists to notice it is missing.
708
+ body = self.summary()
709
+ self.assertIn("unreadable 0", body)
710
+
711
+ def test_an_aborted_arm_suppresses_the_pass_rate(self):
712
+ # The four-strikes abort fires, `run.json` says `aborted`, and the
713
+ # headline still printed a percentage. Live: `passed 0 of 4 decided
714
+ # (0%)` for an arm whose answerer never started.
715
+ body = self.summary(aborted=True)
716
+ self.assertIn("INCOMPLETE: the arm ABORTED", body)
717
+ self.assertNotIn("(50%)", body)
718
+
719
+ def test_a_truncated_case_suppresses_the_pass_rate(self):
720
+ # The stop rule, made mechanical. A directive did not hold against an
721
+ # arm that had already been paid for: on the run this comes from, the
722
+ # truncation was known before the cases were judged and the rate was
723
+ # quoted anyway.
724
+ body = self.summary(truncated=["cq-01", "cq-05"], max_turns=30)
725
+ self.assertIn("INCOMPLETE: 2 truncated", body)
726
+ self.assertIn("no pass rate until re-run", body)
727
+ self.assertNotIn("(50%)", body) # 4 of 8 would have printed this
728
+
729
+ def test_it_names_the_command_that_finishes_the_arm(self):
730
+ # Four cases, not a whole arm: the answers already paid for are kept
731
+ # and only the truncated ones are re-run, at twice the cap.
732
+ body = self.summary(truncated=["cq-05", "cq-01"], max_turns=30)
733
+ self.assertIn("--only cq-01,cq-05", body)
734
+ self.assertIn("--max-turns 60", body)
735
+
736
+ def test_a_contaminated_case_suppresses_it_too(self):
737
+ body = self.summary(contaminated=["cq-03"])
738
+ self.assertIn("INCOMPLETE: 1 contaminated", body)
739
+ self.assertNotIn("(50%)", body)
740
+
741
+ def test_both_are_named_when_both_happened(self):
742
+ body = self.summary(truncated=["cq-01"], contaminated=["cq-03"])
743
+ self.assertIn("INCOMPLETE: 1 truncated, 1 contaminated", body)
744
+
745
+ def test_contamination_reasons_are_counted(self):
746
+ # One breach across every case is a harness misconfiguration to fix
747
+ # once; one breach on one case is that answerer going somewhere it
748
+ # should not have. The count is what tells them apart.
749
+ body = self.summary(
750
+ contaminated=["cq-03", "cq-04"],
751
+ contamination_reasons={"host tool available to the answerer: Bash": 2})
752
+ self.assertIn("2x host tool available to the answerer: Bash", body)
753
+
754
+ def test_a_complete_run_still_prints_its_rate(self):
755
+ # The suppression is narrow: neither `unscorable` (a dataset state) nor
756
+ # `unreadable` (which has a retry) withholds the number.
757
+ body = self.summary(unscorable=3, unparseable=["cq-02"])
758
+ self.assertIn("(50%)", body)
759
+ self.assertNotIn("INCOMPLETE", body)
760
+
761
+ def test_it_is_not_folded_into_unscorable(self):
762
+ # `unscorable` is a DATASET state no answerer can change. This is the
763
+ # harness failing to read its own judge. Counting them together would
764
+ # send someone to fix answer keys that are fine.
765
+ body = self.summary(unscorable=3, unparseable=["cq-02"])
766
+ self.assertIn("unscorable 3", body)
767
+ self.assertIn("unreadable 1", body)
768
+
769
+
770
+ class HostedProbe(unittest.TestCase):
771
+ """What the hosted reachability probe concludes from its transcript.
772
+
773
+ Three outcomes that are three different problems: no tool granted (a login
774
+ that was never done), a tool that answered with an error (reachable and
775
+ authenticated, wrong call), and a payload.
776
+ """
777
+
778
+ TOOLS = ("mcp__hosted__get_context",)
779
+
780
+ def events(self, *blocks):
781
+ out = []
782
+ for kind, content in blocks:
783
+ out.append({"type": kind, "message": {"content": content}})
784
+ return out
785
+
786
+ def use(self, tid="t1", name="mcp__hosted__get_context"):
787
+ return {"type": "tool_use", "id": tid, "name": name, "input": {}}
788
+
789
+ def res(self, text, tid="t1", err=False):
790
+ return {"type": "tool_result", "tool_use_id": tid,
791
+ "content": text, "is_error": err}
792
+
793
+ def test_no_tool_call_at_all_is_not_granted(self):
794
+ ev = self.events(("assistant", [{"type": "text",
795
+ "text": "I have no such tool."}]))
796
+ self.assertEqual(rb.probe_outcome(ev, self.TOOLS)[0], "not_granted")
797
+
798
+ def test_an_errored_result_is_rejected_not_reached(self):
799
+ # The defect this replaces: a bare tool_use returned True without ever
800
+ # looking at its result, so a refused call passed the gate.
801
+ ev = self.events(("assistant", [self.use()]),
802
+ ("user", [self.res("search_targets is required",
803
+ err=True)]))
804
+ outcome, said, payload = rb.probe_outcome(ev, self.TOOLS)
805
+ self.assertEqual(outcome, "rejected")
806
+ self.assertIn("search_targets is required", said)
807
+ self.assertIsNone(payload)
808
+
809
+ def test_a_payload_is_reached_and_comes_back(self):
810
+ ev = self.events(("assistant", [self.use()]),
811
+ ("user", [self.res(json.dumps(
812
+ {"retrieval": "semantic", "sources": []}))]))
813
+ outcome, _said, payload = rb.probe_outcome(ev, self.TOOLS)
814
+ self.assertEqual(outcome, "reached")
815
+ self.assertEqual(payload["retrieval"], "semantic")
816
+
817
+ def test_a_good_call_outweighs_an_earlier_refused_one(self):
818
+ ev = self.events(
819
+ ("assistant", [self.use("t1")]),
820
+ ("user", [self.res("nope", "t1", err=True)]),
821
+ ("assistant", [self.use("t2")]),
822
+ ("user", [self.res(json.dumps({"retrieval": "lexical"}), "t2")]))
823
+ self.assertEqual(rb.probe_outcome(ev, self.TOOLS)[0], "reached")
824
+
825
+ def test_a_call_with_no_result_is_not_a_missing_login(self):
826
+ ev = self.events(("assistant", [self.use()]))
827
+ self.assertEqual(rb.probe_outcome(ev, self.TOOLS)[0], "rejected")
828
+
829
+ def test_a_call_to_an_unallowed_tool_is_ignored(self):
830
+ ev = self.events(("assistant", [self.use(name="Bash")]),
831
+ ("user", [self.res("ok")]))
832
+ self.assertEqual(rb.probe_outcome(ev, self.TOOLS)[0], "not_granted")
833
+
834
+ def test_the_probe_asks_for_the_arguments_the_tool_requires(self):
835
+ # A get_context with no arguments is a validation error on any server
836
+ # that enforces its required parameters, which is what the probe used
837
+ # to ask for.
838
+ a = argparse.Namespace(environment="examples", package="storefront")
839
+ args = rb.probe_arguments(a)
840
+ self.assertTrue(args["search_targets"])
841
+ self.assertEqual(args["scopes"],
842
+ [{"environment": "examples", "package": "storefront"}])
843
+
844
+
344
845
  class UnscorablePreflight(unittest.TestCase):
345
846
  """A set of bare questions is a supported set, not a set to refuse.
346
847
 
@@ -532,6 +1033,123 @@ class PlatformMcpUrl(unittest.TestCase):
532
1033
 
533
1034
 
534
1035
 
1036
+ class RetrievalPrecisionIsReportedHonestly(unittest.TestCase):
1037
+ """Precision was computed and thrown away; the run printed recall alone.
1038
+
1039
+ It is worth printing, but it reads everything outside `acceptable` as
1040
+ noise, so a set that never authored one scores every legitimate extra as a
1041
+ miss. Measured on a real run: 3% to 11%, on a set whose eight cases all had
1042
+ an empty `acceptable`. The caveat ships with the number.
1043
+ """
1044
+
1045
+ def rs(self, **over):
1046
+ base = {"retrieval_scored": 8, "mean_recall": 0.938,
1047
+ "complete_retrievals": 7, "mean_precision": 0.073,
1048
+ "mean_returned": 25.6, "mean_required": 1.8,
1049
+ "cases_with_acceptable": 0, "failures_by_where_to_fix": {}}
1050
+ base.update(over)
1051
+ return base
1052
+
1053
+ def test_precision_prints_with_its_caveat_when_nothing_authored_acceptable(self):
1054
+ text = "\n".join(self.lines(rs=self.rs()))
1055
+ self.assertIn("entity precision mean 7.3%", text)
1056
+ self.assertIn("no case authored `acceptable`", text)
1057
+ self.assertIn("breadth, not as a verdict", text)
1058
+
1059
+ def test_breadth_does_not_depend_on_authoring(self):
1060
+ text = "\n".join(self.lines(rs=self.rs()))
1061
+ self.assertIn("26 returned per attempt for 2 the answer named", text)
1062
+
1063
+ def test_a_fully_authored_set_gets_the_number_without_the_caveat(self):
1064
+ text = "\n".join(self.lines(rs=self.rs(cases_with_acceptable=8)))
1065
+ self.assertIn("entity precision mean 7.3%", text)
1066
+ self.assertNotIn("no case authored", text)
1067
+ self.assertNotIn("only 8 of 8", text)
1068
+
1069
+ def test_a_partly_authored_set_says_it_is_uneven(self):
1070
+ text = "\n".join(self.lines(rs=self.rs(cases_with_acceptable=3)))
1071
+ self.assertIn("only 3 of 8", text)
1072
+
1073
+ def lines(self, **over):
1074
+ return RunSummary.lines(self, **over)
1075
+
1076
+
1077
+ class SkillsActuallyOpened(unittest.TestCase):
1078
+ """A run names the skills it granted; only the ones opened shaped anything.
1079
+
1080
+ Measured on a real run: every attempt invoked zero of its 11 skills, so an
1081
+ edit to one could not have changed the answers and nothing in the report
1082
+ said so.
1083
+ """
1084
+
1085
+ def test_zero_says_the_run_does_not_measure_the_skills(self):
1086
+ lines = rb.skill_lines({"attempts": 8, "with_skill": 0, "skills": []})
1087
+ text = "\n".join(lines)
1088
+ self.assertIn("0 of 8", text)
1089
+ self.assertIn("cannot be credited or blamed", text)
1090
+
1091
+ def test_some_usage_names_the_skills_and_does_not_warn(self):
1092
+ lines = rb.skill_lines({"attempts": 8, "with_skill": 3,
1093
+ "skills": ["malloy-phrase-detection"]})
1094
+ text = "\n".join(lines)
1095
+ self.assertIn("3 of 8", text)
1096
+ self.assertIn("malloy-phrase-detection", text)
1097
+ self.assertNotIn("cannot be credited", text)
1098
+
1099
+ def test_no_attempts_prints_nothing(self):
1100
+ self.assertEqual(rb.skill_lines({"attempts": 0, "with_skill": 0}), [])
1101
+ self.assertEqual(rb.skill_lines(None), [])
1102
+
1103
+
1104
+ class NoLocalShadowsAnImportedModule(unittest.TestCase):
1105
+ """A local named after an imported module breaks every call to that module
1106
+ in the same function, and only at runtime.
1107
+
1108
+ `ledger = verify_definitions.load_ledger(...)` in main() made `ledger` a
1109
+ local for the whole function, so `ledger.run_config(...)` 260 lines earlier
1110
+ raised UnboundLocalError on EVERY run. The unit tests all call helpers, so
1111
+ nothing noticed until a real arm was run end to end. This is the cheap
1112
+ structural guard that would have.
1113
+ """
1114
+
1115
+ def test_no_function_rebinds_a_module_this_file_imports(self):
1116
+ import ast
1117
+ src = pathlib.Path(rb.__file__).read_text()
1118
+ tree = ast.parse(src)
1119
+ modules = {n.names[0].asname or n.names[0].name.split(".")[0]
1120
+ for n in ast.walk(tree) if isinstance(n, ast.Import)}
1121
+ bad = []
1122
+ for fn in ast.walk(tree):
1123
+ if not isinstance(fn, (ast.FunctionDef, ast.AsyncFunctionDef)):
1124
+ continue
1125
+ for node in ast.walk(fn):
1126
+ if isinstance(node, ast.Name) and isinstance(node.ctx, ast.Store) \
1127
+ and node.id in modules:
1128
+ bad.append(f"{fn.name}() rebinds the module name "
1129
+ f"{node.id!r} at line {node.lineno}")
1130
+ self.assertEqual(bad, [], "; ".join(bad))
1131
+
1132
+
1133
+ class UsageFields(unittest.TestCase):
1134
+ """The ledger must be able to reprice a run from its own token columns."""
1135
+
1136
+ def test_all_four_token_counts_are_captured(self):
1137
+ u = {"input_tokens": 224, "output_tokens": 31507,
1138
+ "cache_read_input_tokens": 2435273,
1139
+ "cache_creation_input_tokens": 900000}
1140
+ self.assertEqual(rb.usage_fields(u), {
1141
+ "input_tokens": 224, "output_tokens": 31507,
1142
+ "cache_read_tokens": 2435273, "cache_write_tokens": 900000})
1143
+
1144
+ def test_cache_writes_were_the_missing_column(self):
1145
+ # The one the ledger never held. On one analysed run it was 44% of the
1146
+ # agent's cost; cost_usd carried it and the breakdown could not.
1147
+ self.assertIn("cache_write_tokens", rb.usage_fields({}))
1148
+
1149
+ def test_absent_usage_is_all_null_not_a_crash(self):
1150
+ self.assertEqual(set(rb.usage_fields(None).values()), {None})
1151
+
1152
+
535
1153
  class RunSummary(unittest.TestCase):
536
1154
  """The end-of-run report is three layers, and the order is the point.
537
1155
 
@@ -559,6 +1177,53 @@ class RunSummary(unittest.TestCase):
559
1177
  def index_of(self, lines, needle):
560
1178
  return next(i for i, l in enumerate(lines) if needle in l)
561
1179
 
1180
+ def test_a_consumed_coverage_report_is_named_not_pointed_at(self):
1181
+ # With a report given, "not measured here" is false and must not print.
1182
+ lines = self.lines(coverage_report={
1183
+ "path": "cov.json", "version": "0.0.58", "agentModel": "sonnet",
1184
+ "decided": 45, "cases": 49})
1185
+ text = "\n".join(lines)
1186
+ self.assertIn("cov.json", text)
1187
+ self.assertIn("45 of 49", text)
1188
+ self.assertNotIn("not measured here", text)
1189
+ # 4 undecided: the report did not settle every case, and the reader
1190
+ # has to know which rows fell back to what.
1191
+ self.assertIn("undecided cases fell back", text)
1192
+
1193
+ def test_a_report_that_decided_everything_carries_no_fallback_warning(self):
1194
+ lines = self.lines(coverage_report={
1195
+ "path": "cov.json", "version": "1", "agentModel": "m",
1196
+ "decided": 49, "cases": 49})
1197
+ self.assertNotIn("undecided cases fell back", "\n".join(lines))
1198
+
1199
+ def test_no_report_keeps_the_pointer(self):
1200
+ self.assertIn("not measured here", "\n".join(self.lines()))
1201
+
1202
+ def test_the_cascade_reads_as_a_funnel_with_owners(self):
1203
+ lines = self.lines(cascade={
1204
+ "total": 49, "not covered": 6, "unmeasured": 2,
1205
+ "no entities named": 0, "not retrieved": 5,
1206
+ "delivered, wrong": 6, "delivered, right": 30, "not scored": 0})
1207
+ text = "\n".join(lines)
1208
+ self.assertIn("cascade 49 cases", text)
1209
+ self.assertIn("covered? 41 yes, 6 no (model gap), 2 unmeasured", text)
1210
+ # The rung must not assert the docs: a miss is the docs OR the search
1211
+ # wording, and only diagnose separates them. This assertion is here
1212
+ # because the label was renamed in score_retrieval and the display line
1213
+ # in this file was missed, so the two disagreed in a shipped commit.
1214
+ self.assertIn("retrieved? 36 yes, 5 no (the entity exists and did "
1215
+ "not come back", text)
1216
+ self.assertNotIn("(documentation", text)
1217
+ # And a delivered-but-wrong answer names no owner until diagnose runs.
1218
+ self.assertIn("correct? 30 yes, 6 no (delivered, wrong", text)
1219
+ self.assertIn("diagnose decides", text)
1220
+ # It heads the COVERAGE & RETRIEVAL layer, above the retrieval-mode line.
1221
+ self.assertLess(self.index_of(lines, "cascade"),
1222
+ self.index_of(lines, " retrieval "))
1223
+
1224
+ def test_no_cascade_prints_nothing(self):
1225
+ self.assertNotIn("cascade", "\n".join(self.lines()))
1226
+
562
1227
  def test_the_three_layers_appear_in_order(self):
563
1228
  lines = self.lines()
564
1229
  self.assertLess(self.index_of(lines, "RESULTS"),
@@ -729,5 +1394,138 @@ class RebuildCaseList(unittest.TestCase):
729
1394
  finally:
730
1395
  shutil.rmtree(tmp, ignore_errors=True)
731
1396
 
1397
+
1398
+ class ContaminatedAttemptsLeaveTheAggregates(unittest.TestCase):
1399
+ """A flagged attempt must not reach the printed score.
1400
+
1401
+ The ledger has nulled a contaminated verdict since 2026-09-01, but the
1402
+ nulling landed on the copy bound for events.jsonl while the summary read
1403
+ the judge's original. A 33-case run in which EVERY attempt was flagged
1404
+ printed `12 of 21 decided (57%)` over a ledger whose every score event said
1405
+ `verdict: null`. That is the contamination check working and the line a
1406
+ human reads disagreeing with it.
1407
+ """
1408
+
1409
+ def test_a_flagged_attempt_is_not_counted_as_a_pass(self):
1410
+ verdicts = {"q1": {"verdict": "match"}, "q2": {"verdict": "match"}}
1411
+ attempts = {"q1": {"breaches": ["host tool available: TaskCreate"]},
1412
+ "q2": {"breaches": []}}
1413
+ for qid, v in verdicts.items():
1414
+ if attempts[qid].get("breaches"):
1415
+ v["verdict"] = None
1416
+ decided = [v for v in verdicts.values()
1417
+ if v.get("verdict") in ("match", "no_match")]
1418
+ self.assertEqual(len(decided), 1, "the flagged attempt still counted")
1419
+ self.assertIsNone(verdicts["q1"]["verdict"])
1420
+ self.assertEqual(verdicts["q2"]["verdict"], "match")
1421
+
1422
+ def test_a_fully_contaminated_run_decides_nothing(self):
1423
+ verdicts = {f"q{i}": {"verdict": "match"} for i in range(21)}
1424
+ attempts = {q: {"breaches": ["host tool available: TaskCreate"]}
1425
+ for q in verdicts}
1426
+ for qid, v in verdicts.items():
1427
+ if attempts[qid].get("breaches"):
1428
+ v["verdict"] = None
1429
+ decided = [v for v in verdicts.values()
1430
+ if v.get("verdict") in ("match", "no_match")]
1431
+ self.assertEqual(len(decided), 0,
1432
+ "a run with no clean attempt reported a score")
1433
+
1434
+ class GitSha(unittest.TestCase):
1435
+ """The pin has to mark a dirty model repo, and it did not.
1436
+
1437
+ The path is used BOTH as git's working directory (`-C`) and as its
1438
+ pathspec. A relative one means different things in those two positions:
1439
+ git runs IN the directory and then resolves the pathspec against it, so
1440
+ `--model-repo sub` looks for `sub/sub`, matches nothing, and every tree
1441
+ reads clean. The sibling in `ledger.skills_git_sha` was fixed for exactly
1442
+ this; `git_sha` was not, and had no test at all.
1443
+ """
1444
+
1445
+ def setUp(self):
1446
+ self.tmp = tempfile.mkdtemp()
1447
+ self.repo = pathlib.Path(self.tmp) / "repo"
1448
+ (self.repo / "sub").mkdir(parents=True)
1449
+ (self.repo / "sub" / "model.malloy").write_text("source: a is x\n")
1450
+ run = lambda *a: subprocess.run(a, cwd=self.repo, capture_output=True)
1451
+ run("git", "init", "-q")
1452
+ run("git", "config", "user.email", "t@t")
1453
+ run("git", "config", "user.name", "t")
1454
+ run("git", "add", "-A")
1455
+ run("git", "commit", "-qm", "init")
1456
+ self.cwd = os.getcwd()
1457
+ os.chdir(self.repo)
1458
+
1459
+ def tearDown(self):
1460
+ os.chdir(self.cwd)
1461
+ shutil.rmtree(self.tmp, ignore_errors=True)
1462
+
1463
+ def dirty(self):
1464
+ (self.repo / "sub" / "model.malloy").write_text("source: a is CHANGED\n")
1465
+
1466
+ def test_a_clean_tree_has_no_marker(self):
1467
+ self.assertFalse(
1468
+ rb.git_sha(pathlib.Path("sub"), scope=pathlib.Path("sub"))
1469
+ .endswith("-dirty"))
1470
+
1471
+ def test_a_relative_path_still_marks_dirt(self):
1472
+ self.dirty()
1473
+ self.assertTrue(
1474
+ rb.git_sha(pathlib.Path("sub"), scope=pathlib.Path("sub"))
1475
+ .endswith("-dirty"),
1476
+ "a relative --model-repo silently reported a dirty model as clean")
1477
+
1478
+ def test_an_absolute_path_agrees_with_the_relative_one(self):
1479
+ self.dirty()
1480
+ rel = rb.git_sha(pathlib.Path("sub"), scope=pathlib.Path("sub"))
1481
+ abs_ = rb.git_sha(self.repo / "sub", scope=self.repo / "sub")
1482
+ self.assertEqual(rel, abs_, "the pin depends on how it was spelled")
1483
+
1484
+ def test_the_scope_still_narrows(self):
1485
+ # Dirt OUTSIDE the scope is not the model's, which is the whole reason
1486
+ # the scope exists. This must not regress into "any dirt anywhere".
1487
+ (self.repo / "scratch.txt").write_text("unrelated")
1488
+ self.assertFalse(
1489
+ rb.git_sha(pathlib.Path("sub"), scope=pathlib.Path("sub"))
1490
+ .endswith("-dirty"))
1491
+
1492
+ def test_not_a_repo_is_none(self):
1493
+ self.assertIsNone(rb.git_sha(pathlib.Path(self.tmp)))
1494
+
1495
+
1496
+ class GoldenCheckScope(unittest.TestCase):
1497
+ """The pre-arm audit covers the cases the arm RUNS, not the whole file.
1498
+
1499
+ A set is built incrementally, so one un-derivable golden elsewhere in
1500
+ cases.jsonl blocked an arm over a subset that did not include it. The only
1501
+ way past was --skip-golden-check, which switches the audit off for the
1502
+ cases that would have passed and stamps "skipped" into run.json.
1503
+ """
1504
+
1505
+ SRC = (pathlib.Path(__file__).resolve().parent / "run_baseline.py").read_text()
1506
+
1507
+ def test_the_call_passes_the_selected_qids(self):
1508
+ # A source pin, like test_kinds_by_target_matches_the_server: the call
1509
+ # sits inside main() between a server probe and an arm, so the cheap
1510
+ # way to stop it silently losing the argument again is to read it.
1511
+ call = self.SRC.split("verify_goldens.verify(")[1][:400]
1512
+ self.assertIn("qids=checked_qids", call,
1513
+ "the golden check audits the whole set again")
1514
+
1515
+ def test_the_qids_come_from_the_narrowed_case_list(self):
1516
+ self.assertIn('checked_qids = {c["qid"] for c in cases}', self.SRC)
1517
+
1518
+ def test_verify_actually_filters_on_qids(self):
1519
+ # The other half: the argument has to mean something.
1520
+ import inspect
1521
+ import sys as _sys
1522
+ _sys.path.insert(0, str(pathlib.Path(__file__).resolve().parent.parent.parent
1523
+ / "eval-answer" / "scripts"))
1524
+ import verify_goldens
1525
+ src = inspect.getsource(verify_goldens.verify)
1526
+ self.assertIn("qids", src)
1527
+ self.assertIn('c["qid"] in qids', src)
1528
+
1529
+
732
1530
  if __name__ == "__main__":
733
1531
  unittest.main()