codeer-cli 0.1.7__tar.gz → 0.1.8__tar.gz
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.
- {codeer_cli-0.1.7 → codeer_cli-0.1.8}/API_REFERENCE.md +7 -6
- {codeer_cli-0.1.7 → codeer_cli-0.1.8}/PKG-INFO +1 -1
- {codeer_cli-0.1.7 → codeer_cli-0.1.8}/pyproject.toml +1 -1
- {codeer_cli-0.1.7 → codeer_cli-0.1.8}/src/codeer_cli/commands/_util.py +3 -1
- {codeer_cli-0.1.7 → codeer_cli-0.1.8}/src/codeer_cli/commands/eval_cmd.py +139 -30
- {codeer_cli-0.1.7 → codeer_cli-0.1.8}/src/codeer_cli/eval_.py +19 -1
- {codeer_cli-0.1.7 → codeer_cli-0.1.8}/tests/test_eval_pairs.py +94 -1
- codeer_cli-0.1.8/tests/test_util.py +22 -0
- {codeer_cli-0.1.7 → codeer_cli-0.1.8}/uv.lock +1 -1
- {codeer_cli-0.1.7 → codeer_cli-0.1.8}/.gitignore +0 -0
- {codeer_cli-0.1.7 → codeer_cli-0.1.8}/README.md +0 -0
- {codeer_cli-0.1.7 → codeer_cli-0.1.8}/src/codeer_cli/__init__.py +0 -0
- {codeer_cli-0.1.7 → codeer_cli-0.1.8}/src/codeer_cli/_validate.py +0 -0
- {codeer_cli-0.1.7 → codeer_cli-0.1.8}/src/codeer_cli/agents.py +0 -0
- {codeer_cli-0.1.7 → codeer_cli-0.1.8}/src/codeer_cli/chats.py +0 -0
- {codeer_cli-0.1.7 → codeer_cli-0.1.8}/src/codeer_cli/cli.py +0 -0
- {codeer_cli-0.1.7 → codeer_cli-0.1.8}/src/codeer_cli/client.py +0 -0
- {codeer_cli-0.1.7 → codeer_cli-0.1.8}/src/codeer_cli/commands/__init__.py +0 -0
- {codeer_cli-0.1.7 → codeer_cli-0.1.8}/src/codeer_cli/commands/agent.py +0 -0
- {codeer_cli-0.1.7 → codeer_cli-0.1.8}/src/codeer_cli/commands/check.py +0 -0
- {codeer_cli-0.1.7 → codeer_cli-0.1.8}/src/codeer_cli/commands/history.py +0 -0
- {codeer_cli-0.1.7 → codeer_cli-0.1.8}/src/codeer_cli/commands/kb.py +0 -0
- {codeer_cli-0.1.7 → codeer_cli-0.1.8}/src/codeer_cli/commands/profile.py +0 -0
- {codeer_cli-0.1.7 → codeer_cli-0.1.8}/src/codeer_cli/constants.py +0 -0
- {codeer_cli-0.1.7 → codeer_cli-0.1.8}/src/codeer_cli/histories.py +0 -0
- {codeer_cli-0.1.7 → codeer_cli-0.1.8}/src/codeer_cli/kb.py +0 -0
- {codeer_cli-0.1.7 → codeer_cli-0.1.8}/src/codeer_cli/parse.py +0 -0
- {codeer_cli-0.1.7 → codeer_cli-0.1.8}/tests/test_eval_labels.py +0 -0
- {codeer_cli-0.1.7 → codeer_cli-0.1.8}/tests/test_kb_nodes.py +0 -0
- {codeer_cli-0.1.7 → codeer_cli-0.1.8}/tests/test_kb_ranges.py +0 -0
|
@@ -222,17 +222,18 @@ normalized `tool_calls`, `tool_calls_summary`, and `tool_total_duration_ms`;
|
|
|
222
222
|
in `eval_table_full.json`. Per-tool time is computed from `start_at/end_at`.
|
|
223
223
|
|
|
224
224
|
**`evaluator_id` is singular — one call returns results for one evaluator
|
|
225
|
-
only.**
|
|
226
|
-
|
|
227
|
-
|
|
228
|
-
|
|
229
|
-
prefer `eval_mod.get_case_evaluator_infos(case_ids=[...])` as the source of
|
|
230
|
-
truth for which pairs should run.
|
|
225
|
+
only.** To see the full picture for a case, call results once per evaluator
|
|
226
|
+
you care about. `codeer eval run` and `codeer eval rubrics` handle this
|
|
227
|
+
automatically; when no evaluator is supplied, the public CLI uses external
|
|
228
|
+
rubric batches to find case/evaluator pairs with configured rubrics.
|
|
231
229
|
|
|
232
230
|
Regression workflow (apply prompt change → re-run all assigned pairs → spot
|
|
233
231
|
side effects): `codeer eval run --agent <agent_id>` runs the latest
|
|
234
232
|
AgentHistory by default. For the common "many cases, one tester" flow, use
|
|
235
233
|
`codeer eval run --agent <agent_id> --cases <ids> --evaluator <evaluator_id>`.
|
|
234
|
+
When triggering runs, call `POST /external/eval/runs` once per evaluator with
|
|
235
|
+
that evaluator's case IDs. Do not call legacy internal trigger endpoints from
|
|
236
|
+
the public CLI.
|
|
236
237
|
|
|
237
238
|
## Stage 7 — Publish
|
|
238
239
|
|
|
@@ -62,5 +62,7 @@ def print_json(value: Any) -> None:
|
|
|
62
62
|
def write_json(path: str | None, value: Any) -> None:
|
|
63
63
|
if not path:
|
|
64
64
|
return
|
|
65
|
-
Path(path)
|
|
65
|
+
out = Path(path)
|
|
66
|
+
out.parent.mkdir(parents=True, exist_ok=True)
|
|
67
|
+
out.write_text(json.dumps(value, ensure_ascii=False, indent=2, default=str) + "\n")
|
|
66
68
|
log(f"wrote full detail to {path}")
|
|
@@ -653,6 +653,64 @@ def _group_case_ids_by_evaluator(pairs: list[dict[str, str]]) -> dict[str, list[
|
|
|
653
653
|
return dict(grouped)
|
|
654
654
|
|
|
655
655
|
|
|
656
|
+
def _pairs_from_rubric_batches(
|
|
657
|
+
client,
|
|
658
|
+
*,
|
|
659
|
+
case_ids: list[str],
|
|
660
|
+
evaluator_ids: list[str],
|
|
661
|
+
) -> list[dict[str, str]]:
|
|
662
|
+
pairs: list[dict[str, str]] = []
|
|
663
|
+
for evaluator_id in evaluator_ids:
|
|
664
|
+
for row in eval_mod.get_rubrics_batch(client, case_ids=case_ids, evaluator_id=evaluator_id):
|
|
665
|
+
if row.get("rubric"):
|
|
666
|
+
case_id = row.get("case_id") or row.get("evaluation_case_id")
|
|
667
|
+
if case_id:
|
|
668
|
+
pairs.append({"case_id": str(case_id), "evaluator_id": evaluator_id})
|
|
669
|
+
return pairs
|
|
670
|
+
|
|
671
|
+
|
|
672
|
+
def _pair_key(pair: dict[str, str]) -> tuple[str, str]:
|
|
673
|
+
return pair["case_id"], pair["evaluator_id"]
|
|
674
|
+
|
|
675
|
+
|
|
676
|
+
def _skipped_pairs_from_trigger_response(response: Any) -> list[dict[str, str]]:
|
|
677
|
+
if not isinstance(response, dict):
|
|
678
|
+
return []
|
|
679
|
+
payload = response.get("data") if isinstance(response.get("data"), dict) else response
|
|
680
|
+
skipped = payload.get("skipped_pairs") if isinstance(payload, dict) else None
|
|
681
|
+
if not isinstance(skipped, list):
|
|
682
|
+
return []
|
|
683
|
+
|
|
684
|
+
out: list[dict[str, str]] = []
|
|
685
|
+
for row in skipped:
|
|
686
|
+
if not isinstance(row, dict):
|
|
687
|
+
continue
|
|
688
|
+
case_id = row.get("case_id")
|
|
689
|
+
evaluator_id = row.get("evaluator_id")
|
|
690
|
+
if not case_id or not evaluator_id:
|
|
691
|
+
continue
|
|
692
|
+
out.append({
|
|
693
|
+
"case_id": str(case_id),
|
|
694
|
+
"evaluator_id": str(evaluator_id),
|
|
695
|
+
"reason": str(row.get("reason") or "skipped"),
|
|
696
|
+
})
|
|
697
|
+
return out
|
|
698
|
+
|
|
699
|
+
|
|
700
|
+
def _remove_non_runnable_skipped_pairs(
|
|
701
|
+
pairs: list[dict[str, str]],
|
|
702
|
+
skipped_pairs: list[dict[str, str]],
|
|
703
|
+
) -> list[dict[str, str]]:
|
|
704
|
+
non_runnable = {
|
|
705
|
+
_pair_key(pair)
|
|
706
|
+
for pair in skipped_pairs
|
|
707
|
+
if pair.get("reason") == "not_assigned"
|
|
708
|
+
}
|
|
709
|
+
if not non_runnable:
|
|
710
|
+
return pairs
|
|
711
|
+
return [pair for pair in pairs if _pair_key(pair) not in non_runnable]
|
|
712
|
+
|
|
713
|
+
|
|
656
714
|
def run_run(args, client) -> int:
|
|
657
715
|
workspace_id, _ = client.resolve_scope()
|
|
658
716
|
if args.latest or not args.history:
|
|
@@ -682,18 +740,22 @@ def run_run(args, client) -> int:
|
|
|
682
740
|
evaluator_ids = [args.evaluator] if args.evaluator else (_ids(args.evaluators) or [])
|
|
683
741
|
requested_evaluator_ids = evaluator_ids or None
|
|
684
742
|
|
|
685
|
-
|
|
686
|
-
|
|
687
|
-
|
|
688
|
-
|
|
689
|
-
|
|
690
|
-
|
|
691
|
-
|
|
743
|
+
skipped_unassigned: list[dict[str, str]] = []
|
|
744
|
+
if requested_evaluator_ids:
|
|
745
|
+
pairs = [
|
|
746
|
+
{"case_id": case_id, "evaluator_id": evaluator_id}
|
|
747
|
+
for evaluator_id in requested_evaluator_ids
|
|
748
|
+
for case_id in case_ids
|
|
749
|
+
]
|
|
750
|
+
else:
|
|
751
|
+
evaluator_ids = [e["id"] for e in eval_mod.list_evaluators(client, workspace_id)]
|
|
752
|
+
pairs = _pairs_from_rubric_batches(
|
|
753
|
+
client,
|
|
754
|
+
case_ids=case_ids,
|
|
755
|
+
evaluator_ids=evaluator_ids,
|
|
756
|
+
)
|
|
692
757
|
if not pairs:
|
|
693
|
-
|
|
694
|
-
log("error: none of the requested case/evaluator pairs are assigned")
|
|
695
|
-
else:
|
|
696
|
-
log("error: no assigned case/evaluator pairs to run")
|
|
758
|
+
log("error: no case/evaluator pairs to run")
|
|
697
759
|
print_json({
|
|
698
760
|
"agent_id": args.agent,
|
|
699
761
|
"history_id": args.history,
|
|
@@ -710,14 +772,47 @@ def run_run(args, client) -> int:
|
|
|
710
772
|
case_label_by_id = {c["id"]: truncate(c.get("input") or "", 60) for c in case_objs}
|
|
711
773
|
evaluator_name_by_id = {e["id"]: e.get("name", e["id"]) for e in evaluators}
|
|
712
774
|
|
|
775
|
+
requested_pairs = list(pairs)
|
|
776
|
+
requested_pair_count = len(requested_pairs)
|
|
777
|
+
log(f"triggering: {requested_pair_count} case/evaluator pairs on history {args.history}")
|
|
778
|
+
trigger_response: list[dict[str, Any]] = []
|
|
779
|
+
skipped_pairs: list[dict[str, str]] = []
|
|
780
|
+
for ev_id, ev_case_ids in _group_case_ids_by_evaluator(pairs).items():
|
|
781
|
+
response = eval_mod.trigger(
|
|
782
|
+
client,
|
|
783
|
+
case_ids=ev_case_ids,
|
|
784
|
+
evaluator_ids=[ev_id],
|
|
785
|
+
agent_history_id=args.history,
|
|
786
|
+
)
|
|
787
|
+
response_skipped = _skipped_pairs_from_trigger_response(response)
|
|
788
|
+
skipped_pairs.extend(response_skipped)
|
|
789
|
+
trigger_response.append({
|
|
790
|
+
"evaluator_id": ev_id,
|
|
791
|
+
"case_ids": ev_case_ids,
|
|
792
|
+
"response": response,
|
|
793
|
+
"skipped_pairs": response_skipped,
|
|
794
|
+
})
|
|
795
|
+
|
|
796
|
+
pairs = _remove_non_runnable_skipped_pairs(pairs, skipped_pairs)
|
|
797
|
+
skipped_unassigned = [pair for pair in skipped_pairs if pair.get("reason") == "not_assigned"]
|
|
713
798
|
if skipped_unassigned:
|
|
714
|
-
log(f"skipping {len(skipped_unassigned)}
|
|
715
|
-
|
|
716
|
-
|
|
717
|
-
|
|
718
|
-
|
|
719
|
-
|
|
720
|
-
|
|
799
|
+
log(f"skipping {len(skipped_unassigned)} not-assigned pairs from polling")
|
|
800
|
+
if not pairs:
|
|
801
|
+
log("error: no runnable case/evaluator pairs after trigger response")
|
|
802
|
+
print_json({
|
|
803
|
+
"agent_id": args.agent,
|
|
804
|
+
"history_id": args.history,
|
|
805
|
+
"requested_case_count": len(case_ids),
|
|
806
|
+
"requested_evaluator_count": len(requested_evaluator_ids or evaluator_ids),
|
|
807
|
+
"requested_pair_count": requested_pair_count,
|
|
808
|
+
"triggered_pair_count": 0,
|
|
809
|
+
"skipped_pair_count": len(skipped_pairs),
|
|
810
|
+
"skipped_unassigned_count": len(skipped_unassigned),
|
|
811
|
+
"trigger_response": trigger_response,
|
|
812
|
+
"skipped_pairs": skipped_pairs,
|
|
813
|
+
"skipped_unassigned": skipped_unassigned,
|
|
814
|
+
})
|
|
815
|
+
return 2
|
|
721
816
|
|
|
722
817
|
deadline = time.time() + args.poll_timeout
|
|
723
818
|
results_by_eval: dict[str, list[dict]] = {}
|
|
@@ -825,22 +920,27 @@ def run_run(args, client) -> int:
|
|
|
825
920
|
"history_id": args.history,
|
|
826
921
|
"requested_case_count": len(case_ids),
|
|
827
922
|
"requested_evaluator_count": len(requested_evaluator_ids or evaluator_ids),
|
|
923
|
+
"requested_pair_count": requested_pair_count,
|
|
828
924
|
"triggered_pair_count": len(pairs),
|
|
829
925
|
"scored_pair_count": len(scored_pair_keys),
|
|
926
|
+
"skipped_pair_count": len(skipped_pairs),
|
|
830
927
|
"skipped_unassigned_count": len(skipped_unassigned),
|
|
831
928
|
"all_perfect": all_perfect,
|
|
832
929
|
"result_count": len(result_summaries),
|
|
833
930
|
"non_perfect_count": len(non_perfect),
|
|
834
931
|
"wrote_full_detail": bool(args.out),
|
|
835
932
|
"trigger_response": trigger_response,
|
|
933
|
+
"skipped_pairs": skipped_pairs,
|
|
836
934
|
"skipped_unassigned": skipped_unassigned,
|
|
837
935
|
"results": result_summaries,
|
|
838
936
|
}
|
|
839
937
|
full_out = {
|
|
840
938
|
"agent_id": args.agent,
|
|
841
939
|
"history_id": args.history,
|
|
940
|
+
"requested_pairs": requested_pairs,
|
|
842
941
|
"triggered_pairs": pairs,
|
|
843
942
|
"trigger_response": trigger_response,
|
|
943
|
+
"skipped_pairs": skipped_pairs,
|
|
844
944
|
"skipped_unassigned": skipped_unassigned,
|
|
845
945
|
"all_perfect": all_perfect,
|
|
846
946
|
"results": flat,
|
|
@@ -1466,35 +1566,44 @@ def run_rubrics(args, client) -> int:
|
|
|
1466
1566
|
log("error: no cases for this agent")
|
|
1467
1567
|
return 2
|
|
1468
1568
|
|
|
1469
|
-
assignment_rows = eval_mod.get_case_evaluator_infos(client, case_ids=case_ids)
|
|
1470
|
-
assigned_by_case = _assigned_evaluators_by_case(assignment_rows)
|
|
1471
|
-
|
|
1472
1569
|
if args.evaluators:
|
|
1473
1570
|
evaluator_ids = _ids(args.evaluators) or []
|
|
1474
1571
|
evaluators = [eval_mod.get_evaluator(client, eid) for eid in evaluator_ids]
|
|
1475
|
-
|
|
1572
|
+
else:
|
|
1476
1573
|
evaluators = eval_mod.list_evaluators(client, workspace_id)
|
|
1477
1574
|
evaluator_ids = [e["id"] for e in evaluators]
|
|
1575
|
+
|
|
1576
|
+
rubrics = eval_mod.get_case_rubrics(
|
|
1577
|
+
client, agent_id=args.agent, workspace_id=workspace_id,
|
|
1578
|
+
evaluator_ids=evaluator_ids, case_ids=case_ids,
|
|
1579
|
+
)
|
|
1580
|
+
assigned_by_case = {
|
|
1581
|
+
cid: {
|
|
1582
|
+
ev_id: {"evaluator_id": ev_id, "rubric": rubric_text}
|
|
1583
|
+
for ev_id, rubric_text in (rubrics.get(cid) or {}).items()
|
|
1584
|
+
if rubric_text
|
|
1585
|
+
}
|
|
1586
|
+
for cid in case_ids
|
|
1587
|
+
}
|
|
1588
|
+
|
|
1589
|
+
if args.evaluators or args.all_pairs:
|
|
1590
|
+
pass
|
|
1478
1591
|
else:
|
|
1479
1592
|
evaluator_ids = _dedupe_preserve_order([
|
|
1480
1593
|
evaluator_id
|
|
1481
1594
|
for case_id in case_ids
|
|
1482
1595
|
for evaluator_id in assigned_by_case.get(case_id, {})
|
|
1483
1596
|
])
|
|
1484
|
-
|
|
1597
|
+
evaluator_id_set = set(evaluator_ids)
|
|
1598
|
+
evaluators = [e for e in evaluators if e["id"] in evaluator_id_set]
|
|
1485
1599
|
evaluator_name = {e["id"]: e.get("name", e["id"]) for e in evaluators}
|
|
1486
1600
|
if not evaluator_ids:
|
|
1487
|
-
log("error: no
|
|
1601
|
+
log("error: no evaluators with configured rubrics for these cases")
|
|
1488
1602
|
return 2
|
|
1489
1603
|
|
|
1490
|
-
mode = "all requested pairs" if args.evaluators or args.all_pairs else "
|
|
1604
|
+
mode = "all requested pairs" if args.evaluators or args.all_pairs else "pairs with configured rubrics"
|
|
1491
1605
|
log(f"reading {mode}: {len(case_ids)} cases, {len(evaluator_ids)} evaluators...")
|
|
1492
1606
|
|
|
1493
|
-
rubrics = eval_mod.get_case_rubrics(
|
|
1494
|
-
client, agent_id=args.agent, workspace_id=workspace_id,
|
|
1495
|
-
evaluator_ids=evaluator_ids, case_ids=case_ids,
|
|
1496
|
-
)
|
|
1497
|
-
|
|
1498
1607
|
if args.full:
|
|
1499
1608
|
for cid in case_ids:
|
|
1500
1609
|
log("=" * 80)
|
|
@@ -54,8 +54,26 @@ def create_case(
|
|
|
54
54
|
return client.post("/external/eval/cases", json=body)
|
|
55
55
|
|
|
56
56
|
|
|
57
|
+
def _unwrap_list_response(value: Any, *keys: str) -> list[dict]:
|
|
58
|
+
"""Normalize list endpoints that may return either a bare list or envelope."""
|
|
59
|
+
if isinstance(value, list):
|
|
60
|
+
return value
|
|
61
|
+
if isinstance(value, dict):
|
|
62
|
+
for key in keys:
|
|
63
|
+
rows = value.get(key)
|
|
64
|
+
if isinstance(rows, list):
|
|
65
|
+
return rows
|
|
66
|
+
return []
|
|
67
|
+
|
|
68
|
+
|
|
57
69
|
def list_cases(client: CodeerClient, agent_id: str) -> list[dict]:
|
|
58
|
-
return
|
|
70
|
+
return _unwrap_list_response(
|
|
71
|
+
client.get(f"/external/eval/agents/{agent_id}/cases"),
|
|
72
|
+
"cases",
|
|
73
|
+
"evaluation_cases",
|
|
74
|
+
"data",
|
|
75
|
+
"items",
|
|
76
|
+
)
|
|
59
77
|
|
|
60
78
|
|
|
61
79
|
def get_case(client: CodeerClient, case_id: str) -> dict:
|
|
@@ -3,17 +3,34 @@ from __future__ import annotations
|
|
|
3
3
|
import unittest
|
|
4
4
|
|
|
5
5
|
from codeer_cli import eval_ as eval_mod
|
|
6
|
-
from codeer_cli.commands.eval_cmd import
|
|
6
|
+
from codeer_cli.commands.eval_cmd import (
|
|
7
|
+
_assigned_evaluators_by_case,
|
|
8
|
+
_pairs_from_rubric_batches,
|
|
9
|
+
_planned_eval_pairs,
|
|
10
|
+
_remove_non_runnable_skipped_pairs,
|
|
11
|
+
_skipped_pairs_from_trigger_response,
|
|
12
|
+
)
|
|
7
13
|
|
|
8
14
|
|
|
9
15
|
class FakeClient:
|
|
10
16
|
def __init__(self) -> None:
|
|
11
17
|
self.calls: list[tuple[str, str, dict]] = []
|
|
12
18
|
|
|
19
|
+
def get(self, path: str, **kwargs):
|
|
20
|
+
self.calls.append(("GET", path, kwargs))
|
|
21
|
+
if path == "/external/eval/agents/agent-1/cases":
|
|
22
|
+
return {"cases": [{"id": "case-1"}, {"id": "case-2"}], "total": 2}
|
|
23
|
+
return []
|
|
24
|
+
|
|
13
25
|
def post(self, path: str, **kwargs):
|
|
14
26
|
self.calls.append(("POST", path, kwargs))
|
|
15
27
|
if path == "/external/eval/cases":
|
|
16
28
|
return {"id": "case-1"}
|
|
29
|
+
if path == "/external/eval/rubrics:batch":
|
|
30
|
+
return [
|
|
31
|
+
{"case_id": "case-1", "evaluator_id": "eval-1", "rubric": "Must pass"},
|
|
32
|
+
{"case_id": "case-2", "evaluator_id": "eval-1", "rubric": ""},
|
|
33
|
+
]
|
|
17
34
|
return {"ok": True}
|
|
18
35
|
|
|
19
36
|
def put(self, path: str, **kwargs):
|
|
@@ -22,6 +39,46 @@ class FakeClient:
|
|
|
22
39
|
|
|
23
40
|
|
|
24
41
|
class EvalPairClientTests(unittest.TestCase):
|
|
42
|
+
def test_list_cases_accepts_enveloped_server_response(self) -> None:
|
|
43
|
+
client = FakeClient()
|
|
44
|
+
|
|
45
|
+
cases = eval_mod.list_cases(client, "agent-1") # type: ignore[arg-type]
|
|
46
|
+
|
|
47
|
+
self.assertEqual(cases, [{"id": "case-1"}, {"id": "case-2"}])
|
|
48
|
+
self.assertEqual(client.calls[0][0:2], ("GET", "/external/eval/agents/agent-1/cases"))
|
|
49
|
+
|
|
50
|
+
def test_external_trigger_endpoint_for_agent_history_runs(self) -> None:
|
|
51
|
+
client = FakeClient()
|
|
52
|
+
|
|
53
|
+
eval_mod.trigger( # type: ignore[arg-type]
|
|
54
|
+
client,
|
|
55
|
+
case_ids=["case-1", "case-2"],
|
|
56
|
+
evaluator_ids=["eval-1"],
|
|
57
|
+
agent_history_id="hist-1",
|
|
58
|
+
)
|
|
59
|
+
|
|
60
|
+
self.assertEqual(client.calls[0][0:2], ("POST", "/external/eval/runs"))
|
|
61
|
+
self.assertEqual(
|
|
62
|
+
client.calls[0][2]["json"],
|
|
63
|
+
{
|
|
64
|
+
"case_ids": ["case-1", "case-2"],
|
|
65
|
+
"evaluator_ids": ["eval-1"],
|
|
66
|
+
"version_id": "hist-1",
|
|
67
|
+
},
|
|
68
|
+
)
|
|
69
|
+
|
|
70
|
+
def test_pairs_from_rubric_batches_keeps_configured_rubrics_only(self) -> None:
|
|
71
|
+
client = FakeClient()
|
|
72
|
+
|
|
73
|
+
pairs = _pairs_from_rubric_batches( # type: ignore[arg-type]
|
|
74
|
+
client,
|
|
75
|
+
case_ids=["case-1", "case-2"],
|
|
76
|
+
evaluator_ids=["eval-1"],
|
|
77
|
+
)
|
|
78
|
+
|
|
79
|
+
self.assertEqual(pairs, [{"case_id": "case-1", "evaluator_id": "eval-1"}])
|
|
80
|
+
self.assertEqual(client.calls[0][0:2], ("POST", "/external/eval/rubrics:batch"))
|
|
81
|
+
|
|
25
82
|
def test_assignment_helper_paths(self) -> None:
|
|
26
83
|
client = FakeClient()
|
|
27
84
|
|
|
@@ -120,6 +177,42 @@ class EvalPairPlannerTests(unittest.TestCase):
|
|
|
120
177
|
)
|
|
121
178
|
self.assertEqual(skipped, [])
|
|
122
179
|
|
|
180
|
+
def test_trigger_response_skipped_pairs_are_normalized(self) -> None:
|
|
181
|
+
skipped = _skipped_pairs_from_trigger_response({
|
|
182
|
+
"skipped_pairs": [
|
|
183
|
+
{"case_id": "case-1", "evaluator_id": "eval-1", "reason": "not_assigned"},
|
|
184
|
+
{"case_id": "case-2", "evaluator_id": "eval-1"},
|
|
185
|
+
{"case_id": None, "evaluator_id": "eval-1", "reason": "not_assigned"},
|
|
186
|
+
]
|
|
187
|
+
})
|
|
188
|
+
|
|
189
|
+
self.assertEqual(
|
|
190
|
+
skipped,
|
|
191
|
+
[
|
|
192
|
+
{"case_id": "case-1", "evaluator_id": "eval-1", "reason": "not_assigned"},
|
|
193
|
+
{"case_id": "case-2", "evaluator_id": "eval-1", "reason": "skipped"},
|
|
194
|
+
],
|
|
195
|
+
)
|
|
196
|
+
|
|
197
|
+
def test_non_runnable_skipped_pairs_are_removed_from_poll_targets(self) -> None:
|
|
198
|
+
pairs = [
|
|
199
|
+
{"case_id": "case-1", "evaluator_id": "eval-1"},
|
|
200
|
+
{"case_id": "case-2", "evaluator_id": "eval-1"},
|
|
201
|
+
{"case_id": "case-3", "evaluator_id": "eval-1"},
|
|
202
|
+
]
|
|
203
|
+
skipped = [
|
|
204
|
+
{"case_id": "case-2", "evaluator_id": "eval-1", "reason": "not_assigned"},
|
|
205
|
+
{"case_id": "case-3", "evaluator_id": "eval-1", "reason": "already_running"},
|
|
206
|
+
]
|
|
207
|
+
|
|
208
|
+
self.assertEqual(
|
|
209
|
+
_remove_non_runnable_skipped_pairs(pairs, skipped),
|
|
210
|
+
[
|
|
211
|
+
{"case_id": "case-1", "evaluator_id": "eval-1"},
|
|
212
|
+
{"case_id": "case-3", "evaluator_id": "eval-1"},
|
|
213
|
+
],
|
|
214
|
+
)
|
|
215
|
+
|
|
123
216
|
|
|
124
217
|
if __name__ == "__main__":
|
|
125
218
|
unittest.main()
|
|
@@ -0,0 +1,22 @@
|
|
|
1
|
+
from __future__ import annotations
|
|
2
|
+
|
|
3
|
+
import json
|
|
4
|
+
import tempfile
|
|
5
|
+
import unittest
|
|
6
|
+
from pathlib import Path
|
|
7
|
+
|
|
8
|
+
from codeer_cli.commands._util import write_json
|
|
9
|
+
|
|
10
|
+
|
|
11
|
+
class WriteJsonTests(unittest.TestCase):
|
|
12
|
+
def test_write_json_creates_parent_directories(self) -> None:
|
|
13
|
+
with tempfile.TemporaryDirectory() as tmp:
|
|
14
|
+
out = Path(tmp) / "nested" / "artifact.json"
|
|
15
|
+
|
|
16
|
+
write_json(str(out), {"ok": True})
|
|
17
|
+
|
|
18
|
+
self.assertEqual(json.loads(out.read_text()), {"ok": True})
|
|
19
|
+
|
|
20
|
+
|
|
21
|
+
if __name__ == "__main__":
|
|
22
|
+
unittest.main()
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|