use std::fs;
use std::path::PathBuf;
use std::process::{Command, Output};
use std::sync::atomic::{AtomicU64, Ordering};
static NEXT_FIXTURE: AtomicU64 = AtomicU64::new(0);
struct Fixture {
root: PathBuf,
}
impl Fixture {
fn new() -> Self {
let root = std::env::temp_dir().join(format!(
"noxid-wo22-base-path-{}-{}",
std::process::id(),
NEXT_FIXTURE.fetch_add(1, Ordering::Relaxed)
));
fs::create_dir_all(root.join("server/api")).expect("create agent-surface fixture");
fs::write(
root.join("Noxid.toml"),
"[app]\ntitle = \"Agent doors\"\nbase = \"/shop\"\n\n[server]\napi_docs = true\nmcp = true\n",
)
.expect("write manifest");
fs::write(root.join("package.json"), "{\"type\":\"module\"}\n")
.expect("write package marker");
fs::write(
root.join("server/api/status.get.nox"),
"endpoint Status { result: String handler { return \"ready\" } }\n",
)
.expect("write endpoint");
Self { root }
}
fn noxid(&self, arguments: &[&str]) -> Output {
Command::new(env!("CARGO_BIN_EXE_noxid"))
.args(arguments)
.current_dir(&self.root)
.env_remove("DATABASE_URL")
.env_remove("VERCEL")
.env_remove("NETLIFY")
.env_remove("CF_PAGES")
.output()
.expect("run noxid")
}
}
impl Drop for Fixture {
fn drop(&mut self) {
let _ = fs::remove_dir_all(&self.root);
}
}
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)
);
}
#[test]
fn shop_base_prefixes_openapi_and_mcp_in_the_emitted_handler() {
let fixture = Fixture::new();
let build = fixture.noxid(&["build", ".", "--out-dir", "dist"]);
assert_success(&build, "build /shop agent-surface fixture");
fs::write(
fixture.root.join("dist/check.mjs"),
r#"import { fetch as handle } from "./server/handler.js";
let response = await handle(new Request("http://noxid.test/shop/_noxid/openapi.json"));
if (response.status !== 200) throw new Error(`prefixed OpenAPI returned ${response.status}`);
response = await handle(new Request("http://noxid.test/_noxid/openapi.json"));
if (response.status !== 404 || (await response.json()).error?.code !== "BOUNDARY_NOT_FOUND") throw new Error(`unprefixed OpenAPI returned ${response.status}`);
const mcp = (path) => new Request(`http://noxid.test${path}`, {
method: "POST",
headers: { "content-type": "application/json", accept: "application/json, text/event-stream" },
body: JSON.stringify({ jsonrpc: "2.0", id: 1, method: "tools/list" }),
});
response = await handle(mcp("/shop/_noxid/mcp"));
if (response.status !== 200 || !(await response.json()).result?.tools?.some((tool) => tool.name === "Status")) throw new Error("prefixed MCP did not expose Status");
response = await handle(mcp("/_noxid/mcp"));
if (response.status !== 404 || (await response.json()).error?.code !== "BOUNDARY_NOT_FOUND") throw new Error(`unprefixed MCP returned ${response.status}`);
"#,
)
.expect("write handler check");
let node = Command::new("node")
.arg("check.mjs")
.current_dir(fixture.root.join("dist"))
.output()
.expect("execute emitted handler");
assert_success(&node, "exercise base-prefixed handler doors");
}
#[test]
fn every_server_adapter_emits_the_base_aware_agent_surface_matcher() {
let fixture = Fixture::new();
fs::remove_file(fixture.root.join("server/api/status.get.nox"))
.expect("remove server-only endpoint from cross-adapter fixture");
fs::create_dir_all(fixture.root.join("src/routes")).expect("create SSR route directory");
fs::write(
fixture.root.join("src/routes/+page.nox"),
"component Home { route { render: ssr } view { <main>Shop</main> } }\n",
)
.expect("write cross-adapter SSR route");
fs::write(
fixture.root.join("server/host.js"),
"export const endpoints = Object.freeze({});\n",
)
.expect("write SSR host module");
let adapters = [
("node", "server.mjs"),
("deno", "server.ts"),
("cloudflare", "_worker.js"),
("vercel", ".vercel/output/functions/noxid.func/index.mjs"),
("netlify", "netlify/functions/noxid/index.mjs"),
];
for (adapter, artifact) in adapters {
let output = format!("dist-{adapter}");
let adapted = fixture.noxid(&["adapt", ".", "--adapter", adapter, "--out-dir", &output]);
assert_success(&adapted, &format!("adapt /shop project for {adapter}"));
let source = fs::read_to_string(fixture.root.join(&output).join(artifact))
.unwrap_or_else(|error| panic!("read {adapter} matcher {artifact}: {error}"));
assert!(source.contains("isAgentSurfacePath"), "{adapter}: {source}");
assert!(source.contains("isAgentSurfaceDoor"), "{adapter}: {source}");
assert!(source.contains("/shop"), "{adapter}: {source}");
assert!(
source.contains("/_noxid/openapi.json") && source.contains("/_noxid/mcp"),
"{adapter}: {source}"
);
if matches!(adapter, "node" | "deno") {
assert!(
source.contains("isReservedStaticPath"),
"{adapter} omitted the base-aware reserved static-path guard: {source}"
);
}
}
}
#[test]
fn every_server_adapter_forwards_both_agent_run_doors_under_its_base() {
let fixture = Fixture::new();
fs::remove_file(fixture.root.join("server/api/status.get.nox"))
.expect("remove server-only endpoint from cross-adapter fixture");
fs::create_dir_all(fixture.root.join("src/routes")).expect("create SSR route directory");
fs::write(
fixture.root.join("src/routes/+page.nox"),
"component Home { route { render: ssr } view { <main>Shop</main> } }\n",
)
.expect("write cross-adapter SSR route");
fs::write(
fixture.root.join("server/host.js"),
"export const endpoints = Object.freeze({});\n",
)
.expect("write SSR host module");
let cases: [(&str, bool); 9] = [
("/shop/_noxid/agents/Support/runs", true),
(
"/shop/_noxid/agents/Support/runs/aaaaaaaaaaaaaaaa/resume",
true,
),
("/shop/_noxid/agents/Support/runs/", true),
("/_noxid/agents/Support/runs", true),
("/_noxid/agents/Support/runs/aaaaaaaaaaaaaaaa/resume", true),
("/shop/_noxid/agents/Support/runs/aaaaaaaaaaaaaaaa", false),
("/shop/_noxid/agents/Support", false),
("/shop/_noxid/agents", false),
("/shop/_noxid/agentsx/Support/runs", false),
];
for (adapter, artifact) in [
("node", "server.mjs"),
("deno", "server.ts"),
("cloudflare", "_worker.js"),
("vercel", ".vercel/output/functions/noxid.func/index.mjs"),
("netlify", "netlify/functions/noxid/index.mjs"),
] {
let output = format!("runs-{adapter}");
let adapted = fixture.noxid(&["adapt", ".", "--adapter", adapter, "--out-dir", &output]);
assert_success(&adapted, &format!("adapt /shop project for {adapter}"));
let source = fs::read_to_string(fixture.root.join(&output).join(artifact))
.unwrap_or_else(|error| panic!("read {adapter} artifact {artifact}: {error}"));
assert!(
source.contains("isAgentRunDoor(pathname)"),
"{adapter} does not forward the agent run doors through the shared matcher:\n{source}"
);
let (_, tail) = source
.split_once("const isAgentRunDoor = (pathname) => {")
.unwrap_or_else(|| panic!("{adapter} carries no run-door matcher:\n{source}"));
let (body, _) = tail.split_once("};").expect("matcher body is closed");
let matcher = format!("const isAgentRunDoor = (pathname) => {{{body}}};");
let probes = cases
.iter()
.map(|(path, _)| {
format!(
"console.log(`{path} ${{isAgentRunDoor({path:?}) ? \"FORWARD\" : \"MISS\"}}`);"
)
})
.collect::<Vec<_>>()
.join("\n");
let probed = Command::new("node")
.args([
"--input-type=module",
"--eval",
&format!("{matcher}\n{probes}\n"),
])
.output()
.expect("run the emitted run-door matcher");
assert_success(&probed, &format!("evaluate the {adapter} run-door matcher"));
let observed = String::from_utf8_lossy(&probed.stdout).into_owned();
for (path, forwarded) in cases {
let expected = format!("{path} {}", if forwarded { "FORWARD" } else { "MISS" });
assert!(
observed.lines().any(|line| line == expected),
"the {adapter} front door disagrees about `{path}` (expected \
{expected:?}):\n{observed}"
);
}
}
}