feat: complete catalog-driven preprocessing
Publish documentation / publish (push) Successful in 2m12s

This commit is contained in:
Codex
2026-09-06 17:49:35 +02:00
parent 8707ae1d46
commit cffa60772e
141 changed files with 5898 additions and 3015 deletions
@@ -0,0 +1,259 @@
import Fastify from "fastify";
import { expect, test, vi } from "vitest";
import type { PrincipalContext } from "../src/auth/principal.js";
import type { CatalogRepository, WorkspaceDatabase } from "../src/catalog/types.js";
import {
readWorkspacePreprocessingStatus,
workspacePreprocessingRoutes,
type WorkspacePreprocessingRouteDeps,
} from "../src/routes/workspace-preprocessing.js";
import type { WorkspaceRegistry } from "../src/workspaces/registry.js";
const admin: PrincipalContext = {
issuer: "test",
subject: "admin",
roles: ["admin"],
permissions: ["session.use", "database.manage"],
isAdmin: true,
};
function database(overrides: Partial<WorkspaceDatabase> = {}): WorkspaceDatabase {
return {
id: "49b6491a-78bb-4cee-b27b-0efaf4419774",
workspaceId: "catalog-workspace",
engine: "postgres",
databaseName: "warehouse",
schema: "analytics",
version: 4,
createdAt: "2026-09-05T10:00:00.000Z",
updatedAt: "2026-09-05T10:00:00.000Z",
binding: { transport: "postgres_direct", host: "db", port: 5432, username: "reader" },
connectionStatus: "reachable",
schemaSyncedVersion: 4,
metadataContentRevision: 18,
preprocessingStatus: "failed",
...overrides,
};
}
function routeDeps(
current: () => WorkspaceDatabase | undefined,
overrides: Partial<WorkspacePreprocessingRouteDeps> = {},
): WorkspacePreprocessingRouteDeps {
const repository = {
getByWorkspace: vi.fn(async () => current()),
listSyncRuns: vi.fn(async () => []),
getActiveDescriptionGenerationRun: vi.fn(async () => undefined),
listSensitivityAnalysisRuns: vi.fn(async () => []),
} as unknown as CatalogRepository;
const registry = {
read: vi.fn(async () => ({
workspace: {
workspace: {
schema_version: 4,
id: "catalog-workspace",
name: "Catalog workspace",
language: "en",
},
},
revision: {
id: "catalog-workspace",
commit: "a".repeat(40),
blob: "b".repeat(40),
snapshotPath: "/data/workspaces/catalog-workspace.yaml",
},
})),
} as unknown as WorkspaceRegistry;
return {
repository,
registry,
service: {
run: vi.fn(async () => ({ status: "succeeded" })),
clear: vi.fn(async () => ({ status: "succeeded" })),
} as never,
inputFingerprint: {
workspaceInputFingerprint: vi.fn(async () => "sha256:current"),
} as never,
...overrides,
};
}
test("reports ready only when the Catalog revision and runtime fingerprint are current", async () => {
const ready = database({
preprocessingStatus: "succeeded",
preprocessedMetadataRevision: 18,
preprocessingInputFingerprint: "sha256:current",
preprocessingFinishedAt: "2026-09-05T10:04:00.000Z",
});
await expect(readWorkspacePreprocessingStatus(
"catalog-workspace",
routeDeps(() => ready),
)).resolves.toMatchObject({
state: "ready",
actionable: true,
clearable: true,
detail: "Catalog revision 18 is indexed.",
});
ready.preprocessingInputFingerprint = "sha256:old";
await expect(readWorkspacePreprocessingStatus(
"catalog-workspace",
routeDeps(() => ready),
)).resolves.toMatchObject({
state: "required",
actionable: true,
clearable: true,
detail: "Catalog revision 18 is not indexed.",
});
});
test("explains a current blocked prerequisite without inventing a run log", async () => {
const status = await readWorkspacePreprocessingStatus(
"catalog-workspace",
routeDeps(() => database({ schemaSyncedVersion: 3 })),
);
expect(status).toMatchObject({
state: "blocked",
actionable: false,
clearable: true,
detail: "Catalog synchronization is required.",
reason: "Database configuration v4 is newer than the latest Catalog synchronization v3.",
nextStep: "Open Database management and run Synchronize schema.",
});
expect(status).not.toHaveProperty("lastFailure");
});
test("exposes only the latest sanitized failed-run diagnostic", async () => {
const status = await readWorkspacePreprocessingStatus(
"catalog-workspace",
routeDeps(() => database({
preprocessingStatus: "failed",
preprocessingErrorCode: "schema_index_failed",
preprocessingFinishedAt: "2026-09-05T10:04:00.000Z",
})),
);
expect(status).toMatchObject({
state: "failed",
actionable: true,
clearable: true,
reason: expect.stringContaining("schema indexing worker"),
lastFailure: {
stage: "schema_index",
errorCode: "schema_index_failed",
finishedAt: "2026-09-05T10:04:00.000Z",
},
});
expect(status).not.toHaveProperty("history");
});
test("reports the durable phase of the active preprocessing run", async () => {
const status = await readWorkspacePreprocessingStatus(
"catalog-workspace",
routeDeps(
() => database({ preprocessingStatus: "running" }),
{
readLatestJob: () => ({
status: "active",
completedStages: ["catalog_snapshot"],
}) as never,
},
),
);
expect(status).toMatchObject({
state: "running",
detail: "Building schema vectors and LSH indexes.",
progress: { stage: "schema_index", step: 2, totalSteps: 4 },
});
});
test("explicitly reruns preprocessing when the current Catalog input is already ready", async () => {
const ready = database({
preprocessingStatus: "succeeded",
preprocessedMetadataRevision: 18,
preprocessingInputFingerprint: "sha256:current",
});
const deps = routeDeps(() => ready);
const app = Fastify();
app.addHook("preHandler", async (request) => { request.principal = admin; });
workspacePreprocessingRoutes(app, deps);
const response = await app.inject({
method: "POST",
url: "/workspaces/catalog-workspace/preprocessing",
});
expect(response.statusCode).toBe(200);
expect(response.json()).toMatchObject({ state: "ready", actionable: true });
expect(deps.service.run).toHaveBeenCalledTimes(1);
await app.close();
});
test("runs preprocessing once from the sanctioned admin endpoint and returns the new state", async () => {
let current = database();
const deps = routeDeps(() => current, {
service: {
run: vi.fn(async () => {
current = database({
preprocessingStatus: "succeeded",
preprocessedMetadataRevision: 18,
preprocessingInputFingerprint: "sha256:current",
preprocessingFinishedAt: "2026-09-05T10:04:00.000Z",
});
return { status: "succeeded" } as never;
}),
clear: vi.fn(async () => ({ status: "succeeded" } as never)),
},
});
const app = Fastify();
app.addHook("preHandler", async (request) => { request.principal = admin; });
workspacePreprocessingRoutes(app, deps);
const response = await app.inject({
method: "POST",
url: "/workspaces/catalog-workspace/preprocessing",
});
expect(response.statusCode).toBe(200);
expect(response.json()).toMatchObject({ state: "ready", actionable: true });
expect(deps.service.run).toHaveBeenCalledTimes(1);
await app.close();
});
test("clears only derived preprocessing data from the sanctioned admin endpoint", async () => {
let current = database({
preprocessingStatus: "succeeded",
preprocessedMetadataRevision: 18,
preprocessingInputFingerprint: "sha256:current",
});
const deps = routeDeps(() => current, {
service: {
run: vi.fn(),
clear: vi.fn(async () => {
current = database({
preprocessingStatus: "failed",
preprocessingErrorCode: "derived_data_cleared",
preprocessingFinishedAt: "2026-09-05T10:05:00.000Z",
});
return { status: "succeeded" } as never;
}),
},
});
const app = Fastify();
app.addHook("preHandler", async (request) => { request.principal = admin; });
workspacePreprocessingRoutes(app, deps);
const response = await app.inject({
method: "DELETE",
url: "/workspaces/catalog-workspace/preprocessing",
});
expect(response.statusCode).toBe(200);
expect(response.json()).toMatchObject({
state: "required",
clearable: false,
detail: "Reference vectors and LSH are empty. Memory is preserved.",
});
expect(deps.service.clear).toHaveBeenCalledTimes(1);
await app.close();
});