← Pocket Notes history

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 2aec720e3ab5d92f84867b1a371c988c711807b8
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/pocket-notes/package.json b/apps/pocket-notes/package.json
new file mode 100644
index 0000000..8e80c1e
--- /dev/null
+++ b/apps/pocket-notes/package.json
@@ -0,0 +1,16 @@
+{
+  "name": "@corvid-examples/pocket-notes",
+  "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/pocket-notes/src/model.ts b/apps/pocket-notes/src/model.ts
new file mode 100644
index 0000000..b3059c2
--- /dev/null
+++ b/apps/pocket-notes/src/model.ts
@@ -0,0 +1,22 @@
+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 interface NoteState {
+    readonly version: 1;
+    readonly text: string;
+    readonly updatedAt: number;
+}
+export const MAX_NOTE_LENGTH = 20_000;
+export function createNoteState(): NoteState { throw new Error("Not implemented"); }
+export function validateNoteState(_value: unknown): ModelResult<NoteState> { throw new Error("Not implemented"); }
+export function editNote(_state: NoteState, _text: string, _at: number): ModelResult<NoteState> {
+    throw new Error("Not implemented");
+}
+export function createNoteExport(_state: NoteState): FileExport { throw new Error("Not implemented"); }
diff --git a/apps/pocket-notes/src/storage.ts b/apps/pocket-notes/src/storage.ts
new file mode 100644
index 0000000..78d8b25
--- /dev/null
+++ b/apps/pocket-notes/src/storage.ts
@@ -0,0 +1,21 @@
+import type { NoteState } 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 NOTE_STORAGE_KEY = "corvid-examples:pocket-notes:v1";
+export function loadNote(_storage: StorageAdapter): LoadResult<NoteState> {
+    throw new Error("Not implemented");
+}
+export function saveNote(_storage: StorageAdapter, _state: NoteState): SaveResult {
+    throw new Error("Not implemented");
+}
diff --git a/apps/pocket-notes/tests/model.test.ts b/apps/pocket-notes/tests/model.test.ts
new file mode 100644
index 0000000..72bf76a
--- /dev/null
+++ b/apps/pocket-notes/tests/model.test.ts
@@ -0,0 +1,70 @@
+import { expect, test } from "bun:test";
+import { createNoteState, createNoteExport, editNote, validateNoteState,
+    type ModelResult, type NoteState } from "../src/model";
+
+const initial: NoteState = { version: 1, text: "Earlier", updatedAt: 10 };
+function value<Value>(result: ModelResult<Value>): Value {
+    if (!result.ok) throw new Error(result.error);
+    return result.value;
+}
+test("creates one empty versioned note", () => {
+    expect(createNoteState()).toEqual({ version: 1, text: "", updatedAt: 0 });
+});
+test("editing preserves exact multiline Unicode and leaves previous state unchanged", () => {
+    const before = structuredClone(initial);
+    const text = "  Café 🐦\nSecond line\n ";
+    expect(value(editNote(initial, text, 20))).toEqual({ version: 1, text, updatedAt: 20 });
+    expect(initial).toEqual(before);
+});
+test("second edit and explicit clearing are valid", () => {
+    const edited = value(editNote(initial, "Second", 20));
+    expect(value(editNote(edited, "", 21))).toEqual({ version: 1, text: "", updatedAt: 21 });
+    expect(edited.text).toBe("Second");
+});
+test("accepts 20000 characters and equal timestamps", () => {
+    expect(value(editNote(initial, "x".repeat(20000), 10)).text).toHaveLength(20000);
+});
+test("rejects oversized text without mutating prior note", () => {
+    const before = structuredClone(initial);
+    {
+        const previous = structuredClone(initial);
+        const rejected = editNote(initial, "x".repeat(20001), 20);
+        expect(rejected.ok).toBe(false);
+        if (!rejected.ok) expect(rejected.error.length).toBeGreaterThan(0);
+        expect(initial).toEqual(previous);
+    }
+    expect(initial).toEqual(before);
+});
+for (const at of [-1, 9, NaN, Infinity, 1.5, Number.MAX_SAFE_INTEGER + 1]) {
+    test("rejects invalid or decreasing edit time: " + at, () => {
+        {
+            const previous = structuredClone(initial);
+            const rejected = editNote(initial, "Changed", at);
+            expect(rejected.ok).toBe(false);
+            if (!rejected.ok) expect(rejected.error.length).toBeGreaterThan(0);
+            expect(initial).toEqual(previous);
+        }
+    });
+}
+test("validates a nonempty note into an independent value", () => {
+    const fixture = { version: 1, text: "Saved\nexactly ", updatedAt: 20 };
+    const result = value(validateNoteState(fixture));
+    fixture.text = "Changed outside";
+    expect(result.text).toBe("Saved\nexactly ");
+});
+const invalid: readonly unknown[] = [
+    null, [], {}, { ...initial, version: 2 }, { ...initial, text: 42 },
+    { ...initial, text: "x".repeat(20001) }, { ...initial, updatedAt: NaN },
+    { ...initial, updatedAt: -1 }, { ...initial, updatedAt: 1.5 }, { ...initial, extra: true },
+];
+invalid.forEach((input, index) => test("rejects malformed persisted note " + index, () => {
+    const result = validateNoteState(input);
+    expect(result.ok).toBe(false);
+    if (!result.ok) expect(result.error.length).toBeGreaterThan(0);
+}));
+test("exports exact current note text without headers or appended newline", () => {
+    const note: NoteState = { version: 1, text: "  Café 🐦\nKeep whitespace ", updatedAt: 20 };
+    expect(createNoteExport(note)).toEqual({
+        filename: "pocket-note.txt", mediaType: "text/plain", contents: note.text,
+    });
+});
diff --git a/apps/pocket-notes/tests/storage.test.ts b/apps/pocket-notes/tests/storage.test.ts
new file mode 100644
index 0000000..d1ae640
--- /dev/null
+++ b/apps/pocket-notes/tests/storage.test.ts
@@ -0,0 +1,76 @@
+import { expect, test } from "bun:test";
+import { editNote, type NoteState } from "../src/model";
+import { loadNote, saveNote, NOTE_STORAGE_KEY, type StorageAdapter } from "../src/storage";
+
+const fixture: NoteState = { version: 1, text: "Saved\nexactly ", updatedAt: 20 };
+const expectedKey = "corvid-examples:pocket-notes:v1";
+
+test("loads absent storage without writes or destructive reset", () => {
+    const reads: string[] = [];
+    let writes = 0;
+    const result = loadNote({ 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 = loadNote({ 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 = loadNote({ 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 = loadNote({ 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(saveNote(adapter, fixture)).toEqual({ ok: true });
+    expect(writes).toHaveLength(1);
+    expect(writes[0]?.[0]).toBe(expectedKey);
+    expect(map.get(NOTE_STORAGE_KEY)).toBe(JSON.stringify(fixture));
+    expect(loadNote(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 = saveNote({ 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 NoteState;
+    const result = saveNote({ 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 = editNote({ version: 1, text: "Earlier", updatedAt: 10 }, "Saved\nexactly ", 20);
+    if (!edited.ok) throw new Error(edited.error);
+    const before = structuredClone(edited.value);
+    const failed = saveNote({ 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(saveNote(adapter, edited.value)).toEqual({ ok: true });
+    expect(loadNote(adapter)).toEqual({ status: "loaded", value: before });
+});
diff --git a/apps/pocket-notes/tsconfig.json b/apps/pocket-notes/tsconfig.json
new file mode 100644
index 0000000..5c09ca1
--- /dev/null
+++ b/apps/pocket-notes/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"
+  ]
+}