110 lines
4.5 KiB
Go
110 lines
4.5 KiB
Go
package store
|
|
|
|
import (
|
|
"strings"
|
|
"testing"
|
|
"time"
|
|
)
|
|
|
|
func TestStopSuppressesUnstartedCallsButPreservesUnknown(t *testing.T) {
|
|
s := preparedCurrentCallStore(t)
|
|
pending := currentCall("queued-before-stop")
|
|
if _, _, err := s.RecordExecute(pending); err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
unknown := currentCall("unknown-before-stop")
|
|
if _, _, err := s.RecordExecute(unknown); err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
if err := s.ReserveExecute(unknown.DispatcherID, unknown.EventID, currentReservation(), currentMondayUTC()); err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
if err := s.MarkExecuteUnknown(unknown.DispatcherID, unknown.EventID); err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
if err := s.ApplyControl(currentDispatcherID, 1001, "task-asr", "stop"); err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
calls, err := s.ListPendingExecute(currentDispatcherID)
|
|
if err != nil || len(calls) != 0 {
|
|
t.Fatalf("stopped task still has dispatchable commands: %+v %v", calls, err)
|
|
}
|
|
var pendingStatus, unknownStatus string
|
|
if err := s.db.QueryRow(`SELECT status FROM dispatcher_inbox WHERE event_id=?`, pending.EventID).Scan(&pendingStatus); err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
if err := s.db.QueryRow(`SELECT status FROM dispatcher_inbox WHERE event_id=?`, unknown.EventID).Scan(&unknownStatus); err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
if pendingStatus != "suppressed" || unknownStatus != "unknown" {
|
|
t.Fatalf("stop did not preserve unknown and silently suppress unstarted: pending=%s unknown=%s", pendingStatus, unknownStatus)
|
|
}
|
|
if err := s.ApplyControl(currentDispatcherID, 1001, "task-asr", "resume"); err == nil {
|
|
t.Fatal("stopped task resumed")
|
|
}
|
|
if outbox, err := s.ListPendingOutbox(currentDispatcherID); err != nil || len(outbox) != 0 {
|
|
t.Fatalf("stop emitted per-call results: %+v %v", outbox, err)
|
|
}
|
|
}
|
|
|
|
func TestEditCommitsSnapshotRevisionAndAckTogether(t *testing.T) {
|
|
s := preparedCurrentCallStore(t)
|
|
updated, err := s.ReadSnapshot(currentDispatcherID, 1001, "task-asr")
|
|
if err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
updated.Task.Raw = []byte(strings.Replace(string(updated.Task.Raw), `"task_revision":1`, `"task_revision":2`, 1))
|
|
updated.Task.TaskRevision = 2
|
|
if status, err := s.CompleteEdit(updated, "edit-success"); err != nil || status != "applied" {
|
|
t.Fatalf("edit status=%q: %v", status, err)
|
|
}
|
|
persisted, err := s.ReadSnapshot(currentDispatcherID, 1001, "task-asr")
|
|
if err != nil || persisted.Task.TaskRevision != 2 {
|
|
t.Fatalf("edit not durably bound: revision=%d err=%v", persisted.Task.TaskRevision, err)
|
|
}
|
|
var revision int64
|
|
if err := s.db.QueryRow(`SELECT task_revision FROM dispatcher_tasks WHERE task_id='task-asr'`).Scan(&revision); err != nil || revision != 2 {
|
|
t.Fatalf("task and snapshot revisions differ: revision=%d err=%v", revision, err)
|
|
}
|
|
stale := currentStoreSnapshot(t)
|
|
if _, err := s.CompleteEdit(stale, "edit-stale"); err == nil {
|
|
t.Fatal("stale edit downgraded the task")
|
|
}
|
|
conflicting := updated
|
|
conflicting.Task.Raw = []byte(strings.Replace(string(updated.Task.Raw), `"status":"running"`, `"status":"paused"`, 1))
|
|
if _, err := s.CompleteEdit(conflicting, "edit-conflicting"); err == nil {
|
|
t.Fatal("same revision changed task contents")
|
|
}
|
|
persisted, err = s.ReadSnapshot(currentDispatcherID, 1001, "task-asr")
|
|
if err != nil || persisted.Task.TaskRevision != 2 || string(persisted.Task.Raw) != string(updated.Task.Raw) {
|
|
t.Fatalf("failed edit changed bound snapshot: revision=%d err=%v", persisted.Task.TaskRevision, err)
|
|
}
|
|
if err := s.ApplyControl(currentDispatcherID, 1001, "task-asr", "stop"); err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
updated.Task.Raw = []byte(strings.Replace(string(updated.Task.Raw), `"task_revision":2`, `"task_revision":3`, 1))
|
|
updated.Task.TaskRevision = 3
|
|
if status, err := s.CompleteEdit(updated, "edit-stopped"); err != nil || status != "rejected" {
|
|
t.Fatalf("stopped edit status=%q: %v", status, err)
|
|
}
|
|
outbox, err := s.ListPendingOutbox(currentDispatcherID)
|
|
if err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
foundRejected := false
|
|
for _, item := range outbox {
|
|
if strings.Contains(string(item.Body), `"event_id":"edit-stopped"`) && strings.Contains(string(item.Body), `"status":"rejected"`) {
|
|
foundRejected = true
|
|
}
|
|
}
|
|
if !foundRejected {
|
|
t.Fatal("edit on stopped task lacked durable rejection receipt")
|
|
}
|
|
persisted, err = s.ReadSnapshot(currentDispatcherID, 1001, "task-asr")
|
|
if err != nil || persisted.Task.TaskRevision != 2 {
|
|
t.Fatalf("stopped edit modified task: revision=%d err=%v", persisted.Task.TaskRevision, err)
|
|
}
|
|
}
|
|
|
|
func currentMondayUTC() time.Time { return time.Date(2026, 9, 21, 1, 30, 0, 0, time.UTC) }
|