| import importlib.util |
| import json |
| from pathlib import Path |
|
|
| import pytest |
|
|
|
|
| def _row(row_id: str, task_type: str, category: str, repair_scope: str | None = None) -> dict: |
| metadata = {"task_type": task_type} |
| if repair_scope: |
| metadata["repair_scope"] = repair_scope |
| return { |
| "case_id": row_id, |
| "uuid": row_id, |
| "category": category, |
| "messages": [ |
| {"role": "user", "content": f"prompt {row_id}"}, |
| {"role": "assistant", "content": '{"protocol_urgency":"routine"}'}, |
| ], |
| "metadata": metadata, |
| } |
|
|
|
|
| def _write_jsonl(path: Path, rows: list[dict]) -> None: |
| path.write_text("".join(json.dumps(row) + "\n" for row in rows), encoding="utf-8") |
|
|
|
|
| def _load_modal_module(): |
| module_path = Path(__file__).resolve().parents[1] / "modal" / "finetune_figment_nemotron.py" |
| spec = importlib.util.spec_from_file_location("figment_modal_finetune", module_path) |
| assert spec is not None |
| module = importlib.util.module_from_spec(spec) |
| assert spec.loader is not None |
| spec.loader.exec_module(module) |
| return module |
|
|
|
|
| def test_prepare_modal_dataset_stratifies_and_preserves_rows(tmp_path): |
| from scripts.prepare_modal_finetune_dataset import prepare_dataset |
|
|
| rows = [] |
| rows.extend(_row(f"full-{index}", "navigator_full", "missing_observation_cues") for index in range(8)) |
| rows.extend(_row(f"repair-schema-{index}", "focused_repair", "focused_repair:schema", "schema") for index in range(6)) |
| rows.extend( |
| _row( |
| f"repair-sbar-{index}", |
| "focused_repair", |
| "focused_repair:handoff_note_sbar", |
| "handoff_note_sbar", |
| ) |
| for index in range(6) |
| ) |
| dataset = tmp_path / "input.jsonl" |
| output_dir = tmp_path / "prepared" |
| _write_jsonl(dataset, rows) |
|
|
| manifest = prepare_dataset( |
| dataset_path=dataset, |
| output_dir=output_dir, |
| dataset_version="figment_sft_test", |
| validation_fraction=0.2, |
| seed="unit-test", |
| min_validation_group_size=5, |
| ) |
|
|
| train_rows = [json.loads(line) for line in (output_dir / "train.jsonl").read_text().splitlines()] |
| validation_rows = [json.loads(line) for line in (output_dir / "validation.jsonl").read_text().splitlines()] |
| train_ids = {row["uuid"] for row in train_rows} |
| validation_ids = {row["uuid"] for row in validation_rows} |
|
|
| assert train_ids.isdisjoint(validation_ids) |
| assert train_ids | validation_ids == {row["uuid"] for row in rows} |
| assert manifest["row_count"] == 20 |
| assert manifest["train_count"] + manifest["validation_count"] == 20 |
| assert manifest["validation_group_counts"]["navigator_full:missing_observation_cues"] >= 1 |
| assert manifest["validation_group_counts"]["focused_repair:schema"] >= 1 |
| assert manifest["validation_group_counts"]["focused_repair:handoff_note_sbar"] >= 1 |
|
|
|
|
| def test_prepare_modal_dataset_rejects_rows_outside_chat_shape(tmp_path): |
| from scripts.prepare_modal_finetune_dataset import DatasetPrepError |
| from scripts.prepare_modal_finetune_dataset import prepare_dataset |
|
|
| dataset = tmp_path / "bad.jsonl" |
| bad_row = _row("bad-1", "navigator_full", "missing_observation_cues") |
| bad_row["messages"] = [{"role": "user", "content": "prompt only"}] |
| _write_jsonl(dataset, [bad_row]) |
|
|
| with pytest.raises(DatasetPrepError, match="expected user/assistant messages"): |
| prepare_dataset( |
| dataset_path=dataset, |
| output_dir=tmp_path / "prepared", |
| dataset_version="figment_sft_test", |
| ) |
|
|
|
|
| def test_modal_smoke_config_is_small_and_namespaced(): |
| module = _load_modal_module() |
|
|
| config = module.build_train_config( |
| dataset_version="figment_sft_v1", |
| output_name="unit", |
| smoke=True, |
| max_steps=100, |
| max_seq_length=12288, |
| ) |
| paths = module.dataset_volume_paths("figment_sft_v1") |
|
|
| assert config["max_steps"] == 5 |
| assert config["max_seq_length"] == 2048 |
| assert config["output_dir"].endswith("/figment_sft_v1/unit-smoke") |
| assert paths["train"].endswith("/figment_sft_v1/train.jsonl") |
| assert paths["validation"].endswith("/figment_sft_v1/validation.jsonl") |
|
|
|
|
| def test_modal_default_config_matches_first_run_plan(): |
| module = _load_modal_module() |
|
|
| config = module.build_train_config(dataset_version="figment_sft_v1", output_name="pilot") |
|
|
| assert config["learning_rate"] == 1e-4 |
| assert config["max_steps"] == 40 |
| assert config["max_seq_length"] == 16384 |
| assert config["gradient_accumulation_steps"] == 8 |
| assert config["validation_steps"] == 25 |
| assert config["save_steps"] == 40 |
|
|
|
|
| def test_modal_v4_config_can_use_lower_lr_and_lora_controls(): |
| module = _load_modal_module() |
|
|
| config = module.build_train_config( |
| dataset_version="figment_sft_v4", |
| output_name="figment-sft-v4-lora", |
| max_steps=900, |
| learning_rate=2e-5, |
| lora_r=16, |
| lora_alpha=32, |
| lora_dropout=0.05, |
| gradient_accumulation_steps=8, |
| validation_steps=50, |
| save_steps=100, |
| ) |
|
|
| assert config["dataset_version"] == "figment_sft_v4" |
| assert config["learning_rate"] == 2e-5 |
| assert config["lora_r"] == 16 |
| assert config["lora_alpha"] == 32 |
| assert config["lora_dropout"] == 0.05 |
| assert config["gradient_accumulation_steps"] == 8 |
| assert config["validation_steps"] == 50 |
| assert config["save_steps"] == 100 |
|
|
|
|
| def test_modal_v5_config_can_resume_from_v4_adapter(): |
| module = _load_modal_module() |
|
|
| config = module.build_train_config( |
| dataset_version="figment_sft_v5", |
| output_name="figment-sft-v5-lora", |
| resume_adapter_name="figment-sft-v4-lora", |
| resume_adapter_dataset_version="figment_sft_v4", |
| ) |
|
|
| assert config["resume_adapter_name"] == "figment-sft-v4-lora" |
| assert config["resume_adapter_dataset_version"] == "figment_sft_v4" |
| assert config["resume_adapter_dir"] == "/checkpoints/figment_sft_v4/figment-sft-v4-lora" |
| assert config["output_dir"] == "/checkpoints/figment_sft_v5/figment-sft-v5-lora" |
|
|
|
|
| def test_modal_entrypoint_exposes_v4_training_knobs(): |
| source = (Path(__file__).resolve().parents[1] / "modal" / "finetune_figment_nemotron.py").read_text( |
| encoding="utf-8" |
| ) |
|
|
| for parameter in ( |
| "learning_rate", |
| "lora_r", |
| "lora_alpha", |
| "lora_dropout", |
| "gradient_accumulation_steps", |
| "validation_steps", |
| "save_steps", |
| "resume_adapter_name", |
| "resume_adapter_dataset_version", |
| ): |
| assert f"{parameter}:" in source |
| assert f"{parameter}={parameter}" in source or f'"{parameter}": {parameter}' in source |
|
|
|
|
| def test_modal_merge_config_points_at_checkpoint_volume(): |
| module = _load_modal_module() |
|
|
| config = module.build_merge_config( |
| dataset_version="figment_sft_v1", |
| adapter_name="pilot-20260608", |
| ) |
|
|
| assert config["adapter_dir"] == "/checkpoints/figment_sft_v1/pilot-20260608" |
| assert config["output_dir"] == "/checkpoints/figment_sft_v1/pilot-20260608-merged-bf16" |
| assert config["output_name"] == "pilot-20260608-merged-bf16" |
|
|