ConusAI vendor v0.1.0-dev.20261027+9b1131fb
ci / gate (push) Waiting to run

This commit is contained in:
ConusAI vendor v0.1.0-dev.20261027+9b1131fb
2026-10-27 18:05:17 +00:00
commit 3102273ec7
538 changed files with 80203 additions and 0 deletions
+415
View File
@@ -0,0 +1,415 @@
// SPDX-License-Identifier: LicenseRef-ConusAI-Template-1.0
// The document-review flow end to end on the fake platform (plan item AT2.3): an uploaded file is
// read by an OCR job, becomes proposals, a person approves, edits or rejects each line, and only
// approved lines are exported. Every step runs through the kernel's caller, the files module's
// processor point, the proposals module's writer and the outbox relay's behaviour, so what is proven
// is the wiring and not a unit.
import { readFileSync } from "node:fs"
import { type LineView, lines as linesTable, REVIEW_PROCEDURES, type ReadDocument } from "@app/modules/documents"
import { AppError } from "@conusai-kit/core"
import { uploadViaProcedures } from "@conusai-kit/testing"
import { afterEach, describe, expect, it } from "vitest"
import { sheetRows } from "../modules/documents/__tests__/support/unzip.js"
import {
bytesOf,
closeApps,
type DocumentsApp,
makeDocumentsApp,
type SeededProject,
uploadAndProcess,
} from "./support/documents-app.js"
const document = JSON.parse(
readFileSync(new URL("../modules/documents/__tests__/fixtures/ocr-document.json", import.meta.url), "utf8"),
) as ReadDocument & Record<string, unknown>
afterEach(closeApps)
interface Detail {
document: {
status: string
failureCode: string | null
pending: number
approved: number
pageCount: number | null
skippedPages: unknown[]
}
lines: LineView[]
}
async function fresh(): Promise<{ app: DocumentsApp; project: SeededProject }> {
const app = await makeDocumentsApp({ document })
return { app, project: await app.seedProject("acme", "owner@acme.test") }
}
const detail = async (project: SeededProject, fileId: string): Promise<Detail> =>
(await project.call.callByName("documents.document.get", { ...project.slugs, fileId })) as Detail
const decide = (project: SeededProject, procedure: string, input: Record<string, unknown>) =>
project.call.callByName(procedure, { ...project.slugs, ...input })
const exportRows = async (project: SeededProject, fileId: string) => {
const made = (await decide(project, "documents.export.create", { fileId })) as {
body: string
lineCount: number
filename: string
}
return { ...made, rows: sheetRows(Buffer.from(made.body, "base64")) }
}
const refusal = async (promise: Promise<unknown>): Promise<AppError> => {
try {
await promise
} catch (error) {
if (error instanceof AppError) return error
throw error
}
throw new Error("expected a refusal")
}
describe("uploading a document", () => {
it("answers before OCR runs: the request enqueues, and the worker reads", async () => {
const { app, project } = await fresh()
await uploadViaProcedures(project.call, project.slugs, {
filename: "report.pdf",
contentType: "application/pdf",
bytes: bytesOf("%PDF-1.4 fixture"),
})
// The upload has returned and the reader has not been asked anything.
expect(app.ocr.submitted).toHaveLength(0)
expect(app.queue.of("documents.ocr")).toHaveLength(0)
// The stored file's event is what enqueues the job, once.
await app.deliver()
expect(app.queue.of("documents.ocr")).toHaveLength(1)
expect(app.ocr.submitted).toHaveLength(0)
await app.runJobs()
expect(app.ocr.submitted).toHaveLength(1)
})
it("is not read by this module when it is not a document type it reads", async () => {
const { app, project } = await fresh()
await uploadViaProcedures(project.call, project.slugs, {
filename: "notes.csv",
contentType: "text/csv",
bytes: bytesOf("a,b"),
})
await app.deliver()
expect(app.queue.of("documents.ocr")).toHaveLength(0)
})
it("lists an upload nobody has read yet as queued", async () => {
const { app, project } = await fresh()
const file = await uploadViaProcedures(project.call, project.slugs, {
filename: "report.pdf",
contentType: "application/pdf",
bytes: bytesOf("%PDF-1.4"),
})
const { documents } = (await decide(project, "documents.document.list", {})) as {
documents: Array<{ fileId: string; status: string }>
}
expect(documents).toEqual([expect.objectContaining({ fileId: file.fileId, status: "queued" })])
expect(app.ocr.submitted).toHaveLength(0)
})
})
describe("the extraction", () => {
it("turns the fixture document into one pending line per labelled line, each with its page and verbatim quote", async () => {
const { app, project } = await fresh()
const file = await uploadAndProcess(app, project)
const { document: view, lines } = await detail(project, file.fileId)
// the fixture's third page is a drawing: left out, and said so on the document
expect(view).toMatchObject({
status: "extracted",
pending: 4,
approved: 0,
pageCount: 3,
skippedPages: [{ page: 3, pageClass: "drawing" }],
})
expect(lines.map((line) => [line.page, line.label, line.value, line.status])).toEqual([
[1, "Reference", "QR-2026-04", "pending"],
[1, "Issued on", "2026-04-01", "pending"],
[2, "Total amount", "1 250,00 EUR", "pending"],
[2, "Signed by", "A. Example", "pending"],
])
for (const line of lines) {
expect(document.pages[line.page - 1]?.text).toContain(line.quote)
expect(line.box).not.toBeNull()
// The line says which engine and extractor produced it: the Art. 50 disclosure's subject.
expect(line.model).toContain("fixture-engine")
expect(line.promptVersion).toBe("extract-lines.v2")
}
})
it("submits the document once, with a key that names the file and its content", async () => {
const { app, project } = await fresh()
const file = await uploadAndProcess(app, project)
expect(app.ocr.submitted).toHaveLength(1)
expect(app.ocr.submitted[0]?.idempotencyKey).toMatch(
new RegExp(`^documents\\.ocr\\.${file.fileId}\\.[0-9a-f]{64}$`),
)
})
it("writes nothing the reviewer has not approved", async () => {
const { app, project } = await fresh()
const file = await uploadAndProcess(app, project)
expect(
await app.data
.forScope({ level: "project", actor: project.owner, ...scopeIds(project) })
.of(linesTable)
.count(),
).toBe(0)
// and so an export has nothing to say yet
const error = await refusal(decide(project, "documents.export.create", { fileId: file.fileId }))
expect(error.status).toBe(409)
})
})
describe("retrying the job", () => {
it("a job run twice adds no proposal and submits no second document", async () => {
const { app, project } = await fresh()
const file = await uploadAndProcess(app, project)
const job = app.queue.jobs.find((entry) => entry.job === "documents.ocr")
expect(job).toBeDefined()
// The queue delivers the same job again (a lapsed claim): run its handler once more.
const again = app.queue.jobs.length
await app.queue.enqueue(job?.scope as never, "documents.ocr", job?.payload as never)
expect(app.queue.jobs).toHaveLength(again + 1)
await app.runJobs()
expect(app.ocr.submitted).toHaveLength(1)
expect((await detail(project, file.fileId)).lines).toHaveLength(4)
})
it("a transient failure while waiting resumes the stored OCR job instead of submitting another", async () => {
const { app, project } = await fresh()
app.ocr.failWaitOnce(new Error("the platform is unreachable"))
const file = await uploadViaProcedures(project.call, project.slugs, {
filename: "report.pdf",
contentType: "application/pdf",
bytes: bytesOf("%PDF-1.4"),
})
await app.deliver()
await expect(app.runJobs()).rejects.toThrow("unreachable")
expect((await detail(project, file.fileId)).document.status).toBe("processing")
// pg-boss retries a job that throws: the same payload, the same scope.
const job = app.queue.jobs.find((entry) => entry.job === "documents.ocr")
await app.queue.enqueue(job?.scope as never, "documents.ocr", job?.payload as never, { key: "retry" })
await app.runJobs()
expect(app.ocr.submitted).toHaveLength(1)
expect(app.ocr.waited).toHaveLength(2)
expect((await detail(project, file.fileId)).document).toMatchObject({ status: "extracted", pending: 4 })
})
it("marks the document failed, with a code and no proposals, when the platform says the job failed", async () => {
const { app, project } = await fresh()
app.platform.ocr.failJobs(true)
const file = await uploadAndProcess(app, project)
const { document: view, lines } = await detail(project, file.fileId)
expect(view).toMatchObject({ status: "failed", failureCode: "ocr_failed" })
expect(lines).toEqual([])
})
it("marks the document failed when the stored bytes are not the ones that were uploaded", async () => {
const { app, project } = await fresh()
const file = await uploadViaProcedures(project.call, project.slugs, {
filename: "report.pdf",
contentType: "application/pdf",
bytes: bytesOf("%PDF-1.4 original"),
})
for (const [key, object] of app.store.objects)
app.store.objects.set(key, { ...object, bytes: new TextEncoder().encode("tampered") })
await app.deliver()
await app.runJobs()
expect((await detail(project, file.fileId)).document).toMatchObject({
status: "failed",
failureCode: "file_changed",
})
expect(app.ocr.submitted).toHaveLength(0)
})
})
describe("the review", () => {
it("approving a line writes the record and one audit event; the other lines stay pending", async () => {
const { app, project } = await fresh()
const file = await uploadAndProcess(app, project)
const [first] = (await detail(project, file.fileId)).lines
await decide(project, REVIEW_PROCEDURES.approve, {
proposalId: first?.lineId,
expectedVersion: first?.expectedVersion,
})
await app.deliver()
const after = await detail(project, file.fileId)
expect(after.document).toMatchObject({ pending: 3, approved: 1 })
expect(after.lines.find((line) => line.lineId === first?.lineId)?.status).toBe("approved")
const approvals = app.audit.events.filter((event) => event.action === "proposals.proposal_approved")
expect(approvals).toHaveLength(1)
// ids and hashes only: no label, value or quote in an audit event
expect(JSON.stringify(approvals[0])).not.toContain("QR-2026-04")
})
it("rejecting a line changes no record, and the line stays rejected with its audit event", async () => {
const { app, project } = await fresh()
const file = await uploadAndProcess(app, project)
const [first] = (await detail(project, file.fileId)).lines
await decide(project, REVIEW_PROCEDURES.reject, { proposalId: first?.lineId, reason: "not a field" })
await app.deliver()
const after = await detail(project, file.fileId)
expect(after.lines.find((line) => line.lineId === first?.lineId)?.status).toBe("rejected")
expect(after.document).toMatchObject({ pending: 3, approved: 0 })
expect(app.audit.events.filter((event) => event.action === "proposals.proposal_rejected")).toHaveLength(1)
// nothing was written to the approved table: the export has nothing to say
expect((await refusal(decide(project, "documents.export.create", { fileId: file.fileId }))).status).toBe(409)
})
it("an edit replaces the line with the reviewer's words, which are then approved like any other", async () => {
const { app, project } = await fresh()
const file = await uploadAndProcess(app, project)
const [, second] = (await detail(project, file.fileId)).lines
const edited = (await decide(project, "documents.line.edit", {
lineId: second?.lineId,
label: "Issue date",
value: "2026-04-02",
})) as LineView
// the same line, with the reviewer's words in it
expect(edited).toMatchObject({
lineId: second?.lineId,
label: "Issue date",
value: "2026-04-02",
edited: true,
status: "pending",
})
// the quote still names the words the machine read, not the reviewer's
expect(edited.quote).toBe(second?.quote)
const listed = (await detail(project, file.fileId)).lines
expect(listed).toHaveLength(4)
expect(listed.find((line) => line.lineId === second?.lineId)).toMatchObject({ label: "Issue date", edited: true })
await app.deliver()
// one audit event for the edit, with no word of it
const audited = app.audit.events.filter((event) => event.action === "proposals.proposal_edited")
expect(audited).toHaveLength(1)
expect(JSON.stringify(audited[0])).not.toContain("Issue date")
await decide(project, REVIEW_PROCEDURES.approve, {
proposalId: edited.lineId,
expectedVersion: edited.expectedVersion,
})
const { rows } = await exportRows(project, file.fileId)
expect(rows).toEqual([
["Label", "Value", "Page", "Source quote", "Edited by a reviewer"],
["Issue date", "2026-04-02", 1, "Issued on: 2026-04-01", "yes"],
])
})
it("a line that has been decided cannot be edited", async () => {
const { app, project } = await fresh()
const file = await uploadAndProcess(app, project)
const [first] = (await detail(project, file.fileId)).lines
await decide(project, REVIEW_PROCEDURES.reject, { proposalId: first?.lineId })
const error = await refusal(
decide(project, "documents.line.edit", { lineId: first?.lineId, label: "x", value: "y" }),
)
expect(error.status).toBe(409)
})
it("approves every pending line in one decision", async () => {
const { app, project } = await fresh()
const file = await uploadAndProcess(app, project)
const lines = (await detail(project, file.fileId)).lines
const ids = lines.map((line) => line.lineId)
const expectations = Object.fromEntries(
ids.map((id, index) => [id, { expectedVersion: lines[index]?.expectedVersion }]),
)
await decide(project, REVIEW_PROCEDURES.approveMany, { proposalIds: ids, expectations })
expect((await detail(project, file.fileId)).document).toMatchObject({ pending: 0, approved: 4 })
})
})
describe("the export", () => {
it("contains the approved lines and only those, as an Excel workbook", async () => {
const { app, project } = await fresh()
const file = await uploadAndProcess(app, project, "quarterly.report.pdf")
const lines = (await detail(project, file.fileId)).lines
await decide(project, REVIEW_PROCEDURES.approve, {
proposalId: lines[2]?.lineId,
expectedVersion: lines[2]?.expectedVersion,
})
await decide(project, REVIEW_PROCEDURES.approve, {
proposalId: lines[0]?.lineId,
expectedVersion: lines[0]?.expectedVersion,
})
await decide(project, REVIEW_PROCEDURES.reject, { proposalId: lines[1]?.lineId })
const made = await exportRows(project, file.fileId)
expect(made.filename).toBe("quarterly.report-approved.xlsx")
expect(made.lineCount).toBe(2)
expect(made.rows.slice(1)).toEqual([
["Reference", "QR-2026-04", 1, "Reference: QR-2026-04", "no"],
["Total amount", "1 250,00 EUR", 2, "Total amount: 1 250,00 EUR", "no"],
])
})
it("writes one audit event naming no content", async () => {
const { app, project } = await fresh()
const file = await uploadAndProcess(app, project)
const [first] = (await detail(project, file.fileId)).lines
await decide(project, REVIEW_PROCEDURES.approve, {
proposalId: first?.lineId,
expectedVersion: first?.expectedVersion,
})
await decide(project, "documents.export.create", { fileId: file.fileId })
// The transport records a procedure's declared audit event (apps/web router); here the
// contract's own declaration is the proof, since no transport runs.
expect(app.registry.procedures.get("documents.export.create")?.def.audit).toBe("documents.export_created")
expect(app.registry.procedures.get("documents.line.edit")?.def.audit).toBe("documents.line_edited")
})
})
describe("who may do what", () => {
it("lets a viewer read a document and refuses them (404, as a stranger) an edit, an approval and an export", async () => {
const { app, project } = await fresh()
const file = await uploadAndProcess(app, project)
const [first] = (await detail(project, file.fileId)).lines
const viewer = await app.addMember(project, "viewer@acme.test", "viewer")
const read = (await viewer.call.callByName("documents.document.get", {
...project.slugs,
fileId: file.fileId,
})) as Detail
expect(read.lines).toHaveLength(4)
for (const [name, input] of [
["documents.line.edit", { lineId: first?.lineId, label: "x", value: "y" }],
[REVIEW_PROCEDURES.approve, { proposalId: first?.lineId, expectedVersion: first?.expectedVersion }],
["documents.export.create", { fileId: file.fileId }],
] as const) {
const error = await refusal(viewer.call.callByName(name, { ...project.slugs, ...input }))
// The kernel answers a member who lacks the permission exactly as it answers a stranger
// (spec §3.3): 404, so a refusal does not say which procedures exist for whom.
expect(error.status, name).toBe(404)
}
expect((await detail(project, file.fileId)).document.approved).toBe(0)
})
it("lets a member review: the permission of the line's writer is the member role's", async () => {
const { app, project } = await fresh()
const file = await uploadAndProcess(app, project)
const [first] = (await detail(project, file.fileId)).lines
const member = await app.addMember(project, "member@acme.test", "member")
await member.call.callByName(REVIEW_PROCEDURES.approve, {
...project.slugs,
proposalId: first?.lineId,
expectedVersion: first?.expectedVersion,
})
expect((await detail(project, file.fileId)).document.approved).toBe(1)
})
})
const scopeIds = (project: SeededProject) => ({
orgId: project.orgId,
workspaceId: project.workspaceId,
projectId: project.projectId,
})