Spaces:
Running
Running
File size: 15,680 Bytes
69f9a4b | 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 95 96 97 98 99 100 101 102 103 104 105 106 107 108 109 110 111 112 113 114 115 116 117 118 119 120 121 122 123 124 125 126 127 128 129 130 131 132 133 134 135 136 137 138 139 140 141 142 143 144 145 146 147 148 149 150 151 152 153 154 155 156 157 158 159 160 161 162 163 164 165 166 167 168 169 170 171 172 173 174 175 176 177 178 179 180 181 182 183 184 185 186 187 188 189 190 191 192 193 194 195 196 | import assert from "node:assert/strict"
import { spawn, type ChildProcess } from "node:child_process"
import { createServer, type Server } from "node:http"
import { cp, mkdir, mkdtemp, readFile, readdir, rm } from "node:fs/promises"
import path from "node:path"
import { tmpdir } from "node:os"
import { fileURLToPath } from "node:url"
export const FIXTURES = { NODEPAD_LOGIN_KEY: "SUPER_SECRET_LOGIN_928374", NODEPAD_API_KEY: "SUPER_SECRET_NODEPAD_API_928374", OPENAI_API: "SUPER_SECRET_OPENAI_928374" }
export function isolatedEnv(): NodeJS.ProcessEnv {
const env = { ...process.env }
for (const name of Object.keys(env)) if (/^(NODEPAD_|OPENAI_|GH_TOKEN$|GITHUB_TOKEN$|HF_TOKEN$|NEXT_PUBLIC_.*(?:KEY|TOKEN|SECRET))/.test(name)) delete env[name]
return { ...env, ...FIXTURES, NODE_ENV: "production", NODEPAD_GITHUB_SYNC: "false", NODEPAD_MAX_RESEARCH_TASKS_PER_HEARTBEAT: "0", NODEPAD_RESEARCH_ENABLED: "false", NODEPAD_SYNTHESIS_MODE: "deterministic", NEXT_TELEMETRY_DISABLED: "1" }
}
const secretSafe = (value: string) => Object.values(FIXTURES).every(secret => !value.includes(secret))
const listen = (server: Server, host = "127.0.0.1") => new Promise<number>((resolve, reject) => { server.once("error", reject); server.listen(0, host, () => resolve((server.address() as { port: number }).port)) })
const close = (server: Server) => new Promise<void>(resolve => { server.closeAllConnections(); server.close(() => resolve()) })
export async function freePort() { const server = createServer(); const port = await listen(server); await close(server); return port }
export async function smoke(docker = false) {
const root = process.cwd()
const temporary = await mkdtemp(path.join(tmpdir(), "nodepad-predeploy-"))
let child: ChildProcess | undefined
let logs = ""
let modelVersion = 1
let discoveryFailure = false
let completionFailure = false
const observed: { model: string; authorized: boolean }[] = []
const groups: Record<string, { passed: number; failed: number; result: string }> = {}
let group = ""
const check = (condition: unknown, message: string) => { assert.ok(condition, message); groups[group].passed++ }
async function section(name: string, action: () => Promise<void>) {
group = name; groups[name] = { passed: 0, failed: 0, result: "PASS" }
try { await action(); console.log(`PASS ${name}: ${groups[name].passed} assertions`) }
catch (error) { groups[name].failed++; groups[name].result = "FAIL"; throw error }
}
const provider = createServer(async (req, res) => {
const authorized = req.headers.authorization === `Bearer ${FIXTURES.OPENAI_API}`
if (!authorized) { res.writeHead(401).end(); return }
if (req.url === "/v1/models") {
res.setHeader("Content-Type", "application/json")
if (discoveryFailure) { res.writeHead(404).end("unsupported"); return }
res.end(JSON.stringify({ data: ["model-a", "Spark-X2.5", "Qwen/Qwen3.8-27B-FP8", "strange/custom:model@v2", ...(modelVersion > 1 ? ["model-c"] : [])].map(id => ({ id })) }))
return
}
if (req.url === "/v1/chat/completions") {
let text = ""; for await (const chunk of req) text += chunk
const body = JSON.parse(text); observed.push({ model: body.model, authorized })
if (completionFailure) { res.writeHead(503).end("fixture provider unavailable"); return }
res.setHeader("Content-Type", "application/json")
res.end(JSON.stringify({ model: body.model, choices: [{ message: { content: "NODEPAD_LLM_OK" } }] }))
return
}
res.writeHead(404).end()
})
let base = ""
let cookie = ""
const captures: string[] = []
async function request(url: string, method = "GET", body?: unknown, auth: "none" | "session" | "machine" | "bad-session" | "bad-machine" | "human-bearer" = "none", extra: Record<string, string> = {}) {
const headers: Record<string, string> = { ...extra }
if (auth === "session") headers.Cookie = cookie
if (auth === "machine") headers.Authorization = `Bearer ${FIXTURES.NODEPAD_API_KEY}`
if (auth === "bad-machine") headers.Authorization = "Bearer invalid-fixture"
if (auth === "human-bearer") headers.Authorization = `Bearer ${FIXTURES.NODEPAD_LOGIN_KEY}`
if (auth === "bad-session") headers.Cookie = "nodepad_session=invalid-fixture"
if (body !== undefined) headers["Content-Type"] = "application/json"
const response = await fetch(base + url, { method, headers, body: body === undefined ? undefined : JSON.stringify(body), redirect: "manual", signal: AbortSignal.timeout(20_000) })
const text = await response.text(); captures.push(text)
let data: any; try { data = JSON.parse(text) } catch { data = text }
return { response, data, text }
}
async function stop() {
if (!child || child.exitCode !== null || child.signalCode !== null) return
const exited = new Promise<void>(resolve => child!.once("exit", () => resolve()))
child.kill("SIGTERM")
const timer = setTimeout(() => child?.kill("SIGKILL"), 5000)
await exited; clearTimeout(timer)
}
try {
const providerPort = await listen(provider, docker ? "0.0.0.0" : "127.0.0.1")
const port = await freePort()
base = `http://127.0.0.1:${port}`
const env = { ...isolatedEnv(), NODEPAD_AUTH_DIR: path.join(temporary, "sessions"), NODEPAD_DATA_DIR: path.join(temporary, "data"), OPENAI_URL: `http://127.0.0.1:${providerPort}/v1`, OPENAI_MODEL: "qwythos-9b", PORT: String(port), HOSTNAME: "127.0.0.1" }
await cp(path.join(root, ".next/static"), path.join(root, ".next/standalone/.next/static"), { recursive: true })
await cp(path.join(root, "public"), path.join(root, ".next/standalone/public"), { recursive: true })
const start = () => {
child = spawn(process.execPath, [path.join(root, ".next/standalone/server.js")], { cwd: root, env, stdio: ["ignore", "pipe", "pipe"] })
child.stdout?.on("data", data => { logs += data.toString() }); child.stderr?.on("data", data => { logs += data.toString() })
}
const ready = async () => {
for (let attempt = 0; attempt < 100; attempt++) {
if (child?.exitCode !== null) throw new Error("Production server exited before readiness")
try { if ((await fetch(base + "/health", { signal: AbortSignal.timeout(500) })).ok) return } catch { /* poll readiness */ }
await new Promise(resolve => setTimeout(resolve, 100))
}
throw new Error("Production server did not become ready")
}
start(); await ready()
await section("Authentication", async () => {
check((await request("/health")).response.status === 200, "health public")
check((await request("/login")).response.status === 200, "login public")
check((await request("/api-docs")).response.status === 200, "docs public")
const anonymous = await request("/")
check(anonymous.response.status === 307 && new URL(anonymous.response.headers.get("location")!, base).pathname === "/login", "anonymous redirect")
check((await request("/api/auth/status")).data.authenticated === false, "anonymous status")
for (const body of [{}, { accessKey: "" }, { accessKey: "wrong" }, { accessKey: FIXTURES.NODEPAD_API_KEY }]) check((await request("/api/auth/login", "POST", body)).response.status === 401, "bad human login")
const login = await request("/api/auth/login", "POST", { accessKey: FIXTURES.NODEPAD_LOGIN_KEY })
check(login.response.status === 200 && login.data.success, "human login works")
const header = login.response.headers.get("set-cookie") || ""
for (const attribute of ["HttpOnly", "Secure", "Path=/", "SameSite=lax", "Max-Age="]) check(header.includes(attribute), "cookie security attribute")
cookie = header.split(";")[0]
check((await request("/", "GET", undefined, "session")).response.status === 200, "authenticated page no redirect loop")
check((await request("/api/auth/status", "GET", undefined, "session")).data.authenticated === true, "session status")
})
await section("Route protection", async () => {
const routes: [string, string, unknown?][] = [
["/api/v1/workspaces", "GET"], ["/api/v1/portfolio", "GET"], ["/api/v1/llm/status", "GET"], ["/api/v1/llm/models", "GET"], ["/api/v1/embeddings/status", "GET"], ["/api/v1/workspaces/matrix/adjudications", "GET"],
...["context", "graph", "evidence", "hypotheses", "brain", "events"].map(name => [`/api/v1/workspaces/matrix/${name}`, "GET"] as [string, string]),
["/api/v1/workspaces/matrix/evidence", "POST", { claim: "Matrix evidence" }], ["/api/v1/workspaces/matrix/heartbeat", "POST", { runId: "matrix" }], ["/api/v1/workspaces/matrix/brain/export", "POST", {}],
]
for (const [url, method, body] of routes) for (const auth of ["none", "bad-session", "bad-machine", "human-bearer", "session", "machine"] as const) {
const result = await request(url, method, body, auth)
check(auth === "session" || auth === "machine" ? result.response.ok : result.response.status === 401, `protected matrix ${method} ${url} ${auth}`)
}
})
await section("Custom models", async () => {
const models = await request("/api/v1/llm/models", "GET", undefined, "session")
check(models.data.defaultModel === "qwythos-9b", "environment default")
for (const id of ["qwythos-9b", "Spark-X2.5", "Qwen/Qwen3.8-27B-FP8", "strange/custom:model@v2"]) check(models.data.models.some((m: any) => m.id === id), "opaque discovered model")
modelVersion++
check((await request("/api/v1/llm/models", "GET", undefined, "session")).data.models.some((m: any) => m.id === "model-c"), "refresh exposes new model")
discoveryFailure = true
check((await request("/api/v1/llm/models", "GET", undefined, "session")).data.models.some((m: any) => m.id === "qwythos-9b"), "failed discovery retains default")
discoveryFailure = false
check((await request("/api/v1/llm/test", "POST", {}, "session")).data.content === "NODEPAD_LLM_OK", "mock inference success")
check(observed.at(-1)?.model === "qwythos-9b" && observed.at(-1)?.authorized, "default inference payload")
check((await request("/api/v1/llm/chat/completions", "POST", { model: "my-private-model-v37", messages: [{ role: "user", content: "test" }] }, "session")).response.ok, "manual model accepted")
check(observed.at(-1)?.model === "my-private-model-v37", "manual inference payload")
})
await section("Nodepad core", async () => {
const evidence = await request("/api/v1/workspaces/workspace-a/evidence", "POST", { claim: "Independent demand", strength: 5, sourceType: "interview" }, "machine", { "Idempotency-Key": "e1", "x-nodepad-actor-type": "agent", "x-nodepad-actor-id": "fixture-agent" })
check(evidence.response.status === 201 && evidence.data.workspaceId === "workspace-a" && evidence.data.provenance.actorId === "fixture-agent", "evidence persisted actor metadata")
const duplicate = await request("/api/v1/workspaces/workspace-a/evidence", "POST", { claim: "Independent demand" }, "machine", { "Idempotency-Key": "e1" })
check(duplicate.data.id === evidence.data.id, "idempotent evidence")
const hypothesis = await request("/api/v1/workspaces/workspace-a/hypotheses", "POST", { statement: "Demand exists", supportingEvidence: [evidence.data.id], evidenceScore: 100, confidence: 100, qualification: "strongly_qualified" }, "machine")
check(hypothesis.response.status === 201 && hypothesis.data.confidence < 100, "scoring overrides supplied LLM authority")
for (const name of ["context", "graph", "evidence", "hypotheses", "brain"]) check((await request(`/api/v1/workspaces/workspace-a/${name}`, "GET", undefined, "machine")).response.ok, "workspace retrieval")
check((await request("/api/v1/workspaces/workspace-b/evidence", "GET", undefined, "machine")).data.length === 0, "workspace isolation")
const context = (await request("/api/v1/workspaces/workspace-a/context", "GET", undefined, "machine")).data
check((await request("/api/v1/workspaces/workspace-a/research/tasks", "POST", { question: "q", objective: "support" }, "machine", { "If-Match": String(context.revision) })).response.ok, "revision write")
const conflict = await request("/api/v1/workspaces/workspace-a/research/tasks", "POST", { question: "stale", objective: "support" }, "machine", { "If-Match": String(context.revision) })
check(conflict.response.status === 409 && conflict.data.error.startsWith("revision_conflict"), "stale revision conflict")
completionFailure = true
check((await request("/api/v1/workspaces/workspace-a/syntheses/run", "POST", { synthesisType: "S011", runId: "fallback" }, "machine")).response.ok, "deterministic synthesis without LLM")
const before = observed.length
for (let i = 0; i < 3; i++) check((await request("/api/v1/workspaces/workspace-a/heartbeat", "POST", { runId: `repeat-${i}` }, "machine")).response.ok, "deterministic heartbeat")
check(observed.length === before, "heartbeat makes no inference calls")
const graph = (await request("/api/v1/workspaces/workspace-a/graph", "GET", undefined, "machine")).data
check(new Set(graph.nodes.map((n: any) => n.id)).size === graph.nodes.length && graph.nodes.length < 200, "bounded deduplicated graph")
const oversized = await request("/api/v1/workspaces/workspace-a/evidence", "POST", { claim: "x".repeat(1_000_001) }, "machine")
check(!oversized.response.ok && oversized.data.error === "request_body_too_large", "oversized mutation rejected")
completionFailure = false
})
await section("Secret isolation", async () => {
for (const response of captures) check(secretSafe(response), "response contains no fixture credentials")
check(secretSafe(logs), "production logs secret safe")
async function scan(directory: string) {
for (const entry of await readdir(directory, { withFileTypes: true })) {
const file = path.join(directory, entry.name)
if (entry.isDirectory()) await scan(file)
else check(secretSafe(await readFile(file, "utf8")), "client/persistent file secret safe")
}
}
await scan(path.join(root, ".next/static"))
await scan(temporary)
})
await section("Persistence and logout", async () => {
await stop(); start(); await ready()
check((await request("/api/auth/status", "GET", undefined, "session")).data.authenticated === true, "session survives process restart")
check((await request("/api/v1/workspaces/workspace-a/evidence", "GET", undefined, "machine")).data.length === 1, "evidence survives process restart")
check((await request("/api/auth/logout", "POST", undefined, "session")).response.ok, "logout success")
check((await request("/api/v1/workspaces", "GET", undefined, "session")).response.status === 401, "old cookie revoked")
check((await request("/api/v1/workspaces", "GET", undefined, "machine")).response.ok, "machine independent of logout")
})
return groups
} catch (error) {
const message = Object.values(FIXTURES).reduce((text, secret) => text.split(secret).join("[FIXTURE_REDACTED]"), error instanceof Error ? error.message : "smoke failure")
throw Object.assign(new Error(`${group || "Startup"}: ${message}`), { groups })
} finally {
await stop(); await close(provider); await rm(temporary, { recursive: true, force: true })
}
}
if (process.argv[1] && fileURLToPath(import.meta.url) === path.resolve(process.argv[1])) {
smoke().catch(error => { console.error(`FAIL ${error instanceof Error ? error.message : "local production smoke"}`); process.exitCode = 1 })
}
|