Spaces:
Build error
Build error
File size: 29,544 Bytes
595cac0 aa11fdb 3194e65 595cac0 3544425 595cac0 3194e65 595cac0 489a9c3 aa11fdb 16324bb aa11fdb 595cac0 7700840 595cac0 7700840 595cac0 7700840 266b0a1 | 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 95 96 97 98 99 100 101 102 103 104 105 106 107 108 109 110 111 112 113 114 115 116 117 118 119 120 121 122 123 124 125 126 127 128 129 130 131 132 133 134 135 136 137 138 139 140 141 142 143 144 145 146 147 148 149 150 151 152 153 154 155 156 157 158 159 160 161 162 163 164 165 166 167 168 169 170 171 172 173 174 175 176 177 178 179 180 181 182 183 184 185 186 187 188 189 190 191 192 193 194 195 196 197 198 199 200 201 202 203 204 205 206 207 208 209 210 211 212 213 214 215 216 217 218 219 220 221 222 223 224 225 226 227 228 229 230 231 232 233 234 235 236 237 238 239 240 241 242 243 244 245 246 247 248 249 250 251 252 253 254 255 256 257 258 259 260 261 262 263 264 265 266 267 268 269 270 271 272 273 274 275 276 277 278 279 280 281 282 283 284 285 286 287 288 289 290 291 292 293 294 295 296 297 298 299 300 301 302 303 304 305 306 307 308 309 310 311 312 313 314 315 316 317 318 319 320 321 322 323 324 325 326 327 328 329 330 331 332 333 334 335 336 337 338 339 340 341 342 343 344 345 346 347 348 349 350 351 352 353 354 355 356 357 358 359 360 361 362 363 364 365 366 367 368 369 370 371 372 373 374 375 376 377 378 379 380 381 382 383 384 385 386 387 388 389 390 391 392 393 394 395 396 397 398 399 400 401 402 403 404 405 406 407 408 409 410 411 412 413 414 415 416 417 418 419 420 421 422 423 424 425 426 427 428 429 430 431 432 433 434 435 436 437 438 439 440 441 442 443 444 445 446 447 448 449 450 451 452 453 454 455 456 457 458 459 460 461 462 463 464 465 466 467 468 469 470 471 472 473 474 475 476 477 478 479 480 481 482 483 484 485 486 487 488 489 490 491 492 493 494 495 496 497 498 499 500 501 502 503 504 505 506 507 508 509 510 511 512 513 514 515 516 517 518 519 520 521 522 523 524 525 526 527 528 529 530 531 532 533 534 535 536 537 538 539 540 541 542 543 544 545 546 547 548 549 550 551 552 553 554 555 556 557 558 559 560 561 562 563 564 565 566 567 568 569 570 571 572 573 574 575 | """Contract tests for every API route, on a generated tiny artifact set."""
import json
import os
import time
from types import SimpleNamespace
import pytest
from fastapi.testclient import TestClient
from plastic.api.app import create_app
from plastic.config import ModelConfig
from plastic.data.text import encode_documents_to_bin
from plastic.harness.canary import CanarySuite
from plastic.store import ArtifactStore
from plastic.tokenizer.bpe import Tokenizer
from plastic.train.loop import TrainConfig, train
DOCS = ["alpha beta gamma delta epsilon " * 80, "one two three four five six " * 80]
PROMPT = "alpha beta gamma delta epsilon alpha beta gamma delta epsilon one two three four"
TRANSACTION_KEYS = {"index", "t_unix", "pos_start", "pos_end", "decision", "requested", "signals", "accepted",
"read_only", "read_only_reason", "seconds"}
ACCEPTED_KEYS = {"delta_norm", "budget_charge", "budget_used", "budget_remaining"}
SUMMARY_KEYS = {"pos", "pending", "budget_used", "read_only", "n_transactions", "cusum", "state_norms", "drift_from_anchor"}
@pytest.fixture(scope="module")
def api(tmp_path_factory):
root = str(tmp_path_factory.mktemp("api"))
data_dir = os.path.join(root, "data", "tiny")
os.makedirs(data_dir)
tok = Tokenizer.train(DOCS, vocab_size=300)
tok.save(os.path.join(data_dir, "tokenizer.json"))
n_train = encode_documents_to_bin(tok, DOCS, os.path.join(data_dir, "train.bin"))
n_val = encode_documents_to_bin(tok, DOCS, os.path.join(data_dir, "validation.bin"))
with open(os.path.join(data_dir, "meta.json"), "w", encoding="utf-8") as f:
json.dump(
{
"corpus": "tiny",
"vocab_size": tok.vocab_size,
"splits": {"train": n_train, "validation": n_val, "test": 0},
"created_at_unix": int(time.time()),
"tokenizer_lines": len(DOCS),
},
f,
)
text_id = train(
TrainConfig(
domain="text",
model=ModelConfig(d_model=32, n_heads=2, n_layers=2, chunk=8, vocab_size=tok.vocab_size),
artifacts_root=root, model_id="lm_tiny", data_dir=data_dir, steps=3, batch_size=2, seq_len=32,
warmup_steps=1, eval_every=0, save_every=0, eval_batches=1, log_every=1, device="cpu", mqar_frac=0.0,
),
log=lambda s: None,
)
physics_id = train(
TrainConfig(
domain="physics",
model=ModelConfig(domain="physics", d_model=32, n_heads=2, n_layers=1, chunk=8),
artifacts_root=root, model_id="phys_tiny", steps=3, batch_size=2, seq_len=32, episodes_per_seq=2,
warmup_steps=1, eval_every=0, save_every=0, eval_batches=1, log_every=1, device="cpu", use_muon=False,
),
log=lambda s: None,
)
store = ArtifactStore(root)
CanarySuite.default_text(
os.path.join(data_dir, "validation.bin"), vocab_size=tok.vocab_size, n_probe=2, probe_len=16
).save(store.canary_path(text_id))
CanarySuite.default_physics(n_probe=2, steps=8).save(store.canary_path(physics_id))
app = create_app(root, device="cpu")
with TestClient(app) as client:
yield SimpleNamespace(client=client, store=store, root=root, data=data_dir, text=text_id, physics=physics_id)
# ---------------------------------------------------------------------- health and data
def test_health(api):
body = api.client.get("/api/health").json()
assert body["capabilities"] == {"create_session": True, "fork": True, "reset": True, "delete": True, "resume": True, "calibrate": True, "sleep": True}
assert body["public"] is False
assert body["ok"] is True
assert body["artifacts_root"] == api.store.root and body["device"] == "cpu"
assert body["n_models"] >= 2 and body["n_sessions"] >= 0
# ---------------------------------------------------------------------- models
@pytest.mark.parametrize('backend', ['plastic', 'qwen'])
def test_model_identity_survives_list_and_detail_without_inventing_config(tmp_path, backend):
store = ArtifactStore(str(tmp_path))
record = {'domain': 'text', 'status': 'completed', 'params': 12}
if backend == 'qwen':
record['backend'] = backend
store.register_model('model', record)
with TestClient(create_app(str(tmp_path), device='cpu')) as client:
assert client.get('/api/models').json()[0]['backend'] == backend
detail = client.get('/api/models/model').json()
assert detail['record']['backend'] == backend
assert detail['config'] == {}
def test_models_listing(api):
body = api.client.get("/api/models").json()
by_id = {m["model_id"]: m for m in body}
assert {api.text, api.physics} <= set(by_id)
text = by_id[api.text]
assert text["domain"] == "text" and text["status"] == "completed"
assert text["params"] > 0 and text["steps"] == 3 and text["has_canary"] is True
assert text["calibrated"] is False
assert text["eval"]["heldout_loss"] > 0 and "memory_value" in text["eval"]
assert by_id[api.physics]["domain"] == "physics"
def test_models_listing_survives_non_finite_eval_and_sleep(api):
# A model whose consolidation ran against an empty canary set carries NaN in its sleep
# manifest (score_suite returns NaN for an empty probe set); an unevaluated model's eval
# can be NaN/Inf too. GET /api/models is the first call every dashboard tab makes, so a
# single non-finite float there must render as null, not 500 the whole response.
api.store.register_model(
"nan_probe",
{
"domain": "text",
"status": "completed",
"params": 1,
"eval": {"heldout_loss": float("nan"), "memory_value": float("inf")},
"sleep": {
"accepted": True,
"canary_before": {"coherence": float("nan"), "poison": 0.1},
"canary_after": {"coherence": float("nan"), "poison": float("-inf")},
},
},
)
resp = api.client.get("/api/models")
assert resp.status_code == 200
rec = {m["model_id"]: m for m in resp.json()}["nan_probe"]
assert rec["eval"]["heldout_loss"] is None and rec["eval"]["memory_value"] is None
assert rec["sleep"]["canary_before"]["coherence"] is None and rec["sleep"]["canary_before"]["poison"] == 0.1
assert rec["sleep"]["canary_after"]["coherence"] is None and rec["sleep"]["canary_after"]["poison"] is None
def test_model_detail(api):
body = api.client.get(f"/api/models/{api.text}").json()
assert body["record"]["model_id"] == api.text
assert body["config"]["d_model"] == 32 and body["config"]["chunk"] == 8
assert body["eval"]["heldout_loss"] > 0 and "beta_hist" in body["eval"] # the detail eval keeps the histogram
assert body["calibration"] is None
assert body["canary"] == {"n_coherence": 2, "n_poison": 3}
assert body["log"] and any("loss" in rec for rec in body["log"])
def test_calibrate_dispatches_pretrained_records_to_the_real_chat_calibration(api, tmp_path, monkeypatch):
from plastic.harness import calibrate as calibrate_mod
from plastic.harness.calibration_prompts import DEFAULT_CALIBRATION_PROMPTS
ckpt = tmp_path / "ckpt"
ckpt.mkdir()
api.store.register_model("chat_pre", {"backend": "ttt", "domain": "text", "checkpoint_dir": str(ckpt), "params": 1})
calls = []
def fake_qwen(store, model_id, prompts, **kw):
calls.append((model_id, list(prompts), kw))
return calibrate_mod.Calibration(thresholds={"chunk_loss": 2.0}, achievable_fpr={"chunk_loss": 0.1},
reference={"chunk_loss": [1.0, 2.0]}, n_chunks=2, target_fpr=kw["target_fpr"])
monkeypatch.setattr(calibrate_mod, "calibrate_qwen", fake_qwen)
monkeypatch.setattr(calibrate_mod, "calibrate_model", lambda *a, **k: pytest.fail("toy path used for a pretrained record"))
r = api.client.post("/api/models/chat_pre/calibrate", json={})
assert r.status_code == 200, r.text
assert r.json()["thresholds"] == {"chunk_loss": 2.0} and r.json()["n_chunks"] == 2
assert calls[-1][1] == list(DEFAULT_CALIBRATION_PROMPTS) and calls[-1][2]["max_new_tokens"] == 32
r = api.client.post("/api/models/chat_pre/calibrate", json={"prompts": ["hi there"], "cusum_prompts": ["again"], "max_new_tokens": 4, "fpr": 0.2})
assert r.status_code == 200 and calls[-1][1] == ["hi there"]
assert calls[-1][2]["cusum_prompts"] == ["again"] and calls[-1][2]["max_new_tokens"] == 4 and calls[-1][2]["target_fpr"] == 0.2
# request-shape family: empty prompt list and an out-of-range generation cap are rejected before any work
assert api.client.post("/api/models/chat_pre/calibrate", json={"prompts": []}).status_code == 422
assert api.client.post("/api/models/chat_pre/calibrate", json={"max_new_tokens": 0}).status_code == 422
# a pretrained record whose checkpoint directory is gone is "no checkpoint yet"
api.store.register_model("chat_gone", {"backend": "ttt", "domain": "text", "checkpoint_dir": str(tmp_path / "missing")})
assert api.client.post("/api/models/chat_gone/calibrate", json={}).status_code == 400
def test_sleep_routes_start_a_process_and_report_its_outcome(api, tmp_path, monkeypatch):
"""The route validates the record and body, starts one process per run, and reads the outcome back from
the report the process writes. The process here is a stand-in that writes an accepted/rejected report."""
import sys
from plastic.harness.config import HarnessConfig
from plastic.api import sleep_jobs
ckpt = tmp_path / "ckpt"
ckpt.mkdir()
api.store.register_model("chat_sleep", {"backend": "ttt", "domain": "text", "checkpoint_dir": str(ckpt), "params": 1})
api.store.create_session("teach", model_id="chat_sleep", domain="text", harness_cfg=HarnessConfig())
seen = {}
def fake_argv(model_id, run_dir, artifacts_root, device, options, sessions, probes_path):
seen.update(model_id=model_id, options=options, sessions=sessions, probes_path=probes_path, device=device)
outcome = "rejected" if options.get("method") == "anchor" else "accepted"
script = (
"import json,sys,os; d=sys.argv[1]; open(os.path.join(d,'log.txt'),'a').write('[sleep] stand-in\\n');"
f"json.dump({{'run_id': os.path.basename(d), 'parent_model_id': {model_id!r}, 'status': {outcome!r}, 'model_id': 'sleep_child' if {outcome!r}=='accepted' else None,"
" 'gate': {'passed': " + ("False" if outcome == "rejected" else "True") + ", 'checks': []}, 'losses': [1.0]}, open(os.path.join(d,'sleep_report.json'),'w'))"
)
return [sys.executable, "-c", script, run_dir]
monkeypatch.setattr(sleep_jobs, "build_sleep_argv", fake_argv)
# a toy record has no fast weights to consolidate
assert api.client.post(f"/api/models/{api.text}/sleep", json={}).status_code == 400
assert api.client.post("/api/models/nope/sleep", json={}).status_code == 404
# request-shape family
for bad in ({"method": "nap"}, {"steps": 0}, {"replay_ratio": 2}, {"sessions": []}, {"probes": [{"question": "", "answer": "x"}]}):
assert api.client.post("/api/models/chat_sleep/sleep", json=bad).status_code == 422, bad
assert api.client.post("/api/models/chat_sleep/sleep", json={"sessions": ["missing"]}).status_code == 400
r = api.client.post("/api/models/chat_sleep/sleep", json={"method": "distill", "steps": 3, "sessions": ["teach"],
"probes": [{"question": "q", "answer": "a", "paraphrase": "q2"}]})
assert r.status_code == 200, r.text
job = r.json()
assert job["model_id"] == "chat_sleep" and job["status"] in ("running", "accepted") and job["n_probes"] == 1
assert seen["options"]["method"] == "distill" and seen["options"]["steps"] == 3 and seen["sessions"] == ["teach"]
assert seen["probes_path"] and seen["probes_path"].endswith("probes.json") and seen["device"] == "cpu"
run_id = job["run_id"]
for _ in range(100):
s = api.client.get(f"/api/sleep/{run_id}").json()
if s["status"] != "running":
break
time.sleep(0.05)
assert s["status"] == "accepted" and s["report"]["model_id"] == "sleep_child" and s["log_tail"] == ["[sleep] stand-in"]
r2 = api.client.post("/api/models/chat_sleep/sleep", json={"method": "anchor"}).json()
for _ in range(100):
s2 = api.client.get(f"/api/sleep/{r2['run_id']}").json()
if s2["status"] != "running":
break
time.sleep(0.05)
assert s2["status"] == "rejected" and s2["report"]["gate"]["passed"] is False
listing = api.client.get("/api/sleep").json()
assert {j["run_id"] for j in listing} >= {run_id, r2["run_id"]} and all("losses" not in (j["report"] or {}) for j in listing)
assert api.client.get("/api/sleep/nope").status_code == 404
def test_model_unknown_is_404(api):
assert api.client.get("/api/models/nope").status_code == 404
assert api.client.post("/api/models/nope/calibrate", json={}).status_code == 404
def test_calibrate(api):
body = api.client.post(
f"/api/models/{api.text}/calibrate",
json={"data_dir": api.data, "chunks": 16, "fisher_chunks": 2, "fpr": 0.1},
).json()
assert body["n_chunks"] >= 16 and body["target_fpr"] == 0.1
assert body["thresholds"] and all(v is None or isinstance(v, float) for v in body["thresholds"].values())
assert body["reference_sizes"] and "reference" not in body
assert body["created_at_unix"] > 0
# the rate the sample can support, which is not the requested target_fpr
assert set(body["achievable_fpr"]) <= set(body["thresholds"])
assert body["achievable_fpr"] and all(v > 0 for v in body["achievable_fpr"].values())
detail = api.client.get(f"/api/models/{api.text}").json()
assert detail["record"]["calibrated"] is True
assert detail["calibration"]["thresholds"] == body["thresholds"]
assert detail["calibration"]["achievable_fpr"] == body["achievable_fpr"]
def test_an_unbounded_threshold_is_null(api):
"""A threshold can be infinite (an unbounded bound); JSON carries it as null."""
path = os.path.join(api.store.model_dir(api.text), "calibration.json")
original = open(path, "rb").read()
payload = json.loads(original)
signal = sorted(payload["thresholds"])[0]
payload["thresholds"][signal] = float("inf")
try:
with open(path, "w", encoding="utf-8") as f:
json.dump(payload, f)
body = api.client.get(f"/api/models/{api.text}")
assert body.status_code == 200
assert body.json()["calibration"]["thresholds"][signal] is None
finally:
with open(path, "wb") as f:
f.write(original)
# ---------------------------------------------------------------------- sessions
def test_create_sessions(api):
text = api.client.post("/api/sessions", json={"model_id": api.text, "session_id": "t1",
"harness": {"enable_projection": False}})
assert text.status_code == 200
body = text.json()
assert body["session_id"] == "t1" and body["domain"] == "text" and body["model_id"] == api.text
assert body["pos"] == 0 and body["n_transactions"] == 0 and body["read_only"] is False
physics = api.client.post("/api/sessions", json={"model_id": api.physics, "session_id": "p1"}).json()
assert physics["session_id"] == "p1" and physics["domain"] == "physics"
auto = api.client.post("/api/sessions", json={"model_id": api.text}).json()
assert auto["session_id"] and auto["session_id"] not in ("t1", "p1")
api.client.delete(f"/api/sessions/{auto['session_id']}")
def test_create_session_errors(api):
assert api.client.post("/api/sessions", json={"model_id": "nope"}).status_code == 404
assert api.client.post("/api/sessions", json={"model_id": api.text, "session_id": "t1"}).status_code == 409
bad = api.client.post("/api/sessions", json={"model_id": api.text, "session_id": "bad", "harness": {"nope": 1}})
assert bad.status_code == 400 and "nope" in bad.json()["detail"]
def test_sessions_listing(api):
body = api.client.get("/api/sessions").json()
by_id = {s["session_id"]: s for s in body}
assert {"t1", "p1"} <= set(by_id)
assert by_id["t1"]["model_id"] == api.text and by_id["p1"]["domain"] == "physics"
def test_chat(api):
body = api.client.post("/api/sessions/t1/chat", json={"prompt": PROMPT, "max_new_tokens": 4, "seed": 0}).json()
assert body["prompt"] == PROMPT and isinstance(body["completion"], str)
assert body["n_tokens_in"] > 0 and body["n_tokens_out"] <= 4
assert body["transactions"] and TRANSACTION_KEYS <= set(body["transactions"][0])
assert body["transactions"][0]["decision"]["kind"] in ("commit", "rollback", "scale", "project", "readonly")
signals = body["transactions"][0]["signals"]
assert signals["n_tokens"] > 0 and "log_delta_norm" in signals and "z" in signals
# signals are the proposed update; accepted is what the decision actually committed
accepted = body["transactions"][0]["accepted"]
assert ACCEPTED_KEYS <= set(accepted) and accepted["delta_norm"] >= 0.0
assert "canary_delta_coherence" in accepted
assert SUMMARY_KEYS <= set(body["summary"])
def test_session_detail(api):
body = api.client.get("/api/sessions/t1").json()
assert body["meta"]["session_id"] == "t1"
assert body["meta"]["harness"]["enable_projection"] is False and body["meta"]["model_signature"]
assert body["lineage"] == ["t1"]
assert body["summary"]["n_transactions"] == body["meta"]["n_transactions"]
assert body["summary"]["state_norms"]["s_norm"] and body["summary"]["cusum"]
assert body["transactions"] and TRANSACTION_KEYS <= set(body["transactions"][0])
assert body["trace"] and body["trace"][0]["kind"] == "chat"
def test_transactions_paging(api):
full = api.client.get("/api/sessions/t1/transactions", params={"limit": 100}).json()
assert full["total"] == len(full["items"]) >= 1
page = api.client.get("/api/sessions/t1/transactions", params={"limit": 1, "offset": 0}).json()
assert page["total"] == full["total"] and len(page["items"]) == 1
assert page["items"][0]["index"] == full["items"][0]["index"]
empty = api.client.get("/api/sessions/t1/transactions", params={"limit": 5, "offset": full["total"] + 10}).json()
assert empty["items"] == [] and empty["total"] == full["total"]
def test_state(api):
body = api.client.get("/api/sessions/t1/state").json()
assert body["kind"] == "plastic" # the toy per-layer S/h shape, discriminated for the client
assert body["pos"] > 0 and len(body["layers"]) == 2
layer = body["layers"][0]
assert len(layer["s_norm_per_head"]) == 2 and layer["h_norm"] >= 0.0
assert len(layer["singular_values"]) == 2 and len(layer["singular_values"][0]) <= 8
assert layer["drift_from_anchor"] >= 0.0
def test_state_payload_native_backend_has_no_plastic_layers():
# ASTRA-081 #5: a pretrained (Qwen) session's committed state has no per-layer S/h shape, so the
# /state route must return an honest recurrent payload instead of a 500 (committed.layers) or
# fabricated tensors. Driven with a fake session so it needs no model/isolated deps.
import torch
from plastic.api.service import state_payload
class _QwenLikeState: # no `.layers`, exactly like QwenState
pass
class _FakeBackend:
def state_norms(self, _s):
return {"recurrent_norm": [1.5, 2.0], "recurrent_norm_total": 2.5}
def state_delta(self, _a, _b):
return [torch.tensor([3.0, 4.0]), torch.tensor([0.0])] # per-unit norms 5.0, 0.0
class _FakeRunner:
committed = _QwenLikeState()
anchor = _QwenLikeState()
pos = 12
backend = _FakeBackend()
class _FakeSession:
runner = _FakeRunner()
backend_kind = "qwen"
body = state_payload(_FakeSession())
assert body["kind"] == "recurrent" and body["backend"] == "qwen" and body["pos"] == 12
assert body["recurrent_norm_total"] == 2.5
assert [u["recurrent_norm"] for u in body["units"]] == [1.5, 2.0]
assert body["units"][0]["drift_from_anchor"] == 5.0 and body["units"][1]["drift_from_anchor"] == 0.0
assert "layers" not in body # never the plastic shape, never fabricated S/h tensors
def test_fork(api):
child = api.client.post("/api/sessions/t1/fork", json={"child_session_id": "t1_fork"}).json()
assert child["session_id"] == "t1_fork" and child["parent_session_id"] == "t1"
assert child["root_session_id"] == "t1" and child["pos"] == api.store.load_session_meta("t1")["pos"]
grand = api.client.post("/api/sessions/t1_fork/fork", json={}).json()
detail = api.client.get(f"/api/sessions/{grand['session_id']}").json()
assert detail["lineage"] == ["t1", "t1_fork", grand["session_id"]]
assert api.client.post("/api/sessions/t1/fork", json={"child_session_id": "t1_fork"}).status_code == 409
assert api.client.post("/api/sessions/nope/fork", json={}).status_code == 404
def test_reset_and_resume(api):
before = api.client.get("/api/sessions/t1_fork").json()["meta"]
assert before["pos"] > 0
reset = api.client.post("/api/sessions/t1_fork/reset").json()
assert reset["pos"] == 0 and reset["read_only"] is False
resumed = api.client.post("/api/sessions/t1_fork/resume").json()
assert resumed["read_only"] is False and resumed["session_id"] == "t1_fork"
assert api.client.post("/api/sessions/nope/reset").status_code == 404
assert api.client.post("/api/sessions/nope/resume").status_code == 404
def test_delete(api):
created = api.client.post("/api/sessions", json={"model_id": api.text, "session_id": "doomed"}).json()
assert created["session_id"] == "doomed"
assert api.client.delete("/api/sessions/doomed").json() == {"deleted": True}
assert api.client.get("/api/sessions/doomed").status_code == 404
assert api.client.delete("/api/sessions/doomed").status_code == 404
assert "doomed" not in {s["session_id"] for s in api.client.get("/api/sessions").json()}
def test_session_unknown_is_404(api):
assert api.client.get("/api/sessions/nope").status_code == 404
assert api.client.get("/api/sessions/nope/state").status_code == 404
assert api.client.get("/api/sessions/nope/transactions").status_code == 404
assert api.client.post("/api/sessions/nope/chat", json={"prompt": "x"}).status_code == 404
# ---------------------------------------------------------------------- training jobs
# ---------------------------------------------------------------------- red team
# ---------------------------------------------------------------------- sleep
def test_model_summary_exposes_sleep_field():
from plastic.api.service import model_summary
from plastic.store import ArtifactStore
store = ArtifactStore("/nonexistent-root-for-summary-only")
rec = {"model_id": "m", "domain": "text", "type": "sleep_consolidation", "parent_model_id": "base",
"sleep": {"accepted": True, "delta_coherence": 0.01, "delta_poison": -0.2}}
out = model_summary(store, rec)
assert out["sleep"] == rec["sleep"]
assert out["type"] == "sleep_consolidation" and out["parent_model_id"] == "base"
# a model without a sleep record reports None, not a fabricated value
assert model_summary(store, {"model_id": "m2", "domain": "text"})["sleep"] is None
def test_state_payload_native_missing_or_failed_drift_is_null():
# ASTRA-094: a failed / missing / nonfinite measurement is null (unavailable), never a measured
# zero; a genuine zero is preserved. Driven with fakes, no model.
import torch
from plastic.api.service import state_payload
class _QwenLikeState:
pass
def _sess(norms, delta):
class _Backend:
def state_norms(self, _s):
return norms
def state_delta(self, _a, _b):
if isinstance(delta, Exception):
raise delta
return delta
class _Runner:
committed = _QwenLikeState()
anchor = _QwenLikeState()
pos = 5
backend = _Backend()
class _Session:
runner = _Runner()
backend_kind = "qwen"
return _Session()
# state_delta raises -> every drift null, but the recurrent norms are still reported
body = state_payload(_sess({"recurrent_norm": [1.0, 2.0], "recurrent_norm_total": 3.0}, RuntimeError("boom")))
assert [u["drift_from_anchor"] for u in body["units"]] == [None, None]
assert [u["recurrent_norm"] for u in body["units"]] == [1.0, 2.0]
# a short delta list -> the unmatched unit is null; a genuine zero drift is preserved as 0.0
body = state_payload(_sess({"recurrent_norm": [1.0, 2.0, 3.0], "recurrent_norm_total": 4.0},
[torch.tensor([0.0]), torch.tensor([4.0])]))
assert [u["drift_from_anchor"] for u in body["units"]] == [0.0, 4.0, None]
# an absent total is null, not zero
body = state_payload(_sess({"recurrent_norm": [1.0]}, [torch.tensor([2.0])]))
assert body["recurrent_norm_total"] is None
# nonfinite norm / total / drift are all null
body = state_payload(_sess({"recurrent_norm": [float("inf")], "recurrent_norm_total": float("nan")},
[torch.tensor([float("inf")])]))
assert body["units"][0]["recurrent_norm"] is None
assert body["units"][0]["drift_from_anchor"] is None
assert body["recurrent_norm_total"] is None
def test_session_detail_exposes_loaded_calibration_not_replaced_artifact(api):
# ASTRA-096 #2: a session's active calibration is the one it verified at open, not the model's
# CURRENT saved artifact. A separate calibration process replacing the artifact same-model must
# not change what the open session (and its running policy) uses or what the UI labels active.
from plastic.harness.calibrate import Calibration
mid = api.text
api.client.post(f"/api/models/{mid}/calibrate",
json={"data_dir": api.data, "chunks": 16, "fisher_chunks": 2, "fpr": 0.1})
model_dir = api.store.model_dir(mid)
sid = api.client.post("/api/sessions", json={"model_id": mid, "session_id": "cal_identity"}).json()["session_id"]
detail = api.client.get(f"/api/sessions/{sid}").json()
assert detail["summary"]["calibration"] == "installed"
assert detail["calibration"] is not None
loaded_thresholds = detail["calibration"]["thresholds"]
assert loaded_thresholds # the session exposes its loaded calibration's thresholds
# a SEPARATE calibration process replaces the saved artifact with a valid same-signature one whose
# thresholds differ (bumped by 100); the model signature is unchanged so it is still "installable"
replacement = Calibration.load(model_dir)
replacement.thresholds = {k: (None if v is None else float(v) + 100.0) for k, v in replacement.thresholds.items()}
replacement.save(model_dir)
model_after = api.client.get(f"/api/models/{mid}").json()
assert model_after["calibration"]["thresholds"] != loaded_thresholds # the model detail shows the replacement
# the OPEN session still exposes the calibration it actually loaded, not the replacement
detail_after = api.client.get(f"/api/sessions/{sid}").json()
assert detail_after["summary"]["calibration"] == "installed"
assert detail_after["calibration"]["thresholds"] == loaded_thresholds
assert detail_after["calibration"]["thresholds"] != model_after["calibration"]["thresholds"]
def test_session_summary_reports_effective_generation_write_policy(api):
# ASTRA-101: writes_generation is the EFFECTIVE policy (backend source capability OR
# learn_from_generation). A plastic session freezes generation by default; the flag makes it
# write-eligible. (Qwen's generation writes regardless -- that path is model-gated.)
sid = api.client.post("/api/sessions", json={"model_id": api.text}).json()["session_id"]
body = api.client.get(f"/api/sessions/{sid}").json()
assert body["summary"]["backend"] == "plastic"
assert body["summary"]["writes_generation"] is False # default: generation frozen read-only
sid2 = api.client.post(
"/api/sessions", json={"model_id": api.text, "harness": {"learn_from_generation": True}}
).json()["session_id"]
body2 = api.client.get(f"/api/sessions/{sid2}").json()
assert body2["summary"]["writes_generation"] is True # the override makes generation write-eligible
def test_sleep_argv_forwards_the_replay_revision_and_rejects_a_malformed_one(api):
"""ASTRA-181: the API's replay_revision reaches the sleep process; a non-hex value is a request-shape error."""
from plastic.api.schemas import SleepRequest
from plastic.api.sleep_jobs import build_sleep_argv
argv = build_sleep_argv("m", "/run", "/root", "cpu", {"method": "replay", "replay_revision": "5feaf2fd3ffca7"}, None, None)
assert argv[argv.index("--replay-revision") + 1] == "5feaf2fd3ffca7"
assert "--replay-revision" not in build_sleep_argv("m", "/run", "/root", "cpu", {"method": "replay"}, None, None)
assert SleepRequest().model_dump(exclude_none=True).get("replay_revision") is None
assert api.client.post("/api/models/nope/sleep", json={"replay_revision": "not-a-sha!"}).status_code == 422
|