package reconcile import ( "context" "errors" "fmt" "testing" "sub2api-cn-relay-manager/internal/host/sub2api" "sub2api-cn-relay-manager/internal/pack" "sub2api-cn-relay-manager/internal/store/sqlite" "sub2api-cn-relay-manager/internal/testutil" ) func TestRerunAccountProbesReturnsErrorForInvalidProbeSummary(t *testing.T) { t.Parallel() store := openReconcileTestStore(t) defer closeReconcileTestStore(t, store) fixture := seedReconcileFixture(t, store) itemID := mustCreateImportBatchItem(t, store, fixture.batchID, "fp-1", "{") svc := NewService(store, &reconcileHostStub{}) _, err := svc.rerunAccountProbes(context.Background(), []sqlite.ImportBatchItem{{ ID: itemID, BatchID: fixture.batchID, KeyFingerprint: "fp-1", AccountStatus: "pending", ProbeSummaryJSON: "{", }}, "deepseek-chat") if err == nil || err.Error() != fmt.Sprintf("decode import batch item %d probe summary: unexpected end of JSON input", itemID) { t.Fatalf("rerunAccountProbes() error = %v, want decode error", err) } } func TestRerunAccountProbesReturnsErrorForMissingAccountID(t *testing.T) { t.Parallel() store := openReconcileTestStore(t) defer closeReconcileTestStore(t, store) fixture := seedReconcileFixture(t, store) itemID := mustCreateImportBatchItem(t, store, fixture.batchID, "fp-1", `{}`) svc := NewService(store, &reconcileHostStub{}) _, err := svc.rerunAccountProbes(context.Background(), []sqlite.ImportBatchItem{{ ID: itemID, BatchID: fixture.batchID, KeyFingerprint: "fp-1", AccountStatus: "pending", ProbeSummaryJSON: `{}`, }}, "deepseek-chat") if err == nil || err.Error() != fmt.Sprintf("import batch item %d missing account_id in probe summary", itemID) { t.Fatalf("rerunAccountProbes() error = %v, want missing account_id", err) } } func TestRerunAccountProbesReturnsErrorWhenRetestFails(t *testing.T) { t.Parallel() store := openReconcileTestStore(t) defer closeReconcileTestStore(t, store) fixture := seedReconcileFixture(t, store) itemID := mustCreateImportBatchItem(t, store, fixture.batchID, "fp-1", `{"account_id":"account-1"}`) host := &reconcileHostStub{ testErrors: map[string]error{"account-1": errors.New("probe failed")}, } svc := NewService(store, host) _, err := svc.rerunAccountProbes(context.Background(), []sqlite.ImportBatchItem{{ ID: itemID, BatchID: fixture.batchID, KeyFingerprint: "fp-1", AccountStatus: "pending", ProbeSummaryJSON: `{"account_id":"account-1"}`, }}, "deepseek-chat") if err == nil || err.Error() != "re-test account account-1: probe failed" { t.Fatalf("rerunAccountProbes() error = %v, want retest failure", err) } } func TestRerunAccountProbesReturnsErrorWhenReloadModelsFails(t *testing.T) { t.Parallel() store := openReconcileTestStore(t) defer closeReconcileTestStore(t, store) fixture := seedReconcileFixture(t, store) itemID := mustCreateImportBatchItem(t, store, fixture.batchID, "fp-1", `{"account_id":"account-1"}`) host := &reconcileHostStub{ testResults: map[string]sub2api.ProbeResult{ "account-1": {OK: true, Status: "passed"}, }, modelErrors: map[string]error{"account-1": errors.New("models unavailable")}, } svc := NewService(store, host) _, err := svc.rerunAccountProbes(context.Background(), []sqlite.ImportBatchItem{{ ID: itemID, BatchID: fixture.batchID, KeyFingerprint: "fp-1", AccountStatus: "pending", ProbeSummaryJSON: `{"account_id":"account-1"}`, }}, "deepseek-chat") if err == nil || err.Error() != "reload account models account-1: models unavailable" { t.Fatalf("rerunAccountProbes() error = %v, want model reload failure", err) } } func TestRerunAccountProbesPersistsWarningsAndDeduplicatesSuspectAccounts(t *testing.T) { t.Parallel() store := openReconcileTestStore(t) defer closeReconcileTestStore(t, store) fixture := seedReconcileFixture(t, store) itemID1 := mustCreateImportBatchItem(t, store, fixture.batchID, "fp-1", `{"account_id":"account-1"}`) itemID2 := mustCreateImportBatchItem(t, store, fixture.batchID, "fp-2", `{"account_id":" account-1 "}`) host := &reconcileHostStub{ testResults: map[string]sub2api.ProbeResult{ "account-1": { OK: false, Status: "failed", Message: "API returned 403: Forbidden", }, }, models: map[string][]sub2api.AccountModel{ "account-1": {{ID: "deepseek-chat"}}, }, } svc := NewService(store, host) summary, err := svc.rerunAccountProbes(context.Background(), []sqlite.ImportBatchItem{ { ID: itemID1, BatchID: fixture.batchID, KeyFingerprint: "fp-1", AccountStatus: "pending", ProbeSummaryJSON: `{"account_id":"account-1"}`, }, { ID: itemID2, BatchID: fixture.batchID, KeyFingerprint: "fp-2", AccountStatus: "pending", ProbeSummaryJSON: `{"account_id":" account-1 "}`, }, }, "deepseek-chat") if err != nil { t.Fatalf("rerunAccountProbes() error = %v", err) } if summary.Failures != 0 { t.Fatalf("summary.Failures = %d, want 0 for advisory warning", summary.Failures) } if !summary.ResponsesCapabilitySuspect { t.Fatal("summary.ResponsesCapabilitySuspect = false, want true") } if len(summary.AccountIDs) != 1 || summary.AccountIDs[0] != "account-1" { t.Fatalf("summary.AccountIDs = %v, want [account-1]", summary.AccountIDs) } for _, itemID := range []int64{itemID1, itemID2} { items, err := store.ImportBatchItems().GetByBatchID(context.Background(), fixture.batchID) if err != nil { t.Fatalf("ImportBatchItems().GetByBatchID() error = %v", err) } var got sqlite.ImportBatchItem for _, item := range items { if item.ID == itemID { got = item break } } if got.AccountStatus != accountStatusWarning { t.Fatalf("item %d AccountStatus = %q, want %q", itemID, got.AccountStatus, accountStatusWarning) } probes, err := store.ProbeResults().GetByBatchItemID(context.Background(), itemID) if err != nil { t.Fatalf("ProbeResults().GetByBatchItemID(%d) error = %v", itemID, err) } if len(probes) != 1 || probes[0].Status != accountStatusWarning { t.Fatalf("probe results for item %d = %+v, want single warning result", itemID, probes) } } } func TestRerunAccessClosureReturnsPreviousStatusWithoutProbeAPIKey(t *testing.T) { t.Parallel() store := openReconcileTestStore(t) defer closeReconcileTestStore(t, store) fixture := seedReconcileFixture(t, store) closures := []sqlite.AccessClosureRecord{{ BatchID: fixture.batchID, ClosureType: accessModeSelfService, Status: accessStatusSelfServiceReady, }} status, checked, err := NewService(store, &reconcileHostStub{}).rerunAccessClosure( context.Background(), fixture.batchID, closures, "", "deepseek-chat", nil, false, ) if err != nil { t.Fatalf("rerunAccessClosure() error = %v", err) } if checked { t.Fatal("checked = true, want false without probe api key") } if status != accessStatusSelfServiceReady { t.Fatalf("status = %q, want %q", status, accessStatusSelfServiceReady) } } func TestRerunAccessClosureReturnsErrorWhenGatewayCheckFails(t *testing.T) { t.Parallel() store := openReconcileTestStore(t) defer closeReconcileTestStore(t, store) fixture := seedReconcileFixture(t, store) closures := mustCreateAndLoadAccessClosures(t, store, fixture.batchID, sqlite.AccessClosureRecord{ BatchID: fixture.batchID, ClosureType: accessModeSelfService, Status: accessStatusSelfServiceReady, }) host := &reconcileHostStub{gatewayErr: errors.New("gateway down")} _, _, err := NewService(store, host).rerunAccessClosure(context.Background(), fixture.batchID, closures, "user-key", "deepseek-chat", nil, false) if err == nil || err.Error() != "re-check gateway access: gateway down" { t.Fatalf("rerunAccessClosure() error = %v, want gateway failure", err) } } func TestRerunAccessClosureReturnsErrorWhenCompletionCheckFails(t *testing.T) { t.Parallel() store := openReconcileTestStore(t) defer closeReconcileTestStore(t, store) fixture := seedReconcileFixture(t, store) closures := mustCreateAndLoadAccessClosures(t, store, fixture.batchID, sqlite.AccessClosureRecord{ BatchID: fixture.batchID, ClosureType: accessModeSelfService, Status: accessStatusSelfServiceReady, }) host := &reconcileHostStub{ gatewayResult: sub2api.GatewayAccessResult{OK: true, HasExpectedModel: true}, completionErrs: []error{ errors.New("completion failed"), }, } _, _, err := NewService(store, host).rerunAccessClosure(context.Background(), fixture.batchID, closures, "user-key", "deepseek-chat", nil, false) if err == nil || err.Error() != "re-check gateway completion: completion failed" { t.Fatalf("rerunAccessClosure() error = %v, want completion failure", err) } } func TestRerunAccessClosureReturnsErrorWhenCompletionAfterRepairFails(t *testing.T) { t.Parallel() store := openReconcileTestStore(t) defer closeReconcileTestStore(t, store) fixture := seedReconcileFixture(t, store) closures := mustCreateAndLoadAccessClosures(t, store, fixture.batchID, sqlite.AccessClosureRecord{ BatchID: fixture.batchID, ClosureType: accessModeSelfService, Status: accessStatusSelfServiceReady, }) host := &reconcileHostStub{ gatewayResult: sub2api.GatewayAccessResult{OK: true, HasExpectedModel: true}, completionResults: []sub2api.GatewayCompletionResult{ {OK: false, StatusCode: 502, BodyPreview: `{"error":{"message":"No available accounts"}}`}, }, completionErrs: []error{ nil, errors.New("still failing"), }, } _, _, err := NewService(store, host).rerunAccessClosure(context.Background(), fixture.batchID, closures, "user-key", "deepseek-chat", []string{"account-1"}, true) if err == nil || err.Error() != "re-check gateway completion after capability repair: still failing" { t.Fatalf("rerunAccessClosure() error = %v, want post-repair completion failure", err) } if host.clearTempUnschedulableCalls != 1 { t.Fatalf("clearTempUnschedulableCalls = %d, want 1", host.clearTempUnschedulableCalls) } } func TestRerunAccessClosurePersistsBrokenRecordWhenRepairCannotRun(t *testing.T) { t.Parallel() store := openReconcileTestStore(t) defer closeReconcileTestStore(t, store) fixture := seedReconcileFixture(t, store) closures := mustCreateAndLoadAccessClosures(t, store, fixture.batchID, sqlite.AccessClosureRecord{ BatchID: fixture.batchID, ClosureType: accessModeSelfService, Status: accessStatusSelfServiceReady, }) host := &reconcileHostStub{ gatewayResult: sub2api.GatewayAccessResult{OK: true, StatusCode: 200, HasExpectedModel: true, Models: []string{"deepseek-chat"}}, completionResults: []sub2api.GatewayCompletionResult{ {OK: false, StatusCode: 502, ContentType: "application/json", BodyPreview: `{"error":{"message":"Upstream service temporarily unavailable"}}`}, }, disableResponsesErr: errors.New("host rejected update"), } status, checked, err := NewService(store, host).rerunAccessClosure(context.Background(), fixture.batchID, closures, "user-key", "deepseek-chat", []string{"account-1"}, true) if err != nil { t.Fatalf("rerunAccessClosure() error = %v", err) } if !checked { t.Fatal("checked = false, want true") } if status != accessStatusBroken { t.Fatalf("status = %q, want %q", status, accessStatusBroken) } if host.disableResponsesCalls != 1 { t.Fatalf("disableResponsesCalls = %d, want 1", host.disableResponsesCalls) } if host.clearTempUnschedulableCalls != 0 { t.Fatalf("clearTempUnschedulableCalls = %d, want 0 after disable failure", host.clearTempUnschedulableCalls) } if host.completionCalls != 1 { t.Fatalf("completionCalls = %d, want 1 without retry after disable failure", host.completionCalls) } records, err := store.AccessClosures().GetByBatchID(context.Background(), fixture.batchID) if err != nil { t.Fatalf("AccessClosures().GetByBatchID() error = %v", err) } if len(records) != 2 { t.Fatalf("access closure records = %d, want 2", len(records)) } if records[1].Status != accessStatusBroken { t.Fatalf("persisted access closure status = %q, want %q", records[1].Status, accessStatusBroken) } } func TestStoredResourcesForReconcileMergesCurrentBatchAndSharedScaffoldingOnly(t *testing.T) { t.Parallel() store := openReconcileTestStore(t) defer closeReconcileTestStore(t, store) fixture := seedReconcileFixture(t, store) otherBatchID := mustCreateImportBatch(t, store, fixture.hostPK, fixture.packPK, fixture.providerPK, "partial", "succeeded", "self_service_ready") mustCreateManagedResource(t, store, fixture.batchID, fixture.hostPK, "group", "group-1", "group one") mustCreateManagedResource(t, store, fixture.batchID, fixture.hostPK, "account", "account-1", "account one") mustCreateManagedResource(t, store, otherBatchID, fixture.hostPK, "group", "group-2", "shared group") mustCreateManagedResource(t, store, otherBatchID, fixture.hostPK, "channel", "channel-2", "shared channel") mustCreateManagedResource(t, store, otherBatchID, fixture.hostPK, "plan", "plan-2", "shared plan") mustCreateManagedResource(t, store, otherBatchID, fixture.hostPK, "account", "account-2", "shared account should not merge") got, err := NewService(store, &reconcileHostStub{}).storedResourcesForReconcile(context.Background(), fixture.providerPK, fixture.hostPK, fixture.batchID) if err != nil { t.Fatalf("storedResourcesForReconcile() error = %v", err) } if len(got) != 5 { t.Fatalf("storedResourcesForReconcile() len = %d, want 5; values = %+v", len(got), got) } want := map[string]bool{ "group:group-1": false, "group:group-2": false, "account:account-1": false, "channel:channel-2": false, "plan:plan-2": false, } for _, resource := range got { key := resource.ResourceType + ":" + resource.HostResourceID if _, ok := want[key]; !ok { t.Fatalf("unexpected merged resource %q in %+v", key, got) } want[key] = true } for key, seen := range want { if !seen { t.Fatalf("missing merged resource %q in %+v", key, got) } } } func TestReconcileValidatesTopLevelRequest(t *testing.T) { t.Parallel() req := Request{} if _, err := NewService(nil, &reconcileHostStub{}).Reconcile(context.Background(), req); err == nil || err.Error() != "store is required" { t.Fatalf("Reconcile(nil store) error = %v, want store is required", err) } store := openReconcileTestStore(t) defer closeReconcileTestStore(t, store) if _, err := NewService(store, nil).Reconcile(context.Background(), req); err == nil || err.Error() != "host adapter is required" { t.Fatalf("Reconcile(nil host) error = %v, want host adapter is required", err) } if _, err := NewService(store, &reconcileHostStub{}).Reconcile(context.Background(), req); err == nil || err.Error() != "host_id is required" { t.Fatalf("Reconcile(missing host_id) error = %v, want host_id is required", err) } if _, err := NewService(store, &reconcileHostStub{}).Reconcile(context.Background(), Request{HostID: "host-1"}); err == nil || err.Error() != "host_base_url is required" { t.Fatalf("Reconcile(missing host_base_url) error = %v, want host_base_url is required", err) } } func TestReconcileRejectsNonReconcilableLatestBatch(t *testing.T) { t.Parallel() store := openReconcileTestStore(t) defer closeReconcileTestStore(t, store) fixture := seedReconcileFixture(t, store) mustExecReconcileSQL(t, store, `UPDATE import_batches SET batch_status = 'failed' WHERE id = ?`, fixture.batchID) _, err := NewService(store, &reconcileHostStub{}).Reconcile(context.Background(), Request{ HostID: "host-1", HostBaseURL: "https://sub2api.example.com", AccessProbeAPIKey: "user-key", Pack: pack.LoadedPack{ Manifest: pack.Manifest{PackID: "openai-cn-pack", Version: "1.0.0", TargetHost: "sub2api"}, }, Provider: pack.ProviderManifest{ ProviderID: "deepseek", SmokeTestModel: "deepseek-chat", }, }) if err == nil || err.Error() != "latest import batch is failed; run import again before reconcile" { t.Fatalf("Reconcile() error = %v, want non-reconcilable batch error", err) } } func TestReconcilePersistsActiveRunForHealthySnapshot(t *testing.T) { t.Parallel() store := openReconcileTestStore(t) defer closeReconcileTestStore(t, store) fixture := seedReconcileFixture(t, store) mustCreateImportBatchItem(t, store, fixture.batchID, "fp-1", `{"account_id":"account-1"}`) mustCreateManagedResource(t, store, fixture.batchID, fixture.hostPK, "group", "group-1", "group one") mustCreateManagedResource(t, store, fixture.batchID, fixture.hostPK, "account", "account-1", "account one") mustCreateAndLoadAccessClosures(t, store, fixture.batchID, sqlite.AccessClosureRecord{ BatchID: fixture.batchID, ClosureType: accessModeSelfService, Status: accessStatusSelfServiceReady, }) host := &reconcileHostStub{ testResults: map[string]sub2api.ProbeResult{ "account-1": {OK: true, Status: accountStatusPassed, Message: "ok"}, }, models: map[string][]sub2api.AccountModel{ "account-1": {{ID: "deepseek-chat"}}, }, gatewayResult: sub2api.GatewayAccessResult{ OK: true, StatusCode: 200, HasExpectedModel: true, CompletionOK: true, Models: []string{"deepseek-chat"}, }, completionResults: []sub2api.GatewayCompletionResult{ {OK: true, StatusCode: 200}, }, managedResourceSnapshot: sub2api.ManagedResourceSnapshot{ Groups: []sub2api.NamedResource{{ID: "group-1", Name: "group one"}}, Accounts: []sub2api.NamedResource{{ID: "account-1", Name: "account one"}}, }, } result, err := NewService(store, host).Reconcile(context.Background(), Request{ HostID: "host-1", HostBaseURL: "https://sub2api.example.com", AccessProbeAPIKey: "user-key", Pack: pack.LoadedPack{ Manifest: pack.Manifest{PackID: "openai-cn-pack", Version: "1.0.0", TargetHost: "sub2api"}, }, Provider: pack.ProviderManifest{ ProviderID: "deepseek", SmokeTestModel: "deepseek-chat", }, }) if err != nil { t.Fatalf("Reconcile() error = %v", err) } if result.Status != "active" { t.Fatalf("result.Status = %q, want active", result.Status) } if result.MissingCount != 0 || result.ExtraCount != 0 || result.ProbeFailureCount != 0 { t.Fatalf("result = %+v, want no drift and no probe failures", result) } if result.AccessStatus != accessStatusSelfServiceReady { t.Fatalf("result.AccessStatus = %q, want %q", result.AccessStatus, accessStatusSelfServiceReady) } runs, err := store.ReconcileRuns().GetByBatchID(context.Background(), fixture.batchID) if err != nil { t.Fatalf("ReconcileRuns().GetByBatchID() error = %v", err) } if len(runs) != 1 || runs[0].Status != "active" { t.Fatalf("persisted reconcile runs = %+v, want single active run", runs) } } type reconcileFixture struct { hostPK int64 packPK int64 providerPK int64 batchID int64 } func openReconcileTestStore(t *testing.T) *sqlite.DB { t.Helper() return testutil.OpenSQLiteStore(t, testutil.SQLiteTestDSN(t, "state.db", true)) } func closeReconcileTestStore(t *testing.T, store *sqlite.DB) { t.Helper() if err := store.Close(); err != nil { t.Fatalf("store.Close() error = %v", err) } } func seedReconcileFixture(t *testing.T, store *sqlite.DB) reconcileFixture { t.Helper() hostPK, err := store.Hosts().Create(context.Background(), sqlite.Host{ HostID: "host-1", BaseURL: "https://sub2api.example.com", HostVersion: "0.1.126", AuthType: "apikey", AuthToken: "test-token", }) if err != nil { t.Fatalf("Hosts().Create() error = %v", err) } packPK, err := store.Packs().Create(context.Background(), sqlite.Pack{ PackID: "openai-cn-pack", Version: "1.0.0", Checksum: "checksum-1", }) if err != nil { t.Fatalf("Packs().Create() error = %v", err) } providerPK, err := store.Providers().Create(context.Background(), sqlite.Provider{ PackID: packPK, ProviderID: "deepseek", DisplayName: "DeepSeek", BaseURL: "https://api.example.com", Platform: "openai", AccountType: "openai", SmokeTestModel: "deepseek-chat", }) if err != nil { t.Fatalf("Providers().Create() error = %v", err) } batchID := mustCreateImportBatch(t, store, hostPK, packPK, providerPK, "partial", "partially_succeeded", "self_service_ready") return reconcileFixture{ hostPK: hostPK, packPK: packPK, providerPK: providerPK, batchID: batchID, } } func mustCreateImportBatch(t *testing.T, store *sqlite.DB, hostPK, packPK, providerPK int64, mode, batchStatus, accessStatus string) int64 { t.Helper() id, err := store.ImportBatches().Create(context.Background(), sqlite.ImportBatch{ HostID: hostPK, PackID: packPK, ProviderID: providerPK, Mode: mode, BatchStatus: batchStatus, AccessStatus: accessStatus, }) if err != nil { t.Fatalf("ImportBatches().Create() error = %v", err) } return id } func mustCreateImportBatchItem(t *testing.T, store *sqlite.DB, batchID int64, fingerprint, probeSummary string) int64 { t.Helper() id, err := store.ImportBatchItems().Create(context.Background(), sqlite.ImportBatchItem{ BatchID: batchID, KeyFingerprint: fingerprint, AccountStatus: "pending", ProbeSummaryJSON: probeSummary, }) if err != nil { t.Fatalf("ImportBatchItems().Create() error = %v", err) } return id } func mustCreateManagedResource(t *testing.T, store *sqlite.DB, batchID, hostPK int64, resourceType, hostResourceID, resourceName string) int64 { t.Helper() id, err := store.ManagedResources().Create(context.Background(), sqlite.ManagedResource{ BatchID: batchID, HostID: hostPK, ResourceType: resourceType, HostResourceID: hostResourceID, ResourceName: resourceName, }) if err != nil { t.Fatalf("ManagedResources().Create() error = %v", err) } return id } func mustCreateAndLoadAccessClosures(t *testing.T, store *sqlite.DB, batchID int64, records ...sqlite.AccessClosureRecord) []sqlite.AccessClosureRecord { t.Helper() for _, record := range records { if _, err := store.AccessClosures().Create(context.Background(), record); err != nil { t.Fatalf("AccessClosures().Create() error = %v", err) } } loaded, err := store.AccessClosures().GetByBatchID(context.Background(), batchID) if err != nil { t.Fatalf("AccessClosures().GetByBatchID() error = %v", err) } return loaded } func mustExecReconcileSQL(t *testing.T, store *sqlite.DB, query string, args ...any) { t.Helper() if _, err := store.SQLDB().Exec(query, args...); err != nil { t.Fatalf("Exec(%q) error = %v", query, err) } } type reconcileHostStub struct { disableResponsesErr error gatewayErr error gatewayResult sub2api.GatewayAccessResult testResults map[string]sub2api.ProbeResult testErrors map[string]error models map[string][]sub2api.AccountModel modelErrors map[string]error completionResults []sub2api.GatewayCompletionResult completionErrs []error completionCalls int disableResponsesCalls int disabledResponsesAccounts []string clearTempUnschedulableCalls int clearedTempUnschedulableAccounts []string managedResourceSnapshot sub2api.ManagedResourceSnapshot listManagedResourcesErr error } func (h *reconcileHostStub) GetHostVersion(context.Context) (string, error) { return "0.1.126", nil } func (h *reconcileHostStub) ProbeCapabilities(context.Context) (sub2api.HostCapabilities, error) { return sub2api.HostCapabilities{}, nil } func (h *reconcileHostStub) CreateGroup(context.Context, sub2api.CreateGroupRequest) (sub2api.GroupRef, error) { return sub2api.GroupRef{}, nil } func (h *reconcileHostStub) DeleteGroup(context.Context, string) error { return nil } func (h *reconcileHostStub) CreateChannel(context.Context, sub2api.CreateChannelRequest) (sub2api.ChannelRef, error) { return sub2api.ChannelRef{}, nil } func (h *reconcileHostStub) UpdateChannel(context.Context, string, sub2api.CreateChannelRequest) error { return nil } func (h *reconcileHostStub) DeleteChannel(context.Context, string) error { return nil } func (h *reconcileHostStub) CreatePlan(context.Context, sub2api.CreatePlanRequest) (sub2api.PlanRef, error) { return sub2api.PlanRef{}, nil } func (h *reconcileHostStub) DeletePlan(context.Context, string) error { return nil } func (h *reconcileHostStub) CreateAccount(context.Context, sub2api.CreateAccountRequest) (sub2api.AccountRef, error) { return sub2api.AccountRef{}, nil } func (h *reconcileHostStub) BatchCreateAccounts(context.Context, sub2api.BatchCreateAccountsRequest) ([]sub2api.AccountRef, error) { return nil, nil } func (h *reconcileHostStub) DeleteAccount(context.Context, string) error { return nil } func (h *reconcileHostStub) TestAccount(_ context.Context, accountID, _ string) (sub2api.ProbeResult, error) { if err, ok := h.testErrors[accountID]; ok { return sub2api.ProbeResult{}, err } if result, ok := h.testResults[accountID]; ok { return result, nil } return sub2api.ProbeResult{}, fmt.Errorf("missing test result for %s", accountID) } func (h *reconcileHostStub) GetAccountModels(_ context.Context, accountID string) ([]sub2api.AccountModel, error) { if err, ok := h.modelErrors[accountID]; ok { return nil, err } if models, ok := h.models[accountID]; ok { return models, nil } return nil, fmt.Errorf("missing models for %s", accountID) } func (h *reconcileHostStub) EnsureSubscriptionAccess(context.Context, sub2api.EnsureSubscriptionAccessRequest) (sub2api.SubscriptionAccessRef, error) { return sub2api.SubscriptionAccessRef{}, nil } func (h *reconcileHostStub) AssignSubscription(context.Context, sub2api.AssignSubscriptionRequest) (sub2api.SubscriptionRef, error) { return sub2api.SubscriptionRef{}, nil } func (h *reconcileHostStub) CheckGatewayAccess(_ context.Context, _ sub2api.GatewayAccessCheckRequest) (sub2api.GatewayAccessResult, error) { if h.gatewayErr != nil { return sub2api.GatewayAccessResult{}, h.gatewayErr } return h.gatewayResult, nil } func (h *reconcileHostStub) CheckGatewayCompletion(_ context.Context, _ sub2api.GatewayCompletionCheckRequest) (sub2api.GatewayCompletionResult, error) { idx := h.completionCalls h.completionCalls++ if idx < len(h.completionErrs) && h.completionErrs[idx] != nil { return sub2api.GatewayCompletionResult{}, h.completionErrs[idx] } if len(h.completionResults) == 0 { return sub2api.GatewayCompletionResult{}, nil } if idx >= len(h.completionResults) { idx = len(h.completionResults) - 1 } return h.completionResults[idx], nil } func (h *reconcileHostStub) DisableOpenAIResponsesAPI(_ context.Context, accountIDs []string) error { h.disableResponsesCalls++ h.disabledResponsesAccounts = append([]string(nil), accountIDs...) return h.disableResponsesErr } func (h *reconcileHostStub) ClearTempUnschedulable(_ context.Context, accountIDs []string) error { h.clearTempUnschedulableCalls++ h.clearedTempUnschedulableAccounts = append([]string(nil), accountIDs...) return nil } func (h *reconcileHostStub) ListManagedResources(context.Context, sub2api.ListManagedResourcesRequest) (sub2api.ManagedResourceSnapshot, error) { if h.listManagedResourcesErr != nil { return sub2api.ManagedResourceSnapshot{}, h.listManagedResourcesErr } return h.managedResourceSnapshot, nil }