@civitai/blocks-react 0.64.0 → 0.65.1
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +136 -12
- package/dist/hooks/useBuzzPurchase.d.ts +3 -1
- package/dist/hooks/useBuzzPurchase.js +3 -1
- package/dist/hooks/useBuzzWorkflow.d.ts +102 -24
- package/dist/hooks/useBuzzWorkflow.js +59 -33
- package/dist/hooks/useGoodPurchase.d.ts +7 -7
- package/dist/hooks/usePrepareTrainingDataset.d.ts +99 -0
- package/dist/hooks/usePrepareTrainingDataset.js +157 -0
- package/dist/hooks/useRunTraining.d.ts +151 -0
- package/dist/hooks/useRunTraining.js +212 -0
- package/dist/index.d.ts +4 -0
- package/dist/index.js +2 -0
- package/dist/internal/liveHost.d.ts +12 -1
- package/dist/internal/liveHost.js +54 -1
- package/dist/internal/mockHost.d.ts +118 -6
- package/dist/internal/mockHost.js +276 -1
- package/dist/transport/requestTimeouts.d.ts +19 -0
- package/dist/transport/requestTimeouts.js +30 -0
- package/dist/transport/validate.d.ts +27 -0
- package/dist/transport/validate.js +114 -11
- package/dist/ui/styles.d.ts +1 -1
- package/package.json +3 -3
|
@@ -47,7 +47,7 @@
|
|
|
47
47
|
* fetch chokepoints ..... 2
|
|
48
48
|
* REST endpoints ........ GET /api/v1/blocks/me
|
|
49
49
|
* tRPC procedures ....... 28 = blocks.* (13) + apps.shared.* (10) + apps.storage.* (5)
|
|
50
|
-
* switch case labels ....
|
|
50
|
+
* switch case labels .... 47, of which 10 REFUSE (enumerated under SCOPE below)
|
|
51
51
|
* --- END DERIVED ---
|
|
52
52
|
*
|
|
53
53
|
* PICKERS (Phase 1 of "make dev:live a faithful local host"): the live host
|
|
@@ -101,6 +101,12 @@
|
|
|
101
101
|
* host-chrome consent. Replies `collection-unavailable`.
|
|
102
102
|
* • CREATE_POST_FROM_APP — needs the server-resolved preview plus host-chrome
|
|
103
103
|
* confirm before a PUBLIC post is written. Replies with a refusal.
|
|
104
|
+
* • PREPARE_TRAINING_DATASET — the server refuses every `kind: 'training'`
|
|
105
|
+
* request from a dev token, so there is nothing to forward. Replies with a
|
|
106
|
+
* refusal.
|
|
107
|
+
* • RUN_TRAINING — the same dev-token refusal, plus the host-chrome consent
|
|
108
|
+
* dialog a run is confirmed in, which this harness does not have. Replies
|
|
109
|
+
* with a refusal.
|
|
104
110
|
* • PUBLISH_GENERATION_OUTPUTS — publishing requires the viewer's signed-in
|
|
105
111
|
* civitai.com session, which the local harness does not have; test
|
|
106
112
|
* publishing against the mock host — `createMockHost` or `Harness` from
|
|
@@ -181,6 +187,11 @@ export interface LiveHostOptions {
|
|
|
181
187
|
*/
|
|
182
188
|
onPickerReady?: (handle: PickerOverlayHandle) => void;
|
|
183
189
|
}
|
|
190
|
+
/**
|
|
191
|
+
* The reply both training bridges get in dev:live — see their handlers. One
|
|
192
|
+
* string so the two cannot drift.
|
|
193
|
+
*/
|
|
194
|
+
export declare const TRAINING_UNSUPPORTED_ERROR = "training is not supported in dev:live \u2014 use dev:mock";
|
|
184
195
|
/**
|
|
185
196
|
* Decoded payload of a block-token JWT (the claims `BlockTokenService.sign`
|
|
186
197
|
* stamps — see civitai `block-token.service.ts`). Everything is optional/unknown
|
|
@@ -47,7 +47,7 @@
|
|
|
47
47
|
* fetch chokepoints ..... 2
|
|
48
48
|
* REST endpoints ........ GET /api/v1/blocks/me
|
|
49
49
|
* tRPC procedures ....... 28 = blocks.* (13) + apps.shared.* (10) + apps.storage.* (5)
|
|
50
|
-
* switch case labels ....
|
|
50
|
+
* switch case labels .... 47, of which 10 REFUSE (enumerated under SCOPE below)
|
|
51
51
|
* --- END DERIVED ---
|
|
52
52
|
*
|
|
53
53
|
* PICKERS (Phase 1 of "make dev:live a faithful local host"): the live host
|
|
@@ -101,6 +101,12 @@
|
|
|
101
101
|
* host-chrome consent. Replies `collection-unavailable`.
|
|
102
102
|
* • CREATE_POST_FROM_APP — needs the server-resolved preview plus host-chrome
|
|
103
103
|
* confirm before a PUBLIC post is written. Replies with a refusal.
|
|
104
|
+
* • PREPARE_TRAINING_DATASET — the server refuses every `kind: 'training'`
|
|
105
|
+
* request from a dev token, so there is nothing to forward. Replies with a
|
|
106
|
+
* refusal.
|
|
107
|
+
* • RUN_TRAINING — the same dev-token refusal, plus the host-chrome consent
|
|
108
|
+
* dialog a run is confirmed in, which this harness does not have. Replies
|
|
109
|
+
* with a refusal.
|
|
104
110
|
* • PUBLISH_GENERATION_OUTPUTS — publishing requires the viewer's signed-in
|
|
105
111
|
* civitai.com session, which the local harness does not have; test
|
|
106
112
|
* publishing against the mock host — `createMockHost` or `Harness` from
|
|
@@ -130,6 +136,15 @@ const DEFAULT_BACKEND_BASE_URL = 'https://civitai.com';
|
|
|
130
136
|
const PAGE_SLOT_ID = 'app.page';
|
|
131
137
|
/** The budgeted-spend scope the workflow procedures require. */
|
|
132
138
|
const BUDGETED_SCOPE = 'ai:write:budgeted';
|
|
139
|
+
/**
|
|
140
|
+
* The reply both training bridges get in dev:live — see their handlers. One
|
|
141
|
+
* string so the two cannot drift.
|
|
142
|
+
*/
|
|
143
|
+
export const TRAINING_UNSUPPORTED_ERROR = 'training is not supported in dev:live — use dev:mock';
|
|
144
|
+
/** The once-per-session console line for a refused training bridge. */
|
|
145
|
+
const trainingUnsupportedLog = (type) => `${type} is not supported in dev:live (the server refuses every training request from a ` +
|
|
146
|
+
'dev token, and a run needs the host-chrome consent dialog). Replying with a refusal. ' +
|
|
147
|
+
'Use dev:mock to exercise the training flow.';
|
|
133
148
|
/**
|
|
134
149
|
* Decode the middle (payload) segment of a JWT (base64url) WITHOUT verifying
|
|
135
150
|
* the signature — client-side we only need the claims to build BLOCK_INIT; the
|
|
@@ -967,6 +982,44 @@ export function createLiveHost(options) {
|
|
|
967
982
|
});
|
|
968
983
|
return;
|
|
969
984
|
}
|
|
985
|
+
case 'PREPARE_TRAINING_DATASET': {
|
|
986
|
+
// UNSUPPORTED in live, and here the SERVER decides it, not this
|
|
987
|
+
// harness: every `kind: 'training'` procedure refuses a dev token
|
|
988
|
+
// ("training is not available from a development or review session"
|
|
989
|
+
// — `assertTrainingRequestAllowed` in civitai/civitai), because there
|
|
990
|
+
// is no host page whose chrome can show the viewer a price. A
|
|
991
|
+
// training ESTIMATE is still forwarded (ESTIMATE_WORKFLOW above) and
|
|
992
|
+
// comes back carrying that same server refusal.
|
|
993
|
+
//
|
|
994
|
+
// FREE TEXT on the message's own reply channel; no host code is
|
|
995
|
+
// honest about "this harness cannot do it". Use dev:mock — its
|
|
996
|
+
// `trainingDatasetError` / `trainingDatasetRejected` knobs cover the
|
|
997
|
+
// refusal set.
|
|
998
|
+
if (!isRoutableRequestId(requestId))
|
|
999
|
+
return;
|
|
1000
|
+
logOnce('training-dataset', trainingUnsupportedLog('PREPARE_TRAINING_DATASET'));
|
|
1001
|
+
dispatchToBlock({
|
|
1002
|
+
type: 'TRAINING_DATASET_RESULT',
|
|
1003
|
+
payload: { requestId, error: TRAINING_UNSUPPORTED_ERROR },
|
|
1004
|
+
});
|
|
1005
|
+
return;
|
|
1006
|
+
}
|
|
1007
|
+
case 'RUN_TRAINING': {
|
|
1008
|
+
// UNSUPPORTED in live for the same server-side reason as
|
|
1009
|
+
// PREPARE_TRAINING_DATASET directly above — and doubly so: a run is
|
|
1010
|
+
// CHARGED and confirmed in host chrome this harness does not have, so
|
|
1011
|
+
// faking either half would let dev prove out a flow production does
|
|
1012
|
+
// not have. Use dev:mock — its `runTrainingError` knob covers
|
|
1013
|
+
// `declined`, `submission-unconfirmed` and the rest.
|
|
1014
|
+
if (!isRoutableRequestId(requestId))
|
|
1015
|
+
return;
|
|
1016
|
+
logOnce('training-run', trainingUnsupportedLog('RUN_TRAINING'));
|
|
1017
|
+
dispatchToBlock({
|
|
1018
|
+
type: 'TRAINING_RESULT',
|
|
1019
|
+
payload: { requestId, error: TRAINING_UNSUPPORTED_ERROR },
|
|
1020
|
+
});
|
|
1021
|
+
return;
|
|
1022
|
+
}
|
|
970
1023
|
case 'GET_WILDCARD_PACK': {
|
|
971
1024
|
// UNSUPPORTED in live v1 (honest-by-design, like OPEN_BUZZ_PURCHASE):
|
|
972
1025
|
// the real host resolves the pack via a SESSION-authed proc and does
|
|
@@ -37,7 +37,7 @@
|
|
|
37
37
|
* called). Only the postMessage bridge round-trips are exercised. Never import
|
|
38
38
|
* this from production code.
|
|
39
39
|
*/
|
|
40
|
-
import { type BlockContext, type BlockResourceInfo, type BlockResourcePickerType, type BlockUploadedImageInfo, type BlockGenerationSourceImageInfo, type BuzzAccountType, type BlockBuzzTransaction, type BlockBuzzAccount, type BlockDailyCompensationResource, type BlockViewer, type BlockWildcardPack, type BlockWildcardPackErrorCode, type AppWorkflow, type BlockPublishedModel, type BlockGatedImage, type BlockCollectionFollowErrorCode, type BlockCreatePostHostError, type BlockCreatePostResult, type ColorDomain, type SharedStorageValue, type Theme, type ViewerInfo, type WorkflowBody } from '@civitai/app-sdk/blocks';
|
|
40
|
+
import { type BlockContext, type BlockResourceInfo, type BlockResourcePickerType, type BlockUploadedImageInfo, type BlockGenerationSourceImageInfo, type BuzzAccountType, type BlockBuzzTransaction, type BlockBuzzAccount, type BlockDailyCompensationResource, type BlockViewer, type BlockWildcardPack, type BlockWildcardPackErrorCode, type AppWorkflow, type BlockPublishedModel, type BlockGatedImage, type BlockCollectionFollowErrorCode, type BlockCreatePostHostError, type BlockCreatePostResult, type BlockPrepareTrainingDatasetHostError, type BlockRunTrainingHostError, type BlockTrainingRejectionReason, type ColorDomain, type SharedStorageValue, type Theme, type ViewerInfo, type WorkflowBody } from '@civitai/app-sdk/blocks';
|
|
41
41
|
/**
|
|
42
42
|
* How submits behave. `'none'` = everything succeeds; `'all'` / `'insufficient'`
|
|
43
43
|
* = every submit RESOLVES an insufficient-Buzz `failed` snapshot, priced
|
|
@@ -179,10 +179,13 @@ export interface MockGenerationScenario {
|
|
|
179
179
|
*/
|
|
180
180
|
images?: string[] | ((req: WorkflowBody) => string[]);
|
|
181
181
|
/**
|
|
182
|
-
* How many trained epochs a succeeded
|
|
182
|
+
* How many trained epochs a succeeded training run reports on
|
|
183
183
|
* {@link BlockWorkflowSnapshot.trainedEpochs} — epochs `1…n`, each tagged with
|
|
184
|
-
* the
|
|
185
|
-
*
|
|
184
|
+
* the run's `$type`. Applies only to a PASS-THROUGH `{ kind: 'step', $type:
|
|
185
|
+
* 'training' }` / `'imageResourceTraining'` body (tagged with that `$type`) and
|
|
186
|
+
* to a `kind: 'training'` run started through `RUN_TRAINING` (tagged
|
|
187
|
+
* `'training'`, the step the host builds for it); every other body is
|
|
188
|
+
* unaffected.
|
|
186
189
|
*
|
|
187
190
|
* Default `0`: the field is omitted, as the host omits it for a run that
|
|
188
191
|
* produced no checkpoint. Opt in with `n > 0` to simulate a finished run —
|
|
@@ -200,6 +203,11 @@ export interface MockGenerationScenario {
|
|
|
200
203
|
* NOT simulated: the mock neither quotes nor times out, so a success here
|
|
201
204
|
* says nothing about whether your run is quoted, what it costs, or whether
|
|
202
205
|
* it finishes.
|
|
206
|
+
*
|
|
207
|
+
* Snapshot-only: the app-queue read (`QUERY_APP_WORKFLOWS`) replies with the
|
|
208
|
+
* canned {@link MockHostOptions.appWorkflows} rows verbatim — they carry no
|
|
209
|
+
* body, so the mock cannot tell a training row from any other. To exercise
|
|
210
|
+
* {@link AppWorkflow.trainedEpochs}, put it on the rows you pass there.
|
|
203
211
|
*/
|
|
204
212
|
trainedEpochs?: number;
|
|
205
213
|
/**
|
|
@@ -207,10 +215,38 @@ export interface MockGenerationScenario {
|
|
|
207
215
|
* {@link BlockWorkflowSnapshot.publishedModel} — simulating a run the viewer
|
|
208
216
|
* has started (`published: false`) or finished (`published: true`) publishing
|
|
209
217
|
* through the model wizard. Default: unset (field omitted). For the app-queue
|
|
210
|
-
* read, put `publishedModel` on the rows you pass as
|
|
218
|
+
* read, put `publishedModel` (and `trainedEpochs`) on the rows you pass as
|
|
219
|
+
* `appWorkflows`.
|
|
211
220
|
*/
|
|
212
221
|
trainingPublishedModel?: BlockPublishedModel;
|
|
213
222
|
}
|
|
223
|
+
/**
|
|
224
|
+
* The training bounds the mock enforces — COPIES of `@civitai/app-sdk/blocks`'s
|
|
225
|
+
* `BLOCK_TRAINING_DATASET_MAX_ITEMS`, `BLOCK_TRAINING_CAPTION_MAX_CHARS` and
|
|
226
|
+
* `BLOCK_TRAINING_MAX_BUZZ_PER_RUN`, deliberately not value-imported: a value
|
|
227
|
+
* import of a symbol the newest PUBLISHED app-sdk lacks raises this package's
|
|
228
|
+
* peer floor (`tests/guards/blocks-react-peer-floor.test.mjs`). The copy is held
|
|
229
|
+
* to the SDK's value by `test/trainingKindFlow.test.tsx`, which imports both.
|
|
230
|
+
*/
|
|
231
|
+
export declare const MOCK_TRAINING_BOUNDS: {
|
|
232
|
+
readonly datasetMaxItems: 50;
|
|
233
|
+
readonly captionMaxChars: 1000;
|
|
234
|
+
readonly maxBuzzPerRun: 5000;
|
|
235
|
+
};
|
|
236
|
+
/** The real server's refusal for a training request from a token without the spend scope. */
|
|
237
|
+
export declare const MOCK_TRAINING_SCOPE_ERROR = "block lacks ai:write:budgeted scope";
|
|
238
|
+
/** The real server's refusal for an unknown, expired or spent quote on `RUN_TRAINING`. */
|
|
239
|
+
export declare const MOCK_TRAINING_QUOTE_GONE_ERROR = "training quote not found, expired or already used \u2014 estimate again";
|
|
240
|
+
/** The real server's refusal for a training estimate naming a dataset it does not hold. */
|
|
241
|
+
export declare const MOCK_TRAINING_DATASET_GONE_ERROR = "training dataset not found or expired";
|
|
242
|
+
/**
|
|
243
|
+
* The real server's two refusals for a dataset with NOTHING admitted
|
|
244
|
+
* (`prepareBlockTrainingDataset`): every image failed eligibility, or every
|
|
245
|
+
* eligible image failed its import. The server THROWS rather than returning
|
|
246
|
+
* `count: 0`, so a successful reply always admits at least one image.
|
|
247
|
+
*/
|
|
248
|
+
export declare const MOCK_TRAINING_NONE_ELIGIBLE_ERROR = "none of the requested images can be used for training";
|
|
249
|
+
export declare const MOCK_TRAINING_NONE_IMPORTED_ERROR = "none of the requested images could be prepared for training";
|
|
214
250
|
/**
|
|
215
251
|
* BUZZ scenario controls — simulate a balance so the insufficient-Buzz / top-up
|
|
216
252
|
* UX is exercisable. The mock host treats `balance` as a spendable wallet:
|
|
@@ -619,6 +655,82 @@ export interface MockHostOptions {
|
|
|
619
655
|
* exercised by `declined`, never by the timing or the content.
|
|
620
656
|
*/
|
|
621
657
|
createPostError?: BlockCreatePostHostError | string;
|
|
658
|
+
/**
|
|
659
|
+
* Images `PREPARE_TRAINING_DATASET` leaves out, with their reasons — what
|
|
660
|
+
* `usePrepareTrainingDataset()` reports on `rejected`. Only entries whose
|
|
661
|
+
* `imageId` the request actually named are reported, and `count` is the rest,
|
|
662
|
+
* exactly as the server derives it. Absent → every image is admitted.
|
|
663
|
+
* Live-tunable via {@link MockHost.setScenario}.
|
|
664
|
+
*
|
|
665
|
+
* When this rejects EVERY named image the reply is an `error`, as on the real
|
|
666
|
+
* server — {@link MOCK_TRAINING_NONE_ELIGIBLE_ERROR}, or
|
|
667
|
+
* {@link MOCK_TRAINING_NONE_IMPORTED_ERROR} when every image that passed
|
|
668
|
+
* eligibility was rejected with an `import-*` reason. A success never has
|
|
669
|
+
* `count: 0`.
|
|
670
|
+
*
|
|
671
|
+
* KIND-FAITHFUL, like the rest of the training path: the mock holds the
|
|
672
|
+
* datasets it prepared, an estimate naming any other `datasetId` fails as the
|
|
673
|
+
* server's does, a quote is run at most once, and every training call needs
|
|
674
|
+
* `ai:write:budgeted` on the token ({@link MockHostOptions.consentGranted}) and
|
|
675
|
+
* a signed-in viewer. What it cannot model: image ownership and moderation (it
|
|
676
|
+
* has no images), the flag, and the page-only / dev-token refusals.
|
|
677
|
+
*
|
|
678
|
+
* ⚠️ The quote↔body check is NARROWER than the server's: the mock compares only
|
|
679
|
+
* `datasetId`, while the server hashes the whole body (minus `quoteId`) and
|
|
680
|
+
* refuses any change. A body edited between estimate and run passes here and
|
|
681
|
+
* fails in production.
|
|
682
|
+
*/
|
|
683
|
+
trainingDatasetRejected?: Array<{
|
|
684
|
+
imageId: number;
|
|
685
|
+
reason: BlockTrainingRejectionReason;
|
|
686
|
+
}>;
|
|
687
|
+
/**
|
|
688
|
+
* Force `PREPARE_TRAINING_DATASET` to reply with an `error` — a
|
|
689
|
+
* {@link BlockPrepareTrainingDatasetHostError} for a host refusal, or any other
|
|
690
|
+
* string for the free-text server-message variant. Absent → it prepares.
|
|
691
|
+
* Live-tunable via {@link MockHost.setScenario}.
|
|
692
|
+
*/
|
|
693
|
+
trainingDatasetError?: BlockPrepareTrainingDatasetHostError | string;
|
|
694
|
+
/**
|
|
695
|
+
* The Buzz total a `kind: 'training'` estimate quotes. Default `500`. Above
|
|
696
|
+
* `BLOCK_TRAINING_MAX_BUZZ_PER_RUN` (5,000) the estimate fails exactly as the
|
|
697
|
+
* server's does. The run's terminal snapshot reports this as its `cost`.
|
|
698
|
+
* Live-tunable via {@link MockHost.setScenario}.
|
|
699
|
+
*/
|
|
700
|
+
trainingQuoteTotal?: number;
|
|
701
|
+
/**
|
|
702
|
+
* Force `RUN_TRAINING` to reply with an `error` instead of starting a run — a
|
|
703
|
+
* {@link BlockRunTrainingHostError} (`'declined'`: the viewer dismissed the
|
|
704
|
+
* dialog, no run; `'submission-unconfirmed'`: the run MAY exist) or any other
|
|
705
|
+
* string for a server message. Absent → the run starts.
|
|
706
|
+
*
|
|
707
|
+
* Whether the QUOTE survives follows the real flow: the host's own gate and
|
|
708
|
+
* dialog codes (`declined`, `review-mode`, `block is not ready`, `sign in to
|
|
709
|
+
* train`, `invalid training request`, `no block token`) settle before the
|
|
710
|
+
* server claims the quote, so it stays runnable; `submission-unconfirmed` and
|
|
711
|
+
* any server message settle after the claim, so it is used up — re-running it
|
|
712
|
+
* gets the quote-gone error. Live-tunable via {@link MockHost.setScenario};
|
|
713
|
+
* `undefined` clears it.
|
|
714
|
+
*
|
|
715
|
+
* 🔴 Same real gap as {@link createPostError}: the consent dialog is HOST
|
|
716
|
+
* chrome, so the mock settles immediately where the real host waits on a click.
|
|
717
|
+
*/
|
|
718
|
+
runTrainingError?: BlockRunTrainingHostError | string;
|
|
719
|
+
/**
|
|
720
|
+
* Make `RUN_TRAINING` resolve the server's spend-cap or temporary-availability
|
|
721
|
+
* refusal instead of a run:
|
|
722
|
+
* a snapshot `{ workflowId: 'failed', status: 'failed', cost: { total: <quote> },
|
|
723
|
+
* error: <this string> }` — the shape the real training submit returns when
|
|
724
|
+
* the viewer's daily / private-run Buzz cap, the per-app consent budget, the
|
|
725
|
+
* app's spend or rate limit (or its "temporarily unavailable" deny), or a
|
|
726
|
+
* dev-session cap stops the run (refunded, no run). `useRunTraining()` rejects
|
|
727
|
+
* it with `.refused`. Pass the server's text, e.g. `'daily Buzz cap reached: …'`.
|
|
728
|
+
* 🔴 The quote is CONSUMED, as on the server (it is claimed before the cap
|
|
729
|
+
* checks): running the same `quoteId` again gets the quote-gone error — estimate
|
|
730
|
+
* again. Absent → the run starts. Live-tunable via {@link MockHost.setScenario};
|
|
731
|
+
* `setScenario({ runTrainingCapRefusal: undefined })` clears it.
|
|
732
|
+
*/
|
|
733
|
+
runTrainingCapRefusal?: string;
|
|
622
734
|
/**
|
|
623
735
|
* Buzz pools a `SUBMIT_WORKFLOW` must REJECT when named in `body.accountType`
|
|
624
736
|
* — simulates the real backend's content-rating clamp. The real host throws a
|
|
@@ -783,7 +895,7 @@ export interface MockHostOptions {
|
|
|
783
895
|
* {@link MockHost.setTheme}, which pushes a real `THEME_CHANGE` — the mock of
|
|
784
896
|
* the viewer toggling dark mode with the block already mounted.)
|
|
785
897
|
*/
|
|
786
|
-
export type MockHostScenarioPatch = Pick<MockHostOptions, 'consentGrantable' | 'failMode' | 'cost' | 'pollsUntilDone' | 'cannedPicks' | 'cannedImageUpload' | 'cannedGenerationSourceUpload' | 'cannedImageScan' | 'generation' | 'buzz' | 'storage' | 'shared' | 'buzzBalanceError' | 'viewerResult' | 'viewerError' | 'buzzTransactions' | 'buzzAccounts' | 'dailyCompensation' | 'buzzReadError' | 'wildcardPack' | 'wildcardPackError' | 'collectionFollowError' | 'createPostResult' | 'createPostError' | 'appWorkflows' | 'appWorkflowsError' | 'publishImageIds' | 'publishError' | 'gatedImages' | 'gatedImagesError' | 'disallowedAccountTypes'>;
|
|
898
|
+
export type MockHostScenarioPatch = Pick<MockHostOptions, 'consentGrantable' | 'failMode' | 'cost' | 'pollsUntilDone' | 'cannedPicks' | 'cannedImageUpload' | 'cannedGenerationSourceUpload' | 'cannedImageScan' | 'generation' | 'buzz' | 'storage' | 'shared' | 'buzzBalanceError' | 'viewerResult' | 'viewerError' | 'buzzTransactions' | 'buzzAccounts' | 'dailyCompensation' | 'buzzReadError' | 'wildcardPack' | 'wildcardPackError' | 'collectionFollowError' | 'createPostResult' | 'createPostError' | 'trainingDatasetRejected' | 'trainingDatasetError' | 'trainingQuoteTotal' | 'runTrainingError' | 'runTrainingCapRefusal' | 'appWorkflows' | 'appWorkflowsError' | 'publishImageIds' | 'publishError' | 'gatedImages' | 'gatedImagesError' | 'disallowedAccountTypes'>;
|
|
787
899
|
/** Runtime Buzz-balance handle exposed on {@link MockHost.buzz}. */
|
|
788
900
|
export interface MockBuzzHandle {
|
|
789
901
|
/** Current simulated balance, or `undefined` when balance isn't simulated. */
|
|
@@ -41,6 +41,7 @@ import { APP_STORAGE_ERROR_REQUEST_FAILED, APP_STORAGE_ERROR_USER_QUOTA_EXCEEDED
|
|
|
41
41
|
import { consentUnavailablePayload, isKnownBlockScope, resolveUngrantableConsentNotice, } from './consent.js';
|
|
42
42
|
import { requiredStorageScope, storageResultType, storageScopeDeniedMessage, storageScopeDeniedPayload, } from './mockHostScopes.js';
|
|
43
43
|
import { governsIdempotencyKey, idempotencyKeyDeniedMessage, idempotencyKeyRefusal, } from './mockHostIdempotency.js';
|
|
44
|
+
import { RUN_TRAINING_ERROR_CODES } from '../hooks/useRunTraining.js';
|
|
44
45
|
import { hostContextWithTheme } from '../transport/transport.js';
|
|
45
46
|
import { isRoutableRequestId } from '../transport/requestId.js';
|
|
46
47
|
/**
|
|
@@ -79,6 +80,10 @@ const preferredAccountType = (body) => {
|
|
|
79
80
|
return body.mode === 'inline' ? undefined : body.params.accountType;
|
|
80
81
|
case 'step':
|
|
81
82
|
return undefined;
|
|
83
|
+
// A training body names no pool either: the host's `blockTrainingBodySchema`
|
|
84
|
+
// is `.strict()` with no `accountType`.
|
|
85
|
+
case 'training':
|
|
86
|
+
return undefined;
|
|
82
87
|
default: {
|
|
83
88
|
// Exhaustiveness check: a new `WorkflowBody` member makes this assignment
|
|
84
89
|
// fail to compile, right here, rather than defaulting somewhere.
|
|
@@ -121,6 +126,77 @@ const passThroughTrainingType = (body) => {
|
|
|
121
126
|
const t = body.$type;
|
|
122
127
|
return t === 'training' || t === 'imageResourceTraining' ? t : undefined;
|
|
123
128
|
};
|
|
129
|
+
/**
|
|
130
|
+
* The orchestrator `$type` a body's run trains with, if it trains at all: the
|
|
131
|
+
* pass-through training `$type`s above, and `'training'` for a
|
|
132
|
+
* `kind: 'training'` body — the host builds an ai-toolkit `training` step for it.
|
|
133
|
+
*/
|
|
134
|
+
const trainingStepType = (body) => body.kind === 'training' ? 'training' : passThroughTrainingType(body);
|
|
135
|
+
/**
|
|
136
|
+
* The training bounds the mock enforces — COPIES of `@civitai/app-sdk/blocks`'s
|
|
137
|
+
* `BLOCK_TRAINING_DATASET_MAX_ITEMS`, `BLOCK_TRAINING_CAPTION_MAX_CHARS` and
|
|
138
|
+
* `BLOCK_TRAINING_MAX_BUZZ_PER_RUN`, deliberately not value-imported: a value
|
|
139
|
+
* import of a symbol the newest PUBLISHED app-sdk lacks raises this package's
|
|
140
|
+
* peer floor (`tests/guards/blocks-react-peer-floor.test.mjs`). The copy is held
|
|
141
|
+
* to the SDK's value by `test/trainingKindFlow.test.tsx`, which imports both.
|
|
142
|
+
*/
|
|
143
|
+
export const MOCK_TRAINING_BOUNDS = {
|
|
144
|
+
datasetMaxItems: 50,
|
|
145
|
+
captionMaxChars: 1000,
|
|
146
|
+
maxBuzzPerRun: 5000,
|
|
147
|
+
};
|
|
148
|
+
/**
|
|
149
|
+
* The host's `PREPARE_TRAINING_DATASET` item check, mirrored: the server's
|
|
150
|
+
* `blockTrainingDatasetItemsSchema` — 1…50 items, each EXACTLY `{ imageId:
|
|
151
|
+
* positive integer, caption: string ≤ 1000 }`. The real host runs this BEFORE any
|
|
152
|
+
* server call and refuses with `invalid training dataset`, so the mock does too.
|
|
153
|
+
*/
|
|
154
|
+
function isValidTrainingDatasetItems(items) {
|
|
155
|
+
if (!Array.isArray(items))
|
|
156
|
+
return false;
|
|
157
|
+
if (items.length < 1 || items.length > MOCK_TRAINING_BOUNDS.datasetMaxItems)
|
|
158
|
+
return false;
|
|
159
|
+
return items.every((it) => {
|
|
160
|
+
if (typeof it !== 'object' || it === null || Array.isArray(it))
|
|
161
|
+
return false;
|
|
162
|
+
const keys = Object.keys(it);
|
|
163
|
+
if (keys.length !== 2 || !keys.includes('imageId') || !keys.includes('caption'))
|
|
164
|
+
return false;
|
|
165
|
+
const { imageId, caption } = it;
|
|
166
|
+
return (typeof imageId === 'number' &&
|
|
167
|
+
Number.isInteger(imageId) &&
|
|
168
|
+
imageId > 0 &&
|
|
169
|
+
typeof caption === 'string' &&
|
|
170
|
+
caption.length <= MOCK_TRAINING_BOUNDS.captionMaxChars);
|
|
171
|
+
});
|
|
172
|
+
}
|
|
173
|
+
/** The real server's refusal for a training request from a token without the spend scope. */
|
|
174
|
+
export const MOCK_TRAINING_SCOPE_ERROR = 'block lacks ai:write:budgeted scope';
|
|
175
|
+
/** The real server's refusal for an unknown, expired or spent quote on `RUN_TRAINING`. */
|
|
176
|
+
export const MOCK_TRAINING_QUOTE_GONE_ERROR = 'training quote not found, expired or already used — estimate again';
|
|
177
|
+
/** The real server's refusal for a training estimate naming a dataset it does not hold. */
|
|
178
|
+
export const MOCK_TRAINING_DATASET_GONE_ERROR = 'training dataset not found or expired';
|
|
179
|
+
/**
|
|
180
|
+
* The real server's two refusals for a dataset with NOTHING admitted
|
|
181
|
+
* (`prepareBlockTrainingDataset`): every image failed eligibility, or every
|
|
182
|
+
* eligible image failed its import. The server THROWS rather than returning
|
|
183
|
+
* `count: 0`, so a successful reply always admits at least one image.
|
|
184
|
+
*/
|
|
185
|
+
export const MOCK_TRAINING_NONE_ELIGIBLE_ERROR = 'none of the requested images can be used for training';
|
|
186
|
+
export const MOCK_TRAINING_NONE_IMPORTED_ERROR = 'none of the requested images could be prepared for training';
|
|
187
|
+
/**
|
|
188
|
+
* The forced `RUN_TRAINING` errors that settle BEFORE the server claims (and so
|
|
189
|
+
* consumes) the quote: the host's own gate and dialog codes. Derived from the
|
|
190
|
+
* host-code list — every code except `submission-unconfirmed`, which the host
|
|
191
|
+
* emits only after a submit was sent. Any other forced error (a server message
|
|
192
|
+
* such as "Not enough Buzz for this training run.") stands for a refusal the
|
|
193
|
+
* server made AFTER its claim, so it consumes the quote.
|
|
194
|
+
*/
|
|
195
|
+
const PRE_CLAIM_RUN_TRAINING_ERRORS = new Set(RUN_TRAINING_ERROR_CODES.filter((c) => c !== 'submission-unconfirmed'));
|
|
196
|
+
/** Default quote total for a `kind: 'training'` estimate when {@link MockHostOptions.trainingQuoteTotal} is unset. */
|
|
197
|
+
const DEFAULT_TRAINING_QUOTE_TOTAL = 500;
|
|
198
|
+
/** The real quote lifetime (`BLOCK_TRAINING_QUOTE_TTL_SECONDS`, 15 min). */
|
|
199
|
+
const TRAINING_QUOTE_TTL_MS = 15 * 60_000;
|
|
124
200
|
// The canned picks carry the WIDENED BlockResourceInfo projection (PR-C) — the
|
|
125
201
|
// public recommended settings a real host now returns — so dev:mock mirrors prod
|
|
126
202
|
// (a picked resource seeds a weight slider + trigger words). Defaults match the
|
|
@@ -731,6 +807,13 @@ export function createMockHost(options = {}) {
|
|
|
731
807
|
// App Blocks → Post bridge: the canned post + a forced-refusal knob.
|
|
732
808
|
let createPostResult = options.createPostResult ?? DEFAULT_CREATE_POST_RESULT;
|
|
733
809
|
let createPostError = options.createPostError;
|
|
810
|
+
// App Blocks training bridges: canned dataset rejections, quote total, and
|
|
811
|
+
// forced-refusal knobs. The datasets/quotes themselves live per install.
|
|
812
|
+
let trainingDatasetRejected = options.trainingDatasetRejected ?? [];
|
|
813
|
+
let trainingDatasetError = options.trainingDatasetError;
|
|
814
|
+
let trainingQuoteTotal = options.trainingQuoteTotal ?? DEFAULT_TRAINING_QUOTE_TOTAL;
|
|
815
|
+
let runTrainingError = options.runTrainingError;
|
|
816
|
+
let runTrainingCapRefusal = options.runTrainingCapRefusal;
|
|
734
817
|
// App-subqueue bridge data + forced free-text-error knob. `appWorkflows` is
|
|
735
818
|
// MUTABLE — CANCEL_APP_WORKFLOW marks the matching row canceled in place so a
|
|
736
819
|
// follow-up QUERY reflects it.
|
|
@@ -882,6 +965,16 @@ export function createMockHost(options = {}) {
|
|
|
882
965
|
let submitCount = 0;
|
|
883
966
|
// body + cost remembered per workflow so the succeeded snapshot can echo them.
|
|
884
967
|
const workflows = new Map();
|
|
968
|
+
// Training: datasets this install prepared (id → admitted count) and the
|
|
969
|
+
// quotes its estimates stored (id → what a run is checked against).
|
|
970
|
+
const trainingDatasets = new Map();
|
|
971
|
+
const trainingQuotes = new Map();
|
|
972
|
+
let trainingSerial = 0;
|
|
973
|
+
/** `<prefix>` + 32 hex characters — the real handles' shape. */
|
|
974
|
+
const trainingHandle = (prefix) => {
|
|
975
|
+
trainingSerial += 1;
|
|
976
|
+
return `${prefix}${trainingSerial.toString(16).padStart(32, '0')}`;
|
|
977
|
+
};
|
|
885
978
|
const timers = new Set();
|
|
886
979
|
const dispatchToBlock = (data) => {
|
|
887
980
|
win.dispatchEvent(new MessageEvent('message', { data, origin: parentOrigin }));
|
|
@@ -911,7 +1004,7 @@ export function createMockHost(options = {}) {
|
|
|
911
1004
|
* a checkpoint. Empty for every other body, so their snapshots are unchanged.
|
|
912
1005
|
*/
|
|
913
1006
|
const trainingFields = (body) => {
|
|
914
|
-
const $type =
|
|
1007
|
+
const $type = trainingStepType(body);
|
|
915
1008
|
if (!$type)
|
|
916
1009
|
return {};
|
|
917
1010
|
const count = Math.max(0, Math.floor(gen.trainedEpochs ?? 0));
|
|
@@ -1159,6 +1252,53 @@ export function createMockHost(options = {}) {
|
|
|
1159
1252
|
});
|
|
1160
1253
|
return;
|
|
1161
1254
|
}
|
|
1255
|
+
// TRAINING ESTIMATE — KIND-FAITHFUL, the one exception to the
|
|
1256
|
+
// kind-agnostic rule above, because the real arm is not a priced
|
|
1257
|
+
// pass-through: it needs the spend scope, names a dataset the server
|
|
1258
|
+
// must hold, refuses a price above the per-run ceiling, and STORES a
|
|
1259
|
+
// quote the run is later checked against. Each refusal is the
|
|
1260
|
+
// server's message in the host's `failureSnapshot` shape.
|
|
1261
|
+
if (body.kind === 'training') {
|
|
1262
|
+
const refuse = (error) => dispatchToBlock({
|
|
1263
|
+
type: 'ESTIMATE_RESULT',
|
|
1264
|
+
payload: { requestId, snapshot: { workflowId: 'failed', status: 'failed', error } },
|
|
1265
|
+
});
|
|
1266
|
+
if (!consentGranted)
|
|
1267
|
+
return refuse(MOCK_TRAINING_SCOPE_ERROR);
|
|
1268
|
+
const imageCount = trainingDatasets.get(body.datasetId);
|
|
1269
|
+
if (imageCount === undefined)
|
|
1270
|
+
return refuse(MOCK_TRAINING_DATASET_GONE_ERROR);
|
|
1271
|
+
const total = trainingQuoteTotal;
|
|
1272
|
+
if (total > MOCK_TRAINING_BOUNDS.maxBuzzPerRun) {
|
|
1273
|
+
return refuse(`this training run costs ${total} Buzz, above the per-run limit of ${MOCK_TRAINING_BOUNDS.maxBuzzPerRun} for apps`);
|
|
1274
|
+
}
|
|
1275
|
+
const quoteId = trainingHandle('tq_');
|
|
1276
|
+
const expiresAtMs = Date.now() + TRAINING_QUOTE_TTL_MS;
|
|
1277
|
+
trainingQuotes.set(quoteId, {
|
|
1278
|
+
datasetId: body.datasetId,
|
|
1279
|
+
total,
|
|
1280
|
+
expiresAtMs,
|
|
1281
|
+
spent: false,
|
|
1282
|
+
});
|
|
1283
|
+
dispatchToBlock({
|
|
1284
|
+
type: 'ESTIMATE_RESULT',
|
|
1285
|
+
payload: {
|
|
1286
|
+
requestId,
|
|
1287
|
+
snapshot: {
|
|
1288
|
+
workflowId: 'wf_estimate',
|
|
1289
|
+
status: 'pending',
|
|
1290
|
+
cost: { total },
|
|
1291
|
+
trainingQuote: {
|
|
1292
|
+
quoteId,
|
|
1293
|
+
total,
|
|
1294
|
+
imageCount,
|
|
1295
|
+
expiresAt: new Date(expiresAtMs).toISOString(),
|
|
1296
|
+
},
|
|
1297
|
+
},
|
|
1298
|
+
},
|
|
1299
|
+
});
|
|
1300
|
+
return;
|
|
1301
|
+
}
|
|
1162
1302
|
dispatchToBlock({
|
|
1163
1303
|
type: 'ESTIMATE_RESULT',
|
|
1164
1304
|
payload: {
|
|
@@ -1182,6 +1322,26 @@ export function createMockHost(options = {}) {
|
|
|
1182
1322
|
// the largest-wallet stamp. No recipe-registry validation.
|
|
1183
1323
|
submitCount += 1;
|
|
1184
1324
|
const body = typed.payload?.body ?? {};
|
|
1325
|
+
// A `kind: 'training'` body is never started by SUBMIT_WORKFLOW: the
|
|
1326
|
+
// server charges a training run only against a quote a signed-in
|
|
1327
|
+
// session confirmed, which only RUN_TRAINING's dialog records. The
|
|
1328
|
+
// server's own refusal, in the host's `failureSnapshot` shape.
|
|
1329
|
+
// (`useBuzzWorkflow().submit()` refuses before sending; this covers a
|
|
1330
|
+
// raw transport send.)
|
|
1331
|
+
if (body.kind === 'training') {
|
|
1332
|
+
dispatchToBlock({
|
|
1333
|
+
type: 'WORKFLOW_SUBMITTED',
|
|
1334
|
+
payload: {
|
|
1335
|
+
requestId,
|
|
1336
|
+
snapshot: {
|
|
1337
|
+
workflowId: 'failed',
|
|
1338
|
+
status: 'failed',
|
|
1339
|
+
error: 'this training run has not been confirmed by the viewer in this session',
|
|
1340
|
+
},
|
|
1341
|
+
},
|
|
1342
|
+
});
|
|
1343
|
+
return;
|
|
1344
|
+
}
|
|
1185
1345
|
const cost = costFor(body);
|
|
1186
1346
|
// CAUGHT-SERVER-EXCEPTION simulation (`generation.failSubmitException`).
|
|
1187
1347
|
// Reproduces the host's `failureSnapshot(err)` byte-for-byte: the
|
|
@@ -1633,6 +1793,108 @@ export function createMockHost(options = {}) {
|
|
|
1633
1793
|
});
|
|
1634
1794
|
return;
|
|
1635
1795
|
}
|
|
1796
|
+
case 'PREPARE_TRAINING_DATASET': {
|
|
1797
|
+
// The host's gate order (`prepareTrainingDatasetGate.ts`): no
|
|
1798
|
+
// requestId → drop; signed-out → `sign in to train`; items outside
|
|
1799
|
+
// the server's schema → `invalid training dataset`, before any call.
|
|
1800
|
+
// Then the server: the spend scope, then the dataset.
|
|
1801
|
+
if (!isRoutableRequestId(requestId))
|
|
1802
|
+
return;
|
|
1803
|
+
const reply = (payload) => dispatchToBlock({ type: 'TRAINING_DATASET_RESULT', payload: { requestId, ...payload } });
|
|
1804
|
+
if (viewer === null)
|
|
1805
|
+
return reply({ error: 'sign in to train' });
|
|
1806
|
+
const items = typed.payload?.items;
|
|
1807
|
+
if (!isValidTrainingDatasetItems(items)) {
|
|
1808
|
+
return reply({ error: 'invalid training dataset' });
|
|
1809
|
+
}
|
|
1810
|
+
if (!consentGranted)
|
|
1811
|
+
return reply({ error: MOCK_TRAINING_SCOPE_ERROR });
|
|
1812
|
+
if (trainingDatasetError !== undefined)
|
|
1813
|
+
return reply({ error: trainingDatasetError });
|
|
1814
|
+
const named = new Set(items.map((it) => it.imageId));
|
|
1815
|
+
const rejected = trainingDatasetRejected
|
|
1816
|
+
.filter((r) => named.has(r.imageId))
|
|
1817
|
+
.map(({ imageId, reason }) => ({ imageId, reason }));
|
|
1818
|
+
const rejectedIds = new Set(rejected.map((r) => r.imageId));
|
|
1819
|
+
const count = [...named].filter((id) => !rejectedIds.has(id)).length;
|
|
1820
|
+
if (count === 0) {
|
|
1821
|
+
// The server THROWS here, eligibility before import: an `import-*`
|
|
1822
|
+
// rejection means that image PASSED eligibility, so the server
|
|
1823
|
+
// reached the import and refuses there.
|
|
1824
|
+
const anyImported = rejected.some((r) => r.reason.startsWith('import-'));
|
|
1825
|
+
return reply({
|
|
1826
|
+
error: anyImported
|
|
1827
|
+
? MOCK_TRAINING_NONE_IMPORTED_ERROR
|
|
1828
|
+
: MOCK_TRAINING_NONE_ELIGIBLE_ERROR,
|
|
1829
|
+
});
|
|
1830
|
+
}
|
|
1831
|
+
const datasetId = trainingHandle('tds_');
|
|
1832
|
+
trainingDatasets.set(datasetId, count);
|
|
1833
|
+
const result = { datasetId, count, rejected };
|
|
1834
|
+
return reply({ result });
|
|
1835
|
+
}
|
|
1836
|
+
case 'RUN_TRAINING': {
|
|
1837
|
+
// The host's gate (`runTrainingGate.ts`): no requestId → drop;
|
|
1838
|
+
// signed-out → `sign in to train`; not a `kind:'training'` body with
|
|
1839
|
+
// a `quoteId` → `invalid training request`. Then the server: the
|
|
1840
|
+
// spend scope, then the quote (unknown, expired or already spent).
|
|
1841
|
+
if (!isRoutableRequestId(requestId))
|
|
1842
|
+
return;
|
|
1843
|
+
const reply = (payload) => dispatchToBlock({ type: 'TRAINING_RESULT', payload: { requestId, ...payload } });
|
|
1844
|
+
if (viewer === null)
|
|
1845
|
+
return reply({ error: 'sign in to train' });
|
|
1846
|
+
const raw = typed.payload?.body;
|
|
1847
|
+
const b = raw && typeof raw === 'object' && !Array.isArray(raw)
|
|
1848
|
+
? raw
|
|
1849
|
+
: null;
|
|
1850
|
+
if (!b || b.kind !== 'training' || typeof b.quoteId !== 'string' || !b.quoteId) {
|
|
1851
|
+
return reply({ error: 'invalid training request' });
|
|
1852
|
+
}
|
|
1853
|
+
if (!consentGranted)
|
|
1854
|
+
return reply({ error: MOCK_TRAINING_SCOPE_ERROR });
|
|
1855
|
+
const quote = trainingQuotes.get(b.quoteId);
|
|
1856
|
+
if (!quote || quote.spent || Date.now() >= quote.expiresAtMs) {
|
|
1857
|
+
return reply({ error: MOCK_TRAINING_QUOTE_GONE_ERROR });
|
|
1858
|
+
}
|
|
1859
|
+
// Which outcomes consume the quote, modelled on the real flow: the
|
|
1860
|
+
// host's gate and consent dialog come first (quote kept); the
|
|
1861
|
+
// server's submit then claims the quote with a GETDEL
|
|
1862
|
+
// (`claimTrainingQuote`) BEFORE any of its own checks, so every
|
|
1863
|
+
// server outcome — a refusal, a body mismatch, a cap refusal,
|
|
1864
|
+
// `submission-unconfirmed`, a run — consumes it. The mock's check
|
|
1865
|
+
// ORDER below is its own; only the spent/kept split is the server's.
|
|
1866
|
+
if (runTrainingError !== undefined) {
|
|
1867
|
+
if (!PRE_CLAIM_RUN_TRAINING_ERRORS.has(runTrainingError))
|
|
1868
|
+
quote.spent = true;
|
|
1869
|
+
return reply({ error: runTrainingError });
|
|
1870
|
+
}
|
|
1871
|
+
quote.spent = true;
|
|
1872
|
+
if (b.datasetId !== quote.datasetId) {
|
|
1873
|
+
return reply({
|
|
1874
|
+
error: 'the training body differs from the one that was quoted — estimate again',
|
|
1875
|
+
});
|
|
1876
|
+
}
|
|
1877
|
+
// The server's cap / availability refusals RESOLVE (refunded, no run
|
|
1878
|
+
// started) — after the quote was consumed above.
|
|
1879
|
+
if (runTrainingCapRefusal !== undefined) {
|
|
1880
|
+
return reply({
|
|
1881
|
+
snapshot: {
|
|
1882
|
+
workflowId: 'failed',
|
|
1883
|
+
status: 'failed',
|
|
1884
|
+
cost: { total: quote.total },
|
|
1885
|
+
error: runTrainingCapRefusal,
|
|
1886
|
+
},
|
|
1887
|
+
});
|
|
1888
|
+
}
|
|
1889
|
+
submitCount += 1;
|
|
1890
|
+
const workflowId = `wf_${submitCount}_${Date.now()}`;
|
|
1891
|
+
workflows.set(workflowId, {
|
|
1892
|
+
polls: 0,
|
|
1893
|
+
cost: quote.total,
|
|
1894
|
+
body: raw,
|
|
1895
|
+
});
|
|
1896
|
+
return reply({ snapshot: { workflowId, status: 'pending' } });
|
|
1897
|
+
}
|
|
1636
1898
|
case 'PUBLISH_GENERATION_OUTPUTS': {
|
|
1637
1899
|
// Publish selected outputs of one of the app's OWN workflows as bare,
|
|
1638
1900
|
// real-scanned public Image rows. Drop a request with no requestId
|
|
@@ -2347,6 +2609,19 @@ export function createMockHost(options = {}) {
|
|
|
2347
2609
|
createPostResult = patch.createPostResult;
|
|
2348
2610
|
if (patch.createPostError !== undefined)
|
|
2349
2611
|
createPostError = patch.createPostError;
|
|
2612
|
+
if (patch.trainingDatasetRejected !== undefined) {
|
|
2613
|
+
trainingDatasetRejected = patch.trainingDatasetRejected;
|
|
2614
|
+
}
|
|
2615
|
+
// The training error knobs clear on an explicit `undefined` (key PRESENT in
|
|
2616
|
+
// the patch), so a harness can turn a refusal off without re-installing.
|
|
2617
|
+
if ('trainingDatasetError' in patch)
|
|
2618
|
+
trainingDatasetError = patch.trainingDatasetError;
|
|
2619
|
+
if (patch.trainingQuoteTotal !== undefined)
|
|
2620
|
+
trainingQuoteTotal = patch.trainingQuoteTotal;
|
|
2621
|
+
if ('runTrainingError' in patch)
|
|
2622
|
+
runTrainingError = patch.runTrainingError;
|
|
2623
|
+
if ('runTrainingCapRefusal' in patch)
|
|
2624
|
+
runTrainingCapRefusal = patch.runTrainingCapRefusal;
|
|
2350
2625
|
if (patch.appWorkflows !== undefined) {
|
|
2351
2626
|
appWorkflows = {
|
|
2352
2627
|
workflows: patch.appWorkflows.workflows,
|