use std::fs;
use std::path::{Path, PathBuf};
use std::process::{Command, Output};
use std::sync::atomic::{AtomicU64, Ordering};
static NEXT_FIXTURE: AtomicU64 = AtomicU64::new(0);
struct Fixture {
root: PathBuf,
}
const SUMMARY_TYPES: &str = r#"type Summary {
title: String
bullets: Array<String>
}
"#;
impl Fixture {
fn new(label: &str) -> Self {
let ordinal = NEXT_FIXTURE.fetch_add(1, Ordering::Relaxed);
let root = std::env::temp_dir().join(format!(
"noxid-wo30-{label}-{}-{ordinal}",
std::process::id()
));
let _ = fs::remove_dir_all(&root);
fs::create_dir_all(&root).expect("create WO-30 fixture");
let fixture = Self { root };
fixture.write("package.json", "{\"private\":true,\"type\":\"module\"}\n");
fixture.write(
"Noxid.toml",
"[app]\ntitle = \"WO-30 model boundary\"\nroutes = \"src/routes\"\n\n[server]\nruntime = \"node\"\nsecrets = [\"ANTHROPIC_API_KEY\", \"OPENAI_API_KEY\", \"MODEL_GATEWAY_URL\"]\ntracing = \"full\"\n",
);
fixture.write(
"src/routes/+page.nox",
"component Page {\n view {\n <p>model boundary</p>\n }\n}\n",
);
fixture.write(
"server/models/Assistant.nox",
"model Assistant {\n provider: anthropic\n id: \"claude-sonnet-5\"\n baseUrl: MODEL_GATEWAY_URL\n temperature: 0.2\n maxTokens: 512\n retries: 2\n secret: ANTHROPIC_API_KEY\n}\n",
);
fixture.write(
"server/models/Summarizer.nox",
"model Summarizer {\n provider: openai\n id: \"gpt-5-mini\"\n baseUrl: MODEL_GATEWAY_URL\n maxTokens: 256\n retries: 1\n secret: OPENAI_API_KEY\n}\n",
);
fixture.write(
"server/api/summarize.post.nox",
&format!(
"{SUMMARY_TYPES}\nendpoint Summarize {{\n version: 1\n body {{ text: String }}\n result: Summary\n}}\n"
),
);
fixture.write(
"server/host.js",
r#"import { models, types, generateObject } from "noxid:server";
export const endpoints = Object.freeze({
"endpoint:Summarize@1": async ({ text }, { request }) => {
const result = await generateObject(models.Summarizer, text, types.Summary, { request });
return result.value;
},
});
"#,
);
fixture
}
fn write(&self, relative: &str, contents: &str) {
let path = self.root.join(relative);
if let Some(parent) = path.parent() {
fs::create_dir_all(parent).expect("create fixture parent");
}
fs::write(path, contents).expect("write fixture file");
}
fn build(&self) -> Output {
Command::new(env!("CARGO_BIN_EXE_noxid"))
.args(["build", ".", "--out-dir", "dist"])
.current_dir(&self.root)
.output()
.expect("build WO-30 fixture")
}
fn read(&self, relative: &str) -> String {
fs::read_to_string(self.root.join(relative))
.unwrap_or_else(|error| panic!("read {relative}: {error}"))
}
fn run(&self, script: &str, secrets: &[(&str, &str)]) -> Output {
self.write("dist/probe.mjs", &format!("{}{script}", sink_preamble()));
let mut command = Command::new("node");
command
.arg("probe.mjs")
.current_dir(self.root.join("dist"))
.env("MODEL_WIRE_FIXTURES", wire_fixture_root())
.env_remove("ANTHROPIC_API_KEY")
.env_remove("OPENAI_API_KEY");
for (name, value) in secrets {
command.env(name, value);
}
command.output().expect("execute model probe")
}
}
impl Drop for Fixture {
fn drop(&mut self) {
let _ = fs::remove_dir_all(&self.root);
}
}
fn wire_fixture_root() -> PathBuf {
Path::new(env!("CARGO_MANIFEST_DIR")).join("tests/fixtures/model-wire")
}
fn assert_success(output: &Output, context: &str) {
assert!(
output.status.success(),
"{context}:\nstdout:\n{}\nstderr:\n{}",
String::from_utf8_lossy(&output.stdout),
String::from_utf8_lossy(&output.stderr)
);
}
fn stdout(output: &Output) -> String {
String::from_utf8_lossy(&output.stdout).into_owned()
}
fn sink_preamble() -> String {
r#"import http from "node:http";
import { readFileSync } from "node:fs";
const fixtureRoot = process.env.MODEL_WIRE_FIXTURES;
const recorded = [];
const scripted = [];
function expect(fixture) { scripted.push(fixture); }
const sink = http.createServer((request, response) => {
const chunks = [];
request.on("data", (chunk) => chunks.push(chunk));
request.on("end", () => {
const raw = Buffer.concat(chunks).toString("utf8");
let body = null;
try { body = JSON.parse(raw); } catch {}
recorded.push({ path: request.url, headers: request.headers, body });
const next = scripted.shift();
if (next === undefined) {
response.writeHead(500, { "content-type": "application/json" });
response.end(JSON.stringify({ error: { type: "sink_unscripted", code: "sink_unscripted" } }));
return;
}
const contents = readFileSync(`${fixtureRoot}/${next}`, "utf8");
if (next.endsWith(".sse")) {
response.writeHead(200, { "content-type": "text/event-stream" });
response.end(contents);
return;
}
const fixture = JSON.parse(contents);
response.writeHead(fixture.status, { "content-type": "application/json" });
response.end(JSON.stringify(fixture.body));
});
});
await new Promise((resolve) => sink.listen(0, "127.0.0.1", resolve));
process.env.MODEL_GATEWAY_URL = `http://127.0.0.1:${sink.address().port}`;
await import("./server/handler.js");
const { models, types, generateText, generateObject, streamText } = await import("./server/noxid-server.js");
const done = () => { sink.close(); };
function report(name, value) { console.log(`PROBE ${name} ${JSON.stringify(value)}`); }
async function failure(operation) {
try { await operation(); } catch (error) { return { code: error?.code ?? null, message: error?.message ?? String(error), detail: error?.detail ?? null }; }
throw new Error("expected a refusal");
}
"#
.to_string()
}
#[test]
fn object_generation_validates_refuses_and_retries_through_the_boundary_validator() {
let fixture = Fixture::new("object");
assert_success(&fixture.build(), "build model fixture");
let output = fixture.run(
r#"
expect("openai/object.json");
const valid = await generateObject(models.Summarizer, "summarize", types.Summary);
report("valid", valid.value);
report("usage", valid.usage);
// One invalid answer inside the declared cap is retried, not accepted.
expect("openai/object-invalid.json");
expect("openai/object.json");
const retried = await generateObject(models.Summarizer, "summarize", types.Summary);
report("retried", retried.value);
report("retriedRequests", recorded.length);
// Past the cap the boundary refuses with the validation detail.
expect("openai/object-invalid.json");
expect("openai/object-invalid.json");
const refused = await failure(() => generateObject(models.Summarizer, "summarize", types.Summary));
report("refused", refused);
done();
"#,
&[("OPENAI_API_KEY", "openai-test-key")],
);
assert_success(&output, "run object probe");
let log = stdout(&output);
assert!(
log.contains(
r#"PROBE valid {"title":"Typed model output","bullets":["validated","fail-closed"]}"#
),
"{log}"
);
assert!(
log.contains(r#"PROBE usage {"inputTokens":44,"outputTokens":19}"#),
"{log}"
);
assert!(
log.contains(r#"PROBE retried {"title":"Typed model output""#),
"{log}"
);
assert!(log.contains("PROBE retriedRequests 3"), "{log}");
assert!(log.contains("\"code\":\"MODEL_OUTPUT_INVALID\""), "{log}");
assert!(
log.contains("after 1 retry (2 attempts)"),
"the refusal must name the declared cap it exhausted: {log}"
);
assert!(
log.contains("\"detail\":"),
"the refusal must carry the last validation detail: {log}"
);
}
#[test]
fn undeclared_object_fields_are_refused_however_strict_the_provider_promised_to_be() {
let fixture = Fixture::new("undeclared-fields");
fixture.write(
"server/api/summarize.post.nox",
"type Bullet {\n label: String\n}\ntype Summary {\n title: String\n bullets: Array<String>\n detail: Bullet\n items: Array<Bullet>\n}\n\nendpoint Summarize {\n version: 1\n body { text: String }\n result: Summary\n}\n",
);
assert_success(&fixture.build(), "build nested-type model fixture");
let output = fixture.run(
r#"
expect("openai/object-nested.json");
const accepted = await generateObject(models.Summarizer, "summarize", types.Summary);
report("accepted", accepted.value);
expect("openai/object-undeclared-root-field.json");
expect("openai/object-undeclared-root-field.json");
report("root", await failure(() => generateObject(models.Summarizer, "summarize", types.Summary)));
expect("openai/object-undeclared-nested-field.json");
expect("openai/object-undeclared-nested-field.json");
report("nested", await failure(() => generateObject(models.Summarizer, "summarize", types.Summary)));
expect("openai/object-undeclared-array-element-field.json");
expect("openai/object-undeclared-array-element-field.json");
report("element", await failure(() => generateObject(models.Summarizer, "summarize", types.Summary)));
const strict = recorded.at(-1)?.body?.response_format?.json_schema;
report("requestedStrict", { strict: strict?.strict === true, additionalProperties: strict?.schema?.additionalProperties });
done();
"#,
&[("OPENAI_API_KEY", "openai-test-key")],
);
assert_success(&output, "run undeclared-field probe");
let log = stdout(&output);
assert!(
log.contains(r#"PROBE accepted {"title":"Typed model output""#),
"the exactly-declared shape must still be accepted: {log}"
);
assert!(
log.contains(r#"PROBE requestedStrict {"strict":true,"additionalProperties":false}"#),
"the generated client must still request strict structured output: {log}"
);
for (position, field) in [
("root", "untypedRoot"),
("nested", "untypedNested"),
("element", "untypedElement"),
] {
let line = log
.lines()
.find(|line| line.starts_with(&format!("PROBE {position} ")))
.unwrap_or_else(|| panic!("no {position} refusal in:\n{log}"));
assert!(
line.contains(r#""code":"MODEL_OUTPUT_INVALID""#),
"an undeclared {position} field must fail closed: {line}"
);
assert!(
line.contains(field),
"the refusal must name the undeclared field `{field}`: {line}"
);
assert!(
line.contains("Declared endpoint field"),
"the refusal detail must say the field is undeclared: {line}"
);
}
}
#[test]
fn stream_text_yields_typed_deltas_then_a_final_usage_element() {
let fixture = Fixture::new("stream");
assert_success(&fixture.build(), "build model fixture");
let output = fixture.run(
r#"
expect("anthropic/stream.sse");
const anthropicElements = [];
for await (const element of streamText(models.Assistant, "stream please")) anthropicElements.push(element);
report("anthropic", anthropicElements);
expect("openai/stream.sse");
const openaiElements = [];
for await (const element of streamText(models.Summarizer, "stream please")) openaiElements.push(element);
report("openai", openaiElements);
report("anthropicStreamBody", recorded[0].body.stream);
report("openaiStreamOptions", recorded[1].body.stream_options);
done();
"#,
&[
("ANTHROPIC_API_KEY", "anthropic-test-key"),
("OPENAI_API_KEY", "openai-test-key"),
],
);
assert_success(&output, "run stream probe");
let log = stdout(&output);
assert!(
log.contains(
r#"PROBE anthropic [{"delta":"Typed "},{"delta":"deltas."},{"usage":{"inputTokens":11,"outputTokens":4}}]"#
),
"{log}"
);
assert!(
log.contains(
r#"PROBE openai [{"delta":"Typed "},{"delta":"deltas."},{"usage":{"inputTokens":9,"outputTokens":4}}]"#
),
"{log}"
);
assert!(log.contains("PROBE anthropicStreamBody true"), "{log}");
assert!(
log.contains(r#"PROBE openaiStreamOptions {"include_usage":true}"#),
"{log}"
);
}
#[test]
fn recorded_fixtures_round_trip_each_providers_wire_format() {
let fixture = Fixture::new("wire");
assert_success(&fixture.build(), "build model fixture");
let output = fixture.run(
r#"
expect("anthropic/text.json");
report("anthropicText", (await generateText(models.Assistant, "explain")).text);
report("anthropicRequest", { path: recorded[0].path, version: recorded[0].headers["anthropic-version"], key: recorded[0].headers["x-api-key"], body: recorded[0].body });
expect("anthropic/object.json");
report("anthropicObject", (await generateObject(models.Assistant, "summarize", types.Summary)).value);
report("anthropicTool", { tools: recorded[1].body.tools, choice: recorded[1].body.tool_choice });
expect("openai/text.json");
report("openaiText", (await generateText(models.Summarizer, "explain", { system: "be brief" })).text);
report("openaiRequest", { path: recorded[2].path, authorization: recorded[2].headers.authorization, body: recorded[2].body });
expect("openai/object.json");
report("openaiObject", (await generateObject(models.Summarizer, "summarize", types.Summary)).value);
report("openaiFormat", recorded[3].body.response_format);
// A provider refusal carries status and the provider's own code, never the body.
expect("openai/rate-limited.json");
report("openaiRefusal", await failure(() => generateText(models.Summarizer, "explain")));
expect("anthropic/overloaded.json");
report("anthropicRefusal", await failure(() => generateText(models.Assistant, "explain")));
done();
"#,
&[
("ANTHROPIC_API_KEY", "anthropic-test-key"),
("OPENAI_API_KEY", "openai-test-key"),
],
);
assert_success(&output, "run wire probe");
let log = stdout(&output);
assert!(
log.contains(r#"PROBE anthropicText "Noxid validates model output at the boundary.""#),
"{log}"
);
assert!(log.contains(r#""path":"/v1/messages""#), "{log}");
assert!(log.contains(r#""version":"2023-06-01""#), "{log}");
assert!(log.contains(r#""key":"anthropic-test-key""#), "{log}");
assert!(log.contains(r#""model":"claude-sonnet-5""#), "{log}");
assert!(
log.contains(r#""max_tokens":512"#),
"the declared maxTokens must reach the wire: {log}"
);
assert!(log.contains(r#""temperature":0.2"#), "{log}");
assert!(
log.contains(r#"PROBE anthropicObject {"title":"Typed model output""#),
"{log}"
);
assert!(
log.contains(r#""name":"noxid_structured_result""#)
&& log.contains(r#""choice":{"type":"tool","name":"noxid_structured_result"}"#),
"structured output is a forced tool on Anthropic: {log}"
);
assert!(
log.contains(r#""additionalProperties":false"#),
"the tool schema must be closed: {log}"
);
assert!(
log.contains(r#"PROBE openaiText "Noxid validates model output at the boundary.""#),
"{log}"
);
assert!(log.contains(r#""path":"/v1/chat/completions""#), "{log}");
assert!(
log.contains(r#""authorization":"Bearer openai-test-key""#),
"{log}"
);
assert!(
log.contains(r#"{"role":"system","content":"be brief"}"#),
"{log}"
);
assert!(
log.contains(r#"PROBE openaiFormat {"type":"json_schema","json_schema":{"name":"Summary","strict":true"#),
"OpenAI-compatible structured output is strict json_schema: {log}"
);
assert!(
log.contains("\"code\":\"MODEL_PROVIDER_ERROR\"") && log.contains("status 429"),
"{log}"
);
assert!(log.contains("(rate_limit_exceeded)"), "{log}");
assert!(
log.contains("status 529") && log.contains("(overloaded_error)"),
"{log}"
);
assert!(
!log.contains("ORG_SECRET_MUST_NOT_LEAK") && !log.contains("PROMPT_TEXT_MUST_NOT_LEAK"),
"a provider error body must never reach the error message: {log}"
);
}
#[test]
fn a_missing_model_secret_fails_only_the_request_that_reaches_for_it() {
let fixture = Fixture::new("secret");
assert_success(&fixture.build(), "build model fixture");
let output = fixture.run(
r#"
report("missing", await failure(() => generateText(models.Assistant, "explain")));
// A different model with a present secret is unaffected...
expect("openai/text.json");
report("other", (await generateText(models.Summarizer, "explain")).text);
// ...and so is a request that never touches a model.
const { fetch: handle } = await import("./server/handler.js");
const response = await handle(new Request("https://noxid.test/", { method: "GET" }));
report("untouched", response.status);
report("calls", recorded.length);
done();
"#,
&[("OPENAI_API_KEY", "openai-test-key")],
);
assert_success(&output, "run secret probe");
let log = stdout(&output);
assert!(log.contains("\"code\":\"MODEL_SECRET_MISSING\""), "{log}");
assert!(log.contains("ANTHROPIC_API_KEY"), "{log}");
assert!(
log.contains(r#"PROBE other "Noxid validates model output at the boundary.""#),
"a missing credential for one model must not disable another: {log}"
);
assert!(
log.contains("PROBE calls 1"),
"the refused call must not reach the provider at all: {log}"
);
}
#[test]
fn model_spans_carry_identity_tokens_and_latency_but_never_prompt_or_completion() {
let fixture = Fixture::new("spans");
assert_success(&fixture.build(), "build model fixture");
let output = fixture.run(
r#"
expect("openai/object.json");
await generateObject(models.Summarizer, "PROMPT_TEXT_MUST_NOT_LEAK", types.Summary);
expect("openai/object-invalid.json");
expect("openai/object.json");
await generateObject(models.Summarizer, "PROMPT_TEXT_MUST_NOT_LEAK", types.Summary);
done();
"#,
&[("OPENAI_API_KEY", "openai-test-key")],
);
assert_success(&output, "run span probe");
let log = stdout(&output);
let spans = log
.lines()
.filter(|line| line.contains("\"event\":\"model.generate\""))
.collect::<Vec<_>>();
assert_eq!(spans.len(), 2, "one span per call: {log}");
for span in &spans {
for field in [
"\"semanticId\":\"model:Summarizer\"",
"\"model\":\"Summarizer\"",
"\"modelProvider\":\"openai\"",
"\"modelId\":\"gpt-5-mini\"",
"\"tokensInput\":",
"\"tokensOutput\":",
"\"modelRetries\":",
"\"durationMs\":",
] {
assert!(span.contains(field), "span is missing {field}: {span}");
}
}
assert!(spans[0].contains("\"modelRetries\":0"), "{log}");
assert!(
spans[1].contains("\"modelRetries\":1"),
"the retried call reports the retries it spent: {log}"
);
assert!(
spans[1].contains("\"tokensInput\":88") && spans[1].contains("\"tokensOutput\":30"),
"a retried call reports the tokens every attempt cost: {log}"
);
assert!(
!log.contains("PROMPT_TEXT_MUST_NOT_LEAK") && !log.contains("Typed model output"),
"no prompt or completion text may reach a span: {log}"
);
}
#[test]
fn a_model_free_project_emits_no_models_runtime_section() {
let fixture = Fixture::new("modelfree");
fs::remove_file(fixture.root.join("server/models/Assistant.nox")).expect("drop model");
fs::remove_file(fixture.root.join("server/models/Summarizer.nox")).expect("drop model");
fixture.write(
"server/host.js",
"export const endpoints = Object.freeze({\n \"endpoint:Summarize@1\": async ({ text }) => ({ title: text, bullets: [] }),\n});\n",
);
assert_success(&fixture.build(), "build model-free fixture");
let handler = fixture.read("dist/server/handler.js");
for marker in [
"noxid-runtime:feature-start:models",
"modelDeclarations",
"modelTypeSchemas",
"__NOXID_MODEL_RUNTIME__",
"anthropic-version",
"noxid_structured_result",
] {
assert!(
!handler.contains(marker),
"a model-free project must not carry `{marker}` in its server graph"
);
}
assert!(
fixture
.read("dist/server/models.manifest.json")
.contains("\"models\":[]"),
"a model-free project still publishes an empty, honest manifest"
);
}
impl Fixture {
fn scenario_gate(&self) -> Output {
self.scenario_gate_with(&[])
}
fn scenario_gate_with(&self, values: &[(&str, &Path)]) -> Output {
let mut command = Command::new(env!("CARGO_BIN_EXE_noxid"));
command
.args(["test", ".", "--gate"])
.current_dir(&self.root)
.env_remove("ANTHROPIC_API_KEY")
.env_remove("OPENAI_API_KEY")
.env_remove("MODEL_GATEWAY_URL");
for (name, value) in values {
command.env(name, value);
}
command.output().expect("run scenario gate")
}
}
#[test]
fn scenario_stubs_cover_all_four_shapes_and_refuse_an_unstubbed_call() {
let fixture = Fixture::new("scenarios");
fixture.write(
"server/api/summarize.post.nox",
&format!(
r#"{SUMMARY_TYPES}
endpoint Summarize {{
version: 1
body {{ text: String }}
result: Summary
scenario StubbedObject {{
description: "an object stub is validated and returned"
given: model Summarizer = object Summary(title = "Typed", bullets = ["one"])
when: request(body: Shape(text = "hello"))
expect: status == 200, value.title == "Typed"
}}
scenario StubbedText {{
description: "a text stub answers generateText"
given: model Summarizer = text "a whole completion"
when: request(body: Shape(text = "text"))
expect: status == 200, value.title == "a whole completion"
}}
scenario StubbedTokens {{
description: "a token stub drives streamText deltas in order"
given: model Summarizer = tokens ["Typed ", "deltas."]
when: request(body: Shape(text = "tokens"))
expect: status == 200, value.title == "Typed deltas."
}}
scenario StubbedFailure {{
description: "a scripted failure surfaces its structured code"
given: model Summarizer = fails MODEL_PROVIDER_ERROR
when: request(body: Shape(text = "fails"))
expect: status == 200, value.title == "MODEL_PROVIDER_ERROR"
}}
scenario Unstubbed {{
description: "a model call with no stub left fails closed"
given: model Assistant = text "never consumed"
when: request(body: Shape(text = "hello"))
expect: status == 500
}}
}}
"#
),
);
fixture.write(
"server/host.js",
r#"import { models, types, generateObject, generateText, streamText } from "noxid:server";
export const endpoints = Object.freeze({
"endpoint:Summarize@1": async ({ text }, { request }) => {
if (text === "text") {
const answer = await generateText(models.Summarizer, text, { request });
return { title: answer.text, bullets: [] };
}
if (text === "tokens") {
let joined = "";
for await (const element of streamText(models.Summarizer, text, { request })) {
if (typeof element.delta === "string") joined += element.delta;
}
return { title: joined, bullets: [] };
}
if (text === "fails") {
try {
await generateText(models.Summarizer, text, { request });
} catch (error) {
return { title: error.code, bullets: [] };
}
}
const result = await generateObject(models.Summarizer, text, types.Summary, { request });
return result.value;
},
});
"#,
);
let output = fixture.scenario_gate();
let report = stdout(&output);
let logs = String::from_utf8_lossy(&output.stderr).to_string();
let log = format!("{report}{logs}");
assert!(output.status.success(), "scenario gate failed:\n{log}");
assert!(logs.contains("5 passed, 0 failed"), "{logs}");
assert!(
logs.contains("\"code\":\"MODEL_STUB_REQUIRED\""),
"the unstubbed call must fail closed with MODEL_STUB_REQUIRED on stderr: {logs}"
);
assert!(
report.contains(
"\"modelStubRefusals\":[{\"model\":\"Summarizer\",\"callSite\":\"endpoint:Summarize@1\"}]"
),
"the scenario report must carry the refused model and its call site: {report}"
);
assert!(
!log.contains("api.openai.com") && !log.contains("api.anthropic.com"),
"a scenario must never name a provider endpoint: {log}"
);
}
#[test]
fn an_unstubbed_model_call_names_its_call_site_in_the_message_and_the_report() {
let fixture = Fixture::new("stubsite");
fixture.write(
"server/api/summarize.post.nox",
&format!(
r#"{SUMMARY_TYPES}
endpoint Summarize {{
version: 1
body {{ text: String }}
result: Summary
scenario SecondCallHasNoStub {{
description: "one stub cannot satisfy two calls, and the refusal says which call"
given: model Summarizer = text "first"
when: request(body: Shape(text = "twice"))
expect: status == 200, value.title == "MODEL_STUB_REQUIRED"
}}
}}
"#
),
);
fixture.write(
"server/host.js",
r#"import { models, generateText } from "noxid:server";
export const endpoints = Object.freeze({
"endpoint:Summarize@1": async ({ text }, { request }) => {
try {
await generateText(models.Summarizer, text, { request });
await generateText(models.Summarizer, text, { request });
return { title: "second call unexpectedly succeeded", bullets: [] };
} catch (error) {
return { title: error.code, bullets: [error.message] };
}
},
});
"#,
);
let output = fixture.scenario_gate();
let log = format!(
"{}{}",
stdout(&output),
String::from_utf8_lossy(&output.stderr)
);
assert!(output.status.success(), "scenario gate failed:\n{log}");
assert!(
log.contains(
"scenario called model `Summarizer` at call site `endpoint:Summarize@1` with no stub left"
),
"the runtime refusal must name the model and the call site: {log}"
);
assert!(
log.contains(
"\"modelStubRefusals\":[{\"model\":\"Summarizer\",\"callSite\":\"endpoint:Summarize@1\"}]"
),
"the scenario report must carry the same pair: {log}"
);
}
#[test]
fn an_object_stub_that_names_no_declared_type_is_refused_at_scenario_compile_time() {
let fixture = Fixture::new("stubshape");
fixture.write(
"server/api/summarize.post.nox",
&format!(
r#"{SUMMARY_TYPES}
endpoint Summarize {{
version: 1
body {{ text: String }}
result: Summary
scenario BadStub {{
description: "an object stub must name a declared type"
given: model Summarizer = object 7
when: request(body: Shape(text = "hello"))
expect: status == 200
}}
}}
"#
),
);
let output = fixture.scenario_gate();
let log = format!(
"{}{}",
stdout(&output),
String::from_utf8_lossy(&output.stderr)
);
assert!(!output.status.success(), "expected a refusal:\n{log}");
assert!(log.contains("MODEL_STUB_SHAPE_MISMATCH"), "{log}");
}
#[test]
fn an_object_stub_of_the_wrong_declared_type_is_refused_at_scenario_compile_time() {
let fixture = Fixture::new("stubanchor");
fixture.write(
"server/api/summarize.post.nox",
&format!(
r#"{SUMMARY_TYPES}
type Unrelated {{
count: Int
}}
endpoint Summarize {{
version: 1
body {{ text: String }}
result: Summary
scenario WrongDeclaredType {{
description: "a declared object of another type cannot stand in for Summary"
given: model Summarizer = object Unrelated(count = 1)
when: request(body: Shape(text = "hello"))
expect: status == 200
}}
}}
"#
),
);
let output = fixture.scenario_gate();
let log = format!(
"{}{}",
stdout(&output),
String::from_utf8_lossy(&output.stderr)
);
assert!(!output.status.success(), "expected a refusal:\n{log}");
assert!(log.contains("MODEL_STUB_SHAPE_MISMATCH"), "{log}");
assert!(
log.contains("`Unrelated`") || log.contains("object Unrelated"),
"the refusal must name the stub's type: {log}"
);
assert!(
log.contains("Summary"),
"the refusal must name the declared type the call is anchored to: {log}"
);
assert!(
!log.contains("MODEL_OUTPUT_INVALID"),
"the mismatch must be caught before the scenario executes: {log}"
);
}
fn directory_has_files(path: &Path) -> bool {
let Ok(entries) = fs::read_dir(path) else {
return false;
};
entries.flatten().any(|entry| {
let candidate = entry.path();
candidate.is_file() || (candidate.is_dir() && directory_has_files(&candidate))
})
}
#[test]
fn a_delegated_host_reaches_no_boundary_that_outlives_the_scenario() {
let fixture = Fixture::new("scenario-io");
fixture.write(
"Noxid.toml",
"[app]\ntitle = \"WO-30 scenario I/O\"\nroutes = \"src/routes\"\n\n[server]\nruntime = \"node\"\nstorage = \"fs\"\nsecrets = [\"ANTHROPIC_API_KEY\", \"OPENAI_API_KEY\", \"MODEL_GATEWAY_URL\"]\n",
);
fixture.write(
"plugins/drizzle-orm/adapter.js",
r#"export function __installNoxidPrincipalAuthority() { return true; }
export function database() { return { real: true }; }
export function validatedRows(table, rows) { return rows; }
export function unscopedTable(table) { return table; }
export function eq(left, right) { return { left, right }; }
export async function closeDatabase() {}
"#,
);
fixture.write(
"server/api/boundary.post.nox",
r#"endpoint Boundary {
version: 1
body { mode: String }
result: String
scenario StorageWrites {
description: "storage works, against a store made for this scenario"
given: model Summarizer = text "unused"
when: request(body: Shape(mode = "write"))
expect: status == 200, value == "wrote:kept"
}
scenario StorageIsDiscarded {
description: "the previous scenario's writes did not survive it"
given: model Summarizer = text "unused"
when: request(body: Shape(mode = "read"))
expect: status == 200, value == "absent"
}
scenario QueueIsRefused {
description: "a scenario cannot hand work to a durable queue"
given: model Summarizer = text "unused"
when: request(body: Shape(mode = "queue"))
expect: status == 200, value == "MODEL_SCENARIO_IO_FORBIDDEN:enqueue(\"Reindex\")"
}
scenario DatabaseIsRefused {
description: "a scenario cannot reach the database adapter"
given: model Summarizer = text "unused"
when: request(body: Shape(mode = "database"))
expect: status == 200, value == "MODEL_SCENARIO_IO_FORBIDDEN:database"
}
scenario FetchIsStillBanned {
description: "live fetch stays banned for a delegated host"
given: model Summarizer = text "unused"
when: request(body: Shape(mode = "fetch"))
expect: status == 200, value == "fetch-blocked"
}
}
"#,
);
fixture.write(
"server/host.js",
r#"import { storage, enqueue } from "noxid:server";
import { database } from "../plugins/drizzle-orm/adapter.js";
const refusal = (error) => `${error?.code ?? "no-code"}:${error?.call ?? "no-call"}`;
export const endpoints = Object.freeze({
"endpoint:Boundary@1": async ({ mode }) => {
if (mode === "write") {
await storage("wo30-io").set("kept", { written: true });
const keys = await storage("wo30-io").list();
return `wrote:${keys.join(",")}`;
}
if (mode === "read") {
const kept = await storage("wo30-io").get("kept");
return kept === null ? "absent" : "leaked";
}
if (mode === "queue") {
try { await enqueue("Reindex", { id: 1 }); return "queue-reached"; }
catch (error) { return refusal(error); }
}
if (mode === "database") {
try { database({}); return "database-reached"; }
catch (error) { return refusal(error); }
}
try { await globalThis.fetch("http://127.0.0.1:9/escape"); return "fetch-reached"; }
catch (error) {
return String(error?.message).includes("SCENARIO_LIVE_IO_FORBIDDEN") ? "fetch-blocked" : `wrong-error:${error?.message}`;
}
},
});
"#,
);
let storage_root = fixture.root.join("scenario-storage");
fs::create_dir_all(&storage_root).expect("create isolated scenario storage root");
let output = fixture.scenario_gate_with(&[("NOXID_STORAGE_DIR", storage_root.as_path())]);
let log = format!(
"{}{}",
stdout(&output),
String::from_utf8_lossy(&output.stderr)
);
assert!(output.status.success(), "scenario gate failed:\n{log}");
assert!(log.contains("5 passed, 0 failed"), "{log}");
assert!(
!directory_has_files(&storage_root),
"the delegated host wrote durable storage under {}: {log}",
storage_root.display(),
);
}
#[test]
fn live_smoke_calls_each_provider_when_explicitly_enabled() {
if std::env::var("MODEL_LIVE_SMOKE").as_deref() != Ok("1") {
return;
}
let anthropic = std::env::var("ANTHROPIC_API_KEY").unwrap_or_default();
let openai = std::env::var("OPENAI_API_KEY").unwrap_or_default();
assert!(
!anthropic.is_empty() && !openai.is_empty(),
"MODEL_LIVE_SMOKE=1 needs real ANTHROPIC_API_KEY and OPENAI_API_KEY values"
);
let fixture = Fixture::new("live");
fixture.write(
"server/models/Assistant.nox",
"model Assistant {\n provider: anthropic\n id: \"claude-sonnet-4-5\"\n maxTokens: 64\n retries: 1\n secret: ANTHROPIC_API_KEY\n}\n",
);
fixture.write(
"server/models/Summarizer.nox",
"model Summarizer {\n provider: openai\n id: \"gpt-4.1-mini\"\n maxTokens: 64\n retries: 1\n secret: OPENAI_API_KEY\n}\n",
);
assert_success(&fixture.build(), "build live smoke fixture");
let output = fixture.run(
r#"
report("anthropic", (await generateText(models.Assistant, "Reply with the single word: ready")).text.trim().length > 0);
report("openai", (await generateText(models.Summarizer, "Reply with the single word: ready")).text.trim().length > 0);
done();
"#,
&[
("ANTHROPIC_API_KEY", anthropic.as_str()),
("OPENAI_API_KEY", openai.as_str()),
],
);
assert_success(&output, "run live smoke");
let log = stdout(&output);
assert!(log.contains("PROBE anthropic true"), "{log}");
assert!(log.contains("PROBE openai true"), "{log}");
}