Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
19 changes: 18 additions & 1 deletion apps/loopover-ui/public/openapi.json
Original file line number Diff line number Diff line change
Expand Up @@ -20901,11 +20901,25 @@
},
"400": {
"description": "Malformed JSON or invalid payload shape"
},
"401": {
"description": "Missing or invalid ingest credential"
},
"403": {
"description": "Instance not authenticated"
},
"413": {
"description": "Batch exceeds the 1 MiB (MAX_ORB_INGEST_BODY_BYTES) body ceiling"
}
},
"operationId": "postOrbIngest",
"tags": [
"ORB"
],
"security": [
{
"OrbBearer": []
}
]
}
},
Expand Down Expand Up @@ -26063,14 +26077,17 @@
}
],
"responses": {
"202": {
"200": {
"description": "Batch accepted"
},
"400": {
"description": "Malformed batch"
},
"401": {
"description": "Missing or invalid ingest credential"
},
"413": {
"description": "Batch exceeds the 1 MiB (MAX_ORB_INGEST_BODY_BYTES) body ceiling"
}
}
}
Expand Down
26 changes: 25 additions & 1 deletion src/openapi/internal-and-public-route-specs.ts
Original file line number Diff line number Diff line change
Expand Up @@ -303,7 +303,31 @@ const MISC_ROUTES: SpecEntry[] = [
// Its own shared-secret header, like the ORB ingress -- not a LoopOver bearer.
auth: "orb",
summary: "Ingest an AMS miner telemetry batch",
responses: { 202: { description: "Batch accepted" }, 400: { description: "Malformed batch" }, 401: { description: "Missing or invalid ingest credential" } },
// 200, not 202: the handler returns 200 on success, and that is the shipped, client-observed status. 413
// reflects the readOrbIngestBody 1 MiB (MAX_ORB_INGEST_BODY_BYTES) hard body ceiling.
responses: {
200: { description: "Batch accepted" },
400: { description: "Malformed batch" },
401: { description: "Missing or invalid ingest credential" },
413: { description: "Batch exceeds the 1 MiB (MAX_ORB_INGEST_BODY_BYTES) body ceiling" },
},
},
{
method: "post",
path: "/v1/orb/ingest",
operationId: "postOrbIngest",
tags: ["ORB"],
// Its own ORB_INGEST_TOKEN shared-secret bearer, not a LoopOver API token -- so auth: "orb", which is
// what derives the OrbBearer security stanza (the legacy registerPath block had none at all).
auth: "orb",
summary: "Ingest a batch of Orb events",
responses: {
200: { description: "Batch accepted; returns { accepted: number }" },
400: { description: "Malformed JSON or invalid payload shape" },
401: { description: "Missing or invalid ingest credential" },
403: { description: "Instance not authenticated" },
413: { description: "Batch exceeds the 1 MiB (MAX_ORB_INGEST_BODY_BYTES) body ceiling" },
},
},
{
method: "post",
Expand Down
11 changes: 0 additions & 11 deletions src/openapi/spec.ts
Original file line number Diff line number Diff line change
Expand Up @@ -1959,17 +1959,6 @@ export function buildOpenApiSpec() {
404: { description: "Public stats disabled (same flag as /v1/public/stats)" },
},
});
registry.registerPath({
method: "post",
path: "/v1/orb/ingest",
operationId: "postOrbIngest",
tags: ["ORB"],
summary: "Ingest a batch of Orb events",
responses: {
200: { description: "Batch accepted; returns { accepted: number }" },
400: { description: "Malformed JSON or invalid payload shape" },
},
});
registry.registerPath({
method: "get",
path: "/v1/auth/github/start",
Expand Down
63 changes: 63 additions & 0 deletions test/unit/openapi-ingest-status-parity.test.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,63 @@
import { describe, expect, it } from "vitest";
import { createApp } from "../../src/api/routes";
import { buildOpenApiSpec } from "../../src/openapi/spec";
import { MAX_ORB_INGEST_BODY_BYTES } from "../../src/orb/ingest";
import { createTestEnv } from "../helpers/d1";

// #9708: the two telemetry-ingest routes' PUBLISHED operations disagreed with their handlers -- AMS declared a
// 202 it never returns (its real status is 200) and omitted 413; ORB was a legacy registerPath block declaring
// only 200/400 with NO security stanza at all, despite carrying its own ORB_INGEST_TOKEN bearer. This test pins,
// for BOTH routes, that the handler's real 401/413/200 statuses are exactly what the OpenAPI document declares.

const AUTH = { authorization: "Bearer fleet-secret" };

const ROUTES = [
{
path: "/v1/ams/ingest",
tokenEnv: "AMS_INGEST_TOKEN" as const,
// AMS batch shape (camelCase).
validBody: JSON.stringify({ instanceId: "abc0", events: [{ repoHash: "rhash", prHash: "phash", decision: "merged" }] }),
},
{
path: "/v1/orb/ingest",
tokenEnv: "ORB_INGEST_TOKEN" as const,
// ORB batch shape (snake_case) -- a first-contact instance ingests successfully (registered=0).
validBody: JSON.stringify({ instance_id: "abc0", events: [{ repo_hash: "rhash", pr_hash: "phash", outcome: "merged", reversal_flag: "none" }] }),
},
];

describe("ingest routes: handler status <-> OpenAPI declaration parity (#9708)", () => {
const app = createApp();

for (const route of ROUTES) {
const authedEnv = () => createTestEnv({ [route.tokenEnv]: "fleet-secret" } as Partial<Env>);

it(`${route.path}: 401 with no bearer, 413 when oversized, 200 for a valid authenticated batch`, async () => {
// No bearer -> 401 (the collector fails closed).
const noAuth = await app.request(route.path, { method: "POST", headers: { "content-type": "application/json" }, body: route.validBody }, authedEnv());
expect(noAuth.status).toBe(401);

// Over the 1 MiB readOrbIngestBody ceiling -> 413.
const oversized = await app.request(route.path, { method: "POST", headers: AUTH, body: "x".repeat(MAX_ORB_INGEST_BODY_BYTES + 16) }, authedEnv());
expect(oversized.status).toBe(413);

// Valid authenticated batch -> 200 (never the AMS 202 the spec used to claim).
const ok = await app.request(route.path, { method: "POST", headers: { "content-type": "application/json", ...AUTH }, body: route.validBody }, authedEnv());
expect(ok.status).toBe(200);
});

it(`${route.path}: the OpenAPI document declares each observed status (200, 401, 413), and never 202`, () => {
const responses = buildOpenApiSpec().paths[route.path]?.post?.responses ?? {};
const declared = Object.keys(responses);
expect(declared).toEqual(expect.arrayContaining(["200", "401", "413"]));
expect(declared).not.toContain("202");
});
}

it("/v1/orb/ingest carries its OrbBearer security stanza (it had none as a legacy registerPath block)", () => {
const op = buildOpenApiSpec().paths["/v1/orb/ingest"]?.post;
expect(op?.security).toEqual([{ OrbBearer: [] }]);
// 403 (instance_unauthenticated) is a real ORB-only status the AMS route never returns.
expect(Object.keys(op?.responses ?? {})).toContain("403");
});
});
Loading