Deploy v4 review hardening
Browse files- app.py +8 -3
- docs/HACKATHON_EVALUATION.md +3 -3
- docs/HANDOFF_FINAL_HACKATHON.md +6 -6
- src/nexus_visual_weaver/exporter.py +27 -11
- src/nexus_visual_weaver/hf_runtime.py +2 -1
- src/nexus_visual_weaver/lora_adapter.py +1 -0
- src/nexus_visual_weaver/model_relay.py +3 -2
- src/nexus_visual_weaver/planner.py +11 -3
- src/nexus_visual_weaver/provider_runtime.py +37 -2
- src/nexus_visual_weaver/wardrobe.py +5 -1
- tests/test_app_callbacks.py +28 -0
- tests/test_command_center.py +18 -1
- tests/test_exporter.py +47 -0
- tests/test_hf_runtime.py +71 -0
- tests/test_lora_adapter.py +20 -0
- tests/test_model_relay.py +13 -0
- tests/test_provider_runtime.py +153 -25
app.py
CHANGED
|
@@ -629,7 +629,11 @@ def approve_checkpoint(
|
|
| 629 |
"checkpoint": "approved",
|
| 630 |
"generated_scan": scan,
|
| 631 |
"export": export_state,
|
| 632 |
-
"message":
|
|
|
|
|
|
|
|
|
|
|
|
|
| 633 |
}
|
| 634 |
return _render_stateful(run, adult_mode, scan, active_section, next_state)
|
| 635 |
|
|
@@ -675,9 +679,10 @@ def export_packet(
|
|
| 675 |
next_state = {**state, "provider_state": "blocked", "export": scan.get("export_gate", "blocked"), "message": "Export blocked: ST3GG gate is not clear. Add an explicit override reason to write an audit packet."}
|
| 676 |
else:
|
| 677 |
export_state = "clear" if scan.get("export_gate") == "clear" else "override"
|
|
|
|
| 678 |
export_operator_state = {
|
| 679 |
**state,
|
| 680 |
-
**({"st3gg_override_reason": override_reason} if
|
| 681 |
"export": export_state,
|
| 682 |
}
|
| 683 |
export = write_export_packet(run=run, scan=scan, operator_state=export_operator_state, adult_mode=adult_mode)
|
|
@@ -839,7 +844,7 @@ with gr.Blocks(title="NEXUS Visual Weaver") as demo:
|
|
| 839 |
reset_btn = gr.Button("Reset Demo State", scale=1)
|
| 840 |
override_reason = gr.Textbox(
|
| 841 |
label="ST3GG Override Reason",
|
| 842 |
-
placeholder="Required
|
| 843 |
lines=2,
|
| 844 |
max_lines=3,
|
| 845 |
)
|
|
|
|
| 629 |
"checkpoint": "approved",
|
| 630 |
"generated_scan": scan,
|
| 631 |
"export": export_state,
|
| 632 |
+
"message": (
|
| 633 |
+
"Checkpoint approved. Export is ready after clear ST3GG scan."
|
| 634 |
+
if export_state == "clear"
|
| 635 |
+
else "Checkpoint approved. ST3GG is not clear; add an override reason and click Prepare Export Packet to write an audit packet."
|
| 636 |
+
),
|
| 637 |
}
|
| 638 |
return _render_stateful(run, adult_mode, scan, active_section, next_state)
|
| 639 |
|
|
|
|
| 679 |
next_state = {**state, "provider_state": "blocked", "export": scan.get("export_gate", "blocked"), "message": "Export blocked: ST3GG gate is not clear. Add an explicit override reason to write an audit packet."}
|
| 680 |
else:
|
| 681 |
export_state = "clear" if scan.get("export_gate") == "clear" else "override"
|
| 682 |
+
override_applies = scan.get("export_gate") != "clear" and bool(override_reason)
|
| 683 |
export_operator_state = {
|
| 684 |
**state,
|
| 685 |
+
**({"st3gg_override_reason": override_reason} if override_applies else {}),
|
| 686 |
"export": export_state,
|
| 687 |
}
|
| 688 |
export = write_export_packet(run=run, scan=scan, operator_state=export_operator_state, adult_mode=adult_mode)
|
|
|
|
| 844 |
reset_btn = gr.Button("Reset Demo State", scale=1)
|
| 845 |
override_reason = gr.Textbox(
|
| 846 |
label="ST3GG Override Reason",
|
| 847 |
+
placeholder="Required when ST3GG is review/blocked; explain why this audit packet may be written.",
|
| 848 |
lines=2,
|
| 849 |
max_lines=3,
|
| 850 |
)
|
docs/HACKATHON_EVALUATION.md
CHANGED
|
@@ -13,8 +13,8 @@ NEXUS Visual Weaver should open as a working command center, not a landing page.
|
|
| 13 |
## Current Strengths
|
| 14 |
|
| 15 |
- Gradio-compatible app shape with `mcp_server=True`.
|
| 16 |
-
- Pinned model governance is visible: FLUX.2 Klein
|
| 17 |
-
- Real FLUX.2 Klein
|
| 18 |
- Generated artifacts are scanned by ST3GG before checkpoint/export.
|
| 19 |
- Above-fold trust strip makes ST3GG verdict, export gate, fixture evidence, and adult-mode safety boundaries visible immediately.
|
| 20 |
- OpenBMB MiniCPM-V 4.6 and NVIDIA Nemotron evidence lanes are represented as real optional provider adapters with missing-secret/failed/success states.
|
|
@@ -48,5 +48,5 @@ NEXUS Visual Weaver should open as a working command center, not a landing page.
|
|
| 48 |
| OpenBMB | Claim only after MiniCPM-V returns `success` in export evidence. |
|
| 49 |
| NVIDIA | Claim only after Nemotron returns `success` in export evidence. |
|
| 50 |
| OpenAI Codex | GitHub branch/PR provides Codex development trail. |
|
| 51 |
-
| Tiny Titan |
|
| 52 |
| Modal | Not claimed unless a real Modal job runs. |
|
|
|
|
| 13 |
## Current Strengths
|
| 14 |
|
| 15 |
- Gradio-compatible app shape with `mcp_server=True`.
|
| 16 |
+
- Pinned model governance is visible: FLUX.2 Klein 9B, LocateAnything-3B, and ST3GG, with FLUX.2 Klein 4B kept as a sidecar fallback.
|
| 17 |
+
- Real FLUX.2 Klein 9B generation is wired for HF Space, falls back to the 4B sidecar when needed, and reports an honest dry-run state outside Space.
|
| 18 |
- Generated artifacts are scanned by ST3GG before checkpoint/export.
|
| 19 |
- Above-fold trust strip makes ST3GG verdict, export gate, fixture evidence, and adult-mode safety boundaries visible immediately.
|
| 20 |
- OpenBMB MiniCPM-V 4.6 and NVIDIA Nemotron evidence lanes are represented as real optional provider adapters with missing-secret/failed/success states.
|
|
|
|
| 48 |
| OpenBMB | Claim only after MiniCPM-V returns `success` in export evidence. |
|
| 49 |
| NVIDIA | Claim only after Nemotron returns `success` in export evidence. |
|
| 50 |
| OpenAI Codex | GitHub branch/PR provides Codex development trail. |
|
| 51 |
+
| Tiny Titan | Sidecar-only: claim only from an explicit 4B export packet where every active model is <=4B. |
|
| 52 |
| Modal | Not claimed unless a real Modal job runs. |
|
docs/HANDOFF_FINAL_HACKATHON.md
CHANGED
|
@@ -8,13 +8,13 @@
|
|
| 8 |
- Public Space URL: `https://build-small-hackathon-nexus-visual-weaver-a107340.hf.space/`
|
| 9 |
- HF rollback SHA: `410a467c55d11e7308249198bd5fe0b2c190aec6`.
|
| 10 |
- Branch discipline: use only `main` and `codex/specimba/ui-polish-command-center`; no extra recovery branches.
|
| 11 |
-
- Primary goal: finish a countable Build Small submission with real FLUX.2
|
| 12 |
|
| 13 |
## Secrets Needed
|
| 14 |
|
| 15 |
Do not paste these into chat, commits, logs, or export packets.
|
| 16 |
|
| 17 |
-
- `HF_TOKEN`:
|
| 18 |
- `MINICPM_BASE_URL`: OpenBMB OpenAI-compatible endpoint base URL.
|
| 19 |
- `MINICPM_API_KEY`: OpenBMB bearer token.
|
| 20 |
- `MINICPM_MODEL`: default `MiniCPM-V-4.6`.
|
|
@@ -39,7 +39,7 @@ Avoid pytest `--basetemp=C:\tmp` in this Windows sandbox if `tmp_path` fixtures
|
|
| 39 |
## Runtime Flow
|
| 40 |
|
| 41 |
1. `run_active_weave` builds the Raven Chronicle run packet.
|
| 42 |
-
2. FLUX.2 Klein
|
| 43 |
3. Generated artifact is scanned by ST3GG.
|
| 44 |
4. MiniCPM-V judge runs when OpenBMB secrets are present.
|
| 45 |
5. Nemotron evidence runs when Nemotron/NVIDIA endpoint secrets are present.
|
|
@@ -51,14 +51,14 @@ Avoid pytest `--basetemp=C:\tmp` in this Windows sandbox if `tmp_path` fixtures
|
|
| 51 |
- OpenBMB prize claim requires `minicpm_judge.status == "success"` in an export packet.
|
| 52 |
- NVIDIA prize claim requires `nemotron_evidence.status == "success"` in an export packet.
|
| 53 |
- LocateAnything supports the grounding story but does not replace Nemotron for the NVIDIA prize.
|
| 54 |
-
- Tiny Titan can be claimed only from
|
| 55 |
-
- FLUX.2 Klein 9B
|
| 56 |
- Modal is not claimed unless a real Modal job runs and is documented.
|
| 57 |
|
| 58 |
## Known Risks
|
| 59 |
|
| 60 |
- GitHub CLI may fail behind proxy `127.0.0.1:9`; use local git status and HF verification when blocked.
|
| 61 |
-
- Real FLUX generation depends on Space GPU availability and the
|
| 62 |
- OpenBMB and Nemotron endpoints are optional and must show `missing secret` rather than fake success when not configured.
|
| 63 |
- Demo video and social post links must be added before final submission.
|
| 64 |
|
|
|
|
| 8 |
- Public Space URL: `https://build-small-hackathon-nexus-visual-weaver-a107340.hf.space/`
|
| 9 |
- HF rollback SHA: `410a467c55d11e7308249198bd5fe0b2c190aec6`.
|
| 10 |
- Branch discipline: use only `main` and `codex/specimba/ui-polish-command-center`; no extra recovery branches.
|
| 11 |
+
- Primary goal: finish a countable Build Small submission with real FLUX.2 Klein 9B generation, 4B sidecar fallback, ST3GG scan, optional OpenBMB MiniCPM-V judge evidence, optional NVIDIA Nemotron evidence, checkpointed export packet, README prize mapping, demo video, and social post.
|
| 12 |
|
| 13 |
## Secrets Needed
|
| 14 |
|
| 15 |
Do not paste these into chat, commits, logs, or export packets.
|
| 16 |
|
| 17 |
+
- `HF_TOKEN`: required when the FLUX.2 Klein 9B Space runtime needs authenticated access; the 4B sidecar remains the public-safe fallback/Tiny Titan lane.
|
| 18 |
- `MINICPM_BASE_URL`: OpenBMB OpenAI-compatible endpoint base URL.
|
| 19 |
- `MINICPM_API_KEY`: OpenBMB bearer token.
|
| 20 |
- `MINICPM_MODEL`: default `MiniCPM-V-4.6`.
|
|
|
|
| 39 |
## Runtime Flow
|
| 40 |
|
| 41 |
1. `run_active_weave` builds the Raven Chronicle run packet.
|
| 42 |
+
2. FLUX.2 Klein 9B generates the image on Space when HF runtime is enabled, with FLUX.2 Klein 4B as the fallback/sidecar lane.
|
| 43 |
3. Generated artifact is scanned by ST3GG.
|
| 44 |
4. MiniCPM-V judge runs when OpenBMB secrets are present.
|
| 45 |
5. Nemotron evidence runs when Nemotron/NVIDIA endpoint secrets are present.
|
|
|
|
| 51 |
- OpenBMB prize claim requires `minicpm_judge.status == "success"` in an export packet.
|
| 52 |
- NVIDIA prize claim requires `nemotron_evidence.status == "success"` in an export packet.
|
| 53 |
- LocateAnything supports the grounding story but does not replace Nemotron for the NVIDIA prize.
|
| 54 |
+
- Tiny Titan can be claimed only from an explicit 4B sidecar export packet where every active model is <=4B.
|
| 55 |
+
- Raven Quality Stack uses FLUX.2 Klein 9B by default; OFFELLIA/Gemma remain private research options behind Adult Mode/catalog partitioning.
|
| 56 |
- Modal is not claimed unless a real Modal job runs and is documented.
|
| 57 |
|
| 58 |
## Known Risks
|
| 59 |
|
| 60 |
- GitHub CLI may fail behind proxy `127.0.0.1:9`; use local git status and HF verification when blocked.
|
| 61 |
+
- Real FLUX generation depends on Space GPU availability and the 9B runtime loading successfully, with a 4B sidecar fallback when configured.
|
| 62 |
- OpenBMB and Nemotron endpoints are optional and must show `missing secret` rather than fake success when not configured.
|
| 63 |
- Demo video and social post links must be added before final submission.
|
| 64 |
|
src/nexus_visual_weaver/exporter.py
CHANGED
|
@@ -64,7 +64,18 @@ def _artifact_name(output_path: Any) -> str | None:
|
|
| 64 |
"""
|
| 65 |
if not output_path:
|
| 66 |
return None
|
| 67 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 68 |
|
| 69 |
|
| 70 |
SENSITIVE_KEY_RE = re.compile(r"(token|secret|api[_-]?key|authorization|payload_excerpt|raw|base64|bytes)", re.IGNORECASE)
|
|
@@ -245,7 +256,8 @@ def write_export_packet(
|
|
| 245 |
Dictionary with "path" (str) pointing to the written JSON file and "packet" (dict)
|
| 246 |
containing the constructed export packet.
|
| 247 |
"""
|
| 248 |
-
|
|
|
|
| 249 |
run_adult_mode = bool(getattr(getattr(run, "request", None), "adult_mode", adult_mode))
|
| 250 |
stack = list(getattr(run, "model_stack", None) or active_stack(run_adult_mode))
|
| 251 |
budget = parameter_budget(stack)
|
|
@@ -260,11 +272,12 @@ def write_export_packet(
|
|
| 260 |
st3gg_scan = _safe_scan(scan)
|
| 261 |
minicpm_judge = _safe_provider(operator_state.get("minicpm_judge") or {})
|
| 262 |
nemotron_evidence = _safe_provider(operator_state.get("nemotron_evidence") or {})
|
|
|
|
| 263 |
provider_states = {
|
| 264 |
"generation": generation.get("provider_state"),
|
| 265 |
"minicpm": minicpm_judge.get("status"),
|
| 266 |
"nemotron": nemotron_evidence.get("status"),
|
| 267 |
-
"operator":
|
| 268 |
}
|
| 269 |
override_reason = _safe_dict(operator_state.get("st3gg_override_reason", ""))
|
| 270 |
packet = {
|
|
@@ -272,8 +285,8 @@ def write_export_packet(
|
|
| 272 |
"run_id": run_id,
|
| 273 |
"created_at_epoch": int(time.time()),
|
| 274 |
"adult_mode": run_adult_mode,
|
| 275 |
-
"prompt": getattr(getattr(run, "request", None), "prompt", ""),
|
| 276 |
-
"refined_prompt": getattr(getattr(run, "refined_prompt", None), "refined", ""),
|
| 277 |
"artifact": artifact,
|
| 278 |
"image_basename": artifact,
|
| 279 |
"creator_controls": creator_controls,
|
|
@@ -296,13 +309,13 @@ def write_export_packet(
|
|
| 296 |
"minicpm_judge": minicpm_judge,
|
| 297 |
"nemotron_evidence": nemotron_evidence,
|
| 298 |
"checkpoint": {
|
| 299 |
-
"status": operator_state.get("checkpoint"),
|
| 300 |
-
"message": operator_state.get("message"),
|
| 301 |
-
"recommendation": getattr(getattr(run, "checkpoint", None), "recommendation", None),
|
| 302 |
"trust_score": getattr(getattr(run, "checkpoint", None), "trust_score", None),
|
| 303 |
-
"required_actions": getattr(getattr(run, "checkpoint", None), "required_actions", []),
|
| 304 |
},
|
| 305 |
-
"provider_state":
|
| 306 |
"provider_states": provider_states,
|
| 307 |
"model_stack": [
|
| 308 |
{
|
|
@@ -324,6 +337,9 @@ def write_export_packet(
|
|
| 324 |
"st3gg_export_gate": st3gg_scan.get("export_gate"),
|
| 325 |
},
|
| 326 |
}
|
| 327 |
-
|
|
|
|
|
|
|
|
|
|
| 328 |
target.write_text(json.dumps(packet, indent=2, ensure_ascii=True), encoding="utf-8")
|
| 329 |
return {"path": str(target), "packet": packet}
|
|
|
|
| 64 |
"""
|
| 65 |
if not output_path:
|
| 66 |
return None
|
| 67 |
+
text = str(output_path)
|
| 68 |
+
if "\\" in text or re.match(r"^[A-Za-z]:[\\/]", text):
|
| 69 |
+
return PureWindowsPath(text).name
|
| 70 |
+
return Path(text).name
|
| 71 |
+
|
| 72 |
+
|
| 73 |
+
def _safe_run_id(value: Any) -> str:
|
| 74 |
+
text = str(value or f"nw-{int(time.time())}")
|
| 75 |
+
safe = re.sub(r"[^A-Za-z0-9_.-]+", "-", text).strip(".-")
|
| 76 |
+
while ".." in safe:
|
| 77 |
+
safe = safe.replace("..", ".")
|
| 78 |
+
return safe or f"nw-{int(time.time())}"
|
| 79 |
|
| 80 |
|
| 81 |
SENSITIVE_KEY_RE = re.compile(r"(token|secret|api[_-]?key|authorization|payload_excerpt|raw|base64|bytes)", re.IGNORECASE)
|
|
|
|
| 256 |
Dictionary with "path" (str) pointing to the written JSON file and "packet" (dict)
|
| 257 |
containing the constructed export packet.
|
| 258 |
"""
|
| 259 |
+
raw_run_id = getattr(getattr(run, "checkpoint", None), "checkpoint_id", f"nw-{int(time.time())}")
|
| 260 |
+
run_id = _safe_run_id(raw_run_id)
|
| 261 |
run_adult_mode = bool(getattr(getattr(run, "request", None), "adult_mode", adult_mode))
|
| 262 |
stack = list(getattr(run, "model_stack", None) or active_stack(run_adult_mode))
|
| 263 |
budget = parameter_budget(stack)
|
|
|
|
| 272 |
st3gg_scan = _safe_scan(scan)
|
| 273 |
minicpm_judge = _safe_provider(operator_state.get("minicpm_judge") or {})
|
| 274 |
nemotron_evidence = _safe_provider(operator_state.get("nemotron_evidence") or {})
|
| 275 |
+
operator_provider_state = _safe_dict(operator_state.get("provider_state"))
|
| 276 |
provider_states = {
|
| 277 |
"generation": generation.get("provider_state"),
|
| 278 |
"minicpm": minicpm_judge.get("status"),
|
| 279 |
"nemotron": nemotron_evidence.get("status"),
|
| 280 |
+
"operator": operator_provider_state,
|
| 281 |
}
|
| 282 |
override_reason = _safe_dict(operator_state.get("st3gg_override_reason", ""))
|
| 283 |
packet = {
|
|
|
|
| 285 |
"run_id": run_id,
|
| 286 |
"created_at_epoch": int(time.time()),
|
| 287 |
"adult_mode": run_adult_mode,
|
| 288 |
+
"prompt": _safe_dict(getattr(getattr(run, "request", None), "prompt", "")),
|
| 289 |
+
"refined_prompt": _safe_dict(getattr(getattr(run, "refined_prompt", None), "refined", "")),
|
| 290 |
"artifact": artifact,
|
| 291 |
"image_basename": artifact,
|
| 292 |
"creator_controls": creator_controls,
|
|
|
|
| 309 |
"minicpm_judge": minicpm_judge,
|
| 310 |
"nemotron_evidence": nemotron_evidence,
|
| 311 |
"checkpoint": {
|
| 312 |
+
"status": _safe_dict(operator_state.get("checkpoint")),
|
| 313 |
+
"message": _safe_dict(operator_state.get("message")),
|
| 314 |
+
"recommendation": _safe_dict(getattr(getattr(run, "checkpoint", None), "recommendation", None)),
|
| 315 |
"trust_score": getattr(getattr(run, "checkpoint", None), "trust_score", None),
|
| 316 |
+
"required_actions": _safe_dict(getattr(getattr(run, "checkpoint", None), "required_actions", [])),
|
| 317 |
},
|
| 318 |
+
"provider_state": operator_provider_state,
|
| 319 |
"provider_states": provider_states,
|
| 320 |
"model_stack": [
|
| 321 |
{
|
|
|
|
| 337 |
"st3gg_export_gate": st3gg_scan.get("export_gate"),
|
| 338 |
},
|
| 339 |
}
|
| 340 |
+
root = export_root()
|
| 341 |
+
target = (root / f"{run_id}.json").resolve(strict=False)
|
| 342 |
+
if not (target == root or _is_within(target, root)):
|
| 343 |
+
raise ValueError("Unsafe export target path.")
|
| 344 |
target.write_text(json.dumps(packet, indent=2, ensure_ascii=True), encoding="utf-8")
|
| 345 |
return {"path": str(target), "packet": packet}
|
src/nexus_visual_weaver/hf_runtime.py
CHANGED
|
@@ -8,6 +8,7 @@ import time
|
|
| 8 |
from dataclasses import dataclass, asdict
|
| 9 |
from pathlib import Path
|
| 10 |
from typing import Any
|
|
|
|
| 11 |
|
| 12 |
from .catalog import ADAPTER_CATALOG
|
| 13 |
from .lora_adapter import load_and_apply, unload_all
|
|
@@ -274,7 +275,7 @@ def generate_flux_image(
|
|
| 274 |
).images[0]
|
| 275 |
finally:
|
| 276 |
unload_all(pipe)
|
| 277 |
-
output_path = _output_dir() / f"nexus_flux_{
|
| 278 |
image.save(output_path)
|
| 279 |
return HFGenerationResult(
|
| 280 |
status="success",
|
|
|
|
| 8 |
from dataclasses import dataclass, asdict
|
| 9 |
from pathlib import Path
|
| 10 |
from typing import Any
|
| 11 |
+
from uuid import uuid4
|
| 12 |
|
| 13 |
from .catalog import ADAPTER_CATALOG
|
| 14 |
from .lora_adapter import load_and_apply, unload_all
|
|
|
|
| 275 |
).images[0]
|
| 276 |
finally:
|
| 277 |
unload_all(pipe)
|
| 278 |
+
output_path = _output_dir() / f"nexus_flux_{time.time_ns()}_{seed}_{uuid4().hex[:8]}.png"
|
| 279 |
image.save(output_path)
|
| 280 |
return HFGenerationResult(
|
| 281 |
status="success",
|
src/nexus_visual_weaver/lora_adapter.py
CHANGED
|
@@ -116,6 +116,7 @@ def load_and_apply(
|
|
| 116 |
pipe.set_adapters([adapter_name], adapter_weights=[recipe.weight])
|
| 117 |
return _status("loaded", recipe, message="Adapter loaded and applied for this generation.", adapter_name=adapter_name)
|
| 118 |
except Exception as exc:
|
|
|
|
| 119 |
return _status("failed", recipe, message=_short_error(exc), adapter_name=adapter_name)
|
| 120 |
|
| 121 |
|
|
|
|
| 116 |
pipe.set_adapters([adapter_name], adapter_weights=[recipe.weight])
|
| 117 |
return _status("loaded", recipe, message="Adapter loaded and applied for this generation.", adapter_name=adapter_name)
|
| 118 |
except Exception as exc:
|
| 119 |
+
unload_all(pipe)
|
| 120 |
return _status("failed", recipe, message=_short_error(exc), adapter_name=adapter_name)
|
| 121 |
|
| 122 |
|
src/nexus_visual_weaver/model_relay.py
CHANGED
|
@@ -196,7 +196,8 @@ class WeaverModelRelay:
|
|
| 196 |
lane_records = [record for record in self.records.values() if record.lane == lane]
|
| 197 |
if lane in PINNED_LANES:
|
| 198 |
primary = next((record for record in lane_records if record.pinned), None)
|
| 199 |
-
|
|
|
|
| 200 |
return LaneDecision(
|
| 201 |
lane=lane,
|
| 202 |
strategy="pinned",
|
|
@@ -206,7 +207,7 @@ class WeaverModelRelay:
|
|
| 206 |
expected_cost_hint=primary.cost_hint if primary else "unavailable",
|
| 207 |
quota_impact=self._quota_impact(primary, now) if primary else {},
|
| 208 |
context_packet=context,
|
| 209 |
-
skipped=
|
| 210 |
)
|
| 211 |
|
| 212 |
candidates, skipped = self._eligible_records(lane_records, budget_b, public_demo, strategy, now)
|
|
|
|
| 196 |
lane_records = [record for record in self.records.values() if record.lane == lane]
|
| 197 |
if lane in PINNED_LANES:
|
| 198 |
primary = next((record for record in lane_records if record.pinned), None)
|
| 199 |
+
fallback_records = [self.records[model_id] for model_id in (primary.fallback_chain if primary else ()) if model_id in self.records]
|
| 200 |
+
fallbacks, skipped = self._eligible_records(fallback_records, budget_b, public_demo, strategy, now)
|
| 201 |
return LaneDecision(
|
| 202 |
lane=lane,
|
| 203 |
strategy="pinned",
|
|
|
|
| 207 |
expected_cost_hint=primary.cost_hint if primary else "unavailable",
|
| 208 |
quota_impact=self._quota_impact(primary, now) if primary else {},
|
| 209 |
context_packet=context,
|
| 210 |
+
skipped=skipped if primary else [f"{lane}: no pinned model registered"],
|
| 211 |
)
|
| 212 |
|
| 213 |
candidates, skipped = self._eligible_records(lane_records, budget_b, public_demo, strategy, now)
|
src/nexus_visual_weaver/planner.py
CHANGED
|
@@ -2,6 +2,7 @@
|
|
| 2 |
|
| 3 |
from __future__ import annotations
|
| 4 |
|
|
|
|
| 5 |
from uuid import uuid4
|
| 6 |
|
| 7 |
from .catalog import ADAPTER_CATALOG, active_stack
|
|
@@ -34,12 +35,19 @@ def build_command_center_run(
|
|
| 34 |
Returns:
|
| 35 |
A GenerationRun containing the refined prompt, generated outfit, inspection results, model stack, adapters, video plan, lore components, and a human checkpoint with approval recommendation based on a computed trust score.
|
| 36 |
"""
|
| 37 |
-
controls = creator_controls or {}
|
| 38 |
-
references = reference_metadata or []
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 39 |
request = CreativeRequest(
|
| 40 |
prompt=prompt,
|
| 41 |
adult_mode=adult_mode,
|
| 42 |
-
references=
|
| 43 |
creator_controls=controls,
|
| 44 |
reference_metadata=references,
|
| 45 |
)
|
|
|
|
| 2 |
|
| 3 |
from __future__ import annotations
|
| 4 |
|
| 5 |
+
from copy import deepcopy
|
| 6 |
from uuid import uuid4
|
| 7 |
|
| 8 |
from .catalog import ADAPTER_CATALOG, active_stack
|
|
|
|
| 35 |
Returns:
|
| 36 |
A GenerationRun containing the refined prompt, generated outfit, inspection results, model stack, adapters, video plan, lore components, and a human checkpoint with approval recommendation based on a computed trust score.
|
| 37 |
"""
|
| 38 |
+
controls = deepcopy(creator_controls or {})
|
| 39 |
+
references = deepcopy(reference_metadata or [])
|
| 40 |
+
reference_ids: list[str] = []
|
| 41 |
+
for item in references:
|
| 42 |
+
if not isinstance(item, dict):
|
| 43 |
+
continue
|
| 44 |
+
value = item.get("id") or item.get("basename") or item.get("url_hash")
|
| 45 |
+
if value is not None and str(value):
|
| 46 |
+
reference_ids.append(str(value))
|
| 47 |
request = CreativeRequest(
|
| 48 |
prompt=prompt,
|
| 49 |
adult_mode=adult_mode,
|
| 50 |
+
references=reference_ids,
|
| 51 |
creator_controls=controls,
|
| 52 |
reference_metadata=references,
|
| 53 |
)
|
src/nexus_visual_weaver/provider_runtime.py
CHANGED
|
@@ -17,6 +17,14 @@ from typing import Any
|
|
| 17 |
OPENBMB_REPO_ID = "openbmb/MiniCPM-V-4.6"
|
| 18 |
NEMOTRON_PARSE_REPO_ID = "nvidia/NVIDIA-Nemotron-Parse-v1.2"
|
| 19 |
NEMOTRON_NANO_REPO_ID = "nvidia/NVIDIA-Nemotron-3-Nano-4B-GGUF"
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 20 |
|
| 21 |
|
| 22 |
@dataclass(frozen=True)
|
|
@@ -67,13 +75,36 @@ def _image_data_url(path: str | None) -> str | None:
|
|
| 67 |
if not target.exists() or not target.is_file():
|
| 68 |
return None
|
| 69 |
suffix = target.suffix.lower()
|
| 70 |
-
mime =
|
|
|
|
|
|
|
| 71 |
data = base64.b64encode(target.read_bytes()).decode("ascii")
|
| 72 |
return f"data:{mime};base64,{data}"
|
| 73 |
except OSError:
|
| 74 |
return None
|
| 75 |
|
| 76 |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 77 |
def _post_json(url: str, token: str, payload: dict[str, Any], timeout: float) -> dict[str, Any]:
|
| 78 |
"""
|
| 79 |
Send an authenticated JSON POST request and return the parsed response.
|
|
@@ -91,6 +122,8 @@ def _post_json(url: str, token: str, payload: dict[str, Any], timeout: float) ->
|
|
| 91 |
parsed = urllib.parse.urlparse(url)
|
| 92 |
if parsed.scheme not in {"http", "https"} or not parsed.netloc:
|
| 93 |
raise ValueError(f"Invalid URL: expected http(s) URL with host, got {url!r}.")
|
|
|
|
|
|
|
| 94 |
body = json.dumps(payload).encode("utf-8")
|
| 95 |
request = urllib.request.Request(
|
| 96 |
url,
|
|
@@ -287,12 +320,14 @@ def judge_with_nemotron(
|
|
| 287 |
"Return strict JSON only with keys: sponsor_model_used, structured_parse, "
|
| 288 |
"risk_notes, parameter_budget_notes, final_claim_status. Parse this visual creation run."
|
| 289 |
)
|
|
|
|
|
|
|
| 290 |
payload = {
|
| 291 |
"model": model,
|
| 292 |
"messages": [
|
| 293 |
{
|
| 294 |
"role": "user",
|
| 295 |
-
"content": f"{instruction}\nPrompt: {prompt}\nRun: {json.dumps(
|
| 296 |
}
|
| 297 |
],
|
| 298 |
"temperature": 0.1,
|
|
|
|
| 17 |
OPENBMB_REPO_ID = "openbmb/MiniCPM-V-4.6"
|
| 18 |
NEMOTRON_PARSE_REPO_ID = "nvidia/NVIDIA-Nemotron-Parse-v1.2"
|
| 19 |
NEMOTRON_NANO_REPO_ID = "nvidia/NVIDIA-Nemotron-3-Nano-4B-GGUF"
|
| 20 |
+
MAX_PROVIDER_IMAGE_BYTES = 10 * 1024 * 1024
|
| 21 |
+
IMAGE_MIME_BY_SUFFIX = {
|
| 22 |
+
".png": "image/png",
|
| 23 |
+
".jpg": "image/jpeg",
|
| 24 |
+
".jpeg": "image/jpeg",
|
| 25 |
+
".webp": "image/webp",
|
| 26 |
+
}
|
| 27 |
+
SENSITIVE_PROVIDER_KEYS = ("token", "secret", "api_key", "authorization", "payload", "raw", "base64", "bytes")
|
| 28 |
|
| 29 |
|
| 30 |
@dataclass(frozen=True)
|
|
|
|
| 75 |
if not target.exists() or not target.is_file():
|
| 76 |
return None
|
| 77 |
suffix = target.suffix.lower()
|
| 78 |
+
mime = IMAGE_MIME_BY_SUFFIX.get(suffix)
|
| 79 |
+
if mime is None or target.stat().st_size > MAX_PROVIDER_IMAGE_BYTES:
|
| 80 |
+
return None
|
| 81 |
data = base64.b64encode(target.read_bytes()).decode("ascii")
|
| 82 |
return f"data:{mime};base64,{data}"
|
| 83 |
except OSError:
|
| 84 |
return None
|
| 85 |
|
| 86 |
|
| 87 |
+
def _is_loopback_host(hostname: str | None) -> bool:
|
| 88 |
+
return hostname in {"localhost", "127.0.0.1", "::1"}
|
| 89 |
+
|
| 90 |
+
|
| 91 |
+
def _safe_provider_payload(value: Any) -> Any:
|
| 92 |
+
if isinstance(value, dict):
|
| 93 |
+
redacted: dict[str, Any] = {}
|
| 94 |
+
for key, item in value.items():
|
| 95 |
+
normalized = str(key).lower()
|
| 96 |
+
if any(marker in normalized for marker in SENSITIVE_PROVIDER_KEYS):
|
| 97 |
+
redacted[str(key)] = "[redacted]"
|
| 98 |
+
else:
|
| 99 |
+
redacted[str(key)] = _safe_provider_payload(item)
|
| 100 |
+
return redacted
|
| 101 |
+
if isinstance(value, list):
|
| 102 |
+
return [_safe_provider_payload(item) for item in value]
|
| 103 |
+
if isinstance(value, tuple):
|
| 104 |
+
return [_safe_provider_payload(item) for item in value]
|
| 105 |
+
return value
|
| 106 |
+
|
| 107 |
+
|
| 108 |
def _post_json(url: str, token: str, payload: dict[str, Any], timeout: float) -> dict[str, Any]:
|
| 109 |
"""
|
| 110 |
Send an authenticated JSON POST request and return the parsed response.
|
|
|
|
| 122 |
parsed = urllib.parse.urlparse(url)
|
| 123 |
if parsed.scheme not in {"http", "https"} or not parsed.netloc:
|
| 124 |
raise ValueError(f"Invalid URL: expected http(s) URL with host, got {url!r}.")
|
| 125 |
+
if parsed.scheme == "http" and not _is_loopback_host(parsed.hostname):
|
| 126 |
+
raise ValueError("Provider URLs must use HTTPS unless targeting loopback development hosts.")
|
| 127 |
body = json.dumps(payload).encode("utf-8")
|
| 128 |
request = urllib.request.Request(
|
| 129 |
url,
|
|
|
|
| 320 |
"Return strict JSON only with keys: sponsor_model_used, structured_parse, "
|
| 321 |
"risk_notes, parameter_budget_notes, final_claim_status. Parse this visual creation run."
|
| 322 |
)
|
| 323 |
+
safe_run_packet = _safe_provider_payload(run_packet)
|
| 324 |
+
safe_minicpm_result = _safe_provider_payload(minicpm_result or {})
|
| 325 |
payload = {
|
| 326 |
"model": model,
|
| 327 |
"messages": [
|
| 328 |
{
|
| 329 |
"role": "user",
|
| 330 |
+
"content": f"{instruction}\nPrompt: {prompt}\nRun: {json.dumps(safe_run_packet, ensure_ascii=True)[:6000]}\nMiniCPM: {json.dumps(safe_minicpm_result, ensure_ascii=True)[:2500]}",
|
| 331 |
}
|
| 332 |
],
|
| 333 |
"temperature": 0.1,
|
src/nexus_visual_weaver/wardrobe.py
CHANGED
|
@@ -17,6 +17,10 @@ SLOT_BLUEPRINTS: list[tuple[str, str, str, str, str]] = [
|
|
| 17 |
]
|
| 18 |
|
| 19 |
|
|
|
|
|
|
|
|
|
|
|
|
|
| 20 |
def build_outfit_graph(prompt: str, adult_mode: bool = False, controls: dict | None = None) -> OutfitGraph:
|
| 21 |
"""
|
| 22 |
Constructs an outfit graph with wardrobe slots configured by prompt analysis and optional controls.
|
|
@@ -42,7 +46,7 @@ def build_outfit_graph(prompt: str, adult_mode: bool = False, controls: dict | N
|
|
| 42 |
if name == "footwear" and controls.get("footwear"):
|
| 43 |
description = str(controls["footwear"])
|
| 44 |
if name == "jewelry" and controls.get("hardware"):
|
| 45 |
-
material =
|
| 46 |
if controls.get("palette"):
|
| 47 |
palette = str(controls["palette"])
|
| 48 |
locked = any(token in lowered for token in name.split("_")) or material.replace("_", " ") in lowered
|
|
|
|
| 17 |
]
|
| 18 |
|
| 19 |
|
| 20 |
+
def _material_token(value: object) -> str:
|
| 21 |
+
return str(value).strip().lower().replace("-", "_").replace(" ", "_")
|
| 22 |
+
|
| 23 |
+
|
| 24 |
def build_outfit_graph(prompt: str, adult_mode: bool = False, controls: dict | None = None) -> OutfitGraph:
|
| 25 |
"""
|
| 26 |
Constructs an outfit graph with wardrobe slots configured by prompt analysis and optional controls.
|
|
|
|
| 46 |
if name == "footwear" and controls.get("footwear"):
|
| 47 |
description = str(controls["footwear"])
|
| 48 |
if name == "jewelry" and controls.get("hardware"):
|
| 49 |
+
material = _material_token(controls["hardware"])
|
| 50 |
if controls.get("palette"):
|
| 51 |
palette = str(controls["palette"])
|
| 52 |
locked = any(token in lowered for token in name.split("_")) or material.replace("_", " ") in lowered
|
tests/test_app_callbacks.py
CHANGED
|
@@ -159,6 +159,34 @@ def test_operator_actions_transition_checkpoint_export_and_stop() -> None:
|
|
| 159 |
assert stopped[13]["provider_state"] == "stopped"
|
| 160 |
|
| 161 |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 162 |
def test_export_blocks_without_checkpoint() -> None:
|
| 163 |
result = app.run_weave(
|
| 164 |
"gothic patent leather platform boots, crimson hardware",
|
|
|
|
| 159 |
assert stopped[13]["provider_state"] == "stopped"
|
| 160 |
|
| 161 |
|
| 162 |
+
def test_clear_export_ignores_stale_override_reason(monkeypatch) -> None:
|
| 163 |
+
monkeypatch.setenv("NEXUS_EXPORT_DIR", "outputs/test-exports")
|
| 164 |
+
result = app.run_weave(
|
| 165 |
+
"gothic patent leather platform boots, crimson hardware",
|
| 166 |
+
"Strict",
|
| 167 |
+
"Wan2.2 I2V",
|
| 168 |
+
False,
|
| 169 |
+
None,
|
| 170 |
+
"Forge",
|
| 171 |
+
)
|
| 172 |
+
run = result[13]
|
| 173 |
+
artifact_path = app.ROOT / "outputs" / "test-clear-override-artifact.png"
|
| 174 |
+
artifact_path.parent.mkdir(parents=True, exist_ok=True)
|
| 175 |
+
artifact_path.write_bytes(b"\x89PNG\r\n\x1a\n\x00\x00\x00\rIEND\xaeB`\x82")
|
| 176 |
+
operator_state = {
|
| 177 |
+
**result[15],
|
| 178 |
+
"generation": {**result[15]["generation"], "output_path": str(artifact_path)},
|
| 179 |
+
}
|
| 180 |
+
clean_scan = {"status": "pass", "export_gate": "clear", "findings": [], "purification_actions": []}
|
| 181 |
+
|
| 182 |
+
approved = app.approve_checkpoint(run, False, clean_scan, "Forge", operator_state)
|
| 183 |
+
exported = app.export_packet(run, False, clean_scan, "Forge", approved[13], "stale text from prior blocked run")
|
| 184 |
+
|
| 185 |
+
assert exported[13]["provider_state"] == "exported"
|
| 186 |
+
assert exported[13]["export"] == "clear"
|
| 187 |
+
assert "st3gg_override_reason" not in exported[13]
|
| 188 |
+
|
| 189 |
+
|
| 190 |
def test_export_blocks_without_checkpoint() -> None:
|
| 191 |
result = app.run_weave(
|
| 192 |
"gothic patent leather platform boots, crimson hardware",
|
tests/test_command_center.py
CHANGED
|
@@ -94,6 +94,23 @@ def test_command_center_run_is_checkpointed() -> None:
|
|
| 94 |
assert any(model.repo_id == "nvidia/LocateAnything-3B" for model in run.model_stack)
|
| 95 |
|
| 96 |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 97 |
def test_security_scan_does_not_return_payload_excerpt() -> None:
|
| 98 |
sample = Path(__file__).parent / "fixtures" / "sample.png"
|
| 99 |
scan = scan_file(str(sample))
|
|
@@ -746,7 +763,7 @@ def test_build_outfit_graph_controls_override_hardware() -> None:
|
|
| 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 == "
|
| 750 |
|
| 751 |
|
| 752 |
def test_build_outfit_graph_locked_slots_control_locks_named_slots() -> None:
|
|
|
|
| 94 |
assert any(model.repo_id == "nvidia/LocateAnything-3B" for model in run.model_stack)
|
| 95 |
|
| 96 |
|
| 97 |
+
def test_command_center_run_snapshots_controls_and_skips_empty_reference_ids() -> None:
|
| 98 |
+
controls = {"wardrobe": {"footwear": "platform boots"}}
|
| 99 |
+
references = [{"source": "upload"}, {"basename": "reference.png"}]
|
| 100 |
+
|
| 101 |
+
run = build_command_center_run(
|
| 102 |
+
"gothic couture archivist",
|
| 103 |
+
creator_controls=controls,
|
| 104 |
+
reference_metadata=references,
|
| 105 |
+
)
|
| 106 |
+
controls["wardrobe"]["footwear"] = "mutated"
|
| 107 |
+
references[1]["basename"] = "mutated.png"
|
| 108 |
+
|
| 109 |
+
assert run.request.references == ["reference.png"]
|
| 110 |
+
assert run.request.creator_controls["wardrobe"]["footwear"] == "platform boots"
|
| 111 |
+
assert run.request.reference_metadata[1]["basename"] == "reference.png"
|
| 112 |
+
|
| 113 |
+
|
| 114 |
def test_security_scan_does_not_return_payload_excerpt() -> None:
|
| 115 |
sample = Path(__file__).parent / "fixtures" / "sample.png"
|
| 116 |
scan = scan_file(str(sample))
|
|
|
|
| 763 |
jewelry_slot = next((s for s in outfit.slots if s.name == "jewelry"), None)
|
| 764 |
|
| 765 |
assert jewelry_slot is not None
|
| 766 |
+
assert jewelry_slot.material == "silver_occult_buckles"
|
| 767 |
|
| 768 |
|
| 769 |
def test_build_outfit_graph_locked_slots_control_locks_named_slots() -> None:
|
tests/test_exporter.py
CHANGED
|
@@ -1,4 +1,5 @@
|
|
| 1 |
import json
|
|
|
|
| 2 |
from pathlib import Path
|
| 3 |
|
| 4 |
from nexus_visual_weaver.exporter import export_root, write_export_packet
|
|
@@ -73,6 +74,52 @@ def test_write_export_packet_records_evidence_without_secrets(monkeypatch) -> No
|
|
| 73 |
assert "/data/" not in json.dumps(payload)
|
| 74 |
|
| 75 |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 76 |
def test_export_packet_has_correct_schema_version(monkeypatch) -> None:
|
| 77 |
monkeypatch.setenv("NEXUS_EXPORT_DIR", "outputs/test-exports")
|
| 78 |
run = build_command_center_run("dark couture brief")
|
|
|
|
| 1 |
import json
|
| 2 |
+
from dataclasses import replace
|
| 3 |
from pathlib import Path
|
| 4 |
|
| 5 |
from nexus_visual_weaver.exporter import export_root, write_export_packet
|
|
|
|
| 74 |
assert "/data/" not in json.dumps(payload)
|
| 75 |
|
| 76 |
|
| 77 |
+
def test_export_packet_sanitizes_run_id_before_writing(monkeypatch) -> None:
|
| 78 |
+
monkeypatch.setenv("NEXUS_EXPORT_DIR", "outputs/test-exports")
|
| 79 |
+
run = build_command_center_run("path traversal export brief")
|
| 80 |
+
run = replace(run, checkpoint=replace(run.checkpoint, checkpoint_id="../unsafe/path\\with:chars"))
|
| 81 |
+
scan = {"status": "pass", "export_gate": "clear"}
|
| 82 |
+
|
| 83 |
+
result = write_export_packet(run=run, scan=scan, operator_state=_make_base_state(), adult_mode=False)
|
| 84 |
+
path = Path(result["path"])
|
| 85 |
+
payload = json.loads(path.read_text(encoding="utf-8"))
|
| 86 |
+
|
| 87 |
+
assert path.parent == export_root()
|
| 88 |
+
assert ".." not in path.name
|
| 89 |
+
assert "\\" not in path.name
|
| 90 |
+
assert "/" not in path.name
|
| 91 |
+
assert payload["run_id"] == "unsafe-path-with-chars"
|
| 92 |
+
|
| 93 |
+
|
| 94 |
+
def test_export_packet_sanitizes_prompt_checkpoint_and_provider_state(monkeypatch) -> None:
|
| 95 |
+
monkeypatch.setenv("NEXUS_EXPORT_DIR", "outputs/test-exports")
|
| 96 |
+
run = build_command_center_run("prompt contains MINICPM_API_KEY and C:/Users/speci.000/secret.png")
|
| 97 |
+
run = replace(
|
| 98 |
+
run,
|
| 99 |
+
refined_prompt=replace(run.refined_prompt, refined="refined path /data/nexus_visual_weaver/secret.png"),
|
| 100 |
+
checkpoint=replace(
|
| 101 |
+
run.checkpoint,
|
| 102 |
+
recommendation="set NEMOTRON_API_KEY",
|
| 103 |
+
required_actions=["review C:/Users/speci.000/Downloads/raw.png"],
|
| 104 |
+
),
|
| 105 |
+
)
|
| 106 |
+
scan = {"status": "pass", "export_gate": "clear"}
|
| 107 |
+
state = _make_base_state(
|
| 108 |
+
provider_state="Bearer " + "hidden-provider-token",
|
| 109 |
+
message="operator message with HF_TOKEN",
|
| 110 |
+
)
|
| 111 |
+
|
| 112 |
+
result = write_export_packet(run=run, scan=scan, operator_state=state, adult_mode=False)
|
| 113 |
+
serialized = json.dumps(json.loads(Path(result["path"]).read_text(encoding="utf-8")))
|
| 114 |
+
|
| 115 |
+
assert "MINICPM_API_KEY" not in serialized
|
| 116 |
+
assert "NEMOTRON_API_KEY" not in serialized
|
| 117 |
+
assert "HF_TOKEN" not in serialized
|
| 118 |
+
assert "C:/Users/speci.000" not in serialized
|
| 119 |
+
assert "/data/" not in serialized
|
| 120 |
+
assert "Bearer " + "hidden-provider-token" not in serialized
|
| 121 |
+
|
| 122 |
+
|
| 123 |
def test_export_packet_has_correct_schema_version(monkeypatch) -> None:
|
| 124 |
monkeypatch.setenv("NEXUS_EXPORT_DIR", "outputs/test-exports")
|
| 125 |
run = build_command_center_run("dark couture brief")
|
tests/test_hf_runtime.py
CHANGED
|
@@ -1,9 +1,13 @@
|
|
| 1 |
from PIL import Image
|
|
|
|
|
|
|
| 2 |
|
|
|
|
| 3 |
from nexus_visual_weaver.hf_runtime import (
|
| 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,
|
|
@@ -14,6 +18,9 @@ from nexus_visual_weaver.hf_runtime import (
|
|
| 14 |
from nexus_visual_weaver.render import render_artifact_lane
|
| 15 |
|
| 16 |
|
|
|
|
|
|
|
|
|
|
| 17 |
def test_hf_runtime_is_disabled_locally_by_default(monkeypatch) -> None:
|
| 18 |
monkeypatch.delenv("SPACE_ID", raising=False)
|
| 19 |
monkeypatch.delenv("HF_SPACE_ID", raising=False)
|
|
@@ -209,3 +216,67 @@ def test_default_lora_repo_id_excludes_requires_image_adapters() -> None:
|
|
| 209 |
recipe_result = _adapter_recipe(result)
|
| 210 |
assert recipe_result is not None
|
| 211 |
assert recipe_result.requires_image is False
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
from PIL import Image
|
| 2 |
+
from pathlib import Path
|
| 3 |
+
import types
|
| 4 |
|
| 5 |
+
import nexus_visual_weaver.hf_runtime as hf_runtime
|
| 6 |
from nexus_visual_weaver.hf_runtime import (
|
| 7 |
FLUX_REPO_ID,
|
| 8 |
PRIVATE_RESEARCH_FLUX_REPO_ID,
|
| 9 |
TINY_TITAN_FLUX_REPO_ID,
|
| 10 |
+
_PIPELINE_CACHE,
|
| 11 |
_adapter_recipe,
|
| 12 |
_repo_candidates,
|
| 13 |
active_flux_repo_id,
|
|
|
|
| 18 |
from nexus_visual_weaver.render import render_artifact_lane
|
| 19 |
|
| 20 |
|
| 21 |
+
RUNTIME_FIXTURE_DIR = Path("tests/fixtures/runtime")
|
| 22 |
+
|
| 23 |
+
|
| 24 |
def test_hf_runtime_is_disabled_locally_by_default(monkeypatch) -> None:
|
| 25 |
monkeypatch.delenv("SPACE_ID", raising=False)
|
| 26 |
monkeypatch.delenv("HF_SPACE_ID", raising=False)
|
|
|
|
| 216 |
recipe_result = _adapter_recipe(result)
|
| 217 |
assert recipe_result is not None
|
| 218 |
assert recipe_result.requires_image is False
|
| 219 |
+
|
| 220 |
+
|
| 221 |
+
def test_generate_flux_image_reports_sidecar_fallback(monkeypatch) -> None:
|
| 222 |
+
class FakeCuda:
|
| 223 |
+
@staticmethod
|
| 224 |
+
def is_available() -> bool:
|
| 225 |
+
return True
|
| 226 |
+
|
| 227 |
+
class FakeGenerator:
|
| 228 |
+
def __init__(self, device):
|
| 229 |
+
self.device = device
|
| 230 |
+
|
| 231 |
+
def manual_seed(self, seed):
|
| 232 |
+
self.seed = seed
|
| 233 |
+
return self
|
| 234 |
+
|
| 235 |
+
fake_torch = types.SimpleNamespace(cuda=FakeCuda(), bfloat16="bfloat16", Generator=FakeGenerator)
|
| 236 |
+
|
| 237 |
+
class FakePipeline:
|
| 238 |
+
@classmethod
|
| 239 |
+
def from_pretrained(cls, repo_id, torch_dtype=None, token=None):
|
| 240 |
+
if repo_id == FLUX_REPO_ID:
|
| 241 |
+
raise RuntimeError("primary denied")
|
| 242 |
+
return cls(repo_id)
|
| 243 |
+
|
| 244 |
+
def __init__(self, repo_id):
|
| 245 |
+
self.repo_id = repo_id
|
| 246 |
+
|
| 247 |
+
def enable_model_cpu_offload(self):
|
| 248 |
+
return None
|
| 249 |
+
|
| 250 |
+
def set_progress_bar_config(self, disable):
|
| 251 |
+
self.progress_disabled = disable
|
| 252 |
+
|
| 253 |
+
def __call__(self, **kwargs):
|
| 254 |
+
return types.SimpleNamespace(images=[Image.new("RGB", (8, 8), color=(2, 4, 6))])
|
| 255 |
+
|
| 256 |
+
fake_diffusers = types.SimpleNamespace(Flux2KleinPipeline=FakePipeline)
|
| 257 |
+
monkeypatch.setitem(__import__("sys").modules, "torch", fake_torch)
|
| 258 |
+
monkeypatch.setitem(__import__("sys").modules, "diffusers", fake_diffusers)
|
| 259 |
+
monkeypatch.setenv("NEXUS_ENABLE_REAL_HF", "1")
|
| 260 |
+
monkeypatch.delenv("NEXUS_DISABLE_REAL_HF", raising=False)
|
| 261 |
+
monkeypatch.delenv("NEXUS_DISABLE_TINY_TITAN_FALLBACK", raising=False)
|
| 262 |
+
output_dir = RUNTIME_FIXTURE_DIR / "hf-output"
|
| 263 |
+
output_dir.mkdir(parents=True, exist_ok=True)
|
| 264 |
+
monkeypatch.setenv("NEXUS_OUTPUT_DIR", str(output_dir))
|
| 265 |
+
monkeypatch.setattr(hf_runtime, "load_and_apply", lambda pipe, recipe, repo_id, adult_mode=False: {"status": "disabled", "repo_id": None, "message": "fake"})
|
| 266 |
+
monkeypatch.setattr(hf_runtime, "unload_all", lambda pipe: None)
|
| 267 |
+
_PIPELINE_CACHE.clear()
|
| 268 |
+
|
| 269 |
+
try:
|
| 270 |
+
result = generate_flux_image("prompt", seed=7)
|
| 271 |
+
finally:
|
| 272 |
+
for artifact in output_dir.glob("nexus_flux_*_7_*.png"):
|
| 273 |
+
artifact.unlink(missing_ok=True)
|
| 274 |
+
|
| 275 |
+
assert result.status == "success"
|
| 276 |
+
assert result.repo_id == TINY_TITAN_FLUX_REPO_ID
|
| 277 |
+
assert result.fallback_used is True
|
| 278 |
+
assert result.primary_error is not None
|
| 279 |
+
assert "primary denied" in result.primary_error
|
| 280 |
+
assert result.output_path is not None
|
| 281 |
+
assert Path(result.output_path).name.startswith("nexus_flux_")
|
| 282 |
+
assert "_7_" in Path(result.output_path).name
|
tests/test_lora_adapter.py
CHANGED
|
@@ -36,6 +36,11 @@ class FailingLoraPipe(FakeLoraPipe):
|
|
| 36 |
raise RuntimeError("adapter load failed")
|
| 37 |
|
| 38 |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 39 |
class UnsupportedPipe:
|
| 40 |
pass
|
| 41 |
|
|
@@ -95,6 +100,21 @@ def test_load_and_apply_reports_failed_without_raising() -> None:
|
|
| 95 |
assert "RuntimeError" in result["message"]
|
| 96 |
|
| 97 |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 98 |
def test_unload_all_uses_pipeline_unload_hook() -> None:
|
| 99 |
pipe = FakeLoraPipe()
|
| 100 |
|
|
|
|
| 36 |
raise RuntimeError("adapter load failed")
|
| 37 |
|
| 38 |
|
| 39 |
+
class FailingSetAdapterPipe(FakeLoraPipe):
|
| 40 |
+
def set_adapters(self, adapter_names: list[str], adapter_weights: list[float]) -> None:
|
| 41 |
+
raise RuntimeError("adapter apply failed")
|
| 42 |
+
|
| 43 |
+
|
| 44 |
class UnsupportedPipe:
|
| 45 |
pass
|
| 46 |
|
|
|
|
| 100 |
assert "RuntimeError" in result["message"]
|
| 101 |
|
| 102 |
|
| 103 |
+
def test_load_and_apply_unloads_when_apply_fails() -> None:
|
| 104 |
+
recipe = AdapterRecipe(
|
| 105 |
+
repo_id="example/style-lora",
|
| 106 |
+
adapter_for="black-forest-labs/FLUX.2-klein-9B",
|
| 107 |
+
task="style",
|
| 108 |
+
)
|
| 109 |
+
pipe = FailingSetAdapterPipe()
|
| 110 |
+
|
| 111 |
+
result = load_and_apply(pipe, recipe, "black-forest-labs/FLUX.2-klein-9B")
|
| 112 |
+
|
| 113 |
+
assert result["status"] == "failed"
|
| 114 |
+
assert pipe.loaded == [("example/style-lora", {"adapter_name": "nexus_style"})]
|
| 115 |
+
assert pipe.unloaded is True
|
| 116 |
+
|
| 117 |
+
|
| 118 |
def test_unload_all_uses_pipeline_unload_hook() -> None:
|
| 119 |
pipe = FakeLoraPipe()
|
| 120 |
|
tests/test_model_relay.py
CHANGED
|
@@ -201,6 +201,19 @@ def test_flux2_klein_9b_is_pinned_with_4b_sidecar_fallback() -> None:
|
|
| 201 |
assert fallback.pinned is False
|
| 202 |
|
| 203 |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 204 |
def test_flux2_klein_9b_is_not_pinned_and_in_private_research() -> None:
|
| 205 |
relay = WeaverModelRelay()
|
| 206 |
|
|
|
|
| 201 |
assert fallback.pinned is False
|
| 202 |
|
| 203 |
|
| 204 |
+
def test_pinned_lane_fallbacks_respect_governance_filters() -> None:
|
| 205 |
+
relay = WeaverModelRelay()
|
| 206 |
+
fallback = relay.records["flux2-klein-4b-sidecar"]
|
| 207 |
+
fallback.health = "unhealthy"
|
| 208 |
+
|
| 209 |
+
decision = relay.select_lane("image_generation")
|
| 210 |
+
|
| 211 |
+
assert decision.primary is not None
|
| 212 |
+
assert decision.primary.model_id == "flux2-klein-9b-quality"
|
| 213 |
+
assert decision.fallbacks == []
|
| 214 |
+
assert any("flux2-klein-4b-sidecar: health=unhealthy" in reason for reason in decision.skipped)
|
| 215 |
+
|
| 216 |
+
|
| 217 |
def test_flux2_klein_9b_is_not_pinned_and_in_private_research() -> None:
|
| 218 |
relay = WeaverModelRelay()
|
| 219 |
|
tests/test_provider_runtime.py
CHANGED
|
@@ -1,4 +1,5 @@
|
|
| 1 |
from PIL import Image
|
|
|
|
| 2 |
|
| 3 |
from nexus_visual_weaver.provider_runtime import (
|
| 4 |
NEMOTRON_NANO_REPO_ID,
|
|
@@ -9,12 +10,21 @@ from nexus_visual_weaver.provider_runtime import (
|
|
| 9 |
_image_data_url,
|
| 10 |
_post_json,
|
| 11 |
_safe_json_from_text,
|
|
|
|
| 12 |
_short_error,
|
| 13 |
judge_with_minicpm,
|
| 14 |
judge_with_nemotron,
|
| 15 |
)
|
| 16 |
|
| 17 |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 18 |
def test_minicpm_reports_missing_secret(monkeypatch) -> None:
|
| 19 |
monkeypatch.delenv("MINICPM_BASE_URL", raising=False)
|
| 20 |
monkeypatch.delenv("MINICPM_API_KEY", raising=False)
|
|
@@ -48,7 +58,7 @@ def test_minicpm_blocks_when_artifact_missing(monkeypatch) -> None:
|
|
| 48 |
|
| 49 |
|
| 50 |
def test_minicpm_success_with_mocked_post(monkeypatch) -> None:
|
| 51 |
-
image = "
|
| 52 |
Image.new("RGB", (8, 8), color=(12, 16, 20)).save(image)
|
| 53 |
monkeypatch.setenv("MINICPM_BASE_URL", "http://example.test")
|
| 54 |
monkeypatch.setenv("MINICPM_API_KEY", "test-token")
|
|
@@ -61,12 +71,15 @@ def test_minicpm_success_with_mocked_post(monkeypatch) -> None:
|
|
| 61 |
|
| 62 |
monkeypatch.setattr("nexus_visual_weaver.provider_runtime._post_json", fake_post)
|
| 63 |
|
| 64 |
-
|
| 65 |
-
|
| 66 |
-
|
| 67 |
-
|
| 68 |
-
|
| 69 |
-
|
|
|
|
|
|
|
|
|
|
| 70 |
|
| 71 |
assert result.status == "success"
|
| 72 |
assert result.evidence["overall_status"] == "pass"
|
|
@@ -119,25 +132,52 @@ def test_image_data_url_returns_none_for_missing_file() -> None:
|
|
| 119 |
|
| 120 |
|
| 121 |
def test_image_data_url_encodes_png_as_base64() -> None:
|
| 122 |
-
image_path = "
|
| 123 |
Image.new("RGB", (4, 4), color=(0, 0, 0)).save(image_path)
|
| 124 |
|
| 125 |
-
|
|
|
|
|
|
|
|
|
|
| 126 |
|
| 127 |
assert result is not None
|
| 128 |
assert result.startswith("data:image/png;base64,")
|
| 129 |
|
| 130 |
|
| 131 |
def test_image_data_url_uses_jpeg_mime_for_jpg() -> None:
|
| 132 |
-
image_path = "
|
| 133 |
Image.new("RGB", (4, 4), color=(0, 0, 0)).save(image_path)
|
| 134 |
|
| 135 |
-
|
|
|
|
|
|
|
|
|
|
| 136 |
|
| 137 |
assert result is not None
|
| 138 |
assert result.startswith("data:image/jpeg;base64,")
|
| 139 |
|
| 140 |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 141 |
# --- _extract_content tests ---
|
| 142 |
|
| 143 |
def test_extract_content_returns_string_from_choices() -> None:
|
|
@@ -263,7 +303,7 @@ def test_nemotron_failed_api_call_returns_failed_status(monkeypatch) -> None:
|
|
| 263 |
def test_minicpm_failed_api_call_returns_failed_status(monkeypatch) -> None:
|
| 264 |
import urllib.error
|
| 265 |
|
| 266 |
-
image_path = "
|
| 267 |
Image.new("RGB", (4, 4), color=(0, 0, 0)).save(image_path)
|
| 268 |
monkeypatch.setenv("MINICPM_BASE_URL", "http://minicpm.test")
|
| 269 |
monkeypatch.setenv("MINICPM_API_KEY", "test-token")
|
|
@@ -273,12 +313,15 @@ def test_minicpm_failed_api_call_returns_failed_status(monkeypatch) -> None:
|
|
| 273 |
|
| 274 |
monkeypatch.setattr("nexus_visual_weaver.provider_runtime._post_json", fake_post)
|
| 275 |
|
| 276 |
-
|
| 277 |
-
|
| 278 |
-
|
| 279 |
-
|
| 280 |
-
|
| 281 |
-
|
|
|
|
|
|
|
|
|
|
| 282 |
|
| 283 |
assert result.status == "failed"
|
| 284 |
assert result.provider_state == "failed"
|
|
@@ -321,7 +364,7 @@ def test_minicpm_uses_openbmb_api_key_as_fallback(monkeypatch) -> None:
|
|
| 321 |
monkeypatch.delenv("MINICPM_API_KEY", raising=False)
|
| 322 |
monkeypatch.setenv("OPENBMB_API_KEY", "openbmb-fallback-token")
|
| 323 |
|
| 324 |
-
image_path = "
|
| 325 |
Image.new("RGB", (4, 4), color=(0, 0, 0)).save(image_path)
|
| 326 |
|
| 327 |
captured = {}
|
|
@@ -332,12 +375,15 @@ def test_minicpm_uses_openbmb_api_key_as_fallback(monkeypatch) -> None:
|
|
| 332 |
|
| 333 |
monkeypatch.setattr("nexus_visual_weaver.provider_runtime._post_json", fake_post)
|
| 334 |
|
| 335 |
-
|
| 336 |
-
|
| 337 |
-
|
| 338 |
-
|
| 339 |
-
|
| 340 |
-
|
|
|
|
|
|
|
|
|
|
| 341 |
|
| 342 |
assert result.status == "success"
|
| 343 |
assert captured["token"] == "openbmb-fallback-token"
|
|
@@ -362,6 +408,47 @@ def test_nemotron_uses_nvidia_api_key_as_fallback(monkeypatch) -> None:
|
|
| 362 |
assert captured["token"] == "nvidia-fallback-token"
|
| 363 |
|
| 364 |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 365 |
def test_post_json_rejects_unsupported_url_schemes_before_urlopen(monkeypatch) -> None:
|
| 366 |
called = False
|
| 367 |
|
|
@@ -381,3 +468,44 @@ def test_post_json_rejects_unsupported_url_schemes_before_urlopen(monkeypatch) -
|
|
| 381 |
raise AssertionError(f"{url} should have been rejected")
|
| 382 |
|
| 383 |
assert called is False
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
from PIL import Image
|
| 2 |
+
from pathlib import Path
|
| 3 |
|
| 4 |
from nexus_visual_weaver.provider_runtime import (
|
| 5 |
NEMOTRON_NANO_REPO_ID,
|
|
|
|
| 10 |
_image_data_url,
|
| 11 |
_post_json,
|
| 12 |
_safe_json_from_text,
|
| 13 |
+
_safe_provider_payload,
|
| 14 |
_short_error,
|
| 15 |
judge_with_minicpm,
|
| 16 |
judge_with_nemotron,
|
| 17 |
)
|
| 18 |
|
| 19 |
|
| 20 |
+
RUNTIME_FIXTURE_DIR = Path("tests/fixtures/runtime")
|
| 21 |
+
|
| 22 |
+
|
| 23 |
+
def _runtime_fixture_path(name: str) -> Path:
|
| 24 |
+
RUNTIME_FIXTURE_DIR.mkdir(parents=True, exist_ok=True)
|
| 25 |
+
return RUNTIME_FIXTURE_DIR / name
|
| 26 |
+
|
| 27 |
+
|
| 28 |
def test_minicpm_reports_missing_secret(monkeypatch) -> None:
|
| 29 |
monkeypatch.delenv("MINICPM_BASE_URL", raising=False)
|
| 30 |
monkeypatch.delenv("MINICPM_API_KEY", raising=False)
|
|
|
|
| 58 |
|
| 59 |
|
| 60 |
def test_minicpm_success_with_mocked_post(monkeypatch) -> None:
|
| 61 |
+
image = _runtime_fixture_path("test-provider-artifact.png")
|
| 62 |
Image.new("RGB", (8, 8), color=(12, 16, 20)).save(image)
|
| 63 |
monkeypatch.setenv("MINICPM_BASE_URL", "http://example.test")
|
| 64 |
monkeypatch.setenv("MINICPM_API_KEY", "test-token")
|
|
|
|
| 71 |
|
| 72 |
monkeypatch.setattr("nexus_visual_weaver.provider_runtime._post_json", fake_post)
|
| 73 |
|
| 74 |
+
try:
|
| 75 |
+
result = judge_with_minicpm(
|
| 76 |
+
prompt="gothic couture",
|
| 77 |
+
image_path=str(image),
|
| 78 |
+
scan={"export_gate": "clear"},
|
| 79 |
+
wardrobe_summary="boots and lace",
|
| 80 |
+
)
|
| 81 |
+
finally:
|
| 82 |
+
image.unlink(missing_ok=True)
|
| 83 |
|
| 84 |
assert result.status == "success"
|
| 85 |
assert result.evidence["overall_status"] == "pass"
|
|
|
|
| 132 |
|
| 133 |
|
| 134 |
def test_image_data_url_encodes_png_as_base64() -> None:
|
| 135 |
+
image_path = _runtime_fixture_path("test-img-data-url.png")
|
| 136 |
Image.new("RGB", (4, 4), color=(0, 0, 0)).save(image_path)
|
| 137 |
|
| 138 |
+
try:
|
| 139 |
+
result = _image_data_url(str(image_path))
|
| 140 |
+
finally:
|
| 141 |
+
image_path.unlink(missing_ok=True)
|
| 142 |
|
| 143 |
assert result is not None
|
| 144 |
assert result.startswith("data:image/png;base64,")
|
| 145 |
|
| 146 |
|
| 147 |
def test_image_data_url_uses_jpeg_mime_for_jpg() -> None:
|
| 148 |
+
image_path = _runtime_fixture_path("test-img-data-url.jpg")
|
| 149 |
Image.new("RGB", (4, 4), color=(0, 0, 0)).save(image_path)
|
| 150 |
|
| 151 |
+
try:
|
| 152 |
+
result = _image_data_url(str(image_path))
|
| 153 |
+
finally:
|
| 154 |
+
image_path.unlink(missing_ok=True)
|
| 155 |
|
| 156 |
assert result is not None
|
| 157 |
assert result.startswith("data:image/jpeg;base64,")
|
| 158 |
|
| 159 |
|
| 160 |
+
def test_image_data_url_rejects_unknown_suffix() -> None:
|
| 161 |
+
image_path = _runtime_fixture_path("not-an-image.txt")
|
| 162 |
+
image_path.write_text("not an image", encoding="utf-8")
|
| 163 |
+
|
| 164 |
+
try:
|
| 165 |
+
assert _image_data_url(str(image_path)) is None
|
| 166 |
+
finally:
|
| 167 |
+
image_path.unlink(missing_ok=True)
|
| 168 |
+
|
| 169 |
+
|
| 170 |
+
def test_image_data_url_rejects_oversized_file(monkeypatch) -> None:
|
| 171 |
+
image_path = _runtime_fixture_path("large.png")
|
| 172 |
+
image_path.write_bytes(b"not actually decoded because size fails")
|
| 173 |
+
monkeypatch.setattr("nexus_visual_weaver.provider_runtime.MAX_PROVIDER_IMAGE_BYTES", 4)
|
| 174 |
+
|
| 175 |
+
try:
|
| 176 |
+
assert _image_data_url(str(image_path)) is None
|
| 177 |
+
finally:
|
| 178 |
+
image_path.unlink(missing_ok=True)
|
| 179 |
+
|
| 180 |
+
|
| 181 |
# --- _extract_content tests ---
|
| 182 |
|
| 183 |
def test_extract_content_returns_string_from_choices() -> None:
|
|
|
|
| 303 |
def test_minicpm_failed_api_call_returns_failed_status(monkeypatch) -> None:
|
| 304 |
import urllib.error
|
| 305 |
|
| 306 |
+
image_path = _runtime_fixture_path("test-minicpm-fail.png")
|
| 307 |
Image.new("RGB", (4, 4), color=(0, 0, 0)).save(image_path)
|
| 308 |
monkeypatch.setenv("MINICPM_BASE_URL", "http://minicpm.test")
|
| 309 |
monkeypatch.setenv("MINICPM_API_KEY", "test-token")
|
|
|
|
| 313 |
|
| 314 |
monkeypatch.setattr("nexus_visual_weaver.provider_runtime._post_json", fake_post)
|
| 315 |
|
| 316 |
+
try:
|
| 317 |
+
result = judge_with_minicpm(
|
| 318 |
+
prompt="gothic couture",
|
| 319 |
+
image_path=str(image_path),
|
| 320 |
+
scan={"export_gate": "clear"},
|
| 321 |
+
wardrobe_summary="platform boots",
|
| 322 |
+
)
|
| 323 |
+
finally:
|
| 324 |
+
image_path.unlink(missing_ok=True)
|
| 325 |
|
| 326 |
assert result.status == "failed"
|
| 327 |
assert result.provider_state == "failed"
|
|
|
|
| 364 |
monkeypatch.delenv("MINICPM_API_KEY", raising=False)
|
| 365 |
monkeypatch.setenv("OPENBMB_API_KEY", "openbmb-fallback-token")
|
| 366 |
|
| 367 |
+
image_path = _runtime_fixture_path("test-openbmb-key.png")
|
| 368 |
Image.new("RGB", (4, 4), color=(0, 0, 0)).save(image_path)
|
| 369 |
|
| 370 |
captured = {}
|
|
|
|
| 375 |
|
| 376 |
monkeypatch.setattr("nexus_visual_weaver.provider_runtime._post_json", fake_post)
|
| 377 |
|
| 378 |
+
try:
|
| 379 |
+
result = judge_with_minicpm(
|
| 380 |
+
prompt="test",
|
| 381 |
+
image_path=str(image_path),
|
| 382 |
+
scan={},
|
| 383 |
+
wardrobe_summary="",
|
| 384 |
+
)
|
| 385 |
+
finally:
|
| 386 |
+
image_path.unlink(missing_ok=True)
|
| 387 |
|
| 388 |
assert result.status == "success"
|
| 389 |
assert captured["token"] == "openbmb-fallback-token"
|
|
|
|
| 408 |
assert captured["token"] == "nvidia-fallback-token"
|
| 409 |
|
| 410 |
|
| 411 |
+
def test_safe_provider_payload_redacts_sensitive_nested_keys() -> None:
|
| 412 |
+
result = _safe_provider_payload(
|
| 413 |
+
{
|
| 414 |
+
"checkpoint": "ok",
|
| 415 |
+
"HF_TOKEN": "should-not-leak",
|
| 416 |
+
"nested": {"raw_payload": "hidden", "items": [{"base64_image": "hidden"}, {"safe": "visible"}]},
|
| 417 |
+
}
|
| 418 |
+
)
|
| 419 |
+
|
| 420 |
+
assert result["checkpoint"] == "ok"
|
| 421 |
+
assert result["HF_TOKEN"] == "[redacted]"
|
| 422 |
+
assert result["nested"]["raw_payload"] == "[redacted]"
|
| 423 |
+
assert result["nested"]["items"][0]["base64_image"] == "[redacted]"
|
| 424 |
+
assert result["nested"]["items"][1]["safe"] == "visible"
|
| 425 |
+
|
| 426 |
+
|
| 427 |
+
def test_nemotron_redacts_run_packet_before_provider_call(monkeypatch) -> None:
|
| 428 |
+
monkeypatch.setenv("NEMOTRON_BASE_URL", "http://localhost:8001")
|
| 429 |
+
monkeypatch.setenv("NEMOTRON_API_KEY", "nvidia-token")
|
| 430 |
+
captured = {}
|
| 431 |
+
|
| 432 |
+
def fake_post(url, token, payload, timeout):
|
| 433 |
+
captured["content"] = payload["messages"][0]["content"]
|
| 434 |
+
return {"choices": [{"message": {"content": '{"final_claim_status":"pass"}'}}]}
|
| 435 |
+
|
| 436 |
+
monkeypatch.setattr("nexus_visual_weaver.provider_runtime._post_json", fake_post)
|
| 437 |
+
|
| 438 |
+
result = judge_with_nemotron(
|
| 439 |
+
prompt="brief",
|
| 440 |
+
run_packet={"safe": "visible", "api_key": "hidden-key", "nested": {"payload_bytes": "hidden-bytes"}},
|
| 441 |
+
minicpm_result={"status": "success", "authorization": "hidden-auth"},
|
| 442 |
+
)
|
| 443 |
+
|
| 444 |
+
assert result.status == "success"
|
| 445 |
+
assert "visible" in captured["content"]
|
| 446 |
+
assert "hidden-key" not in captured["content"]
|
| 447 |
+
assert "hidden-bytes" not in captured["content"]
|
| 448 |
+
assert "hidden-auth" not in captured["content"]
|
| 449 |
+
assert "[redacted]" in captured["content"]
|
| 450 |
+
|
| 451 |
+
|
| 452 |
def test_post_json_rejects_unsupported_url_schemes_before_urlopen(monkeypatch) -> None:
|
| 453 |
called = False
|
| 454 |
|
|
|
|
| 468 |
raise AssertionError(f"{url} should have been rejected")
|
| 469 |
|
| 470 |
assert called is False
|
| 471 |
+
|
| 472 |
+
|
| 473 |
+
def test_post_json_rejects_plain_http_non_loopback_before_urlopen(monkeypatch) -> None:
|
| 474 |
+
called = False
|
| 475 |
+
|
| 476 |
+
def fake_urlopen(*args, **kwargs):
|
| 477 |
+
nonlocal called
|
| 478 |
+
called = True
|
| 479 |
+
raise AssertionError("urlopen should not be called for plaintext remote provider URLs")
|
| 480 |
+
|
| 481 |
+
monkeypatch.setattr("urllib.request.urlopen", fake_urlopen)
|
| 482 |
+
|
| 483 |
+
try:
|
| 484 |
+
_post_json("http://example.test/v1/chat/completions", "token", {"ok": True}, 1.0)
|
| 485 |
+
except ValueError as exc:
|
| 486 |
+
assert "HTTPS" in str(exc)
|
| 487 |
+
assert "loopback" in str(exc)
|
| 488 |
+
else:
|
| 489 |
+
raise AssertionError("remote http provider URL should have been rejected")
|
| 490 |
+
|
| 491 |
+
assert called is False
|
| 492 |
+
|
| 493 |
+
|
| 494 |
+
def test_post_json_allows_loopback_http_for_local_tests(monkeypatch) -> None:
|
| 495 |
+
class FakeResponse:
|
| 496 |
+
def __enter__(self):
|
| 497 |
+
return self
|
| 498 |
+
|
| 499 |
+
def __exit__(self, exc_type, exc, tb):
|
| 500 |
+
return False
|
| 501 |
+
|
| 502 |
+
def read(self):
|
| 503 |
+
return b'{"ok": true}'
|
| 504 |
+
|
| 505 |
+
def fake_urlopen(request, timeout):
|
| 506 |
+
assert request.full_url == "http://127.0.0.1:8000/v1/chat/completions"
|
| 507 |
+
return FakeResponse()
|
| 508 |
+
|
| 509 |
+
monkeypatch.setattr("urllib.request.urlopen", fake_urlopen)
|
| 510 |
+
|
| 511 |
+
assert _post_json("http://127.0.0.1:8000/v1/chat/completions", "token", {"ok": True}, 1.0) == {"ok": True}
|