520 lines
24 KiB
Go
520 lines
24 KiB
Go
package orchestration
|
|
|
|
import (
|
|
"context"
|
|
"encoding/json"
|
|
"errors"
|
|
"reflect"
|
|
"testing"
|
|
"time"
|
|
|
|
"git.nianxx.cn/wangxuming/NianAIGC/backend/internal/assets"
|
|
"git.nianxx.cn/wangxuming/NianAIGC/backend/internal/billing"
|
|
"git.nianxx.cn/wangxuming/NianAIGC/backend/internal/identity"
|
|
"git.nianxx.cn/wangxuming/NianAIGC/backend/internal/jobs"
|
|
"git.nianxx.cn/wangxuming/NianAIGC/backend/internal/providers"
|
|
"git.nianxx.cn/wangxuming/NianAIGC/backend/internal/usage"
|
|
"git.nianxx.cn/wangxuming/NianAIGC/backend/internal/webhook"
|
|
)
|
|
|
|
func TestUsageRecorderMapsPlatformJobAndSkipsPublicJobs(t *testing.T) {
|
|
sink := &usageSinkStub{}
|
|
recorder := NewUsageRecorder(sink, func() string { return "usage-1" }, func() time.Time {
|
|
return time.Date(2026, 8, 13, 8, 0, 0, 0, time.UTC)
|
|
})
|
|
job := jobs.Job{ID: "job-1", OwnerID: "owner-1", Capability: "image.generate", Provider: "bailian", ReqKey: "wanx", UsageContext: json.RawMessage(`{"source":"platform","username":"alice","displayName":"Alice","tenantId":"tenant-1","organizationId":"org-1","organizationName":"Org"}`), Billing: json.RawMessage(`{"quantity":2,"unit":"image","amountFen":35,"currency":"CNY"}`)}
|
|
if err := recorder.Record(context.Background(), job); err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
want := usage.Event{ID: "usage-1", OwnerID: "owner-1", JobID: "job-1", Source: "platform", Capability: "image.generate", Provider: "bailian", ReqKey: "wanx", AccountUsername: "alice", AccountDisplayName: "Alice", TenantID: "tenant-1", OrganizationID: "org-1", OrganizationName: "Org", Quantity: 2, EstimatedUnit: "image", ChargedAmountFen: int64Pointer(35), Currency: "CNY", CreatedAt: "2026-08-13T08:00:00Z"}
|
|
if !reflect.DeepEqual(sink.event, want) {
|
|
t.Fatalf("event = %#v, want %#v", sink.event, want)
|
|
}
|
|
if err := recorder.Record(context.Background(), jobs.Job{ID: "public", ExternalClientID: "partner"}); err != nil || sink.calls != 1 {
|
|
t.Fatalf("public Record err=%v calls=%d", err, sink.calls)
|
|
}
|
|
}
|
|
|
|
func TestUsageRecorderReturnsGenericError(t *testing.T) {
|
|
recorder := NewUsageRecorder(&usageSinkStub{err: errors.New("postgres password leaked")}, func() string { return "id" }, time.Now)
|
|
err := recorder.Record(context.Background(), jobs.Job{ID: "job", Provider: "bailian"})
|
|
if err == nil || err.Error() != "record generation usage" {
|
|
t.Fatalf("error = %v", err)
|
|
}
|
|
}
|
|
|
|
func TestTerminalRefundUsesLedgerKeyAndPersistsRefundedBilling(t *testing.T) {
|
|
poster := &walletPosterStub{posting: billing.WalletPosting{LedgerID: "refund-ledger", CreatedAt: time.Date(2026, 8, 13, 8, 1, 0, 0, time.UTC)}}
|
|
ledger := billing.Ledger{Poster: poster, NewID: func() string { return "refund-ledger" }}
|
|
writer := &stateWriterStub{}
|
|
adapter := NewTerminalRefund(ledger, writer)
|
|
job := jobs.Job{ID: "job-1", OwnerID: "owner", Capability: "video.generate", Status: jobs.StatusFailed, UsageContext: json.RawMessage(`{"accountId":"account-1","organizationId":"org-1"}`), Billing: json.RawMessage(`{"status":"charged","amountFen":88,"ledgerEntryId":"charge-ledger","quotaExempt":false}`)}
|
|
got, err := adapter.Refund(context.Background(), job, "provider failed")
|
|
if err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
if poster.params.JobID != "job-1" || poster.params.DeltaFen != 88 || poster.params.OrganizationID != "org-1" || poster.params.AccountID != "account-1" || poster.params.IdempotencyKey != "job-refund:job-1" {
|
|
t.Fatalf("refund posting = %#v", poster.params)
|
|
}
|
|
if poster.params.Description != "视频生成失败退款 · provider failed" || writer.jobID != "job-1" {
|
|
t.Fatalf("description=%q writer=%q", poster.params.Description, writer.jobID)
|
|
}
|
|
quote, _ := poster.params.Metadata["quote"].(map[string]any)
|
|
if quote["status"] != "charged" {
|
|
t.Fatalf("refund metadata quote mutated = %#v", quote)
|
|
}
|
|
var state map[string]any
|
|
if err := json.Unmarshal(got.Billing, &state); err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
if state["status"] != "refunded" || state["refundLedgerEntryId"] != "refund-ledger" || state["refundReason"] != "provider failed" {
|
|
t.Fatalf("billing = %#v", state)
|
|
}
|
|
}
|
|
|
|
func TestTerminalRefundNoopsUnlessChargedAndHidesDependencyErrors(t *testing.T) {
|
|
ledger := &refundLedgerStub{err: errors.New("wallet schema secret")}
|
|
writer := &stateWriterStub{}
|
|
adapter := NewTerminalRefund(ledger, writer)
|
|
unchanged := jobs.Job{ID: "pending", Status: jobs.StatusFailed, Billing: json.RawMessage(`{"status":"pending","amountFen":10}`)}
|
|
if got, err := adapter.Refund(context.Background(), unchanged, "x"); err != nil || string(got.Billing) != string(unchanged.Billing) || ledger.calls != 0 {
|
|
t.Fatalf("noop = %#v, %v calls=%d", got, err, ledger.calls)
|
|
}
|
|
charged := jobs.Job{ID: "charged", Status: jobs.StatusFailed, UsageContext: json.RawMessage(`{"organizationId":"org"}`), Billing: json.RawMessage(`{"status":"charged","amountFen":10}`)}
|
|
if _, err := adapter.Refund(context.Background(), charged, "x"); err == nil || err.Error() != "refund generation charge" {
|
|
t.Fatalf("error = %v", err)
|
|
}
|
|
}
|
|
|
|
func TestWebhookBridgePreservesResult(t *testing.T) {
|
|
deliverer := &webhookStub{result: webhook.Result{Attempts: 2, LastStatus: &webhook.LastStatus{OK: true, Status: 204}}}
|
|
bridge := NewWebhookBridge(deliverer)
|
|
got, err := bridge.Deliver(context.Background(), jobs.Job{ID: "job"})
|
|
if err != nil || got.Attempts != 2 || !reflect.DeepEqual(got.LastStatus, deliverer.result.LastStatus) {
|
|
t.Fatalf("Deliver = %#v, %v", got, err)
|
|
}
|
|
}
|
|
|
|
func TestOutputRegisteringProcessorRegistersAndPersistsSuccessOutputs(t *testing.T) {
|
|
inner := &processorStub{job: jobs.Job{ID: "job-1", OwnerID: "owner", Capability: "image.generate", Status: jobs.StatusSucceeded}}
|
|
registrar := &outputRegistrarStub{ids: []string{"asset-1", "asset-2"}}
|
|
writer := &stateWriterStub{}
|
|
processor := NewOutputRegisteringProcessor(inner, registrar, writer)
|
|
got, err := processor.Advance(context.Background(), jobs.Job{ID: "job-1"})
|
|
if err != nil || !reflect.DeepEqual(got.OutputAssetIDs, registrar.ids) || !reflect.DeepEqual(writer.outputIDs, registrar.ids) {
|
|
t.Fatalf("Advance = %#v, %v writer=%#v", got, err, writer.outputIDs)
|
|
}
|
|
if _, err := processor.Advance(context.Background(), got); err != nil || registrar.calls != 1 {
|
|
t.Fatalf("idempotent Advance error=%v calls=%d", err, registrar.calls)
|
|
}
|
|
}
|
|
|
|
func TestAssetOutputRegistrarUsesGeneratedOwnerScopedAssets(t *testing.T) {
|
|
creator := &assetCreatorStub{}
|
|
registrar := NewAssetOutputRegistrar(creator, func(job jobs.Job) ([]string, error) {
|
|
return []string{"https://cdn.test/one.png", "https://cdn.test/two.png"}, nil
|
|
})
|
|
ids, err := registrar.Register(context.Background(), jobs.Job{ID: "job-1", OwnerID: "owner", Capability: "image.generate"})
|
|
if err != nil || !reflect.DeepEqual(ids, []string{"asset-1", "asset-2"}) {
|
|
t.Fatalf("Register = %#v, %v", ids, err)
|
|
}
|
|
if creator.scopes[0] != assets.PlatformScope("owner") || creator.commands[0].Source != assets.SourceGenerated || !reflect.DeepEqual(creator.commands[0].Tags, []string{"generated", "image.generate", "job:job-1", "output:0"}) {
|
|
t.Fatalf("asset request = %#v %#v", creator.scopes[0], creator.commands[0])
|
|
}
|
|
creator.existing = []assets.Asset{{ID: "asset-existing", Tags: []string{"job:job-1", "output:0"}}}
|
|
ids, err = registrar.Register(context.Background(), jobs.Job{ID: "job-1", OwnerID: "owner", Capability: "image.generate"})
|
|
if err != nil || ids[0] != "asset-existing" || len(creator.commands) != 3 {
|
|
t.Fatalf("retry Register = %#v, %v creates=%d", ids, err, len(creator.commands))
|
|
}
|
|
}
|
|
|
|
func TestOutputRegisteringProcessorCreatesAndPersistsMockAssetWithoutRemoteFetch(t *testing.T) {
|
|
for _, capability := range []string{"image.generate", "video.generate"} {
|
|
t.Run(capability, func(t *testing.T) {
|
|
creator := &assetCreatorStub{}
|
|
registrar := NewAssetOutputRegistrar(creator, ResolveProviderOutputURLs)
|
|
completed := jobs.Job{
|
|
ID: "job-mock", OwnerID: "owner", Provider: "mock", Capability: capability,
|
|
Prompt: "mock output", Status: jobs.StatusSucceeded,
|
|
ResponsePayload: json.RawMessage(`{"status":"succeeded","outputUrls":["/generated-results/mock-task"]}`),
|
|
}
|
|
writer := &stateWriterStub{}
|
|
processor := NewOutputRegisteringProcessor(&processorStub{job: completed}, registrar, writer)
|
|
got, err := processor.Advance(context.Background(), jobs.Job{ID: "job-mock"})
|
|
if err != nil || !reflect.DeepEqual(got.OutputAssetIDs, []string{"asset-1"}) || !reflect.DeepEqual(writer.outputIDs, []string{"asset-1"}) || len(creator.mockCommands) != 1 || creator.mockCommands[0].JobID != "job-mock" {
|
|
t.Fatalf("Advance = %#v, %v mock=%#v persisted=%#v", got, err, creator.mockCommands, writer.outputIDs)
|
|
}
|
|
wantKind := assets.KindImage
|
|
if capability == "video.generate" {
|
|
wantKind = assets.KindVideo
|
|
}
|
|
if creator.mockCommands[0].Kind != wantKind || len(creator.commands) != 0 {
|
|
t.Fatalf("mock command=%#v remote imports=%#v", creator.mockCommands[0], creator.commands)
|
|
}
|
|
})
|
|
}
|
|
}
|
|
|
|
func TestResolveProviderOutputURLsAcceptsCurrentShapesAndDeduplicates(t *testing.T) {
|
|
job := jobs.Job{ResponsePayload: json.RawMessage(`{"data":{"image_urls":["https://cdn.test/a.png","javascript:alert(1)"],"results":[{"url":"https://cdn.test/a.png"},{"url":"http://cdn.test/b.png"}]},"unrelated":"https://secret.test/not-output"}`)}
|
|
got, err := ResolveProviderOutputURLs(job)
|
|
if err != nil || !reflect.DeepEqual(got, []string{"https://cdn.test/a.png", "http://cdn.test/b.png"}) {
|
|
t.Fatalf("ResolveProviderOutputURLs = %#v, %v", got, err)
|
|
}
|
|
if _, err := ResolveProviderOutputURLs(jobs.Job{ResponsePayload: json.RawMessage(`{"status":"done"}`)}); err == nil || err.Error() != "resolve generation outputs" {
|
|
t.Fatalf("empty error = %v", err)
|
|
}
|
|
if _, err := ResolveProviderOutputURLs(jobs.Job{ResponsePayload: json.RawMessage(`{"outputUrls":["/generated-results/mock-task"]}`)}); err == nil || err.Error() != "resolve generation outputs" {
|
|
t.Fatalf("relative URL error = %v", err)
|
|
}
|
|
}
|
|
|
|
func TestCreationCoordinatorQuotesPersistsThenChargesPlatformJob(t *testing.T) {
|
|
order := []string{}
|
|
creator := &creationStoreStub{order: &order}
|
|
quoter := "erStub{order: &order, quote: &billing.Quote{Provider: "bailian", Capability: "image.generate", ReqKey: "wanx", Unit: billing.UnitImage, Quantity: 2, AmountFen: 35, Currency: "CNY"}}
|
|
charger := &chargeLedgerStub{order: &order, posting: billing.WalletPosting{LedgerID: "charge-ledger", CreatedAt: time.Date(2026, 8, 13, 8, 2, 0, 0, time.UTC)}}
|
|
state := &creationStateStub{order: &order}
|
|
coordinator := NewCreationCoordinator(platformBuilderStub{}, creator, quoter, charger, state)
|
|
session := identity.Session{User: identity.User{ID: "account-1", Username: "alice", DisplayName: "Alice", TenantID: "tenant-1", OrganizationID: "org-1", OrganizationName: "Org", Role: "user"}}
|
|
created, reused, err := coordinator.CreatePlatform(context.Background(), session, CreationInput{Capability: "image.generate", Body: map[string]any{"prompt": "hello", "settings": map[string]any{"imageCount": 2}}, IdempotencyKey: "idem"})
|
|
if err != nil || reused || !reflect.DeepEqual(order, []string{"quote", "create", "charge", "billing"}) {
|
|
t.Fatalf("CreatePlatform = %#v,%v,%v order=%#v", created, reused, err, order)
|
|
}
|
|
if charger.request.JobID != "job-1" || charger.request.AmountFen != 35 || charger.request.OrganizationID != "org-1" || charger.request.AccountID != "account-1" {
|
|
t.Fatalf("charge = %#v", charger.request)
|
|
}
|
|
var use map[string]any
|
|
_ = json.Unmarshal(created.UsageContext, &use)
|
|
if use["username"] != "alice" || use["organizationId"] != "org-1" || use["source"] != "platform" {
|
|
t.Fatalf("usageContext = %#v", use)
|
|
}
|
|
var charged map[string]any
|
|
_ = json.Unmarshal(created.Billing, &charged)
|
|
if charged["status"] != "charged" || charged["ledgerEntryId"] != "charge-ledger" {
|
|
t.Fatalf("billing = %#v", charged)
|
|
}
|
|
if quoter.command.Parameters["imageCount"] != float64(2) {
|
|
t.Fatalf("quote parameters = %#v", quoter.command.Parameters)
|
|
}
|
|
}
|
|
|
|
func TestCreationCoordinatorMarksPersistedJobFailedWhenChargeFails(t *testing.T) {
|
|
creator := &creationStoreStub{}
|
|
quoter := "erStub{quote: &billing.Quote{AmountFen: 35, Currency: "CNY"}}
|
|
charger := &chargeLedgerStub{err: &billing.StatusError{Status: 402, Err: billing.ErrInsufficientBalance}}
|
|
state := &creationStateStub{}
|
|
coordinator := NewCreationCoordinator(platformBuilderStub{}, creator, quoter, charger, state)
|
|
session := identity.Session{User: identity.User{ID: "account", DisplayName: "A", OrganizationID: "org", Role: "user"}}
|
|
_, _, err := coordinator.CreatePlatform(context.Background(), session, CreationInput{Capability: "image.generate", Body: map[string]any{}})
|
|
if billing.HTTPStatus(err) != 402 || state.failed.ID != "job-1" || state.failed.Status != jobs.StatusFailed {
|
|
t.Fatalf("error=%v failed=%#v", err, state.failed)
|
|
}
|
|
var failedBilling map[string]any
|
|
_ = json.Unmarshal(state.failed.Billing, &failedBilling)
|
|
if failedBilling["status"] != "not_charged" {
|
|
t.Fatalf("failed billing = %#v", failedBilling)
|
|
}
|
|
}
|
|
|
|
func TestCreationCoordinatorReplaysPendingChargeWithSameJobID(t *testing.T) {
|
|
pending := jobs.Job{ID: "existing-job", OwnerID: "account", Provider: "bailian", Capability: "image.generate", ReqKey: "wanx", Status: jobs.StatusQueued, UsageContext: json.RawMessage(`{"accountId":"account","organizationId":"org"}`), Billing: json.RawMessage(`{"status":"pending","amountFen":35,"currency":"CNY"}`)}
|
|
creator := &creationStoreStub{existing: &pending}
|
|
quoter := "erStub{quote: &billing.Quote{AmountFen: 35, Currency: "CNY"}}
|
|
charger := &chargeLedgerStub{posting: billing.WalletPosting{LedgerID: "same-key-ledger"}}
|
|
coordinator := NewCreationCoordinator(platformBuilderStub{}, creator, quoter, charger, &creationStateStub{})
|
|
created, reused, err := coordinator.CreatePlatform(context.Background(), identity.Session{User: identity.User{ID: "account", DisplayName: "A", OrganizationID: "org"}}, CreationInput{Capability: "image.generate", Body: map[string]any{}, IdempotencyKey: "same"})
|
|
if err != nil || !reused || created.ID != "existing-job" || charger.request.JobID != "existing-job" {
|
|
t.Fatalf("replay = %#v,%v,%v charge=%#v", created, reused, err, charger.request)
|
|
}
|
|
}
|
|
|
|
func TestCreationCoordinatorKeepsPendingChargeUndispatchableAndActivatesAfterDurableCharge(t *testing.T) {
|
|
creator := &creationStoreStub{}
|
|
quoter := "erStub{quote: &billing.Quote{AmountFen: 35, Currency: "CNY"}}
|
|
charger := &chargeLedgerStub{posting: billing.WalletPosting{LedgerID: "charge-ledger"}}
|
|
state := &creationStateStub{}
|
|
coordinator := NewCreationCoordinator(platformBuilderStub{}, creator, quoter, charger, state)
|
|
|
|
created, _, err := coordinator.CreatePlatform(context.Background(), identity.Session{User: identity.User{ID: "account", OrganizationID: "org"}}, CreationInput{Capability: "image.generate", Body: map[string]any{"prompt": "fresh"}})
|
|
if err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
if creator.created.DispatchReadyAt != nil {
|
|
t.Fatalf("pending creation was dispatchable: %#v", creator.created)
|
|
}
|
|
if state.activatedID != created.ID {
|
|
t.Fatalf("activation = %q, want %q", state.activatedID, created.ID)
|
|
}
|
|
}
|
|
|
|
func TestCreationCoordinatorUsesAtomicChargeAndActivationWhenStoreSupportsIt(t *testing.T) {
|
|
creator := &creationStoreStub{}
|
|
quoter := "erStub{quote: &billing.Quote{AmountFen: 35, Currency: "CNY"}}
|
|
state := &atomicCreationStateStub{charged: json.RawMessage(`{"status":"charged","amountFen":35,"ledgerEntryId":"atomic"}`)}
|
|
charger := &chargeLedgerStub{}
|
|
coordinator := NewCreationCoordinator(platformBuilderStub{}, creator, quoter, charger, state)
|
|
created, _, err := coordinator.CreatePlatform(context.Background(), identity.Session{User: identity.User{ID: "account", OrganizationID: "org"}}, CreationInput{Capability: "image.generate", Body: map[string]any{"prompt": "fresh"}})
|
|
if err != nil || state.calls != 1 || charger.calls != 0 || string(created.Billing) != string(state.charged) {
|
|
t.Fatalf("created=%#v err=%v atomic=%d fallback=%d", created, err, state.calls, charger.calls)
|
|
}
|
|
}
|
|
|
|
func TestCreationCoordinatorRetryRebuildsAnyOwnedImageJobWithFreshQuote(t *testing.T) {
|
|
creator := &creationStoreStub{}
|
|
quoter := "erStub{quote: &billing.Quote{AmountFen: 41, Currency: "CNY"}}
|
|
charger := &chargeLedgerStub{posting: billing.WalletPosting{LedgerID: "fresh-ledger"}}
|
|
state := &creationStateStub{}
|
|
coordinator := NewCreationCoordinator(platformBuilderStub{}, creator, quoter, charger, state)
|
|
original := jobs.Job{ID: "old", OwnerID: "account", Capability: "image.generate", Status: jobs.StatusRunning, Priority: 73, WebhookURL: "https://hooks.test/result", InputAssetIDs: []string{"asset-1"}, Billing: json.RawMessage(`{"status":"charged","amountFen":999}`), RequestPayload: json.RawMessage(`{"capability":"image.generate","model":"wanx","prompt":"again","inputUrls":["https://in.test/a.png"],"settings":{"imageCount":2}}`)}
|
|
|
|
retried, err := coordinator.RetryPlatform(context.Background(), identity.Session{User: identity.User{ID: "account", OrganizationID: "org"}}, original)
|
|
if err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
if retried.RetryOf != "old" || retried.ID == "old" || charger.request.AmountFen != 41 {
|
|
t.Fatalf("retry=%#v charge=%#v", retried, charger.request)
|
|
}
|
|
var fresh map[string]any
|
|
_ = json.Unmarshal(retried.Billing, &fresh)
|
|
if fresh["amountFen"] != float64(41) || fresh["ledgerEntryId"] != "fresh-ledger" {
|
|
t.Fatalf("fresh billing = %#v", fresh)
|
|
}
|
|
if !reflect.DeepEqual(quoter.command.Payload["inputAssetIds"], []string{"asset-1"}) || quoter.command.Payload["webhookUrl"] != "https://hooks.test/result" || quoter.command.Payload["priority"] != 73 {
|
|
t.Fatalf("retry payload = %#v", quoter.command.Payload)
|
|
}
|
|
}
|
|
|
|
func TestSafeBillingErrorMapsPostgresWalletFailures(t *testing.T) {
|
|
tests := []struct {
|
|
name string
|
|
err error
|
|
want int
|
|
}{
|
|
{name: "insufficient", err: errors.New("charge and activate: BILLING_INSUFFICIENT_BALANCE"), want: 402},
|
|
{name: "idempotency", err: errors.New("charge and activate: BILLING_IDEMPOTENCY_PAYLOAD_MISMATCH"), want: 409},
|
|
{name: "unknown", err: errors.New("database unavailable"), want: 500},
|
|
}
|
|
for _, test := range tests {
|
|
t.Run(test.name, func(t *testing.T) {
|
|
got := safeBillingError(test.err)
|
|
if status := billing.HTTPStatus(got); status != test.want {
|
|
t.Fatalf("status = %d, want %d (error %v)", status, test.want, got)
|
|
}
|
|
if test.want == 500 && got.Error() != "charge generation job" {
|
|
t.Fatalf("unknown error leaked: %v", got)
|
|
}
|
|
})
|
|
}
|
|
}
|
|
|
|
func TestCreationCoordinatorPublicCreationNeverQuotesOrCharges(t *testing.T) {
|
|
creator := &creationStoreStub{}
|
|
quoter := "erStub{}
|
|
charger := &chargeLedgerStub{}
|
|
coordinator := NewCreationCoordinator(platformBuilderStub{}, creator, quoter, charger, &creationStateStub{})
|
|
created, _, err := coordinator.CreatePublic(context.Background(), CreationInput{OwnerID: "api:partner", ExternalClientID: "partner", Capability: "image.generate", Body: map[string]any{}})
|
|
if err != nil || created.ExternalClientID != "partner" || quoter.calls != 0 || charger.calls != 0 {
|
|
t.Fatalf("CreatePublic = %#v,%v quote=%d charge=%d", created, err, quoter.calls, charger.calls)
|
|
}
|
|
}
|
|
|
|
type usageSinkStub struct {
|
|
event usage.Event
|
|
calls int
|
|
err error
|
|
}
|
|
|
|
func (s *usageSinkStub) Record(event usage.Event) (*usage.Event, error) {
|
|
s.calls++
|
|
s.event = event
|
|
return &event, s.err
|
|
}
|
|
|
|
type refundLedgerStub struct {
|
|
request billing.RefundRequest
|
|
posting billing.WalletPosting
|
|
calls int
|
|
err error
|
|
}
|
|
|
|
type walletPosterStub struct {
|
|
params billing.WalletPostParams
|
|
posting billing.WalletPosting
|
|
err error
|
|
}
|
|
|
|
func (s *walletPosterStub) PostWalletEntry(_ context.Context, params billing.WalletPostParams) (billing.WalletPosting, error) {
|
|
s.params = params
|
|
return s.posting, s.err
|
|
}
|
|
|
|
func (s *refundLedgerStub) Refund(_ context.Context, request billing.RefundRequest) (billing.WalletPosting, error) {
|
|
s.calls++
|
|
s.request = request
|
|
return s.posting, s.err
|
|
}
|
|
|
|
type stateWriterStub struct {
|
|
jobID string
|
|
billing json.RawMessage
|
|
outputIDs []string
|
|
err error
|
|
}
|
|
|
|
func (s *stateWriterStub) WriteBilling(_ context.Context, id string, value json.RawMessage) error {
|
|
s.jobID = id
|
|
s.billing = append([]byte(nil), value...)
|
|
return s.err
|
|
}
|
|
func (s *stateWriterStub) WriteOutputAssetIDs(_ context.Context, id string, ids []string) error {
|
|
s.jobID = id
|
|
s.outputIDs = append([]string(nil), ids...)
|
|
return s.err
|
|
}
|
|
|
|
type webhookStub struct {
|
|
result webhook.Result
|
|
err error
|
|
}
|
|
|
|
func (s *webhookStub) Deliver(context.Context, jobs.Job) (webhook.Result, error) {
|
|
return s.result, s.err
|
|
}
|
|
|
|
type processorStub struct {
|
|
job jobs.Job
|
|
err error
|
|
}
|
|
|
|
func (s *processorStub) Advance(context.Context, jobs.Job) (jobs.Job, error) { return s.job, s.err }
|
|
|
|
type outputRegistrarStub struct {
|
|
ids []string
|
|
calls int
|
|
err error
|
|
}
|
|
|
|
func (s *outputRegistrarStub) Register(context.Context, jobs.Job) ([]string, error) {
|
|
s.calls++
|
|
return append([]string(nil), s.ids...), s.err
|
|
}
|
|
|
|
type assetCreatorStub struct {
|
|
scopes []assets.Scope
|
|
commands []assets.ImportGeneratedCommand
|
|
mockCommands []assets.ImportMockCommand
|
|
existing []assets.Asset
|
|
}
|
|
|
|
func (s *assetCreatorStub) List(_ context.Context, _ assets.Scope) ([]assets.Asset, error) {
|
|
return append([]assets.Asset(nil), s.existing...), nil
|
|
}
|
|
|
|
func (s *assetCreatorStub) ImportGenerated(_ context.Context, scope assets.Scope, command assets.ImportGeneratedCommand) (assets.Asset, error) {
|
|
s.scopes = append(s.scopes, scope)
|
|
s.commands = append(s.commands, command)
|
|
return assets.Asset{ID: "asset-" + string(rune('0'+len(s.commands)))}, nil
|
|
}
|
|
|
|
func (s *assetCreatorStub) ImportMock(_ context.Context, scope assets.Scope, command assets.ImportMockCommand) (assets.Asset, error) {
|
|
s.scopes = append(s.scopes, scope)
|
|
s.mockCommands = append(s.mockCommands, command)
|
|
return assets.Asset{ID: "asset-" + string(rune('0'+len(s.mockCommands)))}, nil
|
|
}
|
|
|
|
type platformBuilderStub struct{}
|
|
|
|
func (platformBuilderStub) Build(_ context.Context, owner, client, capability, idempotency string, body map[string]any) (jobs.CreateCommand, error) {
|
|
settings, _ := body["settings"].(map[string]any)
|
|
request, _ := json.Marshal(providers.Request{Capability: capability, Model: "wanx", Settings: settings})
|
|
return jobs.CreateCommand{Job: jobs.Job{ID: "job-1", OwnerID: owner, ExternalClientID: client, Capability: capability, Provider: "bailian", ReqKey: "wanx", Status: jobs.StatusQueued, IdempotencyKey: idempotency, RequestPayload: request}, IdempotencyBody: body}, nil
|
|
}
|
|
|
|
type creationStoreStub struct {
|
|
order *[]string
|
|
existing *jobs.Job
|
|
created jobs.Job
|
|
}
|
|
|
|
func (s *creationStoreStub) Create(_ context.Context, command jobs.CreateCommand) (jobs.Job, bool, error) {
|
|
if s.order != nil {
|
|
*s.order = append(*s.order, "create")
|
|
}
|
|
if s.existing != nil {
|
|
return *s.existing, true, nil
|
|
}
|
|
s.created = command.Job
|
|
return command.Job, false, nil
|
|
}
|
|
|
|
type quoterStub struct {
|
|
order *[]string
|
|
quote *billing.Quote
|
|
err error
|
|
calls int
|
|
command billing.QuoteCommand
|
|
}
|
|
|
|
func (s *quoterStub) Quote(_ context.Context, command billing.QuoteCommand) (*billing.Quote, error) {
|
|
s.calls++
|
|
s.command = command
|
|
if s.order != nil {
|
|
*s.order = append(*s.order, "quote")
|
|
}
|
|
return s.quote, s.err
|
|
}
|
|
|
|
type chargeLedgerStub struct {
|
|
order *[]string
|
|
request billing.ChargeRequest
|
|
posting billing.WalletPosting
|
|
err error
|
|
calls int
|
|
}
|
|
|
|
func (s *chargeLedgerStub) Charge(_ context.Context, request billing.ChargeRequest) (billing.WalletPosting, error) {
|
|
s.calls++
|
|
s.request = request
|
|
if s.order != nil {
|
|
*s.order = append(*s.order, "charge")
|
|
}
|
|
return s.posting, s.err
|
|
}
|
|
|
|
type creationStateStub struct {
|
|
order *[]string
|
|
failed jobs.Job
|
|
activatedID string
|
|
}
|
|
|
|
type atomicCreationStateStub struct {
|
|
creationStateStub
|
|
charged json.RawMessage
|
|
calls int
|
|
}
|
|
|
|
func (s *atomicCreationStateStub) ChargeAndActivateCreation(_ context.Context, _ billing.ChargeRequest, _ json.RawMessage) (json.RawMessage, error) {
|
|
s.calls++
|
|
return append(json.RawMessage(nil), s.charged...), nil
|
|
}
|
|
|
|
func (s *creationStateStub) WriteBilling(_ context.Context, _ string, _ json.RawMessage) error {
|
|
if s.order != nil {
|
|
*s.order = append(*s.order, "billing")
|
|
}
|
|
return nil
|
|
}
|
|
func (s *creationStateStub) FailCreation(_ context.Context, job jobs.Job) error {
|
|
s.failed = job
|
|
return nil
|
|
}
|
|
func (s *creationStateStub) ActivateCreation(_ context.Context, id string, _ json.RawMessage) error {
|
|
s.activatedID = id
|
|
if s.order != nil {
|
|
*s.order = append(*s.order, "billing")
|
|
}
|
|
return nil
|
|
}
|
|
func int64Pointer(value int64) *int64 { return &value }
|