diff --git a/runtime/tests/runtime-signing.test.mjs b/runtime/tests/runtime-signing.test.mjs index 7ffe52e..86b11e2 100644 --- a/runtime/tests/runtime-signing.test.mjs +++ b/runtime/tests/runtime-signing.test.mjs @@ -222,6 +222,142 @@ test("/verify accepts both wrapped and bare receipts from the production signing } }); +test("POST /execute dispatches execution.verb to clean and keeps canonical commons receipt entry", async () => { + const keys = makeKeys(); + const srv = await startServer({ + RECEIPT_SIGNING_PRIVATE_KEY_PEM_B64: keys.privatePemB64, + RECEIPT_SIGNING_PUBLIC_KEY_B64: keys.publicRaw32B64, + RECEIPT_SIGNER_ID: "runtime.commandlayer.eth", + }); + + try { + const resp = await fetch(`${srv.base}/execute`, { + method: "POST", + headers: { "content-type": "application/json" }, + body: JSON.stringify({ + execution: { verb: "clean", version: "1.1.0", class: "commons" }, + input: { content: " Hello world. " }, + }), + }); + const json = await resp.json(); + const { receipt } = unwrapReceiptResponse(json); + + assert.equal(resp.status, 200); + assert.equal(receipt.verb, "clean"); + assert.equal(receipt.entry, "https://runtime.commandlayer.org/execute"); + assert.equal(typeof receipt.result.cleaned_content, "string"); + assert.ok(receipt.result.cleaned_content.length > 0); + } finally { + await stop(srv.proc); + } +}); + +test("POST /execute falls back to top-level verb for summarize", async () => { + const keys = makeKeys(); + const srv = await startServer({ + RECEIPT_SIGNING_PRIVATE_KEY_PEM_B64: keys.privatePemB64, + RECEIPT_SIGNING_PUBLIC_KEY_B64: keys.publicRaw32B64, + RECEIPT_SIGNER_ID: "runtime.commandlayer.eth", + }); + + try { + const resp = await fetch(`${srv.base}/execute`, { + method: "POST", + headers: { "content-type": "application/json" }, + body: JSON.stringify({ + verb: "summarize", + input: { content: "Sentence one. Sentence two. Sentence three." }, + }), + }); + const json = await resp.json(); + const { receipt } = unwrapReceiptResponse(json); + + assert.equal(resp.status, 200); + assert.equal(receipt.verb, "summarize"); + assert.equal(typeof receipt.result.summary, "string"); + assert.ok(receipt.result.summary.length > 0); + } finally { + await stop(srv.proc); + } +}); + +test("POST /execute returns JSON 400 when the verb is missing", async () => { + const keys = makeKeys(); + const srv = await startServer({ + RECEIPT_SIGNING_PRIVATE_KEY_PEM_B64: keys.privatePemB64, + RECEIPT_SIGNING_PUBLIC_KEY_B64: keys.publicRaw32B64, + RECEIPT_SIGNER_ID: "runtime.commandlayer.eth", + }); + + try { + const resp = await fetch(`${srv.base}/execute`, { + method: "POST", + headers: { "content-type": "application/json" }, + body: JSON.stringify({ input: { content: "no verb" } }), + }); + const json = await resp.json(); + + assert.equal(resp.status, 400); + assert.equal(json.ok, false); + assert.equal(json.error, "missing_verb"); + } finally { + await stop(srv.proc); + } +}); + +test("POST /execute returns JSON 404 for unknown verbs", async () => { + const keys = makeKeys(); + const srv = await startServer({ + RECEIPT_SIGNING_PRIVATE_KEY_PEM_B64: keys.privatePemB64, + RECEIPT_SIGNING_PUBLIC_KEY_B64: keys.publicRaw32B64, + RECEIPT_SIGNER_ID: "runtime.commandlayer.eth", + }); + + try { + const resp = await fetch(`${srv.base}/execute`, { + method: "POST", + headers: { "content-type": "application/json" }, + body: JSON.stringify({ execution: { verb: "unknown-verb" } }), + }); + const json = await resp.json(); + + assert.equal(resp.status, 404); + assert.equal(json.status, "error"); + assert.match(String(json.message || json.error || ""), /Verb not enabled|Verb not implemented/); + } finally { + await stop(srv.proc); + } +}); + +test("legacy per-verb routes still work after adding POST /execute", async () => { + const keys = makeKeys(); + const srv = await startServer({ + RECEIPT_SIGNING_PRIVATE_KEY_PEM_B64: keys.privatePemB64, + RECEIPT_SIGNING_PUBLIC_KEY_B64: keys.publicRaw32B64, + RECEIPT_SIGNER_ID: "runtime.commandlayer.eth", + }); + + try { + const resp = await fetch(`${srv.base}/clean/v1.1.0`, { + method: "POST", + headers: { "content-type": "application/json" }, + body: JSON.stringify({ + execution: { verb: "clean", version: "1.1.0", class: "commons" }, + input: { content: " Legacy route. " }, + }), + }); + const json = await resp.json(); + const { receipt } = unwrapReceiptResponse(json); + + assert.equal(resp.status, 200); + assert.equal(receipt.verb, "clean"); + assert.equal(typeof receipt.result.cleaned_content, "string"); + assert.ok(receipt.result.cleaned_content.length > 0); + } finally { + await stop(srv.proc); + } +}); + test("schema validation fails on malformed receipt", async () => { const keys = makeKeys(); const schemaHost = await startSchemaHost(); diff --git a/server.mjs b/server.mjs index 41714a5..3e12633 100644 --- a/server.mjs +++ b/server.mjs @@ -2024,6 +2024,14 @@ app.post("/verify", async (req, res) => { }); // verb routes // ----------------------- +app.post("/execute", (req, res) => { + const resolvedVerb = String(req.body?.execution?.verb || req.body?.verb || "").trim(); + if (!resolvedVerb) { + return res.status(400).json({ ok: false, error: "missing_verb", message: "execution.verb or verb is required", ...instancePayload() }); + } + return handleVerb(resolvedVerb, req, res); +}); + for (const verb of ENABLED_VERBS) { app.post(`/${verb}/v${API_VERSION}`, (req, res) => handleVerb(verb, req, res)); }