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 0e48e06b78618b9a8ced1aa78787fef20dcc8651
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/focus-timer/LICENSE b/apps/focus-timer/LICENSE
new file mode 100644
index 0000000..812de8f
--- /dev/null
+++ b/apps/focus-timer/LICENSE
@@ -0,0 +1,21 @@
+MIT License
+
+Copyright (c) 2026 CorvidLabs
+
+Permission is hereby granted, free of charge, to any person obtaining a copy
+of this software and associated documentation files (the "Software"), to deal
+in the Software without restriction, including without limitation the rights
+to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
+copies of the Software, and to permit persons to whom the Software is
+furnished to do so, subject to the following conditions:
+
+The above copyright notice and this permission notice shall be included in all
+copies or substantial portions of the Software.
+
+THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
+IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
+FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
+AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
+LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
+OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
+SOFTWARE.
diff --git a/apps/focus-timer/package.json b/apps/focus-timer/package.json
new file mode 100644
index 0000000..1e1ed6b
--- /dev/null
+++ b/apps/focus-timer/package.json
@@ -0,0 +1,16 @@
+{
+ "name": "focus-timer",
+ "version": "0.1.0",
+ "private": true,
+ "type": "module",
+ "license": "MIT",
+ "scripts": {
+ "test": "bun test",
+ "typecheck": "tsc --noEmit -p tsconfig.json",
+ "build": "bun build.ts",
+ "start": "bun serve.ts"
+ },
+ "devDependencies": {
+ "typescript": "5.9.3"
+ }
+}
diff --git a/apps/focus-timer/src/model.ts b/apps/focus-timer/src/model.ts
new file mode 100644
index 0000000..90b021a
--- /dev/null
+++ b/apps/focus-timer/src/model.ts
@@ -0,0 +1,23 @@
+// Red-phase API stubs. Root commits these failure inputs before implementation.
+export const DEFAULT_DURATION_MS = 750_000;
+export type TimerPhase = "idle" | "running" | "paused" | "completed";
+export interface TimerState {
+ readonly phase: TimerPhase;
+ readonly remainingMs: number;
+ readonly deadlineMs: number | null;
+}
+export function createTimer(): TimerState {
+ throw new Error("Not implemented: createTimer");
+}
+export function startTimer(state: TimerState, nowMs: number): TimerState {
+ throw new Error("Not implemented: startTimer");
+}
+export function pauseTimer(state: TimerState, nowMs: number): TimerState {
+ throw new Error("Not implemented: pauseTimer");
+}
+export function advanceTimer(state: TimerState, nowMs: number): TimerState {
+ throw new Error("Not implemented: advanceTimer");
+}
+export function resetTimer(): TimerState {
+ throw new Error("Not implemented: resetTimer");
+}
diff --git a/apps/focus-timer/tests/model.test.ts b/apps/focus-timer/tests/model.test.ts
new file mode 100644
index 0000000..496830a
--- /dev/null
+++ b/apps/focus-timer/tests/model.test.ts
@@ -0,0 +1,70 @@
+import { describe, expect, test } from "bun:test";
+import { DEFAULT_DURATION_MS, createTimer, startTimer, pauseTimer, advanceTimer, resetTimer, type TimerState } from "../src/model";
+const idle: TimerState = Object.freeze({ phase: "idle", remainingMs: 750_000, deadlineMs: null });
+const running: TimerState = Object.freeze({ phase: "running", remainingMs: 750_000, deadlineMs: 751_000 });
+
+describe("REQ-focus-timer-001", () => {
+ test("default and reset are idle twelve minutes thirty seconds", () => {
+ expect(DEFAULT_DURATION_MS).toBe(750_000);
+ expect(createTimer()).toEqual(idle);
+ expect(resetTimer()).toEqual(idle);
+ });
+ test("start sets a deadline; repeated start never extends it", () => {
+ const started = Object.freeze(startTimer(idle, 1_000));
+ expect(started).toEqual({ phase: "running", remainingMs: 750_000, deadlineMs: 751_000 });
+ expect(startTimer(started, 2_000)).toEqual({ phase: "running", remainingMs: 749_000, deadlineMs: 751_000 });
+ expect(idle.phase).toBe("idle");
+ expect(started.remainingMs).toBe(750_000);
+ });
+ test("elapsed time, pause and resume preserve exact subsecond budget", () => {
+ expect(advanceTimer(running, 101_000).remainingMs).toBe(650_000);
+ const paused = Object.freeze(pauseTimer(running, 126_000));
+ expect(paused).toEqual({ phase: "paused", remainingMs: 625_000, deadlineMs: null });
+ expect(advanceTimer(paused, 499_000)).toEqual(paused);
+ expect(pauseTimer(paused, 499_001)).toEqual(paused);
+ const resumed = Object.freeze(startTimer(paused, 500_000));
+ expect(resumed.deadlineMs).toBe(1_125_000);
+ expect(resumed.remainingMs).toBe(625_000);
+ expect(advanceTimer(resumed, 500_500).remainingMs).toBe(624_500);
+ expect(paused.remainingMs).toBe(625_000);
+ expect(running.remainingMs).toBe(750_000);
+ });
+ test("exact deadline completes once and clamps late observations to zero", () => {
+ expect(advanceTimer(running, 750_999)).toEqual({ phase: "running", remainingMs: 1, deadlineMs: 751_000 });
+ const done = Object.freeze(advanceTimer(running, 751_000));
+ expect(done).toEqual({ phase: "completed", remainingMs: 0, deadlineMs: null });
+ expect(advanceTimer(done, 9_000_000)).toEqual(done);
+ expect(startTimer(done, 9_000_001)).toEqual(done);
+ expect(pauseTimer(done, 9_000_002)).toEqual(done);
+ expect(advanceTimer(running, 9_000_000)).toEqual(done);
+ expect(running.phase).toBe("running");
+ });
+ test("pause at or beyond expiry cannot resurrect a completed timer", () => {
+ for (const time of [751_000, 751_001, 9_000_000]) {
+ expect(pauseTimer(running, time)).toEqual({ phase: "completed", remainingMs: 0, deadlineMs: null });
+ }
+ });
+ test("reset after each phase permits a fresh full deadline", () => {
+ const states = [running, pauseTimer(running, 126_000), advanceTimer(running, 751_000)];
+ for (const state of states) {
+ const snapshot = structuredClone(state);
+ const reset = Object.freeze(resetTimer());
+ expect(reset).toEqual(idle);
+ expect(startTimer(reset, 2_000_000).deadlineMs).toBe(2_750_000);
+ expect(state).toEqual(snapshot);
+ }
+ expect(advanceTimer(idle, 3_000_000)).toEqual(idle);
+ expect(pauseTimer(idle, 3_000_001)).toEqual(idle);
+ });
+ test("one background gap completes without counting callback ticks", () => {
+ const started = startTimer(idle, 10);
+ expect(advanceTimer(started, 750_011)).toEqual({ phase: "completed", remainingMs: 0, deadlineMs: null });
+ });
+ test("invalid clock values require RangeError rather than generic stub error", () => {
+ for (const value of [-1, NaN, Infinity]) {
+ expect(() => startTimer(idle, value)).toThrow(RangeError);
+ expect(() => pauseTimer(running, value)).toThrow(RangeError);
+ expect(() => advanceTimer(running, value)).toThrow(RangeError);
+ }
+ });
+});
diff --git a/apps/focus-timer/tsconfig.json b/apps/focus-timer/tsconfig.json
new file mode 100644
index 0000000..3a66cf3
--- /dev/null
+++ b/apps/focus-timer/tsconfig.json
@@ -0,0 +1,19 @@
+{
+ "compilerOptions": {
+ "target": "ES2022",
+ "module": "ESNext",
+ "moduleResolution": "Bundler",
+ "lib": [
+ "ES2022",
+ "DOM"
+ ],
+ "strict": true,
+ "noEmit": true,
+ "types": [],
+ "skipLibCheck": true,
+ "forceConsistentCasingInFileNames": true
+ },
+ "include": [
+ "src/**/*.ts"
+ ]
+}