Refactoring complete: v0.4.0 architectural milestone (#1)

## Summary

Completed Phase 5 refactoring and reached the target architecture.

**Architectural milestone achieved:**
- Service layer owns all state and is the sole writer
- UI is a thin Fyne view, all widget updates marshaled via `fyne.Do`
- Core engines are stateless and injectable
- Domain types are pure (no `yaml:"-"` fields)
- Full module builds and `go vet ./...` clean

## Changes

- Bump version: 0.3.6 → 0.4.0
- Update CHANGELOG with Phase 5 summary
- Add ROADMAP "Refactoring Follow-Ups" section

## Known follow-up work

1. **Linux test build broken** — `runner_test.go` needs `//go:build windows` tag
2. **File-size limits exceeded** — `operations.go` (486 lines), `jobs_view.go` (415 lines)

See ROADMAP.md for details.

---------

Co-authored-by: mixeme <mix.public@ya.ru>
Reviewed-on: #1
This commit was merged in pull request #1.
This commit is contained in:
2026-06-22 08:05:10 +03:00
parent d24211cab2
commit 01fd572a89
74 changed files with 5424 additions and 2712 deletions
+57
View File
@@ -0,0 +1,57 @@
package app
import (
"testing"
"gitea.mixdep.ru/mix/gosentry/src/domain"
"gitea.mixdep.ru/mix/gosentry/src/storage"
)
func newTestService(jobs []domain.Job) *Service {
return NewService(&storage.Store{}, jobs)
}
func TestNewServiceBuildsRuntimePerJob(t *testing.T) {
jobs := []domain.Job{
{ID: 1, Name: "Enabled", Enabled: true},
{ID: 2, Name: "Disabled", Enabled: false},
}
svc := newTestService(jobs)
if got := svc.Runtime(1); got == nil {
t.Fatal("expected runtime for enabled job 1")
} else if got.LastState != "Ready" {
t.Errorf("enabled job runtime state = %q, want %q", got.LastState, "Ready")
}
if got := svc.Runtime(2); got == nil {
t.Fatal("expected runtime for disabled job 2")
} else if got.LastState != "Paused" {
t.Errorf("disabled job runtime state = %q, want %q", got.LastState, "Paused")
}
if got := svc.Runtime(99); got != nil {
t.Errorf("expected nil runtime for unknown job, got %+v", got)
}
}
func TestJobsReturnsCopy(t *testing.T) {
jobs := []domain.Job{{ID: 1, Name: "Original"}}
svc := newTestService(jobs)
snapshot := svc.Jobs()
if len(snapshot) != 1 {
t.Fatalf("Jobs() len = %d, want 1", len(snapshot))
}
// Mutating the returned slice must not affect Service-owned state.
snapshot[0].Name = "Mutated"
if again := svc.Jobs(); again[0].Name != "Original" {
t.Errorf("Service state leaked through Jobs(): name = %q, want %q", again[0].Name, "Original")
}
}
func TestStoreReturnsWiredStore(t *testing.T) {
store := &storage.Store{}
svc := NewService(store, nil)
if svc.Store() != store {
t.Error("Store() did not return the wired store")
}
}