From a093312868c000a7914515cbc832f851a1537b9c Mon Sep 17 00:00:00 2001 From: FanouZeng-TT <18280587072@163.com> Date: Tue, 25 Aug 2026 21:35:41 +0800 Subject: [PATCH] fix(rest/nodejs): return UCP envelope for missing orders --- rest/nodejs/src/api/order.ts | 5 +- rest/nodejs/test/error_envelope.test.ts | 71 ++++++++++++++++++++++--- 2 files changed, 68 insertions(+), 8 deletions(-) diff --git a/rest/nodejs/src/api/order.ts b/rest/nodejs/src/api/order.ts index 819fe04..6b1c348 100644 --- a/rest/nodejs/src/api/order.ts +++ b/rest/nodejs/src/api/order.ts @@ -14,6 +14,7 @@ import { getOrder, logRequest, saveOrder } from "../data"; import { type Order } from "../models"; +import { ResourceNotFoundError, ucpErrorResponse } from "../utils/ucp_error"; import { type IdParamContext } from "../utils/validation"; /** @@ -28,7 +29,7 @@ export class OrderService { const order = getOrder(id); if (!order) { - return c.json({ error: "Order not found" }, 404); + return ucpErrorResponse(c, new ResourceNotFoundError("Order not found")); } return c.json(order, 200); }; @@ -47,7 +48,7 @@ export class OrderService { const existing = getOrder(id); if (!existing) { - return c.json({ error: "Order not found" }, 404); + return ucpErrorResponse(c, new ResourceNotFoundError("Order not found")); } // Ensure ID matches diff --git a/rest/nodejs/test/error_envelope.test.ts b/rest/nodejs/test/error_envelope.test.ts index 75de216..fb554a8 100644 --- a/rest/nodejs/test/error_envelope.test.ts +++ b/rest/nodejs/test/error_envelope.test.ts @@ -19,23 +19,27 @@ import { zValidator } from "@hono/zod-validator"; import { Hono } from "hono"; import { CheckoutService } from "../src/api/checkout"; +import { OrderService } from "../src/api/order"; import { TestingService } from "../src/api/testing"; import { getProductsDb, getTransactionsDb, initDbs } from "../src/data/db"; import { CheckoutCompleteRequestSchema, ExtendedCheckoutCreateRequestSchema, ExtendedCheckoutUpdateRequestSchema, + OrderSchema, + type Order, } from "../src/models"; import { UCP_VERSION } from "../src/utils/config"; import { IdParamSchema, prettyValidation } from "../src/utils/validation"; -// Checkout business/protocol failures must answer with the UCP error +// Checkout/order business and protocol failures must answer with the UCP error // envelope — `ucp.status: "error"` plus a typed `messages[]` entry carrying // `code` and `content` — matching the Python reference server // (rest/python/server/exceptions.py + server.py ucp_exception_handler), -// rather than the flat `{ detail }` shape. +// rather than a flat `{ detail }` or `{ error }` shape. function buildApp() { const svc = new CheckoutService(); + const orderSvc = new OrderService(); const app = new Hono<{ Variables: { logger: typeof console } }>(); app.use(async (c, next) => { c.set("logger", console); @@ -57,6 +61,17 @@ function buildApp() { zValidator("json", ExtendedCheckoutUpdateRequestSchema, prettyValidation), svc.updateCheckout ); + app.get( + "/orders/:id", + zValidator("param", IdParamSchema, prettyValidation), + orderSvc.getOrder + ); + app.put( + "/orders/:id", + zValidator("param", IdParamSchema, prettyValidation), + zValidator("json", OrderSchema, prettyValidation), + orderSvc.updateOrder + ); app.post( "/checkout-sessions/:id/complete", zValidator("param", IdParamSchema, prettyValidation), @@ -104,6 +119,32 @@ const CREATE_BODY = { payment: {}, }; +const ORDER_BODY: TestOrderBody = { + checkout_id: "chk_order_envelope", + currency: "USD", + fulfillment: {}, + id: "ord_missing_envelope", + line_items: [ + { + id: "line_1", + item: { + id: "bouquet_roses", + price: 3500, + title: "Red Rose", + }, + quantity: { fulfilled: 0, total: 1 }, + status: "processing", + totals: [{ amount: 3500, type: "subtotal" }], + }, + ], + permalink_url: "https://example.com/orders/ord_missing_envelope", + totals: [ + { amount: 3500, type: "subtotal" }, + { amount: 3500, type: "total" }, + ], + ucp: { capabilities: {}, version: UCP_VERSION }, +}; + interface UcpErrorBody { ucp?: { version?: string; status?: string }; messages?: Array<{ @@ -113,14 +154,18 @@ interface UcpErrorBody { severity?: string; }>; detail?: string; + error?: string; } +type TestOrderBody = Order; + function assertUcpError( body: UcpErrorBody, code: string, severity = "unrecoverable" ) { assert.equal(body.detail, undefined, "flat detail shape must be gone"); + assert.equal(body.error, undefined, "flat error shape must be gone"); assert.equal(body.ucp?.status, "error", "ucp.status must be 'error'"); assert.equal(body.ucp?.version, UCP_VERSION); assert.ok( @@ -174,10 +219,24 @@ test("unknown checkout id answers 404 with a RESOURCE_NOT_FOUND envelope", async assertUcpError((await res.json()) as UcpErrorBody, "RESOURCE_NOT_FOUND"); }); -// The Python reference envelopes this same failure through -// ResourceNotFoundError (services/checkout_service.py ship_order -> -// server.py ucp_exception_handler); the Node twin answered a flat -// { detail } until this test's fix. +test("unknown order id answers 404 with a RESOURCE_NOT_FOUND envelope", async () => { + const app = buildApp(); + const res = await app.request("/orders/no_such_order"); + assert.equal(res.status, 404); + assertUcpError((await res.json()) as UcpErrorBody, "RESOURCE_NOT_FOUND"); +}); + +test("updating an unknown order answers 404 with a RESOURCE_NOT_FOUND envelope", async () => { + const app = buildApp(); + const res = await app.request("/orders/no_such_order", { + method: "PUT", + headers: JSON_HEADERS, + body: JSON.stringify(ORDER_BODY), + }); + assert.equal(res.status, 404); + assertUcpError((await res.json()) as UcpErrorBody, "RESOURCE_NOT_FOUND"); +}); + test("simulate-shipping an unknown order answers 404 with a RESOURCE_NOT_FOUND envelope", async () => { const app = buildApp(); const res = await app.request(