specimba commited on
Commit
f76b6e4
·
verified ·
1 Parent(s): 41b3e98

sync PR7 reviewed snapshot

Browse files
tests/test_app_callbacks.py CHANGED
@@ -1,6 +1,4 @@
1
  import app
2
- import hashlib
3
- from pathlib import Path
4
  from nexus_visual_weaver.planner import build_command_center_run
5
 
6
 
@@ -45,21 +43,6 @@ def test_checkpoint_seed_uses_only_last_8_chars() -> None:
45
  assert result == int("11223344", 16) % 1_000_000
46
 
47
 
48
- def test_safe_file_hash_streams_digest_and_size() -> None:
49
- payload = b"nexus-visual-weaver" * 1024
50
- target = Path("outputs/test-hash-reference.bin")
51
- target.parent.mkdir(parents=True, exist_ok=True)
52
- target.write_bytes(payload)
53
-
54
- try:
55
- digest, size = app._safe_file_hash(str(target))
56
-
57
- assert digest == hashlib.sha256(payload).hexdigest()
58
- assert size == len(payload)
59
- finally:
60
- target.unlink(missing_ok=True)
61
-
62
-
63
  # --- _wardrobe_summary tests ---
64
 
65
  def test_wardrobe_summary_returns_string_with_slot_fields() -> None:
@@ -270,6 +253,366 @@ def test_checkpoint_blocks_without_generated_artifact() -> None:
270
  assert "no generated artifact" in blocked[13]["message"].lower()
271
 
272
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
273
  def test_export_packet_allows_blocked_st3gg_with_explicit_override(monkeypatch) -> None:
274
  monkeypatch.setenv("NEXUS_EXPORT_DIR", "outputs/test-exports")
275
  run = build_command_center_run("gothic patent leather platform boots")
 
1
  import app
 
 
2
  from nexus_visual_weaver.planner import build_command_center_run
3
 
4
 
 
43
  assert result == int("11223344", 16) % 1_000_000
44
 
45
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
46
  # --- _wardrobe_summary tests ---
47
 
48
  def test_wardrobe_summary_returns_string_with_slot_fields() -> None:
 
253
  assert "no generated artifact" in blocked[13]["message"].lower()
254
 
255
 
