118 lines
5.6 KiB
TypeScript
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()
|
|
}
|
|
})
|
|
})
|