test(examples): add failing behavioral tests before app implementation
Actual project-only Git commit. Email addresses are redacted; source bytes and recorded timestamps are preserved. This is a code record, not human acceptance.
commit 6bae5399f81de2a775e1a36272beef77dd879f00
Author: 0xLeif <redacted@invalid>
AuthorDate: Sun Nov 8 19:26:05 2026 -0700
Commit: 0xLeif <redacted@invalid>
CommitDate: Sun Nov 8 19:26:05 2026 -0700
test(examples): add failing behavioral tests before app implementation
diff --git a/apps/tiny-tasks/package.json b/apps/tiny-tasks/package.json
new file mode 100644
index 0000000..3fc31dc
--- /dev/null
+++ b/apps/tiny-tasks/package.json
@@ -0,0 +1,16 @@
+{
+ "name": "@corvid-examples/tiny-tasks",
+ "version": "0.1.0",
+ "private": true,
+ "type": "module",
+ "license": "MIT",
+ "scripts": {
+ "test": "bun test",
+ "typecheck": "tsc --project tsconfig.json",
+ "build": "bun run build.ts",
+ "start": "bun run serve.ts"
+ },
+ "devDependencies": {
+ "typescript": "5.9.3"
+ }
+}
diff --git a/apps/tiny-tasks/src/model.ts b/apps/tiny-tasks/src/model.ts
new file mode 100644
index 0000000..1777df3
--- /dev/null
+++ b/apps/tiny-tasks/src/model.ts
@@ -0,0 +1,33 @@
+export type ModelResult<Value> =
+ | { readonly ok: true; readonly value: Value }
+ | { readonly ok: false; readonly error: string };
+
+export interface FileExport {
+ readonly filename: string;
+ readonly mediaType: "application/json" | "text/plain";
+ readonly contents: string;
+}
+
+export type TaskEventType = "added" | "completed" | "reopened";
+export interface TaskEvent { readonly type: TaskEventType; readonly at: number; }
+export interface Task {
+ readonly id: string;
+ readonly text: string;
+ readonly completed: boolean;
+ readonly history: readonly TaskEvent[];
+}
+export interface TasksState { readonly version: 1; readonly tasks: readonly Task[]; }
+export const MAX_TASK_TEXT_LENGTH = 200;
+
+export function createTasksState(): TasksState { throw new Error("Not implemented"); }
+export function validateTasksState(_value: unknown): ModelResult<TasksState> { throw new Error("Not implemented"); }
+export function addTask(_state: TasksState, _text: string, _id: string, _at: number): ModelResult<TasksState> {
+ throw new Error("Not implemented");
+}
+export function completeTask(_state: TasksState, _id: string, _at: number): ModelResult<TasksState> {
+ throw new Error("Not implemented");
+}
+export function reopenTask(_state: TasksState, _id: string, _at: number): ModelResult<TasksState> {
+ throw new Error("Not implemented");
+}
+export function createTasksExport(_state: TasksState): FileExport { throw new Error("Not implemented"); }
diff --git a/apps/tiny-tasks/src/storage.ts b/apps/tiny-tasks/src/storage.ts
new file mode 100644
index 0000000..ce5d87e
--- /dev/null
+++ b/apps/tiny-tasks/src/storage.ts
@@ -0,0 +1,21 @@
+import type { TasksState } from "./model";
+
+export interface StorageAdapter {
+ getItem(key: string): string | null;
+ setItem(key: string, value: string): void;
+}
+export type LoadResult<Value> =
+ | { readonly status: "loaded"; readonly value: Value }
+ | { readonly status: "empty" }
+ | { readonly status: "warning"; readonly message: string };
+export type SaveResult =
+ | { readonly ok: true }
+ | { readonly ok: false; readonly message: string };
+
+export const TASKS_STORAGE_KEY = "corvid-examples:tiny-tasks:v1";
+export function loadTasks(_storage: StorageAdapter): LoadResult<TasksState> {
+ throw new Error("Not implemented");
+}
+export function saveTasks(_storage: StorageAdapter, _state: TasksState): SaveResult {
+ throw new Error("Not implemented");
+}
diff --git a/apps/tiny-tasks/tests/model.test.ts b/apps/tiny-tasks/tests/model.test.ts
new file mode 100644
index 0000000..6cabba0
--- /dev/null
+++ b/apps/tiny-tasks/tests/model.test.ts
@@ -0,0 +1,161 @@
+import { describe, expect, test } from "bun:test";
+import {
+ addTask, completeTask, createTasksExport, createTasksState, reopenTask, validateTasksState,
+ type ModelResult, type TasksState,
+} from "../src/model";
+
+const open: TasksState = { version: 1, tasks: [
+ { id: "a", text: "Read", completed: false, history: [{ type: "added", at: 10 }] },
+ { id: "b", text: "Write", completed: false, history: [{ type: "added", at: 11 }] },
+] };
+const done: TasksState = { version: 1, tasks: [
+ { id: "a", text: "Read", completed: true,
+ history: [{ type: "added", at: 10 }, { type: "completed", at: 20 }] },
+] };
+function value<Value>(result: ModelResult<Value>): Value {
+ if (!result.ok) throw new Error(result.error);
+ return result.value;
+}
+describe("Tiny Tasks actual model contract", () => {
+ test("creates an empty versioned state", () => {
+ expect(createTasksState()).toEqual({ version: 1, tasks: [] });
+ });
+ test("adds trimmed text with exact identity and initial history without mutating input", () => {
+ const before = structuredClone(open);
+ const result = value(addTask(open, " Ship ", "c", 12));
+ expect(result.tasks).toEqual([...open.tasks, {
+ id: "c", text: "Ship", completed: false, history: [{ type: "added", at: 12 }],
+ }]);
+ expect(open).toEqual(before);
+ expect(result).not.toBe(open);
+ });
+ test("completes a task and preserves its sibling", () => {
+ const before = structuredClone(open);
+ const result = value(completeTask(open, "a", 20));
+ expect(result.tasks[0]).toEqual({
+ id: "a", text: "Read", completed: true,
+ history: [{ type: "added", at: 10 }, { type: "completed", at: 20 }],
+ });
+ expect(result.tasks[1]).toEqual(open.tasks[1]);
+ expect(open).toEqual(before);
+ });
+ test("reopens retaining every earlier event", () => {
+ const before = structuredClone(done);
+ const result = value(reopenTask(done, "a", 30));
+ expect(result.tasks[0]).toEqual({ ...done.tasks[0], completed: false,
+ history: [...([{ type: "added", at: 10 }, { type: "completed", at: 20 }] as const), { type: "reopened", at: 30 }] });
+ expect(done).toEqual(before);
+ });
+ test("add complete reopen produces three ordered events", () => {
+ const added = value(addTask({ version: 1, tasks: [] }, "Read", "a", 10));
+ const completed = value(completeTask(added, "a", 20));
+ const reopened = value(reopenTask(completed, "a", 30));
+ expect(reopened.tasks[0]?.history).toEqual([
+ { type: "added", at: 10 }, { type: "completed", at: 20 }, { type: "reopened", at: 30 },
+ ]);
+ expect(reopened.tasks[0]?.completed).toBe(false);
+ });
+ test("accepts the exact 200-character boundary", () => {
+ expect(value(addTask(open, "x".repeat(200), "c", 20)).tasks[2]?.text).toHaveLength(200);
+ });
+ for (const text of ["", " ", "x".repeat(201)]) {
+ test("rejects invalid task input without changing the state: " + text.length, () => {
+ const before = structuredClone(open);
+ {
+ const previous = structuredClone(open);
+ const rejected = addTask(open, text, "c", 20);
+ expect(rejected.ok).toBe(false);
+ if (!rejected.ok) expect(rejected.error.length).toBeGreaterThan(0);
+ expect(open).toEqual(previous);
+ }
+ expect(open).toEqual(before);
+ });
+ }
+ for (const id of ["", "bad id", "x".repeat(65), "a"]) {
+ test("rejects invalid or duplicate ID: " + id, () => {
+ {
+ const previous = structuredClone(open);
+ const rejected = addTask(open, "Read", id, 20);
+ expect(rejected.ok).toBe(false);
+ if (!rejected.ok) expect(rejected.error.length).toBeGreaterThan(0);
+ expect(open).toEqual(previous);
+ }
+ });
+ }
+ for (const at of [-1, NaN, Infinity, 1.5, Number.MAX_SAFE_INTEGER + 1]) {
+ test("rejects invalid time: " + at, () => {
+ {
+ const previous = structuredClone(open);
+ const rejected = addTask(open, "Read", "c", at);
+ expect(rejected.ok).toBe(false);
+ if (!rejected.ok) expect(rejected.error.length).toBeGreaterThan(0);
+ expect(open).toEqual(previous);
+ }
+ });
+ }
+ test("rejects missing IDs and illegal repeated transitions", () => {
+ {
+ const previous = structuredClone(open);
+ const rejected = completeTask(open, "missing", 20);
+ expect(rejected.ok).toBe(false);
+ if (!rejected.ok) expect(rejected.error.length).toBeGreaterThan(0);
+ expect(open).toEqual(previous);
+ }
+ {
+ const previous = structuredClone(done);
+ const rejected = completeTask(done, "a", 30);
+ expect(rejected.ok).toBe(false);
+ if (!rejected.ok) expect(rejected.error.length).toBeGreaterThan(0);
+ expect(done).toEqual(previous);
+ }
+ {
+ const previous = structuredClone(open);
+ const rejected = reopenTask(open, "a", 30);
+ expect(rejected.ok).toBe(false);
+ if (!rejected.ok) expect(rejected.error.length).toBeGreaterThan(0);
+ expect(open).toEqual(previous);
+ }
+ });
+ test("rejects backwards transition time while allowing equal milliseconds", () => {
+ {
+ const previous = structuredClone(open);
+ const rejected = completeTask(open, "a", 9);
+ expect(rejected.ok).toBe(false);
+ if (!rejected.ok) expect(rejected.error.length).toBeGreaterThan(0);
+ expect(open).toEqual(previous);
+ }
+ expect(value(completeTask(open, "a", 10)).tasks[0]?.completed).toBe(true);
+ });
+ test("validates a nonempty fixture by value without aliasing external data", () => {
+ const fixture = structuredClone(done);
+ const result = value(validateTasksState(fixture));
+ fixture.tasks = [];
+ expect(result).toEqual(done);
+ });
+ const invalid: readonly unknown[] = [
+ null, [], {}, { version: 2, tasks: [] }, { ...open, extra: true },
+ { version: 1, tasks: [...open.tasks, open.tasks[0]] },
+ { version: 1, tasks: [{ ...open.tasks[0], history: [] }] },
+ { version: 1, tasks: [{ ...done.tasks[0], completed: false }] },
+ { version: 1, tasks: [{ ...open.tasks[0], text: " untrimmed " }] },
+ { version: 1, tasks: [{ ...open.tasks[0], history: [{ type: "added", at: NaN }] }] },
+ { version: 1, tasks: [{ ...open.tasks[0], history: [
+ { type: "added", at: 10 }, { type: "reopened", at: 20 },
+ ] }] },
+ { version: 1, tasks: [{ ...done.tasks[0], history: [
+ { type: "added", at: 10 }, { type: "completed", at: 9 },
+ ] }] },
+ ];
+ invalid.forEach((input, index) => test("rejects malformed persisted state " + index, () => {
+ const result = validateTasksState(input);
+ expect(result.ok).toBe(false);
+ if (!result.ok) expect(result.error.length).toBeGreaterThan(0);
+ }));
+ test("exports current tasks including complete history as versioned JSON", () => {
+ const result = createTasksExport(done);
+ expect(result.filename).toBe("tiny-tasks.json");
+ expect(result.mediaType).toBe("application/json");
+ expect(result.contents).toBe(JSON.stringify(done, null, 2) + "\n");
+ expect(JSON.parse(result.contents)).toEqual(done);
+ });
+});
diff --git a/apps/tiny-tasks/tests/storage.test.ts b/apps/tiny-tasks/tests/storage.test.ts
new file mode 100644
index 0000000..a9bc838
--- /dev/null
+++ b/apps/tiny-tasks/tests/storage.test.ts
@@ -0,0 +1,76 @@
+import { expect, test } from "bun:test";
+import { completeTask, type TasksState } from "../src/model";
+import { loadTasks, saveTasks, TASKS_STORAGE_KEY, type StorageAdapter } from "../src/storage";
+
+const fixture: TasksState = { version: 1, tasks: [{ id: "a", text: "Read", completed: true, history: [{ type: "added", at: 10 }, { type: "completed", at: 20 }] }] };
+const expectedKey = "corvid-examples:tiny-tasks:v1";
+
+test("loads absent storage without writes or destructive reset", () => {
+ const reads: string[] = [];
+ let writes = 0;
+ const result = loadTasks({ getItem(key) { reads.push(key); return null; }, setItem() { writes++; } });
+ expect(result).toEqual({ status: "empty" });
+ expect(reads).toEqual([expectedKey]);
+ expect(writes).toBe(0);
+});
+test("loads actual nonempty JSON through model validation", () => {
+ const result = loadTasks({ getItem(key) { expect(key).toBe(expectedKey); return JSON.stringify(fixture); }, setItem() { throw new Error("Unexpected write"); } });
+ expect(result).toEqual({ status: "loaded", value: fixture });
+});
+for (const raw of ["{", JSON.stringify({ version: 9 }), "null", "[]"]) {
+ test("warns on malformed or unsupported persisted data: " + raw, () => {
+ let writes = 0;
+ const result = loadTasks({ getItem() { return raw; }, setItem() { writes++; } });
+ expect(result.status).toBe("warning");
+ if (result.status === "warning") expect(result.message.length).toBeGreaterThan(0);
+ expect("value" in result).toBe(false);
+ expect(writes).toBe(0);
+ });
+}
+test("warns when the actual adapter read throws", () => {
+ const result = loadTasks({ getItem() { throw new Error("Storage denied"); }, setItem() { throw new Error("Unexpected write"); } });
+ expect(result.status).toBe("warning");
+ if (result.status === "warning") expect(result.message.length).toBeGreaterThan(0);
+ expect("value" in result).toBe(false);
+});
+test("writes correct key and round-trips exact state through real helpers", () => {
+ const map = new Map<string, string>();
+ const writes: [string, string][] = [];
+ const adapter: StorageAdapter = {
+ getItem(key) { return map.get(key) ?? null; },
+ setItem(key, contents) { writes.push([key, contents]); map.set(key, contents); },
+ };
+ expect(saveTasks(adapter, fixture)).toEqual({ ok: true });
+ expect(writes).toHaveLength(1);
+ expect(writes[0]?.[0]).toBe(expectedKey);
+ expect(map.get(TASKS_STORAGE_KEY)).toBe(JSON.stringify(fixture));
+ expect(loadTasks(adapter)).toEqual({ status: "loaded", value: fixture });
+});
+test("actual write failure preserves current state and returns useful warning", () => {
+ const before = structuredClone(fixture);
+ let calls = 0;
+ const result = saveTasks({ getItem() { return null; }, setItem(key) { expect(key).toBe(expectedKey); calls++; throw new Error("Quota exceeded"); } }, fixture);
+ expect(result.ok).toBe(false);
+ if (!result.ok) expect(result.message.length).toBeGreaterThan(0);
+ expect(calls).toBe(1);
+ expect(fixture).toEqual(before);
+});
+test("save validates state before touching adapter", () => {
+ let calls = 0;
+ const invalid = { ...fixture, version: 2 } as unknown as TasksState;
+ const result = saveTasks({ getItem() { return null; }, setItem() { calls++; } }, invalid);
+ expect(result.ok).toBe(false);
+ expect(calls).toBe(0);
+});
+test("current real model edit survives failure then saves and reloads on recovery", () => {
+ const edited = completeTask({ version: 1, tasks: [{ id: "a", text: "Read", completed: false, history: [{ type: "added", at: 10 }] }] }, "a", 20);
+ if (!edited.ok) throw new Error(edited.error);
+ const before = structuredClone(edited.value);
+ const failed = saveTasks({ getItem() { return null; }, setItem() { throw new Error("Unavailable"); } }, edited.value);
+ expect(failed.ok).toBe(false);
+ expect(edited.value).toEqual(before);
+ let saved: string | null = null;
+ const adapter: StorageAdapter = { getItem() { return saved; }, setItem(_key, contents) { saved = contents; } };
+ expect(saveTasks(adapter, edited.value)).toEqual({ ok: true });
+ expect(loadTasks(adapter)).toEqual({ status: "loaded", value: before });
+});
diff --git a/apps/tiny-tasks/tsconfig.json b/apps/tiny-tasks/tsconfig.json
new file mode 100644
index 0000000..5c09ca1
--- /dev/null
+++ b/apps/tiny-tasks/tsconfig.json
@@ -0,0 +1,19 @@
+{
+ "compilerOptions": {
+ "target": "ES2022",
+ "module": "ESNext",
+ "moduleResolution": "Bundler",
+ "lib": [
+ "ES2022",
+ "DOM",
+ "DOM.Iterable"
+ ],
+ "strict": true,
+ "noEmit": true,
+ "skipLibCheck": true,
+ "types": []
+ },
+ "include": [
+ "src/**/*.ts"
+ ]
+}