Refactoring complete: v0.4.0 architectural milestone #1
@@ -352,14 +352,21 @@ Windows:
|
|||||||
|
|
||||||
## Project Layout
|
## Project Layout
|
||||||
|
|
||||||
- `cmd/gosentry` starts the desktop app.
|
- `cmd/gosentry` — entry point; starts the desktop app.
|
||||||
- `src/gui` contains the GUI.
|
- `src/domain` — pure value types: `Job`, `Config`, `RunRecord`, `Schedule`, `JobRuntime`.
|
||||||
- `src/core` contains YAML storage, command execution, scheduling, and log cleanup.
|
- `src/app` — `Service`: sole owner of job and runtime state; emits typed events to the UI.
|
||||||
- `assets` contains app icons that are embedded into the application binary.
|
- `src/scheduler` — pure timing loop; calls `Service.RunDue` on every tick.
|
||||||
- `scripts` contains build helpers.
|
- `src/runner` — shell command execution, log file writing, and log cleanup.
|
||||||
- `docs` contains architecture notes, the changelog, and the roadmap.
|
- `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
|
## Dependencies
|
||||||
|
|
||||||
|
|||||||
+72
-41
@@ -1,73 +1,104 @@
|
|||||||
# GoSentry Architecture
|
# GoSentry Architecture
|
||||||
|
|
||||||
This document shows the current component interaction model. GoSentry is still a
|
This document shows the current component interaction model. GoSentry is a
|
||||||
single desktop process: the GUI, scheduler, storage, and command runner live in
|
single desktop process: the GUI, application service, scheduler, storage, and
|
||||||
one application and communicate through Go function calls and shared in-memory
|
command runner live in one application. They communicate through typed events
|
||||||
job state.
|
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
|
## Component Diagram
|
||||||
|
|
||||||
```mermaid
|
```mermaid
|
||||||
flowchart LR
|
flowchart LR
|
||||||
user["Desktop user"]
|
user["Desktop user"]
|
||||||
gui["src/gui - Fyne windows, tabs, dialogs"]
|
ui["src/ui\nFyne windows, tabs, dialogs"]
|
||||||
store["src/core Store - YAML config and jobs"]
|
svc["src/app Service\nsole owner of job + runtime state"]
|
||||||
scheduler["src/core Scheduler - @every and cron timing"]
|
store["src/storage Store\nYAML config and jobs"]
|
||||||
runner["src/core Runner - shell command execution"]
|
sched["src/scheduler Scheduler\npure timing loop"]
|
||||||
autostart["src/core Autostart - Windows Startup shortcut / Linux desktop startup"]
|
runner["src/runner\nshell command execution"]
|
||||||
config["gosentry.yaml - application settings"]
|
autostart["src/platform/autostart Manager\nWindows shortcut / Linux XDG"]
|
||||||
jobs["jobs.yaml - job definitions"]
|
config["gosentry.yaml\napplication settings"]
|
||||||
logs["logs_dir - per-run command output logs"]
|
jobs["jobs.yaml\njob definitions"]
|
||||||
shell["Platform shell - cmd.exe /C or sh -c"]
|
logs["logs_dir\nper-run command output logs"]
|
||||||
|
shell["Platform shell\ncmd.exe /C or sh -c"]
|
||||||
|
|
||||||
user -->|"edits jobs, settings, runs commands"| gui
|
user -->|"edits jobs, settings, runs commands"| ui
|
||||||
gui -->|"OpenStore, SaveConfig, SaveJobs"| store
|
ui -->|"CreateJob, UpdateJob, DeleteJob, RunNow, UpdateSettings, …"| svc
|
||||||
|
svc -->|"SaveJobs, SaveConfig, LoadJobs, LoadConfig"| store
|
||||||
store -->|"read/write"| config
|
store -->|"read/write"| config
|
||||||
store -->|"read/write"| jobs
|
store -->|"read/write"| jobs
|
||||||
|
|
||||||
gui -->|"Start, Pause, RunNow, RefreshSchedule"| scheduler
|
svc -->|"Start(RunDue)"| sched
|
||||||
scheduler -->|"SaveJobs after state changes"| store
|
sched -->|"RunDue(now)"| svc
|
||||||
scheduler -->|"RunJob(trigger)"| runner
|
svc -->|"RunJob"| runner
|
||||||
runner -->|"execute command"| shell
|
runner -->|"execute command"| shell
|
||||||
runner -->|"write stdout/stderr log"| logs
|
runner -->|"write stdout/stderr log"| logs
|
||||||
runner -->|"RunRecord with status, duration, log path"| scheduler
|
runner -->|"RunRecord"| svc
|
||||||
scheduler -->|"onChange RunRecord"| gui
|
svc -->|"emit JobChanged / RunRecorded / ErrorOccurred"| ui
|
||||||
gui -->|"display History, command output, job state"| user
|
ui -->|"display jobs, history, status"| user
|
||||||
|
|
||||||
gui -->|"SetAutostart, AutostartStatus"| autostart
|
ui -->|"SetAutostart, AutostartStatus"| autostart
|
||||||
autostart -->|"use executable path from resolved Paths"| config
|
svc -->|"Set / Status via Manager"| autostart
|
||||||
```
|
```
|
||||||
|
|
||||||
## Main Flows
|
## Main Flows
|
||||||
|
|
||||||
1. Startup:
|
1. Startup:
|
||||||
The executable starts `cmd/gosentry`, which calls the GUI package. The GUI
|
`cmd/gosentry` calls `ui.Run`, which creates an `app.Service`, opens the
|
||||||
opens the store, loads `gosentry.yaml` and `jobs.yaml`, creates the main tabs,
|
store, loads `gosentry.yaml` and `jobs.yaml`, subscribes the UI to service
|
||||||
then starts the scheduler with the loaded job slice.
|
events, builds the main window, and calls `Service.Start` to begin the
|
||||||
|
scheduler loop.
|
||||||
|
|
||||||
2. Editing settings or jobs:
|
2. Editing settings or jobs:
|
||||||
The GUI updates the in-memory job/config state and asks `Store` to write YAML
|
The UI calls mutating methods on `app.Service` (e.g. `CreateJob`,
|
||||||
back to disk. Job definitions stay in one `jobs.yaml`; runtime command output
|
`UpdateJob`, `UpdateSettings`). The Service validates the request, updates
|
||||||
is not stored there.
|
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:
|
3. Scheduled run:
|
||||||
`Scheduler` checks due jobs on a one-second ticker. When a job is due, it marks
|
`scheduler.Scheduler` fires a tick every second. On each tick it calls
|
||||||
the job as running, saves state, and starts `Runner` asynchronously.
|
`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:
|
4. Manual run:
|
||||||
`Run now` calls the same scheduler path as scheduled execution, but the
|
`Run now` in the UI calls `Service.RunNow`. The Service checks that the job
|
||||||
resulting history record uses the `Manual` trigger.
|
exists, is not already running, and that the scheduler is not paused, then
|
||||||
|
executes `runner.RunJob` with the `Manual` trigger.
|
||||||
|
|
||||||
5. Command execution:
|
5. Command execution:
|
||||||
`Runner` executes the command through the platform shell, captures stdout and
|
`runner.RunJob` builds the platform-specific invocation, executes the
|
||||||
stderr, writes one timestamped `.log` file, and returns a `RunRecord`.
|
command through the platform shell, captures stdout and stderr, writes one
|
||||||
|
timestamped `.log` file, and returns a `domain.RunRecord`.
|
||||||
|
|
||||||
6. History update:
|
6. History update:
|
||||||
The scheduler receives the `RunRecord`, updates the matching job, saves YAML,
|
When a run goroutine completes, `Service` updates the job's runtime, saves
|
||||||
runs log cleanup, and calls the GUI callback so the `History` tab refreshes.
|
YAML, triggers log cleanup, and emits `RunRecorded`. The UI observer appends
|
||||||
|
the record to the History tab.
|
||||||
|
|
||||||
7. Autostart:
|
7. Autostart:
|
||||||
The Settings tab calls the platform autostart implementation. Windows uses a
|
`UpdateSettings` in the Service calls `autostart.Manager.Set`. The Manager
|
||||||
shortcut in the current user's Startup folder. Linux uses a desktop-session
|
interface has two implementations: Windows writes a `.lnk` shortcut to the
|
||||||
startup entry. Both autostart mechanisms pass `--start-in-tray`, so the
|
user Startup folder; Linux writes an XDG Autostart `.desktop` file. Both
|
||||||
scheduler starts without opening the main window after sign-in.
|
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.
|
||||||
|
|||||||
+1
-1
@@ -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.2 — Introduce `autostart.Manager` interface
|
||||||
- [x] T5.3 — Fill test gaps (folder filtering, cleanup, migration, concurrency)
|
- [x] T5.3 — Fill test gaps (folder filtering, cleanup, migration, concurrency)
|
||||||
- [x] T5.4 — Run `go test -race ./...` clean on both platforms
|
- [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)
|
||||||
|
|
||||||
---
|
---
|
||||||
|
|
||||||
|
|||||||
+227
-57
@@ -41,13 +41,13 @@ go test -v ./...
|
|||||||
|
|
||||||
Run a specific test by name:
|
Run a specific test by name:
|
||||||
```bash
|
```bash
|
||||||
go test -run TestRunJobWritesLogFile ./src/core
|
go test -run TestRunJobWritesLogFile ./src/runner
|
||||||
```
|
```
|
||||||
|
|
||||||
Run tests with code coverage:
|
Run tests with code coverage:
|
||||||
```bash
|
```bash
|
||||||
go test -cover ./src/core
|
go test -cover ./src/runner
|
||||||
go test -coverprofile=coverage.out ./src/core
|
go test -coverprofile=coverage.out ./src/runner
|
||||||
go tool cover -html=coverage.out
|
go tool cover -html=coverage.out
|
||||||
```
|
```
|
||||||
|
|
||||||
@@ -55,106 +55,272 @@ go tool cover -html=coverage.out
|
|||||||
|
|
||||||
## Test Files Overview
|
## Test Files Overview
|
||||||
|
|
||||||
### store_test.go
|
### src/domain/schedule_test.go
|
||||||
**Location:** `src/core/store_test.go`
|
|
||||||
**Package:** `core`
|
|
||||||
|
|
||||||
Tests YAML serialization and storage behavior.
|
**Package:** `domain`
|
||||||
|
|
||||||
|
Tests schedule parsing and validation.
|
||||||
|
|
||||||
| Test | Purpose |
|
| 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
|
### src/app/service_test.go
|
||||||
**Location:** `src/core/scheduler_test.go`
|
|
||||||
**Package:** `core`
|
|
||||||
|
|
||||||
Tests schedule parsing and job invocation output formatting.
|
**Package:** `app`
|
||||||
|
|
||||||
|
Tests `Service` construction and the state-accessor contract.
|
||||||
|
|
||||||
| Test | Purpose |
|
| Test | Purpose |
|
||||||
|------|---------|
|
|------|---------|
|
||||||
| `TestNextRunTimeSupportsEvery` | Verifies `@every` duration syntax (e.g., `@every 10s`) correctly calculates next run time. Tests with 10-second interval. |
|
| `TestNewServiceBuildsRuntimePerJob` | Verifies that `NewService` creates a `JobRuntime` entry for every loaded job. |
|
||||||
| `TestNextRunTimeSupportsCron` | Verifies standard 5-field cron expressions (e.g., `*/5 * * * *`) correctly calculate next run time. Tests 5-minute interval. |
|
| `TestJobsReturnsCopy` | Verifies that `Service.Jobs` returns a defensive copy so callers cannot mutate internal state. |
|
||||||
| `TestRunningOutputIncludesInvocation` | Verifies the running job output header includes all relevant invocation details: command, arguments, success exit codes, start time, and trigger type. |
|
| `TestStoreReturnsWiredStore` | Verifies that `Service.Store` returns the injected `storage.Store`. |
|
||||||
|
|
||||||
---
|
---
|
||||||
|
|
||||||
### runner_test.go
|
### src/app/operations_test.go
|
||||||
**Location:** `src/core/runner_test.go`
|
|
||||||
**Package:** `core`
|
**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.
|
Tests command execution, exit code handling, output capture, and Windows-specific process behavior.
|
||||||
|
|
||||||
#### Log File Tests
|
#### Log file tests
|
||||||
|
|
||||||
| Test | Purpose |
|
| 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 |
|
| Test | Platform | Purpose |
|
||||||
|------|----------|---------|
|
|------|----------|---------|
|
||||||
| `TestRunJobRunsQuotedWindowsExecutable` | Windows | Verifies that executable paths with quotes (e.g., `"C:\Program Files\..."`) are executed correctly via cmd.exe. |
|
| `TestRunJobRunsQuotedWindowsExecutable` | Windows | Verifies that executable paths with quotes 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. |
|
| `TestRunJobRunsUnquotedWindowsProgramPathWithSpaces` | Windows | Verifies that unquoted executable paths with spaces are quoted and executed correctly. |
|
||||||
| `TestRunJobRunsWindowsCommandWithSeparateArguments` | Windows | Verifies that command and arguments separated in the Job struct are combined 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 |
|
| 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. |
|
| `TestRunJobAcceptsConfiguredExitCode` | Verifies that exit codes in `SuccessExitCodes` result in "OK" status even when nonzero. |
|
||||||
| `TestRunJobRejectsUnconfiguredExitCode` | Verifies that exit codes not listed in `SuccessExitCodes` result in "Failed" status with exit code detail. |
|
| `TestRunJobRejectsUnconfiguredExitCode` | Verifies that exit codes absent from `SuccessExitCodes` result in "Failed" status. |
|
||||||
|
|
||||||
#### Start-Only Mode Tests
|
#### Start-only mode
|
||||||
|
|
||||||
| Test | Purpose |
|
| 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. |
|
| `TestRunJobStartOnlyDoesNotWaitForExitCode` | Verifies that `StartOnly: true` jobs launch and return "OK" immediately without waiting for the process to exit. |
|
||||||
| `TestRunJobStartOnlyReportsStartFailure` | Verifies that jobs with `StartOnly: true` still report "Failed" if the process fails to start (e.g., executable not found). |
|
| `TestRunJobStartOnlyReportsStartFailure` | Verifies that `StartOnly: true` jobs still report "Failed" if the process cannot be started. |
|
||||||
|
|
||||||
#### Utility Function Tests
|
#### Utility / Windows invocation
|
||||||
|
|
||||||
| Test | Platform | Purpose |
|
| Test | Platform | Purpose |
|
||||||
|------|----------|---------|
|
|------|----------|---------|
|
||||||
| `TestParseExitCodes` | All | Verifies that exit code strings with mixed separators (comma, semicolon, newline) are correctly parsed into integer slice. |
|
| `TestParseExitCodes` | All | Verifies that mixed-separator exit-code strings (comma, semicolon, newline) are parsed correctly. |
|
||||||
| `TestDirectCommandDoesNotHideWindow` | Windows | Verifies that direct executable commands (with explicit path and arguments) do not request hidden window startup. |
|
| `TestDirectCommandDoesNotHideWindow` | Windows | Verifies that direct executable commands do not request hidden-window startup. |
|
||||||
| `TestShellCommandHidesWindow` | Windows | Verifies that shell commands (passed to cmd.exe) request hidden window startup to prevent console flash. |
|
| `TestShellCommandHidesWindow` | Windows | Verifies that shell commands 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. |
|
| `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. |
|
| `TestWindowsShellCommandLineQuotesUnquotedProgramPath` | Windows | Verifies that unquoted program paths in shell commands are quoted while preserving already-quoted arguments. |
|
||||||
|
|
||||||
---
|
---
|
||||||
|
|
||||||
### autostart_windows_test.go
|
### src/runner/cleanup_test.go
|
||||||
**Location:** `src/core/autostart_windows_test.go`
|
|
||||||
**Package:** `core`
|
|
||||||
**Build Tags:** `//go:build windows` (Windows only)
|
|
||||||
|
|
||||||
Tests Windows autostart entry creation via shortcuts in the Startup folder.
|
**Package:** `runner`
|
||||||
|
|
||||||
|
Tests log-file cleanup by age and by count.
|
||||||
|
|
||||||
| Test | Purpose |
|
| Test | Purpose |
|
||||||
|------|---------|
|
|------|---------|
|
||||||
| `TestParseRegistryRunValue` | Verifies that legacy Windows Registry `Run` entry values are correctly parsed from `reg query` output (for migration/cleanup). |
|
| `TestCleanupLogsMissingDirReturnsNil` | Verifies that cleanup returns nil (not an error) when the logs directory does not exist. |
|
||||||
| `TestSameWindowsPathIgnoresCaseAndQuotes` | Verifies that Windows path comparison is case-insensitive and handles quote marks correctly (e.g., `"D:\..."` matches `d:\...`). |
|
| `TestCleanupLogsRemovesFilesPastMaxAge` | Verifies that `.log` files older than `MaxLogAgeDays` are deleted. |
|
||||||
| `TestSameWindowsPathHandlesSpaces` | Verifies that Windows path comparison correctly matches paths with spaces both with and without quotes. |
|
| `TestCleanupLogsKeepsFilesWithinAgeLimit` | Verifies that `.log` files within the age limit are retained. |
|
||||||
| `TestStartupShortcutPathUsesUserStartupFolder` | Verifies that the startup shortcut path resolves to the user's Startup folder using `%APPDATA%` environment variable. |
|
| `TestCleanupLogsByCountDeletesOldest` | Verifies that when file count exceeds `MaxLogFiles`, the oldest files are removed first. |
|
||||||
| `TestCreateStartupShortcutHandlesSpaces` | Verifies that `.lnk` shortcut files are created with correct `TargetPath` and `Arguments` (--start-in-tray) even when target path contains spaces. |
|
| `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
|
### src/platform/autostart/autostart_windows_test.go
|
||||||
**Location:** `src/core/autostart_linux_test.go`
|
|
||||||
**Package:** `core`
|
|
||||||
**Build Tags:** `//go:build linux` (Linux only)
|
|
||||||
|
|
||||||
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 |
|
| 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. |
|
| `TestParseRegistryRunValue` | Verifies that legacy `HKCU\...\Run` entry values are parsed correctly from `reg query` output (for migration/cleanup). |
|
||||||
| `TestLinuxAutostartRemovesLegacyDesktopEntry` | Verifies that legacy autostart entries (from old PySentry implementation) are cleaned up when enabling autostart through the new system. |
|
| `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.
|
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
|
## Remaining Test Coverage Gaps
|
||||||
|
|
||||||
- GUI integration tests — Fyne widget interaction is not yet tested
|
- GUI integration tests — Fyne widget interaction is not yet tested end-to-end
|
||||||
- Job history and run record storage — on-disk run-record retrieval not covered
|
- Job history on-disk retrieval — RunRecord file reading is not covered
|
||||||
|
|||||||
Reference in New Issue
Block a user