116 lines
4.7 KiB
Go
116 lines
4.7 KiB
Go
package creator
|
|
|
|
import (
|
|
"errors"
|
|
"reflect"
|
|
"testing"
|
|
"time"
|
|
)
|
|
|
|
func TestAISettingsValidation(t *testing.T) {
|
|
if _, err := validateAISettings(SettingsUpdate{}); err != nil {
|
|
t.Fatalf("unconfigured settings: %v", err)
|
|
}
|
|
for _, input := range []SettingsUpdate{
|
|
{AIBaseURL: "https://example.com/v1"},
|
|
{AIAPIKey: "key"},
|
|
{AIModel: "model"},
|
|
{AIBaseURL: "https://example.com/v1", AIAPIKey: "key"},
|
|
{AIBaseURL: "bad-url", AIAPIKey: "key", AIModel: "model"},
|
|
} {
|
|
if _, err := validateAISettings(input); !errors.Is(err, ErrInvalid) {
|
|
t.Fatalf("incomplete settings accepted: %+v err=%v", input, err)
|
|
}
|
|
}
|
|
input, err := validateAISettings(SettingsUpdate{AIBaseURL: " https://example.com/custom/v1/ ", AIAPIKey: " key ", AIModel: " model "})
|
|
if err != nil || input.AIBaseURL != "https://example.com/custom/v1" || input.AIAPIKey != "key" || input.AIModel != "model" {
|
|
t.Fatalf("normalization=%+v err=%v", input, err)
|
|
}
|
|
}
|
|
|
|
func TestAISettingsDoNotResetActiveCollectionOrMetrics(t *testing.T) {
|
|
store, phaseAStore, ctx := openCreatorIntegrationStore(t)
|
|
accountID := createIntegrationAccount(t, ctx, phaseAStore, "ai-settings-owner")
|
|
now := time.Now().UTC().Truncate(time.Second)
|
|
start, end, err := NewCollectionWindow(now, 30)
|
|
if err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
lease, err := store.beginCheckpoint(ctx, SourceOwned, accountID, "works", start, end)
|
|
if err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
if lease == "" {
|
|
t.Fatal("checkpoint lease missing")
|
|
}
|
|
if _, err := store.db.ExecContext(ctx, `UPDATE creator_collection_checkpoint SET cursor='page-4',last_error='keep this',last_completed_at=$1 WHERE source_type=$2 AND source_id=$3`, now.Add(-time.Hour), SourceOwned, accountID); err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
before, err := store.checkpoint(ctx, SourceOwned, accountID, "works")
|
|
if err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
work, _, err := store.UpsertWork(ctx, WorkInput{Platform: "douyin", WorkKey: "ai-settings-work", SourceType: SourceOwned, SourceID: accountID, Title: "work", PublishedAt: &now, PublishedAtStatus: "verified"}, now)
|
|
if err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
var nextBefore, nextAfter *time.Time
|
|
if err := store.db.QueryRowContext(ctx, `SELECT metric_plan_next_at FROM creator_work WHERE work_id=$1`, work.ID).Scan(&nextBefore); err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
settings, err := store.GetSettings(ctx)
|
|
if err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
input := SettingsUpdate{LookbackDays: settings.LookbackDays, NewWorkIntervalSeconds: settings.NewWorkIntervalSeconds, MetricInitialIntervalSeconds: settings.MetricInitialIntervalSeconds, MetricMultiplier: settings.MetricMultiplier, MetricMaxIntervalSeconds: settings.MetricMaxIntervalSeconds, MetricAgeSeconds: settings.MetricAgeSeconds, AIBaseURL: "https://example.com/v1", AIAPIKey: "key", AIModel: "model"}
|
|
if _, err := store.UpdateSettings(ctx, input); err != nil {
|
|
t.Fatalf("AI-only save must work during collection: %v", err)
|
|
}
|
|
after, err := store.checkpoint(ctx, SourceOwned, accountID, "works")
|
|
if err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
if !reflect.DeepEqual(before, after) {
|
|
t.Fatalf("checkpoint changed: before=%+v after=%+v", before, after)
|
|
}
|
|
if err := store.db.QueryRowContext(ctx, `SELECT metric_plan_next_at FROM creator_work WHERE work_id=$1`, work.ID).Scan(&nextAfter); err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
if !reflect.DeepEqual(nextBefore, nextAfter) {
|
|
t.Fatalf("metric schedule changed: %v -> %v", nextBefore, nextAfter)
|
|
}
|
|
if _, err := store.UpdateSettings(ctx, input); err != nil {
|
|
t.Fatalf("unchanged save: %v", err)
|
|
}
|
|
input.AIBaseURL, input.AIAPIKey, input.AIModel = "", "", ""
|
|
if _, err := store.UpdateSettings(ctx, input); err != nil {
|
|
t.Fatalf("clear AI: %v", err)
|
|
}
|
|
saved, err := store.GetSettings(ctx)
|
|
if err != nil || saved.AIBaseURL != "" || saved.AIAPIKey != "" || saved.AIModel != "" {
|
|
t.Fatalf("clear was not persisted: %+v err=%v", saved, err)
|
|
}
|
|
input.LookbackDays++
|
|
if _, err := store.UpdateSettings(ctx, input); !errors.Is(err, ErrConflict) {
|
|
t.Fatalf("collection change while running: %v", err)
|
|
}
|
|
if err := store.finishCheckpoint(ctx, SourceOwned, accountID, "works", lease, "succeeded", "", nil); err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
if _, err := store.UpdateSettings(ctx, input); err != nil {
|
|
t.Fatalf("collection change after completion: %v", err)
|
|
}
|
|
if err := store.db.QueryRowContext(ctx, `SELECT metric_plan_next_at FROM creator_work WHERE work_id=$1`, work.ID).Scan(&nextAfter); err != nil || nextAfter == nil {
|
|
t.Fatalf("metric plan not recalculated: %v err=%v", nextAfter, err)
|
|
}
|
|
input.AIBaseURL = "not-a-url"
|
|
if _, err := store.UpdateSettings(ctx, input); !errors.Is(err, ErrInvalid) {
|
|
t.Fatalf("invalid URL: %v", err)
|
|
}
|
|
input.AIBaseURL = ""
|
|
input.LookbackDays = 0
|
|
if _, err := store.UpdateSettings(ctx, input); !errors.Is(err, ErrInvalid) {
|
|
t.Fatalf("invalid collection setting: %v", err)
|
|
}
|
|
}
|