kipimo 0.2.2__tar.gz → 0.3.0__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.
- {kipimo-0.2.2/src/kipimo.egg-info → kipimo-0.3.0}/PKG-INFO +27 -1
- {kipimo-0.2.2 → kipimo-0.3.0}/README.md +26 -0
- {kipimo-0.2.2 → kipimo-0.3.0}/pyproject.toml +1 -1
- {kipimo-0.2.2 → kipimo-0.3.0}/src/kipimo/__init__.py +2 -0
- {kipimo-0.2.2 → kipimo-0.3.0}/src/kipimo/cli.py +15 -1
- kipimo-0.3.0/src/kipimo/harness.py +145 -0
- {kipimo-0.2.2 → kipimo-0.3.0/src/kipimo.egg-info}/PKG-INFO +27 -1
- {kipimo-0.2.2 → kipimo-0.3.0}/src/kipimo.egg-info/SOURCES.txt +2 -0
- kipimo-0.3.0/tests/test_harness.py +69 -0
- {kipimo-0.2.2 → kipimo-0.3.0}/LICENSE +0 -0
- {kipimo-0.2.2 → kipimo-0.3.0}/setup.cfg +0 -0
- {kipimo-0.2.2 → kipimo-0.3.0}/src/kipimo/data/kipimo_v0.1.jsonl +0 -0
- {kipimo-0.2.2 → kipimo-0.3.0}/src/kipimo/targets.py +0 -0
- {kipimo-0.2.2 → kipimo-0.3.0}/src/kipimo.egg-info/dependency_links.txt +0 -0
- {kipimo-0.2.2 → kipimo-0.3.0}/src/kipimo.egg-info/entry_points.txt +0 -0
- {kipimo-0.2.2 → kipimo-0.3.0}/src/kipimo.egg-info/top_level.txt +0 -0
- {kipimo-0.2.2 → kipimo-0.3.0}/tests/test_kipimo.py +0 -0
- {kipimo-0.2.2 → kipimo-0.3.0}/tests/test_smoke.py +0 -0
- {kipimo-0.2.2 → kipimo-0.3.0}/tests/test_targets.py +0 -0
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
Metadata-Version: 2.4
|
|
2
2
|
Name: kipimo
|
|
3
|
-
Version: 0.
|
|
3
|
+
Version: 0.3.0
|
|
4
4
|
Summary: Swahili agent-task evaluation suite for the East Africa coordination stack — model-agnostic seed benchmark (46 tasks: server routing, term grounding, cascade routing).
|
|
5
5
|
Author: AfriKaziOS maintainers
|
|
6
6
|
License-Expression: MIT
|
|
@@ -65,3 +65,29 @@ MIT-licensed harness, CC BY 4.0 data. Feedback via GitHub Issues only — pull r
|
|
|
65
65
|
|
|
66
66
|
Model-agnostic by design: closed APIs, open-weight models, and small distilled models are all first-class citizens.
|
|
67
67
|
<!-- /interconnect:v1 -->
|
|
68
|
+
|
|
69
|
+
## Running a scorecard (`kipimo run`)
|
|
70
|
+
|
|
71
|
+
Fan out across targets in parallel, converge to one ranked scorecard. kipimo
|
|
72
|
+
still never calls a model API — you supply a *generator command* per target
|
|
73
|
+
(any process that reads tasks JSONL on stdin and writes predictions JSONL on
|
|
74
|
+
stdout; see `examples/generate_predictions.py`).
|
|
75
|
+
|
|
76
|
+
```bash
|
|
77
|
+
cat > generators.json <<'JSON'
|
|
78
|
+
{
|
|
79
|
+
"gemma-3-12b": "python generate_predictions.py --model gemma-3-12b",
|
|
80
|
+
"inkubalm-0.4b": "python generate_predictions.py --model inkubalm-0.4b",
|
|
81
|
+
"kimi-k3": ""
|
|
82
|
+
}
|
|
83
|
+
JSON
|
|
84
|
+
|
|
85
|
+
kipimo run generators.json --timeout 600 --workers 4
|
|
86
|
+
```
|
|
87
|
+
|
|
88
|
+
**An untested target is UNKNOWN, never zero.** If a generator crashes, times
|
|
89
|
+
out, or is unconfigured, that target appears under `untested` with a reason —
|
|
90
|
+
it is not ranked at 0.0. Conflating "the model failed the task" with "we never
|
|
91
|
+
tested the model" is how an evaluation starts measuring its own assumptions
|
|
92
|
+
instead of reality. Every scorecard also reports `coverage`, so a partial run
|
|
93
|
+
cannot be misread as a ranking of the field.
|
|
@@ -47,3 +47,29 @@ MIT-licensed harness, CC BY 4.0 data. Feedback via GitHub Issues only — pull r
|
|
|
47
47
|
|
|
48
48
|
Model-agnostic by design: closed APIs, open-weight models, and small distilled models are all first-class citizens.
|
|
49
49
|
<!-- /interconnect:v1 -->
|
|
50
|
+
|
|
51
|
+
## Running a scorecard (`kipimo run`)
|
|
52
|
+
|
|
53
|
+
Fan out across targets in parallel, converge to one ranked scorecard. kipimo
|
|
54
|
+
still never calls a model API — you supply a *generator command* per target
|
|
55
|
+
(any process that reads tasks JSONL on stdin and writes predictions JSONL on
|
|
56
|
+
stdout; see `examples/generate_predictions.py`).
|
|
57
|
+
|
|
58
|
+
```bash
|
|
59
|
+
cat > generators.json <<'JSON'
|
|
60
|
+
{
|
|
61
|
+
"gemma-3-12b": "python generate_predictions.py --model gemma-3-12b",
|
|
62
|
+
"inkubalm-0.4b": "python generate_predictions.py --model inkubalm-0.4b",
|
|
63
|
+
"kimi-k3": ""
|
|
64
|
+
}
|
|
65
|
+
JSON
|
|
66
|
+
|
|
67
|
+
kipimo run generators.json --timeout 600 --workers 4
|
|
68
|
+
```
|
|
69
|
+
|
|
70
|
+
**An untested target is UNKNOWN, never zero.** If a generator crashes, times
|
|
71
|
+
out, or is unconfigured, that target appears under `untested` with a reason —
|
|
72
|
+
it is not ranked at 0.0. Conflating "the model failed the task" with "we never
|
|
73
|
+
tested the model" is how an evaluation starts measuring its own assumptions
|
|
74
|
+
instead of reality. Every scorecard also reports `coverage`, so a partial run
|
|
75
|
+
cannot be misread as a ranking of the field.
|
|
@@ -4,7 +4,7 @@ build-backend = "setuptools.build_meta"
|
|
|
4
4
|
|
|
5
5
|
[project]
|
|
6
6
|
name = "kipimo"
|
|
7
|
-
version = "0.
|
|
7
|
+
version = "0.3.0"
|
|
8
8
|
description = "Swahili agent-task evaluation suite for the East Africa coordination stack — model-agnostic seed benchmark (46 tasks: server routing, term grounding, cascade routing)."
|
|
9
9
|
readme = "README.md"
|
|
10
10
|
requires-python = ">=3.10"
|
|
@@ -8,3 +8,5 @@ from .targets import FAMILIES as FAMILIES
|
|
|
8
8
|
from .targets import TARGETS as TARGETS
|
|
9
9
|
from .targets import get_target as get_target
|
|
10
10
|
from .targets import list_targets as list_targets
|
|
11
|
+
from .harness import run_scorecard as run_scorecard
|
|
12
|
+
from .harness import run_target as run_target
|
|
@@ -10,6 +10,7 @@ Usage:
|
|
|
10
10
|
kipimo tasks # emit the task set (JSONL) to stdout
|
|
11
11
|
kipimo template # emit an empty predictions file to fill in
|
|
12
12
|
kipimo score preds.jsonl # score predictions against gold
|
|
13
|
+
kipimo run generators.json # run targets in parallel -> ranked scorecard
|
|
13
14
|
kipimo targets # emit the scorecard target registry (v0.2)
|
|
14
15
|
"""
|
|
15
16
|
|
|
@@ -21,7 +22,7 @@ import sys
|
|
|
21
22
|
from collections import defaultdict
|
|
22
23
|
from importlib import resources
|
|
23
24
|
|
|
24
|
-
__version__ = "0.
|
|
25
|
+
__version__ = "0.3.0"
|
|
25
26
|
|
|
26
27
|
DISCLAIMER = ("kipimo v0.1 is a SEED benchmark (46 tasks). Swahili phrasing is "
|
|
27
28
|
"simple-register and pending native-speaker review (issue #1). "
|
|
@@ -86,6 +87,10 @@ def main(argv: list[str] | None = None) -> int:
|
|
|
86
87
|
tp.add_argument("--family", choices=("closed-api", "open-weight", "small-open"),
|
|
87
88
|
default=None, help="filter by target family")
|
|
88
89
|
sub.add_parser("template", help="emit empty predictions JSONL to stdout")
|
|
90
|
+
rp = sub.add_parser("run", help="run generators across targets in parallel -> scorecard")
|
|
91
|
+
rp.add_argument("generators", help='JSON file: {"target_id": "generator command", ...}')
|
|
92
|
+
rp.add_argument("--timeout", type=int, default=600, help="seconds per target (default 600)")
|
|
93
|
+
rp.add_argument("--workers", type=int, default=4, help="parallel targets (default 4)")
|
|
89
94
|
sp = sub.add_parser("score", help="score a predictions file")
|
|
90
95
|
sp.add_argument("predictions", help="JSONL with {id, prediction:[...]} rows")
|
|
91
96
|
args = p.parse_args(argv)
|
|
@@ -100,6 +105,15 @@ def main(argv: list[str] | None = None) -> int:
|
|
|
100
105
|
elif args.cmd == "template":
|
|
101
106
|
for t in load_tasks():
|
|
102
107
|
print(json.dumps({"id": t["id"], "prediction": []}))
|
|
108
|
+
elif args.cmd == "run":
|
|
109
|
+
from .harness import load_generators, run_scorecard
|
|
110
|
+
card = run_scorecard(load_generators(args.generators),
|
|
111
|
+
timeout=args.timeout, max_workers=args.workers)
|
|
112
|
+
print(json.dumps(card, indent=2, ensure_ascii=False))
|
|
113
|
+
if card["targets_untested"]:
|
|
114
|
+
print(f"\nNote: {card['targets_untested']} target(s) UNTESTED (unknown, not zero). "
|
|
115
|
+
f"See 'untested' in the scorecard.", file=sys.stderr)
|
|
116
|
+
print(f"\n{DISCLAIMER}", file=sys.stderr)
|
|
103
117
|
else:
|
|
104
118
|
rep = score_file(args.predictions)
|
|
105
119
|
print(json.dumps(rep, indent=2))
|
|
@@ -0,0 +1,145 @@
|
|
|
1
|
+
"""kipimo.harness — run the benchmark across targets in parallel, converge to a scorecard.
|
|
2
|
+
|
|
3
|
+
The diamond: fan out one worker per target, converge into a single ranked
|
|
4
|
+
scorecard. Targets are independent (a real dependency test — no target's
|
|
5
|
+
prediction informs another's), so they run concurrently; the scorecard is the
|
|
6
|
+
only true convergence point.
|
|
7
|
+
|
|
8
|
+
Two design rules, both deliberate:
|
|
9
|
+
|
|
10
|
+
1. **kipimo still never calls a model API.** This module orchestrates *generator
|
|
11
|
+
commands you supply* (any process that reads tasks JSONL on stdin and writes
|
|
12
|
+
predictions JSONL on stdout). Model access, keys, and endpoints stay outside
|
|
13
|
+
the package, so the benchmark remains vendor-neutral and reproducible by
|
|
14
|
+
anyone. See examples/generate_predictions.py for a stdlib generator.
|
|
15
|
+
|
|
16
|
+
2. **A missing run is never a zero.** The scoring path already treats an absent
|
|
17
|
+
prediction as 0.0, which is correct *within* a run. Across targets it would
|
|
18
|
+
be a lie: a target whose generator crashed, timed out, or was never
|
|
19
|
+
configured has an UNKNOWN score, not a bad one. Conflating "the model failed
|
|
20
|
+
the task" with "we never tested the model" is how an evaluation system starts
|
|
21
|
+
training on its own assumptions instead of on evidence. Every result
|
|
22
|
+
therefore carries an explicit ``status``:
|
|
23
|
+
|
|
24
|
+
ok — generator ran, predictions scored
|
|
25
|
+
error — generator failed (message retained)
|
|
26
|
+
timeout — generator exceeded its deadline
|
|
27
|
+
skipped — no generator configured for this target
|
|
28
|
+
|
|
29
|
+
Only ``ok`` results carry a score. The scorecard reports coverage alongside
|
|
30
|
+
results so a partial run can never be read as a complete one.
|
|
31
|
+
"""
|
|
32
|
+
from __future__ import annotations
|
|
33
|
+
|
|
34
|
+
import concurrent.futures
|
|
35
|
+
import json
|
|
36
|
+
import shlex
|
|
37
|
+
import subprocess
|
|
38
|
+
import tempfile
|
|
39
|
+
import time
|
|
40
|
+
from pathlib import Path
|
|
41
|
+
|
|
42
|
+
from .cli import load_tasks, score_file
|
|
43
|
+
from .targets import get_target, list_targets
|
|
44
|
+
|
|
45
|
+
DEFAULT_TIMEOUT = 600 # seconds per target; generators are network-bound
|
|
46
|
+
|
|
47
|
+
|
|
48
|
+
def _tasks_jsonl() -> str:
|
|
49
|
+
return "\n".join(json.dumps(t, ensure_ascii=False) for t in load_tasks())
|
|
50
|
+
|
|
51
|
+
|
|
52
|
+
def run_target(target_id: str, command: str, timeout: int = DEFAULT_TIMEOUT) -> dict:
|
|
53
|
+
"""Run one generator command and score its predictions.
|
|
54
|
+
|
|
55
|
+
``command`` is a shell-style command that reads tasks JSONL on stdin and
|
|
56
|
+
writes predictions JSONL ({"id":..., "prediction":[...]}) on stdout.
|
|
57
|
+
Returns a result dict with an explicit status; never raises for generator
|
|
58
|
+
failure (a broken target must not abort the whole scorecard).
|
|
59
|
+
"""
|
|
60
|
+
started = time.time()
|
|
61
|
+
base = {"target": target_id, "command": command}
|
|
62
|
+
try:
|
|
63
|
+
get_target(target_id) # validate against the registry
|
|
64
|
+
except Exception:
|
|
65
|
+
base["registry"] = "unregistered"
|
|
66
|
+
if not command:
|
|
67
|
+
return {**base, "status": "skipped", "score": None,
|
|
68
|
+
"reason": "no generator command configured"}
|
|
69
|
+
try:
|
|
70
|
+
proc = subprocess.run(
|
|
71
|
+
shlex.split(command), input=_tasks_jsonl(), text=True,
|
|
72
|
+
capture_output=True, timeout=timeout,
|
|
73
|
+
)
|
|
74
|
+
except subprocess.TimeoutExpired:
|
|
75
|
+
return {**base, "status": "timeout", "score": None,
|
|
76
|
+
"elapsed_s": round(time.time() - started, 1),
|
|
77
|
+
"reason": f"exceeded {timeout}s"}
|
|
78
|
+
except Exception as e: # generator binary missing, bad command, etc.
|
|
79
|
+
return {**base, "status": "error", "score": None,
|
|
80
|
+
"reason": f"{type(e).__name__}: {e}"}
|
|
81
|
+
if proc.returncode != 0:
|
|
82
|
+
return {**base, "status": "error", "score": None,
|
|
83
|
+
"reason": f"exit {proc.returncode}: {(proc.stderr or '').strip()[:200]}"}
|
|
84
|
+
if not proc.stdout.strip():
|
|
85
|
+
return {**base, "status": "error", "score": None,
|
|
86
|
+
"reason": "generator produced no predictions"}
|
|
87
|
+
with tempfile.NamedTemporaryFile("w", suffix=".jsonl", delete=False,
|
|
88
|
+
encoding="utf-8") as fh:
|
|
89
|
+
fh.write(proc.stdout)
|
|
90
|
+
path = fh.name
|
|
91
|
+
try:
|
|
92
|
+
report = score_file(path)
|
|
93
|
+
except Exception as e:
|
|
94
|
+
return {**base, "status": "error", "score": None,
|
|
95
|
+
"reason": f"unscoreable predictions: {type(e).__name__}: {e}"}
|
|
96
|
+
finally:
|
|
97
|
+
Path(path).unlink(missing_ok=True)
|
|
98
|
+
return {**base, "status": "ok", "score": report["overall"], "report": report,
|
|
99
|
+
"elapsed_s": round(time.time() - started, 1)}
|
|
100
|
+
|
|
101
|
+
|
|
102
|
+
def run_scorecard(generators: dict[str, str], timeout: int = DEFAULT_TIMEOUT,
|
|
103
|
+
max_workers: int = 4) -> dict:
|
|
104
|
+
"""Fan out across targets, converge to one scorecard.
|
|
105
|
+
|
|
106
|
+
``generators`` maps target_id -> generator command. Targets run in parallel
|
|
107
|
+
because they are genuinely independent; the scorecard is the convergence.
|
|
108
|
+
"""
|
|
109
|
+
results: list[dict] = []
|
|
110
|
+
with concurrent.futures.ThreadPoolExecutor(max_workers=max_workers) as pool:
|
|
111
|
+
futures = {pool.submit(run_target, tid, cmd, timeout): tid
|
|
112
|
+
for tid, cmd in generators.items()}
|
|
113
|
+
for fut in concurrent.futures.as_completed(futures):
|
|
114
|
+
results.append(fut.result())
|
|
115
|
+
ok = [r for r in results if r["status"] == "ok"]
|
|
116
|
+
ranked = sorted(ok, key=lambda r: r["score"], reverse=True)
|
|
117
|
+
untested = [r for r in results if r["status"] != "ok"]
|
|
118
|
+
registry_total = len(list_targets())
|
|
119
|
+
return {
|
|
120
|
+
"kipimo_tasks": len(load_tasks()),
|
|
121
|
+
"targets_attempted": len(results),
|
|
122
|
+
"targets_scored": len(ok),
|
|
123
|
+
"targets_untested": len(untested),
|
|
124
|
+
"registry_targets": registry_total,
|
|
125
|
+
# coverage makes an incomplete run impossible to misread as complete
|
|
126
|
+
"coverage": f"{len(ok)}/{registry_total} registry targets scored",
|
|
127
|
+
"ranked": [{"target": r["target"], "score": r["score"],
|
|
128
|
+
"by_type": {k: v for k, v in r["report"].items()
|
|
129
|
+
if k not in ("overall", "n_tasks", "n_missing")},
|
|
130
|
+
"n_missing": r["report"]["n_missing"]} for r in ranked],
|
|
131
|
+
"untested": [{"target": r["target"], "status": r["status"],
|
|
132
|
+
"reason": r.get("reason", "")} for r in untested],
|
|
133
|
+
"caveat": ("Scores are stack-routing competence, not general Swahili fluency. "
|
|
134
|
+
"Untested targets have UNKNOWN scores — not zero. A partial "
|
|
135
|
+
"scorecard is not a ranking of the field."),
|
|
136
|
+
}
|
|
137
|
+
|
|
138
|
+
|
|
139
|
+
def load_generators(path: str) -> dict[str, str]:
|
|
140
|
+
"""Load a target_id -> command mapping from a JSON file."""
|
|
141
|
+
with open(path, encoding="utf-8") as f:
|
|
142
|
+
data = json.load(f)
|
|
143
|
+
if not isinstance(data, dict):
|
|
144
|
+
raise ValueError("generators file must be a JSON object: {target_id: command}")
|
|
145
|
+
return {str(k): str(v) for k, v in data.items()}
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
Metadata-Version: 2.4
|
|
2
2
|
Name: kipimo
|
|
3
|
-
Version: 0.
|
|
3
|
+
Version: 0.3.0
|
|
4
4
|
Summary: Swahili agent-task evaluation suite for the East Africa coordination stack — model-agnostic seed benchmark (46 tasks: server routing, term grounding, cascade routing).
|
|
5
5
|
Author: AfriKaziOS maintainers
|
|
6
6
|
License-Expression: MIT
|
|
@@ -65,3 +65,29 @@ MIT-licensed harness, CC BY 4.0 data. Feedback via GitHub Issues only — pull r
|
|
|
65
65
|
|
|
66
66
|
Model-agnostic by design: closed APIs, open-weight models, and small distilled models are all first-class citizens.
|
|
67
67
|
<!-- /interconnect:v1 -->
|
|
68
|
+
|
|
69
|
+
## Running a scorecard (`kipimo run`)
|
|
70
|
+
|
|
71
|
+
Fan out across targets in parallel, converge to one ranked scorecard. kipimo
|
|
72
|
+
still never calls a model API — you supply a *generator command* per target
|
|
73
|
+
(any process that reads tasks JSONL on stdin and writes predictions JSONL on
|
|
74
|
+
stdout; see `examples/generate_predictions.py`).
|
|
75
|
+
|
|
76
|
+
```bash
|
|
77
|
+
cat > generators.json <<'JSON'
|
|
78
|
+
{
|
|
79
|
+
"gemma-3-12b": "python generate_predictions.py --model gemma-3-12b",
|
|
80
|
+
"inkubalm-0.4b": "python generate_predictions.py --model inkubalm-0.4b",
|
|
81
|
+
"kimi-k3": ""
|
|
82
|
+
}
|
|
83
|
+
JSON
|
|
84
|
+
|
|
85
|
+
kipimo run generators.json --timeout 600 --workers 4
|
|
86
|
+
```
|
|
87
|
+
|
|
88
|
+
**An untested target is UNKNOWN, never zero.** If a generator crashes, times
|
|
89
|
+
out, or is unconfigured, that target appears under `untested` with a reason —
|
|
90
|
+
it is not ranked at 0.0. Conflating "the model failed the task" with "we never
|
|
91
|
+
tested the model" is how an evaluation starts measuring its own assumptions
|
|
92
|
+
instead of reality. Every scorecard also reports `coverage`, so a partial run
|
|
93
|
+
cannot be misread as a ranking of the field.
|
|
@@ -3,6 +3,7 @@ README.md
|
|
|
3
3
|
pyproject.toml
|
|
4
4
|
src/kipimo/__init__.py
|
|
5
5
|
src/kipimo/cli.py
|
|
6
|
+
src/kipimo/harness.py
|
|
6
7
|
src/kipimo/targets.py
|
|
7
8
|
src/kipimo.egg-info/PKG-INFO
|
|
8
9
|
src/kipimo.egg-info/SOURCES.txt
|
|
@@ -10,6 +11,7 @@ src/kipimo.egg-info/dependency_links.txt
|
|
|
10
11
|
src/kipimo.egg-info/entry_points.txt
|
|
11
12
|
src/kipimo.egg-info/top_level.txt
|
|
12
13
|
src/kipimo/data/kipimo_v0.1.jsonl
|
|
14
|
+
tests/test_harness.py
|
|
13
15
|
tests/test_kipimo.py
|
|
14
16
|
tests/test_smoke.py
|
|
15
17
|
tests/test_targets.py
|
|
@@ -0,0 +1,69 @@
|
|
|
1
|
+
"""Harness tests — the load-bearing property is that UNTESTED is never scored as zero."""
|
|
2
|
+
import json
|
|
3
|
+
import sys
|
|
4
|
+
|
|
5
|
+
from kipimo.harness import run_target, run_scorecard
|
|
6
|
+
|
|
7
|
+
GOOD = f'{sys.executable} -c "import sys,json\nfor l in sys.stdin:\n l=l.strip()\n if l: t=json.loads(l); print(json.dumps({{\'id\':t[\'id\'],\'prediction\':[\'mpesa-mcp\']}}))"'
|
|
8
|
+
CRASH = f'{sys.executable} -c "import sys; sys.exit(3)"'
|
|
9
|
+
EMPTY = f'{sys.executable} -c "pass"'
|
|
10
|
+
HANG = f'{sys.executable} -c "import time; time.sleep(60)"'
|
|
11
|
+
|
|
12
|
+
|
|
13
|
+
def test_working_generator_scores():
|
|
14
|
+
r = run_target("gemma-3-12b", GOOD, timeout=60)
|
|
15
|
+
assert r["status"] == "ok"
|
|
16
|
+
assert isinstance(r["score"], float)
|
|
17
|
+
assert r["report"]["n_missing"] == 0
|
|
18
|
+
|
|
19
|
+
|
|
20
|
+
def test_crashed_generator_is_error_not_zero():
|
|
21
|
+
r = run_target("gemma-3-12b", CRASH, timeout=60)
|
|
22
|
+
assert r["status"] == "error"
|
|
23
|
+
assert r["score"] is None # NOT 0.0 — we did not test it
|
|
24
|
+
assert "exit 3" in r["reason"]
|
|
25
|
+
|
|
26
|
+
|
|
27
|
+
def test_empty_output_is_error_not_zero():
|
|
28
|
+
r = run_target("gemma-3-12b", EMPTY, timeout=60)
|
|
29
|
+
assert r["status"] == "error"
|
|
30
|
+
assert r["score"] is None
|
|
31
|
+
assert "no predictions" in r["reason"]
|
|
32
|
+
|
|
33
|
+
|
|
34
|
+
def test_timeout_is_recorded_not_zero():
|
|
35
|
+
r = run_target("gemma-3-12b", HANG, timeout=2)
|
|
36
|
+
assert r["status"] == "timeout"
|
|
37
|
+
assert r["score"] is None
|
|
38
|
+
|
|
39
|
+
|
|
40
|
+
def test_unconfigured_target_is_skipped():
|
|
41
|
+
r = run_target("kimi-k3", "", timeout=5)
|
|
42
|
+
assert r["status"] == "skipped"
|
|
43
|
+
assert r["score"] is None
|
|
44
|
+
|
|
45
|
+
|
|
46
|
+
def test_scorecard_separates_scored_from_untested():
|
|
47
|
+
card = run_scorecard({"gemma-3-12b": GOOD, "inkubalm-0.4b": CRASH, "kimi-k3": ""},
|
|
48
|
+
timeout=30, max_workers=3)
|
|
49
|
+
assert card["targets_scored"] == 1
|
|
50
|
+
assert card["targets_untested"] == 2
|
|
51
|
+
ranked_ids = [r["target"] for r in card["ranked"]]
|
|
52
|
+
assert ranked_ids == ["gemma-3-12b"] # only real results ranked
|
|
53
|
+
untested_ids = {r["target"] for r in card["untested"]}
|
|
54
|
+
assert untested_ids == {"inkubalm-0.4b", "kimi-k3"}
|
|
55
|
+
# coverage must make a partial run unmistakable
|
|
56
|
+
assert "/" in card["coverage"]
|
|
57
|
+
assert "not zero" in card["caveat"]
|
|
58
|
+
|
|
59
|
+
|
|
60
|
+
def test_one_broken_target_does_not_abort_the_run():
|
|
61
|
+
card = run_scorecard({"a": CRASH, "b": GOOD}, timeout=30, max_workers=2)
|
|
62
|
+
assert card["targets_attempted"] == 2
|
|
63
|
+
assert card["targets_scored"] == 1 # the good one still ran
|
|
64
|
+
|
|
65
|
+
|
|
66
|
+
def test_ranking_is_by_score_descending():
|
|
67
|
+
card = run_scorecard({"gemma-3-12b": GOOD, "qwen3-14b": GOOD}, timeout=30, max_workers=2)
|
|
68
|
+
scores = [r["score"] for r in card["ranked"]]
|
|
69
|
+
assert scores == sorted(scores, reverse=True)
|
|
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
|