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
+110
View File
@@ -0,0 +1,110 @@
// Every string a page of the module shows is a message in English and in Lithuanian (spec §10.4), the
// Lithuanian plural forms are real ones, and no component of the module carries a string of its own.
import { readdirSync, readFileSync } from "node:fs"
import { join } from "node:path"
import { buildRegistry } from "@conusai-kit/core"
import { createI18n, lintUiStrings } from "@conusai-kit/i18n"
import { describe, expect, it } from "vitest"
import { appModules } from "../../index.js"
import { feedbackLabels } from "../guides/feedback-labels.js" // kit:guides
import {
attachmentLabels,
createFormLabels,
deleteLabels,
editFormLabels,
listLabels,
sizeLabel,
} from "../ui/labels.js"
// The messages as the app has them: from the manifest, flattened and merged by the kit.
const catalogues = buildRegistry([...appModules]).catalogues
const en = catalogues.en?.notes ?? {}
const lt = catalogues.lt?.notes ?? {}
/** The keys a builder asks for, recorded by running it with a lookup that notes each one. */
function keysUsed(): string[] {
const asked: string[] = []
const t = (key: string) => {
asked.push(key)
return key
}
createFormLabels(t, "failed")
editFormLabels(t, "failed")
listLabels(t)
attachmentLabels(t, "failed", 2)
deleteLabels(t, "failed")
sizeLabel(t, 2048)
feedbackLabels(t) // kit:guides
return asked
}
describe("the notes catalogues", () => {
it("hold the same keys in English and in Lithuanian", () => {
expect(Object.keys(lt).sort()).toEqual(Object.keys(en).sort())
})
it("hold no empty message", () => {
for (const [key, text] of [...Object.entries(en), ...Object.entries(lt)]) expect(text.trim(), key).not.toBe("")
})
it("hold every message the components' labels ask for, in both languages", () => {
const asked = keysUsed()
expect(asked.length).toBeGreaterThan(30)
for (const key of asked) {
expect(en[key as keyof typeof en], `en is missing notes.${key}`).toBeTruthy()
expect(lt[key as keyof typeof lt], `lt is missing notes.${key}`).toBeTruthy()
}
})
it("hold no message nothing asks for, so a renamed label does not leave a dead string behind", () => {
// The pages and the navigation ask for these directly, not through a label builder.
const direct = new Set(["title", "intro", "detail.back", "detail.changed"])
const asked = new Set([...keysUsed(), ...direct])
expect(Object.keys(en).filter((key) => !asked.has(key))).toEqual([])
})
it("answer the navigation label the manifest names", () => {
const nav = appModules.find((module) => module.id === "notes")?.ui?.nav ?? []
expect(nav.map((item) => item.label)).toEqual(["notes.title"])
expect(en.title).toBeTruthy()
expect(lt.title).toBeTruthy()
})
})
describe("the Lithuanian plural forms", () => {
const count = (locale: "en" | "lt", n: number) =>
createI18n(locale, { en: { notes: en }, lt: { notes: lt } }).message("notes.attachments.count", { count: n })
it("select one, few and other the way Lithuanian counts", () => {
expect(count("lt", 1)).toBe("1 priedas")
expect(count("lt", 2)).toBe("2 priedai")
expect(count("lt", 9)).toBe("9 priedai")
expect(count("lt", 10)).toBe("10 priedų")
expect(count("lt", 21)).toBe("21 priedas")
})
it("keep English to its two forms", () => {
expect(count("en", 1)).toBe("1 attachment")
expect(count("en", 2)).toBe("2 attachments")
expect(lt["attachments.count"]).toContain("few {")
expect(en["attachments.count"]).not.toContain("few {")
})
})
describe("the components", () => {
const dir = new URL("../ui/", import.meta.url).pathname
const sources = readdirSync(dir)
.filter((name) => name.endsWith(".tsx"))
.map((name) => ({ name, text: readFileSync(join(dir, name), "utf8") }))
it("carry no hard-coded user-facing string", () => {
expect(sources.length).toBeGreaterThanOrEqual(4)
for (const { name, text } of sources) expect(lintUiStrings(text, name), name).toEqual([])
})
it("carry no colour literal: the brand tokens are the only source of colour", () => {
for (const { name, text } of sources)
expect(text, name).not.toMatch(/#[0-9a-fA-F]{3,8}\b|\brgb\(|\bhsl\(|\boklch\(/)
})
})
+58
View File
@@ -0,0 +1,58 @@
// What the generated tenant-isolation suite (test/isolation.test.ts) needs of this module, from this
// folder: the rows it seeds in each tenant, and one fixture per tenant-scoped procedure. The suite
// finds this file by its place, so a module added with `pnpm new:module` has its fixtures read
// without a line outside its folder; a procedure with no fixture fails the suite by name.
//
// A fixture is built from the *other* tenant's ids: the call is made as a member of one
// organization with another's slugs and ids, and has to be refused -- for being another tenant's, not
// for naming nothing, which is why `seed` leaves a note, a file and a link in every tenant.
import type { TransportCaller } from "@conusai-kit/core"
import { type IsolationFixture, type IsolationTenant, uploadViaProcedures } from "@conusai-kit/testing"
/** What a module's isolation file exports: the shape the suite reads, so it needs no import of the suite. */
export interface ModuleIsolation {
seed(
call: TransportCaller,
inProject: { org: string; workspace: string; project: string },
): Promise<Record<string, string>>
fixtures: Record<string, IsolationFixture>
}
const NOWHERE = "00000000-0000-0000-0000-000000000000"
const inProjectOf = (t: IsolationTenant) => ({ org: t.orgSlug, workspace: t.workspaceSlug, project: t.projectSlug })
const noteOf = (t: IsolationTenant) => t.extra.noteId ?? NOWHERE
export const isolation: ModuleIsolation = {
async seed(call, inProject) {
const note = (await call.callByName("notes.note.create", { ...inProject, title: "Seed note", body: "Seed" })) as {
noteId: string
}
const file = await uploadViaProcedures(call, inProject, {
filename: "notes-seed.txt",
contentType: "text/plain",
bytes: new TextEncoder().encode("seed"),
})
const link = (await call.callByName("notes.attachment.add", {
...inProject,
noteId: note.noteId,
fileId: file.fileId,
})) as { attachmentId: string }
return { noteId: note.noteId, noteFileId: file.fileId, attachmentId: link.attachmentId }
},
fixtures: {
"notes.note.list": { input: (t) => inProjectOf(t) },
"notes.note.get": { input: (t) => ({ ...inProjectOf(t), noteId: noteOf(t) }) },
"notes.note.create": { input: (t) => ({ ...inProjectOf(t), title: "Intruder", body: "x" }) },
"notes.note.update": {
input: (t) => ({ ...inProjectOf(t), noteId: noteOf(t), title: "Intruder", body: "x", expectedVersion: 1 }),
},
"notes.note.delete": { input: (t) => ({ ...inProjectOf(t), noteId: noteOf(t) }) },
"notes.attachment.add": {
input: (t) => ({ ...inProjectOf(t), noteId: noteOf(t), fileId: t.extra.noteFileId ?? NOWHERE }),
},
"notes.attachment.remove": {
input: (t) => ({ ...inProjectOf(t), noteId: noteOf(t), attachmentId: t.extra.attachmentId ?? NOWHERE }),
},
},
}
+92
View File
@@ -0,0 +1,92 @@
// The module's migration is its own file and exactly `schemaStatements()` over its two entities
// (decision AT-D20: the entity definitions are the one source of a column, so a hand-written column
// cannot drift from the schema the module validates its rows with). A change to an entity changes
// this snapshot, which is the moment a customer must see a new migration in review. The MariaDB
// folder is the same tables in the other dialect, generated by `mysqlSchemaStatements` (spec §5.3:
// the dialect differences are explicit, one numbered folder per dialect), and the MongoDB folder is
// the same collections and indexes as declarative commands, generated by `mongoSchemaCommands`.
import { readFileSync } from "node:fs"
import { schemaStatements } from "@conusai-kit/data-kysely"
import { mysqlSchemaStatements } from "@conusai-kit/data-kysely/mysql"
import { MONGO_KERNEL_COMMANDS, mongoSchemaCommands } from "@conusai-kit/data-mongo"
import { describe, expect, it } from "vitest"
import { NOTES_ENTITIES, notesModule } from "../index.js"
/** The statements for these tables alone: `schemaStatements` leads with the kernel's own two. */
function tablesOf(entities: Parameters<typeof schemaStatements>[0]): string {
const kernel = schemaStatements([]).length
return `${schemaStatements(entities)
.slice(kernel)
.map((statement) => `${statement};`)
.join("\n\n")}\n`
}
/** The same for the MariaDB folder: `mysqlSchemaStatements` leads with the kernel's own two. */
function mysqlTablesOf(entities: Parameters<typeof mysqlSchemaStatements>[0]): string {
const kernel = mysqlSchemaStatements([]).length
return `${mysqlSchemaStatements(entities)
.slice(kernel)
.map((statement) => `${statement};`)
.join("\n\n")}\n`
}
/** The same for the MongoDB folder: `mongoSchemaCommands` leads with the kernel's own collections. */
function mongoCommandsOf(entities: Parameters<typeof mongoSchemaCommands>[0]): ReturnType<typeof mongoSchemaCommands> {
return mongoSchemaCommands(entities).slice(MONGO_KERNEL_COMMANDS.length)
}
describe("the migration of the notes module", () => {
it("is its own file, exactly its two tables", async () => {
await expect(tablesOf(NOTES_ENTITIES)).toMatchFileSnapshot("../migrations/0001_notes.sql")
})
it("is declared by the manifest, so the migrate job finds it, and every table is row-level secured", () => {
expect(notesModule.migrations).toEqual({
postgres: "migrations",
mariadb: "adapters/kysely/mysql/migrations",
mongodb: "adapters/mongo/migrations",
})
const sql = readFileSync(new URL("../migrations/0001_notes.sql", import.meta.url), "utf8")
for (const table of ["notes_note", "notes_attachment"]) {
expect(sql).toContain(`create table "${table}"`)
expect(sql).toContain(`alter table "${table}" force row level security`)
}
})
it("has the same two tables as MariaDB DDL, and no policies -- MariaDB has no row-level security", async () => {
const sql = readFileSync(new URL("../adapters/kysely/mysql/migrations/0001_notes.sql", import.meta.url), "utf8")
await expect(mysqlTablesOf(NOTES_ENTITIES)).toMatchFileSnapshot(
"../adapters/kysely/mysql/migrations/0001_notes.sql",
)
for (const table of ["notes_note", "notes_attachment"]) {
expect(sql).toContain(`create table \`${table}\``)
}
expect(sql).not.toContain("row level security")
})
it("has the same two collections as MongoDB commands, each index leading with org_id (AT3.2)", () => {
const file = new URL("../adapters/mongo/migrations/0001_notes.json", import.meta.url)
// The file is data (Biome keeps its JSON formatting canonical), so the pin is structural:
// exactly the commands `mongoSchemaCommands` generates for these entities, nothing else.
const commands = JSON.parse(readFileSync(file, "utf8")) as Array<Record<string, unknown>>
expect(commands).toEqual(mongoCommandsOf(NOTES_ENTITIES))
const created = commands
.filter((command) => "createCollection" in command)
.map((command) => command.createCollection)
expect(created).toEqual(["notes_attachment", "notes_note"])
// MongoDB has no row-level security: the wall is the repository predicate plus these
// org_id-leading indexes, and the contract suite of @conusai-kit/data-mongo is its proof.
const indexes = commands.filter((command) => "createIndex" in command)
for (const index of indexes) {
const keys = Object.keys((index.createIndex as { keys: Record<string, 1> }).keys)
expect(keys[0]).toBe("org_id")
}
// The validator is the entity's zod schema as $jsonSchema, carried by the file itself.
const note = commands.find((command) => command.createCollection === "notes_note") as {
validator: { $jsonSchema: { properties: Record<string, unknown>; required: string[] } }
}
expect(Object.keys(note.validator.$jsonSchema.properties)).toContain("org_id")
expect(note.validator.$jsonSchema.required).toContain("org_id")
})
})
+168
View File
@@ -0,0 +1,168 @@
// The components' markup, rendered on the server the way a page renders it: what a screen reader and
// a keyboard user get is in the HTML, so the accessibility basics are asserted there -- a name for
// every control, a caption and scoped headers for every table, a status that says what a colour would.
import { buildRegistry } from "@conusai-kit/core"
import { createI18n } from "@conusai-kit/i18n"
import { TransportProvider } from "@conusai-kit/ui"
import { renderToStaticMarkup } from "react-dom/server"
import { describe, expect, it } from "vitest"
import { appModules } from "../../index.js"
import type { NoteView } from "../contract/index.js"
import {
Attachments,
attachmentLabels,
createFormLabels,
DeleteNote,
deleteLabels,
editFormLabels,
listLabels,
NoteForm,
NoteList,
sizeLabel,
} from "../ui/index.js"
const catalogues = buildRegistry([...appModules]).catalogues
const i18n = createI18n("en", { en: { notes: catalogues.en?.notes ?? {} } })
const t = (key: string, values?: Record<string, string | number>) => i18n.message(`notes.${key}`, values)
const slugs = { org: "acme", workspace: "ws", project: "p" }
const failed = "That did not work: {detail}"
const inProvider = (node: React.ReactNode) =>
renderToStaticMarkup(
<TransportProvider call={async () => undefined} describe={() => "x"}>
{node}
</TransportProvider>,
)
const note = (over: Partial<NoteView> = {}): NoteView => ({
noteId: "n1",
title: "Kick-off",
body: "Agenda",
version: 3,
createdBy: "u1",
createdAt: "2026-04-01T08:00:00.000Z",
updatedAt: "2026-04-02T08:00:00.000Z",
attachmentCount: 2,
...over,
})
describe("the note list", () => {
const html = renderToStaticMarkup(
<NoteList
notes={[note(), note({ noteId: "n2", title: "Retro" })]}
hrefFor={(id) => `/acme/ws/p/notes/${id}`}
labels={listLabels(t)}
formatDate={(iso) => iso.slice(0, 10)}
/>,
)
it("is a table with a caption and a scoped header for every column", () => {
expect(html).toContain("<caption>Notes</caption>")
expect(html.match(/<th scope="col">/g)).toHaveLength(3)
expect(html).toContain('<th scope="row">')
})
it("names each link by its note, so a list of links is not a list of the same word", () => {
expect(html).toContain('aria-label="Open Kick-off"')
expect(html).toContain('aria-label="Open Retro"')
expect(html).toContain('href="/acme/ws/p/notes/n1"')
})
it("says so in words when there is nothing to list", () => {
const empty = renderToStaticMarkup(
<NoteList notes={[]} hrefFor={() => ""} labels={listLabels(t)} formatDate={() => ""} />,
)
expect(empty).toBe('<p data-guide="notes.note-list">No notes yet.</p>')
})
})
describe("the note form", () => {
const html = inProvider(
<NoteForm
slugs={slugs}
target={{ mode: "create", listHref: "/acme/ws/p/notes" }}
labels={createFormLabels(t, failed)}
/>,
)
it("labels every field", () => {
for (const name of ["title", "body"]) {
const id = new RegExp(`<(?:input|textarea) [^>]*\\bid="([^"]+)"[^>]*\\bname="${name}"`).exec(html)?.[1]
expect(id, name).toBeTruthy()
expect(html).toMatch(new RegExp(`<label for="${id}">`))
}
})
it("starts empty to create, and carries the note's words to edit", () => {
expect(html).toContain("New note")
const edit = inProvider(
<NoteForm
slugs={slugs}
target={{ mode: "edit", noteId: "n1", version: 3, title: "Kick-off", body: "Agenda" }}
labels={editFormLabels(t, failed)}
/>,
)
expect(edit).toContain("Edit note")
expect(edit).toContain('value="Kick-off"')
expect(edit).toContain(">Agenda</textarea>")
})
it("announces its outcome in a status region", () => {
expect(html).toContain('<p role="status">')
})
})
describe("the attachments", () => {
const render = (enabled: boolean, rows: Parameters<typeof Attachments>[0]["rows"]) =>
inProvider(
<Attachments
slugs={slugs}
noteId="n1"
rows={rows}
enabled={enabled}
accept={["text/plain", "application/pdf"]}
labels={attachmentLabels(t, failed, rows.length)}
/>,
)
const rows = [
{ attachmentId: "a1", fileId: "f1", filename: "minutes.txt", contentType: "text/plain", size: sizeLabel(t, 2048) },
]
it("lists each file in a captioned table and names each remove button by its file", () => {
const html = render(true, rows)
expect(html).toContain("<caption>Attachments</caption>")
expect(html).toContain('<th scope="row">minutes.txt</th>')
expect(html).toContain("2 KB")
expect(html).toContain('aria-label="Remove minutes.txt"')
expect(html).toContain('href="/api/v1/files.file.download?org=acme&amp;workspace=ws&amp;project=p&amp;fileId=f1"')
expect(html).toContain('aria-label="Download minutes.txt"')
expect(html).toContain("1 attachment")
})
it("labels its file input, describes it with the hint, and asks for the types it accepts", () => {
const html = render(true, [])
const id = /<input [^>]*\bid="([^"]+)"[^>]*\bname="file"/.exec(html)?.[1]
expect(id).toBeTruthy()
expect(html).toContain(`<label for="${id}">Attach a file</label>`)
expect(html).toContain("aria-describedby=")
expect(html).toContain('accept="text/plain,application/pdf"')
expect(html).toContain("Nothing is attached.")
})
it("offers no upload when the feature flag is off, and says why", () => {
const html = render(false, [])
expect(html).not.toContain('type="file"')
expect(html).toContain("Attachments are switched off for this project.")
})
})
describe("the delete button", () => {
it("asks first: the destructive button is not on the page until the person has said yes to a question", () => {
const html = inProvider(
<DeleteNote slugs={slugs} noteId="n1" listHref="/acme/ws/p/notes" labels={deleteLabels(t, failed)} />,
)
expect(html).toContain("Delete note")
expect(html).not.toContain("Yes, delete the note")
})
})