Files
template-app/modules/documents/__tests__/mongo-forward.test.ts
T
2026-11-14 09:56:45 +00:00

118 lines
5.6 KiB
TypeScript

// SPDX-License-Identifier: LicenseRef-ConusAI-Template-1.0
// Disposable real MongoDB evidence; ACCOUNT_TEST_MONGODB_URL must name a replica set.
// Each run owns a fresh database and removes only that database afterwards.
import { randomUUID } from "node:crypto"
import { readFileSync } from "node:fs"
import { dialectModuleMigrationFiles } from "@app/config"
import { kernelModules } from "@app/modules"
import { buildRegistry, newId } from "@conusai-kit/core"
import {
connectMongoDataSource,
type MongoCommand,
mongoCommandsMigration,
runMongoMigrations,
type StoredDoc,
} from "@conusai-kit/data-mongo"
import { makeTenant } from "@conusai-kit/testing"
import { MongoClient } from "mongodb"
import { describe, expect, it } from "vitest"
import { DOCUMENTS_ENTITIES, documents, documentsModule, FAILURE_CODES } from "../index.js"
const url = process.env.ACCOUNT_TEST_MONGODB_URL
const migration = (name: string) =>
mongoCommandsMigration(
`documents/${name}`,
JSON.parse(
readFileSync(new URL(`../adapters/mongo/migrations/${name}.json`, import.meta.url), "utf8"),
) as MongoCommand[],
)
describe.skipIf(!url)("Documents forward MongoDB validator evolution", () => {
it("preserves existing rows and indexes, persists exhaustion, and retains enum and tenant refusals", async () => {
const client = await new MongoClient(url ?? "").connect()
const database = `documents_forward_${randomUUID().replaceAll("-", "")}`
const db = client.db(database)
const data = await connectMongoDataSource(client, { database, entities: DOCUMENTS_ENTITIES })
try {
const discovered = await dialectModuleMigrationFiles(
buildRegistry([...kernelModules, documentsModule]),
"mongodb",
)
expect(discovered.filter(({ name }) => name.startsWith("documents/")).map(({ name }) => name)).toEqual([
"documents/0001_documents",
"documents/0002_skipped_pages",
"documents/0003_nullable_failure_code",
"documents/0004_processing_exhausted",
])
const history = ["0001_documents", "0002_skipped_pages", "0003_nullable_failure_code"].map(migration)
await runMongoMigrations(db, history)
const tenant = makeTenant()
const repo = data.forScope(tenant.project).of(documents)
const input = {
fileId: newId(),
filename: "synthetic.pdf",
status: "processing" as const,
ocrJobId: null,
pageCount: null,
proposedCount: null,
failureCode: null,
skippedPages: null,
finishedAt: null,
}
const row = await repo.insert(input)
const rows = db.collection<StoredDoc>("documents_document")
const before = await rows.findOne({ _id: row.id })
const indexes = await rows.indexes()
const [collectionBefore] = await db.listCollections({ name: "documents_document" }).toArray()
const terminal = { status: "failed" as const, failureCode: "processing_exhausted" as const }
// The current typed model accepts this transition, but the applied historical validator refuses it.
await expect(repo.update(row.id, terminal, row.version)).rejects.toThrow(/[Dd]ocument failed validation/)
expect(await rows.findOne({ _id: row.id })).toEqual(before)
const forward = migration("0004_processing_exhausted")
expect(await runMongoMigrations(db, [...history, forward])).toEqual({
applied: [forward.name],
skipped: history.map(({ name }) => name),
})
expect(await rows.findOne({ _id: row.id })).toEqual(before)
expect(await rows.indexes()).toEqual(indexes)
const [collectionAfter] = await db.listCollections({ name: "documents_document" }).toArray()
if (!collectionBefore || !("info" in collectionBefore) || !collectionAfter || !("info" in collectionAfter))
throw new Error("missing collection metadata")
expect(collectionBefore.info?.uuid).toBeDefined()
expect(collectionAfter.info?.uuid).toEqual(collectionBefore.info?.uuid)
const failed = await repo.update(row.id, terminal, row.version)
expect(await repo.get(row.id)).toEqual(failed)
expect(failed).toMatchObject({ ...terminal, filename: input.filename, fileId: input.fileId })
expect(await rows.countDocuments()).toBe(1)
// All current codes and null remain usable; no blanket weakening of the enum.
for (const failureCode of [...FAILURE_CODES, null]) {
await repo.insert({ ...input, fileId: newId(), status: "failed", failureCode })
}
await expect(rows.updateOne({ _id: row.id }, { $set: { failure_code: "unknown_failure" } })).rejects.toThrow(
/[Dd]ocument failed validation/,
)
// Zod still rejects an unknown code before an adapter write too.
await expect(repo.update(row.id, { failureCode: "unknown_failure" as never }, failed.version)).rejects.toThrow()
for (const scope of [
makeTenant().project,
{ ...tenant.project, workspaceId: tenant.siblingWorkspace.workspaceId },
tenant.siblingProject,
]) {
const other = data.forScope(scope).of(documents)
expect(await other.get(row.id)).toBeNull()
await expect(other.update(row.id, { status: "processing" }, failed.version)).rejects.toThrow()
}
expect(await repo.get(row.id)).toEqual(failed)
const stored = await rows.find().sort({ _id: 1 }).toArray()
expect(await runMongoMigrations(db, [...history, forward])).toEqual({
applied: [],
skipped: [...history, forward].map(({ name }) => name),
})
expect(await rows.find().sort({ _id: 1 }).toArray()).toEqual(stored)
} finally {
await db.dropDatabase()
await data.close()
}
})
})