256
+ # --- _safe_file_hash tests ---
257
+
258
+ def test_safe_file_hash_returns_sha256_and_size_for_valid_file() -> None:
259
+ import hashlib
260
+ from pathlib import Path
261
+
262
+ path = app.ROOT / "outputs" / "test-hash-target.png"
263
+ path.parent.mkdir(parents=True, exist_ok=True)
264
+ data = b"\x89PNG\r\n\x1a\n" + b"\x00" * 32
265
+ path.write_bytes(data)
266
+
267
+ file_hash, size = app._safe_file_hash(str(path))
268
+
269
+ assert file_hash == hashlib.sha256(data).hexdigest()
270
+ assert size == len(data)
271
+
272
+
273
+ def test_safe_file_hash_returns_none_for_none_path() -> None:
274
+ assert app._safe_file_hash(None) == (None, None)
275
+
276
+
277
+ def test_safe_file_hash_returns_none_for_empty_string() -> None:
278
+ assert app._safe_file_hash("") == (None, None)
279
+
280
+
281
+ def test_safe_file_hash_returns_none_for_missing_file() -> None:
282
+ result = app._safe_file_hash("/nonexistent/path/no-such-file.png")
283
+ assert result == (None, None)
284
+
285
+
286
+ def test_safe_file_hash_hash_is_64_hex_chars() -> None:
287
+ from pathlib import Path
288
+
289
+ path = app.ROOT / "outputs" / "test-hash-hex.png"
290
+ path.parent.mkdir(parents=True, exist_ok=True)
291
+ path.write_bytes(b"\x89PNG\r\n\x1a\n" + b"\x01" * 8)
292
+
293
+ file_hash, _ = app._safe_file_hash(str(path))
294
+
295
+ assert file_hash is not None
296
+ assert len(file_hash) == 64
297
+ assert all(c in "0123456789abcdef" for c in file_hash)
298
+
299
+
300
+ # --- _safe_reference_url_metadata tests ---
301
+
302
+ def test_safe_reference_url_metadata_returns_none_for_falsy() -> None:
303
+ assert app._safe_reference_url_metadata(None) is None
304
+ assert app._safe_reference_url_metadata("") is None
305
+
306
+
307
+ def test_safe_reference_url_metadata_returns_metadata_for_valid_https_url() -> None:
308
+ result = app._safe_reference_url_metadata("https://shop.example.test/item/123")
309
+
310
+ assert result is not None
311
+ assert result["status"] == "metadata_only"
312
+ assert result["domain"] == "shop.example.test"
313
+ assert "url_hash" in result
314
+ assert len(result["url_hash"]) == 64
315
+
316
+
317
+ def test_safe_reference_url_metadata_returns_metadata_for_valid_http_url() -> None:
318
+ result = app._safe_reference_url_metadata("http://example.test/path?q=1")
319
+
320
+ assert result is not None
321
+ assert result["status"] == "metadata_only"
322
+ assert result["domain"] == "example.test"
323
+
324
+
325
+ def test_safe_reference_url_metadata_rejects_file_scheme() -> None:
326
+ result = app._safe_reference_url_metadata("file:///etc/passwd")
327
+
328
+ assert result is not None
329
+ assert result["status"] == "invalid_url"
330
+
331
+
332
+ def test_safe_reference_url_metadata_rejects_ftp_scheme() -> None:
333
+ result = app._safe_reference_url_metadata("ftp://ftp.example.test/file.txt")
334
+
335
+ assert result is not None
336
+ assert result["status"] == "invalid_url"
337
+
338
+
339
+ def test_safe_reference_url_metadata_rejects_missing_host() -> None:
340
+ result = app._safe_reference_url_metadata("http:///no-host")
341
+
342
+ assert result is not None
343
+ assert result["status"] == "invalid_url"
344
+
345
+
346
+ def test_safe_reference_url_metadata_domain_is_lowercase() -> None:
347
+ result = app._safe_reference_url_metadata("https://SHOP.EXAMPLE.TEST/Item/123")
348
+
349
+ assert result is not None
350
+ assert result["domain"] == "shop.example.test"
351
+
352
+
353
+ def test_safe_reference_url_metadata_url_hash_is_stable() -> None:
354
+ url = "https://shop.example.test/stable-hash-test"
355
+ first = app._safe_reference_url_metadata(url)
356
+ second = app._safe_reference_url_metadata(url)
357
+
358
+ assert first is not None and second is not None
359
+ assert first["url_hash"] == second["url_hash"]
360
+
361
+
362
+ def test_safe_reference_url_metadata_different_urls_produce_different_hashes() -> None:
363
+ url_a = "https://shop.example.test/item/1"
364
+ url_b = "https://shop.example.test/item/2"
365
+
366
+ result_a = app._safe_reference_url_metadata(url_a)
367
+ result_b = app._safe_reference_url_metadata(url_b)
368
+
369
+ assert result_a is not None and result_b is not None
370
+ assert result_a["url_hash"] != result_b["url_hash"]
371
+
372
+
373
+ # --- _reference_metadata tests ---
374
+
375
+ def test_reference_metadata_returns_empty_for_no_inputs() -> None:
376
+ scan = {"status": "pass", "export_gate": "clear"}
377
+ result = app._reference_metadata(None, None, scan)
378
+
379
+ assert result == []
380
+
381
+
382
+ def test_reference_metadata_includes_file_record_when_uploaded() -> None:
383
+ from pathlib import Path
384
+
385
+ path = app.ROOT / "outputs" / "test-ref-meta.png"
386
+ path.parent.mkdir(parents=True, exist_ok=True)
387
+ path.write_bytes(b"\x89PNG\r\n\x1a\n" + b"\x00" * 16)
388
+ scan = {"status": "pass", "export_gate": "clear", "magic": "PNG", "extension": ".png"}
389
+
390
+ result = app._reference_metadata(str(path), None, scan)
391
+
392
+ assert len(result) == 1
393
+ assert result[0]["source"] == "upload"
394
+ assert result[0]["basename"] == "test-ref-meta.png"
395
+ assert result[0]["sha256"] is not None
396
+ assert result[0]["st3gg_status"] == "pass"
397
+ assert result[0]["export_gate"] == "clear"
398
+ assert result[0]["magic"] == "PNG"
399
+ assert result[0]["extension"] == ".png"
400
+
401
+
402
+ def test_reference_metadata_includes_url_record_when_url_given() -> None:
403
+ scan = {}
404
+ result = app._reference_metadata(None, "https://shop.example.test/item/99", scan)
405
+
406
+ assert len(result) == 1
407
+ assert result[0]["source"] == "url"
408
+ assert result[0]["domain"] == "shop.example.test"
409
+
410
+
411
+ def test_reference_metadata_includes_both_file_and_url() -> None:
412
+ from pathlib import Path
413
+
414
+ path = app.ROOT / "outputs" / "test-ref-both.png"
415
+ path.parent.mkdir(parents=True, exist_ok=True)
416
+ path.write_bytes(b"\x89PNG\r\n\x1a\n\x00\x00\x00\rIEND\xaeB`\x82")
417
+ scan = {"status": "pass", "export_gate": "clear"}
418
+
419
+ result = app._reference_metadata(str(path), "https://shop.example.test/item/77", scan)
420
+
421
+ assert len(result) == 2
422
+ sources = {r["source"] for r in result}
423
+ assert sources == {"upload", "url"}
424
+
425
+
426
+ def test_reference_metadata_size_bytes_is_integer() -> None:
427
+ from pathlib import Path
428
+
429
+ data = b"\x89PNG\r\n\x1a\n" + b"\x00" * 24
430
+ path = app.ROOT / "outputs" / "test-ref-size.png"
431
+ path.parent.mkdir(parents=True, exist_ok=True)
432
+ path.write_bytes(data)
433
+ scan = {}
434
+
435
+ result = app._reference_metadata(str(path), None, scan)
436
+
437
+ assert result[0]["size_bytes"] == len(data)
438
+
439
+
440
+ # --- _creator_controls tests ---
441
+
442
+ def test_creator_controls_returns_dict_with_wardrobe_and_generation() -> None:
443
+ result = app._creator_controls("Strict", "Wan2.2 I2V")
444
+
445
+ assert "wardrobe" in result
446
+ assert "generation" in result
447
+ assert result["reasoning_mode"] == "Strict"
448
+ assert result["video_preset"] == "Wan2.2 I2V"
449
+
450
+
451
+ def test_creator_controls_default_wardrobe_values() -> None:
452
+ result = app._creator_controls("Strict", "Wan2.2 I2V")
453
+ wardrobe = result["wardrobe"]
454
+
455
+ assert wardrobe["footwear"] == "platform boots"
456
+ assert wardrobe["outerwear"] == "black patent leather long coat"
457
+ assert wardrobe["palette"] == "black, crimson, cyan neon"
458
+
459
+
460
+ def test_creator_controls_overrides_wardrobe_slots() -> None:
461
+ result = app._creator_controls(
462
+ "Frontier",
463
+ "LTX-2.3",
464
+ footwear="patent leather heels",
465
+ outerwear="faux fur collar coat",
466
+ palette="obsidian, pearl, crimson",
467
+ )
468
+ wardrobe = result["wardrobe"]
469
+
470
+ assert wardrobe["footwear"] == "patent leather heels"
471
+ assert wardrobe["outerwear"] == "faux fur collar coat"
472
+ assert wardrobe["palette"] == "obsidian, pearl, crimson"
473
+
474
+
475
+ def test_creator_controls_locked_slots_present() -> None:
476
+ result = app._creator_controls("Strict", "Wan2.2 I2V")
477
+ wardrobe = result["wardrobe"]
478
+
479
+ assert "locked_slots" in wardrobe
480
+ assert isinstance(wardrobe["locked_slots"], list)
481
+ assert "outerwear" in wardrobe["locked_slots"]
482
+
483
+
484
+ def test_creator_controls_generation_specifies_flux_primary() -> None:
485
+ result = app._creator_controls("Strict", "Wan2.2 I2V")
486
+ generation = result["generation"]
487
+
488
+ assert "black-forest-labs/FLUX.2-klein-9B" in generation["flux_primary"]
489
+ assert "black-forest-labs/FLUX.2-klein-4B" in generation["flux_sidecar"]
490
+
491
+
492
+ # --- _prompt_with_controls tests ---
493
+
494
+ def test_prompt_with_controls_appends_wardrobe_suffix() -> None:
495
+ controls = app._creator_controls("Strict", "Wan2.2 I2V", footwear="platform boots")
496
+ result = app._prompt_with_controls("gothic couture portrait", controls)
497
+
498
+ assert result.startswith("gothic couture portrait")
499
+ assert "Wardrobe controls:" in result
500
+ assert "platform boots" in result
501
+
502
+
503
+ def test_prompt_with_controls_returns_prompt_unchanged_when_no_wardrobe() -> None:
504
+ # A controls dict with no wardrobe key
505
+ result = app._prompt_with_controls("gothic couture portrait", {})
506
+
507
+ assert result == "gothic couture portrait"
508
+
509
+
510
+ def test_prompt_with_controls_includes_all_wardrobe_fields() -> None:
511
+ controls = app._creator_controls(
512
+ "Strict",
513
+ "Wan2.2 I2V",
514
+ silhouette="structured long coat",
515
+ outerwear="black patent leather long coat",
516
+ upper_body="Chantilly lace neckline",
517
+ footwear="platform boots",
518
+ palette="black, crimson, cyan neon",
519
+ hardware="crimson hardware",
520
+ )
521
+ result = app._prompt_with_controls("brief", controls)
522
+
523
+ assert "structured long coat" in result
524
+ assert "black patent leather long coat" in result
525
+ assert "Chantilly lace neckline" in result
526
+ assert "platform boots" in result
527
+ assert "crimson hardware" in result
528
+
529
+
530
+ def test_prompt_with_controls_does_not_duplicate_prompt() -> None:
531
+ controls = app._creator_controls("Strict", "Wan2.2 I2V")
532
+ prompt = "gothic couture brief"
533
+ result = app._prompt_with_controls(prompt, controls)
534
+
535
+ assert result.count(prompt) == 1
536
+
537
+
538
+ # --- _generated_output_path tests ---
539
+
540
+ def test_generated_output_path_returns_none_when_no_state() -> None:
541
+ assert app._generated_output_path(None) is None
542
+
543
+
544
+ def test_generated_output_path_returns_none_when_no_generation_key() -> None:
545
+ assert app._generated_output_path({"provider_state": "idle"}) is None
546
+
547
+
548
+ def test_generated_output_path_returns_none_when_no_output_path() -> None:
549
+ state = {"generation": {"status": "disabled"}}
550
+ assert app._generated_output_path(state) is None
551
+
552
+
553
+ def test_generated_output_path_returns_string_when_present() -> None:
554
+ state = {"generation": {"output_path": "/data/artifact.png"}}
555
+ result = app._generated_output_path(state)
556
+
557
+ assert result == "/data/artifact.png"
558
+
559
+
560
+ def test_generated_output_path_returns_none_for_empty_path() -> None:
561
+ state = {"generation": {"output_path": ""}}
562
+ assert app._generated_output_path(state) is None
563
+
564
+
565
+ # --- _authoritative_generated_scan tests ---
566
+
567
+ def test_authoritative_generated_scan_returns_default_for_no_state() -> None:
568
+ result = app._authoritative_generated_scan(None)
569
+
570
+ assert isinstance(result, dict)
571
+ assert "status" in result
572
+
573
+
574
+ def test_authoritative_generated_scan_scans_output_path_when_present() -> None:
575
+ from pathlib import Path
576
+
577
+ path = app.ROOT / "outputs" / "test-auth-scan.png"
578
+ path.parent.mkdir(parents=True, exist_ok=True)
579
+ path.write_bytes(b"\x89PNG\r\n\x1a\n\x00\x00\x00\rIEND\xaeB`\x82")
580
+ state = {"generation": {"output_path": str(path)}}
581
+
582
+ result = app._authoritative_generated_scan(state)
583
+
584
+ assert result["status"] in {"pass", "review"}
585
+ assert "export_gate" in result
586
+
587
+
588
+ def test_authoritative_generated_scan_falls_back_to_stored_scan() -> None:
589
+ stored_scan = {"status": "review", "export_gate": "blocked", "findings": ["trailing data"]}
590
+ state = {"generated_scan": stored_scan}
591
+
592
+ result = app._authoritative_generated_scan(state)
593
+
594
+ assert result == stored_scan
595
+
596
+
597
+ def test_authoritative_generated_scan_uses_output_path_over_stored_scan() -> None:
598
+ from pathlib import Path
599
+
600
+ path = app.ROOT / "outputs" / "test-auth-scan-prefer.png"
601
+ path.parent.mkdir(parents=True, exist_ok=True)
602
+ path.write_bytes(b"\x89PNG\r\n\x1a\n\x00\x00\x00\rIEND\xaeB`\x82")
603
+ stale_scan = {"status": "review", "export_gate": "blocked"}
604
+ state = {
605
+ "generation": {"output_path": str(path)},
606
+ "generated_scan": stale_scan,
607
+ }
608
+
609
+ result = app._authoritative_generated_scan(state)
610
+
611
+ # Should re-scan from file, not use stored scan
612
+ assert result["status"] == "pass"
613
+ assert result["export_gate"] == "clear"
614
+
615
+
616
  def test_export_packet_allows_blocked_st3gg_with_explicit_override(monkeypatch) -> None:
