diff --git a/src/cli/lab.ts b/src/cli/lab.ts index 013b96327a..5071404eb9 100644 --- a/src/cli/lab.ts +++ b/src/cli/lab.ts @@ -36,6 +36,7 @@ import { queryPassiveProductionSignals, type PassiveProductionQueryResultV1, } from "../lab/query"; +import { isLabRouteSubjectId } from "../usage/log"; import { CliUsageError, RuntimeApiError, @@ -243,6 +244,9 @@ export async function handleLabCommand(argv: string[], deps: LabCliDeps = {}): P const limit = takeIntegerOption(rest, "--limit", { min: 1 }); rejectArgs(rest, USAGE); if (!subjectId) throw new CliUsageError("--subject is required", USAGE); + if (!isLabRouteSubjectId(subjectId)) { + throw new CliUsageError("--subject must be an exact Lab route subject id", USAGE); + } const result = queryPassiveProductionSignals(subjectId, limit, configDir); printData(result, wantsJson, passiveProductionLines(result)); return; diff --git a/src/server/management/lab-routes.ts b/src/server/management/lab-routes.ts index 5c0929e47f..efd80ff43a 100644 --- a/src/server/management/lab-routes.ts +++ b/src/server/management/lab-routes.ts @@ -30,6 +30,7 @@ import { LabProjectionIncompatibleError, LabProjectionUnavailableError, LAB_QUERY_MAX_PAGE_SIZE, + PASSIVE_PRODUCTION_MAX_LIMIT, queryLabArtifactByDigest, queryLabArtifacts, queryLabCatalogEntries, @@ -75,20 +76,24 @@ function projectionErrorResponse(err: unknown, ctx: ManagementContext): Response return null; } -function parseLimit(raw: string | null, ctx: ManagementContext): number | undefined | Response { +function parseLimit( + raw: string | null, + ctx: ManagementContext, + max = LAB_QUERY_MAX_PAGE_SIZE, +): number | undefined | Response { const parsed = raw === null ? undefined : parseQueryInt(raw); if (parsed === "invalid") { return errorResponse( "invalid_limit", - `limit must be an integer from 1 to ${LAB_QUERY_MAX_PAGE_SIZE}`, + `limit must be an integer from 1 to ${max}`, 400, ctx, ); } - if (parsed !== undefined && (parsed < 1 || parsed > LAB_QUERY_MAX_PAGE_SIZE)) { + if (parsed !== undefined && (parsed < 1 || parsed > max)) { return errorResponse( "invalid_limit", - `limit must be an integer from 1 to ${LAB_QUERY_MAX_PAGE_SIZE}`, + `limit must be an integer from 1 to ${max}`, 400, ctx, ); @@ -198,7 +203,7 @@ export async function handleLabRoutes(ctx: ManagementContext): Promise { + for (const dir of HOMES.splice(0)) { + rmSync(dir, { recursive: true, force: true }); + } + if (originalOpenCodexHome === undefined) delete process.env.OPENCODEX_HOME; + else process.env.OPENCODEX_HOME = originalOpenCodexHome; +}); + +function config(): OcxConfig { + return { providers: {} } as OcxConfig; +} + +async function apiGet(home: string, path: string): Promise { + process.env.OPENCODEX_HOME = home; + const req = new ManagementRequest(`http://127.0.0.1${path}`, { method: "GET" }); + const response = await handleManagementAPI(req, new URL(req.url), config(), { + refreshCodexCatalog: async () => {}, + }); + expect(response).not.toBeNull(); + return response!; +} + +describe("CL-09 passive production read surfaces", () => { + test("management API uses the passive query limit without widening generic Lab pages", async () => { + const home = tempHome(); + const subjectId = "a".repeat(64); + + const accepted = await apiGet( + home, + `/api/lab/production-signals?subjectId=${subjectId}&limit=${PASSIVE_PRODUCTION_MAX_LIMIT}`, + ); + expect(accepted.status).toBe(200); + const acceptedBody = await accepted.json() as { + signals: unknown[]; + summary: { recentProductionAttempts: number }; + }; + expect(acceptedBody.signals).toEqual([]); + expect(acceptedBody.summary.recentProductionAttempts).toBe(0); + + const tooHigh = await apiGet( + home, + `/api/lab/production-signals?subjectId=${subjectId}&limit=${PASSIVE_PRODUCTION_MAX_LIMIT + 1}`, + ); + expect(tooHigh.status).toBe(400); + const tooHighBody = await tooHigh.json() as { error: { code: string; message: string } }; + expect(tooHighBody.error.code).toBe("invalid_limit"); + expect(tooHighBody.error.message).toContain(`1 to ${PASSIVE_PRODUCTION_MAX_LIMIT}`); + + const generic = await apiGet(home, `/api/lab/verdicts?limit=${LAB_QUERY_MAX_PAGE_SIZE + 1}`); + expect(generic.status).toBe(400); + const genericBody = await generic.json() as { error: { code: string; message: string } }; + expect(genericBody.error.code).toBe("invalid_limit"); + expect(genericBody.error.message).toContain(`1 to ${LAB_QUERY_MAX_PAGE_SIZE}`); + }); + + test("CLI reports malformed passive subject ids as the actual usage error", async () => { + const home = tempHome(); + const errors: string[] = []; + const originalError = console.error; + console.error = (...args: unknown[]) => { errors.push(args.join(" ")); }; + try { + expect(await handleLabCommand( + ["production-signals", "--subject", "not-a-subject-id"], + { configDir: home }, + )).toBe(2); + expect(errors.join("\n")).toContain("--subject must be an exact Lab route subject id"); + expect(errors.join("\n")).not.toContain("lab read failed"); + } finally { + console.error = originalError; + } + }); +});