feat(11-02): run plugin schedules as River periodic jobs through bonfire.Call

- pact.HasSchedule with ScheduledCommand and Daily/DailyAt/Every cadences (no River import)
- bonfire.Call, Catalog and ErrUnknownCommand for in-process command runs
- conga Daily/Every wall-clock schedules in app.timezone, periodic jobs on every worker,
  scheduled queue (MaxAttempts 1, unique by args within the cadence period)
- scheduled worker runs only entries matching the compiled table; unregistered
  commands are skipped with a Warn log
- generated app main publishes bonfire.NewCatalog(commands); hello main regenerated
This commit is contained in:
Jakub Zych
2026-09-29 19:47:51 +02:00
parent 77b8ff177a
commit d9f939a1ea
15 changed files with 890 additions and 12 deletions

View File

@@ -142,6 +142,26 @@ func TestGenerateMainRegistersCongaRuntimeCommands(t *testing.T) {
}
}
// TestGenerateMainPublishesCommandCatalog: the generated main publishes the
// final command list as a *bonfire.Catalog after collecting plugin commands
// and before building the root, so scheduled runs can call any command.
func TestGenerateMainPublishesCommandCatalog(t *testing.T) {
mainSrc, err := generateMain(Manifest{Module: "example.com/app", Binary: "hello"})
if err != nil {
t.Fatal(err)
}
line := []byte("if err := app.Publish(bonfire.NewCatalog(commands)); err != nil {")
if n := bytes.Count(mainSrc, line); n != 1 {
t.Fatalf("catalog publish count = %d, want 1\n%s", n, mainSrc)
}
publish := bytes.Index(mainSrc, line)
plugins := bytes.Index(mainSrc, []byte("hasCommands.Commands()"))
root := bytes.Index(mainSrc, []byte("bonfire.NewRoot("))
if plugins < 0 || root < 0 || !(plugins < publish && publish < root) {
t.Fatalf("catalog must be published after plugin commands and before NewRoot:\n%s", mainSrc)
}
}
func TestWriteIfChangedSkipsIdenticalBytes(t *testing.T) {
dir := t.TempDir()
path := filepath.Join(dir, "out.go")