package store import ( "bytes" "path/filepath" "testing" ) func TestSIPOnlyVerifiedNeverOpensCallAdmission(t *testing.T) { s, err := Open(filepath.Join(t.TempDir(), "sip-only.db")) if err != nil { t.Fatal(err) } defer s.Close() if err := s.NoteSIPChange(currentDispatcherID, 9); err != nil { t.Fatal(err) } if err := s.MarkSIPOnlyVerified(currentDispatcherID, 9); err != nil { t.Fatal(err) } if applied, pending, err := s.SIPState(currentDispatcherID); err != nil || applied != 9 || pending != 0 { t.Fatalf("SIP-only checkpoint: %d %d %v", applied, pending, err) } var admitted int if err := s.db.QueryRow(`SELECT discovery_ready FROM dispatcher_state WHERE dispatcher_id=?`, currentDispatcherID).Scan(&admitted); err != nil || admitted != 0 { t.Fatalf("SIP-only opened call admission: %d %v", admitted, err) } if err := s.NoteSIPChange(currentDispatcherID, 10); err != nil { t.Fatal(err) } if err := s.MarkSIPOnlyVerified(currentDispatcherID, 9); err == nil { t.Fatal("stale native SIP confirmation discarded newer notification") } } func TestSIPNotificationRequiresFullDrainAndExactLoadedRevision(t *testing.T) { s := preparedCurrentCallStore(t) if err := s.MarkReadyForSIP(currentDispatcherID, 8); err != nil { t.Fatal(err) } cmd := currentCall("ongoing-before-sip-change") if _, _, err := s.RecordExecute(cmd); err != nil { t.Fatal(err) } if err := s.ReserveExecute(cmd.DispatcherID, cmd.EventID, currentReservation(), currentMondayUTC()); err != nil { t.Fatal(err) } if err := s.MarkExecuteUnknown(cmd.DispatcherID, cmd.EventID); err != nil { t.Fatal(err) } if count, err := s.OccupiedCalls(currentDispatcherID); err != nil || count != 1 { t.Fatalf("unknown call was not counted: %d %v", count, err) } if err := s.NoteSIPChange(currentDispatcherID, 9); err != nil { t.Fatal(err) } if admitted, err := s.CanAdmit(currentDispatcherID, 1001, "task-asr"); err != nil || admitted { t.Fatalf("SIP change failed to close admission: %v %v", admitted, err) } if applied, pending, err := s.SIPState(currentDispatcherID); err != nil || applied != 8 || pending != 9 { t.Fatalf("SIP notification not durable: applied=%d pending=%d %v", applied, pending, err) } if err := s.MarkReadyForSIP(currentDispatcherID, 9); err == nil { t.Fatal("changed SIP opened without matching task snapshot or drain") } snapshot, err := s.ReadSnapshot(currentDispatcherID, 1001, "task-asr") if err != nil { t.Fatal(err) } snapshot.SIP.Revision = 9 if err := s.SaveSnapshot(snapshot); err != nil { t.Fatal(err) } if err := s.MarkReadyForSIP(currentDispatcherID, 9); err == nil { t.Fatal("unknown ongoing call was released by SIP change") } // An external definitive call end must be recorded before old SIP loads can // drain. Unknown outcomes never expire or auto-release on their own. if _, err := s.db.Exec(`UPDATE dispatcher_inbox SET status='finished' WHERE dispatcher_id=? AND event_id=? AND status='unknown'`, cmd.DispatcherID, cmd.EventID); err != nil { t.Fatal(err) } if err := s.MarkReadyForSIP(currentDispatcherID, 9); err != nil { t.Fatal(err) } if admitted, err := s.CanAdmit(currentDispatcherID, 1001, "task-asr"); err != nil || !admitted { t.Fatalf("drained verified SIP revision did not reopen: %v %v", admitted, err) } if applied, pending, err := s.SIPState(currentDispatcherID); err != nil || applied != 9 || pending != 0 { t.Fatalf("SIP state not committed after reload: %d %d %v", applied, pending, err) } if err := s.NoteSIPChange(currentDispatcherID, 8); err != nil { t.Fatal(err) } if applied, pending, err := s.SIPState(currentDispatcherID); err != nil || applied != 9 || pending != 10 { t.Fatalf("notification must fence a fresh read without downgrading loaded SIP: %d %d %v", applied, pending, err) } } func TestRebindSIPPreservesTaskAndRejectsSameRevisionConflict(t *testing.T) { s := preparedCurrentCallStore(t) original, err := s.ReadSnapshot(currentDispatcherID, 1001, "task-asr") if err != nil { t.Fatal(err) } updated := original.SIP updated.Revision++ if err := s.NoteSIPChange(currentDispatcherID, updated.Revision); err != nil { t.Fatal(err) } if err := s.RebindSIP(currentDispatcherID, updated); err != nil { t.Fatal(err) } loaded, err := s.ReadSnapshot(currentDispatcherID, 1001, "task-asr") if err != nil || loaded.SIP.Revision != updated.Revision || !bytes.Equal(loaded.Task.Raw, original.Task.Raw) || loaded.Quota != original.Quota { t.Fatalf("SIP update changed task or quota: %+v %v", loaded, err) } if err := s.MarkReadyForSIP(currentDispatcherID, updated.Revision); err != nil { t.Fatal(err) } altered := updated altered.Trunks = []byte(`[]`) if err := s.RebindSIP(currentDispatcherID, altered); err == nil { t.Fatal("same SIP revision changed content") } } func TestPendingSIPChangeSurvivesRestartWithoutOldDataDeletion(t *testing.T) { path := filepath.Join(t.TempDir(), "state.db") s, err := Open(path) if err != nil { t.Fatal(err) } if err := s.NoteSIPChange(currentDispatcherID, 12); err != nil { t.Fatal(err) } if err := s.Close(); err != nil { t.Fatal(err) } reopened, err := Open(path) if err != nil { t.Fatal(err) } defer reopened.Close() if applied, pending, err := reopened.SIPState(currentDispatcherID); err != nil || applied != 0 || pending != 12 { t.Fatalf("lost pending SIP notification on restart: %d %d %v", applied, pending, err) } if err := reopened.MarkReadyForSIP(currentDispatcherID, 11); err == nil { t.Fatal("older loaded SIP overrode durable pending notification") } }