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