"""Tests for v0.39.0 Part A — PiSSA init + init_strategy field on LoraConfig.""" from __future__ import annotations import pytest from pydantic import ValidationError from soup_cli.config.schema import LoraConfig from soup_cli.utils.peft_builder import build_peft_config class TestInitStrategyField: def test_default_is_random(self): cfg = LoraConfig() assert cfg.init_strategy == "random" def test_accepts_pissa(self): cfg = LoraConfig(init_strategy="pissa") assert cfg.init_strategy == "pissa" def test_accepts_olora(self): cfg = LoraConfig(init_strategy="olora") assert cfg.init_strategy == "olora" def test_rejects_unknown(self): # loftq added in v0.41.0 — use a truly unknown sentinel. with pytest.raises(ValidationError): LoraConfig(init_strategy="bogus_strategy") def test_rejects_non_string(self): with pytest.raises(ValidationError): LoraConfig(init_strategy=42) class TestBackcompatNoMutation: def test_backcompat_does_not_mutate_input_dict(self): """v0.39.0 security fix — _backcompat_align_olora must copy.""" original = {"use_olora": True} LoraConfig(**original) assert "init_strategy" not in original, ( "Validator mutated caller's dict in-place" ) class TestInitStrategyOloraBackcompat: def test_use_olora_true_alone_still_works(self): cfg = LoraConfig(use_olora=True) assert cfg.use_olora is True assert cfg.init_strategy == "olora" # auto-aligned for back-compat def test_init_strategy_olora_alone_works(self): cfg = LoraConfig(init_strategy="olora") assert cfg.init_strategy == "olora" # use_olora may be True or False; effect is via init_strategy def test_both_set_consistent_ok(self): cfg = LoraConfig(use_olora=True, init_strategy="olora") assert cfg.init_strategy == "olora" def test_use_olora_true_with_pissa_rejected(self): with pytest.raises(ValidationError, match="init_strategy"): LoraConfig(use_olora=True, init_strategy="pissa") def test_use_olora_true_with_random_rejected(self): # explicit conflict — user said both random and olora; loud-fail with pytest.raises(ValidationError, match="init_strategy"): LoraConfig(use_olora=True, init_strategy="random") class TestInitStrategyMutualExclusion: def test_pissa_with_dora_rejected(self): # PiSSA + DoRA isn't supported (PEFT init_lora_weights conflicts with use_dora init) with pytest.raises(ValidationError, match="init_strategy"): LoraConfig(use_dora=True, init_strategy="pissa") def test_pissa_with_vera_rejected(self): # VeRA doesn't have init_lora_weights — PiSSA meaningless with pytest.raises(ValidationError, match="init_strategy"): LoraConfig(use_vera=True, init_strategy="pissa") def test_pissa_with_rslora_ok(self): # rsLoRA only changes scaling factor — orthogonal to init cfg = LoraConfig(use_rslora=True, init_strategy="pissa") assert cfg.init_strategy == "pissa" assert cfg.use_rslora is True class TestPeftBuilderInitStrategy: def test_random_does_not_set_init_lora_weights(self): cfg = LoraConfig(init_strategy="random") spec = build_peft_config(cfg, target_modules="auto", task_type="CAUSAL_LM") assert spec["peft_cls"] == "LoraConfig" assert "init_lora_weights" not in spec["init_kwargs"] def test_pissa_sets_init_lora_weights(self): cfg = LoraConfig(init_strategy="pissa") spec = build_peft_config(cfg, target_modules="auto", task_type="CAUSAL_LM") assert spec["init_kwargs"]["init_lora_weights"] == "pissa" def test_olora_via_init_strategy(self): cfg = LoraConfig(init_strategy="olora") spec = build_peft_config(cfg, target_modules="auto", task_type="CAUSAL_LM") assert spec["init_kwargs"]["init_lora_weights"] == "olora" def test_olora_via_use_olora_legacy(self): cfg = LoraConfig(use_olora=True) spec = build_peft_config(cfg, target_modules="auto", task_type="CAUSAL_LM") assert spec["init_kwargs"]["init_lora_weights"] == "olora" def test_vera_ignores_init_strategy_random(self): cfg = LoraConfig(use_vera=True, init_strategy="random") spec = build_peft_config(cfg, target_modules="auto", task_type="CAUSAL_LM") assert spec["peft_cls"] == "VeraConfig" assert "init_lora_weights" not in spec["init_kwargs"] class TestInstantiatePeftConfig: def test_instantiate_lora_config(self): from soup_cli.utils.peft_builder import instantiate_peft_config cfg = LoraConfig(init_strategy="pissa") spec = build_peft_config(cfg, target_modules=["q_proj", "v_proj"], task_type="CAUSAL_LM") result = instantiate_peft_config(spec) # Verify it really is a peft.LoraConfig with the right kwargs import peft assert isinstance(result, peft.LoraConfig) assert result.r == 64 assert result.init_lora_weights == "pissa" def test_instantiate_with_rank_pattern(self): from soup_cli.utils.peft_builder import instantiate_peft_config cfg = LoraConfig(rank_pattern={"q_proj": 8}) spec = build_peft_config(cfg, target_modules=["q_proj"], task_type="CAUSAL_LM") result = instantiate_peft_config(spec) assert result.rank_pattern == {"q_proj": 8} class TestInitStrategyYamlRoundtripV0401Regression: """v0.40.1 Part B — QA found bogus init_strategy via YAML occasionally bypassed validation; assert it always errors at full-config load.""" def test_bogus_init_strategy_via_full_yaml_rejected(self): from soup_cli.config.loader import load_config_from_string yaml_text = """ base: HuggingFaceTB/SmolLM2-135M-Instruct task: sft data: train: data.jsonl training: epochs: 1 lr: 0.0002 output: ./out lora: r: 8 init_strategy: bogus """ with pytest.raises((ValidationError, ValueError), match="init_strategy"): load_config_from_string(yaml_text)