Sub-skills

Testing in depth

For your agentCopy this and give it to the AI you're building with.
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 work in tests; I/O primitives (db, kv, storage, fetch, mail, task) throw. 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 filter

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.
  • Mail lands in .cubek/mail/*.eml; 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 with cubek sql --dev --write using fixed ids and INSERT OR IGNORE).

After a deploy

cubek deploy                   # a preview: https://<name>--r<N>.cubek.app with a copy of production's data
curl -s https://<name>--r<N>.cubek.app/healthz
cubek logs --level warn        # 500s carry their error here
cubek deploy --prod

A GET /healthz route answering ok makes smoke checks one line.