From 7b65e180ebb5e9d2e2918594afc8d278c6ed3fcb Mon Sep 17 00:00:00 2001 From: "s0wlz (Matthias Puchstein)" Date: Fri, 6 Mar 2026 14:11:37 +0100 Subject: [PATCH] switched from codex to claude as tutor and moved tests to their own folder --- AGENTS.md | 26 +++++++++-- CLAUDE.md | 63 +++++++++++++++++++++++++ src-tauri/src/countdown/model.rs | 75 ------------------------------ src-tauri/src/lib.rs | 2 +- src-tauri/tests/countdown_model.rs | 73 +++++++++++++++++++++++++++++ src/app/app.css | 2 +- 6 files changed, 161 insertions(+), 80 deletions(-) create mode 100644 CLAUDE.md create mode 100644 src-tauri/tests/countdown_model.rs diff --git a/AGENTS.md b/AGENTS.md index f6e77f7..49d54c3 100644 --- a/AGENTS.md +++ b/AGENTS.md @@ -1,13 +1,28 @@ # Repository Guidelines +## Project Overview +**Owlerlay** is a Tauri-based **OBS overlay control center**. It is not a simple countdown timer — the countdown feature is just one of several planned core features. + +**Goals:** +- Act as a control center for OBS browser-source overlays (rendered via HTMX + CSS, minimal JS) +- Low resource consumption, cross-platform support +- Extensible via a plugin system + +**Planned core features:** alarms, twitch chat integration, countdowns (timers), alerts, and more. + +**Overlay rendering approach:** HTMX + CSS templates served to OBS browser sources; JavaScript kept to an absolute minimum. + +--- + ## Learning-First Collaboration Rules This is a learning project. The assistant is a guide, not an implementer. -- The developer writes all application code and tests. -- The assistant must not write or modify project source code (for example `src/` or `src-tauri/src/`). +- The developer writes all application code. +- The assistant **may** write test files (Rust integration tests, Vitest specs, etc.) when asked. +- The assistant must not write or modify project source code (for example `src/` or `src-tauri/src/`), **except** for test files. - The assistant may provide step-by-step guidance, debugging help, reviews, architecture feedback, and hints. - The assistant may edit documentation files when explicitly requested. -- If asked to implement code, the assistant should refuse and provide a clear plan the developer can execute. +- If asked to implement non-test code, the assistant should refuse and provide a clear plan the developer can execute. - If asked about a file, function, module, or crate, the assistant must read the current code first before answering. - Reviews and guidance must be based strictly on the current file contents, not earlier snapshots. - Before each technical answer, the assistant must re-open the relevant files/outputs and verify against current on-disk state, never relying on memory from earlier turns. @@ -44,6 +59,11 @@ No automated JS test framework is currently configured. For new features: - add frontend tests only when introducing non-trivial UI/state behavior (Vitest is the preferred choice if added). - include manual verification steps in PRs (OS, command run, expected behavior). +The assistant **is allowed to write test files** when asked. Tests must live in dedicated files, **separate from source code**: +- Rust tests: `src-tauri/tests/` (integration tests using the public API of each module) +- Frontend tests: `src/tests/` or a top-level `tests/` directory (Vitest specs) +- Do **not** add `#[cfg(test)]` blocks inside `src-tauri/src/` files — keep source and test code in separate files. + ## Commit & Pull Request Guidelines Git history is not available in this workspace snapshot, so use Conventional Commit style: diff --git a/CLAUDE.md b/CLAUDE.md new file mode 100644 index 0000000..dc8f8e7 --- /dev/null +++ b/CLAUDE.md @@ -0,0 +1,63 @@ +# CLAUDE.md + +> This file configures Claude Code's behaviour in this repository. +> The canonical collaboration rules live in [AGENTS.md](./AGENTS.md). +> Claude must read and comply with AGENTS.md on every session. + +--- + +## Role + +This is a **learning project**. Claude is a guide and reviewer, not an implementer. + +- **Do not** write or modify files inside `src/` or `src-tauri/src/`. +- **Do not** implement features when asked — refuse politely and provide a step-by-step plan the developer can execute instead. +- **Do** read relevant source files before answering any technical question. +- **Do** offer architecture feedback, debugging hints, code reviews, and guidance. +- **Do** edit documentation files when explicitly asked. + +--- + +## Project Overview + +**Owlerlay** is a desktop countdown-timer app built with: + +| Layer | Tech | +|-------|------| +| Frontend | Svelte 5, TypeScript (strict), Vite 7, PicoCSS | +| Backend | Rust (Edition 2024), Tauri 2 | +| Package manager | pnpm | + +### Key paths + +| Path | Purpose | +|------|---------| +| `src/` | Svelte/TS frontend | +| `src-tauri/src/` | Rust Tauri commands & state | +| `src-tauri/src/lib.rs` | Tauri builder & command registration | +| `src-tauri/src/countdown/` | Countdown feature (commands, logic) | +| `src-tauri/src/app_state.rs` | Shared app state | +| `src-tauri/tauri.conf.json` | Tauri configuration | + +--- + +## Common Commands + +```bash +pnpm install # install JS deps +pnpm dev # Vite dev server (http://localhost:1420) +pnpm tauri dev # full app in dev mode +pnpm build # tsc + Vite build +pnpm tauri build # desktop bundle +pnpm check # svelte-check + tsc +cargo test --manifest-path src-tauri/Cargo.toml # Rust tests +``` + +--- + +## Coding Conventions (summary — see AGENTS.md for full details) + +- **TypeScript**: 2-space indent, strict mode, `camelCase` vars/fns, `PascalCase` types. +- **Rust**: 4-space indent (`rustfmt`), `snake_case` fns/modules, Edition 2024. +- Tauri commands: keep small and side-effect focused; register in `lib.rs`. +- Commit style: Conventional Commits (`feat:`, `fix:`, etc.). diff --git a/src-tauri/src/countdown/model.rs b/src-tauri/src/countdown/model.rs index a0709b5..c7dca77 100644 --- a/src-tauri/src/countdown/model.rs +++ b/src-tauri/src/countdown/model.rs @@ -159,78 +159,3 @@ impl Countdown { self.target_timestamp = None; } } - -#[cfg(test)] -mod tests { - use super::*; - - #[test] - fn start_from_idle_enters_running() { - let now = Instant::now(); - let mut countdown = Countdown::new("test", Duration::from_secs(10)); - countdown.start(now).expect("start should succeed"); - - assert_eq!(countdown.state(), CountdownState::Running); - assert_eq!(countdown.remaining_at(now), Duration::from_secs(10)); - } - - #[test] - fn pause_and_resume_preserve_remaining_time() { - let now = Instant::now(); - let mut countdown = Countdown::new("test", Duration::from_secs(10)); - countdown.start(now).expect("start should succeed"); - - let after_three_seconds = now.checked_add(Duration::from_secs(3)).unwrap(); - countdown - .pause(after_three_seconds) - .expect("pause should succeed"); - assert_eq!(countdown.state(), CountdownState::Paused); - assert_eq!( - countdown.remaining_at(after_three_seconds), - Duration::from_secs(7) - ); - - let resume_time = after_three_seconds - .checked_add(Duration::from_secs(1)) - .unwrap(); - countdown - .resume(resume_time) - .expect("resume should succeed"); - assert_eq!(countdown.state(), CountdownState::Running); - assert_eq!(countdown.remaining_at(resume_time), Duration::from_secs(7)); - } - - #[test] - fn invalid_transition_returns_error() { - let now = Instant::now(); - let mut countdown = Countdown::new("test", Duration::from_secs(5)); - - let err = countdown - .pause(now) - .expect_err("pause should fail from idle"); - assert_eq!( - err, - CountdownError::InvalidTransition { - from: CountdownState::Idle, - action: "pause", - } - ); - } - - #[test] - fn running_countdown_reaches_finished() { - let now = Instant::now(); - let mut countdown = Countdown::new("test", Duration::from_secs(2)); - countdown.start(now).expect("start should succeed"); - - let after_two_seconds = now.checked_add(Duration::from_secs(2)).unwrap(); - countdown.sync_finished_at(after_two_seconds); - - assert_eq!(countdown.state(), CountdownState::Finished); - assert_eq!( - countdown.remaining_at(after_two_seconds), - Duration::from_secs(0) - ); - assert!(countdown.is_finished()); - } -} diff --git a/src-tauri/src/lib.rs b/src-tauri/src/lib.rs index cab9a9a..7588291 100644 --- a/src-tauri/src/lib.rs +++ b/src-tauri/src/lib.rs @@ -1,7 +1,7 @@ extern crate core; mod app_state; -mod countdown; +pub mod countdown; use crate::countdown::commands::{ countdown_create, countdown_delete, countdown_list, countdown_pause, countdown_reset, diff --git a/src-tauri/tests/countdown_model.rs b/src-tauri/tests/countdown_model.rs new file mode 100644 index 0000000..ac7db6d --- /dev/null +++ b/src-tauri/tests/countdown_model.rs @@ -0,0 +1,73 @@ +use owlerlay_lib::countdown::errors::CountdownError; +use owlerlay_lib::countdown::model::{Countdown, CountdownState}; +use tokio::time::{Duration, Instant}; + +#[test] +fn start_from_idle_enters_running() { + let now = Instant::now(); + let mut countdown = Countdown::new("test", Duration::from_secs(10)); + countdown.start(now).expect("start should succeed"); + + assert_eq!(countdown.state(), CountdownState::Running); + assert_eq!(countdown.remaining_at(now), Duration::from_secs(10)); +} + +#[test] +fn pause_and_resume_preserve_remaining_time() { + let now = Instant::now(); + let mut countdown = Countdown::new("test", Duration::from_secs(10)); + countdown.start(now).expect("start should succeed"); + + let after_three_seconds = now.checked_add(Duration::from_secs(3)).unwrap(); + countdown + .pause(after_three_seconds) + .expect("pause should succeed"); + assert_eq!(countdown.state(), CountdownState::Paused); + assert_eq!( + countdown.remaining_at(after_three_seconds), + Duration::from_secs(7) + ); + + let resume_time = after_three_seconds + .checked_add(Duration::from_secs(1)) + .unwrap(); + countdown + .resume(resume_time) + .expect("resume should succeed"); + assert_eq!(countdown.state(), CountdownState::Running); + assert_eq!(countdown.remaining_at(resume_time), Duration::from_secs(7)); +} + +#[test] +fn invalid_transition_returns_error() { + let now = Instant::now(); + let mut countdown = Countdown::new("test", Duration::from_secs(5)); + + let err = countdown + .pause(now) + .expect_err("pause should fail from idle"); + assert_eq!( + err, + CountdownError::InvalidTransition { + from: CountdownState::Idle, + action: "pause", + } + ); +} + +#[test] +fn running_countdown_reaches_finished() { + let now = Instant::now(); + let mut countdown = Countdown::new("test", Duration::from_secs(2)); + countdown.start(now).expect("start should succeed"); + + let after_two_seconds = now.checked_add(Duration::from_secs(2)).unwrap(); + countdown.sync_finished_at(after_two_seconds); + + assert_eq!(countdown.state(), CountdownState::Finished); + assert_eq!( + countdown.remaining_at(after_two_seconds), + Duration::from_secs(0) + ); + assert!(countdown.is_finished()); +} diff --git a/src/app/app.css b/src/app/app.css index b67f2fd..c45c748 100644 --- a/src/app/app.css +++ b/src/app/app.css @@ -1,3 +1,3 @@ -@import "@picocss/pico/css/pico.classless.jade.min.css"; +@import "@picocss/pico/css/pico.jade.min.css"; @import "./styles/base.css"; @import "./styles/countdown.css";