switched from codex to claude as tutor and moved tests to their own folder

This commit is contained in:
2026-03-06 14:11:37 +01:00
parent 03acf441a0
commit 7b65e180eb
6 changed files with 161 additions and 80 deletions

View File

@@ -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:

63
CLAUDE.md Normal file
View File

@@ -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.).

View File

@@ -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());
}
}

View File

@@ -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,

View File

@@ -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());
}

View File

@@ -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";