diff --git a/README.md b/README.md index 7403618..37636b4 100644 --- a/README.md +++ b/README.md @@ -352,14 +352,21 @@ Windows: ## Project Layout -- `cmd/gosentry` starts the desktop app. -- `src/gui` contains the GUI. -- `src/core` contains YAML storage, command execution, scheduling, and log cleanup. -- `assets` contains app icons that are embedded into the application binary. -- `scripts` contains build helpers. -- `docs` contains architecture notes, the changelog, and the roadmap. +- `cmd/gosentry` — entry point; starts the desktop app. +- `src/domain` — pure value types: `Job`, `Config`, `RunRecord`, `Schedule`, `JobRuntime`. +- `src/app` — `Service`: sole owner of job and runtime state; emits typed events to the UI. +- `src/scheduler` — pure timing loop; calls `Service.RunDue` on every tick. +- `src/runner` — shell command execution, log file writing, and log cleanup. +- `src/storage` — YAML persistence (`gosentry.yaml`, `jobs.yaml`). +- `src/platform/autostart` — `Manager` interface with Windows (shortcut) and Linux (XDG) implementations. +- `src/platform/desktop` — display-scale helper (Linux only). +- `src/platform/winproc` — hidden-window startup flags (Windows only). +- `src/ui` — Fyne windows, tabs, and dialogs; reads service state through events. +- `assets` — app icons embedded into the application binary. +- `scripts` — build helpers. +- `docs` — architecture notes, changelog, and roadmap. -Build outputs are written to `dist/`. The old local `bin/` directory is not used. +Build outputs are written to `dist/`. ## Dependencies diff --git a/docs/ARCHITECTURE.md b/docs/ARCHITECTURE.md index 7d757b8..67e6fa5 100644 --- a/docs/ARCHITECTURE.md +++ b/docs/ARCHITECTURE.md @@ -1,73 +1,104 @@ # GoSentry Architecture -This document shows the current component interaction model. GoSentry is still a -single desktop process: the GUI, scheduler, storage, and command runner live in -one application and communicate through Go function calls and shared in-memory -job state. +This document shows the current component interaction model. GoSentry is a +single desktop process: the GUI, application service, scheduler, storage, and +command runner live in one application. They communicate through typed events +and well-defined interfaces rather than shared mutable state. + +## Package Map + +``` +cmd/gosentry entry point — starts the UI +src/ + domain/ pure value types: Job, Config, RunRecord, Schedule, JobRuntime + app/ Service — sole owner of job/runtime state; emits typed Events + scheduler/ pure timing loop; calls app.Service.RunDue on every tick + runner/ shell command execution + log file writing + cleanup + storage/ YAML persistence (gosentry.yaml, jobs.yaml) + platform/ + autostart/ Manager interface + Windows (shortcut) and Linux (XDG) impls + desktop/ display-scale helper (Linux only) + winproc/ hidden-window startup flags (Windows only) + ui/ Fyne windows, tabs, and dialogs; reads service via Events +``` ## Component Diagram ```mermaid flowchart LR user["Desktop user"] - gui["src/gui - Fyne windows, tabs, dialogs"] - store["src/core Store - YAML config and jobs"] - scheduler["src/core Scheduler - @every and cron timing"] - runner["src/core Runner - shell command execution"] - autostart["src/core Autostart - Windows Startup shortcut / Linux desktop startup"] - config["gosentry.yaml - application settings"] - jobs["jobs.yaml - job definitions"] - logs["logs_dir - per-run command output logs"] - shell["Platform shell - cmd.exe /C or sh -c"] + ui["src/ui\nFyne windows, tabs, dialogs"] + svc["src/app Service\nsole owner of job + runtime state"] + store["src/storage Store\nYAML config and jobs"] + sched["src/scheduler Scheduler\npure timing loop"] + runner["src/runner\nshell command execution"] + autostart["src/platform/autostart Manager\nWindows shortcut / Linux XDG"] + config["gosentry.yaml\napplication settings"] + jobs["jobs.yaml\njob definitions"] + logs["logs_dir\nper-run command output logs"] + shell["Platform shell\ncmd.exe /C or sh -c"] - user -->|"edits jobs, settings, runs commands"| gui - gui -->|"OpenStore, SaveConfig, SaveJobs"| store + user -->|"edits jobs, settings, runs commands"| ui + ui -->|"CreateJob, UpdateJob, DeleteJob, RunNow, UpdateSettings, …"| svc + svc -->|"SaveJobs, SaveConfig, LoadJobs, LoadConfig"| store store -->|"read/write"| config store -->|"read/write"| jobs - gui -->|"Start, Pause, RunNow, RefreshSchedule"| scheduler - scheduler -->|"SaveJobs after state changes"| store - scheduler -->|"RunJob(trigger)"| runner + svc -->|"Start(RunDue)"| sched + sched -->|"RunDue(now)"| svc + svc -->|"RunJob"| runner runner -->|"execute command"| shell runner -->|"write stdout/stderr log"| logs - runner -->|"RunRecord with status, duration, log path"| scheduler - scheduler -->|"onChange RunRecord"| gui - gui -->|"display History, command output, job state"| user + runner -->|"RunRecord"| svc + svc -->|"emit JobChanged / RunRecorded / ErrorOccurred"| ui + ui -->|"display jobs, history, status"| user - gui -->|"SetAutostart, AutostartStatus"| autostart - autostart -->|"use executable path from resolved Paths"| config + ui -->|"SetAutostart, AutostartStatus"| autostart + svc -->|"Set / Status via Manager"| autostart ``` ## Main Flows 1. Startup: - The executable starts `cmd/gosentry`, which calls the GUI package. The GUI - opens the store, loads `gosentry.yaml` and `jobs.yaml`, creates the main tabs, - then starts the scheduler with the loaded job slice. + `cmd/gosentry` calls `ui.Run`, which creates an `app.Service`, opens the + store, loads `gosentry.yaml` and `jobs.yaml`, subscribes the UI to service + events, builds the main window, and calls `Service.Start` to begin the + scheduler loop. 2. Editing settings or jobs: - The GUI updates the in-memory job/config state and asks `Store` to write YAML - back to disk. Job definitions stay in one `jobs.yaml`; runtime command output - is not stored there. + The UI calls mutating methods on `app.Service` (e.g. `CreateJob`, + `UpdateJob`, `UpdateSettings`). The Service validates the request, updates + its in-memory state, persists through `storage.Store`, and emits a typed + `Event`. The UI's observer receives the event and refreshes the relevant + widget on the main thread via `fyne.Do`. 3. Scheduled run: - `Scheduler` checks due jobs on a one-second ticker. When a job is due, it marks - the job as running, saves state, and starts `Runner` asynchronously. + `scheduler.Scheduler` fires a tick every second. On each tick it calls + `Service.RunDue(now)`. The Service checks which enabled, non-paused jobs are + due, marks each as running, and launches `runner.RunJob` in a goroutine. 4. Manual run: - `Run now` calls the same scheduler path as scheduled execution, but the - resulting history record uses the `Manual` trigger. + `Run now` in the UI calls `Service.RunNow`. The Service checks that the job + exists, is not already running, and that the scheduler is not paused, then + executes `runner.RunJob` with the `Manual` trigger. 5. Command execution: - `Runner` executes the command through the platform shell, captures stdout and - stderr, writes one timestamped `.log` file, and returns a `RunRecord`. + `runner.RunJob` builds the platform-specific invocation, executes the + command through the platform shell, captures stdout and stderr, writes one + timestamped `.log` file, and returns a `domain.RunRecord`. 6. History update: - The scheduler receives the `RunRecord`, updates the matching job, saves YAML, - runs log cleanup, and calls the GUI callback so the `History` tab refreshes. + When a run goroutine completes, `Service` updates the job's runtime, saves + YAML, triggers log cleanup, and emits `RunRecorded`. The UI observer appends + the record to the History tab. 7. Autostart: - The Settings tab calls the platform autostart implementation. Windows uses a - shortcut in the current user's Startup folder. Linux uses a desktop-session - startup entry. Both autostart mechanisms pass `--start-in-tray`, so the - scheduler starts without opening the main window after sign-in. + `UpdateSettings` in the Service calls `autostart.Manager.Set`. The Manager + interface has two implementations: Windows writes a `.lnk` shortcut to the + user Startup folder; Linux writes an XDG Autostart `.desktop` file. Both + entries pass `--start-in-tray`. + +8. Error surfacing: + Background errors (failed YAML saves, cleanup errors) are emitted as + `ErrorOccurred` events and displayed in the UI status area, rather than + being silently discarded. diff --git a/docs/REFACTORING.md b/docs/REFACTORING.md index 4faa3dd..5e21a79 100644 --- a/docs/REFACTORING.md +++ b/docs/REFACTORING.md @@ -281,7 +281,7 @@ Track progress here. Mark tasks complete as they land and pass review. - [x] T5.2 — Introduce `autostart.Manager` interface - [x] T5.3 — Fill test gaps (folder filtering, cleanup, migration, concurrency) - [x] T5.4 — Run `go test -race ./...` clean on both platforms -- [ ] T5.5 — Update docs (ARCHITECTURE.md, TESTS.md, README) +- [x] T5.5 — Update docs (ARCHITECTURE.md, TESTS.md, README) --- diff --git a/docs/TESTS.md b/docs/TESTS.md index 843e1ca..ab66815 100644 --- a/docs/TESTS.md +++ b/docs/TESTS.md @@ -41,13 +41,13 @@ go test -v ./... Run a specific test by name: ```bash -go test -run TestRunJobWritesLogFile ./src/core +go test -run TestRunJobWritesLogFile ./src/runner ``` Run tests with code coverage: ```bash -go test -cover ./src/core -go test -coverprofile=coverage.out ./src/core +go test -cover ./src/runner +go test -coverprofile=coverage.out ./src/runner go tool cover -html=coverage.out ``` @@ -55,106 +55,272 @@ go tool cover -html=coverage.out ## Test Files Overview -### store_test.go -**Location:** `src/core/store_test.go` -**Package:** `core` +### src/domain/schedule_test.go -Tests YAML serialization and storage behavior. +**Package:** `domain` + +Tests schedule parsing and validation. | Test | Purpose | |------|---------| -| `TestJobsYAMLDoesNotPersistRuntimeNoise` | Verifies that `jobs.yaml` does not persist runtime state fields (LastRun, NextRun, LastState, Output, etc.). Only job definitions are stored; runtime data is kept in memory and log files. | +| `TestParseRejectsInvalidSchedules` | Verifies that invalid schedule strings return an error. | +| `TestParseEveryInterval` | Verifies `@every` duration syntax (e.g., `@every 10s`) is parsed and computes the correct next run time. | +| `TestParseEveryTrimsSurroundingWhitespace` | Verifies leading/trailing whitespace around the `@every` spec is ignored. | +| `TestParseCronExpression` | Verifies 5-field cron expressions (e.g., `*/5 * * * *`) are parsed and compute the correct next run time. | +| `TestParseCronDescriptor` | Verifies predefined cron descriptors such as `@hourly` are accepted. | +| `TestValidateAcceptsValidSchedules` | Verifies that `Schedule.Validate` returns nil for valid schedule strings. | +| `TestZeroScheduleNextIsZero` | Verifies that a zero-value Schedule returns a zero time from `Next`. | +| `TestStringReturnsTrimmedSpec` | Verifies that `Schedule.String` returns the trimmed schedule spec. | --- -### scheduler_test.go -**Location:** `src/core/scheduler_test.go` -**Package:** `core` +### src/app/service_test.go -Tests schedule parsing and job invocation output formatting. +**Package:** `app` + +Tests `Service` construction and the state-accessor contract. | Test | Purpose | |------|---------| -| `TestNextRunTimeSupportsEvery` | Verifies `@every` duration syntax (e.g., `@every 10s`) correctly calculates next run time. Tests with 10-second interval. | -| `TestNextRunTimeSupportsCron` | Verifies standard 5-field cron expressions (e.g., `*/5 * * * *`) correctly calculate next run time. Tests 5-minute interval. | -| `TestRunningOutputIncludesInvocation` | Verifies the running job output header includes all relevant invocation details: command, arguments, success exit codes, start time, and trigger type. | +| `TestNewServiceBuildsRuntimePerJob` | Verifies that `NewService` creates a `JobRuntime` entry for every loaded job. | +| `TestJobsReturnsCopy` | Verifies that `Service.Jobs` returns a defensive copy so callers cannot mutate internal state. | +| `TestStoreReturnsWiredStore` | Verifies that `Service.Store` returns the injected `storage.Store`. | --- -### runner_test.go -**Location:** `src/core/runner_test.go` -**Package:** `core` +### src/app/operations_test.go + +**Package:** `app` + +Tests all mutating operations on the Service, scheduler integration, and settings persistence. + +#### Job CRUD + +| Test | Purpose | +|------|---------| +| `TestCreateJobAssignsIDAndEmits` | Verifies that `CreateJob` assigns a unique ID, persists to YAML, and emits `JobChanged`. | +| `TestCreateJobValidates` | Verifies that `CreateJob` rejects jobs with an invalid schedule. | +| `TestUpdateJobKeepsRuntimeAndReflectsDisable` | Verifies that `UpdateJob` preserves existing runtime state and disables a job correctly. | +| `TestUpdateJobReenablesPausedJob` | Verifies that re-enabling a previously-disabled job clears the paused runtime state. | +| `TestRuntimeLazilyRecreated` | Verifies that `UpdateJob` recreates a missing runtime entry rather than panicking. | +| `TestUpdateJobNotFound` | Verifies that `UpdateJob` returns an error for an unknown job ID. | +| `TestDeleteJobRemovesEverything` | Verifies that `DeleteJob` removes the job from the slice, the runtime map, and the schedule cache. | +| `TestDeleteJobNotFound` | Verifies that `DeleteJob` returns an error for an unknown job ID. | +| `TestSetEnabledNotFound` | Verifies that `SetEnabled` returns an error for an unknown job ID. | +| `TestSetEnabledToggles` | Verifies that `SetEnabled` flips the enabled flag and persists the change. | + +#### Global pause / run-now / run-due + +| Test | Purpose | +|------|---------| +| `TestSetGlobalPauseUpdatesRuntimesAndEmits` | Verifies that `SetGlobalPause` updates all job runtimes, emits `SchedulerStateChanged`, and persists state. | +| `TestRunNowUsesRunnerAndRecords` | Verifies that `RunNow` invokes the runner, records a `RunRecord`, and emits `RunRecorded`. | +| `TestRunNowNotFound` | Verifies that `RunNow` returns an error for an unknown job ID. | +| `TestRunNowRefusedWhileAlreadyRunning` | Verifies that a second concurrent `RunNow` on the same job is rejected while the first is in progress. | +| `TestRunNowRefusedWhilePaused` | Verifies that `RunNow` is rejected when the global pause flag is set. | +| `TestRunDueStartsDueJob` | Verifies that `RunDue` launches a job whose next-run time has passed. | +| `TestRunDueSkipsJobNotYetDue` | Verifies that `RunDue` does not launch a job that is not yet due. | +| `TestRunDueSkipsJobInRunningState` | Verifies that `RunDue` does not start a second concurrent run for an already-running job. | +| `TestRunDueDoesNothingWhilePaused` | Verifies that `RunDue` launches nothing when the global pause flag is set. | +| `TestStartDrivesRunDueOnTick` | Verifies that `Service.Start` wires `RunDue` to the scheduler tick and that each tick advances state. | + +#### Settings + +| Test | Purpose | +|------|---------| +| `TestUpdateSettingsPersistsAndValidates` | Verifies that `UpdateSettings` persists a valid config and rewrites autostart if needed. | +| `TestUpdateSettingsRejectsInvalidConfigs` | Verifies that `UpdateSettings` returns validation errors without persisting. | +| `TestPrependLogCapsActivityList` | Verifies that the activity log never grows beyond its maximum cap. | + +--- + +### src/app/events_test.go + +**Package:** `app` + +Tests the event-emission and observer-subscription machinery. + +| Test | Purpose | +|------|---------| +| `TestEmitDeliversToAllObserversInOrder` | Verifies that all registered observers receive emitted events in registration order. | +| `TestEmitWithNoObserversIsNoop` | Verifies that emitting an event with no observers does not panic. | +| `TestObserverCanReadServiceState` | Verifies that an observer called by `emit` can safely read Service state (jobs, runtimes). | + +--- + +### src/app/format_test.go + +**Package:** `app` + +Tests display-formatting helpers used by the UI. + +| Test | Purpose | +|------|---------| +| `TestStatusText` | Verifies that job status codes map to the correct display strings. | +| `TestEventText` | Verifies trigger-type labels for scheduled, manual, and UI triggers. | +| `TestDisplayFolder` | Verifies that an empty folder string shows "No folder". | +| `TestDisplayArguments` | Verifies that an empty arguments string shows "None". | +| `TestDisplaySuccessExitCodes` | Verifies that an empty exit-codes string shows the default "0". | +| `TestDisplayRunMode` | Verifies run-mode labels for normal and start-only modes. | +| `TestDisplayInvocation` | Verifies that the full invocation display string includes command, arguments, and exit codes. | +| `TestDisplayIndex` | Verifies that the display index is one-based (job slice index + 1). | + +--- + +### src/storage/store_test.go + +**Package:** `storage` + +Tests YAML round-tripping, migration, and default generation. + +| Test | Purpose | +|------|---------| +| `TestJobsRoundTrip` | Verifies that jobs saved to YAML are reloaded with identical field values. | +| `TestConfigRoundTrip` | Verifies that settings saved to YAML are reloaded with identical field values. | +| `TestNormalizeJobsFillsDefaults` | Verifies that `normalizeJobs` assigns sequential IDs and sets default enabled state for jobs missing those fields. | +| `TestLoadOrCreateConfigMigratesFromLegacy` | Verifies that the old flat-config format is migrated to the current `gosentry.yaml` + `jobs_dir` layout on first load. | +| `TestLoadOrCreateConfigCreatesDefaultsOnFirstRun` | Verifies that a missing config file is created with sane defaults and a sample job. | +| `TestJobsYAMLDoesNotPersistRuntimeNoise` | Verifies that `jobs.yaml` does not persist runtime state (LastRun, NextRun, etc.). Only durable job fields are stored. | + +--- + +### src/scheduler/scheduler_test.go + +**Package:** `scheduler` + +Tests the timing-loop contract using a fake clock. + +| Test | Purpose | +|------|---------| +| `TestSchedulerCallsTickWithClockNow` | Verifies that the scheduler calls the injected tick function with the wall-clock time returned by the fake Clock. | +| `TestSchedulerStopReleasesClock` | Verifies that `Stop` terminates the tick loop and releases the Clock without hanging. | + +--- + +### src/runner/runner_test.go + +**Package:** `runner` Tests command execution, exit code handling, output capture, and Windows-specific process behavior. -#### Log File Tests +#### Log file tests | Test | Purpose | |------|---------| -| `TestRunJobWritesLogFile` | Verifies that each job execution creates a `.log` file in the configured logs directory with sanitized job name in filename and proper metadata (trigger type, job name, command output). | +| `TestRunJobLogFileAllHeaders` | Verifies that the log file contains all expected metadata headers: trigger type, job name, command, arguments, and start time. | +| `TestRunJobRecordFields` | Verifies that the returned `RunRecord` carries the correct status, trigger, and log-file path. | +| `TestRunJobWritesLogFile` | Verifies that each job execution creates a `.log` file in the configured logs directory with a sanitized job name in the filename. | -#### Command Execution Tests +#### Output formatting + +| Test | Purpose | +|------|---------| +| `TestFormatOutput` | Verifies that stdout and stderr are combined with section labels in the formatted output. | +| `TestFormatOutputEmptyStreams` | Verifies that empty stdout/stderr streams are omitted from the formatted output. | +| `TestLogArguments` | Verifies that arguments are included in the log header and absent when the arguments field is empty. | +| `TestSanitizeFileName` | Verifies that spaces and special characters in a job name are replaced to produce a safe filename segment. | + +#### Command execution | Test | Platform | Purpose | |------|----------|---------| -| `TestRunJobRunsQuotedWindowsExecutable` | Windows | Verifies that executable paths with quotes (e.g., `"C:\Program Files\..."`) are executed correctly via cmd.exe. | -| `TestRunJobRunsUnquotedWindowsProgramPathWithSpaces` | Windows | Verifies that unquoted executable paths with spaces (e.g., `C:\Program Files\App\app.exe`) are quoted and executed correctly. | -| `TestRunJobRunsWindowsCommandWithSeparateArguments` | Windows | Verifies that command and arguments separated in the Job struct are combined and executed correctly. | +| `TestRunJobRunsQuotedWindowsExecutable` | Windows | Verifies that executable paths with quotes are executed correctly via `cmd.exe`. | +| `TestRunJobRunsUnquotedWindowsProgramPathWithSpaces` | Windows | Verifies that unquoted executable paths with spaces are quoted and executed correctly. | +| `TestRunJobRunsWindowsCommandWithSeparateArguments` | Windows | Verifies that command and arguments from the Job struct are combined and executed correctly. | -#### Exit Code Handling Tests +#### Exit code handling | Test | Purpose | |------|---------| -| `TestRunJobAcceptsConfiguredExitCode` | Verifies that exit codes listed in `SuccessExitCodes` (e.g., `"0,1"`) result in "OK" status even if nonzero. Includes detail message about accepted exit code. | -| `TestRunJobRejectsUnconfiguredExitCode` | Verifies that exit codes not listed in `SuccessExitCodes` result in "Failed" status with exit code detail. | +| `TestRunJobAcceptsConfiguredExitCode` | Verifies that exit codes in `SuccessExitCodes` result in "OK" status even when nonzero. | +| `TestRunJobRejectsUnconfiguredExitCode` | Verifies that exit codes absent from `SuccessExitCodes` result in "Failed" status. | -#### Start-Only Mode Tests +#### Start-only mode | Test | Purpose | |------|---------| -| `TestRunJobStartOnlyDoesNotWaitForExitCode` | Verifies that jobs with `StartOnly: true` launch the process and return "OK" immediately without waiting for process exit or checking exit code. | -| `TestRunJobStartOnlyReportsStartFailure` | Verifies that jobs with `StartOnly: true` still report "Failed" if the process fails to start (e.g., executable not found). | +| `TestRunJobStartOnlyDoesNotWaitForExitCode` | Verifies that `StartOnly: true` jobs launch and return "OK" immediately without waiting for the process to exit. | +| `TestRunJobStartOnlyReportsStartFailure` | Verifies that `StartOnly: true` jobs still report "Failed" if the process cannot be started. | -#### Utility Function Tests +#### Utility / Windows invocation | Test | Platform | Purpose | |------|----------|---------| -| `TestParseExitCodes` | All | Verifies that exit code strings with mixed separators (comma, semicolon, newline) are correctly parsed into integer slice. | -| `TestDirectCommandDoesNotHideWindow` | Windows | Verifies that direct executable commands (with explicit path and arguments) do not request hidden window startup. | -| `TestShellCommandHidesWindow` | Windows | Verifies that shell commands (passed to cmd.exe) request hidden window startup to prevent console flash. | -| `TestShellCommandUsesWindowsSafeQuoting` | Windows | Verifies that shell commands use cmd.exe `/S /C` syntax with proper outer quoting to handle paths with spaces and special characters. | +| `TestParseExitCodes` | All | Verifies that mixed-separator exit-code strings (comma, semicolon, newline) are parsed correctly. | +| `TestDirectCommandDoesNotHideWindow` | Windows | Verifies that direct executable commands do not request hidden-window startup. | +| `TestShellCommandHidesWindow` | Windows | Verifies that shell commands request hidden-window startup to prevent console flash. | +| `TestShellCommandUsesWindowsSafeQuoting` | Windows | Verifies `cmd.exe /S /C` quoting for paths with spaces and special characters. | | `TestWindowsShellCommandLineQuotesUnquotedProgramPath` | Windows | Verifies that unquoted program paths in shell commands are quoted while preserving already-quoted arguments. | --- -### autostart_windows_test.go -**Location:** `src/core/autostart_windows_test.go` -**Package:** `core` -**Build Tags:** `//go:build windows` (Windows only) +### src/runner/cleanup_test.go -Tests Windows autostart entry creation via shortcuts in the Startup folder. +**Package:** `runner` + +Tests log-file cleanup by age and by count. | Test | Purpose | |------|---------| -| `TestParseRegistryRunValue` | Verifies that legacy Windows Registry `Run` entry values are correctly parsed from `reg query` output (for migration/cleanup). | -| `TestSameWindowsPathIgnoresCaseAndQuotes` | Verifies that Windows path comparison is case-insensitive and handles quote marks correctly (e.g., `"D:\..."` matches `d:\...`). | -| `TestSameWindowsPathHandlesSpaces` | Verifies that Windows path comparison correctly matches paths with spaces both with and without quotes. | -| `TestStartupShortcutPathUsesUserStartupFolder` | Verifies that the startup shortcut path resolves to the user's Startup folder using `%APPDATA%` environment variable. | -| `TestCreateStartupShortcutHandlesSpaces` | Verifies that `.lnk` shortcut files are created with correct `TargetPath` and `Arguments` (--start-in-tray) even when target path contains spaces. | +| `TestCleanupLogsMissingDirReturnsNil` | Verifies that cleanup returns nil (not an error) when the logs directory does not exist. | +| `TestCleanupLogsRemovesFilesPastMaxAge` | Verifies that `.log` files older than `MaxLogAgeDays` are deleted. | +| `TestCleanupLogsKeepsFilesWithinAgeLimit` | Verifies that `.log` files within the age limit are retained. | +| `TestCleanupLogsByCountDeletesOldest` | Verifies that when file count exceeds `MaxLogFiles`, the oldest files are removed first. | +| `TestCleanupLogsNonLogFilesNotDeleted` | Verifies that non-`.log` files in the logs directory are never deleted by cleanup. | +| `TestCleanupLogsSubdirsNotDeleted` | Verifies that subdirectories inside the logs directory are not deleted by cleanup. | +| `TestCleanupLogsZeroLimitsDisableBothPolicies` | Verifies that setting both limits to zero disables both the age and count cleanup policies. | --- -### autostart_linux_test.go -**Location:** `src/core/autostart_linux_test.go` -**Package:** `core` -**Build Tags:** `//go:build linux` (Linux only) +### src/platform/autostart/autostart_windows_test.go -Tests Linux autostart entry creation via XDG Desktop Entry files. +**Location:** `src/platform/autostart/autostart_windows_test.go` +**Build Tags:** `//go:build windows` + +Tests Windows autostart via shortcuts in the Startup folder. | Test | Purpose | |------|---------| -| `TestLinuxAutostartStartsInTray` | Verifies that the XDG Desktop Entry is created with the `--start-in-tray` argument in the `Exec=` field, so scheduled jobs run immediately after login without displaying the window. | -| `TestLinuxAutostartRemovesLegacyDesktopEntry` | Verifies that legacy autostart entries (from old PySentry implementation) are cleaned up when enabling autostart through the new system. | +| `TestParseRegistryRunValue` | Verifies that legacy `HKCU\...\Run` entry values are parsed correctly from `reg query` output (for migration/cleanup). | +| `TestSameWindowsPathIgnoresCaseAndQuotes` | Verifies that Windows path comparison is case-insensitive and handles quote marks correctly. | +| `TestSameWindowsPathHandlesSpaces` | Verifies that Windows path comparison matches paths with and without surrounding quotes. | +| `TestSameWindowsPathStripsExtendedLengthPrefix` | Verifies that `\\?\`-prefixed paths are compared correctly after stripping the prefix. | +| `TestSameWindowsPathMatchesShortNameViaFilesystem` | Verifies that 8.3 short names are resolved to long names for comparison. | +| `TestStartupShortcutPathUsesUserStartupFolder` | Verifies that the shortcut path resolves into `%APPDATA%\Microsoft\Windows\Start Menu\Programs\Startup`. | +| `TestCreateStartupShortcutHandlesCyrillicPath` | Verifies that `.lnk` files are created correctly when the executable path contains Cyrillic characters. | +| `TestCreateStartupShortcutHandlesSpaces` | Verifies that `.lnk` files are created with correct `TargetPath` and `--start-in-tray` arguments when the path contains spaces. | + +--- + +### src/platform/autostart/autostart_linux_test.go + +**Location:** `src/platform/autostart/autostart_linux_test.go` +**Build Tags:** `//go:build linux` + +Tests Linux autostart via XDG Desktop Entry files. + +| Test | Purpose | +|------|---------| +| `TestLinuxAutostartStartsInTray` | Verifies that the XDG Desktop Entry is created with `--start-in-tray` in the `Exec=` field. | +| `TestLinuxAutostartRemovesLegacyDesktopEntry` | Verifies that enabling autostart also removes legacy PySentry service files left by earlier builds. | + +--- + +### src/ui/jobs_view_test.go + +**Package:** `ui` + +Tests pure helper functions in the jobs view (no Fyne widget construction). + +| Test | Purpose | +|------|---------| +| `TestFilterValue` | Verifies that `filterValue` returns the correct display string for the current folder filter. | +| `TestFolderOptionsAlwaysIncludesSentinels` | Verifies that the folder filter list always starts with "All" and "No folder" sentinel entries. | +| `TestFolderOptionsAppendsUniqueFolders` | Verifies that folder names from the job list are appended once each, in order, without duplicates. | +| `TestFilteredJobIndexesAll` | Verifies that the "All" filter returns indexes for every job. | +| `TestFilteredJobIndexesByNamedFolder` | Verifies that filtering by a named folder returns only jobs in that folder. | +| `TestFilteredJobIndexesNoFolder` | Verifies that the "No folder" filter returns only jobs with an empty folder field. | +| `TestFilteredJobIndexesEmptySlice` | Verifies that filtering an empty job slice returns an empty index list. | --- @@ -164,15 +330,19 @@ Tests Linux autostart entry creation via XDG Desktop Entry files. 2. **Cross-platform** — Platform-specific tests use `//go:build` tags and `runtime.GOOS` checks to skip when not applicable. -3. **Exit Code Flexibility** — The `SuccessExitCodes` field allows jobs to treat nonzero exit codes as success, tested explicitly. +3. **Fake clocks and runners** — The scheduler is exercised with an injected fake `Clock`; the service operations tests inject a fake `runJob` function to avoid spawning real processes. -4. **Path Handling** — Extensive tests cover Windows path quoting, spaces in paths, and case-insensitive matching to avoid subtle shell escaping bugs. +4. **Event-driven correctness** — `app` tests subscribe to the event bus and assert that the expected events are emitted, rather than inspecting internal fields directly. -5. **Start-Only Mode** — Special handling for long-running processes that should be launched but not waited on, tested separately from normal execution flow. +5. **Exit Code Flexibility** — The `SuccessExitCodes` field allows jobs to treat nonzero exit codes as success, tested explicitly. + +6. **Path Handling** — Extensive tests cover Windows path quoting, spaces in paths, and case-insensitive matching to avoid subtle shell escaping bugs. + +7. **Start-Only Mode** — Special handling for long-running processes that should be launched but not waited on, tested separately from normal execution flow. --- ## Remaining Test Coverage Gaps -- GUI integration tests — Fyne widget interaction is not yet tested -- Job history and run record storage — on-disk run-record retrieval not covered +- GUI integration tests — Fyne widget interaction is not yet tested end-to-end +- Job history on-disk retrieval — RunRecord file reading is not covered