@nodaro/shared 3.8.0 → 3.10.0
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/dist/index.cjs +325 -63
- package/dist/index.cjs.map +1 -1
- package/dist/index.d.cts +806 -41
- package/dist/index.d.ts +806 -41
- package/dist/index.js +295 -64
- package/dist/index.js.map +1 -1
- package/package.json +1 -1
- package/src/__tests__/credit-identifiers.test.ts +51 -1
- package/src/__tests__/gvp-supported-providers.test.ts +20 -3
- package/src/__tests__/model-catalog-sections.test.ts +59 -0
- package/src/__tests__/model-tree.test.ts +5 -2
- package/src/__tests__/parameter-node-value.test.ts +35 -0
- package/src/__tests__/pricing-default-duration.test.ts +45 -0
- package/src/__tests__/prompt-length-limits.test.ts +4 -0
- package/src/__tests__/scene3d-delivery-review.test.ts +512 -0
- package/src/__tests__/suno-credit-type.test.ts +46 -0
- package/src/__tests__/template-categories.test.ts +45 -0
- package/src/__tests__/video-ref-limits.test.ts +11 -2
- package/src/credit-identifiers.ts +64 -14
- package/src/i18n/camera-motions.ar.ts +5 -5
- package/src/i18n/camera-motions.de.ts +5 -5
- package/src/i18n/camera-motions.es.ts +5 -5
- package/src/i18n/camera-motions.fr.ts +5 -5
- package/src/i18n/camera-motions.he.ts +5 -5
- package/src/i18n/camera-motions.hi.ts +5 -5
- package/src/i18n/camera-motions.ja.ts +5 -5
- package/src/i18n/camera-motions.ko.ts +5 -5
- package/src/i18n/camera-motions.pt-BR.ts +5 -5
- package/src/i18n/camera-motions.ru.ts +5 -5
- package/src/i18n/camera-motions.zh-CN.ts +5 -5
- package/src/i18n/character-motion.ar.ts +1031 -0
- package/src/i18n/character-motion.de.ts +1031 -0
- package/src/i18n/character-motion.es.ts +1031 -0
- package/src/i18n/character-motion.fr.ts +1031 -0
- package/src/i18n/character-motion.he.ts +1031 -0
- package/src/i18n/character-motion.hi.ts +1031 -0
- package/src/i18n/character-motion.ja.ts +1031 -0
- package/src/i18n/character-motion.ko.ts +1031 -0
- package/src/i18n/character-motion.pt-BR.ts +1031 -0
- package/src/i18n/character-motion.ru.ts +1031 -0
- package/src/i18n/character-motion.zh-CN.ts +1031 -0
- package/src/i18n/types.ts +1 -0
- package/src/index.ts +28 -0
- package/src/model-catalog.ts +88 -35
- package/src/model-constants.ts +118 -24
- package/src/node-execution-state.ts +95 -0
- package/src/parameter-node-value.ts +33 -0
- package/src/presentation-utils.ts +1 -0
- package/src/pro-3d-render.ts +159 -0
- package/src/scene3d-delivery-notes.ts +490 -0
- package/src/scene3d-v2-plan.ts +8 -2
- package/src/smart-cut-windows.ts +15 -10
- package/src/template-categories.ts +67 -0
|
@@ -0,0 +1,512 @@
|
|
|
1
|
+
import { describe, expect, expectTypeOf, it } from "vitest"
|
|
2
|
+
import {
|
|
3
|
+
SCENE3D_ASSERTION_RESTORED_CODE,
|
|
4
|
+
SCENE3D_REMEDY_AUTO_APPLIED_CODE,
|
|
5
|
+
SCENE3D_REVIEW_REFUSED_CODE,
|
|
6
|
+
SCENE3D_REVIEW_UNAVAILABLE_CODE,
|
|
7
|
+
SCENE3D_REVIEW_UNAVAILABLE_REASONS,
|
|
8
|
+
isScene3DReviewUnavailableReason,
|
|
9
|
+
scene3DReviewNote,
|
|
10
|
+
scene3DReviewVerdictOf,
|
|
11
|
+
type Scene3DAuthoringDelivery,
|
|
12
|
+
type Scene3DAuthoringValidation,
|
|
13
|
+
type Scene3DReviewUnavailable,
|
|
14
|
+
type Scene3DReviewUnavailableReason,
|
|
15
|
+
type Scene3DReviewVerdict,
|
|
16
|
+
} from "../scene3d-delivery-notes.js"
|
|
17
|
+
import { isPro3DRenderJobOutput, pro3DRenderJobOutputSchema, pro3DRenderReviewVerdictSchema } from "../pro-3d-render.js"
|
|
18
|
+
import { FIXTURE_FPS, FIXTURE_FRAMES, FIXTURE_HEIGHT, FIXTURE_WIDTH, planV2 } from "./scene3d-v2-fixtures.js"
|
|
19
|
+
|
|
20
|
+
/**
|
|
21
|
+
* A delivery the visual reviewer did not APPROVE is the one Scene3D outcome a caller CANNOT
|
|
22
|
+
* infer from the fields it already reads: the job completed, `videoUrl` is real, and
|
|
23
|
+
* `validation.status` is `"passed"`. Only `metadata.review` says the reviewer refused the scene
|
|
24
|
+
* anyway — or that it never answered at all. These tests pin the ways that fact gets read wrong:
|
|
25
|
+
* off the status, off the warning count, and off a `verdict` check that knows only `"refused"`.
|
|
26
|
+
*/
|
|
27
|
+
describe("scene3DReviewVerdictOf", () => {
|
|
28
|
+
const verdict: Scene3DReviewVerdict = {
|
|
29
|
+
verdict: "refused",
|
|
30
|
+
objections: [
|
|
31
|
+
{ category: "motion", what: "The suitcase does not cross the frame.", correction: "Extend the keyframe span.", frames: [0, 4] },
|
|
32
|
+
],
|
|
33
|
+
observed: "The frames establish a central square pillar.",
|
|
34
|
+
}
|
|
35
|
+
|
|
36
|
+
it("reads the verdict off a delivered result whose validation still says passed", () => {
|
|
37
|
+
const output = {
|
|
38
|
+
videoUrl: "https://example.invalid/scene.mp4",
|
|
39
|
+
validation: { status: "passed", warnings: [{ code: SCENE3D_REVIEW_REFUSED_CODE, message: "motion: …" }] },
|
|
40
|
+
metadata: { width: 1920, height: 1080, review: verdict },
|
|
41
|
+
}
|
|
42
|
+
expect(scene3DReviewVerdictOf(output)).toEqual(verdict)
|
|
43
|
+
})
|
|
44
|
+
|
|
45
|
+
it("is undefined for a clean delivery, so absence means nothing to report", () => {
|
|
46
|
+
expect(scene3DReviewVerdictOf({ metadata: { width: 1920 }, validation: { status: "passed", warnings: [] } })).toBeUndefined()
|
|
47
|
+
})
|
|
48
|
+
|
|
49
|
+
/** A refusal that named nothing actionable is the shape that makes a critic useless; losing
|
|
50
|
+
* it would make it indistinguishable from a clean run. */
|
|
51
|
+
it("keeps a refusal that raised no objection at all", () => {
|
|
52
|
+
const empty = scene3DReviewVerdictOf({ metadata: { review: { verdict: "refused", objections: [] } } })
|
|
53
|
+
expect(empty).toEqual({ verdict: "refused", objections: [] })
|
|
54
|
+
})
|
|
55
|
+
|
|
56
|
+
it("never invents a verdict from a warning, a status, or a malformed review", () => {
|
|
57
|
+
expect(scene3DReviewVerdictOf({ validation: { warnings: [{ code: SCENE3D_REVIEW_REFUSED_CODE, message: "x" }] } })).toBeUndefined()
|
|
58
|
+
expect(scene3DReviewVerdictOf({ validation: { status: "failed" } })).toBeUndefined()
|
|
59
|
+
expect(scene3DReviewVerdictOf({ metadata: { review: { verdict: "accepted", objections: [] } } })).toBeUndefined()
|
|
60
|
+
expect(scene3DReviewVerdictOf({ metadata: { review: "refused" } })).toBeUndefined()
|
|
61
|
+
expect(scene3DReviewVerdictOf(null)).toBeUndefined()
|
|
62
|
+
expect(scene3DReviewVerdictOf(undefined)).toBeUndefined()
|
|
63
|
+
})
|
|
64
|
+
|
|
65
|
+
/** Objections arrive from a model's report through JSON; a half-formed one must degrade to a
|
|
66
|
+
* readable entry rather than take the whole delivered scene down with it. */
|
|
67
|
+
it("drops an objection with no text and defaults the rest rather than refusing the verdict", () => {
|
|
68
|
+
const read = scene3DReviewVerdictOf({
|
|
69
|
+
metadata: { review: { verdict: "refused", objections: [
|
|
70
|
+
{ category: "", what: "Lighting is flat.", frames: [2, -1, "x", 7] },
|
|
71
|
+
{ category: "motion" },
|
|
72
|
+
"not an objection",
|
|
73
|
+
], observed: "" } },
|
|
74
|
+
})
|
|
75
|
+
expect(read).toEqual({
|
|
76
|
+
verdict: "refused",
|
|
77
|
+
objections: [{ category: "unsupported", what: "Lighting is flat.", frames: [2, 7] }],
|
|
78
|
+
})
|
|
79
|
+
})
|
|
80
|
+
|
|
81
|
+
/**
|
|
82
|
+
* The sibling verdict, and the reason the reader could not stay keyed on `"refused"`.
|
|
83
|
+
*
|
|
84
|
+
* A review whose provider never answered delivers an assertion-passing scene with NO opinion
|
|
85
|
+
* on it. Read through a `verdict === "refused"` test that returns `undefined` for everything
|
|
86
|
+
* else, such a delivery is indistinguishable from a clean one — the exact silence this module
|
|
87
|
+
* exists to prevent.
|
|
88
|
+
*/
|
|
89
|
+
it("reads the verdict of a scene NOBODY reviewed, which a refused-only reader would drop", () => {
|
|
90
|
+
const unreviewed = {
|
|
91
|
+
metadata: { review: { verdict: "unavailable", reason: "provider", attempts: 2, objections: [] } },
|
|
92
|
+
validation: {
|
|
93
|
+
status: "passed",
|
|
94
|
+
warnings: [{ code: SCENE3D_REVIEW_UNAVAILABLE_CODE, message: "… delivered unreviewed." }],
|
|
95
|
+
},
|
|
96
|
+
}
|
|
97
|
+
expect(scene3DReviewVerdictOf(unreviewed)).toEqual({
|
|
98
|
+
verdict: "unavailable", reason: "provider", attempts: 2, objections: [],
|
|
99
|
+
})
|
|
100
|
+
})
|
|
101
|
+
|
|
102
|
+
/** A review is BATCHED: what answered before the outage is real evidence, and is not the
|
|
103
|
+
* whole verdict. Both facts have to survive the read, or `objections` reads as approval. */
|
|
104
|
+
it("keeps the batches that answered before the provider went away, on the unavailable arm", () => {
|
|
105
|
+
const read = scene3DReviewVerdictOf({
|
|
106
|
+
metadata: { review: { verdict: "unavailable", reason: "provider", attempts: 2, objections: [
|
|
107
|
+
{ category: "motion", what: "The suitcase never crosses.", frames: [0] },
|
|
108
|
+
], observed: "A red suitcase on a pale floor." } },
|
|
109
|
+
})
|
|
110
|
+
expect(read).toEqual({
|
|
111
|
+
verdict: "unavailable", reason: "provider", attempts: 2,
|
|
112
|
+
objections: [{ category: "motion", what: "The suitcase never crosses.", frames: [0] }],
|
|
113
|
+
observed: "A red suitcase on a pale floor.",
|
|
114
|
+
})
|
|
115
|
+
})
|
|
116
|
+
|
|
117
|
+
/** `attempts` decides a sentence, never a control flow, so a nonsense one degrades to the
|
|
118
|
+
* floor the fact itself guarantees: a verdict exists, so the review was asked at least once. */
|
|
119
|
+
it("clamps a nonsense attempt count rather than dropping an otherwise good verdict", () => {
|
|
120
|
+
for (const attempts of [0, -3, 1.5, "two", undefined, Number.NaN]) {
|
|
121
|
+
expect(scene3DReviewVerdictOf({ metadata: { review: { verdict: "unavailable", attempts, objections: [] } } }))
|
|
122
|
+
.toEqual({ verdict: "unavailable", reason: "provider", attempts: 1, objections: [] })
|
|
123
|
+
}
|
|
124
|
+
})
|
|
125
|
+
|
|
126
|
+
it("still refuses a verdict it has never heard of, on either arm", () => {
|
|
127
|
+
expect(scene3DReviewVerdictOf({ metadata: { review: { verdict: "unreachable", objections: [] } } })).toBeUndefined()
|
|
128
|
+
})
|
|
129
|
+
|
|
130
|
+
/**
|
|
131
|
+
* Round 10ag: a review the provider ANSWERED, unusably on every asking, is the same unreviewed
|
|
132
|
+
* delivery with `reason: "unusable"`. The reader used to hard-code `"provider"`, so every SDK
|
|
133
|
+
* consumer that went through it lost the one field that makes "did not reach its provider"
|
|
134
|
+
* untrue — and then printed that sentence.
|
|
135
|
+
*/
|
|
136
|
+
it("keeps the unusable reason, and reads a reason it does not know as provider", () => {
|
|
137
|
+
expect(scene3DReviewVerdictOf({
|
|
138
|
+
metadata: { review: { verdict: "unavailable", reason: "unusable", attempts: 2, objections: [] } },
|
|
139
|
+
})).toEqual({ verdict: "unavailable", reason: "unusable", attempts: 2, objections: [] })
|
|
140
|
+
for (const reason of ["weather", "", 7, null, undefined, { cause: "unusable" }]) {
|
|
141
|
+
expect(scene3DReviewVerdictOf({ metadata: { review: { verdict: "unavailable", reason, attempts: 2, objections: [] } } }))
|
|
142
|
+
.toEqual({ verdict: "unavailable", reason: "provider", attempts: 2, objections: [] })
|
|
143
|
+
}
|
|
144
|
+
})
|
|
145
|
+
})
|
|
146
|
+
|
|
147
|
+
/**
|
|
148
|
+
* The note is the anti-invention guard: a surface that spells its own sentence writes "the
|
|
149
|
+
* reviewer refused this scene" for the arm it was not thinking about, and a scene NOBODY
|
|
150
|
+
* reviewed is then reported as carrying an opinion that does not exist.
|
|
151
|
+
*/
|
|
152
|
+
describe("scene3DReviewNote", () => {
|
|
153
|
+
it("never says refused about a scene nobody reviewed, and counts the asks", () => {
|
|
154
|
+
const note = scene3DReviewNote({ verdict: "unavailable", reason: "provider", attempts: 2, objections: [] })
|
|
155
|
+
expect(note).toContain("did not reach its provider in 2 attempts")
|
|
156
|
+
expect(note).toContain("delivered unreviewed")
|
|
157
|
+
expect(note).not.toMatch(/refus/i)
|
|
158
|
+
})
|
|
159
|
+
|
|
160
|
+
it("says one attempt in the singular, and names the partial findings as partial", () => {
|
|
161
|
+
expect(scene3DReviewNote({ verdict: "unavailable", reason: "provider", attempts: 1, objections: [] }))
|
|
162
|
+
.toContain("in one attempt")
|
|
163
|
+
const partial = scene3DReviewNote({
|
|
164
|
+
verdict: "unavailable", reason: "provider", attempts: 2,
|
|
165
|
+
objections: [{ category: "motion", what: "No crossing.", frames: [] }],
|
|
166
|
+
})
|
|
167
|
+
expect(partial).toContain("not the whole verdict")
|
|
168
|
+
})
|
|
169
|
+
|
|
170
|
+
/**
|
|
171
|
+
* Pinned WORD FOR WORD, both arms, against the sentence the engine's own leading
|
|
172
|
+
* `SCENE_REVIEW_UNAVAILABLE` warning carries (`sceneReviewUnavailableSentence`, delivered lane).
|
|
173
|
+
* A banner that paraphrases the warning printed right under it describes one run two ways.
|
|
174
|
+
*/
|
|
175
|
+
it("says the review returned no usable verdict when the provider answered, in the engine's words", () => {
|
|
176
|
+
expect(scene3DReviewNote({ verdict: "unavailable", reason: "unusable", attempts: 2, objections: [] }))
|
|
177
|
+
.toBe("The scene built and every mandatory assertion passed, but the visual review returned no "
|
|
178
|
+
+ "usable verdict in 2 attempts; it was delivered unreviewed.")
|
|
179
|
+
expect(scene3DReviewNote({ verdict: "unavailable", reason: "provider", attempts: 2, objections: [] }))
|
|
180
|
+
.toBe("The scene built and every mandatory assertion passed, but the visual review did not reach "
|
|
181
|
+
+ "its provider in 2 attempts; it was delivered unreviewed.")
|
|
182
|
+
const unusable = scene3DReviewNote({
|
|
183
|
+
verdict: "unavailable", reason: "unusable", attempts: 1,
|
|
184
|
+
objections: [{ category: "motion", what: "No crossing.", frames: [] }],
|
|
185
|
+
})
|
|
186
|
+
expect(unusable).toContain("returned no usable verdict in one attempt")
|
|
187
|
+
expect(unusable).toContain("not the whole verdict")
|
|
188
|
+
expect(unusable).not.toMatch(/reach its provider|refus/i)
|
|
189
|
+
})
|
|
190
|
+
|
|
191
|
+
/** Totality over the one list: a reason added to it must get its own sentence, not inherit one. */
|
|
192
|
+
it("gives every known reason a distinct sentence", () => {
|
|
193
|
+
const notes = SCENE3D_REVIEW_UNAVAILABLE_REASONS.map((reason) =>
|
|
194
|
+
scene3DReviewNote({ verdict: "unavailable", reason, attempts: 2, objections: [] }))
|
|
195
|
+
expect(new Set(notes).size).toBe(SCENE3D_REVIEW_UNAVAILABLE_REASONS.length)
|
|
196
|
+
})
|
|
197
|
+
|
|
198
|
+
it("reports a refusal as a refusal, including one that named nothing actionable", () => {
|
|
199
|
+
expect(scene3DReviewNote({ verdict: "refused", objections: [
|
|
200
|
+
{ category: "motion", what: "No crossing.", frames: [] },
|
|
201
|
+
] })).toContain("refused this scene on one finding")
|
|
202
|
+
expect(scene3DReviewNote({ verdict: "refused", objections: [] }))
|
|
203
|
+
.toContain("without naming anything to change")
|
|
204
|
+
})
|
|
205
|
+
})
|
|
206
|
+
|
|
207
|
+
describe("pro3DRenderReviewVerdictSchema", () => {
|
|
208
|
+
it("parses the verdict the plugin emits, keys and all", () => {
|
|
209
|
+
const parsed = pro3DRenderReviewVerdictSchema.safeParse({
|
|
210
|
+
verdict: "refused",
|
|
211
|
+
objections: [{ category: "evidence", what: "Could not establish the motion.", frames: [0], extra: 1 }],
|
|
212
|
+
observed: "A red suitcase.",
|
|
213
|
+
})
|
|
214
|
+
expect(parsed.success).toBe(true)
|
|
215
|
+
})
|
|
216
|
+
|
|
217
|
+
it("accepts an objection that cited no frames, and refuses a verdict it does not know", () => {
|
|
218
|
+
expect(pro3DRenderReviewVerdictSchema.safeParse({
|
|
219
|
+
verdict: "refused", objections: [{ category: "style", what: "Too dark." }],
|
|
220
|
+
}).success).toBe(true)
|
|
221
|
+
expect(pro3DRenderReviewVerdictSchema.safeParse({ verdict: "accepted", objections: [] }).success).toBe(false)
|
|
222
|
+
})
|
|
223
|
+
|
|
224
|
+
it("parses the unavailable verdict, and defaults the fields an older engine omits", () => {
|
|
225
|
+
const parsed = pro3DRenderReviewVerdictSchema.safeParse({
|
|
226
|
+
verdict: "unavailable", reason: "provider", attempts: 2, objections: [], extra: 1,
|
|
227
|
+
})
|
|
228
|
+
expect(parsed.success).toBe(true)
|
|
229
|
+
const bare = pro3DRenderReviewVerdictSchema.safeParse({ verdict: "unavailable", objections: [] })
|
|
230
|
+
expect(bare.success).toBe(true)
|
|
231
|
+
expect(bare.success && bare.data).toMatchObject({ reason: "provider", attempts: 1 })
|
|
232
|
+
})
|
|
233
|
+
|
|
234
|
+
/** Round 10ag's wire value. It used to parse — and be rewritten to `"provider"` on the way. */
|
|
235
|
+
it("keeps reason unusable rather than rewriting it to provider", () => {
|
|
236
|
+
const parsed = pro3DRenderReviewVerdictSchema.safeParse({
|
|
237
|
+
verdict: "unavailable", reason: "unusable", attempts: 2, objections: [],
|
|
238
|
+
})
|
|
239
|
+
expect(parsed.success).toBe(true)
|
|
240
|
+
expect(parsed.success && parsed.data).toMatchObject({ verdict: "unavailable", reason: "unusable", attempts: 2 })
|
|
241
|
+
})
|
|
242
|
+
|
|
243
|
+
it("still falls back to provider for a reason it does not know, rather than refusing the verdict", () => {
|
|
244
|
+
for (const reason of ["weather", "", 7, null, ["unusable"]]) {
|
|
245
|
+
const parsed = pro3DRenderReviewVerdictSchema.safeParse({ verdict: "unavailable", reason, attempts: 2, objections: [] })
|
|
246
|
+
expect(parsed.success).toBe(true)
|
|
247
|
+
expect(parsed.success && parsed.data).toMatchObject({ reason: "provider", attempts: 2 })
|
|
248
|
+
}
|
|
249
|
+
})
|
|
250
|
+
|
|
251
|
+
/**
|
|
252
|
+
* The guard that keeps ONE list: every reason the package declares survives the schema AND the
|
|
253
|
+
* hand reader unchanged, and the type the SDK exports is exactly that list. A reason added to the
|
|
254
|
+
* list but not to a reader — the drift this round fixed — fails here.
|
|
255
|
+
*/
|
|
256
|
+
it("round-trips every declared reason through both readers, and types the reason as that list", () => {
|
|
257
|
+
for (const reason of SCENE3D_REVIEW_UNAVAILABLE_REASONS) {
|
|
258
|
+
const review = { verdict: "unavailable", reason, attempts: 2, objections: [] }
|
|
259
|
+
const parsed = pro3DRenderReviewVerdictSchema.safeParse(review)
|
|
260
|
+
expect(parsed.success && parsed.data).toMatchObject({ reason })
|
|
261
|
+
expect(scene3DReviewVerdictOf({ metadata: { review } })).toMatchObject({ reason })
|
|
262
|
+
expect(isScene3DReviewUnavailableReason(reason)).toBe(true)
|
|
263
|
+
}
|
|
264
|
+
expect(isScene3DReviewUnavailableReason("weather")).toBe(false)
|
|
265
|
+
expectTypeOf<Scene3DReviewUnavailable["reason"]>().toEqualTypeOf<Scene3DReviewUnavailableReason>()
|
|
266
|
+
expectTypeOf<Scene3DReviewUnavailableReason>().toEqualTypeOf<"provider" | "unusable">()
|
|
267
|
+
})
|
|
268
|
+
})
|
|
269
|
+
|
|
270
|
+
/**
|
|
271
|
+
* The gap this round closes, stated as the assertion that would have caught it.
|
|
272
|
+
*
|
|
273
|
+
* `pro3DRenderReviewVerdictSchema` declared `verdict: z.literal("refused")`, so the whole
|
|
274
|
+
* result of an UNREVIEWED delivery — a real, paid, playable MP4 — failed `isPro3DRenderJobOutput`
|
|
275
|
+
* over a discriminant the schema had never been taught. A reader that gates on that predicate
|
|
276
|
+
* would have shown the caller nothing at all.
|
|
277
|
+
*/
|
|
278
|
+
describe("isPro3DRenderJobOutput with a review nobody could perform", () => {
|
|
279
|
+
const completed = (review?: unknown) => ({
|
|
280
|
+
videoUrl: "https://cdn.example/scene.mp4",
|
|
281
|
+
scenePlan: planV2(),
|
|
282
|
+
sceneRevisionId: "rev-1",
|
|
283
|
+
posterAssetId: "poster-1",
|
|
284
|
+
validation: {
|
|
285
|
+
status: "passed" as const,
|
|
286
|
+
reportAssetId: "report-1",
|
|
287
|
+
warnings: [{ code: SCENE3D_REVIEW_UNAVAILABLE_CODE, message: "… delivered unreviewed." }],
|
|
288
|
+
},
|
|
289
|
+
renderer: "blender",
|
|
290
|
+
metadata: {
|
|
291
|
+
width: FIXTURE_WIDTH, height: FIXTURE_HEIGHT, fps: FIXTURE_FPS,
|
|
292
|
+
frames: FIXTURE_FRAMES, duration: FIXTURE_FRAMES / FIXTURE_FPS,
|
|
293
|
+
...(review === undefined ? {} : { review }),
|
|
294
|
+
},
|
|
295
|
+
})
|
|
296
|
+
|
|
297
|
+
it("the fixture itself parses, so a refusal below is about the verdict and nothing else", () => {
|
|
298
|
+
expect(isPro3DRenderJobOutput(completed())).toBe(true)
|
|
299
|
+
})
|
|
300
|
+
|
|
301
|
+
it("accepts the delivered result of a scene the reviewer never reached", () => {
|
|
302
|
+
expect(isPro3DRenderJobOutput(completed({
|
|
303
|
+
verdict: "unavailable", reason: "provider", attempts: 2, objections: [],
|
|
304
|
+
}))).toBe(true)
|
|
305
|
+
})
|
|
306
|
+
|
|
307
|
+
it("accepts the delivered result of a scene whose review answered unusably, and keeps the reason", () => {
|
|
308
|
+
const parsed = pro3DRenderJobOutputSchema.safeParse(completed({
|
|
309
|
+
verdict: "unavailable", reason: "unusable", attempts: 2, objections: [],
|
|
310
|
+
}))
|
|
311
|
+
expect(parsed.success).toBe(true)
|
|
312
|
+
expect(parsed.success && parsed.data.metadata.review).toMatchObject({ verdict: "unavailable", reason: "unusable" })
|
|
313
|
+
})
|
|
314
|
+
|
|
315
|
+
it("still accepts the refused verdict it always did, and the clean result with no review", () => {
|
|
316
|
+
expect(isPro3DRenderJobOutput(completed({ verdict: "refused", objections: [] }))).toBe(true)
|
|
317
|
+
expect(isPro3DRenderJobOutput(completed())).toBe(true)
|
|
318
|
+
})
|
|
319
|
+
|
|
320
|
+
/** Tolerance in the direction that matters: a malformed advisory must never blank a video the
|
|
321
|
+
* platform already rendered and charged for. */
|
|
322
|
+
it("does not blank a delivered video over a malformed review", () => {
|
|
323
|
+
expect(isPro3DRenderJobOutput(completed({ verdict: "unavailable", attempts: "many", objections: [] }))).toBe(true)
|
|
324
|
+
expect(isPro3DRenderJobOutput(completed({ verdict: "unavailable", reason: "weather", objections: [] }))).toBe(true)
|
|
325
|
+
})
|
|
326
|
+
|
|
327
|
+
/**
|
|
328
|
+
* The regression guard for the NEXT verdict, not this one.
|
|
329
|
+
*
|
|
330
|
+
* The discriminant stays strict — guessing it is how a scene nobody reviewed gets reported as
|
|
331
|
+
* refused — but an unrecognised verdict must cost at most itself. This is the exact shape that
|
|
332
|
+
* broke here: had `review` blanked the whole result for `"unavailable"`, it will do it again
|
|
333
|
+
* for whatever the engine declares next, and the caller loses a paid MP4 over an advisory.
|
|
334
|
+
*/
|
|
335
|
+
it("keeps the delivered video when the verdict is one it has never heard of", () => {
|
|
336
|
+
const parsed = pro3DRenderJobOutputSchema.safeParse(completed({ verdict: "shrugged", objections: [] }))
|
|
337
|
+
expect(parsed.success).toBe(true)
|
|
338
|
+
expect(parsed.success && parsed.data.metadata.review).toBeUndefined()
|
|
339
|
+
// …and the run still says so through the warning, which has no code enum to refuse it.
|
|
340
|
+
expect(parsed.success && parsed.data.validation.warnings?.[0]?.code).toBe(SCENE3D_REVIEW_UNAVAILABLE_CODE)
|
|
341
|
+
})
|
|
342
|
+
})
|
|
343
|
+
|
|
344
|
+
/**
|
|
345
|
+
* `validation.sourceRetained` — the refused-authoring row's one pointer at something fetchable.
|
|
346
|
+
*
|
|
347
|
+
* A run whose recipe never compiled publishes no scene revision and no poster, so nothing on
|
|
348
|
+
* the failed row is a scene. What it can say is whether the recipe it was refused for was kept,
|
|
349
|
+
* which decides whether `GET /v1/3d-scene/deliveries/{deliveryId}` has a `source-json`
|
|
350
|
+
* descriptor to list. Declared rather than merely tolerated by the index signature, so a typed
|
|
351
|
+
* caller can read it without casting — and so it appears in the generated SDK docs at all.
|
|
352
|
+
*/
|
|
353
|
+
describe("Scene3DAuthoringValidation.sourceRetained", () => {
|
|
354
|
+
it("is a typed optional beside the fields the same block already carried", () => {
|
|
355
|
+
const refused: Scene3DAuthoringValidation = {
|
|
356
|
+
status: "failed", scope: "authored", phase: "build", passes: 3,
|
|
357
|
+
reportAssetId: "report-1", sourceRetained: true,
|
|
358
|
+
warnings: [{ code: "SCENE_RECIPE_INVALID", message: "camera.shots[0].target is not resolvable" }],
|
|
359
|
+
}
|
|
360
|
+
expectTypeOf(refused.sourceRetained).toEqualTypeOf<boolean | undefined>()
|
|
361
|
+
expect(refused.sourceRetained).toBe(true)
|
|
362
|
+
// Absent is a first-class answer everywhere in this block: every lane that DID compile
|
|
363
|
+
// carries no such flag, because there is no refused recipe to have retained.
|
|
364
|
+
const delivered: Scene3DAuthoringValidation = { status: "passed", reportAssetId: "report-2", warnings: [] }
|
|
365
|
+
expect(delivered.sourceRetained).toBeUndefined()
|
|
366
|
+
})
|
|
367
|
+
|
|
368
|
+
it("is false, not absent, on a run that never cleared admission", () => {
|
|
369
|
+
// No pass produced a recipe the compiler would admit, so there is nothing to fetch. `false`
|
|
370
|
+
// says that; absent would leave a caller unable to tell it from an older deployment.
|
|
371
|
+
const refused: Scene3DAuthoringValidation = {
|
|
372
|
+
status: "failed", scope: "authored", phase: "planning", passes: 2,
|
|
373
|
+
reportAssetId: "report-3", sourceRetained: false, warnings: [],
|
|
374
|
+
}
|
|
375
|
+
expect(refused.sourceRetained).toBe(false)
|
|
376
|
+
})
|
|
377
|
+
})
|
|
378
|
+
|
|
379
|
+
/**
|
|
380
|
+
* `mechanicalPasses` is counted APART from `repairPasses` — mechanical passes buy their own
|
|
381
|
+
* allowance (a `mechanical` quote line, released when unspent) instead of spending one of the
|
|
382
|
+
* caller's repairs. The trap this pins is that there is NO arithmetic relation between the two
|
|
383
|
+
* numbers to lean on: a run may report more mechanical passes than repairs, and the older
|
|
384
|
+
* accounting (where the count WAS a subset) is still reachable on a result quoted before the
|
|
385
|
+
* allowance existed. The discriminant for which accounting applies is the QUOTE, and the result
|
|
386
|
+
* does not carry it — so any reader deriving the relation from these two numbers is guessing.
|
|
387
|
+
*/
|
|
388
|
+
describe("Scene3DAuthoringDelivery.mechanicalPasses", () => {
|
|
389
|
+
it("is a typed optional that carries no arithmetic relation to repairPasses", () => {
|
|
390
|
+
// Two mechanical passes on a run that spent ONE repair: impossible under the old subset
|
|
391
|
+
// reading, ordinary under its own allowance. This is the case a `<=` assertion would reject.
|
|
392
|
+
const delivery: Scene3DAuthoringDelivery = {
|
|
393
|
+
repairPasses: 1,
|
|
394
|
+
mechanicalPasses: 2,
|
|
395
|
+
validation: {
|
|
396
|
+
status: "passed",
|
|
397
|
+
reportAssetId: "report-1",
|
|
398
|
+
warnings: [{
|
|
399
|
+
code: SCENE3D_REMEDY_AUTO_APPLIED_CODE,
|
|
400
|
+
message: "applied 1 remedy op for cyanVisibility: replace /camera/shots/1/lens/focalLengthMm = 30.8",
|
|
401
|
+
}],
|
|
402
|
+
},
|
|
403
|
+
}
|
|
404
|
+
expectTypeOf(delivery.mechanicalPasses).toEqualTypeOf<number | undefined>()
|
|
405
|
+
expect(delivery.mechanicalPasses).toBe(2)
|
|
406
|
+
expect(delivery.repairPasses).toBe(1)
|
|
407
|
+
expect(delivery.validation?.warnings?.[0]?.code).toBe("REMEDY_AUTO_APPLIED")
|
|
408
|
+
})
|
|
409
|
+
|
|
410
|
+
it("is absent, not zero, on a run that took none — and absent is not evidence of none", () => {
|
|
411
|
+
// An engine that does not report the count is indistinguishable from a run that took none,
|
|
412
|
+
// which is why no reader may default it to 0 and then claim the planner authored every repair.
|
|
413
|
+
const planned: Scene3DAuthoringDelivery = { repairPasses: 1 }
|
|
414
|
+
expect(planned.mechanicalPasses).toBeUndefined()
|
|
415
|
+
})
|
|
416
|
+
})
|
|
417
|
+
|
|
418
|
+
/**
|
|
419
|
+
* The answer-side restore. A repair may change what the feedback names; when an answer re-shapes a
|
|
420
|
+
* mandatory assertion it was NOT invited to touch, the engine puts that assertion back rather than
|
|
421
|
+
* refusing the answer and spending a retry to be told to restore a value it already held.
|
|
422
|
+
*/
|
|
423
|
+
describe("Scene3DAuthoringDelivery.restoredAssertions", () => {
|
|
424
|
+
it("carries the edit that put one back, beside its ASSERTION_RESTORED warning", () => {
|
|
425
|
+
const delivery: Scene3DAuthoringDelivery = {
|
|
426
|
+
restoredAssertions: [{
|
|
427
|
+
op: "replace",
|
|
428
|
+
path: "/assertions/cyanEndVisibility",
|
|
429
|
+
value: { kind: "visibility", minCoverage: 0.08 },
|
|
430
|
+
assertionId: "cyanEndVisibility",
|
|
431
|
+
reason: "the repair re-shaped a mandatory assertion the feedback does not name",
|
|
432
|
+
}],
|
|
433
|
+
validation: {
|
|
434
|
+
status: "passed",
|
|
435
|
+
reportAssetId: "report-2",
|
|
436
|
+
warnings: [{ code: SCENE3D_ASSERTION_RESTORED_CODE, message: "restored cyanEndVisibility" }],
|
|
437
|
+
},
|
|
438
|
+
}
|
|
439
|
+
expect(delivery.restoredAssertions?.[0]?.assertionId).toBe("cyanEndVisibility")
|
|
440
|
+
// `value` is deliberately `unknown` — a restored assertion holds whatever it holds, and
|
|
441
|
+
// narrowing it here would make the type wrong the first time an assertion shape changes.
|
|
442
|
+
expectTypeOf(delivery.restoredAssertions![0]!.value).toEqualTypeOf<unknown>()
|
|
443
|
+
expect(delivery.validation?.warnings?.[0]?.code).toBe("ASSERTION_RESTORED")
|
|
444
|
+
})
|
|
445
|
+
})
|
|
446
|
+
|
|
447
|
+
/**
|
|
448
|
+
* The reader schema is `.passthrough()`, so the field already ARRIVED before it was declared. What
|
|
449
|
+
* declaring it buys is the refusal below: a negative or fractional count is a malformed result, and
|
|
450
|
+
* an untyped passthrough key would have carried it to a caller unexamined.
|
|
451
|
+
*/
|
|
452
|
+
describe("pro3DRenderJobOutputSchema — mechanicalPasses", () => {
|
|
453
|
+
// A REAL plan, from the shared fixture: a hand-rolled stub would fail `scenePlan` first and make
|
|
454
|
+
// every refusal below pass for the wrong reason, proving nothing about this field.
|
|
455
|
+
const base = {
|
|
456
|
+
videoUrl: "https://cdn.example/scene.mp4",
|
|
457
|
+
scenePlan: planV2(),
|
|
458
|
+
sceneRevisionId: "rev-1",
|
|
459
|
+
posterAssetId: "poster-1",
|
|
460
|
+
validation: { status: "passed" as const, reportAssetId: "report-1", warnings: [] },
|
|
461
|
+
renderer: "blender",
|
|
462
|
+
metadata: {
|
|
463
|
+
width: FIXTURE_WIDTH, height: FIXTURE_HEIGHT, fps: FIXTURE_FPS,
|
|
464
|
+
frames: FIXTURE_FRAMES, duration: FIXTURE_FRAMES / FIXTURE_FPS,
|
|
465
|
+
},
|
|
466
|
+
}
|
|
467
|
+
|
|
468
|
+
it("the fixture itself parses, so a refusal below is about mechanicalPasses and nothing else", () => {
|
|
469
|
+
expect(pro3DRenderJobOutputSchema.safeParse(base).success).toBe(true)
|
|
470
|
+
})
|
|
471
|
+
|
|
472
|
+
it("parses the count the engine emits, beside the repair count it is part of", () => {
|
|
473
|
+
const parsed = pro3DRenderJobOutputSchema.safeParse({ ...base, repairPasses: 2, mechanicalPasses: 1 })
|
|
474
|
+
expect(parsed.success).toBe(true)
|
|
475
|
+
if (parsed.success) expect(parsed.data.mechanicalPasses).toBe(1)
|
|
476
|
+
})
|
|
477
|
+
|
|
478
|
+
it("refuses a count that cannot describe passes that were run", () => {
|
|
479
|
+
expect(pro3DRenderJobOutputSchema.safeParse({ ...base, mechanicalPasses: -1 }).success).toBe(false)
|
|
480
|
+
expect(pro3DRenderJobOutputSchema.safeParse({ ...base, mechanicalPasses: 1.5 }).success).toBe(false)
|
|
481
|
+
})
|
|
482
|
+
|
|
483
|
+
it("does not tie mechanicalPasses to repairPasses — more mechanical than repairs parses", () => {
|
|
484
|
+
const parsed = pro3DRenderJobOutputSchema.safeParse({ ...base, repairPasses: 1, mechanicalPasses: 2 })
|
|
485
|
+
expect(parsed.success).toBe(true)
|
|
486
|
+
})
|
|
487
|
+
|
|
488
|
+
it("parses a restored assertion, keeping an unknown value and unknown extra keys", () => {
|
|
489
|
+
const parsed = pro3DRenderJobOutputSchema.safeParse({
|
|
490
|
+
...base,
|
|
491
|
+
restoredAssertions: [{
|
|
492
|
+
op: "replace", path: "/assertions/a1", value: { minCoverage: 0.08 },
|
|
493
|
+
assertionId: "a1", reason: "not named by the feedback", frames: [0, 4],
|
|
494
|
+
}],
|
|
495
|
+
})
|
|
496
|
+
expect(parsed.success).toBe(true)
|
|
497
|
+
if (parsed.success) expect(parsed.data.restoredAssertions?.[0]?.assertionId).toBe("a1")
|
|
498
|
+
})
|
|
499
|
+
|
|
500
|
+
it("keeps an entry whose value is absent, and refuses one that names nothing", () => {
|
|
501
|
+
// A removal carries no value; that entry is still a real restore.
|
|
502
|
+
expect(pro3DRenderJobOutputSchema.safeParse({
|
|
503
|
+
...base,
|
|
504
|
+
restoredAssertions: [{ op: "remove", path: "/assertions/a1", assertionId: "a1", reason: "r" }],
|
|
505
|
+
}).success).toBe(true)
|
|
506
|
+
// No assertionId: the entry cannot say WHAT was restored, so it is not a restore.
|
|
507
|
+
expect(pro3DRenderJobOutputSchema.safeParse({
|
|
508
|
+
...base,
|
|
509
|
+
restoredAssertions: [{ op: "replace", path: "/assertions/a1", reason: "r" }],
|
|
510
|
+
}).success).toBe(false)
|
|
511
|
+
})
|
|
512
|
+
})
|
|
@@ -1,18 +1,64 @@
|
|
|
1
1
|
import { describe, it, expect } from "vitest"
|
|
2
2
|
import {
|
|
3
3
|
sunoCreditType,
|
|
4
|
+
SUNO_VERSION_CREDIT_KEYS,
|
|
4
5
|
SUNO_VERSION_PRICED_OPERATIONS,
|
|
5
6
|
SUNO_SELECT_OPERATIONS,
|
|
6
7
|
SUNO_MODELS,
|
|
8
|
+
SUNO_ACTIVE_MODELS,
|
|
9
|
+
SUNO_LEGACY_MODELS,
|
|
10
|
+
SUNO_ADD_TRACK_MODELS,
|
|
11
|
+
DEFAULT_SUNO_MODEL,
|
|
12
|
+
sunoModelHonoursDuration,
|
|
13
|
+
MODEL_CATALOG,
|
|
7
14
|
} from "../index.js"
|
|
8
15
|
|
|
9
16
|
describe("sunoCreditType — the ONE implementation of the Suno route pricing contract", () => {
|
|
10
17
|
it("prices generate/cover/extend by model version", () => {
|
|
18
|
+
expect(sunoCreditType("V6", "suno-generate")).toBe("suno-v6")
|
|
19
|
+
expect(sunoCreditType("V6_WILD", "suno-cover")).toBe("suno-v6_wild")
|
|
20
|
+
expect(sunoCreditType("V6_MINI", "suno-extend")).toBe("suno-v6_mini")
|
|
11
21
|
expect(sunoCreditType("V5_5", "suno-generate")).toBe("suno-v5_5")
|
|
12
22
|
expect(sunoCreditType("V5", "suno-cover")).toBe("suno-v5")
|
|
13
23
|
expect(sunoCreditType("V5_5", "suno-extend")).toBe("suno-v5_5")
|
|
14
24
|
})
|
|
15
25
|
|
|
26
|
+
// A new active version with no key would silently bill at the operation
|
|
27
|
+
// key — and be invisible in /admin/models. Every active version must own a
|
|
28
|
+
// key, and that key must be the pricing identifier of the catalog entry
|
|
29
|
+
// whose dataValue is that version (the Models tab + /v1/models contract).
|
|
30
|
+
it("gives every ACTIVE version its own credit key, matching its catalog entry", () => {
|
|
31
|
+
const sunoEntries = Object.values(MODEL_CATALOG).filter((m) => m.family === "Suno")
|
|
32
|
+
for (const model of SUNO_ACTIVE_MODELS) {
|
|
33
|
+
const key = SUNO_VERSION_CREDIT_KEYS[model]
|
|
34
|
+
expect(key, `${model} has no SUNO_VERSION_CREDIT_KEYS row`).toBeTruthy()
|
|
35
|
+
for (const op of SUNO_VERSION_PRICED_OPERATIONS) {
|
|
36
|
+
expect(sunoCreditType(model, op)).toBe(key)
|
|
37
|
+
}
|
|
38
|
+
const entry = sunoEntries.find((m) => m.dataValue === model)
|
|
39
|
+
expect(entry, `${model} has no MODEL_CATALOG entry with dataValue`).toBeTruthy()
|
|
40
|
+
expect(entry!.pricing.map((p) => p.identifier)).toContain(key)
|
|
41
|
+
}
|
|
42
|
+
})
|
|
43
|
+
|
|
44
|
+
it("keeps the model lists active-first with the default at the head", () => {
|
|
45
|
+
expect([...SUNO_ACTIVE_MODELS]).toEqual(["V6", "V6_WILD", "V6_MINI"])
|
|
46
|
+
expect(SUNO_MODELS[0]).toBe(DEFAULT_SUNO_MODEL)
|
|
47
|
+
expect([...SUNO_MODELS]).toEqual([...SUNO_ACTIVE_MODELS, ...SUNO_LEGACY_MODELS])
|
|
48
|
+
// The two halves never overlap.
|
|
49
|
+
for (const legacy of SUNO_LEGACY_MODELS) {
|
|
50
|
+
expect(SUNO_ACTIVE_MODELS as readonly string[]).not.toContain(legacy)
|
|
51
|
+
}
|
|
52
|
+
for (const m of SUNO_ADD_TRACK_MODELS) expect(SUNO_MODELS as readonly string[]).toContain(m)
|
|
53
|
+
for (const m of SUNO_ACTIVE_MODELS) expect(SUNO_ADD_TRACK_MODELS as readonly string[]).toContain(m)
|
|
54
|
+
})
|
|
55
|
+
|
|
56
|
+
it("honours duration on the V6 family only (V5_5 was struck from the docs sentence)", () => {
|
|
57
|
+
for (const m of SUNO_ACTIVE_MODELS) expect(sunoModelHonoursDuration(m)).toBe(true)
|
|
58
|
+
for (const m of SUNO_LEGACY_MODELS) expect(sunoModelHonoursDuration(m)).toBe(false)
|
|
59
|
+
expect(sunoModelHonoursDuration(undefined)).toBe(false)
|
|
60
|
+
})
|
|
61
|
+
|
|
16
62
|
it("falls back to the operation key for versions with no dedicated price", () => {
|
|
17
63
|
expect(sunoCreditType("V4", "suno-generate")).toBe("suno-generate")
|
|
18
64
|
expect(sunoCreditType("V4_5ALL", "suno-cover")).toBe("suno-cover")
|
|
@@ -0,0 +1,45 @@
|
|
|
1
|
+
import { describe, it, expect } from "vitest"
|
|
2
|
+
import {
|
|
3
|
+
DEFAULT_TEMPLATE_CATEGORY,
|
|
4
|
+
LEGACY_TEMPLATE_CATEGORIES,
|
|
5
|
+
TEMPLATE_CATEGORIES,
|
|
6
|
+
isTemplateCategory,
|
|
7
|
+
normalizeTemplateCategory,
|
|
8
|
+
resolveTemplateCategory,
|
|
9
|
+
templateCategoryStoredValues,
|
|
10
|
+
} from "../template-categories.js"
|
|
11
|
+
|
|
12
|
+
describe("template categories", () => {
|
|
13
|
+
it("is the eight use cases, each once", () => {
|
|
14
|
+
expect(TEMPLATE_CATEGORIES).toHaveLength(8)
|
|
15
|
+
expect(new Set(TEMPLATE_CATEGORIES).size).toBe(8)
|
|
16
|
+
expect(isTemplateCategory(DEFAULT_TEMPLATE_CATEGORY)).toBe(true)
|
|
17
|
+
})
|
|
18
|
+
|
|
19
|
+
it("every legacy value lands on a current category", () => {
|
|
20
|
+
for (const [legacy, home] of Object.entries(LEGACY_TEMPLATE_CATEGORIES)) {
|
|
21
|
+
expect(isTemplateCategory(legacy), `${legacy} is not a legacy value any more`).toBe(false)
|
|
22
|
+
expect(isTemplateCategory(home), `${legacy} → ${home}`).toBe(true)
|
|
23
|
+
}
|
|
24
|
+
})
|
|
25
|
+
|
|
26
|
+
it("resolves a current value as is, a legacy one as its home, anything else as nothing", () => {
|
|
27
|
+
expect(resolveTemplateCategory("video-ads")).toBe("video-ads")
|
|
28
|
+
expect(resolveTemplateCategory("video-production")).toBe("video-ads")
|
|
29
|
+
expect(resolveTemplateCategory("nonsense")).toBeUndefined()
|
|
30
|
+
expect(resolveTemplateCategory(null)).toBeUndefined()
|
|
31
|
+
expect(resolveTemplateCategory(undefined)).toBeUndefined()
|
|
32
|
+
})
|
|
33
|
+
|
|
34
|
+
it("normalizes the unknown to the default bucket", () => {
|
|
35
|
+
expect(normalizeTemplateCategory("nonsense")).toBe(DEFAULT_TEMPLATE_CATEGORY)
|
|
36
|
+
expect(normalizeTemplateCategory(null)).toBe(DEFAULT_TEMPLATE_CATEGORY)
|
|
37
|
+
expect(normalizeTemplateCategory("social-media")).toBe("social-creatives")
|
|
38
|
+
})
|
|
39
|
+
|
|
40
|
+
it("lists the stored values a category matches, the category first", () => {
|
|
41
|
+
expect(templateCategoryStoredValues("video-ads")).toEqual(["video-ads", "video-production"])
|
|
42
|
+
expect(templateCategoryStoredValues("social-creatives")).toEqual(["social-creatives", "audio-music", "social-media"])
|
|
43
|
+
expect(templateCategoryStoredValues("static-ads")).toEqual(["static-ads"])
|
|
44
|
+
})
|
|
45
|
+
})
|
|
@@ -57,11 +57,20 @@ describe("VIDEO_REF_LIMITS_BY_PROVIDER ⇄ MODEL_CATALOG drift guard", () => {
|
|
|
57
57
|
})
|
|
58
58
|
|
|
59
59
|
describe("VIDEO_REF_LIMITS_BY_PROVIDER caps for the newly-covered models", () => {
|
|
60
|
-
it("VEO
|
|
61
|
-
expect(VIDEO_REF_LIMITS_BY_PROVIDER["veo3"]).toEqual({ images: 3 })
|
|
60
|
+
it("the ref-capable VEO SKUs carry images:3 (the REFERENCE_2_VIDEO slice cap in kie/video.ts)", () => {
|
|
62
61
|
expect(VIDEO_REF_LIMITS_BY_PROVIDER["veo3.1"]).toEqual({ images: 3 })
|
|
63
62
|
expect(VIDEO_REF_LIMITS_BY_PROVIDER["veo3_lite"]).toEqual({ images: 3 })
|
|
64
63
|
})
|
|
64
|
+
|
|
65
|
+
it("veo3 (VEO 3.1 QUALITY) carries NO cap — KIE serves reference-to-video on Fast/Lite only", () => {
|
|
66
|
+
// Its own words on a production job (2026-09-04, app-reports lane G):
|
|
67
|
+
// "Reference to video only supports the Veo Fast model and Veo Lite
|
|
68
|
+
// model." Until 2026-09-15 this entry claimed images:3, so every
|
|
69
|
+
// reference-carrying Quality run was a 422 after the credits were
|
|
70
|
+
// reserved. Absent ⇒ 0 everywhere the map is read.
|
|
71
|
+
expect(VIDEO_REF_LIMITS_BY_PROVIDER["veo3"]).toBeUndefined()
|
|
72
|
+
expect(MODEL_CATALOG["veo3"]?.features).not.toContain("reference-image")
|
|
73
|
+
})
|
|
65
74
|
it("kling-3-omni and grok-i2v carry images:7", () => {
|
|
66
75
|
expect(VIDEO_REF_LIMITS_BY_PROVIDER["kling-3-omni"]).toEqual({ images: 7 })
|
|
67
76
|
expect(VIDEO_REF_LIMITS_BY_PROVIDER["grok-i2v"]).toEqual({ images: 7 })
|