Sub-skills
Testing in depth
Run `cubek skill`, then `cubek skill testing`, read all of both and follow them exactly (no CLI: download https://cubek.dev/SKILL.md and https://cubek.dev/skills/testing.md verbatim with curl; don't summarize them). Run `cubek check` after every edit until it is clean.
What to test and how: pure logic with cubek test (bun), routes and data
with cubek dev + curl, and cubek check after every edit.
Unit tests (cubek test)
*.test.ts files beside the code run under bun and are never deployed.
validate, crypto, encoding, csv, fmt, markdown, time work in
tests; I/O primitives (db, kv, storage, fetch, task)
throw (push.send records: push.md). So keep decisions in pure functions
and let routes do the I/O.
// src/lib/pricing.ts
import { validate, type Infer } from "cubek"
export const cartInput = validate.object({
items: validate.array(validate.object({ sku: validate.string({ min: 1 }), qty: validate.number({ int: true, min: 1, max: 99 }), cents: validate.number({ int: true, min: 0 }) }), { min: 1 }),
coupon: validate.string().optional(),
})
export type Cart = Infer<typeof cartInput>
export function total(cart: Cart): { subtotal: number; discount: number; total: number } {
let subtotal = 0
for (const it of cart.items) subtotal += it.qty * it.cents
const discount = cart.coupon === "TEN" ? Math.round(subtotal / 10) : 0
return { subtotal, discount, total: subtotal - discount }
}
export function slugify(s: string): string {
return s.trim().toLowerCase().replace(/[^a-z0-9]+/g, "-").replace(/^-|-$/g, "")
}// src/lib/pricing.test.ts
import { describe, expect, test } from "bun:test"
import { cartInput, slugify, total } from "./pricing"
describe("total", () => {
test("applies the coupon", () => {
const cart = cartInput.parse({ items: [{ sku: "a", qty: 2, cents: 500 }], coupon: "TEN" })
expect(total(cart)).toEqual({ subtotal: 1000, discount: 100, total: 900 })
})
test("rejects an empty cart", () => {
expect(cartInput.safeParse({ items: [] }).success).toBe(false)
})
})
test("slugify", () => {
expect(slugify(" Hello, World! ")).toBe("hello-world")
})cubek test # every *.test.ts
cubek test -- pricing # bun args after --: a file filterTime-based logic: setSystemTime(new Date("2026-09-27T05:59:59Z")) from
"bun:test" fixes Date and time.* in a test (setSystemTime() restores
it). cubek dev --now 2026-09-27T05:59:59Z starts the dev server's clock
there (it keeps running: Date, time.*, db "now", kv TTLs, cron).
To sanity-run lib code with the real compiler (in-memory db, kv...),
give a file an export function main() and run cubek run src/lib/try.ts:
it prints main()'s result as JSON; imports resolve as in cubek check.
Routes, data and tasks (cubek dev + curl)
The dev server is the production runtime with a local SQLite: exercise the real routes.
cubek dev --json & # prints {"ok":true,"url":...} once it listens
curl -s localhost:8787/api/items
curl -s -X POST localhost:8787/api/items -H 'content-type: application/json' -d '{"name":"pen","qty":3}'
curl -s -c jar -b jar -X POST localhost:8787/auth/signin -d 'email=a@b.co&password=secret123' # a form, keeps the cookie
curl -s -b jar localhost:8787/admin/users
cubek sql --dev "SELECT COUNT(*) AS n FROM items" # the effect in the database
cubek task run cleanup # a task or cron now
cubek dev --stop # this project's server only- Check status codes too:
curl -s -o /dev/null -w '%{http_code}\n' .... - The dev output shows each request, logs, errors and
slow queries with the missing index.
GET /_cubek/stats: per-route latency. - Dev secrets in
.cubek/secrets.json(cubek secrets --dev set NAME VALUE). - Reset the dev data: stop the server, delete
.cubek/dev.sqlite*(or seed withcubek sql --dev --writeusing fixed ids andINSERT OR IGNORE).
Live queries and realtime (curl)
A live query is an SSE stream: /_cubek/live?q=<file>%23<export>.<args>.0,
<args> the base64url of the arguments' JSON (keys sorted, no spaces; none:
null = bnVsbA). The first event: q has the result; a write sends the
next; event: e is a thrown error. Without -b jar it runs signed out.
A=$(printf '{"q":"pen"}' | base64 | tr '+/' '-_' | tr -d '=')
curl -sN -b jar "localhost:8787/_cubek/live?q=src/queries.ts%23itemsQuery.$A.0" & # data: {"k":...,"v":[...]}
curl -s -b jar -X POST localhost:8787/api/items -d 'name=pen&qty=1' # the stream prints the new result
curl -sN "localhost:8787/_cubek/realtime?channel=room:1&token=$T" # $T from realtime.token; none: 403In a browser (end to end)
Pages are server-rendered HTML: curl covers most routes. For islands, mount
points and live updates, drive a real browser against cubek dev (or a
preview URL) with Playwright, a dev dependency outside src/ (never
compiled or deployed):
bun add -d playwright && bunx playwright install chromium
// e2e/smoke.ts: `bun e2e/smoke.ts` while `cubek dev` runs
import { chromium } from "playwright"
const base = process.env.BASE ?? "http://127.0.0.1:8787"
const browser = await chromium.launch()
const page = await browser.newPage()
await page.goto(`${base}/`)
await page.fill("input[name=title]", "from e2e")
await page.click("button[type=submit]")
await page.getByText("from e2e").waitFor() // rendered, or pushed by a live query
await browser.close()Name it *.ts, not *.test.ts (cubek test runs those under bun:test).
On a private preview, page.goto the link of cubek preview url <N> first
(it leaves the access cookie in that browser).
After a deploy
cubek deploy # a preview: https://<name>--r<N>.cubek.app with a copy of production's data
curl -s -H "Authorization: Bearer $(cubek tokens print)" https://<name>--r<N>.cubek.app/healthz # previews are private
cubek logs --level warn # 500s carry their error here
cubek promote <N> # that preview becomes production, unchangedA GET /healthz route answering ok makes smoke checks one line.