Files
turboquant_plus/refract/tests/test_command_construction.py
T
TheTomandClaude Opus 4.7 2d8a2472f7 REFRACT v0.1.3: defensive patch round (tests + fail-loud + honest docs)
v0.1.3 ships no new functional axes — only the test coverage, fail-loud
guards, and corpus-identity machinery needed to stop the v0.1.x
patch-round-after-matrix-run cycle.

Code fixes:
- axes/gtm.py: tokenizer-failure fallback now RAISES instead of silently
  using whitespace (was producing wrong-unit scores with no detection).
- axes/gtm.py: GTM score now normalizes by mean_cand_length instead of
  n_predict (fixes gemma-31B 2.87× retokenize inflation that re-clipped
  the score to 100 in v0.1.2).
- cli.py: --measure-floor additionally asserts mean_prefix == mean_cand
  on ref-vs-ref greedy (KLD ref-vs-ref is bit-exact zero on Metal so the
  composite floor passes even when GTM is broken).
- runner.py: _TOPP_RE regex now tolerates 'Same top p' (space) form
  (test surfaced that v0.1.2 only matched the dashed form).
- runner.py: new corpus_identity / sidecar machinery; KLDResult includes
  corpus path/size/SHA; refuses to score against a base built from a
  different corpus.

New tests (30 across 6 files; all pass in <0.05s, no subprocess):
- test_strip_noise.py: 5 — pins runner._strip_noise (banner / backspace /
  block-char regressions from v0.1.1).
- test_command_construction.py: 4 — pins --single-turn presence,
  --no-conversation absence, --kl-divergence-base, errors='replace'.
- test_tokenize_to_ids.py: 5 — parser + raise-on-error contract.
- test_kld_regex.py: 6 — perplexity output regexes incl. gemma
  Same-top-p missing case (this test caught the regex bug).
- test_report_json_layout.py: 5 — pins schema + composite-is-scalar.
- test_corpus_identity.py: 5 — sidecar machinery.
- test_validation.py: +4 GTM assertions (full_match_rate < 1.0,
  mean_prefix > 0, ≥3 distinct refs, ratio == 1.0 on ref-vs-ref).

Total: 14 → 44 unit tests + 1 strengthened integration test.

Docs:
- LIMITATIONS.md: NEW — documents the limitations we ship with
  (detokenize→retokenize gap, corpus-anchored KLD, provisional bands,
  single-platform tested, GTM/EOS conflation, v0.1.3 fail-loud removal).
- README.md: GTM scoring formula updated; new test files listed; points
  at LIMITATIONS.md.
- axes/gtm.py module docstring: refreshed to reflect v0.1.2 + v0.1.3.
- CHANGELOG.md: v0.1.3 entry at top with the v0.1.2 matrix table and
  the file-by-file change summary.
- report.py: schema bumped refract.report.v0.1.1 → v0.1.3.
- __init__.py: __version__ = "0.1.3".

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Co-Authored-By: tturney <tturney1@gmail.com>
2026-04-29 20:36:25 -05:00

104 lines
3.7 KiB
Python

"""v0.1.3 regression tests for the llama.cpp command lines we construct.
These pin the v0.1.1 + v0.1.2 fixes:
- run_completion must pass --single-turn but NOT --no-conversation
(this fork rejects --no-conversation and prints help; v0.1 captured
the help text as the "completion").
- run_perplexity_kld_base passes --kl-divergence-base.
- run_perplexity_kld passes BOTH --kl-divergence and --kl-divergence-base.
- All three pass errors='replace' to subprocess.run (Mistral-Small-24B
crashed in v0.1.1 on a strict utf-8 decode of stderr).
"""
from __future__ import annotations
import subprocess
from pathlib import Path
from types import SimpleNamespace
import pytest
from refract import runner
from refract.runner import (
KVConfig,
run_completion,
run_perplexity_kld,
run_perplexity_kld_base,
)
@pytest.fixture
def captured(monkeypatch, tmp_path):
"""Patch subprocess.run + _bin so the wrappers run without llama.cpp.
Returns a list captured[0] = SimpleNamespace(cmd, kwargs) per call.
"""
calls: list = []
def fake_run(cmd, **kwargs):
calls.append(SimpleNamespace(cmd=cmd, kwargs=kwargs))
# Return enough fields for the wrappers to parse without raising.
# KLD parser needs a Mean KLD line; perplexity-base just needs rc=0.
stdout = "Mean KLD: 0.000000\nFinal estimate: PPL = 6.0\n"
return SimpleNamespace(returncode=0, stdout=stdout, stderr="")
monkeypatch.setattr(subprocess, "run", fake_run)
# _bin checks file existence — bypass with a temp dummy file.
fake_bin = tmp_path / "fake-bin"
fake_bin.touch()
monkeypatch.setattr(runner, "_bin", lambda name: fake_bin)
return calls
def test_run_completion_has_single_turn_no_no_conversation(captured, tmp_path):
model = tmp_path / "m.gguf"
model.touch()
run_completion(model=model, prompt="hi", kv=KVConfig(), n_predict=4)
assert len(captured) == 1
cmd = captured[0].cmd
assert "--single-turn" in cmd
# Regression: --no-conversation must NOT be in the args. This fork
# rejects it with "please use llama-completion instead" and prints help,
# which v0.1 silently captured as the model's completion.
assert "--no-conversation" not in cmd
def test_run_completion_passes_errors_replace(captured, tmp_path):
model = tmp_path / "m.gguf"
model.touch()
run_completion(model=model, prompt="hi", kv=KVConfig(), n_predict=4)
# Regression: Mistral-Small-24B crashed in v0.1.1 on UnicodeDecodeError
# because subprocess.run(text=True) strict-decodes stderr. errors='replace'
# keeps the stream usable when llama-cli emits non-utf-8 bytes.
assert captured[0].kwargs.get("errors") == "replace"
assert captured[0].kwargs.get("text") is True
def test_run_perplexity_kld_base_has_base_flag(captured, tmp_path):
model = tmp_path / "m.gguf"
model.touch()
corpus = tmp_path / "wiki.raw"
corpus.write_text("hello world")
base = tmp_path / "base.bin"
run_perplexity_kld_base(model=model, corpus=corpus, kv=KVConfig(),
base_path=base, chunks=4)
cmd = captured[0].cmd
assert "--kl-divergence-base" in cmd
assert str(base) in cmd
assert captured[0].kwargs.get("errors") == "replace"
def test_run_perplexity_kld_has_both_flags(captured, tmp_path):
model = tmp_path / "m.gguf"
model.touch()
corpus = tmp_path / "wiki.raw"
corpus.write_text("hello world")
base = tmp_path / "base.bin"
run_perplexity_kld(model=model, corpus=corpus, kv=KVConfig(),
base_path=base, chunks=4)
cmd = captured[0].cmd
assert "--kl-divergence" in cmd
assert "--kl-divergence-base" in cmd
assert captured[0].kwargs.get("errors") == "replace"