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
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;
}
}
95 changes: 95 additions & 0 deletions tests/lab-passive-production-surfaces.test.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,95 @@
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[] = [];
const originalOpenCodexHome = process.env.OPENCODEX_HOME;

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 });
}
if (originalOpenCodexHome === undefined) delete process.env.OPENCODEX_HOME;
else process.env.OPENCODEX_HOME = originalOpenCodexHome;
});
Comment thread
Wibias marked this conversation as 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