Skip to content
Merged
Show file tree
Hide file tree
Changes from 3 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
4 changes: 4 additions & 0 deletions src/cli/lab.ts
Original file line number Diff line number Diff line change
Expand Up @@ -36,6 +36,7 @@ import {
queryPassiveProductionSignals,
type PassiveProductionQueryResultV1,
} from "../lab/query";
import { isLabRouteSubjectId } from "../usage/log";
import {
CliUsageError,
RuntimeApiError,
Expand Down Expand Up @@ -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;
Expand Down
17 changes: 11 additions & 6 deletions src/server/management/lab-routes.ts
Original file line number Diff line number Diff line change
Expand Up @@ -30,6 +30,7 @@ import {
LabProjectionIncompatibleError,
LabProjectionUnavailableError,
LAB_QUERY_MAX_PAGE_SIZE,
PASSIVE_PRODUCTION_MAX_LIMIT,
queryLabArtifactByDigest,
queryLabArtifacts,
queryLabCatalogEntries,
Expand Down Expand Up @@ -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,
);
Expand Down Expand Up @@ -198,7 +203,7 @@ export async function handleLabRoutes(ctx: ManagementContext): Promise<Response
if (url.pathname === "/api/lab/production-signals") {
const subjectId = url.searchParams.get("subjectId")?.trim();
if (!subjectId) return errorResponse("invalid_subject", "subjectId is required", 400, ctx);
const limit = parseLimit(url.searchParams.get("limit"), ctx);
const limit = parseLimit(url.searchParams.get("limit"), ctx, PASSIVE_PRODUCTION_MAX_LIMIT);
if (limit instanceof Response) return limit;
try {
return jsonResponse(queryPassiveProductionSignals(subjectId, limit), 200, req, config);
Expand Down Expand Up @@ -398,4 +403,4 @@ export async function handleLabRoutes(ctx: ManagementContext): Promise<Response
}

return null;
}
}
93 changes: 93 additions & 0 deletions tests/lab-passive-production-surfaces.test.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,93 @@
import { afterEach, describe, expect, test } from "bun:test";
import { mkdirSync, rmSync } from "node:fs";
import { tmpdir } from "node:os";
import { join } from "node:path";
import { handleLabCommand } from "../src/cli/lab";
import {
LAB_QUERY_MAX_PAGE_SIZE,
PASSIVE_PRODUCTION_MAX_LIMIT,
} from "../src/lab/query";
import { handleManagementAPI } from "../src/server/management-api";
import type { OcxConfig } from "../src/types";
import { ManagementRequest } from "./helpers/management-auth";

const HOMES: string[] = [];

function tempHome(): string {
const dir = join(tmpdir(), `ocx-lab-passive-surfaces-${process.pid}-${Math.random().toString(16).slice(2)}`);
mkdirSync(dir, { recursive: true, mode: 0o700 });
HOMES.push(dir);
return dir;
}

afterEach(() => {
for (const dir of HOMES.splice(0)) {
rmSync(dir, { recursive: true, force: true });
}
delete process.env.OPENCODEX_HOME;
});
Comment thread
Wibias marked this conversation as resolved.
Resolved

function config(): OcxConfig {
return { providers: {} } as OcxConfig;
}

async function apiGet(home: string, path: string): Promise<Response> {
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;
}
});
});
Loading