617
  monkeypatch.setenv("NEXUS_EXPORT_DIR", "outputs/test-exports")
618
  run = build_command_center_run("gothic patent leather platform boots")
tests/test_command_center.py CHANGED
@@ -262,14 +262,6 @@ def test_render_trust_strip_pass_scan_shows_clear_export() -> None:
262
  assert "all checks passed" in html
263
 
264
 
265
- def test_render_trust_strip_clean_empty_findings_shows_no_findings() -> None:
266
- scan = {"status": "pass", "export_gate": "clear", "findings": [], "purification_actions": []}
267
- html = render_trust_strip(scan=scan)
268
-
269
- assert "No findings." in html
270
- assert "No upload selected. Always-on scanner ready." not in html
271
-
272
-
273
  def test_render_trust_strip_review_scan_shows_blocked_export() -> None:
274
  scan = {"status": "review", "export_gate": "blocked", "findings": ["trailing data after IEND"], "purification_actions": ["truncate PNG"]}
275
  html = render_trust_strip(scan=scan)
@@ -524,3 +516,278 @@ def test_public_demo_false_models_are_excluded_from_public_filter() -> None:
524
  )
525
  models_public, _ = filter_catalog(False)
526
  assert all(m.public_demo for m in models_public)
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
262
  assert "all checks passed" in html
263
 
264
 
 
 
 
 
 
 
 
 
265
  def test_render_trust_strip_review_scan_shows_blocked_export() -> None:
266
  scan = {"status": "review", "export_gate": "blocked", "findings": ["trailing data after IEND"], "purification_actions": ["truncate PNG"]}
267
  html = render_trust_strip(scan=scan)
 
516
  )
517
  models_public, _ = filter_catalog(False)
518
  assert all(m.public_demo for m in models_public)
