Fill the test gaps in the app package, raising coverage from 82% to 97%:
- format_test.go: cover all display helpers moved in T3.5 (StatusText,
EventText, Display*), previously untested.
- operations_test.go: add RunNow not-found and refused-while-running
paths, DeleteJob/SetEnabled not-found errors, UpdateJob re-enable,
runtimeForLocked lazy-recreate, UpdateSettings invalid-config branches,
and prependLog activity-list capping.
All tests are Fyne-free, using a temp-dir store, a fake runner seam, and
a fake Clock. go vet and go test -race pass across the module.
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
T3.6: Add src/app unit tests (no Fyne)
Fill the test gaps in the app package, raising coverage from 82% to 97%:
- format_test.go: cover all display helpers moved in T3.5 (StatusText,
EventText, Display*), previously untested.
- operations_test.go: add RunNow not-found and refused-while-running
paths, DeleteJob/SetEnabled not-found errors, UpdateJob re-enable,
runtimeForLocked lazy-recreate, UpdateSettings invalid-config branches,
and prependLog activity-list capping.
All tests are Fyne-free, using a temp-dir store, a fake runner seam, and
a fake Clock. go vet and go test -race pass across the module.
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
@
Extract pure display/formatting functions from the GUI:
- StatusText, EventText, DisplayFolder, DisplayArguments,
DisplaySuccessExitCodes, DisplayRunMode, DisplayInvocation, DisplayIndex.
These have no Fyne dependencies and serve as a shared formatting layer for
the service to use (e.g. StatusText in the tests when mocking the GUI).
The GUI imports them as app.DisplayFolder(...) etc.
Co-Authored-By: Claude Haiku 4.5 <noreply@anthropic.com>
The scheduler no longer shares a *[]domain.Job with the GUI. It is now a
thin timing loop with an injected Clock that calls a tick callback; the
application service is the sole writer of job and runtime state.
- scheduler: add Clock interface + RealClock (clock.go); strip all job
logic from scheduler.go (NewScheduler(clock, tick)); rewrite tests to
cover the loop with a fake clock.
- app.Service: add RunDue(now) (pause + one-run-per-tick policy, records
back through the service) and Start(Clock)/Stop() owning a cancelable
run context; prime each job's first next-run at construction. Capture
the run context under the lock for executeRun.
- gui: talk only to app.Service (no shared state) — Open() the service,
keep a refreshed snapshot, route every mutation through the service,
and react to changes via a single Subscribe listener.
- Tests: add RunDue (due/not-due/paused) and Start-drives-RunDue cases.
Verified with CGO + MSYS2 UCRT64: go vet ./... clean, go test -race
./... green (GUI included), full module builds.
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Add src/app/operations.go with the seven intents that make the Service
the sole writer of job and runtime state: CreateJob, UpdateJob,
DeleteJob, SetEnabled, RunNow, SetGlobalPause, UpdateSettings. Each
returns error, persists through the store, and announces changes via
RunRecorded/JobChanged/SchedulerStateChanged events.
Extend the Service with a parsed-schedule cache, a global paused flag,
an injectable runJob seam (defaults to runner.RunJob) for testing the
run-now path, and a lifecycle ctx. Run and next-run timing now live in
the Service (duplicating the scheduler temporarily); T3.4 converts the
scheduler to drive the Service and removes the duplication.
Autostart is left to the caller until T5.2's injectable Manager; async
save errors in the run goroutine remain deferred to T5.1. Adds 12 tests
covering create/update/delete, enable/pause, global pause, run-now with
a fake runner, and settings persistence/validation.
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Add src/app/events.go: a sealed Event interface with three concrete
types (JobChanged, RunRecorded, SchedulerStateChanged), an Observer
interface plus ObserverFunc adapter, and Subscribe/emit on the Service.
This replaces the scheduler's single onChange callback with typed
events the UI can exhaustively type-switch over.
Dispatch is serialized by a dedicated dispatchMu (separate from the
state lock): observers never run concurrently, emit must be called
without holding s.mu so observers can read Service state, and observers
must not re-enter an emitting method. emit is wired to mutating ops in
T3.3. Adds tests for ordered multi-observer delivery, empty-observer
no-op, and observer-reads-state-without-deadlock.
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Create src/app/service.go: the application-service layer that becomes
the single owner of the durable jobs slice and the transient runtime
map, guarded by a non-reentrant sync.Mutex. NewService wires a loaded
store; Open() is the convenience entry point. Read-only accessors
(Jobs/Runtime/Store) take the lock, and Jobs() returns a copy to keep
callers from mutating Service-owned state.
State-mutating intents and the event/observer machinery are deferred to
T3.2-T3.4. Adds no-Fyne unit tests for runtime construction, copy
isolation, and store wiring.
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
- Split durable Job from transient JobRuntime
- Extracted Schedule value object
- Made RunJob pure (no job mutation)
- Simplified runtime initialization
No observable behavior changes.
Co-Authored-By: Claude Haiku 4.5 <noreply@anthropic.com>
Move all transient execution state off domain.Job into a new
domain.JobRuntime, keyed by job ID:
- domain: Job now holds only durable YAML fields; remove the yaml:"-"
fields (LastRun/NextRun/LastState/Logs/Output) and NextDue. Add
runtime.go with JobRuntime plus NewRuntime/NewRuntimes constructors,
which now own the runtime-init logic moved out of normalizeJobs.
- runner: RunJob no longer mutates the job; it is pure and returns the
RunRecord for the caller to fold into the runtime.
- scheduler: take a shared map[int]*JobRuntime and route status/next-run
bookkeeping through runtimeFor(job); prepareNextRun writes a *JobRuntime.
- storage: normalizeJobs touches only durable config.
- gui: own the runtime map (NewRuntimes), share it with the scheduler,
and read/write runtime state via runtimeFor; maintain the map by ID on
add/edit/delete.
- tests: update scheduler/storage tests to the split; tidy a pre-existing
import-order nit in scheduler.go.
This also satisfies T2.4 (storage load/save only Job, runtime init in
domain.NewRuntime, round-trip tests), since removing the fields forced it.
Runtime-map ownership remains GUI-side glue until T3.1.
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Parse each job's schedule once on load (resetNextRuns) and on edit
(RefreshSchedule) via the new parseJobSchedule helper, caching the
result in a map[int]domain.Schedule keyed by job ID. prepareNextRun
now looks up the cached Schedule instead of re-parsing the string on
every call. Remove the nextRunTime wrapper that did the per-call
parsing. Drop the three scheduler_test.go tests that duplicated
coverage already in domain/schedule_test.go.
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
Introduce src/domain/schedule.go with a Schedule value object that
centralizes schedule parsing and validation: Parse, Validate, and
Next(time.Time). It owns the cron parser and @every handling, moved out
of the scheduler. The scheduler's nextRunTime is kept as a thin wrapper
delegating to domain.Parse for now (T2.2 will parse once on load/edit).
Add unit tests covering invalid specs, @every intervals, five-field cron,
cron descriptors, whitespace trimming, the zero-value Next, and String.
Mark T2.1 complete in REFACTORING.md.
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Move version.go from src/core to src/app to prepare for deleting the
now-empty src/core package. Update all imports and build script
references to use src/app.Version instead of src/core.Version.
Fix import aliasing conflict between Fyne's desktop package and our
platform/desktop package by aliasing Fyne imports as fyneapp and
fynedesktop.
Co-Authored-By: Claude Haiku 4.5 <noreply@anthropic.com>
Move desktop_linux.go and desktop_other.go from src/core to the new
src/platform/desktop package. Update src/gui to import and use the
new package location.
Co-Authored-By: Claude Haiku 4.5 <noreply@anthropic.com>
Move autostart_*.go and tests from src/core to src/platform/autostart.
Update src/gui/app.go to call autostart.SetAutostart / autostart.AutostartStatus.
Add quoteDesktopExec to desktop_linux.go (was co-located in autostart_linux.go).
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
Move store.go, paths.go, and store_test.go from src/core into the new
src/storage package. Update src/scheduler and src/gui to import storage
instead of core for Store/Paths/OpenStore. Empty the moved files in core
to preserve the package declaration for the remaining core symbols.
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
Move scheduler.go and scheduler_test.go from src/core to the new
src/scheduler package. The scheduler still takes *[]domain.Job and
*core.Store (storage moves in T1.5). Update src/gui/app.go to import
the new package; rename the local variable to sched to avoid shadowing
the scheduler package name.
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
Move runner.go, runner_windows.go, runner_other.go, runner_test.go from
src/core to src/runner (package runner). Split helpers into focused files:
invocation*.go, exitcodes.go, logfile.go, cleanup.go. Export LogArguments
and SuccessExitCodesText for use by scheduler's runningOutput helper.
Update src/core/scheduler.go and src/gui/app.go to import src/runner.
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
Extract the hidden-window logic out of src/core runner and autostart files
into a new platform/winproc package with per-OS build-tag files. All call
sites updated to use winproc.ConfigureHiddenWindow. Builds clean on both
Windows and Linux; all tests pass.
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
Extracts the five domain types out of src/core/model.go into a new
src/domain package (job.go, record.go, config.go). The unexported
nextDue field is promoted to NextDue so it is accessible from core.
All references across src/core, src/gui, and cmd/gosentry are updated
to use domain.TypeName. src/core/model.go is reduced to a bare package
declaration. Windows and Linux cross-compilation both pass; all tests
remain green.
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
Pin current behavior at the three seams that will move during refactoring:
Store (store_test.go):
- TestJobsRoundTrip: all durable Job fields survive a writeYAML→loadOrCreateJobs
cycle; runtime fields (LastRun, LastState, Logs) do not.
- TestConfigRoundTrip: all Config fields survive a writeYAML→loadOrCreateConfig
cycle, including non-default booleans and custom dirs.
- TestNormalizeJobsFillsDefaults: blank jobs get default name/schedule/exitcodes
and the correct LastState/NextRun for enabled vs disabled.
Scheduler (scheduler_test.go):
- TestNextRunTimeRejectsInvalidSchedules: empty, whitespace, bare @every,
invalid/negative/zero durations, invalid cron, out-of-range minute all return false.
- TestPrepareNextRunSetsDisplayString: valid schedule writes NextRun as
"YYYY-MM-DD HH:MM:SS" and sets nextDue to the matching time.Time.
- TestPrepareNextRunSetsInvalidScheduleLabel: bad schedule writes "Invalid
schedule" and zeroes nextDue.
Runner (runner_test.go):
- TestRunJobLogFileAllHeaders: all log header fields are present (job_id,
job_name, trigger, state, detail, command, arguments, success_exit_codes,
start_only, stdout, stderr) and time parses as 2006-01-02 15:04:05.
- TestRunJobRecordFields: RunRecord matches the job and trigger; Time parses;
Output contains stdout/stderr sections.
- TestFormatOutput / TestFormatOutputEmptyStreams: stdout/stderr sections are
separated by a blank line; empty streams show "<empty>".
- TestLogArguments: empty/whitespace → "<empty>"; CRLF → LF normalised.
- TestSanitizeFileName: special chars → "_"; empty or all-special → "job".
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
Add scripts/test.sh and scripts/test.bat to run go vet and go test -race.
Update docs/TESTS.md with test script usage and reorganized manual test commands.
Co-Authored-By: Claude Haiku 4.5 <noreply@anthropic.com>
Track the 30 tasks across 5 phases with checkboxes. Each checkbox can be
marked complete as tasks land and pass review.
Co-Authored-By: Claude Haiku 4.5 <noreply@anthropic.com>
Document a phased plan to restructure GoSentry into focused packages
under src/ (domain, storage, runner, scheduler, platform, app, ui) with
an application-service layer that owns state, and link it from the README.
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
readShortcut read the shortcut TargetPath via [Console]::Out.Write, which
uses the system OEM code page by default. On Russian Windows (CP866) this
encoded Cyrillic characters differently from UTF-8, so Go's string(output)
produced a garbled path that never matched os.Executable, causing
AutostartStatus to always report "shortcut points to another executable"
for any install directory that contained non-ASCII characters.
Fix: prepend [Console]::OutputEncoding = New-Object System.Text.UTF8Encoding($false)
to the readShortcut PowerShell script so the output is always UTF-8.
Also harden sameWindowsPath against NTFS 8.3 short names: when a directory
name contains spaces Windows assigns a short name (e.g. LOCALG~1 for
"Local Git"), and the OS may use that form when launching from a
Startup-folder shortcut. Add an os.SameFile fallback that compares paths
by volume serial number and file index, which is immune to 8.3 vs long
name differences as well as directory junction points.
Add normalizeWindowsPath helper that strips quotes and the \?\ extended-
length prefix before filepath.Clean so those variants compare equal to
the plain path.
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
Created docs/TESTS.md documenting all 25 tests across 5 test files:
- store_test.go: YAML serialization tests
- scheduler_test.go: Schedule parsing and invocation output tests
- runner_test.go: Command execution, exit codes, and Windows process tests
- autostart_windows_test.go: Windows startup folder shortcut creation tests
- autostart_linux_test.go: Linux XDG Desktop Entry autostart tests
Includes test descriptions, platform requirements, and usage instructions.
Co-Authored-By: Claude Haiku 4.5 <noreply@anthropic.com>
Update the OpenGL workaround to suggest the mingw release of Mesa instead of
msvc, as it matches the MSYS2 GCC toolchain used to build GoSentry. Both
variants work at runtime, but mingw is the more consistent choice.
Co-Authored-By: Claude Haiku 4.5 <noreply@anthropic.com>
Add explanatory comments around startup timing, single-instance focus handoff, config migration, and Windows/Linux autostart choices.
The new comments capture why these implementations were chosen, what alternatives were intentionally avoided, and which user-facing problems those tradeoffs solve.
Bump the version to 0.3.1 and record startup timing after the main window is actually shown.
Keep autostart launches distinct in History by recording a separate tray-start message when the UI intentionally starts hidden.
Rename the application, Go module path, command package, build artifacts, resource script, and embedded icon assets from PySentry/pysentry to GoSentry/gosentry.
Move portable settings to gosentry.yaml while reading legacy pysentry.yaml during the transition, then rewrite settings under the new name.
Update Windows and Linux autostart integration to use GoSentry names while cleaning up legacy PySentry registry, desktop-entry, and systemd artifacts.
Refresh README, architecture notes, roadmap, changelog, and release examples for version 0.3.0.
Bump the application version to 0.2.5 and update documented artifact names.
Document the Windows VirtualBox/RDP OpenGL startup failure and the Mesa software OpenGL workaround.
Record the tray-icon double-click limitation in the roadmap for future Fyne or platform-specific tray work.
Add a shared --start-in-tray argument that lets autostart start the scheduler and tray integration without opening the main window.
Write the argument into Windows Startup shortcuts and Linux XDG Autostart desktop entries, and verify existing autostart entries include it.
Keep manual launches unchanged and let a manual second launch reveal an already-running instance while duplicate autostart launches stay hidden.
Replace the HKCU Run autostart entry with a per-user Startup folder shortcut. A .lnk stores TargetPath separately, which avoids fragile quoting when the executable path contains spaces.
Remove legacy PySentry and GoSentry Run entries when saving autostart settings, and report shortcut status from the actual shortcut target.
Add Windows tests that create and read a temporary shortcut with spaces in the path so the PowerShell/COM invocation remains covered.
Wrap dynamic job detail labels so long names, schedules, commands, and status values no longer change the right panel minimum width when the selected job changes.
Point embedded and Windows resource icons at the current asset filenames so tests and Windows builds continue to work after the asset cleanup.
Switch direct YAML usage from gopkg.in/yaml.v3 to go.yaml.in/yaml/v4, the maintained YAML org fork of the archived go-yaml repository.
Update README dependency and mirroring links so the documented source repository matches the module used by the application.