From a79dcc461794f216fd6cb8fc034f30708235cf7f Mon Sep 17 00:00:00 2001 From: Claude Date: Fri, 17 Apr 2026 16:32:22 +0000 Subject: [PATCH] test: cover CLI workspace resolver, date picker, saturating version Add regression tests for the bugs found in this smoke test: - resolve_workspace: by-name, by-UUID, unknown-identifier, current-fallback, actionable no-workspace message. - DateTimePicker: selected-day highlight must be month-scoped; committing after navigating months uses the selected month, not the viewed one. - create_task: version is saturating_add on u64::MAX (doesn't panic/wrap). Also fixes the three pre-existing clippy warnings (WorkspaceMode now uses #[derive(Default)] + #[default], repository test drops unused binding, sync test uses struct-update syntax instead of field-reassign-default). --- .../src/lib/components/DateTimePicker.test.ts | 74 +++++++++++++++++++ crates/onyx-cli/src/commands/mod.rs | 57 ++++++++++++++ crates/onyx-core/src/config.rs | 9 +-- crates/onyx-core/src/repository.rs | 16 +++- crates/onyx-core/src/sync.rs | 3 +- 5 files changed, 149 insertions(+), 10 deletions(-) create mode 100644 apps/tauri/src/lib/components/DateTimePicker.test.ts diff --git a/apps/tauri/src/lib/components/DateTimePicker.test.ts b/apps/tauri/src/lib/components/DateTimePicker.test.ts new file mode 100644 index 0000000..755c67a --- /dev/null +++ b/apps/tauri/src/lib/components/DateTimePicker.test.ts @@ -0,0 +1,74 @@ +import { describe, it, expect, vi, beforeEach } from "vitest"; +import { render, screen, cleanup } from "@testing-library/svelte"; +import userEvent from "@testing-library/user-event"; +import DateTimePicker from "./DateTimePicker.svelte"; + +beforeEach(() => { + cleanup(); +}); + +describe("DateTimePicker — selected highlight", () => { + it("only marks the selected day in the month/year that was actually picked", async () => { + const user = userEvent.setup(); + // Pick a date in the current month so the component opens on it. + const now = new Date(); + const existing = new Date(now.getFullYear(), now.getMonth(), 15, 0, 0, 0).toISOString(); + + render(DateTimePicker, { + value: existing, + has_time: false, + onchange: vi.fn(), + onclose: vi.fn(), + }); + + // The "15" button for the current month should be rendered with the + // selected styling (bg-primary). + const day15 = screen.getByRole("button", { name: "15" }); + expect(day15.className).toMatch(/bg-primary/); + + // Navigate one month forward. The same "15" cell must NOT be marked as + // selected, because the user hasn't picked a day in that month yet. + const nextMonthBtn = screen.getAllByRole("button").find((b) => + b.querySelector("svg path[d*='M7.21 14.77']"), + ) as HTMLElement; + await user.click(nextMonthBtn); + + const nextMonth15 = screen.getByRole("button", { name: "15" }); + expect(nextMonth15.className).not.toMatch(/bg-primary/); + }); + + it("commits based on the last-selected month, not the currently-viewed month", async () => { + const user = userEvent.setup(); + const onchange = vi.fn(); + const onclose = vi.fn(); + + // Start with April 10 selected (use a fixed month/year so the test is stable). + const existing = new Date(2026, 3, 10, 0, 0, 0).toISOString(); + render(DateTimePicker, { + value: existing, + has_time: false, + onchange, + onclose, + }); + + // Pick the 20th while viewing April. + await user.click(screen.getByRole("button", { name: "20" })); + + // Flip to May. + const nextMonthBtn = screen.getAllByRole("button").find((b) => + b.querySelector("svg path[d*='M7.21 14.77']"), + ) as HTMLElement; + await user.click(nextMonthBtn); + + // Hit Done. + await user.click(screen.getByRole("button", { name: "Done" })); + + expect(onchange).toHaveBeenCalled(); + const committed = new Date(onchange.mock.calls[0][0] as string); + // April == month 3 (0-indexed). We navigated to May without reselecting, + // so the committed date must still be April 20. + expect(committed.getMonth()).toBe(3); + expect(committed.getDate()).toBe(20); + expect(committed.getFullYear()).toBe(2026); + }); +}); diff --git a/crates/onyx-cli/src/commands/mod.rs b/crates/onyx-cli/src/commands/mod.rs index 7731bc0..b4197f1 100644 --- a/crates/onyx-cli/src/commands/mod.rs +++ b/crates/onyx-cli/src/commands/mod.rs @@ -53,3 +53,60 @@ pub fn get_repository(workspace_identifier: Option) -> Result<(TaskRepos Ok((repo, name)) } + +#[cfg(test)] +mod tests { + use super::*; + + fn make_config_with(ws: &[(&str, &str)]) -> (AppConfig, Vec) { + let mut config = AppConfig::new(); + let ids: Vec = ws.iter() + .map(|(name, path)| config.add_workspace(WorkspaceConfig::new(name.to_string(), PathBuf::from(path)))) + .collect(); + (config, ids) + } + + #[test] + fn resolve_by_name() { + let (config, _ids) = make_config_with(&[("dev", "/tmp/dev"), ("home", "/tmp/home")]); + let (id, ws) = resolve_workspace(&config, Some("dev")).unwrap(); + assert_eq!(ws.name, "dev"); + assert!(config.workspaces.contains_key(&id)); + } + + #[test] + fn resolve_by_uuid() { + let (config, ids) = make_config_with(&[("dev", "/tmp/dev")]); + let target = ids[0].clone(); + let (id, ws) = resolve_workspace(&config, Some(&target)).unwrap(); + assert_eq!(id, target); + assert_eq!(ws.name, "dev"); + } + + #[test] + fn resolve_unknown_identifier_errors() { + let (config, _ids) = make_config_with(&[("dev", "/tmp/dev")]); + let err = resolve_workspace(&config, Some("ghost")).unwrap_err(); + assert!(err.to_string().contains("Workspace 'ghost' not found")); + } + + #[test] + fn resolve_falls_back_to_current() { + let (mut config, ids) = make_config_with(&[("a", "/tmp/a"), ("b", "/tmp/b")]); + config.set_current_workspace(ids[1].clone()).unwrap(); + let (id, ws) = resolve_workspace(&config, None).unwrap(); + assert_eq!(id, ids[1]); + assert_eq!(ws.name, "b"); + } + + #[test] + fn resolve_no_current_gives_actionable_message() { + let config = AppConfig::new(); + let err = resolve_workspace(&config, None).unwrap_err(); + let msg = err.to_string(); + // The message should point the user at the right sub-commands, not + // at the obsolete 'onyx init' suggestion. + assert!(msg.contains("workspace add") || msg.contains("workspace switch"), + "expected actionable message, got: {msg}"); + } +} diff --git a/crates/onyx-core/src/config.rs b/crates/onyx-core/src/config.rs index 4498b06..1ad7769 100644 --- a/crates/onyx-core/src/config.rs +++ b/crates/onyx-core/src/config.rs @@ -4,20 +4,15 @@ use serde::{Deserialize, Serialize}; use uuid::Uuid; use crate::error::{Error, Result}; -#[derive(Debug, Clone, Serialize, Deserialize, PartialEq)] +#[derive(Debug, Clone, Serialize, Deserialize, PartialEq, Default)] #[serde(rename_all = "lowercase")] pub enum WorkspaceMode { + #[default] Local, Webdav, GoogleTasks, } -impl Default for WorkspaceMode { - fn default() -> Self { - Self::Local - } -} - #[derive(Debug, Clone, Serialize, Deserialize)] pub struct WorkspaceConfig { pub name: String, diff --git a/crates/onyx-core/src/repository.rs b/crates/onyx-core/src/repository.rs index 5c18b60..aa96764 100644 --- a/crates/onyx-core/src/repository.rs +++ b/crates/onyx-core/src/repository.rs @@ -157,7 +157,7 @@ mod tests { // Create a task let task = Task::new("Test Task".to_string()); - let created_task = repo.create_task(list.id, task).unwrap(); + let _ = repo.create_task(list.id, task).unwrap(); // List tasks let tasks = repo.list_tasks(list.id).unwrap(); @@ -165,6 +165,20 @@ mod tests { assert_eq!(tasks[0].title, "Test Task"); } + #[test] + fn test_create_task_saturates_version_at_max() { + let temp_dir = TempDir::new().unwrap(); + let mut repo = TaskRepository::init(temp_dir.path().to_path_buf()).unwrap(); + let list = repo.create_list("L".to_string()).unwrap(); + + // Simulate a task that is already at u64::MAX. A plain `+=` would + // overflow — saturating_add must clamp. + let mut task = Task::new("max".to_string()); + task.version = u64::MAX; + let created = repo.create_task(list.id, task).unwrap(); + assert_eq!(created.version, u64::MAX); + } + #[test] fn test_update_task() { let temp_dir = TempDir::new().unwrap(); diff --git a/crates/onyx-core/src/sync.rs b/crates/onyx-core/src/sync.rs index bfae1f4..17c10ae 100644 --- a/crates/onyx-core/src/sync.rs +++ b/crates/onyx-core/src/sync.rs @@ -1137,8 +1137,7 @@ mod tests { #[test] fn test_sync_state_save_load_roundtrip() { let temp_dir = TempDir::new().unwrap(); - let mut state = SyncState::default(); - state.last_sync = Some(Utc::now()); + let mut state = SyncState { last_sync: Some(Utc::now()), ..Default::default() }; state.record_file("test.md", "abc123", Some("2026-01-01T00:00:00Z"), 42); state.save(temp_dir.path()).unwrap();