feat(modules): add hello toy module as engine smoke test

This commit is contained in:
MDietermann
2026-04-13 20:31:43 +02:00
parent 7a3ebbb146
commit 3a4855b92a
3 changed files with 162 additions and 0 deletions

View File

@@ -0,0 +1,98 @@
// Package hello is a toy module that writes a marker file. It exists to
// validate the engine end-to-end before real modules arrive in Plan 3.
package hello
import (
"context"
"fmt"
"os"
"path/filepath"
"github.com/MDietermann/ArduinoArchSetup/internal/module"
)
const defaultRelPath = ".cache/archduino/hello.ok"
const markerContent = "archduino-hello\n"
// Module is the exported module value. Path is overridable for tests.
type Module struct {
Path string // absolute path; if empty, ~/.cache/archduino/hello.ok is used
}
func (m Module) resolve() (string, error) {
if m.Path != "" {
return m.Path, nil
}
home, err := os.UserHomeDir()
if err != nil {
return "", err
}
return filepath.Join(home, defaultRelPath), nil
}
func (Module) Meta() module.Meta {
return module.Meta{
ID: "hello",
Name: "Hello",
Description: "Toy module that writes a marker file (engine smoke test).",
}
}
type state struct{ Present bool }
func (m Module) Detect(ctx context.Context) (module.State, error) {
path, err := m.resolve()
if err != nil {
return nil, err
}
data, err := os.ReadFile(path)
if err != nil {
if os.IsNotExist(err) {
return state{Present: false}, nil
}
return nil, err
}
return state{Present: string(data) == markerContent}, nil
}
func (m Module) Plan(ctx context.Context, cur module.State) (module.Plan, error) {
s, _ := cur.(state)
if s.Present {
return module.Plan{}, nil
}
path, err := m.resolve()
if err != nil {
return module.Plan{}, err
}
return module.Plan{Actions: []module.Action{{
Description: "write marker " + path,
Kind: module.ActionFileWrite,
Exec: func(ctx context.Context) error {
if err := os.MkdirAll(filepath.Dir(path), 0o755); err != nil {
return err
}
return os.WriteFile(path, []byte(markerContent), 0o644)
},
}}}, nil
}
func (m Module) Apply(ctx context.Context, p module.Plan, r module.Reporter) error {
for i, a := range p.Actions {
r.Progress(m.Meta().ID, i+1, len(p.Actions), a.Description)
if err := a.Exec(ctx); err != nil {
return err
}
}
return nil
}
func (m Module) Verify(ctx context.Context) error {
st, err := m.Detect(ctx)
if err != nil {
return err
}
if !st.(state).Present {
return fmt.Errorf("hello: marker not present after apply")
}
return nil
}

View File

@@ -0,0 +1,57 @@
package hello
import (
"context"
"os"
"path/filepath"
"testing"
"github.com/MDietermann/ArduinoArchSetup/internal/engine"
)
func TestHelloFullLifecycle(t *testing.T) {
path := filepath.Join(t.TempDir(), "hello.ok")
m := Module{Path: path}
ctx := context.Background()
st, err := m.Detect(ctx)
if err != nil {
t.Fatal(err)
}
if st.(state).Present {
t.Fatal("marker should be absent on fresh tmpdir")
}
plan, err := m.Plan(ctx, st)
if err != nil {
t.Fatal(err)
}
if plan.IsEmpty() {
t.Fatal("plan should have one action")
}
if err := m.Apply(ctx, plan, engine.NewBufferedReporter()); err != nil {
t.Fatal(err)
}
if err := m.Verify(ctx); err != nil {
t.Fatal(err)
}
st2, _ := m.Detect(ctx)
if !st2.(state).Present {
t.Fatal("marker should be present after apply")
}
plan2, _ := m.Plan(ctx, st2)
if !plan2.IsEmpty() {
t.Fatal("second plan should be empty (idempotent)")
}
data, err := os.ReadFile(path)
if err != nil {
t.Fatal(err)
}
if string(data) != markerContent {
t.Fatalf("content = %q", data)
}
}