Files
NianAIGC/backend/internal/orchestration/orchestration_test.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 := &quoterStub{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 := &quoterStub{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 := &quoterStub{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 := &quoterStub{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 := &quoterStub{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 := &quoterStub{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 := &quoterStub{}
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 }