519
+
520
+
521
+ # --- catalog FLUX.2-klein-4B tests ---
522
+
523
+ def test_filter_catalog_includes_flux_4b_in_public_mode() -> None:
524
+ models, _ = filter_catalog(False)
525
+ repo_ids = {model.repo_id for model in models}
526
+
527
+ assert "black-forest-labs/FLUX.2-klein-4B" in repo_ids
528
+
529
+
530
+ def test_filter_catalog_includes_flux_4b_in_adult_mode() -> None:
531
+ models, _ = filter_catalog(True)
532
+ repo_ids = {model.repo_id for model in models}
533
+
534
+ assert "black-forest-labs/FLUX.2-klein-4B" in repo_ids
535
+
536
+
537
+ def test_flux_4b_is_not_in_default_active_stack() -> None:
538
+ stack = active_stack(False)
539
+ repo_ids = {model.repo_id for model in stack}
540
+
541
+ assert "black-forest-labs/FLUX.2-klein-4B" not in repo_ids
542
+
543
+
544
+ def test_flux_4b_role_is_tiny_titan_sidecar() -> None:
545
+ models, _ = filter_catalog(False)
546
+ flux_4b = next((m for m in models if m.repo_id == "black-forest-labs/FLUX.2-klein-4B"), None)
547
+
548
+ assert flux_4b is not None
549
+ assert "tiny_titan" in flux_4b.role
550
+
551
+
552
+ def test_flux_4b_has_apache_license() -> None:
553
+ models, _ = filter_catalog(False)
554
+ flux_4b = next((m for m in models if m.repo_id == "black-forest-labs/FLUX.2-klein-4B"), None)
555
+
556
+ assert flux_4b is not None
557
+ assert flux_4b.license == "apache-2.0"
558
+
559
+
560
+ # --- parameter_budget over_budget tests ---
561
+
562
+ def test_parameter_budget_passes_for_default_public_stack() -> None:
563
+ budget = parameter_budget(active_stack(False))
564
+
565
+ assert budget["status"] == "pass"
566
+ assert budget["active_b"] <= 32.0
567
+
568
+
569
+ def test_parameter_budget_over_budget_for_large_stack() -> None:
570
+ large_stack = [
571
+ ModelCandidate(
572
+ repo_id=f"test/model-{i}",
573
+ role="test",
574
+ task="text-to-image",
575
+ params_b=10.0,
576
+ runtime="local",
577
+ license="apache-2.0",
578
+ )
579
+ for i in range(5) # 50B total, over the 32B limit
580
+ ]
581
+ budget = parameter_budget(large_stack)
582
+
583
+ assert budget["status"] == "over_budget"
584
+ assert budget["active_b"] == 50.0
585
+ assert budget["remaining_b"] < 0.0
586
+
587
+
588
+ def test_parameter_budget_exactly_at_limit() -> None:
589
+ stack = [
590
+ ModelCandidate(
591
+ repo_id="test/model",
592
+ role="test",
593
+ task="text-to-image",
594
+ params_b=32.0,
595
+ runtime="local",
596
+ license="apache-2.0",
597
+ )
598
+ ]
599
+ budget = parameter_budget(stack)
600
+
601
+ assert budget["status"] == "pass"
602
+ assert budget["active_b"] == 32.0
603
+ assert budget["remaining_b"] == 0.0
604
+
605
+
606
+ def test_parameter_budget_just_over_limit() -> None:
607
+ stack = [
608
+ ModelCandidate(
609
+ repo_id="test/model",
610
+ role="test",
611
+ task="text-to-image",
612
+ params_b=32.1,
613
+ runtime="local",
614
+ license="apache-2.0",
615
+ )
616
+ ]
617
+ budget = parameter_budget(stack)
618
+
619
+ assert budget["status"] == "over_budget"
620
+
621
+
622
+ def test_catalog_summary_includes_budget_keys() -> None:
623
+ summary = catalog_summary(False)
624
+
625
+ assert "active_b" in summary
626
+ assert "limit_b" in summary
627
+ assert "remaining_b" in summary
628
+ assert "status" in summary
629
+ assert summary["limit_b"] == 32.0
630
+
631
+
632
+ def test_catalog_summary_models_visible_count_matches_filter() -> None:
633
+ models, adapters = filter_catalog(False)
634
+ summary = catalog_summary(False)
635
+
636
+ assert summary["models_visible"] == len(models)
637
+ assert summary["adapters_visible"] == len(adapters)
638
+
639
+
640
+ # --- catalog MiniCPM-V-4.6 and Nemotron-Parse tests ---
641
+
642
+ def test_minicpm_v46_in_public_catalog() -> None:
643
+ models, _ = filter_catalog(False)
644
+ repo_ids = {model.repo_id for model in models}
645
+
646
+ assert "openbmb/MiniCPM-V-4.6" in repo_ids
647
+
648
+
649
+ def test_nemotron_parse_in_public_catalog() -> None:
650
+ models, _ = filter_catalog(False)
651
+ repo_ids = {model.repo_id for model in models}
652
+
653
+ assert "nvidia/NVIDIA-Nemotron-Parse-v1.2" in repo_ids
654
+
655
+
656
+ def test_minicpm_v46_role_is_sponsor_visual_judge() -> None:
657
+ models, _ = filter_catalog(False)
658
+ minicpm = next((m for m in models if m.repo_id == "openbmb/MiniCPM-V-4.6"), None)
659
+
660
+ assert minicpm is not None
661
+ assert minicpm.role == "sponsor_visual_judge"
662
+ assert minicpm.params_b == 1.30
663
+
664
+
665
+ def test_nemotron_parse_role_is_sponsor_structured_parse() -> None:
666
+ models, _ = filter_catalog(False)
667
+ nemotron = next((m for m in models if m.repo_id == "nvidia/NVIDIA-Nemotron-Parse-v1.2"), None)
668
+
669
+ assert nemotron is not None
670
+ assert nemotron.role == "sponsor_structured_parse"
671
+ assert nemotron.params_b == 0.94
672
+
673
+
674
+ # --- adapter catalog changes tests ---
675
+
676
+ def test_dever_style_lora_compatible_with_9b_and_4b() -> None:
677
+ from nexus_visual_weaver.catalog import ADAPTER_CATALOG
678
+
679
+ dever = next((a for a in ADAPTER_CATALOG if a.repo_id == "DeverStyle/Flux.2-Klein-Loras"), None)
680
+
681
+ assert dever is not None
682
+ assert "black-forest-labs/FLUX.2-klein-9B" in dever.compatible_repo_ids
683
+ assert "black-forest-labs/FLUX.2-klein-4B" in dever.compatible_repo_ids
684
+
685
+
686
+ def test_dever_style_lora_has_trigger_words() -> None:
687
+ from nexus_visual_weaver.catalog import ADAPTER_CATALOG
688
+
689
+ dever = next((a for a in ADAPTER_CATALOG if a.repo_id == "DeverStyle/Flux.2-Klein-Loras"), None)
690
+
691
+ assert dever is not None
692
+ assert len(dever.trigger_words) > 0
693
+ assert "gothic couture" in dever.trigger_words
694
+
695
+
696
+ def test_4b_outpaint_lora_requires_image() -> None:
697
+ from nexus_visual_weaver.catalog import ADAPTER_CATALOG
698
+
699
+ outpaint = next((a for a in ADAPTER_CATALOG if a.repo_id == "fal/flux-2-klein-4B-outpaint-lora"), None)
700
+
701
+ assert outpaint is not None
702
+ assert outpaint.requires_image is True
703
+ assert "black-forest-labs/FLUX.2-klein-4B" in outpaint.compatible_repo_ids
704
+
705
+
706
+ # --- wardrobe controls tests ---
707
+
708
+ def test_build_outfit_graph_controls_override_outerwear() -> None:
709
+ outfit = build_outfit_graph(
710
+ "minimal portrait",
711
+ controls={"outerwear": "tailored rain slicker"},
712
+ )
713
+ outerwear_slot = next((s for s in outfit.slots if s.name == "outerwear"), None)
714
+
715
+ assert outerwear_slot is not None
716
+ assert outerwear_slot.description == "tailored rain slicker"
717
+
718
+
719
+ def test_build_outfit_graph_controls_override_footwear() -> None:
720
+ outfit = build_outfit_graph(
721
+ "minimal portrait",
722
+ controls={"footwear": "patent leather heels"},
723
+ )
724
+ footwear_slot = next((s for s in outfit.slots if s.name == "footwear"), None)
725
+
726
+ assert footwear_slot is not None
727
+ assert footwear_slot.description == "patent leather heels"
728
+
729
+
730
+ def test_build_outfit_graph_controls_override_palette_for_all_slots() -> None:
731
+ custom_palette = "obsidian, pearl, crimson"
732
+ outfit = build_outfit_graph(
733
+ "minimal portrait",
734
+ controls={"palette": custom_palette},
735
+ )
736
+
737
+ for slot in outfit.slots:
738
+ assert slot.palette == custom_palette
739
+
740
+
741
+ def test_build_outfit_graph_controls_override_hardware() -> None:
742
+ outfit = build_outfit_graph(
743
+ "minimal portrait",
744
+ controls={"hardware": "silver occult buckles"},
745
+ )
746
+ jewelry_slot = next((s for s in outfit.slots if s.name == "jewelry"), None)
747
+
748
+ assert jewelry_slot is not None
749
+ assert jewelry_slot.material == "silver occult buckles"
750
+
751
+
752
+ def test_build_outfit_graph_locked_slots_control_locks_named_slots() -> None:
753
+ outfit = build_outfit_graph(
754
+ "minimal portrait",
755
+ controls={"locked_slots": ["hair_headwear", "props"]},
756
+ )
757
+ hair_slot = next((s for s in outfit.slots if s.name == "hair_headwear"), None)
758
+ props_slot = next((s for s in outfit.slots if s.name == "props"), None)
759
+
760
+ assert hair_slot is not None and hair_slot.locked is True
761
+ assert props_slot is not None and props_slot.locked is True
762
+
763
+
764
+ def test_build_outfit_graph_locate_focus_sets_manual_focus_region() -> None:
765
+ outfit = build_outfit_graph(
766
+ "minimal portrait",
767
+ controls={"locate_focus": ["footwear", "jewelry"]},
768
+ )
769
+ footwear_slot = next((s for s in outfit.slots if s.name == "footwear"), None)
770
+ jewelry_slot = next((s for s in outfit.slots if s.name == "jewelry"), None)
771
+ hair_slot = next((s for s in outfit.slots if s.name == "hair_headwear"), None)
772
+
773
+ assert footwear_slot is not None and footwear_slot.locate_region == "manual-focus"
774
+ assert jewelry_slot is not None and jewelry_slot.locate_region == "manual-focus"
775
+ assert hair_slot is not None and hair_slot.locate_region == "auto-map"
776
+
777
+
778
+ def test_build_outfit_graph_with_no_controls_uses_defaults() -> None:
779
+ outfit = build_outfit_graph("gothic couture brief")
780
+
781
+ assert len(outfit.slots) == len(outfit.slots) # All slots present
782
+ assert outfit.score >= 0.68 # Base score
783
+
784
+
785
+ def test_build_outfit_graph_adult_mode_marks_upper_lower_slots() -> None:
786
+ outfit = build_outfit_graph("gothic brief", adult_mode=True)
787
+ upper = next((s for s in outfit.slots if s.name == "upper_body"), None)
788
+ lower = next((s for s in outfit.slots if s.name == "lower_body"), None)
789
+ outerwear = next((s for s in outfit.slots if s.name == "outerwear"), None)
790
+
791
+ assert upper is not None and upper.adult_only is True
792
+ assert lower is not None and lower.adult_only is True
793
+ assert outerwear is not None and outerwear.adult_only is False
tests/test_exporter.py CHANGED
@@ -286,25 +286,6 @@ def test_export_packet_redacts_raw_payload_paths_and_secret_like_values(monkeypa
286
  assert payload["provider_states"]["minicpm"] == "failed"
287
 
288
 
289
- def test_export_packet_redacts_windows_backslash_paths_on_linux(monkeypatch) -> None:
290
- monkeypatch.setenv("NEXUS_EXPORT_DIR", "outputs/test-exports")
291
- run = build_command_center_run("redaction brief")
292
- scan = {
293
- "status": "review",
294
- "export_gate": "blocked",
295
- "findings": [r"raw hidden bytes at C:\Users\speci.000\Downloads\thing.png"],
296
- "purification_actions": [],
297
- }
298
- state = _make_base_state()
299
-
300
- result = write_export_packet(run=run, scan=scan, operator_state=state, adult_mode=False)
301
- payload = json.loads(Path(result["path"]).read_text(encoding="utf-8"))
302
- serialized = json.dumps(payload)
303
-
304
- assert r"C:\Users\speci.000\Downloads" not in serialized
305
- assert "[local_path]/thing.png" in serialized
306
-
307
-
308
  def test_export_packet_records_st3gg_override_reason(monkeypatch) -> None:
309
  monkeypatch.setenv("NEXUS_EXPORT_DIR", "outputs/test-exports")
310
  run = build_command_center_run("gothic patent leather platform boots")
@@ -322,3 +303,376 @@ def test_export_packet_records_st3gg_override_reason(monkeypatch) -> None:
322
  "used": True,
323
  "reason": "Operator reviewed the ST3GG findings and is writing an audit packet only.",
324
  }
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
286
  assert payload["provider_states"]["minicpm"] == "failed"
287
 
288
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
289
  def test_export_packet_records_st3gg_override_reason(monkeypatch) -> None:
290
  monkeypatch.setenv("NEXUS_EXPORT_DIR", "outputs/test-exports")
291
  run = build_command_center_run("gothic patent leather platform boots")
 
303
  "used": True,
304
  "reason": "Operator reviewed the ST3GG findings and is writing an audit packet only.",
305
  }
