package workbenchruntime import ( "context" "database/sql" "errors" "fmt" "net/http" "net/http/httptest" "strings" "testing" ) func TestRealtimeSyncTxOptionsAreReadOnlyRepeatableRead(t *testing.T) { options := realtimeSyncTxOptions() if options.Isolation != sql.LevelRepeatableRead { t.Fatalf("isolation=%v, want repeatable read", options.Isolation) } if !options.ReadOnly { t.Fatal("realtime sync transaction must be read only") } } func TestNormalizeRealtimeSyncQueryRequiresScopedActorAndBoundsLimit(t *testing.T) { query, validation := normalizeRealtimeSyncQuery(realtimeSyncQuery{ SessionID: " ses_sync ", Limit: 900, Actor: sessionActor{ID: " usr_owner ", Role: " USER "}, }) if validation != nil { t.Fatalf("unexpected validation: %#v", validation) } if query.SessionID != "ses_sync" || query.Actor.ID != "usr_owner" || query.Actor.Role != "user" { t.Fatalf("query was not normalized: %#v", query) } if query.Limit != realtimeSyncMaxLimit { t.Fatalf("limit=%d, want %d", query.Limit, realtimeSyncMaxLimit) } _, validation = normalizeRealtimeSyncQuery(realtimeSyncQuery{TraceID: "trc_sync", Actor: sessionActor{Role: "user"}}) if validation == nil || validation.Status != http.StatusForbidden || validation.Code != "workbench_sync_actor_required" { t.Fatalf("ordinary user without owner id validation=%#v", validation) } admin, validation := normalizeRealtimeSyncQuery(realtimeSyncQuery{TraceID: "trc_sync", Actor: sessionActor{Role: "admin"}}) if validation != nil || admin.Actor.ID != "" { t.Fatalf("admin should not require owner id: query=%#v validation=%#v", admin, validation) } _, validation = normalizeRealtimeSyncQuery(realtimeSyncQuery{Actor: sessionActor{ID: "usr_owner", Role: "user"}}) if validation == nil || validation.Code != "workbench_sync_scope_required" { t.Fatalf("missing scope validation=%#v", validation) } _, validation = normalizeRealtimeSyncQuery(realtimeSyncQuery{SessionID: "ses_sync", AfterOutboxSeq: -1, Actor: sessionActor{ID: "usr_owner", Role: "user"}}) if validation == nil || validation.Code != "workbench_sync_cursor_invalid" { t.Fatalf("negative cursor validation=%#v", validation) } _, validation = normalizeRealtimeSyncQuery(realtimeSyncQuery{SessionID: "ses_sync", SnapshotOnly: true, DeltaOnly: true, Actor: sessionActor{ID: "usr_owner", Role: "user"}}) if validation == nil || validation.Code != "workbench_sync_mode_invalid" { t.Fatalf("conflicting sync mode validation=%#v", validation) } } func TestRealtimeSyncRouteIsPostOnlyAndRejectsUnknownFields(t *testing.T) { server := &Server{config: Config{}, mux: http.NewServeMux()} server.routes() methodRecorder := httptest.NewRecorder() server.mux.ServeHTTP(methodRecorder, httptest.NewRequest(http.MethodGet, "/v1/workbench-runtime/sync", nil)) if methodRecorder.Code != http.StatusMethodNotAllowed { t.Fatalf("GET status=%d, want %d", methodRecorder.Code, http.StatusMethodNotAllowed) } if methodRecorder.Header().Get("Allow") != "POST" { t.Fatalf("Allow=%q, want POST", methodRecorder.Header().Get("Allow")) } unknownRecorder := httptest.NewRecorder() unknownRequest := httptest.NewRequest(http.MethodPost, "/v1/workbench-runtime/sync", strings.NewReader(`{"sessionId":"ses_sync","actor":{"id":"usr_owner","role":"user"},"unknown":true}`)) server.mux.ServeHTTP(unknownRecorder, unknownRequest) if unknownRecorder.Code != http.StatusBadRequest || !strings.Contains(unknownRecorder.Body.String(), "invalid_json") { t.Fatalf("unknown field response=%d %s", unknownRecorder.Code, unknownRecorder.Body.String()) } trailingRecorder := httptest.NewRecorder() trailingRequest := httptest.NewRequest(http.MethodPost, "/v1/workbench-runtime/sync", strings.NewReader(`{"sessionId":"ses_sync","actor":{"id":"usr_owner","role":"user"}} {}`)) server.mux.ServeHTTP(trailingRecorder, trailingRequest) if trailingRecorder.Code != http.StatusBadRequest || !strings.Contains(trailingRecorder.Body.String(), "invalid_json") { t.Fatalf("trailing JSON response=%d %s", trailingRecorder.Code, trailingRecorder.Body.String()) } } func TestReadRealtimeSyncSnapshotUsesOwnerScopeAndSingleCutoff(t *testing.T) { queryer := newRealtimeSyncFakeQueryer("usr_owner") query := realtimeSyncQuery{ SessionID: "ses_sync", TraceID: "trc_sync", AfterOutboxSeq: 0, Limit: 2, Actor: sessionActor{ID: "usr_owner", Role: "user"}, } snapshot, err := readRealtimeSyncSnapshot(context.Background(), queryer, query) if err != nil { t.Fatalf("read sync snapshot: %v", err) } if snapshot.CutoffOutboxSeq != 3 || snapshot.CursorOutboxSeq != 2 || !snapshot.HasMore { t.Fatalf("unexpected cursor boundary: %#v", snapshot) } if len(snapshot.Events) != 2 { t.Fatalf("events=%d, want 2", len(snapshot.Events)) } first := snapshot.Events[0] if first.OutboxEventID != "obe_1" || first.EntityFamily != "traceEvents" || first.EntityID != "evt_1" { t.Fatalf("event identity fields were not returned: %#v", first) } if first.EventSeq != int64(101) || first.AggregateID != "trc_sync" || first.AggregateSeq != 1 || first.ProjectionRevision != 11 { t.Fatalf("event aggregate fields were not returned: %#v", first) } if len(snapshot.Facts) != len(realtimeSyncFactSpecs) { t.Fatalf("fact families=%d, want %d: %#v", len(snapshot.Facts), len(realtimeSyncFactSpecs), snapshot.Facts) } for _, spec := range realtimeSyncFactSpecs { if len(snapshot.Facts[spec.Family]) != 1 { t.Fatalf("facts[%s]=%#v, want one fact", spec.Family, snapshot.Facts[spec.Family]) } } if len(queryer.calls) != 3+len(realtimeSyncFactSpecs) { t.Fatalf("query calls=%v", queryer.calls) } if queryer.calls[0].kind != "scope" || queryer.calls[1].kind != "cutoff" || queryer.calls[2].kind != "events" { t.Fatalf("snapshot boundary query order=%v", queryer.calls) } if got := queryer.calls[0].args; len(got) != 4 || got[2] != false || got[3] != "usr_owner" { t.Fatalf("owner scope args=%#v", got) } if got := queryer.calls[2].args; len(got) != 5 || got[3] != int64(3) || got[4] != 3 { t.Fatalf("events must use cutoff and limit+1: %#v", got) } } func TestReadRealtimeSyncSnapshotRejectsOtherOwnerBeforeReadingFacts(t *testing.T) { if !strings.Contains(realtimeSyncResolveScopeSQL, "sessions.owner_user_id = $4") { t.Fatalf("ordinary user scope must be constrained by workbench_sessions.owner_user_id: %s", realtimeSyncResolveScopeSQL) } queryer := newRealtimeSyncFakeQueryer("usr_owner") _, err := readRealtimeSyncSnapshot(context.Background(), queryer, realtimeSyncQuery{ SessionID: "ses_sync", Limit: 100, Actor: sessionActor{ID: "usr_other", Role: "user"}, }) if !errors.Is(err, sql.ErrNoRows) { t.Fatalf("error=%v, want sql.ErrNoRows", err) } if len(queryer.calls) != 1 || queryer.calls[0].kind != "scope" { t.Fatalf("unauthorized scope must stop before outbox/facts reads: %v", queryer.calls) } } func TestReadRealtimeSyncSnapshotOnlySkipsHistoryAndReadsBoundedCurrentFacts(t *testing.T) { queryer := newRealtimeSyncFakeQueryer("usr_owner") snapshot, err := readRealtimeSyncSnapshot(context.Background(), queryer, realtimeSyncQuery{ SessionID: "ses_sync", Limit: 100, SnapshotOnly: true, Actor: sessionActor{ID: "usr_owner", Role: "user"}, }) if err != nil { t.Fatalf("read snapshot-only sync: %v", err) } if len(snapshot.Events) != 0 || snapshot.HasMore || snapshot.CursorOutboxSeq != snapshot.CutoffOutboxSeq { t.Fatalf("snapshot-only boundary=%#v", snapshot) } if len(snapshot.Facts) != len(realtimeSyncSnapshotFactSpecs) { t.Fatalf("snapshot-only facts=%#v", snapshot.Facts) } for _, call := range queryer.calls { if call.kind == "events" || call.kind == "facts.parts" || call.kind == "facts.traceEvents" || call.kind == "facts.checkpoints" { t.Fatalf("snapshot-only performed unbounded read: %v", queryer.calls) } } } func TestReadRealtimeSyncDeltaOnlyReadsOutboxWithoutFacts(t *testing.T) { queryer := newRealtimeSyncFakeQueryer("usr_owner") snapshot, err := readRealtimeSyncSnapshot(context.Background(), queryer, realtimeSyncQuery{ SessionID: "ses_sync", AfterOutboxSeq: 1, Limit: 100, DeltaOnly: true, Actor: sessionActor{ID: "usr_owner", Role: "user"}, }) if err != nil { t.Fatalf("read delta-only sync: %v", err) } if len(snapshot.Events) == 0 || len(snapshot.Facts) != 0 { t.Fatalf("delta-only result=%#v", snapshot) } for _, call := range queryer.calls { if strings.HasPrefix(call.kind, "facts.") { t.Fatalf("delta-only read facts: %v", queryer.calls) } } } func TestReadRealtimeSyncSnapshotClampsForeignCursorToScopedCutoff(t *testing.T) { queryer := newRealtimeSyncFakeQueryer("usr_owner") queryer.events = [][]any{} snapshot, err := readRealtimeSyncSnapshot(context.Background(), queryer, realtimeSyncQuery{ SessionID: "ses_sync", AfterOutboxSeq: 999, Limit: 100, Actor: sessionActor{ID: "usr_owner", Role: "user"}, }) if err != nil { t.Fatalf("read sync snapshot: %v", err) } if snapshot.CutoffOutboxSeq != 3 || snapshot.CursorOutboxSeq != 3 || snapshot.HasMore || len(snapshot.Events) != 0 { t.Fatalf("foreign cursor was not clamped to scoped cutoff: %#v", snapshot) } } func TestReadRealtimeSyncSnapshotAllowsAdminWithoutOwnerFilter(t *testing.T) { queryer := newRealtimeSyncFakeQueryer("usr_owner") snapshot, err := readRealtimeSyncSnapshot(context.Background(), queryer, realtimeSyncQuery{ TraceID: "trc_sync", Limit: 3, Actor: sessionActor{Role: "admin"}, }) if err != nil { t.Fatalf("admin sync: %v", err) } if len(queryer.calls) == 0 || queryer.calls[0].args[2] != true || queryer.calls[0].args[3] != "" { t.Fatalf("admin scope args=%#v", queryer.calls) } if snapshot.CursorOutboxSeq != snapshot.CutoffOutboxSeq || snapshot.HasMore { t.Fatalf("admin complete page boundary=%#v", snapshot) } } type realtimeSyncFakeCall struct { kind string query string args []any } type realtimeSyncFakeQueryer struct { ownerID string sessionID string cutoff int64 events [][]any facts map[string][][]any calls []realtimeSyncFakeCall } func newRealtimeSyncFakeQueryer(ownerID string) *realtimeSyncFakeQueryer { facts := map[string][][]any{} for _, spec := range realtimeSyncFactSpecs { facts[spec.Table] = [][]any{{fmt.Sprintf(`{"family":%q,"sessionId":"ses_sync"}`, spec.Family)}} } return &realtimeSyncFakeQueryer{ ownerID: ownerID, sessionID: "ses_sync", cutoff: 3, events: [][]any{realtimeSyncTestEventRow(1), realtimeSyncTestEventRow(2), realtimeSyncTestEventRow(3)}, facts: facts, } } func (queryer *realtimeSyncFakeQueryer) QueryRowContext(_ context.Context, query string, args ...any) realtimeSyncRow { if query == realtimeSyncResolveScopeSQL { queryer.record("scope", query, args) isAdmin, _ := args[2].(bool) actorID, _ := args[3].(string) if !isAdmin && actorID != queryer.ownerID { return realtimeSyncFakeRow{err: sql.ErrNoRows} } return realtimeSyncFakeRow{values: []any{queryer.sessionID}} } if query == realtimeSyncCutoffSQL { queryer.record("cutoff", query, args) return realtimeSyncFakeRow{values: []any{queryer.cutoff}} } return realtimeSyncFakeRow{err: fmt.Errorf("unexpected QueryRowContext: %s", query)} } func (queryer *realtimeSyncFakeQueryer) QueryContext(_ context.Context, query string, args ...any) (realtimeSyncRows, error) { if query == realtimeSyncEventsSQL { queryer.record("events", query, args) return &realtimeSyncFakeRows{values: queryer.events}, nil } for _, spec := range realtimeSyncFactSpecs { if strings.Contains(query, "FROM "+spec.Table+" ") { queryer.record("facts."+spec.Family, query, args) return &realtimeSyncFakeRows{values: queryer.facts[spec.Table]}, nil } } return nil, fmt.Errorf("unexpected QueryContext: %s", query) } func (queryer *realtimeSyncFakeQueryer) record(kind string, query string, args []any) { cloned := append([]any(nil), args...) queryer.calls = append(queryer.calls, realtimeSyncFakeCall{kind: kind, query: query, args: cloned}) } type realtimeSyncFakeRow struct { values []any err error } func (row realtimeSyncFakeRow) Scan(destinations ...any) error { if row.err != nil { return row.err } return assignRealtimeSyncFakeValues(destinations, row.values) } type realtimeSyncFakeRows struct { values [][]any index int err error } func (rows *realtimeSyncFakeRows) Close() error { return nil } func (rows *realtimeSyncFakeRows) Err() error { return rows.err } func (rows *realtimeSyncFakeRows) Next() bool { return rows.index < len(rows.values) } func (rows *realtimeSyncFakeRows) Scan(destinations ...any) error { if rows.index >= len(rows.values) { return errors.New("realtime sync fake rows exhausted") } values := rows.values[rows.index] rows.index++ return assignRealtimeSyncFakeValues(destinations, values) } func assignRealtimeSyncFakeValues(destinations []any, values []any) error { if len(destinations) != len(values) { return fmt.Errorf("scan destinations=%d values=%d", len(destinations), len(values)) } for index, destination := range destinations { value := values[index] switch target := destination.(type) { case *string: if value == nil { return fmt.Errorf("cannot scan nil into string") } *target = fmt.Sprint(value) case *int64: integer, ok := value.(int64) if !ok { return fmt.Errorf("cannot scan %T into int64", value) } *target = integer case *sql.NullString: if value == nil { *target = sql.NullString{} } else { *target = sql.NullString{String: fmt.Sprint(value), Valid: true} } case *sql.NullInt64: if value == nil { *target = sql.NullInt64{} } else if integer, ok := value.(int64); ok { *target = sql.NullInt64{Int64: integer, Valid: true} } else { return fmt.Errorf("cannot scan %T into NullInt64", value) } case *sql.NullBool: if value == nil { *target = sql.NullBool{} } else if boolean, ok := value.(bool); ok { *target = sql.NullBool{Bool: boolean, Valid: true} } else { return fmt.Errorf("cannot scan %T into NullBool", value) } default: return fmt.Errorf("unsupported scan destination %T", destination) } } return nil } func realtimeSyncTestEventRow(sequence int64) []any { return []any{ sequence, fmt.Sprintf("obe_%d", sequence), "traceEvents", fmt.Sprintf("evt_%d", sequence), int64(100) + sequence, "trc_sync", sequence, int64(10) + sequence, "trc_sync", "ses_sync", "turn_sync", nil, sequence, sequence, fmt.Sprintf("source_%d", sequence), "event", false, false, fmt.Sprintf(`{"sequence":%d}`, sequence), "2026-07-10T12:00:00Z", } }