Files
wehub-resource-sync 426e9eeabd
Voice Workbench / headless workbench (mocked backends) (push) Has been cancelled
Voice Workbench / real acoustic lane (nightly, provisioned only) (push) Has been cancelled
ci / test (push) Has been cancelled
ci / lint-and-format (push) Has been cancelled
ci / build (push) Has been cancelled
ci / dev-startup (push) Has been cancelled
gitleaks / gitleaks (push) Has been cancelled
Markdown Links / Relative Markdown Links (push) Has been cancelled
Quality (Extended) / Homepage Build (PR smoke) (push) Has been cancelled
Quality (Extended) / Comment-only diff guard (push) Has been cancelled
Quality (Extended) / Format + Type Safety Ratchet (push) Has been cancelled
Quality (Extended) / Develop Gate (secret scan + UI determinism) (push) Has been cancelled
Quality (Extended) / Develop Gate (lint) (push) Has been cancelled
Chat shell gestures / Chat shell gesture + parity e2e (push) Has been cancelled
Cloud Gateway Discord / Test (push) Has been cancelled
Benchmark Bridge Tests / benchmark (bunx @biomejs/biome check packages/lifeops-bench/src, benchmark-lint) (push) Has been cancelled
Benchmark Bridge Tests / benchmark (bunx vitest run --config packages/lifeops-bench/vitest.config.ts --root packages/lifeops-bench --passWithNoTests, benchmark-tests) (push) Has been cancelled
Build Agent Image / build-and-push (push) Has been cancelled
Dev Smoke / bun run dev onboarding chat (push) Has been cancelled
Dev Smoke / Vite HMR dependency-level smoke (push) Has been cancelled
Electrobun Submodule Guard / electrobun gitlink is fetchable (push) Has been cancelled
Publish @elizaos/example-code / check_npm (push) Has been cancelled
Publish @elizaos/example-code / publish_npm (push) Has been cancelled
Publish @elizaos/plugin-elizacloud / verify_version (push) Has been cancelled
Publish @elizaos/plugin-elizacloud / publish_npm (push) Has been cancelled
Sandbox Live Smoke / Sandbox live smoke (push) Has been cancelled
Snap Build & Test / Build Snap (amd64) (push) Has been cancelled
Snap Build & Test / Build Snap (arm64) (push) Has been cancelled
Test Packaging / elizaos CLI global-install smoke (node + bun) (push) Has been cancelled
Cloud Gateway Webhook / Test (push) Has been cancelled
Cloud Tests / lint-and-types (push) Has been cancelled
Cloud Tests / unit-tests (push) Has been cancelled
Cloud Tests / integration-tests (push) Has been cancelled
Cloud Tests / e2e-tests (push) Has been cancelled
CodeQL Advanced / Analyze (javascript-typescript) (push) Has been cancelled
Deploy Apps Worker (Product 2) / Determine environment (push) Has been cancelled
Deploy Apps Worker (Product 2) / Deploy apps worker to apps-control host (${{ needs.determine-env.outputs.environment }}) (push) Has been cancelled
Deploy Eliza Provisioning Worker / Determine environment (push) Has been cancelled
Deploy Eliza Provisioning Worker / Deploy worker to Hetzner host (${{ needs.determine-env.outputs.environment }} @ ${{ needs.determine-env.outputs.deployment_sha }}) (push) Has been cancelled
Dev Smoke / Classify changed paths (push) Has been cancelled
supply-chain / sbom (push) Has been cancelled
supply-chain / vulnerability-scan (push) Has been cancelled
Build, Push & Deploy to Phala Cloud / build-and-push (push) Has been cancelled
Test Packaging / Validate Packaging Configs (push) Has been cancelled
Test Packaging / Build & Test PyPI Package (push) Has been cancelled
Test Packaging / PyPI on Python ${{ matrix.python }} (push) Has been cancelled
Test Packaging / Pack & Test JS Tarballs (push) Has been cancelled
UI Fixture E2E / ui-fixture-e2e (push) Has been cancelled
UI Fixture E2E / fixture-e2e (push) Has been cancelled
UI Story Gate / story-gate (push) Has been cancelled
vault-ci / test (macos-latest) (push) Has been cancelled
vault-ci / test (ubuntu-latest) (push) Has been cancelled
vault-ci / test (windows-latest) (push) Has been cancelled
vault-ci / app-core wiring tests (push) Has been cancelled
verify-patches / verify patches/CHECKSUMS.sha256 (push) Has been cancelled
Voice Benchmark Smoke / voice-emotion fixture smoke (push) Has been cancelled
Voice Benchmark Smoke / voiceagentbench fixture smoke (push) Has been cancelled
Voice Benchmark Smoke / voicebench-quality unit smoke (push) Has been cancelled
Voice Benchmark Smoke / voicebench TypeScript unit (no audio) (push) Has been cancelled
Voice Benchmark Smoke / voice bench smoke summary (push) Has been cancelled
Windows CI / windows ([bun run --cwd packages/app-core test bun run --cwd packages/elizaos test bun run --cwd packages/cloud/shared test], app-and-cli) (push) Has been cancelled
Windows CI / windows ([bun run --cwd packages/scenario-runner test bun run --cwd packages/vault test bun run --cwd packages/security test bun run --cwd plugins/plugin-coding-tools test], framework-packages) (push) Has been cancelled
Windows CI / windows ([bun run --cwd plugins/plugin-elizacloud test bun run --cwd plugins/plugin-discord test bun run --cwd plugins/plugin-anthropic test bun run --cwd plugins/plugin-openai test bun run --cwd plugins/plugin-app-control test bun run --cwd plugins/pl… (push) Has been cancelled
Windows CI / windows ([node packages/scripts/run-turbo.mjs run build --filter=@elizaos/core --filter=@elizaos/shared --filter=@elizaos/agent --concurrency=4 node packages/scripts/run-bash-linux-only.mjs scripts/verify-riscv64-buildpaths.sh node packages/scripts/run… (push) Has been cancelled
Windows CI / windows ([node packages/scripts/run-turbo.mjs run typecheck --filter=@elizaos/core --filter=@elizaos/shared --filter=@elizaos/cloud-shared --concurrency=4 bun run --cwd packages/core test bun run --cwd packages/shared test], core-runtime, 75) (push) Has been cancelled
chore: import upstream snapshot with attribution
2026-07-13 12:43:05 +08:00

867 lines
28 KiB
Python
Raw Permalink Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
"""Tests for failure-driven adaptive synth (M6 / W1-S2).
CPU-only. No network. Mocked author + fixture agent → deterministic
output. Covers:
- Fixture failure set (JSON artifacts + a tiny SQLite ResultsStore
written in-test) → loaded by both source implementations.
- Mocked author → variations generated + tagged with
``derived_from`` + ``synth_kind='failure_derived'``.
- No silent drops: author exceptions land in the report, not stdout.
- CLI smoke (--source json end-to-end).
- ``--since`` parsing for ISO-8601, dates, and bare unix-ms.
"""
from __future__ import annotations
import json
import random
import sqlite3
import sys
from pathlib import Path
from typing import Any
import pytest
# Make ``synth.adaptive_synth`` importable from this test file regardless
# of where pytest collects it.
HERE = Path(__file__).resolve().parent
SCRIPTS_ROOT = HERE.parent
if str(SCRIPTS_ROOT) not in sys.path:
sys.path.insert(0, str(SCRIPTS_ROOT))
PACKAGES_ROOT = SCRIPTS_ROOT.parent.parent
if str(PACKAGES_ROOT) not in sys.path:
sys.path.insert(0, str(PACKAGES_ROOT))
from synth.adaptive_synth import ( # noqa: E402
SYNTH_KIND,
AdaptiveSynth,
AdaptiveSynthReport,
BenchmarkFailure,
CallableVariationAuthor,
FixtureVariationAuthor,
JsonArtifactFailureSource,
ResultsStoreFailureSource,
_extract_failed_tasks,
_parse_since,
main as cli_main,
)
from synth.project_simulator import ( # noqa: E402
AgentClient,
EchoAgentClient,
HeuristicTurnDecider,
Project,
ScriptedTurnDecider,
)
# ─────────────────────────── fixture helpers ────────────────────────────
def _failure(
*,
run_id: str = "run-1",
benchmark: str = "synth-fail",
score: float = 0.2,
failed_tasks: tuple[dict[str, Any], ...] = (),
raw: dict[str, Any] | None = None,
) -> BenchmarkFailure:
return BenchmarkFailure(
run_id=run_id,
model_id="eliza-1-2b",
benchmark=benchmark,
score=score,
ts=1_700_000_000_000,
failed_tasks=failed_tasks,
raw=raw or {},
)
# ─────────────────────────── BenchmarkFailure shape ────────────────────────────
def test_failure_to_context_strips_raw():
f = _failure(
raw={"large": "x" * 1000, "user_text": "do x"},
failed_tasks=({"task_id": "t1", "user_text": "do x"},),
)
ctx = f.to_context()
assert set(ctx.keys()) == {"run_id", "model_id", "benchmark", "score", "ts"}
assert ctx["run_id"] == "run-1"
assert ctx["score"] == 0.2
def test_extract_failed_tasks_failed_tasks_key():
raw = {"failed_tasks": [{"task_id": "a"}, {"task_id": "b"}, "garbage"]}
out = _extract_failed_tasks(raw)
assert len(out) == 2
assert out[0]["task_id"] == "a"
def test_extract_failed_tasks_tasks_passed_false():
raw = {
"tasks": [
{"task_id": "a", "passed": True},
{"task_id": "b", "passed": False},
{"task_id": "c", "passed": False},
]
}
out = _extract_failed_tasks(raw)
assert [t["task_id"] for t in out] == ["b", "c"]
def test_extract_failed_tasks_results_low_score():
raw = {
"results": [
{"task_id": "a", "score": 0.9},
{"task_id": "b", "score": 0.2},
{"task_id": "c", "score": 0.49},
]
}
out = _extract_failed_tasks(raw)
assert [t["task_id"] for t in out] == ["b", "c"]
def test_extract_failed_tasks_empty_when_no_signal():
assert _extract_failed_tasks({"unrelated": 1}) == ()
# ─────────────────────────── FixtureVariationAuthor ────────────────────────────
def test_fixture_author_uses_failed_task_prompts():
f = _failure(
failed_tasks=(
{"task_id": "t1", "user_text": "summarize the doc"},
{"task_id": "t2", "user_text": "list the names"},
)
)
rng = random.Random(0)
projects = FixtureVariationAuthor().author_variations(
failure=f, n=3, rng=rng
)
assert len(projects) == 3
# variation 0 picks the first candidate, 1 the second, 2 wraps.
assert "summarize the doc" in projects[0].initial_user_text
assert "list the names" in projects[1].initial_user_text
assert "summarize the doc" in projects[2].initial_user_text
for i, p in enumerate(projects):
assert p.metadata["variation_index"] == i
assert p.metadata["failure_run_id"] == "run-1"
def test_fixture_author_falls_back_to_user_text_in_raw():
f = _failure(raw={"user_text": "the raw prompt"})
projects = FixtureVariationAuthor().author_variations(
failure=f, n=1, rng=random.Random(0)
)
assert "the raw prompt" in projects[0].initial_user_text
def test_fixture_author_synthesizes_prompt_when_no_text_available():
f = _failure(benchmark="numeric-bench", score=0.12, raw={})
projects = FixtureVariationAuthor().author_variations(
failure=f, n=2, rng=random.Random(0)
)
# Synthetic prompt mentions both the benchmark + the score.
for p in projects:
assert "numeric-bench" in p.initial_user_text
assert "0.12" in p.initial_user_text
def test_fixture_author_rejects_n_zero():
with pytest.raises(ValueError, match="n must be >= 1"):
FixtureVariationAuthor().author_variations(
failure=_failure(), n=0, rng=random.Random(0)
)
def test_fixture_author_template_must_have_placeholders():
with pytest.raises(ValueError, match="placeholders"):
FixtureVariationAuthor(template="no_placeholders")
# ─────────────────────────── CallableVariationAuthor ────────────────────────────
def test_callable_author_passes_through_triples():
def fake_llm(failure, n):
return [
(f"title-{i}", f"goal-{i}", f"prompt-{i} for {failure.run_id}")
for i in range(n)
]
author = CallableVariationAuthor(fake_llm)
f = _failure(run_id="run-42")
projects = author.author_variations(failure=f, n=3, rng=random.Random(0))
assert len(projects) == 3
assert projects[0].title == "title-0"
assert projects[1].initial_user_text == "prompt-1 for run-42"
assert projects[2].metadata["variation_index"] == 2
assert projects[0].metadata["failure_run_id"] == "run-42"
def test_callable_author_rejects_wrong_count():
def liar(failure, n):
return [("t", "g", "p")] # always 1, regardless of n
author = CallableVariationAuthor(liar)
with pytest.raises(ValueError, match="returned 1 variations but 3"):
author.author_variations(
failure=_failure(), n=3, rng=random.Random(0)
)
def test_callable_author_rejects_empty_field():
def bad(failure, n):
return [("title", "", "prompt") for _ in range(n)]
author = CallableVariationAuthor(bad)
with pytest.raises(ValueError, match="empty fields"):
author.author_variations(
failure=_failure(), n=2, rng=random.Random(0)
)
def test_callable_author_rejects_non_tuple():
def malformed(failure, n):
return ["just a string" for _ in range(n)]
author = CallableVariationAuthor(malformed)
with pytest.raises(ValueError, match="not a"):
author.author_variations(
failure=_failure(), n=1, rng=random.Random(0)
)
# ─────────────────────────── JsonArtifactFailureSource ────────────────────────────
def test_json_source_rejects_missing_dir(tmp_path):
with pytest.raises(FileNotFoundError):
JsonArtifactFailureSource(root=tmp_path / "nope")
def test_json_source_rejects_file_as_root(tmp_path):
f = tmp_path / "x.json"
f.write_text("{}")
with pytest.raises(NotADirectoryError):
JsonArtifactFailureSource(root=f)
def _write_json(path: Path, blob: Any) -> None:
path.parent.mkdir(parents=True, exist_ok=True)
path.write_text(json.dumps(blob))
def test_json_source_loads_single_json_object(tmp_path):
_write_json(
tmp_path / "run.json",
{
"id": "run-7",
"model_id": "m1",
"benchmark": "b1",
"score": 0.2,
"ts": 1_700_000_000_000,
"failed_tasks": [{"task_id": "t1", "user_text": "do x"}],
},
)
src = JsonArtifactFailureSource(root=tmp_path)
out = src.list_failures(since_ts_ms=0, threshold=0.5)
assert len(out) == 1
f = out[0]
assert f.run_id == "run-7"
assert f.score == 0.2
assert f.failed_tasks[0]["user_text"] == "do x"
def test_json_source_loads_jsonl_lines(tmp_path):
lines = [
{
"id": "run-a",
"model_id": "m1",
"benchmark": "b1",
"score": 0.1,
"ts": 1_700_000_000_000,
},
{
"id": "run-b",
"model_id": "m1",
"benchmark": "b1",
"score": 0.9, # filtered out
"ts": 1_700_000_000_000,
},
{
"id": "run-c",
"model_id": "m1",
"benchmark": "b1",
"score": 0.3,
"ts": 1_600_000_000_000, # too old
},
]
f = tmp_path / "runs.jsonl"
f.write_text("\n".join(json.dumps(line) for line in lines) + "\n")
src = JsonArtifactFailureSource(root=tmp_path)
out = src.list_failures(
since_ts_ms=1_700_000_000_000, threshold=0.5
)
assert [x.run_id for x in out] == ["run-a"]
def test_json_source_synthesizes_run_id_when_missing(tmp_path):
_write_json(
tmp_path / "r.json",
{
"model_id": "m1",
"benchmark": "b1",
"score": 0.1,
"ts": 1_700_000_000_000,
},
)
src = JsonArtifactFailureSource(root=tmp_path)
out = src.list_failures(since_ts_ms=0, threshold=0.5)
assert len(out) == 1
assert out[0].run_id == "b1-1700000000000"
def test_json_source_skips_records_missing_required_fields(tmp_path):
_write_json(tmp_path / "bad.json", {"score": 0.2}) # missing rest
_write_json(
tmp_path / "good.json",
{"model_id": "m", "benchmark": "b", "score": 0.2, "ts": 1_700_000_000_000},
)
src = JsonArtifactFailureSource(root=tmp_path)
out = src.list_failures(since_ts_ms=0, threshold=0.5)
assert len(out) == 1
assert out[0].benchmark == "b"
def test_json_source_handles_list_of_runs_in_one_file(tmp_path):
blob = [
{"id": "x", "model_id": "m", "benchmark": "b",
"score": 0.1, "ts": 1_700_000_000_000},
{"id": "y", "model_id": "m", "benchmark": "b",
"score": 0.4, "ts": 1_700_000_000_001},
]
_write_json(tmp_path / "multi.json", blob)
src = JsonArtifactFailureSource(root=tmp_path)
out = src.list_failures(since_ts_ms=0, threshold=0.5)
assert {f.run_id for f in out} == {"x", "y"}
def test_json_source_rejects_bad_threshold(tmp_path):
src = JsonArtifactFailureSource(root=tmp_path)
with pytest.raises(ValueError):
src.list_failures(since_ts_ms=0, threshold=1.5)
with pytest.raises(ValueError):
src.list_failures(since_ts_ms=0, threshold=-0.1)
def test_json_source_rejects_negative_since(tmp_path):
src = JsonArtifactFailureSource(root=tmp_path)
with pytest.raises(ValueError):
src.list_failures(since_ts_ms=-1, threshold=0.5)
def test_json_source_skips_malformed_jsonl_lines(tmp_path):
f = tmp_path / "broken.jsonl"
f.write_text(
'{"id":"ok","model_id":"m","benchmark":"b","score":0.2,"ts":1700000000000}\n'
"{this is not json\n"
'{"id":"ok2","model_id":"m","benchmark":"b","score":0.3,"ts":1700000000000}\n'
)
src = JsonArtifactFailureSource(root=tmp_path)
out = src.list_failures(since_ts_ms=0, threshold=0.5)
assert {f.run_id for f in out} == {"ok", "ok2"}
# ─────────────────────────── ResultsStoreFailureSource ────────────────────────────
def _make_results_db(tmp_path: Path, rows: list[dict[str, Any]]) -> Path:
db_path = tmp_path / "results.db"
conn = sqlite3.connect(str(db_path))
conn.executescript(
"""
CREATE TABLE benchmark_runs (
id INTEGER PRIMARY KEY AUTOINCREMENT,
model_id TEXT NOT NULL,
benchmark TEXT NOT NULL,
score REAL NOT NULL,
ts INTEGER NOT NULL,
dataset_version TEXT NOT NULL,
code_commit TEXT NOT NULL,
raw_json TEXT NOT NULL
);
"""
)
for r in rows:
conn.execute(
"INSERT INTO benchmark_runs(model_id, benchmark, score, ts, "
"dataset_version, code_commit, raw_json) VALUES (?, ?, ?, ?, ?, ?, ?)",
(
r["model_id"],
r["benchmark"],
float(r["score"]),
int(r["ts"]),
r.get("dataset_version", "v1"),
r.get("code_commit", "abc"),
json.dumps(r.get("raw", {}), sort_keys=True),
),
)
conn.commit()
conn.close()
return db_path
def test_results_store_source_reads_failures(tmp_path):
db = _make_results_db(
tmp_path,
[
# passes threshold — excluded
{"model_id": "m", "benchmark": "b1", "score": 0.9, "ts": 2_000},
# below threshold — included
{
"model_id": "m",
"benchmark": "b1",
"score": 0.2,
"ts": 3_000,
"raw": {
"failed_tasks": [
{"task_id": "t1", "user_text": "please do x"}
]
},
},
# too old — excluded
{"model_id": "m", "benchmark": "b1", "score": 0.1, "ts": 500},
],
)
src = ResultsStoreFailureSource(db_path=db)
out = src.list_failures(since_ts_ms=1_000, threshold=0.5)
assert len(out) == 1
f = out[0]
assert f.run_id.startswith("run-")
assert f.score == 0.2
assert f.failed_tasks[0]["user_text"] == "please do x"
def test_results_store_source_orders_newest_first(tmp_path):
db = _make_results_db(
tmp_path,
[
{"model_id": "m", "benchmark": "b", "score": 0.1, "ts": 1_000},
{"model_id": "m", "benchmark": "b", "score": 0.2, "ts": 3_000},
{"model_id": "m", "benchmark": "b", "score": 0.3, "ts": 2_000},
],
)
src = ResultsStoreFailureSource(db_path=db)
out = src.list_failures(since_ts_ms=0, threshold=0.5)
assert [f.ts for f in out] == [3_000, 2_000, 1_000]
def test_results_store_source_skips_malformed_raw_json(tmp_path):
db_path = tmp_path / "results.db"
conn = sqlite3.connect(str(db_path))
conn.executescript(
"""
CREATE TABLE benchmark_runs (
id INTEGER PRIMARY KEY AUTOINCREMENT,
model_id TEXT NOT NULL,
benchmark TEXT NOT NULL,
score REAL NOT NULL,
ts INTEGER NOT NULL,
dataset_version TEXT NOT NULL,
code_commit TEXT NOT NULL,
raw_json TEXT NOT NULL
);
"""
)
# Bypass record_run's validation to inject a broken row.
conn.execute(
"INSERT INTO benchmark_runs(model_id, benchmark, score, ts, "
"dataset_version, code_commit, raw_json) VALUES (?, ?, ?, ?, ?, ?, ?)",
("m", "b", 0.2, 1_000, "v", "c", "not-json"),
)
conn.execute(
"INSERT INTO benchmark_runs(model_id, benchmark, score, ts, "
"dataset_version, code_commit, raw_json) VALUES (?, ?, ?, ?, ?, ?, ?)",
("m", "b", 0.3, 2_000, "v", "c", "{}"),
)
conn.commit()
conn.close()
src = ResultsStoreFailureSource(db_path=db_path)
out = src.list_failures(since_ts_ms=0, threshold=0.5)
# Malformed row is skipped (logged), but the good row is returned.
assert len(out) == 1
assert out[0].score == 0.3
# ─────────────────────────── AdaptiveSynth orchestration ────────────────────────────
class _StaticFailureSource:
"""In-memory source for orchestrator tests."""
def __init__(self, failures: list[BenchmarkFailure]):
self._failures = failures
def list_failures(
self, *, since_ts_ms: int, threshold: float
) -> list[BenchmarkFailure]:
return list(self._failures)
def test_adaptive_synth_tags_records(tmp_path):
failures = [
_failure(
run_id="run-aaa",
benchmark="bench-x",
failed_tasks=({"task_id": "t1", "user_text": "do alpha"},),
)
]
out_path = tmp_path / "out.jsonl"
synth = AdaptiveSynth(
failures=_StaticFailureSource(failures),
author=FixtureVariationAuthor(),
agent=EchoAgentClient(),
decider=HeuristicTurnDecider(),
max_turns=1, # one turn per variation
)
report = synth.run(
since_ts_ms=0,
variations_per_failure=3,
threshold=0.5,
out_path=out_path,
rng=random.Random(0),
)
assert report.failures_seen == 1
assert report.variations_authored == 3
assert report.sessions_written == 3
assert report.records_written == 3
assert report.errors == []
lines = [json.loads(line) for line in out_path.read_text().splitlines() if line]
assert len(lines) == 3
for i, rec in enumerate(lines):
assert rec["synth_kind"] == SYNTH_KIND
assert rec["derived_from"] == "run-aaa"
assert rec["failure_context"]["benchmark"] == "bench-x"
assert rec["failure_context"]["model_id"] == "eliza-1-2b"
assert rec["variation_index"] == i
# The simulator-level fields survive.
assert rec["turn_index"] == 0
assert "step_id" in rec
assert "messages" in rec
def test_adaptive_synth_runs_multi_turn_when_simulator_extends(tmp_path):
"""With a ScriptedTurnDecider that drives 2 turns, each variation
should land 2 records, all tagged with the same derived_from."""
failures = [_failure(run_id="run-multi")]
author = FixtureVariationAuthor()
# We need to know the project ids the author will pick so the
# scripted decider can address them. Author returns one project per
# variation; we configure the decider in two passes by using a
# passthrough author wrapper.
captured: list[Project] = []
class CapturingAuthor:
def __init__(self, inner):
self._inner = inner
def author_variations(self, *, failure, n, rng):
projects = self._inner.author_variations(failure=failure, n=n, rng=rng)
captured.extend(projects)
return projects
out_path = tmp_path / "out.jsonl"
# Build the simulator out-of-band so we can pre-author and then plan
# against the project ids.
rng = random.Random(0)
initial_projects = CapturingAuthor(author).author_variations(
failure=failures[0], n=2, rng=rng
)
plan = {p.id: ["follow up"] for p in initial_projects}
decider = ScriptedTurnDecider(plan)
# Now run the orchestrator with a fresh author returning the SAME projects.
class FixedAuthor:
def author_variations(self, *, failure, n, rng):
assert n == len(initial_projects)
return list(initial_projects)
synth = AdaptiveSynth(
failures=_StaticFailureSource(failures),
author=FixedAuthor(),
agent=EchoAgentClient(),
decider=decider,
max_turns=3,
)
report = synth.run(
since_ts_ms=0,
variations_per_failure=len(initial_projects),
threshold=0.5,
out_path=out_path,
rng=random.Random(0),
)
assert report.records_written == 4 # 2 projects × 2 turns each
assert report.sessions_written == 2
lines = [json.loads(line) for line in out_path.read_text().splitlines() if line]
for rec in lines:
assert rec["derived_from"] == "run-multi"
assert rec["synth_kind"] == SYNTH_KIND
def test_adaptive_synth_surfaces_author_exceptions(tmp_path):
"""Author exceptions land in the report, not stdout. No silent drops."""
class ExplodingAuthor:
def author_variations(self, *, failure, n, rng):
raise RuntimeError(f"boom for {failure.run_id}")
failures = [_failure(run_id=f"run-{i}") for i in range(3)]
out_path = tmp_path / "out.jsonl"
synth = AdaptiveSynth(
failures=_StaticFailureSource(failures),
author=ExplodingAuthor(),
agent=EchoAgentClient(),
decider=HeuristicTurnDecider(),
max_turns=1,
)
report = synth.run(
since_ts_ms=0,
variations_per_failure=2,
threshold=0.5,
out_path=out_path,
rng=random.Random(0),
)
assert report.failures_seen == 3
assert report.variations_authored == 0
assert report.records_written == 0
assert len(report.errors) == 3
stages = {e["stage"] for e in report.errors}
assert stages == {"author"}
assert all("boom for run-" in e["message"] for e in report.errors)
def test_adaptive_synth_surfaces_simulator_exceptions(tmp_path):
"""Simulator exceptions also land in the report."""
class ExplodingAgent(AgentClient):
def respond(self, *, project, history):
raise RuntimeError("agent down")
failures = [_failure(run_id="run-1")]
out_path = tmp_path / "out.jsonl"
synth = AdaptiveSynth(
failures=_StaticFailureSource(failures),
author=FixtureVariationAuthor(),
agent=ExplodingAgent(),
decider=HeuristicTurnDecider(),
max_turns=1,
)
report = synth.run(
since_ts_ms=0,
variations_per_failure=2,
threshold=0.5,
out_path=out_path,
rng=random.Random(0),
)
assert report.variations_authored == 2
assert report.records_written == 0
assert len(report.errors) == 2
assert all(e["stage"] == "simulate" for e in report.errors)
def test_adaptive_synth_surfaces_source_exception(tmp_path):
"""A source that errors at list time records a top-level error."""
class ExplodingSource:
def list_failures(self, *, since_ts_ms, threshold):
raise RuntimeError("db unreachable")
out_path = tmp_path / "out.jsonl"
synth = AdaptiveSynth(
failures=ExplodingSource(),
author=FixtureVariationAuthor(),
agent=EchoAgentClient(),
decider=HeuristicTurnDecider(),
max_turns=1,
)
report = synth.run(
since_ts_ms=0,
variations_per_failure=1,
threshold=0.5,
out_path=out_path,
rng=random.Random(0),
)
assert report.failures_seen == 0
assert len(report.errors) == 1
assert report.errors[0]["stage"] == "list_failures"
def test_adaptive_synth_rejects_n_zero(tmp_path):
synth = AdaptiveSynth(
failures=_StaticFailureSource([]),
author=FixtureVariationAuthor(),
agent=EchoAgentClient(),
decider=HeuristicTurnDecider(),
max_turns=1,
)
with pytest.raises(ValueError, match="variations_per_failure"):
synth.run(
since_ts_ms=0,
variations_per_failure=0,
threshold=0.5,
out_path=tmp_path / "out.jsonl",
)
def test_adaptive_synth_max_turns_zero_is_invalid():
with pytest.raises(ValueError, match="max_turns"):
AdaptiveSynth(
failures=_StaticFailureSource([]),
author=FixtureVariationAuthor(),
agent=EchoAgentClient(),
decider=HeuristicTurnDecider(),
max_turns=0,
)
# ─────────────────────────── --since parsing ────────────────────────────
def test_parse_since_date():
# 2025-01-01 00:00 UTC == 1_735_689_600 sec
assert _parse_since("2025-01-01") == 1_735_689_600 * 1000
def test_parse_since_iso_datetime():
# 2025-01-01T00:00:00Z
assert _parse_since("2025-01-01T00:00:00Z") == 1_735_689_600 * 1000
def test_parse_since_unix_ms_passthrough():
assert _parse_since("1700000000000") == 1_700_000_000_000
def test_parse_since_empty_raises():
with pytest.raises(ValueError):
_parse_since("")
def test_parse_since_garbage_raises():
with pytest.raises(ValueError):
_parse_since("not a date")
# ─────────────────────────── CLI smoke ────────────────────────────
def test_cli_smoke_with_json_source(tmp_path):
"""End-to-end: --source json + fixture author + echo agent → JSONL out."""
artifacts = tmp_path / "artifacts"
artifacts.mkdir()
_write_json(
artifacts / "run.json",
{
"id": "run-cli",
"model_id": "m",
"benchmark": "synth-bench",
"score": 0.1,
"ts": 1_700_000_000_000,
"failed_tasks": [{"task_id": "t1", "user_text": "do cli thing"}],
},
)
out_dir = tmp_path / "out"
rc = cli_main(
[
"--since", "2020-01-01",
"--variations", "2",
"--threshold", "0.5",
"--turns", "1",
"--output-dir", str(out_dir),
"--source", "json",
"--artifacts-dir", str(artifacts),
"--seed", "1",
]
)
assert rc == 0
out = out_dir / "adaptive_synth.jsonl"
report = out_dir / "adaptive_synth.report.json"
assert out.exists()
assert report.exists()
lines = [json.loads(line) for line in out.read_text().splitlines() if line]
assert len(lines) == 2 # one record per variation @ max_turns=1
for rec in lines:
assert rec["synth_kind"] == SYNTH_KIND
assert rec["derived_from"] == "run-cli"
assert "do cli thing" in rec["user_text"]
assert rec["failure_context"]["benchmark"] == "synth-bench"
rep_blob = json.loads(report.read_text())
assert rep_blob["failures_seen"] == 1
assert rep_blob["variations_authored"] == 2
assert rep_blob["records_written"] == 2
assert rep_blob["errors"] == []
assert rep_blob["variations_per_failure"] == 2
assert "elapsed_ms" in rep_blob
def test_cli_requires_artifacts_dir_with_json_source(tmp_path):
with pytest.raises(SystemExit, match="--artifacts-dir"):
cli_main(
[
"--since", "2020-01-01",
"--variations", "1",
"--output-dir", str(tmp_path / "out"),
"--source", "json",
]
)
def test_cli_rejects_bad_since(tmp_path):
rc = cli_main(
[
"--since", "garbage",
"--variations", "1",
"--output-dir", str(tmp_path / "out"),
"--source", "json",
"--artifacts-dir", str(tmp_path),
]
)
assert rc == 2
def test_cli_rejects_missing_artifacts(tmp_path):
rc = cli_main(
[
"--since", "2020-01-01",
"--variations", "1",
"--output-dir", str(tmp_path / "out"),
"--source", "json",
"--artifacts-dir", str(tmp_path / "does-not-exist"),
]
)
assert rc == 2
# ─────────────────────────── AdaptiveSynthReport bookkeeping ────────────────────────────
def test_report_to_dict_round_trips():
r = AdaptiveSynthReport()
r.failures_seen = 2
r.add_error(run_id="run-1", stage="author", message="boom")
d = r.to_dict()
assert d["failures_seen"] == 2
assert d["errors"] == [
{"run_id": "run-1", "stage": "author", "message": "boom"}
]
# JSON-serializable
json.dumps(d)