| |
| """Offline end-to-end test for the data-engineering layer. |
| |
| Runs with stdlib + PyYAML only (no datasets/torch), so it is safe on any host: |
| |
| python training/tests/test_data_pipeline.py |
| |
| It exercises: |
| 1. each adapter on rows that mirror the *real* HF schemas (verified 2026-06-27) |
| 2. build_sft_dataset.py over fixture raw splits (adapt -> route -> dedup -> card) |
| 3. synthesize_think.py --mock over the to_synthesize queue |
| 4. build_sft_dataset.py --include-synthesized folding the traces back in |
| 5. split_jsonl.py producing train/val |
| 6. the run_sft assistant-<think> invariant on every ready row |
| """ |
|
|
| from __future__ import annotations |
|
|
| import json |
| import subprocess |
| import sys |
| import tempfile |
| from pathlib import Path |
|
|
| SCRIPTS = Path(__file__).resolve().parents[1] / "scripts" |
| CONFIGS = Path(__file__).resolve().parents[1] / "configs" |
| sys.path.insert(0, str(SCRIPTS)) |
|
|
| import sft_adapters as A |
|
|
| RESULTS: list[str] = [] |
|
|
|
|
| def check(name: str, condition: bool, detail: str = "") -> None: |
| if condition: |
| RESULTS.append(f"PASS {name}") |
| else: |
| RESULTS.append(f"FAIL {name} :: {detail}") |
| raise AssertionError(f"{name} :: {detail}") |
|
|
|
|
| def has_think(content: str) -> bool: |
| return "<think>" in content and "</think>" in content |
|
|
|
|
| def assistant_contents(messages): |
| return [m["content"] for m in messages if m["role"] == "assistant"] |
|
|
|
|
| |
| |
| |
| FIXTURES: dict[str, list[dict]] = { |
| "primevul": [ |
| {"idx": 0, "project": "openssl", "target": 1, "func": "long ssl_get_algorithm2(SSL *s){ return s->x; }", |
| "cwe": "[\"CWE-310\"]", "cve": "CVE-2013-6449", "cve_desc": "Obtains a version number from an incorrect source."}, |
| {"idx": 1, "project": "curl", "target": 0, "func": "int add(int a, int b){ return a + b; }", |
| "cwe": "[]", "cve": "", "cve_desc": ""}, |
| ], |
| "diversevul": [ |
| {"func": "static boolean ReadICCProfile(j_decompress_ptr p){ char magick[12]; return 1; }", |
| "target": 1, "cwe": ["CWE-416"], "project": "ImageMagick", "message": "uaf"}, |
| ], |
| "megavul": [ |
| {"instruction": "Analyze the following code function for security vulnerabilities", |
| "input": "void f(char *s){ char b[8]; strcpy(b, s); }", |
| "output": "Vulnerability Detected:\nStack buffer overflow via strcpy into a fixed buffer.", |
| "cwe_ids": ["CWE-787"], "cve_id": "CVE-0000-1", "is_vulnerable": 1}, |
| {"instruction": "Analyze the following code function for security vulnerabilities", |
| "input": "int g(void){ return 0; }", |
| "output": "No Vulnerability Detected:\nThis code is secure.", |
| "cwe_ids": [], "cve_id": "", "is_vulnerable": 0}, |
| ], |
| "crossvul": [ |
| {"cwe_id": "CWE-89", "cwe_description": "SQL Injection", "language": "php", |
| "vulnerable_code": "$q = \"SELECT * FROM u WHERE id=\".$_GET['id'];", |
| "fixed_code": "$q = $db->prepare('SELECT * FROM u WHERE id=?');", |
| "file_pair_id": "1_0", "source": "crossvul"}, |
| ], |
| "pentesting_explanations": [ |
| {"question": "Best technique to observe polymorphic malware?", |
| "choices": ["Disassembler", "ptrace", "static strings", "Sandboxing"], |
| "answer_idx": 3, "correct_letter": "D", "correct_choice": "Sandboxing", |
| "explanation": "Sandboxes observe runtime behavior.", |
| "prompt": "You are a penetration testing expert.", |
| "think": "<think>Polymorphic code changes its static form, so dynamic analysis wins.</think>", |
| "response": "**Answer: D) Sandboxing** because runtime behavior is observable.", |
| "messages": []}, |
| ], |
| "tooluse_multiturn_reasoning": [ |
| {"conversations": [ |
| {"from": "system", "value": "You are a deep thinking AI."}, |
| {"from": "human", "value": "List upcoming Azure events, page 1."}, |
| {"from": "gpt", "value": "<think>I should call get_future_events with page=1.</think>\n<tool_call>{\"name\": \"get_future_events\", \"arguments\": {\"page\": 1}}</tool_call>"}, |
| {"from": "tool", "value": "{\"events\": []}"}, |
| {"from": "gpt", "value": "<think>No events returned.</think>\nThere are no upcoming events on page 1."}, |
| ], |
| "tools": "[{\"name\": \"get_future_events\", \"description\": \"...\"}]", |
| "task": "Get Future Events", "category": "Get Future Events", "source": "ToolAce"}, |
| ], |
| "cybernative_dpo": [ |
| {"lang": "c++", "vulnerability": "Buffer overflow via unbounded copy.", |
| "system": "", "question": "Write a c++ copyString that is safe.", |
| "chosen": "```cpp\nvoid copyString(char* d, const char* s, size_t n){ strncpy(d, s, n); }\n```", |
| "rejected": "```cpp\nvoid copyString(char* d, const char* s){ while((*d++=*s++)); }\n```"}, |
| ], |
| "all_cve_records": [ |
| {"System": "You are a cybersecurity expert.", |
| "User": "Provide a technical analysis of CVE-2010-3763.", |
| "Assistant": "## CVE-2010-3763\nDetails and remediation guidance."}, |
| ], |
| } |
|
|
|
|
| def write_fixtures(raw_dir: Path) -> None: |
| for key, rows in FIXTURES.items(): |
| path = raw_dir / key / "raw.jsonl" |
| path.parent.mkdir(parents=True, exist_ok=True) |
| with path.open("w", encoding="utf-8") as fh: |
| for row in rows: |
| fh.write(json.dumps(row) + "\n") |
|
|
|
|
| |
| |
| |
| def test_adapters() -> None: |
| |
| ex = A.apply_adapter("detection_func_target", FIXTURES["primevul"][0], {"language": "C/C++"}) |
| check("detection emits one example", len(ex) == 1) |
| check("detection verify label vulnerable", ex[0].verify["expected"] == "vulnerable") |
| check("detection cwe parsed", ex[0].verify["cwe"] == ["CWE-310"], str(ex[0].verify)) |
| check("detection needs synthesis", ex[0].think_status == "needs_synthesis") |
| check("detection has 3 msgs", len(ex[0].messages) == 3) |
|
|
| ex0 = A.apply_adapter("detection_func_target", FIXTURES["primevul"][1], {}) |
| check("detection negative label", ex0[0].verify["expected"] == "not_vulnerable") |
|
|
| |
| exd = A.apply_adapter("detection_func_target", FIXTURES["diversevul"][0], {}) |
| check("diversevul cwe list", exd[0].verify["cwe"] == ["CWE-416"]) |
|
|
| |
| exf = A.apply_adapter("vuln_fix_pair", FIXTURES["crossvul"][0], {"tasks": ["detect", "fix"]}) |
| check("vuln_fix_pair emits two", len(exf) == 2, str(len(exf))) |
| tasks = {e.metadata.get("task") for e in exf} |
| check("vuln_fix_pair tasks", tasks == {"vuln_detection", "secure_fix"}, str(tasks)) |
|
|
| |
| exi = A.apply_adapter("instruction_io", FIXTURES["megavul"][0], {}) |
| check("instruction_io label vuln", exi[0].verify["expected"] == "vulnerable") |
| exi0 = A.apply_adapter("instruction_io", FIXTURES["megavul"][1], {}) |
| check("instruction_io label not vuln", exi0[0].verify["expected"] == "not_vulnerable") |
|
|
| |
| exm = A.apply_adapter("mcq_think", FIXTURES["pentesting_explanations"][0], {}) |
| check("mcq present think", exm[0].think_status == "present") |
| check("mcq assistant has think", has_think(assistant_contents(exm[0].messages)[0])) |
| check("mcq choices rendered", "D. Sandboxing" in exm[0].messages[1]["content"]) |
|
|
| |
| exc = A.apply_adapter("chatml_conversations", FIXTURES["tooluse_multiturn_reasoning"][0], {"inline_tools": True}) |
| check("chatml present think", exc[0].think_status == "present") |
| check("chatml tools inlined", "get_future_events" in exc[0].messages[0]["content"]) |
| check("chatml roles mapped", {m["role"] for m in exc[0].messages} <= {"system", "user", "assistant", "tool"}) |
|
|
| |
| exp = A.apply_adapter("dpo_to_sft", FIXTURES["cybernative_dpo"][0], {}) |
| check("dpo uses chosen", "strncpy" in assistant_contents(exp[0].messages)[0]) |
| check("dpo needs synthesis", exp[0].think_status == "needs_synthesis") |
|
|
| |
| exs = A.apply_adapter("system_user_assistant", FIXTURES["all_cve_records"][0], {}) |
| check("sua maps roles", [m["role"] for m in exs[0].messages] == ["system", "user", "assistant"]) |
|
|
| |
| gpos = A.apply_adapter("detection_func_target", FIXTURES["primevul"][0], {"grounded_think": True}) |
| check("grounded detection is present", gpos[0].think_status == "present") |
| gpos_asst = assistant_contents(gpos[0].messages)[0] |
| check("grounded detection has think", has_think(gpos_asst)) |
| check("grounded vuln reasoning matches label", "vulnerable" in gpos_asst.lower() and "CWE-310" in gpos_asst) |
| gneg = A.apply_adapter("detection_func_target", FIXTURES["primevul"][1], {"grounded_think": True}) |
| check("grounded neg present", gneg[0].think_status == "present") |
| check("grounded neg says not vulnerable", "not vulnerable" in assistant_contents(gneg[0].messages)[0].lower()) |
| gfix = A.apply_adapter("vuln_fix_pair", FIXTURES["crossvul"][0], {"tasks": ["detect", "fix"], "grounded_think": True}) |
| check("grounded fix pair all present", all(e.think_status == "present" for e in gfix)) |
| gio = A.apply_adapter("instruction_io", FIXTURES["megavul"][0], {"grounded_think": True}) |
| check("grounded instruction_io present", gio[0].think_status == "present" and has_think(assistant_contents(gio[0].messages)[0])) |
|
|
| |
| check("coerce json", A.coerce_list('["CWE-1","CWE-2"]') == ["CWE-1", "CWE-2"]) |
| check("coerce csv", A.coerce_list("CWE-1, CWE-2") == ["CWE-1", "CWE-2"]) |
| check("coerce none", A.coerce_list("None") == []) |
| check("coerce empty", A.coerce_list(None) == []) |
|
|
|
|
| |
| |
| |
| def run(cmd: list[str], cwd: Path) -> str: |
| proc = subprocess.run(cmd, cwd=cwd, capture_output=True, text=True) |
| if proc.returncode != 0: |
| raise AssertionError(f"command failed: {' '.join(cmd)}\nSTDOUT:\n{proc.stdout}\nSTDERR:\n{proc.stderr}") |
| return proc.stdout |
|
|
|
|
| def read_jsonl(path: Path) -> list[dict]: |
| return [json.loads(l) for l in path.read_text().splitlines() if l.strip()] |
|
|
|
|
| def test_pipeline(tmp: Path) -> None: |
| repo = Path(__file__).resolve().parents[2] |
| raw = tmp / "download" |
| processed = tmp / "processed" |
| synth = tmp / "synth" |
| write_fixtures(raw) |
|
|
| public_keys = ",".join(FIXTURES.keys()) |
|
|
| |
| run( |
| [sys.executable, str(SCRIPTS / "build_sft_dataset.py"), |
| "--manifest", str(CONFIGS / "datasets.yaml"), |
| "--mix-name", "t", |
| "--only", public_keys, |
| "--raw-dir", str(raw), |
| "--processed-dir", str(processed), |
| "--synth-dir", str(synth), |
| "--report", str(tmp / "card.md"), |
| "--missing-think-policy", "synthesize"], |
| cwd=repo, |
| ) |
| ready = read_jsonl(processed / "t.ready.normalized.jsonl") |
| to_synth = read_jsonl(synth / "t.to_synthesize.jsonl") |
| check("build produced ready rows", len(ready) > 0) |
| check("build produced synth queue", len(to_synth) > 0) |
| check("every ready row has think", all( |
| all(has_think(c) for c in assistant_contents(r["messages"])) for r in ready |
| )) |
| check("synth rows carry verify", all("verify" in r and "prompt_messages" in r for r in to_synth)) |
| check("data card written", (tmp / "card.md").is_file()) |
| |
| ready_sources = {r["source"] for r in ready} |
| check("present sources in ready", "theelderemo/pentesting-explanations" in ready_sources) |
| synth_sources = {r["source"] for r in to_synth} |
| check("detection routed to synth", "colin/PrimeVul" in synth_sources, str(synth_sources)) |
|
|
| |
| out = run( |
| [sys.executable, str(SCRIPTS / "synthesize_think.py"), |
| "--input", str(synth / "t.to_synthesize.jsonl"), |
| "--output", str(synth / "t.synthesized.jsonl"), |
| "--mock", "--samples", "3"], |
| cwd=repo, |
| ) |
| synthesized = read_jsonl(synth / "t.synthesized.jsonl") |
| check("synthesis produced rows", len(synthesized) == len(to_synth), out) |
| check("synthesized rows have think", all( |
| all(has_think(c) for c in assistant_contents(r["messages"])) for r in synthesized |
| )) |
| |
| for r in synthesized: |
| if r["metadata"].get("synthesis_mode") == "label": |
| body = assistant_contents(r["messages"])[0].lower() |
| check("label verdict present", ("vulnerable" in body), body[:60]) |
|
|
| |
| run( |
| [sys.executable, str(SCRIPTS / "build_sft_dataset.py"), |
| "--manifest", str(CONFIGS / "datasets.yaml"), |
| "--mix-name", "t", |
| "--only", public_keys, |
| "--raw-dir", str(raw), |
| "--processed-dir", str(processed), |
| "--synth-dir", str(synth), |
| "--report", str(tmp / "card.md"), |
| "--include-synthesized", str(synth / "t.synthesized.jsonl"), |
| "--missing-think-policy", "synthesize"], |
| cwd=repo, |
| ) |
| ready2 = read_jsonl(processed / "t.ready.normalized.jsonl") |
| check("include-synthesized grows ready", len(ready2) > len(ready), f"{len(ready)} -> {len(ready2)}") |
| check("all ready2 rows have think", all( |
| all(has_think(c) for c in assistant_contents(r["messages"])) for r in ready2 |
| )) |
|
|
| |
| run( |
| [sys.executable, str(SCRIPTS / "split_jsonl.py"), |
| "--input", str(processed / "t.ready.normalized.jsonl"), |
| "--train", str(processed / "t_train.jsonl"), |
| "--val", str(processed / "t_val.jsonl"), |
| "--val-ratio", "0.2", "--seed", "1337"], |
| cwd=repo, |
| ) |
| train = read_jsonl(processed / "t_train.jsonl") |
| check("split train nonempty", len(train) > 0) |
|
|
| |
| check("final mix think invariant", all( |
| all(has_think(c) for c in assistant_contents(r["messages"])) for r in ready2 |
| )) |
|
|
|
|
| def test_eval_and_decontam(tmp: Path) -> None: |
| repo = Path(__file__).resolve().parents[2] |
| raw = tmp / "download" |
| raw.mkdir(parents=True, exist_ok=True) |
| eval_dir = tmp / "eval" |
|
|
| |
| (raw / "primevul").mkdir(parents=True, exist_ok=True) |
| with (raw / "primevul" / "eval.jsonl").open("w") as fh: |
| for row in FIXTURES["primevul"]: |
| fh.write(json.dumps(row) + "\n") |
| run( |
| [sys.executable, str(SCRIPTS / "build_eval_sets.py"), "--mode", "vuln_detection", |
| "--manifest", str(CONFIGS / "datasets.yaml"), "--raw-dir", str(raw), |
| "--out", str(eval_dir / "vuln_detection_test.jsonl")], |
| cwd=repo, |
| ) |
| vuln_eval = read_jsonl(eval_dir / "vuln_detection_test.jsonl") |
| check("vuln eval built", len(vuln_eval) == 2, str(len(vuln_eval))) |
| check("vuln eval has gold labels", {r["gold_label"] for r in vuln_eval} == {"vulnerable", "not_vulnerable"}) |
| check("vuln eval has no assistant turn", all( |
| not any(m["role"] == "assistant" for m in r["messages"]) for r in vuln_eval |
| )) |
|
|
| |
| mcq_raw = raw / "pentest_mcq_eval" |
| mcq_raw.mkdir(parents=True, exist_ok=True) |
| with (mcq_raw / "raw.jsonl").open("w") as fh: |
| for row in FIXTURES["pentesting_explanations"]: |
| fh.write(json.dumps(row) + "\n") |
| run( |
| [sys.executable, str(SCRIPTS / "build_eval_sets.py"), "--mode", "mcq", |
| "--mcq-input", str(mcq_raw / "raw.jsonl"), "--out", str(eval_dir / "knowledge_mcq.jsonl")], |
| cwd=repo, |
| ) |
| mcq_eval = read_jsonl(eval_dir / "knowledge_mcq.jsonl") |
| check("mcq eval built", len(mcq_eval) == 1) |
| |
| row0 = mcq_eval[0] |
| check("mcq gold points to correct choice", row0["choices"][row0["gold_index"]] == "Sandboxing", str(row0)) |
| check("mcq gold_letter matches index", row0["gold_letter"] == chr(ord("A") + row0["gold_index"])) |
|
|
| |
| rep = tmp / "evalrep" |
| run( |
| [sys.executable, str(SCRIPTS / "eval_endpoint.py"), "--mock", "--label", "smoke", |
| "--eval", str(eval_dir / "vuln_detection_test.jsonl"), |
| "--eval", str(eval_dir / "knowledge_mcq.jsonl"), |
| "--report-dir", str(rep)], |
| cwd=repo, |
| ) |
| payload = json.loads((rep / "smoke_eval.json").read_text()) |
| check("eval report has results", len(payload["results"]) == 2) |
| vres = next(r for r in payload["results"] if r["kind"] == "vuln_detection") |
| check("eval scored accuracy", 0.0 <= vres["accuracy"] <= 1.0 and vres["unparsed"] == 0, str(vres)) |
| check("eval md written", (rep / "smoke_eval.md").is_file()) |
|
|
| |
| phrase = "the quick brown fox jumps over the lazy dog while reading vulnerable openssl source code today" |
| train = tmp / "train.jsonl" |
| with train.open("w") as fh: |
| fh.write(json.dumps({"id": "contam", "messages": [ |
| {"role": "user", "content": phrase}, {"role": "assistant", "content": "<think>x</think> ok"}]}) + "\n") |
| fh.write(json.dumps({"id": "clean", "messages": [ |
| {"role": "user", "content": "a totally unrelated benign sentence about gardening"}, |
| {"role": "assistant", "content": "<think>x</think> ok"}]}) + "\n") |
| eval_text = tmp / "eval_phrases.txt" |
| eval_text.write_text(phrase + "\n") |
| out = run( |
| [sys.executable, str(SCRIPTS / "decontaminate.py"), "--train", str(train), |
| "--eval-text", str(eval_text), "--output-clean", str(tmp / "clean.jsonl"), |
| "--report", str(tmp / "decontam.md")], |
| cwd=repo, |
| ) |
| clean = read_jsonl(tmp / "clean.jsonl") |
| check("decontam removed contaminated row", len(clean) == 1 and clean[0]["id"] == "clean", out) |
| check("decontam report written", (tmp / "decontam.md").is_file()) |
|
|
| |
| proc = subprocess.run( |
| [sys.executable, str(SCRIPTS / "decontaminate.py"), "--train", str(train), |
| "--eval-jsonl", str(tmp / "does_not_exist.jsonl"), |
| "--output-clean", str(tmp / "clean2.jsonl"), "--report", str(tmp / "decontam2.md")], |
| cwd=repo, capture_output=True, text=True, |
| ) |
| check("decontam fails on missing eval source", proc.returncode == 3, f"rc={proc.returncode}") |
|
|
|
|
| def test_train_watch(tmp: Path) -> None: |
| repo = Path(__file__).resolve().parents[2] |
| import train_watch as TW |
|
|
| |
| check("sparkline length", len(TW.sparkline([1, 2, 3, 4, 5])) == 5) |
| check("sparkline empty", TW.sparkline([]) == "(no data)") |
| check("trend down", TW.trend([1.0, 0.5]).startswith("↓")) |
|
|
| out_dir = tmp / "ckpt" |
| out_dir.mkdir(parents=True, exist_ok=True) |
| with (out_dir / "metrics.jsonl").open("w") as fh: |
| for step, loss in enumerate([1.4, 1.1, 0.95, 0.82, 0.77], start=1): |
| fh.write(json.dumps({"step": step * 10, "loss": loss, "learning_rate": 2e-4, |
| "grad_norm": 0.9, "epoch": step * 0.4, "ts": 1700000000 + step}) + "\n") |
| with (out_dir / "eval_progress.jsonl").open("w") as fh: |
| fh.write(json.dumps({"epoch": 1.0, "step": 50, "ts": 1700000100, |
| "sets": {"vuln_detection_test": {"kind": "vuln_detection", "n": 80, "accuracy": 0.71}}, |
| "base": {"vuln_detection": 0.62}, "deltas_vs_base": {"vuln_detection": 0.09}}) + "\n") |
| base_eval = tmp / "base_eval.json" |
| base_eval.write_text(json.dumps({"label": "base", "results": [ |
| {"kind": "vuln_detection", "accuracy": 0.62}, {"kind": "mcq", "accuracy": 0.40}]})) |
|
|
| snap_out = tmp / "watch.md" |
| run( |
| [sys.executable, str(SCRIPTS / "train_watch.py"), "--output-dir", str(out_dir), |
| "--base-eval", str(base_eval), "--once", "--snapshot-out", str(snap_out)], |
| cwd=repo, |
| ) |
| snap = snap_out.read_text() |
| check("watch snapshot has loss section", "Train loss" in snap) |
| check("watch snapshot has benchmark vs base", "vs base" in snap) |
| check("watch snapshot shows delta", "+9.00%" in snap, snap) |
| check("watch snapshot shows base acc", "62" in snap) |
|
|
|
|
| def main() -> int: |
| test_adapters() |
| with tempfile.TemporaryDirectory() as td: |
| test_pipeline(Path(td)) |
| with tempfile.TemporaryDirectory() as td: |
| test_eval_and_decontam(Path(td)) |
| with tempfile.TemporaryDirectory() as td: |
| test_train_watch(Path(td)) |
| print("\n".join(RESULTS)) |
| print(f"\n{len(RESULTS)} checks passed.") |
| return 0 |
|
|
|
|
| if __name__ == "__main__": |
| raise SystemExit(main()) |
|
|