306
+
307
+
308
+ # --- _is_within tests ---
309
+
310
+ from nexus_visual_weaver.exporter import (
311
+ _artifact_name,
312
+ _is_within,
313
+ _safe_dict,
314
+ _safe_export_candidate,
315
+ _safe_provider,
316
+ _safe_reference_metadata,
317
+ _safe_scan,
318
+ _sanitize_text,
319
+ REPO_ROOT,
320
+ )
321
+
322
+
323
+ def test_is_within_returns_true_for_child_path() -> None:
324
+ root = Path("/some/root")
325
+ child = Path("/some/root/subdir/file.txt")
326
+
327
+ assert _is_within(child, root) is True
328
+
329
+
330
+ def test_is_within_returns_false_for_sibling_path() -> None:
331
+ root = Path("/some/root")
332
+ sibling = Path("/some/other/file.txt")
333
+
334
+ assert _is_within(sibling, root) is False
335
+
336
+
337
+ def test_is_within_returns_false_for_parent_path() -> None:
338
+ root = Path("/some/root/subdir")
339
+ parent = Path("/some/root")
340
+
341
+ assert _is_within(parent, root) is False
342
+
343
+
344
+ def test_is_within_returns_false_for_same_path() -> None:
345
+ root = Path("/some/root")
346
+ # Same path is not "within" itself (relative_to with same path succeeds but returns empty Path)
347
+ # _is_within should return True for same path since relative_to doesn't raise
348
+ result = _is_within(root, root)
349
+ # Same path: relative_to returns Path('.') or empty - shouldn't raise, returns True
350
+ assert result is True
351
+
352
+
353
+ # --- _artifact_name tests ---
354
+
355
+ def test_artifact_name_returns_filename_from_unix_path() -> None:
356
+ assert _artifact_name("/data/nexus_visual_weaver/artifact.png") == "artifact.png"
357
+
358
+
359
+ def test_artifact_name_returns_filename_from_windows_path() -> None:
360
+ assert _artifact_name(r"C:\Users\user\Downloads\artifact.png") == "artifact.png"
361
+
362
+
363
+ def test_artifact_name_returns_none_for_none() -> None:
364
+ assert _artifact_name(None) is None
365
+
366
+
367
+ def test_artifact_name_returns_none_for_empty_string() -> None:
368
+ assert _artifact_name("") is None
369
+
370
+
371
+ def test_artifact_name_returns_filename_for_simple_name() -> None:
372
+ assert _artifact_name("artifact.png") == "artifact.png"
373
+
374
+
375
+ # --- _sanitize_text tests ---
376
+
377
+ def test_sanitize_text_redacts_hf_token() -> None:
378
+ fake_token = "hf_" + "abcdefghijklmnopqrstuvwxyz1234567"
379
+ text = f"token is {fake_token}"
380
+ result = _sanitize_text(text)
381
+
382
+ assert fake_token not in result
383
+ assert "[redacted_secret]" in result
384
+
385
+
386
+ def test_sanitize_text_redacts_bearer_token() -> None:
387
+ fake_token = "sk-test-" + "abcdefghijklmnopqrstu"
388
+ text = "Authorization: Bearer " + fake_token
389
+ result = _sanitize_text(text)
390
+
391
+ assert fake_token not in result
392
+
393
+
394
+ def test_sanitize_text_redacts_credential_names() -> None:
395
+ text = "Please set MINICPM_API_KEY and NEMOTRON_API_KEY"
396
+ result = _sanitize_text(text)
397
+
398
+ assert "MINICPM_API_KEY" not in result
399
+ assert "NEMOTRON_API_KEY" not in result
400
+ assert "[redacted_credential_name]" in result
401
+
402
+
403
+ def test_sanitize_text_redacts_windows_paths() -> None:
404
+ text = "Found at C:\\Users\\user\\Downloads\\artifact.png"
405
+ result = _sanitize_text(text)
406
+
407
+ assert "C:\\Users\\user\\Downloads" not in result
408
+ assert "[local_path]/artifact.png" in result
409
+
410
+
411
+ def test_sanitize_text_redacts_data_paths() -> None:
412
+ text = "Output stored at /data/nexus_visual_weaver/artifact.png"
413
+ result = _sanitize_text(text)
414
+
415
+ assert "/data/nexus_visual_weaver" not in result
416
+ assert "[data]/" in result
417
+
418
+
419
+ def test_sanitize_text_truncates_to_1000_chars() -> None:
420
+ long_text = "safe text " * 200
421
+ result = _sanitize_text(long_text)
422
+
423
+ assert len(result) <= 1000
424
+ assert result.endswith("...")
425
+
426
+
427
+ def test_sanitize_text_does_not_truncate_short_text() -> None:
428
+ short_text = "safe short text without secrets"
429
+ result = _sanitize_text(short_text)
430
+
431
+ assert result == short_text
432
+
433
+
434
+ # --- _safe_dict tests ---
435
+
436
+ def test_safe_dict_drops_sensitive_keys() -> None:
437
+ data = {
438
+ "status": "ok",
439
+ "token": "hf_secretvalue",
440
+ "api_key": "sk-secretvalue",
441
+ "message": "clean",
442
+ }
443
+ result = _safe_dict(data)
444
+
445
+ assert "status" in result
446
+ assert "message" in result
447
+ assert "token" not in result
448
+ assert "api_key" not in result
449
+
450
+
451
+ def test_safe_dict_preserves_safe_keys() -> None:
452
+ data = {"status": "pass", "export_gate": "clear", "extension": ".png"}
453
+ result = _safe_dict(data)
454
+
455
+ assert result == data
456
+
457
+
458
+ def test_safe_dict_handles_nested_dicts() -> None:
459
+ data = {"outer": {"secret": "hidden", "safe_field": "visible"}}
460
+ result = _safe_dict(data)
461
+
462
+ assert "secret" not in result.get("outer", {})
463
+ assert result["outer"]["safe_field"] == "visible"
464
+
465
+
466
+ def test_safe_dict_limits_lists_to_40() -> None:
467
+ data = list(range(50))
468
+ result = _safe_dict(data)
469
+
470
+ assert len(result) == 40
471
+
472
+
473
+ def test_safe_dict_sanitizes_string_values() -> None:
474
+ data = {"message": "Error: MINICPM_API_KEY not set"}
475
+ result = _safe_dict(data)
476
+
477
+ assert "MINICPM_API_KEY" not in result.get("message", "")
478
+
479
+
480
+ def test_safe_dict_preserves_size_bytes_when_allowed() -> None:
481
+ data = {"size_bytes": 1024, "token": "hf_" + "secret123456789012345"}
482
+ result = _safe_dict(data, allow_size_bytes=True)
483
+
484
+ assert "size_bytes" in result
485
+ assert result["size_bytes"] == 1024
486
+ assert "token" not in result
487
+
488
+
489
+ def test_safe_dict_drops_size_bytes_by_default() -> None:
490
+ # "bytes" matches the sensitive key pattern
491
+ data = {"size_bytes": 1024, "status": "ok"}
492
+ result = _safe_dict(data)
493
+
494
+ assert "size_bytes" not in result
495
+
496
+
497
+ def test_safe_dict_returns_non_dict_non_list_unchanged() -> None:
498
+ assert _safe_dict(42) == 42
499
+ assert _safe_dict(3.14) == 3.14
500
+ assert _safe_dict(True) is True
501
+ assert _safe_dict(None) is None
502
+
503
+
504
+ # --- _safe_scan tests ---
505
+
506
+ def test_safe_scan_extracts_required_fields() -> None:
507
+ scan = {
508
+ "status": "pass",
509
+ "scanner": "ST3GG v2",
510
+ "export_gate": "clear",
511
+ "extension": ".png",
512
+ "magic": "PNG",
513
+ "findings": ["no issues"],
514
+ "purification_actions": ["none needed"],
515
+ "payload_excerpt": "HIDDEN_SENSITIVE_DATA",
516
+ }
517
+ result = _safe_scan(scan)
518
+
519
+ assert result["status"] == "pass"
520
+ assert result["scanner"] == "ST3GG v2"
521
+ assert result["export_gate"] == "clear"
522
+ assert result["extension"] == ".png"
523
+ assert result["magic"] == "PNG"
524
+ assert "payload_excerpt" not in result
525
+
526
+
527
+ def test_safe_scan_handles_empty_scan() -> None:
528
+ result = _safe_scan({})
529
+
530
+ assert "status" in result
531
+ assert "export_gate" in result
532
+ assert result["status"] is None
533
+ assert result["export_gate"] is None
534
+
535
+
536
+ def test_safe_scan_sanitizes_findings() -> None:
537
+ scan = {
538
+ "findings": ["found MINICPM_API_KEY pattern", "raw bytes at C:\\Users\\data\\file.png"],
539
+ }
540
+ result = _safe_scan(scan)
541
+
542
+ serialized = json.dumps(result)
543
+ assert "MINICPM_API_KEY" not in serialized
544
+ assert "C:\\Users\\data" not in serialized
545
+
546
+
547
+ # --- _safe_provider tests ---
548
+
549
+ def test_safe_provider_extracts_fields() -> None:
550
+ provider = {
551
+ "status": "success",
552
+ "provider_state": "configured",
553
+ "provider": "OpenBMB",
554
+ "repo_id": "openbmb/MiniCPM-V-4.6",
555
+ "model": "MiniCPM-V-4.6",
556
+ "message": "judge returned evidence",
557
+ "evidence": {"overall_status": "pass"},
558
+ "latency_seconds": 1.23,
559
+ }
560
+ result = _safe_provider(provider)
561
+
562
+ assert result["status"] == "success"
563
+ assert result["provider"] == "OpenBMB"
564
+ assert result["repo_id"] == "openbmb/MiniCPM-V-4.6"
565
+ assert result["latency_seconds"] == 1.23
566
+ assert result["evidence"]["overall_status"] == "pass"
567
+
568
+
569
+ def test_safe_provider_handles_none_input() -> None:
570
+ result = _safe_provider(None)
571
+
572
+ assert isinstance(result, dict)
573
+ assert "status" in result
574
+ assert result["status"] is None
575
+
576
+
577
+ def test_safe_provider_redacts_secret_values_in_evidence() -> None:
578
+ provider = {
579
+ "status": "failed",
580
+ "evidence": {"token_used": "hf_" + "abcdefghijklmnopqrstuvwx", "raw": "sensitive-data"},
581
+ }
582
+ result = _safe_provider(provider)
583
+
584
+ assert "token_used" not in result["evidence"]
585
+ assert "raw" not in result["evidence"]
586
+
587
+
588
+ def test_safe_provider_handles_non_dict_evidence() -> None:
589
+ provider = {"status": "success", "evidence": "plain string evidence"}
590
+ result = _safe_provider(provider)
591
+
592
+ # Non-dict evidence should be treated as empty
593
+ assert result["evidence"] == {}
594
+
595
+
596
+ # --- _safe_reference_metadata tests ---
597
+
598
+ def test_safe_reference_metadata_returns_empty_for_non_list() -> None:
599
+ assert _safe_reference_metadata(None) == []
600
+ assert _safe_reference_metadata("string") == []
601
+ assert _safe_reference_metadata(42) == []
602
+
603
+
604
+ def test_safe_reference_metadata_extracts_fields_from_records() -> None:
605
+ records = [
606
+ {
607
+ "source": "upload",
608
+ "status": "pass",
609
+ "basename": "C:/Users/user/Downloads/reference.png",
610
+ "sha256": "a" * 64,
611
+ "size_bytes": 1024,
612
+ "st3gg_status": "pass",
613
+ "export_gate": "clear",
614
+ "magic": "PNG",
615
+ "extension": ".png",
616
+ }
617
+ ]
618
+ result = _safe_reference_metadata(records)
619
+
620
+ assert len(result) == 1
621
+ assert result[0]["source"] == "upload"
622
+ assert result[0]["basename"] == "reference.png" # Only filename, not full path
623
+ assert result[0]["sha256"] == "a" * 64
624
+ assert result[0]["export_gate"] == "clear"
625
+
626
+
627
+ def test_safe_reference_metadata_limits_to_20_records() -> None:
628
+ records = [{"source": "upload", "sha256": str(i)} for i in range(30)]
629
+ result = _safe_reference_metadata(records)
630
+
631
+ assert len(result) == 20
632
+
633
+
634
+ def test_safe_reference_metadata_skips_non_dict_records() -> None:
635
+ records = [{"source": "upload"}, "not a dict", None, {"source": "url"}]
636
+ result = _safe_reference_metadata(records)
637
+
638
+ assert len(result) == 2
639
+ assert all(isinstance(r, dict) for r in result)
640
+
641
+
642
+ def test_safe_reference_metadata_includes_url_fields() -> None:
643
+ records = [
644
+ {
645
+ "source": "url",
646
+ "status": "metadata_only",
647
+ "domain": "shop.example.test",
648
+ "url_hash": "b" * 64,
649
+ }
650
+ ]
651
+ result = _safe_reference_metadata(records)
652
+
653
+ assert result[0]["domain"] == "shop.example.test"
654
+ assert result[0]["url_hash"] == "b" * 64
655
+
656
+
657
+ # --- _safe_export_candidate tests ---
658
+
659
+ def test_safe_export_candidate_accepts_outputs_dir() -> None:
660
+ candidate = REPO_ROOT / "outputs" / "exports" / "test-subdir"
661
+ result = _safe_export_candidate(candidate)
662
+
663
+ assert result is not None
664
+ assert "outputs" in str(result)
665
+
666
+
667
+ def test_safe_export_candidate_rejects_src_dir() -> None:
668
+ candidate = REPO_ROOT / "src" / "nexus_visual_weaver" / "exports"
669
+ result = _safe_export_candidate(candidate)
670
+
671
+ assert result is None
672
+
673
+
674
+ def test_safe_export_candidate_rejects_src_root_itself() -> None:
675
+ candidate = REPO_ROOT / "src"
676
+ result = _safe_export_candidate(candidate)
677
+
678
+ assert result is None
tests/test_hf_runtime.py CHANGED
@@ -4,6 +4,9 @@ from nexus_visual_weaver.hf_runtime import (
4
  FLUX_REPO_ID,
5
  PRIVATE_RESEARCH_FLUX_REPO_ID,
6
  TINY_TITAN_FLUX_REPO_ID,
 
 
 
7
  default_lora_repo_id,
8
  generate_flux_image,
9
  hf_runtime_enabled,
@@ -54,3 +57,155 @@ def test_artifact_lane_embeds_generated_image() -> None:
54
  assert "nw-preview-real-image" in html
55
  assert "data:image/png;base64" in html
56
  assert "Real FLUX.2 Klein artifact" in html
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
4
  FLUX_REPO_ID,
5
  PRIVATE_RESEARCH_FLUX_REPO_ID,
6
  TINY_TITAN_FLUX_REPO_ID,
7
+ _adapter_recipe,
8
+ _repo_candidates,
9
+ active_flux_repo_id,
10
  default_lora_repo_id,
11
  generate_flux_image,
12
  hf_runtime_enabled,
 
57
  assert "nw-preview-real-image" in html
58
  assert "data:image/png;base64" in html
59
  assert "Real FLUX.2 Klein artifact" in html
60
+
61
+
62
+ # --- active_flux_repo_id tests ---
63
+
64
+ def test_active_flux_repo_id_defaults_to_9b(monkeypatch) -> None:
65
+ monkeypatch.delenv("NEXUS_FLUX_REPO_ID", raising=False)
66
+ monkeypatch.delenv("NEXUS_TINY_TITAN_MODE", raising=False)
67
+
68
+ assert active_flux_repo_id() == FLUX_REPO_ID
69
+
70
+
71
+ def test_active_flux_repo_id_returns_tiny_titan_when_mode_set(monkeypatch) -> None:
72
+ monkeypatch.delenv("NEXUS_FLUX_REPO_ID", raising=False)
73
+ monkeypatch.setenv("NEXUS_TINY_TITAN_MODE", "1")
74
+
75
+ assert active_flux_repo_id() == TINY_TITAN_FLUX_REPO_ID
76
+
77
+
78
+ def test_active_flux_repo_id_returns_configured_repo_id(monkeypatch) -> None:
79
+ monkeypatch.setenv("NEXUS_FLUX_REPO_ID", "custom/flux-model")
80
+ monkeypatch.delenv("NEXUS_TINY_TITAN_MODE", raising=False)
81
+
82
+ assert active_flux_repo_id() == "custom/flux-model"
83
+
84
+
85
+ def test_active_flux_repo_id_configured_env_overrides_tiny_titan(monkeypatch) -> None:
86
+ monkeypatch.setenv("NEXUS_FLUX_REPO_ID", "custom/flux-model")
87
+ monkeypatch.setenv("NEXUS_TINY_TITAN_MODE", "1")
88
+
89
+ # Configured env takes precedence over TINY_TITAN_MODE
90
+ assert active_flux_repo_id() == "custom/flux-model"
91
+
92
+
93
+ # --- _repo_candidates tests ---
94
+
95
+ def test_repo_candidates_includes_primary_and_tiny_titan_fallback(monkeypatch) -> None:
96
+ monkeypatch.delenv("NEXUS_DISABLE_TINY_TITAN_FALLBACK", raising=False)
97
+
98
+ candidates = _repo_candidates(FLUX_REPO_ID)
99
+
100
+ assert candidates[0] == FLUX_REPO_ID
101
+ assert TINY_TITAN_FLUX_REPO_ID in candidates
102
+ assert len(candidates) == 2
103
+
104
+
105
+ def test_repo_candidates_only_primary_when_tiny_titan_is_primary(monkeypatch) -> None:
106
+ monkeypatch.delenv("NEXUS_DISABLE_TINY_TITAN_FALLBACK", raising=False)
107
+
108
+ candidates = _repo_candidates(TINY_TITAN_FLUX_REPO_ID)
109
+
110
+ assert candidates == [TINY_TITAN_FLUX_REPO_ID]
111
+
112
+
113
+ def test_repo_candidates_only_primary_when_fallback_disabled(monkeypatch) -> None:
114
+ monkeypatch.setenv("NEXUS_DISABLE_TINY_TITAN_FALLBACK", "1")
115
+
116
+ candidates = _repo_candidates(FLUX_REPO_ID)
117
+
118
+ assert candidates == [FLUX_REPO_ID]
119
+
120
+
121
+ def test_repo_candidates_first_is_always_primary(monkeypatch) -> None:
122
+ monkeypatch.delenv("NEXUS_DISABLE_TINY_TITAN_FALLBACK", raising=False)
123
+
124
+ for repo_id in [FLUX_REPO_ID, "other/model"]:
125
+ candidates = _repo_candidates(repo_id)
126
+ assert candidates[0] == repo_id
127
+
128
+
129
+ # --- _adapter_recipe tests ---
130
+
131
+ def test_adapter_recipe_returns_none_for_none_input() -> None:
132
+ assert _adapter_recipe(None) is None
133
+
134
+
135
+ def test_adapter_recipe_returns_none_for_unknown_repo_id() -> None:
136
+ assert _adapter_recipe("nonexistent/unknown-repo") is None
137
+
138
+
139
+ def test_adapter_recipe_returns_recipe_for_known_repo_id() -> None:
140
+ result = _adapter_recipe("DeverStyle/Flux.2-Klein-Loras")
141
+
142
+ assert result is not None
143
+ assert result.repo_id == "DeverStyle/Flux.2-Klein-Loras"
144
+
145
+
146
+ def test_adapter_recipe_returns_recipe_for_4b_outpaint() -> None:
147
+ result = _adapter_recipe("fal/flux-2-klein-4B-outpaint-lora")
148
+
149
+ assert result is not None
150
+ assert result.requires_image is True
151
+
152
+
153
+ # --- HFGenerationResult disabled path lora fields tests ---
154
+
155
+ def test_generate_flux_image_disabled_includes_lora_repo_id(monkeypatch) -> None:
156
+ monkeypatch.setenv("NEXUS_DISABLE_REAL_HF", "1")
157
+ monkeypatch.delenv("NEXUS_FLUX_REPO_ID", raising=False)
158
+ monkeypatch.delenv("NEXUS_TINY_TITAN_MODE", raising=False)
159
+
160
+ result = generate_flux_image("test prompt")
161
+
162
+ assert result.status == "disabled"
163
+ assert result.lora_status == "disabled"
164
+ # Default lora_repo_id should be the DeverStyle adapter for the 9B model
165
+ assert result.lora_repo_id == "DeverStyle/Flux.2-Klein-Loras"
166
+
167
+
168
+ def test_generate_flux_image_disabled_includes_fallback_used_false(monkeypatch) -> None:
169
+ monkeypatch.setenv("NEXUS_DISABLE_REAL_HF", "1")
170
+
171
+ result = generate_flux_image("test prompt")
172
+
173
+ assert result.fallback_used is False
174
+ assert result.primary_error is None
175
+
176
+
177
+ def test_generate_flux_image_tiny_titan_mode_uses_4b_repo_id(monkeypatch) -> None:
178
+ monkeypatch.setenv("NEXUS_DISABLE_REAL_HF", "1")
179
+ monkeypatch.setenv("NEXUS_TINY_TITAN_MODE", "1")
180
+
181
+ result = generate_flux_image("test prompt")
182
+
183
+ assert result.status == "disabled"
184
+ assert result.repo_id == TINY_TITAN_FLUX_REPO_ID
185
+
186
+
187
+ def test_generate_flux_image_disabled_uses_lora_message() -> None:
188
+ import os
189
+ os.environ["NEXUS_DISABLE_REAL_HF"] = "1"
190
+ try:
191
+ result = generate_flux_image("test prompt")
192
+ assert "LoRA loading requires" in result.lora_message
193
+ finally:
194
+ del os.environ["NEXUS_DISABLE_REAL_HF"]
195
+
196
+
197
+ # --- default_lora_repo_id tests ---
198
+
199
+ def test_default_lora_repo_id_returns_none_for_unknown_model() -> None:
200
+ result = default_lora_repo_id("unknown/nonexistent-model")
201
+ assert result is None
202
+
203
+
204
+ def test_default_lora_repo_id_excludes_requires_image_adapters() -> None:
205
+ # fal/flux-2-klein-4B-outpaint-lora requires_image=True, so it should be excluded
206
+ result = default_lora_repo_id("black-forest-labs/FLUX.2-klein-4B")
207
+ # DeverStyle/Flux.2-Klein-Loras is compatible with 4B via compatible_repo_ids
208
+ assert result is not None
209
+ recipe_result = _adapter_recipe(result)
210
+ assert recipe_result is not None
211
+ assert recipe_result.requires_image is False
tests/test_lora_adapter.py CHANGED
@@ -1,4 +1,11 @@
1
- from nexus_visual_weaver.lora_adapter import load_and_apply, unload_all
 
 
 
 
 
 
 
2
  from nexus_visual_weaver.schema import AdapterRecipe
3
 
4
 
@@ -94,3 +101,277 @@ def test_unload_all_uses_pipeline_unload_hook() -> None:
94
  unload_all(pipe)
95
 
96
  assert pipe.unloaded is True
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ from nexus_visual_weaver.lora_adapter import (
2
+ _short_error,
3
+ _status,
4
+ adapter_to_dict,
5
+ is_compatible,
6
+ load_and_apply,
7
+ unload_all,
8
+ )
9
  from nexus_visual_weaver.schema import AdapterRecipe
10
 
11
 
 
101
  unload_all(pipe)
102
 
103
  assert pipe.unloaded is True
104
+
105
+
106
+ def test_unload_all_handles_pipes_without_unload_method() -> None:
107
+ # UnsupportedPipe has no unload_lora_weights method, should not raise
108
+ pipe = UnsupportedPipe()
109
+ unload_all(pipe) # Should complete without error
110
+
111
+
112
+ def test_unload_all_silences_exceptions() -> None:
113
+ class RaisingPipe:
114
+ def unload_lora_weights(self) -> None:
115
+ raise RuntimeError("unload failed")
116
+
117
+ unload_all(RaisingPipe()) # Should not raise
118
+
119
+
120
+ # --- _status tests ---
121
+
122
+ def test_status_returns_dict_with_status_field() -> None:
123
+ result = _status("disabled")
124
+
125
+ assert result["status"] == "disabled"
126
+ assert result["repo_id"] is None
127
+ assert result["adapter_for"] is None
128
+ assert result["weight"] is None
129
+
130
+
131
+ def test_status_with_recipe_extracts_recipe_fields() -> None:
132
+ recipe = AdapterRecipe(
133
+ repo_id="example/style-lora",
134
+ adapter_for="black-forest-labs/FLUX.2-klein-9B",
135
+ task="style",
136
+ weight=0.6,
137
+ )
138
+ result = _status("loaded", recipe)
139
+
140
+ assert result["repo_id"] == "example/style-lora"
141
+ assert result["adapter_for"] == "black-forest-labs/FLUX.2-klein-9B"
142
+ assert result["weight"] == 0.6
143
+
144
+
145
+ def test_status_with_extra_kwargs_included() -> None:
146
+ result = _status("loaded", message="Adapter applied.", adapter_name="nexus_style")
147
+
148
+ assert result["message"] == "Adapter applied."
149
+ assert result["adapter_name"] == "nexus_style"
150
+
151
+
152
+ # --- _short_error tests ---
153
+
154
+ def test_lora_short_error_includes_class_name() -> None:
155
+ exc = ValueError("bad input")
156
+ result = _short_error(exc)
157
+
158
+ assert result.startswith("ValueError:")
159
+ assert "bad input" in result
160
+
161
+
162
+ def test_lora_short_error_truncates_at_240() -> None:
163
+ exc = RuntimeError("x" * 300)
164
+ result = _short_error(exc)
165
+
166
+ # Should be truncated
167
+ assert len(result) <= len("RuntimeError: ") + 240
168
+ assert result.endswith("...")
169
+
170
+
171
+ def test_lora_short_error_collapses_newlines() -> None:
172
+ exc = OSError("line one\nline two")
173
+ result = _short_error(exc)
174
+
175
+ assert "\n" not in result
176
+ assert "line one" in result
177
+
178
+
179
+ # --- adapter_to_dict tests ---
180
+
181
+ def test_adapter_to_dict_returns_complete_dict() -> None:
182
+ recipe = AdapterRecipe(
183
+ repo_id="example/style-lora",
184
+ adapter_for="black-forest-labs/FLUX.2-klein-9B",
185
+ task="style",
186
+ weight=0.75,
187
+ license="apache-2.0",
188
+ )
189
+ result = adapter_to_dict(recipe)
190
+
191
+ assert result["repo_id"] == "example/style-lora"
192
+ assert result["adapter_for"] == "black-forest-labs/FLUX.2-klein-9B"
193
+ assert result["task"] == "style"
194
+ assert result["weight"] == 0.75
195
+ assert result["license"] == "apache-2.0"
196
+ assert isinstance(result, dict)
197
+
198
+
199
+ def test_adapter_to_dict_includes_all_fields() -> None:
200
+ recipe = AdapterRecipe(
201
+ repo_id="example/adult-lora",
202
+ adapter_for="model/base",
203
+ task="style",
204
+ adult_only=True,
205
+ requires_image=True,
206
+ runtime_enabled=False,
207
+ )
208
+ result = adapter_to_dict(recipe)
209
+
210
+ assert result["adult_only"] is True
211
+ assert result["requires_image"] is True
212
+ assert result["runtime_enabled"] is False
213
+
214
+
215
+ # --- is_compatible tests ---
216
+
217
+ def test_is_compatible_returns_true_for_matching_adapter() -> None:
218
+ recipe = AdapterRecipe(
219
+ repo_id="example/style-lora",
220
+ adapter_for="black-forest-labs/FLUX.2-klein-9B",
221
+ task="style",
222
+ )
223
+ pipe = FakeLoraPipe()
224
+
225
+ assert is_compatible(pipe, recipe, "black-forest-labs/FLUX.2-klein-9B") is True
226
+
227
+
228
+ def test_is_compatible_returns_false_for_runtime_disabled() -> None:
229
+ recipe = AdapterRecipe(
230
+ repo_id="example/style-lora",
231
+ adapter_for="black-forest-labs/FLUX.2-klein-9B",
232
+ task="style",
233
+ runtime_enabled=False,
234
+ )
235
+ pipe = FakeLoraPipe()
236
+
237
+ assert is_compatible(pipe, recipe, "black-forest-labs/FLUX.2-klein-9B") is False
238
+
239
+
240
+ def test_is_compatible_returns_false_for_adult_only_without_adult_mode() -> None:
241
+ recipe = AdapterRecipe(
242
+ repo_id="example/adult-lora",
243
+ adapter_for="black-forest-labs/FLUX.2-klein-9B",
244
+ task="style",
245
+ adult_only=True,
246
+ )
247
+ pipe = FakeLoraPipe()
248
+
249
+ assert is_compatible(pipe, recipe, "black-forest-labs/FLUX.2-klein-9B", adult_mode=False) is False
250
+
251
+
252
+ def test_is_compatible_returns_true_for_adult_only_with_adult_mode() -> None:
253
+ recipe = AdapterRecipe(
254
+ repo_id="example/adult-lora",
255
+ adapter_for="black-forest-labs/FLUX.2-klein-9B",
256
+ task="style",
257
+ adult_only=True,
258
+ )
259
+ pipe = FakeLoraPipe()
260
+
261
+ assert is_compatible(pipe, recipe, "black-forest-labs/FLUX.2-klein-9B", adult_mode=True) is True
262
+
263
+
264
+ def test_is_compatible_returns_false_for_requires_image() -> None:
265
+ recipe = AdapterRecipe(
266
+ repo_id="example/inpaint-lora",
267
+ adapter_for="black-forest-labs/FLUX.2-klein-4B",
268
+ task="inpaint",
269
+ requires_image=True,
270
+ )
271
+ pipe = FakeLoraPipe()
272
+
273
+ assert is_compatible(pipe, recipe, "black-forest-labs/FLUX.2-klein-4B") is False
274
+
275
+
276
+ def test_is_compatible_returns_false_for_pipeline_without_lora_support() -> None:
277
+ recipe = AdapterRecipe(
278
+ repo_id="example/style-lora",
279
+ adapter_for="black-forest-labs/FLUX.2-klein-9B",
280
+ task="style",
281
+ )
282
+
283
+ assert is_compatible(UnsupportedPipe(), recipe, "black-forest-labs/FLUX.2-klein-9B") is False
284
+
285
+
286
+ def test_is_compatible_returns_false_for_incompatible_target() -> None:
287
+ recipe = AdapterRecipe(
288
+ repo_id="example/style-lora",
289
+ adapter_for="black-forest-labs/FLUX.2-klein-9B",
290
+ task="style",
291
+ )
292
+ pipe = FakeLoraPipe()
293
+
294
+ assert is_compatible(pipe, recipe, "completely/different-model") is False
295
+
296
+
297
+ def test_is_compatible_returns_true_for_compatible_repo_ids() -> None:
298
+ recipe = AdapterRecipe(
299
+ repo_id="example/style-lora",
300
+ adapter_for="black-forest-labs/FLUX.2-klein-9B",
301
+ task="style",
302
+ compatible_repo_ids=["black-forest-labs/FLUX.2-klein-4B"],
303
+ )
304
+ pipe = FakeLoraPipe()
305
+
306
+ assert is_compatible(pipe, recipe, "black-forest-labs/FLUX.2-klein-4B") is True
307
+
308
+
309
+ # --- load_and_apply with adult_only recipe tests ---
310
+
311
+ def test_load_and_apply_skips_adult_only_in_non_adult_mode() -> None:
312
+ recipe = AdapterRecipe(
313
+ repo_id="example/adult-lora",
314
+ adapter_for="black-forest-labs/FLUX.2-klein-9B",
315
+ task="style",
316
+ adult_only=True,
317
+ )
318
+ pipe = FakeLoraPipe()
319
+
320
+ result = load_and_apply(pipe, recipe, "black-forest-labs/FLUX.2-klein-9B", adult_mode=False)
321
+
322
+ assert result["status"] == "skipped_incompatible"
323
+ assert "Adult Mode is off" in result["message"]
324
+
325
+
326
+ def test_load_and_apply_loads_adult_only_in_adult_mode() -> None:
327
+ recipe = AdapterRecipe(
328
+ repo_id="example/adult-lora",
329
+ adapter_for="black-forest-labs/FLUX.2-klein-9B",
330
+ task="style",
331
+ adult_only=True,
332
+ )
333
+ pipe = FakeLoraPipe()
334
+
335
+ result = load_and_apply(pipe, recipe, "black-forest-labs/FLUX.2-klein-9B", adult_mode=True)
336
+
337
+ assert result["status"] == "loaded"
338
+
339
+
340
+ def test_load_and_apply_skips_requires_image_adapter() -> None:
341
+ recipe = AdapterRecipe(
342
+ repo_id="example/inpaint-lora",
343
+ adapter_for="black-forest-labs/FLUX.2-klein-4B",
344
+ task="inpaint",
345
+ requires_image=True,
346
+ )
347
+ pipe = FakeLoraPipe()
348
+
349
+ result = load_and_apply(pipe, recipe, "black-forest-labs/FLUX.2-klein-4B")
350
+
351
+ assert result["status"] == "skipped_incompatible"
352
+ assert "image-conditioning" in result["message"]
353
+
354
+
355
+ def test_load_and_apply_uses_weight_name_when_present() -> None:
356
+ recipe = AdapterRecipe(
357
+ repo_id="example/weight-lora",
358
+ adapter_for="black-forest-labs/FLUX.2-klein-9B",
359
+ task="style",
360
+ weight_name="style_weights.safetensors",
361
+ )
362
+ pipe = FakeLoraPipe()
363
+
364
+ load_and_apply(pipe, recipe, "black-forest-labs/FLUX.2-klein-9B")
365
+
366
+ assert len(pipe.loaded) == 1
367
+ _, kwargs = pipe.loaded[0]
368
+ assert kwargs.get("weight_name") == "style_weights.safetensors"
369
+
370
+
371
+ def test_load_and_apply_no_recipe_returns_disabled() -> None:
372
+ pipe = FakeLoraPipe()
373
+
374
+ result = load_and_apply(pipe, None, "any/model")
375
+
376
+ assert result["status"] == "disabled"
377
+ assert result["repo_id"] is None