dify/knowledge-fs/packages/api/src/document-compilation-worker.test.ts

1841 lines
63 KiB
TypeScript

import { createMemoryObjectStorageAdapter } from "@knowledge/adapters";
import { createNodePlatformAdapter } from "@knowledge/adapters/node";
import { ParseArtifactSchema, type PlatformAdapter } from "@knowledge/core";
import type { ParserAdapter } from "@knowledge/parsers";
import { describe, expect, it } from "vitest";
import {
DeletionLifecycleFenceActiveError,
createDeletionLifecycleFenceGuard,
createInMemoryDeletionLifecycleFenceReader,
} from "./deletion-lifecycle-fence";
import {
createDocumentCompilationJobStateMachine,
createDocumentCompilationWorker,
createDocumentMultimodalManifestBuilder,
createDocumentOutlineBuilder,
createDocumentOutlineSummaryEnhancer,
createInMemoryDocumentAssetRepository,
createInMemoryDocumentCompilationJobRepository,
createInMemoryDocumentMultimodalManifestRepository,
createInMemoryDocumentOutlineRepository,
createInMemoryKnowledgeFsLeaseRepository,
createInMemoryKnowledgePathRepository,
createInMemoryParseArtifactRepository,
createKnowledgeFsOperationLeaseCoordinator,
} from "./index";
describe("createDocumentCompilationWorker lease integration", () => {
it("fails closed instead of silently writing a generation payload as legacy", async () => {
const adapter = createTestPlatformAdapter();
const assets = createInMemoryDocumentAssetRepository({
maxAssets: 1,
now: () => "2026-05-27T10:00:00.000Z",
});
const asset = await assets.create({
filename: "Candidate.md",
id: "018f0d60-7a49-7cc2-9c1b-5b36f18f6a11",
knowledgeSpaceId: "018f0d60-7a49-7cc2-9c1b-5b36f18f2c42",
mimeType: "text/markdown",
objectKey: "tenant-1/spaces/space/documents/asset/Candidate.md",
sha256: "a".repeat(64),
sizeBytes: 12,
});
const generationId = "018f0d60-7a49-7cc2-9c1b-5b36f18f6a12";
const compilationJobs = createDocumentCompilationJobStateMachine({
generateId: () => "document-compilation-job-generation-1",
generatePublicationGenerationId: () => generationId,
jobs: adapter.jobs,
repository: createInMemoryDocumentCompilationJobRepository({ maxJobs: 1 }),
});
const compilationJob = await compilationJobs.start({
documentAssetId: asset.id,
knowledgeSpaceId: asset.knowledgeSpaceId,
tenantId: "tenant-1",
version: asset.version,
});
const worker = createDocumentCompilationWorker({
assets,
jobs: compilationJobs,
multimodalManifests: createInMemoryDocumentMultimodalManifestRepository({
maxManifests: 1,
}),
objectStorage: adapter.objectStorage,
parser: parser(),
reindexer: {
reindex: async () => {
throw new Error("generation payload must fail before reindex");
},
},
});
await expect(
worker.process({
documentAssetId: asset.id,
documentCompilationJobId: compilationJob.id,
knowledgeSpaceId: asset.knowledgeSpaceId,
publicationGenerationId: generationId,
tenantId: "tenant-1",
version: asset.version,
}),
).rejects.toThrow("Generation-scoped document compilation requires a publication coordinator");
await expect(compilationJobs.get(compilationJob.id)).resolves.toMatchObject({
stage: "failed",
});
});
it("does not persist compilation progress after a document deletion fence appears", async () => {
const adapter = createTestPlatformAdapter();
const assets = createInMemoryDocumentAssetRepository({ maxAssets: 1 });
const asset = await assets.create({
filename: "Stale.md",
id: "018f0d60-7a49-7cc2-9c1b-5b36f18f6c11",
knowledgeSpaceId: "018f0d60-7a49-7cc2-9c1b-5b36f18f2c42",
mimeType: "text/markdown",
objectKey: "tenant-1/spaces/space/documents/asset/Stale.md",
sha256: "a".repeat(64),
sizeBytes: 7,
sourceId: "018f0d60-7a49-7cc2-9c1b-5b36f18f6c12",
});
await adapter.objectStorage.putObject({
body: new TextEncoder().encode("# Stale"),
contentType: asset.mimeType,
key: asset.objectKey,
metadata: {},
});
const compilationJobs = createDocumentCompilationJobStateMachine({
generateId: () => "document-compilation-job-stale-1",
jobs: adapter.jobs,
repository: createInMemoryDocumentCompilationJobRepository({ maxJobs: 1 }),
});
const compilationJob = await compilationJobs.start({
documentAssetId: asset.id,
knowledgeSpaceId: asset.knowledgeSpaceId,
tenantId: "tenant-1",
version: asset.version,
});
const fences = createInMemoryDeletionLifecycleFenceReader();
let reindexCalls = 0;
const baseParser = parser();
const worker = createDocumentCompilationWorker({
assets,
deletionFence: createDeletionLifecycleFenceGuard(fences),
jobs: compilationJobs,
multimodalManifests: createInMemoryDocumentMultimodalManifestRepository({ maxManifests: 1 }),
objectStorage: adapter.objectStorage,
parser: {
...baseParser,
parse: async (input) => {
const parsed = await baseParser.parse(input);
await fences.activateFence({
id: "fence-stale-1",
knowledgeSpaceId: asset.knowledgeSpaceId,
targetId: asset.id,
targetType: "document",
tenantId: "tenant-1",
});
return parsed;
},
},
reindexer: {
reindex: async () => {
reindexCalls += 1;
throw new Error("stale worker reached reindex");
},
},
});
await expect(
worker.process({
documentAssetId: asset.id,
documentCompilationJobId: compilationJob.id,
knowledgeSpaceId: asset.knowledgeSpaceId,
tenantId: "tenant-1",
version: asset.version,
}),
).rejects.toBeInstanceOf(DeletionLifecycleFenceActiveError);
expect(reindexCalls).toBe(0);
await expect(compilationJobs.get(compilationJob.id)).resolves.toMatchObject({
stage: "queued",
});
await expect(
assets.get({ id: asset.id, knowledgeSpaceId: asset.knowledgeSpaceId }),
).resolves.toMatchObject({ parserStatus: "pending" });
});
it("compensates a multimodal object written after deletion inventory has passed", async () => {
const adapter = createTestPlatformAdapter();
const assets = createInMemoryDocumentAssetRepository({ maxAssets: 1 });
const asset = await assets.create({
filename: "Late-image.md",
id: "018f0d60-7a49-7cc2-9c1b-5b36f18f6c21",
knowledgeSpaceId: "018f0d60-7a49-7cc2-9c1b-5b36f18f2c42",
mimeType: "text/markdown",
objectKey: "tenant-1/spaces/space/documents/asset/Late-image.md",
sha256: "a".repeat(64),
sizeBytes: 12,
});
await adapter.objectStorage.putObject({
body: new TextEncoder().encode("# Late image"),
contentType: asset.mimeType,
key: asset.objectKey,
metadata: {},
});
const compilationJobs = createDocumentCompilationJobStateMachine({
generateId: () => "document-compilation-job-late-object-1",
jobs: adapter.jobs,
repository: createInMemoryDocumentCompilationJobRepository({ maxJobs: 1 }),
});
const compilationJob = await compilationJobs.start({
documentAssetId: asset.id,
knowledgeSpaceId: asset.knowledgeSpaceId,
tenantId: "tenant-1",
version: asset.version,
});
const fences = createInMemoryDeletionLifecycleFenceReader();
let latePutCount = 0;
const admittedScopes: { knowledgeSpaceId: string; tenantId: string }[] = [];
const objectStorage = {
...adapter.objectStorage,
putObject: async (input: Parameters<typeof adapter.objectStorage.putObject>[0]) => {
const stored = await adapter.objectStorage.putObject(input);
if (input.key.includes("/assets/")) {
latePutCount += 1;
// The delete worker already inventoried this prefix before the expired compiler writes.
await fences.activateFence({
id: "fence-late-object-1",
knowledgeSpaceId: asset.knowledgeSpaceId,
targetId: asset.id,
targetType: "document",
tenantId: "tenant-1",
});
}
return stored;
},
};
const worker = createDocumentCompilationWorker({
assets,
deletionFence: createDeletionLifecycleFenceGuard(fences),
jobs: compilationJobs,
multimodalManifests: createInMemoryDocumentMultimodalManifestRepository({ maxManifests: 1 }),
objectStorage,
objectWriteAdmission: {
withSpaceWriteAdmission: async (scope, write) => {
admittedScopes.push({ ...scope });
return write();
},
},
parser: parser(),
reindexer: {
reindex: async () => {
throw new Error("late multimodal writer reached reindex");
},
},
});
await expect(
worker.process({
documentAssetId: asset.id,
documentCompilationJobId: compilationJob.id,
knowledgeSpaceId: asset.knowledgeSpaceId,
tenantId: "tenant-1",
version: asset.version,
}),
).rejects.toBeInstanceOf(DeletionLifecycleFenceActiveError);
expect(latePutCount).toBe(1);
expect(admittedScopes).toEqual([
{ knowledgeSpaceId: asset.knowledgeSpaceId, tenantId: "tenant-1" },
]);
await expect(
adapter.objectStorage.listObjects({
limit: 10,
prefix: `tenant-1/spaces/${asset.knowledgeSpaceId}/documents/${asset.id}/assets/`,
}),
).resolves.toMatchObject({ objects: [] });
await expect(compilationJobs.get(compilationJob.id)).resolves.toMatchObject({
stage: "queued",
});
});
it("converts an in-flight compilation failure to the deletion fence and compensates objects", async () => {
const adapter = createTestPlatformAdapter();
const assets = createInMemoryDocumentAssetRepository({ maxAssets: 1 });
const asset = await assets.create({
filename: "Fence-on-error.md",
id: "018f0d60-7a49-7cc2-9c1b-5b36f18f6c22",
knowledgeSpaceId: "018f0d60-7a49-7cc2-9c1b-5b36f18f2c42",
mimeType: "text/markdown",
objectKey: "tenant-1/spaces/space/documents/asset/Fence-on-error.md",
sha256: "a".repeat(64),
sizeBytes: 16,
});
await adapter.objectStorage.putObject({
body: new TextEncoder().encode("# Fence on error"),
contentType: asset.mimeType,
key: asset.objectKey,
metadata: {},
});
const compilationJobs = createDocumentCompilationJobStateMachine({
generateId: () => "document-compilation-job-fence-on-error-1",
jobs: adapter.jobs,
repository: createInMemoryDocumentCompilationJobRepository({ maxJobs: 1 }),
});
const compilationJob = await compilationJobs.start({
documentAssetId: asset.id,
knowledgeSpaceId: asset.knowledgeSpaceId,
tenantId: "tenant-1",
version: asset.version,
});
const fences = createInMemoryDeletionLifecycleFenceReader();
const worker = createDocumentCompilationWorker({
assets,
deletionFence: createDeletionLifecycleFenceGuard(fences),
jobs: compilationJobs,
multimodalManifests: createInMemoryDocumentMultimodalManifestRepository({ maxManifests: 1 }),
objectStorage: adapter.objectStorage,
parser: parser(),
reindexer: {
reindex: async () => {
await fences.activateFence({
id: "fence-on-error-1",
knowledgeSpaceId: asset.knowledgeSpaceId,
targetId: asset.id,
targetType: "document",
tenantId: "tenant-1",
});
throw new Error("original compilation failure");
},
},
});
await expect(
worker.process({
documentAssetId: asset.id,
documentCompilationJobId: compilationJob.id,
knowledgeSpaceId: asset.knowledgeSpaceId,
tenantId: "tenant-1",
version: asset.version,
}),
).rejects.toBeInstanceOf(DeletionLifecycleFenceActiveError);
await expect(
adapter.objectStorage.listObjects({
limit: 10,
prefix: `tenant-1/spaces/${asset.knowledgeSpaceId}/documents/${asset.id}/assets/`,
}),
).resolves.toMatchObject({ objects: [] });
await expect(compilationJobs.get(compilationJob.id)).resolves.toMatchObject({
stage: "outline_built",
});
});
it("composes a complete generation receipt and stops before evaluation or publication", async () => {
const adapter = createTestPlatformAdapter();
const assets = createInMemoryDocumentAssetRepository({
maxAssets: 1,
now: () => "2026-07-13T10:00:00.000Z",
});
const asset = await assets.create({
filename: "Shadow.md",
id: "018f0d60-7a49-7cc2-9c1b-5b36f18f6a31",
knowledgeSpaceId: "018f0d60-7a49-7cc2-9c1b-5b36f18f2c42",
mimeType: "text/markdown",
objectKey: "tenant-1/spaces/space/documents/asset/Shadow.md",
sha256: "a".repeat(64),
sizeBytes: 12,
});
await adapter.objectStorage.putObject({
body: new TextEncoder().encode("# Shadow"),
contentType: asset.mimeType,
key: asset.objectKey,
metadata: {},
});
const generationId = "018f0d60-7a49-7cc2-9c1b-5b36f18f6a32";
const compilationJobs = createDocumentCompilationJobStateMachine({
generateId: () => "document-compilation-job-generation-shadow-1",
generatePublicationGenerationId: () => generationId,
jobs: adapter.jobs,
repository: createInMemoryDocumentCompilationJobRepository({ maxJobs: 1 }),
});
const compilationJob = await compilationJobs.start({
documentAssetId: asset.id,
knowledgeSpaceId: asset.knowledgeSpaceId,
tenantId: "tenant-1",
version: asset.version,
});
const receipts: unknown[] = [];
const reindexInputs: unknown[] = [];
let mutableEmbeddingReads = 0;
let pageIndexBuildCalls = 0;
let publishCalls = 0;
const semanticAdmissions: unknown[] = [];
let semanticCalls = 0;
let smokeCalls = 0;
const outlines = createInMemoryDocumentOutlineRepository({ maxOutlines: 2 });
const knowledgePaths = createInMemoryKnowledgePathRepository({
maxBatchSize: 10,
maxListLimit: 10,
maxPaths: 10,
});
const worker = createDocumentCompilationWorker({
assets,
candidateComposer: {
compose: async (input) => {
receipts.push(input);
},
},
denseEmbeddingModel: "legacy-dense-model-must-not-escape-the-frozen-attempt",
embeddingResolver: {
resolve: async () => {
mutableEmbeddingReads += 1;
throw new Error("Research-only frozen attempt must not read a mutable embedding profile");
},
},
failureManagement: "caller",
frozenRetrievalProfile: {
defaultMode: "research",
reasoningModel: {
model: "frozen-reasoning",
pluginId: "reasoning/plugin",
provider: "reasoning-provider",
},
rerank: { enabled: false },
revision: 4,
scoreThreshold: { enabled: false, stage: "mode-final" },
topK: 8,
},
indexOverrides: {
resolve: async () => ({
enableGraph: true,
enablePageIndex: false,
language: "zh-CN",
}),
},
generateKnowledgePathId: sequenceIds([
"018f0d60-7a49-7cc2-9c1b-5b36f18f6a33",
"018f0d60-7a49-7cc2-9c1b-5b36f18f6a34",
"018f0d60-7a49-7cc2-9c1b-5b36f18f6a35",
"018f0d60-7a49-7cc2-9c1b-5b36f18f6a36",
"018f0d60-7a49-7cc2-9c1b-5b36f18f6a37",
"018f0d60-7a49-7cc2-9c1b-5b36f18f6a38",
"018f0d60-7a49-7cc2-9c1b-5b36f18f6a3b",
"018f0d60-7a49-7cc2-9c1b-5b36f18f6a3c",
"018f0d60-7a49-7cc2-9c1b-5b36f18f6a3d",
]),
jobs: compilationJobs,
knowledgePaths,
multimodalManifests: createInMemoryDocumentMultimodalManifestRepository({
maxManifests: 2,
}),
objectStorage: adapter.objectStorage,
outlineBuilder: createDocumentOutlineBuilder({
maxElements: 10,
maxNodes: 10,
maxSummaryChars: 200,
}),
outlines,
pageIndexBuild: {
materializeBuilding: async ({ outline }) => {
pageIndexBuildCalls += 1;
return {
checksum: "a".repeat(64),
documentAssetId: outline.documentAssetId,
documentOutlineId: outline.id,
documentVersion: outline.version,
id: "018f0d60-7a49-7cc2-9c1b-5b36f18f6aff",
knowledgeSpaceId: outline.knowledgeSpaceId,
nodeCount: 1,
publicationGenerationId: generationId,
status: "building" as const,
termCount: 1,
tokenizerVersion: "pageindex-nfkc-exact-v1" as const,
};
},
},
parser: parser(),
reindexer: {
failProjections: async () => 0,
publishProjections: async () => {
publishCalls += 1;
return 0;
},
reindex: async (input) => {
reindexInputs.push(input);
return {
artifact: input.parseArtifact,
nodeIds: ["018f0d60-7a49-7cc2-9c1b-5b36f18f6a39"],
nodesCreated: 1,
outlineArtifact: ParseArtifactSchema.parse({
...input.parseArtifact,
elements: [
{
id: "018f0d60-7a49-7cc2-9c1b-5b36f18f6a39",
metadata: { semanticSectionSummary: "发票身份、购买方和金额信息。" },
sectionPath: ["电子发票", "购买方与金额"],
text: "发票号码、购买方与价税合计",
type: "paragraph",
},
],
metadata: { semanticCompilation: { source: "llm-semantic-v1" } },
}),
projectionIds: ["018f0d60-7a49-7cc2-9c1b-5b36f18f6a3a"],
projectionsCreated: 1,
status: "rebuilt",
};
},
},
semanticEnrichmentAdmission: {
enqueue: async (input) => {
semanticAdmissions.push(input);
},
},
jointSemanticGraph: {
materialize: async () => {
semanticCalls += 1;
return {
entitiesExtracted: 1,
graphEntityIds: ["018f0d60-7a49-7cc2-9c1b-5b36f18f6a3b"],
graphEntitiesIndexed: 1,
graphRelationIds: ["018f0d60-7a49-7cc2-9c1b-5b36f18f6a3c"],
graphRelationsIndexed: 1,
nodesScanned: 1,
semanticProviderCalls: 0,
semanticProviderCallsMaximum: 0,
};
},
},
semanticPostProcessor: {
process: async () => {
semanticCalls += 1;
return {
entitiesExtracted: 0,
graphEntityIds: [],
graphEntitiesIndexed: 0,
graphRelationIds: [],
graphRelationsIndexed: 0,
nodesScanned: 0,
nodesUpdated: 0,
parseArtifactId: "unused",
semanticCommunitiesMaterialized: 0,
};
},
},
smokeEvaluation: {
evaluate: async () => {
smokeCalls += 1;
throw new Error("generation candidate must not run legacy smoke evaluation");
},
},
});
await expect(
worker.process({
documentAssetId: asset.id,
documentCompilationJobId: compilationJob.id,
knowledgeSpaceId: asset.knowledgeSpaceId,
publicationGenerationId: generationId,
tenantId: "tenant-1",
version: asset.version,
}),
).resolves.toMatchObject({ stage: "projection_built" });
expect(receipts).toEqual([
expect.objectContaining({
componentReceipt: {
documentOutlines: [expect.objectContaining({ generationId })],
graphEntities: [
{
componentKey: "018f0d60-7a49-7cc2-9c1b-5b36f18f6a3b",
generationId,
},
],
graphRelations: [
{
componentKey: "018f0d60-7a49-7cc2-9c1b-5b36f18f6a3c",
generationId,
},
],
indexProjections: [
{
componentKey: "018f0d60-7a49-7cc2-9c1b-5b36f18f6a3a",
generationId,
},
],
knowledgePaths: expect.arrayContaining([expect.objectContaining({ generationId })]),
multimodalManifests: [expect.objectContaining({ generationId })],
schemaVersion: 1,
},
publicationGenerationId: generationId,
}),
]);
expect(publishCalls).toBe(0);
expect(smokeCalls).toBe(0);
expect(mutableEmbeddingReads).toBe(0);
expect(pageIndexBuildCalls).toBe(0);
expect(semanticAdmissions).toEqual([]);
expect(semanticCalls).toBe(1);
expect(reindexInputs[0]).toEqual(
expect.objectContaining({
enableGraph: true,
language: "zh-CN",
retrievalProfile: expect.objectContaining({ revision: 4 }),
skipDense: true,
}),
);
expect(reindexInputs[0]).not.toHaveProperty("denseModel");
expect(reindexInputs[0]).not.toHaveProperty("embeddingProfile");
await expect(
outlines.getByDocumentVersion({
documentAssetId: asset.id,
publicationGenerationId: generationId,
version: asset.version,
}),
).resolves.toMatchObject({
metadata: expect.objectContaining({ builder: "semantic-knowledge-nodes" }),
nodes: [
expect.objectContaining({
sectionPath: ["电子发票"],
children: [expect.objectContaining({ sectionPath: ["电子发票", "购买方与金额"] })],
}),
],
});
await expect(
assets.get({ id: asset.id, knowledgeSpaceId: asset.knowledgeSpaceId }),
).resolves.toMatchObject({ parserStatus: "pending" });
});
it("leaves transient status transitions to a durable caller", async () => {
const adapter = createTestPlatformAdapter();
const assets = createInMemoryDocumentAssetRepository({
maxAssets: 1,
now: () => "2026-07-13T10:00:00.000Z",
});
const asset = await assets.create({
filename: "Candidate.md",
id: "018f0d60-7a49-7cc2-9c1b-5b36f18f6a21",
knowledgeSpaceId: "018f0d60-7a49-7cc2-9c1b-5b36f18f2c42",
mimeType: "text/markdown",
objectKey: "tenant-1/spaces/space/documents/asset/Candidate.md",
sha256: "a".repeat(64),
sizeBytes: 12,
});
const generationId = "018f0d60-7a49-7cc2-9c1b-5b36f18f6a22";
const compilationJobs = createDocumentCompilationJobStateMachine({
generateId: () => "document-compilation-job-generation-2",
generatePublicationGenerationId: () => generationId,
jobs: adapter.jobs,
repository: createInMemoryDocumentCompilationJobRepository({ maxJobs: 1 }),
});
const compilationJob = await compilationJobs.start({
documentAssetId: asset.id,
knowledgeSpaceId: asset.knowledgeSpaceId,
tenantId: "tenant-1",
version: asset.version,
});
const worker = createDocumentCompilationWorker({
assets,
failureManagement: "caller",
jobs: compilationJobs,
multimodalManifests: createInMemoryDocumentMultimodalManifestRepository({
maxManifests: 1,
}),
objectStorage: adapter.objectStorage,
parser: parser(),
reindexer: {
reindex: async () => {
throw new Error("generation payload must fail before reindex");
},
},
});
await expect(
worker.process({
documentAssetId: asset.id,
documentCompilationJobId: compilationJob.id,
knowledgeSpaceId: asset.knowledgeSpaceId,
publicationGenerationId: generationId,
tenantId: "tenant-1",
version: asset.version,
}),
).rejects.toThrow("Generation-scoped document compilation requires a publication coordinator");
await expect(compilationJobs.get(compilationJob.id)).resolves.toMatchObject({
stage: "queued",
});
await expect(
assets.get({ id: asset.id, knowledgeSpaceId: asset.knowledgeSpaceId }),
).resolves.toMatchObject({ parserStatus: "pending" });
});
it("resumes an outline-built generation without reparsing or regenerating LLM summaries", async () => {
const adapter = createTestPlatformAdapter();
const assets = createInMemoryDocumentAssetRepository({
maxAssets: 1,
now: () => "2026-07-30T15:16:20.000Z",
});
const asset = await assets.create({
filename: "Retry.md",
id: "018f0d60-7a49-7cc2-9c1b-5b36f18f7001",
knowledgeSpaceId: "018f0d60-7a49-7cc2-9c1b-5b36f18f2c42",
mimeType: "text/markdown",
objectKey: "tenant-1/spaces/space/documents/asset/Retry.md",
sha256: "a".repeat(64),
sizeBytes: 7,
});
await adapter.objectStorage.putObject({
body: new TextEncoder().encode("# Retry"),
contentType: asset.mimeType,
key: asset.objectKey,
metadata: {},
});
const generationId = "018f0d60-7a49-7cc2-9c1b-5b36f18f7002";
const canonicalArtifact = ParseArtifactSchema.parse({
artifactHash: "b".repeat(64),
contentType: "text",
createdAt: "2026-07-30T15:16:21.000Z",
documentAssetId: asset.id,
elements: [
{
id: "018f0d60-7a49-7cc2-9c1b-5b36f18f7003:element-1",
metadata: {},
sectionPath: ["Retry"],
text: "Retry content",
type: "heading",
},
],
id: "018f0d60-7a49-7cc2-9c1b-5b36f18f7003",
metadata: {},
parser: "native-markdown",
version: asset.version,
});
const artifacts = createInMemoryParseArtifactRepository({ maxArtifacts: 2 });
await artifacts.create(canonicalArtifact);
const outlines = createInMemoryDocumentOutlineRepository({ maxOutlines: 2 });
const outlineBuilder = createDocumentOutlineBuilder({
maxElements: 10,
maxNodes: 10,
maxSummaryChars: 200,
now: () => "2026-07-30T15:16:22.000Z",
});
const persistedOutline = await outlines.upsert(
outlineBuilder.build({
knowledgeSpaceId: asset.knowledgeSpaceId,
parseArtifact: canonicalArtifact,
publicationGenerationId: generationId,
}),
);
const multimodalManifests = createInMemoryDocumentMultimodalManifestRepository({
maxManifests: 2,
});
const persistedManifest = await multimodalManifests.upsert(
createDocumentMultimodalManifestBuilder().build({
artifact: canonicalArtifact,
knowledgeSpaceId: asset.knowledgeSpaceId,
publicationGenerationId: generationId,
}),
);
const compilationJobs = createDocumentCompilationJobStateMachine({
generateId: () => "document-compilation-job-outline-retry-1",
generatePublicationGenerationId: () => generationId,
jobs: adapter.jobs,
repository: createInMemoryDocumentCompilationJobRepository({ maxJobs: 1 }),
});
const compilationJob = await compilationJobs.start({
documentAssetId: asset.id,
knowledgeSpaceId: asset.knowledgeSpaceId,
tenantId: "tenant-1",
version: asset.version,
});
await compilationJobs.advance(compilationJob.id, "parsed");
await compilationJobs.advance(compilationJob.id, "outline_built");
let parserCalls = 0;
let summaryCalls = 0;
let canonicalArtifactAvailable = false;
let manifestCheckpoint: "invalid" | "missing" | "valid" = "missing";
const receipts: unknown[] = [];
const resetFailedProjectionFlags: Array<boolean | undefined> = [];
const checkpointManifests = {
...multimodalManifests,
getByDocumentVersion: async (
input: Parameters<typeof multimodalManifests.getByDocumentVersion>[0],
) => {
const manifest = await multimodalManifests.getByDocumentVersion(input);
if (manifestCheckpoint === "missing" || !manifest) {
return null;
}
return manifestCheckpoint === "invalid"
? { ...manifest, artifactHash: "c".repeat(64) }
: manifest;
},
};
const workerWithoutCheckpointLoader = createDocumentCompilationWorker({
assets,
candidateComposer: { compose: async () => undefined },
failureManagement: "caller",
generateKnowledgePathId: () => "018f0d60-7a49-7cc2-9c1b-5b36f18f7004",
jobs: compilationJobs,
knowledgePaths: createInMemoryKnowledgePathRepository({
maxListLimit: 20,
maxPaths: 20,
}),
multimodalManifests,
objectStorage: adapter.objectStorage,
outlineBuilder,
outlines,
pageIndexBuild: {
materializeBuilding: async () => {
throw new Error("PageIndex must not run without a checkpoint artifact loader");
},
},
parser: parser(),
reindexer: {
reindex: async () => {
throw new Error("reindex must not run without a checkpoint artifact loader");
},
},
});
const payload = {
documentAssetId: asset.id,
documentCompilationJobId: compilationJob.id,
knowledgeSpaceId: asset.knowledgeSpaceId,
publicationGenerationId: generationId,
tenantId: "tenant-1",
version: asset.version,
} as const;
await expect(
workerWithoutCheckpointLoader.process({
...payload,
documentCompilationJobId: "missing-document-compilation-job",
}),
).rejects.toThrow("Document compilation job not found");
await expect(workerWithoutCheckpointLoader.process(payload)).rejects.toThrow(
"cannot load its parse artifact",
);
const worker = createDocumentCompilationWorker({
assets,
candidateComposer: {
compose: async (input) => {
receipts.push(input);
},
},
failureManagement: "caller",
generateKnowledgePathId: () => "018f0d60-7a49-7cc2-9c1b-5b36f18f7004",
jobs: compilationJobs,
knowledgePaths: createInMemoryKnowledgePathRepository({
maxListLimit: 20,
maxPaths: 20,
}),
multimodalManifests: checkpointManifests,
objectStorage: adapter.objectStorage,
outlineBuilder,
outlineSummaryEnhancer: {
enhance: async () => {
summaryCalls += 1;
throw new Error("outline summary must not be regenerated");
},
},
outlines,
pageIndexBuild: {
materializeBuilding: async () => {
throw new Error("PageIndex must not be regenerated");
},
},
parser: {
kind: "native-markdown",
parse: async () => {
parserCalls += 1;
throw new Error("document must not be reparsed");
},
},
reindexer: {
getCanonicalArtifact: async (input) =>
canonicalArtifactAvailable ? artifacts.getByDocumentVersion(input) : null,
reindex: async (input) => {
resetFailedProjectionFlags.push(input.resetFailedProjections);
return {
artifact: input.parseArtifact,
nodeIds: ["018f0d60-7a49-7cc2-9c1b-5b36f18f7005"],
nodesCreated: 1,
projectionIds: ["018f0d60-7a49-7cc2-9c1b-5b36f18f7006"],
projectionsCreated: 1,
status: "rebuilt",
};
},
},
});
await expect(worker.process(payload)).rejects.toThrow("parse artifact is missing");
canonicalArtifactAvailable = true;
await expect(worker.process(payload)).rejects.toThrow("derived components are missing");
manifestCheckpoint = "invalid";
await expect(worker.process(payload)).rejects.toThrow("derived component lineage is invalid");
manifestCheckpoint = "valid";
await expect(worker.process(payload)).resolves.toMatchObject({ stage: "projection_built" });
expect(parserCalls).toBe(0);
expect(summaryCalls).toBe(0);
expect(resetFailedProjectionFlags).toEqual([true]);
expect(receipts).toEqual([
expect.objectContaining({
componentReceipt: expect.objectContaining({
documentOutlines: [{ componentKey: persistedOutline.id, generationId }],
multimodalManifests: [{ componentKey: persistedManifest.id, generationId }],
}),
}),
]);
});
it("builds retry derivatives from the canonical artifact returned by reindexing", async () => {
const adapter = createTestPlatformAdapter();
const assets = createInMemoryDocumentAssetRepository({
maxAssets: 1,
now: () => "2026-07-13T10:00:00.000Z",
});
const asset = await assets.create({
filename: "Retry.md",
id: "018f0d60-7a49-7cc2-9c1b-5b36f18f6e01",
knowledgeSpaceId: "018f0d60-7a49-7cc2-9c1b-5b36f18f2c42",
mimeType: "text/markdown",
objectKey: "tenant-1/spaces/space/documents/asset/Retry.md",
sha256: "f".repeat(64),
sizeBytes: 7,
});
await adapter.objectStorage.putObject({
body: new TextEncoder().encode("# Retry"),
contentType: asset.mimeType,
key: asset.objectKey,
metadata: {},
});
const canonicalArtifactId = "018f0d60-7a49-7cc2-9c1b-5b36f18f6e02";
const retryArtifactId = "018f0d60-7a49-7cc2-9c1b-5b36f18f6e03";
const artifacts = createInMemoryParseArtifactRepository({ maxArtifacts: 2 });
await artifacts.create(
ParseArtifactSchema.parse({
artifactHash: "1".repeat(64),
contentType: "text",
createdAt: "2026-07-13T10:00:00.000Z",
documentAssetId: asset.id,
elements: [
{
id: "heading-1",
metadata: {},
sectionPath: ["Retry"],
text: "First attempt",
type: "heading",
},
],
id: canonicalArtifactId,
metadata: {},
parser: "native-markdown",
version: asset.version,
}),
);
const compilationJobs = createDocumentCompilationJobStateMachine({
generateId: () => "document-compilation-job-canonical-retry-1",
jobs: adapter.jobs,
repository: createInMemoryDocumentCompilationJobRepository({ maxJobs: 1 }),
});
const compilationJob = await compilationJobs.start({
documentAssetId: asset.id,
knowledgeSpaceId: asset.knowledgeSpaceId,
tenantId: "tenant-1",
version: asset.version,
});
const outlines = createInMemoryDocumentOutlineRepository({ maxOutlines: 2 });
const multimodalManifests = createInMemoryDocumentMultimodalManifestRepository({
maxManifests: 2,
});
const reindexInputArtifactIds: string[] = [];
const semanticArtifactIds: string[] = [];
const worker = createDocumentCompilationWorker({
assets,
jobs: compilationJobs,
multimodalManifests,
objectStorage: adapter.objectStorage,
outlineBuilder: createDocumentOutlineBuilder({
generateId: sequenceIds([
"018f0d60-7a49-7cc2-9c1b-5b36f18f6e04",
"018f0d60-7a49-7cc2-9c1b-5b36f18f6e05",
]),
maxElements: 10,
maxNodes: 10,
maxSummaryChars: 200,
now: () => "2026-07-13T10:00:00.000Z",
}),
outlines,
parser: {
kind: "native-markdown",
parse: async (input) =>
ParseArtifactSchema.parse({
artifactHash: "2".repeat(64),
contentType: "text",
createdAt: "2026-07-13T10:01:00.000Z",
documentAssetId: input.documentAssetId,
elements: [
{
id: "heading-1",
metadata: {},
sectionPath: ["Retry"],
text: "Retry attempt",
type: "heading",
},
],
id: retryArtifactId,
metadata: {},
parser: "native-markdown",
version: input.version,
}),
},
reindexer: {
canonicalizeArtifact: async (input) => artifacts.create(input),
reindex: async (input) => {
reindexInputArtifactIds.push(input.parseArtifact.id);
const canonicalArtifact = await artifacts.create(input.parseArtifact);
return {
artifact: canonicalArtifact,
nodesCreated: 1,
projectionIds: [],
projectionsCreated: 0,
status: "rebuilt",
};
},
},
semanticPostProcessor: {
process: async ({ parseArtifact }) => {
semanticArtifactIds.push(parseArtifact.id);
return {
entitiesExtracted: 0,
graphEntityIds: [],
graphEntitiesIndexed: 0,
graphRelationIds: [],
graphRelationsIndexed: 0,
nodesScanned: 0,
nodesUpdated: 0,
parseArtifactId: parseArtifact.id,
semanticCommunitiesMaterialized: 0,
};
},
},
});
await expect(
worker.process({
documentAssetId: asset.id,
documentCompilationJobId: compilationJob.id,
knowledgeSpaceId: asset.knowledgeSpaceId,
tenantId: "tenant-1",
version: asset.version,
}),
).resolves.toMatchObject({ stage: "published" });
expect(reindexInputArtifactIds).toEqual([canonicalArtifactId]);
expect(semanticArtifactIds).toEqual([canonicalArtifactId]);
await expect(
artifacts.getByDocumentVersion({ documentAssetId: asset.id, version: asset.version }),
).resolves.toMatchObject({ id: canonicalArtifactId });
await expect(artifacts.getById({ id: retryArtifactId })).resolves.toBeNull();
await expect(
outlines.getByDocumentVersion({ documentAssetId: asset.id, version: asset.version }),
).resolves.toMatchObject({ parseArtifactId: canonicalArtifactId });
await expect(
multimodalManifests.getByDocumentVersion({
documentAssetId: asset.id,
version: asset.version,
}),
).resolves.toMatchObject({ parseArtifactId: canonicalArtifactId });
});
it("does not expose legacy ready projections when outline construction fails", async () => {
const adapter = createTestPlatformAdapter();
const assets = createInMemoryDocumentAssetRepository({
maxAssets: 1,
now: () => "2026-07-13T10:00:00.000Z",
});
const asset = await assets.create({
filename: "Outline-failure.md",
id: "018f0d60-7a49-7cc2-9c1b-5b36f18f6f01",
knowledgeSpaceId: "018f0d60-7a49-7cc2-9c1b-5b36f18f2c42",
mimeType: "text/markdown",
objectKey: "tenant-1/spaces/space/documents/asset/Outline-failure.md",
sha256: "a".repeat(64),
sizeBytes: 12,
});
await adapter.objectStorage.putObject({
body: new TextEncoder().encode("# Failure"),
contentType: asset.mimeType,
key: asset.objectKey,
metadata: {},
});
const compilationJobs = createDocumentCompilationJobStateMachine({
generateId: () => "document-compilation-job-outline-failure-1",
jobs: adapter.jobs,
repository: createInMemoryDocumentCompilationJobRepository({ maxJobs: 1 }),
});
const compilationJob = await compilationJobs.start({
documentAssetId: asset.id,
knowledgeSpaceId: asset.knowledgeSpaceId,
tenantId: "tenant-1",
version: asset.version,
});
let reindexCalls = 0;
const worker = createDocumentCompilationWorker({
assets,
jobs: compilationJobs,
multimodalManifests: createInMemoryDocumentMultimodalManifestRepository({
maxManifests: 1,
}),
objectStorage: adapter.objectStorage,
outlineBuilder: {
build: () => {
throw new Error("outline failed");
},
},
outlines: createInMemoryDocumentOutlineRepository({ maxOutlines: 1 }),
parser: parser(),
reindexer: {
canonicalizeArtifact: async (input) => input,
reindex: async (input) => {
reindexCalls += 1;
return {
artifact: input.parseArtifact,
nodesCreated: 1,
projectionsCreated: 1,
status: "rebuilt",
};
},
},
});
await expect(
worker.process({
documentAssetId: asset.id,
documentCompilationJobId: compilationJob.id,
knowledgeSpaceId: asset.knowledgeSpaceId,
tenantId: "tenant-1",
version: asset.version,
}),
).rejects.toThrow("outline failed");
expect(reindexCalls).toBe(0);
});
it("wraps durable document compilation work in a publish lease", async () => {
const adapter = createTestPlatformAdapter();
const assets = createInMemoryDocumentAssetRepository({
maxAssets: 4,
now: () => "2026-05-27T10:00:00.000Z",
});
const asset = await assets.create({
filename: "Worker.md",
id: "018f0d60-7a49-7cc2-9c1b-5b36f18f6a01",
knowledgeSpaceId: "018f0d60-7a49-7cc2-9c1b-5b36f18f2c42",
mimeType: "text/markdown",
objectKey: "tenant-1/spaces/space/documents/asset/Worker.md",
sha256: "a".repeat(64),
sizeBytes: 12,
});
await adapter.objectStorage.putObject({
body: new TextEncoder().encode("# Worker"),
contentType: asset.mimeType,
key: asset.objectKey,
metadata: {},
});
const compilationJobs = createDocumentCompilationJobStateMachine({
generateId: () => "document-compilation-job-lease-1",
jobs: adapter.jobs,
repository: createInMemoryDocumentCompilationJobRepository({ maxJobs: 4 }),
});
const compilationJob = await compilationJobs.start({
documentAssetId: asset.id,
knowledgeSpaceId: asset.knowledgeSpaceId,
tenantId: "tenant-1",
version: asset.version,
});
const leases = createInMemoryKnowledgeFsLeaseRepository({
maxLeases: 10,
maxListLimit: 10,
});
const knowledgePaths = createInMemoryKnowledgePathRepository({
maxListLimit: 10,
maxPaths: 10,
});
const semanticCalls: unknown[] = [];
const reindexCalls: unknown[] = [];
const projectionLifecycle: string[] = [];
const outlines = createInMemoryDocumentOutlineRepository({ maxOutlines: 4 });
const multimodalManifests = createInMemoryDocumentMultimodalManifestRepository({
maxManifests: 4,
});
const frozenEmbeddingProfile = {
dimension: 768,
model: "frozen-space-model",
pluginId: "frozen-space/plugin",
provider: "frozen-space-provider",
revision: 5,
vectorSpaceId: `embedding-space-sha256:${"e".repeat(64)}`,
} as const;
const frozenRetrievalProfile = {
defaultMode: "research" as const,
reasoningModel: {
model: "frozen-reasoning-model",
pluginId: "frozen-reasoning/plugin",
provider: "frozen-reasoning-provider",
},
rerank: { enabled: false },
revision: 7,
scoreThreshold: { enabled: false, stage: "mode-final" as const },
topK: 12,
};
let mutableEmbeddingReads = 0;
let frozenRetrievalObserved = false;
const summaryEnhancer = createDocumentOutlineSummaryEnhancer({
maxInputChars: 200,
maxSummaryChars: 80,
model: "outline-summary-model",
promptVersion: "document-outline-summary-v1",
provider: {
summarize: async (input) => ({
summary: `provider summary for ${input.title}`,
}),
},
});
const worker = createDocumentCompilationWorker({
assets,
embeddingResolver: {
resolve: async () => {
mutableEmbeddingReads += 1;
throw new Error("mutable embedding profile must not be read");
},
},
frozenEmbeddingProfile,
frozenRetrievalProfile,
generateKnowledgePathId: sequenceIds([
"018f0d60-7a49-7cc2-9c1b-5b36f18f6a05",
"018f0d60-7a49-7cc2-9c1b-5b36f18f6a06",
"018f0d60-7a49-7cc2-9c1b-5b36f18f6a07",
"018f0d60-7a49-7cc2-9c1b-5b36f18f6a08",
"018f0d60-7a49-7cc2-9c1b-5b36f18f6a09",
]),
jobs: compilationJobs,
knowledgePaths,
multimodalManifests,
objectStorage: adapter.objectStorage,
operationLeases: createKnowledgeFsOperationLeaseCoordinator({
generateLeaseId: () => "018f0d60-7a49-7cc2-9c1b-5b36f18f5c01",
leaseTtlMs: 60_000,
leases,
now: () => "2026-05-27T10:00:00.000Z",
sessionId: "018f0d60-7a49-7cc2-9c1b-5b36f18f2c53",
}),
parser: parser(),
outlineBuilder: createDocumentOutlineBuilder({
generateId: sequenceIds([
"018f0d60-7a49-7cc2-9c1b-5b36f18f6a03",
"018f0d60-7a49-7cc2-9c1b-5b36f18f6a04",
]),
maxElements: 20,
maxNodes: 10,
maxSummaryChars: 200,
now: () => "2026-05-27T10:00:00.000Z",
}),
outlineSummaryEnhancer: {
enhance: async (input) => {
frozenRetrievalObserved = input.retrievalProfile === frozenRetrievalProfile;
return summaryEnhancer.enhance(input);
},
},
outlines,
reindexer: {
failProjections: async (input) => {
projectionLifecycle.push("fail");
return input.projectionIds.length;
},
publishProjections: async (input) => {
projectionLifecycle.push("publish");
return input.projectionIds.length;
},
reindex: async (input) => {
projectionLifecycle.push("reindex");
reindexCalls.push(input);
return {
artifact: input.parseArtifact,
nodesCreated: 1,
projectionIds: ["projection-worker-1"],
projectionsCreated: 1,
status: "rebuilt",
};
},
},
semanticPostProcessor: {
process: async (input) => {
semanticCalls.push(input);
return {
entitiesExtracted: 2,
graphEntityIds: [],
graphEntitiesIndexed: 2,
graphRelationIds: [],
graphRelationsIndexed: 0,
nodesScanned: 1,
nodesUpdated: 1,
parseArtifactId: input.parseArtifact.id,
semanticCommunitiesMaterialized: 1,
};
},
},
smokeEvaluation: {
evaluate: async () => {
projectionLifecycle.push("smoke");
return {
decision: "passed",
evaluation: {
items: [],
metrics: {
citationHitRate: 1,
noAnswerRate: 0,
recallAtK: 1,
totalQuestions: 1,
},
},
};
},
},
});
await expect(
worker.process({
documentAssetId: asset.id,
documentCompilationJobId: compilationJob.id,
knowledgeSpaceId: asset.knowledgeSpaceId,
tenantId: "tenant-1",
version: asset.version,
}),
).resolves.toMatchObject({ stage: "published" });
await expect(
outlines.getByDocumentVersion({ documentAssetId: asset.id, version: 1 }),
).resolves.toMatchObject({
documentAssetId: asset.id,
metadata: {
summary: {
model: "outline-summary-model",
promptVersion: "document-outline-summary-v1",
source: "provider",
},
},
nodes: [
{ summary: "provider summary for Worker", title: "Worker", tocSource: "parser-heading" },
],
});
await expect(
multimodalManifests.getByDocumentVersion({ documentAssetId: asset.id, version: 1 }),
).resolves.toMatchObject({
documentAssetId: asset.id,
items: [expect.objectContaining({ parseElementId: "figure-1" })],
version: 1,
});
await expect(
knowledgePaths.get({
knowledgeSpaceId: asset.knowledgeSpaceId,
virtualPath: "/knowledge/docs/Worker.md--018f0d60/outline.json",
}),
).resolves.toMatchObject({
metadata: { contentKind: "document-outline" },
targetId: asset.id,
});
await expect(
knowledgePaths.get({
knowledgeSpaceId: asset.knowledgeSpaceId,
virtualPath: "/knowledge/docs/Worker.md--018f0d60/multimodal.json",
}),
).resolves.toMatchObject({
metadata: { contentKind: "document-multimodal-manifest" },
targetId: asset.id,
});
const assetPaths = await knowledgePaths.listPhysicalDescendants({
knowledgeSpaceId: asset.knowledgeSpaceId,
limit: 10,
parentPath: "/knowledge/docs/Worker.md--018f0d60/assets",
viewName: "docs",
});
expect(assetPaths.items).toEqual([
expect.objectContaining({
metadata: expect.objectContaining({
contentKind: "document-multimodal-asset",
itemId: "018f0d60-7a49-7cc2-9c1b-5b36f18f6a02:1:figure-1",
modality: "image",
objectKey: expect.stringMatching(
/^tenant-1\/spaces\/018f0d60-7a49-7cc2-9c1b-5b36f18f2c42\/documents\/018f0d60-7a49-7cc2-9c1b-5b36f18f6a01\/assets\/figure-1-[a-f0-9]{12}\.png$/u,
),
}),
targetId: asset.id,
virtualPath:
"/knowledge/docs/Worker.md--018f0d60/assets/image-Worker-diagram--018f0d60.json",
}),
]);
await expect(
adapter.objectStorage.getObject(String(assetPaths.items[0]?.metadata.objectKey)),
).resolves.toEqual(new Uint8Array([1, 2, 3, 4]));
const sectionPaths = await knowledgePaths.listPhysicalDescendants({
knowledgeSpaceId: asset.knowledgeSpaceId,
limit: 10,
parentPath: "/knowledge/docs/Worker.md--018f0d60/sections",
viewName: "docs",
});
expect(sectionPaths.items).toEqual([
expect.objectContaining({
metadata: expect.objectContaining({
contentKind: "document-section",
sectionPath: ["Worker"],
}),
targetId: asset.id,
}),
]);
expect(semanticCalls).toEqual([
{
knowledgeSpaceId: asset.knowledgeSpaceId,
parseArtifact: expect.objectContaining({ documentAssetId: asset.id }),
retrievalProfile: frozenRetrievalProfile,
tenantId: "tenant-1",
},
]);
expect(reindexCalls).toEqual([
expect.objectContaining({
denseModel: frozenEmbeddingProfile.vectorSpaceId,
embeddingProfile: frozenEmbeddingProfile,
enableGraph: true,
projectionStatus: "building",
retrievalProfile: frozenRetrievalProfile,
tenantId: "tenant-1",
}),
]);
expect(mutableEmbeddingReads).toBe(0);
expect(frozenRetrievalObserved).toBe(true);
expect(projectionLifecycle).toEqual(["reindex", "smoke", "publish"]);
await expect(
leases.get({
id: "018f0d60-7a49-7cc2-9c1b-5b36f18f5c01",
tenantId: "tenant-1",
}),
).resolves.toMatchObject({
leaseType: "publish",
status: "released",
targetId: asset.id,
targetVersion: 1,
virtualPath: `/sources/documents/${asset.id}`,
});
});
it("fails staged projections without publishing when smoke evaluation rejects them", async () => {
const adapter = createTestPlatformAdapter();
const assets = createInMemoryDocumentAssetRepository({
maxAssets: 4,
now: () => "2026-05-27T10:00:00.000Z",
});
const asset = await assets.create({
filename: "Rejected.md",
id: "018f0d60-7a49-7cc2-9c1b-5b36f18f6c01",
knowledgeSpaceId: "018f0d60-7a49-7cc2-9c1b-5b36f18f2c42",
mimeType: "text/markdown",
objectKey: "tenant-1/spaces/space/documents/asset/Rejected.md",
sha256: "d".repeat(64),
sizeBytes: 12,
});
await adapter.objectStorage.putObject({
body: new TextEncoder().encode("# Rejected"),
contentType: asset.mimeType,
key: asset.objectKey,
metadata: {},
});
const compilationJobs = createDocumentCompilationJobStateMachine({
generateId: () => "document-compilation-job-rejected-1",
jobs: adapter.jobs,
repository: createInMemoryDocumentCompilationJobRepository({ maxJobs: 4 }),
});
const compilationJob = await compilationJobs.start({
documentAssetId: asset.id,
knowledgeSpaceId: asset.knowledgeSpaceId,
tenantId: "tenant-1",
version: asset.version,
});
const projectionLifecycle: string[] = [];
const failedProjectionInputs: unknown[] = [];
const worker = createDocumentCompilationWorker({
assets,
jobs: compilationJobs,
multimodalManifests: createInMemoryDocumentMultimodalManifestRepository({
maxManifests: 4,
}),
objectStorage: adapter.objectStorage,
parser: parser(),
reindexer: {
failProjections: async (input) => {
projectionLifecycle.push("fail");
failedProjectionInputs.push(input);
return input.projectionIds.length;
},
publishProjections: async () => {
projectionLifecycle.push("publish");
return 1;
},
reindex: async (input) => {
projectionLifecycle.push(`reindex:${input.projectionStatus}`);
return {
artifact: input.parseArtifact,
nodesCreated: 1,
projectionIds: ["projection-rejected-1"],
projectionsCreated: 1,
status: "rebuilt",
};
},
},
smokeEvaluation: {
evaluate: async () => {
projectionLifecycle.push("smoke");
return {
decision: "failed",
evaluation: {
items: [],
metrics: {
citationHitRate: 0,
noAnswerRate: 1,
recallAtK: 0,
totalQuestions: 1,
},
},
rejectedReason: "candidate recall below threshold",
};
},
},
});
await expect(
worker.process({
documentAssetId: asset.id,
documentCompilationJobId: compilationJob.id,
knowledgeSpaceId: asset.knowledgeSpaceId,
tenantId: "tenant-1",
version: asset.version,
}),
).rejects.toThrow(
"Document compilation smoke evaluation failed: candidate recall below threshold",
);
expect(projectionLifecycle).toEqual(["reindex:building", "smoke", "fail"]);
expect(failedProjectionInputs).toEqual([
{
knowledgeSpaceId: asset.knowledgeSpaceId,
projectionIds: ["projection-rejected-1"],
},
]);
await expect(
assets.get({ id: asset.id, knowledgeSpaceId: asset.knowledgeSpaceId }),
).resolves.toMatchObject({ parserStatus: "failed" });
await expect(compilationJobs.get(compilationJob.id)).resolves.toMatchObject({
stage: "failed",
});
});
it("fails every staged projection when publication only updates part of the candidate", async () => {
const adapter = createTestPlatformAdapter();
const assets = createInMemoryDocumentAssetRepository({
maxAssets: 4,
now: () => "2026-05-27T10:00:00.000Z",
});
const asset = await assets.create({
filename: "Partial.md",
id: "018f0d60-7a49-7cc2-9c1b-5b36f18f6d01",
knowledgeSpaceId: "018f0d60-7a49-7cc2-9c1b-5b36f18f2c42",
mimeType: "text/markdown",
objectKey: "tenant-1/spaces/space/documents/asset/Partial.md",
sha256: "e".repeat(64),
sizeBytes: 12,
});
await adapter.objectStorage.putObject({
body: new TextEncoder().encode("# Partial"),
contentType: asset.mimeType,
key: asset.objectKey,
metadata: {},
});
const compilationJobs = createDocumentCompilationJobStateMachine({
generateId: () => "document-compilation-job-partial-1",
jobs: adapter.jobs,
repository: createInMemoryDocumentCompilationJobRepository({ maxJobs: 4 }),
});
const compilationJob = await compilationJobs.start({
documentAssetId: asset.id,
knowledgeSpaceId: asset.knowledgeSpaceId,
tenantId: "tenant-1",
version: asset.version,
});
const projectionLifecycle: string[] = [];
const failedProjectionInputs: unknown[] = [];
const worker = createDocumentCompilationWorker({
assets,
jobs: compilationJobs,
multimodalManifests: createInMemoryDocumentMultimodalManifestRepository({
maxManifests: 4,
}),
objectStorage: adapter.objectStorage,
parser: parser(),
reindexer: {
failProjections: async (input) => {
projectionLifecycle.push("fail");
failedProjectionInputs.push(input);
return input.projectionIds.length;
},
publishProjections: async (input) => {
projectionLifecycle.push(`publish:${input.projectionIds.length}`);
return 1;
},
reindex: async (input) => {
projectionLifecycle.push(`reindex:${input.projectionStatus}`);
return {
artifact: input.parseArtifact,
nodesCreated: 1,
projectionIds: ["projection-partial-1", "projection-partial-2"],
projectionsCreated: 2,
status: "rebuilt",
};
},
},
});
await expect(
worker.process({
documentAssetId: asset.id,
documentCompilationJobId: compilationJob.id,
knowledgeSpaceId: asset.knowledgeSpaceId,
tenantId: "tenant-1",
version: asset.version,
}),
).rejects.toThrow("Document compilation published 1 of 2 staged projections");
expect(projectionLifecycle).toEqual(["reindex:building", "publish:2", "fail"]);
expect(failedProjectionInputs).toEqual([
{
knowledgeSpaceId: asset.knowledgeSpaceId,
projectionIds: ["projection-partial-1", "projection-partial-2"],
},
]);
await expect(
assets.get({ id: asset.id, knowledgeSpaceId: asset.knowledgeSpaceId }),
).resolves.toMatchObject({ parserStatus: "failed" });
});
it("rasterizes PDF multimodal candidates before async asset extraction", async () => {
const adapter = createTestPlatformAdapter();
const assets = createInMemoryDocumentAssetRepository({
maxAssets: 4,
now: () => "2026-05-27T10:00:00.000Z",
});
const asset = await assets.create({
filename: "Paper.pdf",
id: "018f0d60-7a49-7cc2-9c1b-5b36f18f6b01",
knowledgeSpaceId: "018f0d60-7a49-7cc2-9c1b-5b36f18f2c42",
mimeType: "application/pdf",
objectKey: "tenant-1/spaces/space/documents/asset/Paper.pdf",
sha256: "c".repeat(64),
sizeBytes: 12,
});
await adapter.objectStorage.putObject({
body: new TextEncoder().encode("%PDF-1.7"),
contentType: asset.mimeType,
key: asset.objectKey,
metadata: {},
});
const compilationJobs = createDocumentCompilationJobStateMachine({
generateId: () => "document-compilation-job-pdf-1",
jobs: adapter.jobs,
repository: createInMemoryDocumentCompilationJobRepository({ maxJobs: 4 }),
});
const compilationJob = await compilationJobs.start({
documentAssetId: asset.id,
knowledgeSpaceId: asset.knowledgeSpaceId,
tenantId: "tenant-1",
version: asset.version,
});
const knowledgePaths = createInMemoryKnowledgePathRepository({
maxListLimit: 10,
maxPaths: 10,
});
const worker = createDocumentCompilationWorker({
assets,
generateKnowledgePathId: sequenceIds([
"018f0d60-7a49-7cc2-9c1b-5b36f18f6b05",
"018f0d60-7a49-7cc2-9c1b-5b36f18f6b06",
"018f0d60-7a49-7cc2-9c1b-5b36f18f6b07",
"018f0d60-7a49-7cc2-9c1b-5b36f18f6b08",
"018f0d60-7a49-7cc2-9c1b-5b36f18f6b09",
]),
jobs: compilationJobs,
knowledgePaths,
multimodalManifests: createInMemoryDocumentMultimodalManifestRepository({
maxManifests: 4,
}),
objectStorage: adapter.objectStorage,
parser: pdfParser(),
pdfRasterizer: {
render: async (input) => {
expect(input).toMatchObject({
boundingBox: { height: 40, width: 30, x: 10, y: 20 },
elementId: "figure-1",
pageNumber: 2,
});
return {
body: new Uint8Array([9, 8, 7, 6]),
contentType: "image/png",
};
},
},
outlineBuilder: createDocumentOutlineBuilder({
generateId: sequenceIds([
"018f0d60-7a49-7cc2-9c1b-5b36f18f6b03",
"018f0d60-7a49-7cc2-9c1b-5b36f18f6b04",
]),
maxElements: 20,
maxNodes: 10,
maxSummaryChars: 200,
now: () => "2026-05-27T10:00:00.000Z",
}),
outlines: createInMemoryDocumentOutlineRepository({ maxOutlines: 4 }),
reindexer: {
reindex: async (input) => ({
artifact: input.parseArtifact,
nodesCreated: 1,
projectionsCreated: 1,
status: "rebuilt",
}),
},
});
await expect(
worker.process({
documentAssetId: asset.id,
documentCompilationJobId: compilationJob.id,
knowledgeSpaceId: asset.knowledgeSpaceId,
tenantId: "tenant-1",
version: asset.version,
}),
).resolves.toMatchObject({ stage: "published" });
const assetPaths = await knowledgePaths.listPhysicalDescendants({
knowledgeSpaceId: asset.knowledgeSpaceId,
limit: 10,
parentPath: "/knowledge/docs/Paper.pdf--018f0d60/assets",
viewName: "docs",
});
expect(assetPaths.items).toEqual([
expect.objectContaining({
metadata: expect.objectContaining({
contentKind: "document-multimodal-asset",
modality: "image",
objectKey: expect.stringMatching(/figure-1-[a-f0-9]{12}\.png$/u),
}),
}),
]);
await expect(
adapter.objectStorage.getObject(String(assetPaths.items[0]?.metadata.objectKey)),
).resolves.toEqual(new Uint8Array([9, 8, 7, 6]));
});
});
function createTestPlatformAdapter(): PlatformAdapter {
const adapter = createNodePlatformAdapter({ env: {} });
return {
...adapter,
objectStorage: createMemoryObjectStorageAdapter({
kind: "memory",
maxObjectBytes: 64 * 1024 * 1024,
}),
};
}
function parser(): ParserAdapter {
return {
kind: "native-markdown",
parse: async (input) =>
ParseArtifactSchema.parse({
artifactHash: "b".repeat(64),
contentType: "mixed",
createdAt: "2026-05-27T10:00:00.000Z",
documentAssetId: input.documentAssetId,
elements: [
{
id: "element-1",
metadata: {},
sectionPath: ["Worker"],
text: "Worker",
type: "heading",
},
{
id: "figure-1",
metadata: {
assetRef: {
contentType: "image/png",
uri: "data:image/png;base64,AQIDBA==",
},
caption: "Worker diagram",
},
sectionPath: ["Worker"],
text: "Worker diagram",
type: "image",
},
],
id: "018f0d60-7a49-7cc2-9c1b-5b36f18f6a02",
metadata: {},
parser: "native-markdown",
version: input.version,
}),
};
}
function pdfParser(): ParserAdapter {
return {
kind: "unstructured",
parse: async (input) =>
ParseArtifactSchema.parse({
artifactHash: "d".repeat(64),
contentType: "mixed",
createdAt: "2026-05-27T10:00:00.000Z",
documentAssetId: input.documentAssetId,
elements: [
{
id: "element-1",
metadata: {},
pageNumber: 1,
sectionPath: ["Paper"],
text: "Paper",
type: "heading",
},
{
id: "figure-1",
metadata: {
boundingBox: { height: 40, width: 30, x: 10, y: 20 },
caption: "PDF figure",
},
pageNumber: 2,
sectionPath: ["Paper"],
text: "PDF figure",
type: "image",
},
],
id: "018f0d60-7a49-7cc2-9c1b-5b36f18f6b02",
metadata: {},
parser: "unstructured",
version: input.version,
}),
};
}
function sequenceIds(ids: readonly string[]): () => string {
let index = 0;
return () => {
const id = ids[index];
if (!id) {
throw new Error("No test id left");
}
index += 1;
return id;
};
}