@nodaro/shared 3.11.0 → 3.12.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/dist/index.cjs +2047 -84
- package/dist/index.cjs.map +1 -1
- package/dist/index.d.cts +1556 -27
- package/dist/index.d.ts +1556 -27
- package/dist/index.js +1861 -85
- package/dist/index.js.map +1 -1
- package/package.json +1 -1
- package/src/__tests__/caption-styles.test.ts +207 -0
- package/src/__tests__/edl-multicam.test.ts +304 -0
- package/src/__tests__/edl.test.ts +822 -0
- package/src/__tests__/fan-out-rows.test.ts +208 -0
- package/src/__tests__/instagram-scrape.test.ts +66 -0
- package/src/__tests__/llm-models.test.ts +48 -11
- package/src/__tests__/meta-ads-scrape.test.ts +284 -0
- package/src/__tests__/node-runtime-keys.test.ts +15 -0
- package/src/__tests__/presentation-utils.test.ts +67 -0
- package/src/__tests__/producer-types.test.ts +19 -0
- package/src/__tests__/schedule-rules.test.ts +265 -0
- package/src/__tests__/speaker-layouts.test.ts +203 -0
- package/src/__tests__/transcribe-capabilities.test.ts +104 -0
- package/src/__tests__/transcribe-preflight.test.ts +60 -0
- package/src/__tests__/trigger-feeds.test.ts +39 -0
- package/src/__tests__/video-duration-auto.test.ts +65 -0
- package/src/__tests__/video-duration.test.ts +56 -0
- package/src/__tests__/video-link.test.ts +137 -0
- package/src/__tests__/workflow-export-strip.test.ts +59 -1
- package/src/caption-styles.ts +240 -0
- package/src/credit-identifiers.ts +31 -0
- package/src/edit-plan-contract.ts +96 -0
- package/src/edl-multicam.ts +185 -0
- package/src/edl.ts +747 -0
- package/src/entity-image-handle.ts +24 -1
- package/src/fan-out-rows.ts +213 -0
- package/src/i18n/action-fx.he.ts +8 -8
- package/src/i18n/aesthetic.he.ts +8 -8
- package/src/i18n/animals.he.ts +34 -34
- package/src/i18n/atmosphere.he.ts +6 -6
- package/src/i18n/backdrop.he.ts +7 -7
- package/src/i18n/camera-format.he.ts +4 -4
- package/src/i18n/camera-motions.he.ts +20 -20
- package/src/i18n/character-fx.he.ts +16 -16
- package/src/i18n/character-motion.he.ts +184 -184
- package/src/i18n/color-look.he.ts +4 -4
- package/src/i18n/composition-effects.he.ts +3 -3
- package/src/i18n/era.he.ts +9 -9
- package/src/i18n/exposure-settings.he.ts +2 -2
- package/src/i18n/framing.he.ts +13 -13
- package/src/i18n/furniture.he.ts +52 -52
- package/src/i18n/held-prop.he.ts +10 -10
- package/src/i18n/instrumentation.he.ts +26 -26
- package/src/i18n/lens.he.ts +6 -6
- package/src/i18n/lighting.he.ts +17 -17
- package/src/i18n/loop-subject.he.ts +4 -4
- package/src/i18n/materials.he.ts +8 -8
- package/src/i18n/mood.he.ts +12 -12
- package/src/i18n/music-genre.he.ts +9 -9
- package/src/i18n/music-mood.he.ts +7 -7
- package/src/i18n/person.he.ts +94 -94
- package/src/i18n/photo-genre.he.ts +24 -24
- package/src/i18n/photographer.he.ts +21 -21
- package/src/i18n/pose.he.ts +14 -14
- package/src/i18n/post-process-effects.he.ts +4 -4
- package/src/i18n/render-quality.he.ts +7 -7
- package/src/i18n/setting.he.ts +14 -14
- package/src/i18n/style.he.ts +9 -9
- package/src/i18n/styling.he.ts +126 -126
- package/src/i18n/temporal.he.ts +8 -8
- package/src/i18n/transitions.he.ts +9 -9
- package/src/i18n/vehicles.he.ts +25 -25
- package/src/i18n/voice-character.he.ts +14 -14
- package/src/i18n/voice-delivery.he.ts +11 -11
- package/src/i18n/weapons.he.ts +23 -23
- package/src/index.ts +206 -3
- package/src/instagram-scrape.ts +204 -0
- package/src/llm-models.ts +80 -3
- package/src/meta-ads-scrape.ts +463 -0
- package/src/model-catalog.ts +48 -5
- package/src/model-constants.ts +148 -5
- package/src/node-mappable-fields.ts +2 -0
- package/src/node-runtime-keys.ts +28 -0
- package/src/presentation-utils.ts +49 -0
- package/src/producer-types.ts +20 -0
- package/src/schedule-rules.ts +484 -0
- package/src/speaker-layouts.ts +220 -0
- package/src/transcribe-preflight.ts +101 -0
- package/src/trigger-feeds.ts +59 -0
- package/src/trigger-node-types.ts +20 -0
- package/src/video-duration-auto.ts +18 -0
- package/src/video-duration.ts +32 -0
- package/src/video-link.ts +167 -0
- package/src/workflow-export.ts +37 -1
|
@@ -0,0 +1,208 @@
|
|
|
1
|
+
import { describe, it, expect } from "vitest"
|
|
2
|
+
import {
|
|
3
|
+
NON_PROMPT_TEXT_LANES,
|
|
4
|
+
fanOutTextFeedsPrompt,
|
|
5
|
+
compactWithRows,
|
|
6
|
+
resolveListFanOut,
|
|
7
|
+
planFanOut,
|
|
8
|
+
alignedFieldList,
|
|
9
|
+
liveRowColumn,
|
|
10
|
+
type FanOutCandidate,
|
|
11
|
+
} from "../fan-out-rows.js"
|
|
12
|
+
import { REPEAT_PLACEHOLDER, encodeProviderItem } from "../repeat-types.js"
|
|
13
|
+
|
|
14
|
+
describe("fanOutTextFeedsPrompt", () => {
|
|
15
|
+
it("a list wired to the prompt (or to a node's single generic input) feeds the prompt", () => {
|
|
16
|
+
expect(fanOutTextFeedsPrompt("generate-image", "prompt")).toBe(true)
|
|
17
|
+
expect(fanOutTextFeedsPrompt("text-to-speech", "in")).toBe(true)
|
|
18
|
+
expect(fanOutTextFeedsPrompt("generate-image", undefined)).toBe(true)
|
|
19
|
+
expect(fanOutTextFeedsPrompt(undefined, null)).toBe(true)
|
|
20
|
+
})
|
|
21
|
+
|
|
22
|
+
it("a list wired to a lane the routers divert does NOT feed the prompt", () => {
|
|
23
|
+
expect(fanOutTextFeedsPrompt("generate-image", "negative")).toBe(false)
|
|
24
|
+
expect(fanOutTextFeedsPrompt("llm-chat", "system-prompt")).toBe(false)
|
|
25
|
+
expect(fanOutTextFeedsPrompt("ai-avatar", "script")).toBe(false)
|
|
26
|
+
})
|
|
27
|
+
|
|
28
|
+
it("is scoped by node type: the same handle can be the prompt elsewhere", () => {
|
|
29
|
+
expect(fanOutTextFeedsPrompt("add-captions", "transcript")).toBe(false)
|
|
30
|
+
expect(fanOutTextFeedsPrompt("forced-alignment", "transcript")).toBe(true)
|
|
31
|
+
})
|
|
32
|
+
|
|
33
|
+
it("a '*' lane covers every node type, including ones that do not exist yet", () => {
|
|
34
|
+
expect(NON_PROMPT_TEXT_LANES.negative).toBe("*")
|
|
35
|
+
expect(fanOutTextFeedsPrompt("some-future-node", "negative")).toBe(false)
|
|
36
|
+
})
|
|
37
|
+
|
|
38
|
+
it("a handle named like an Object.prototype member is just an unlisted handle", () => {
|
|
39
|
+
expect(fanOutTextFeedsPrompt("generate-image", "constructor")).toBe(true)
|
|
40
|
+
expect(fanOutTextFeedsPrompt("generate-image", "toString")).toBe(true)
|
|
41
|
+
})
|
|
42
|
+
})
|
|
43
|
+
|
|
44
|
+
describe("compactWithRows", () => {
|
|
45
|
+
it("drops empty cells but remembers the row each kept value came from", () => {
|
|
46
|
+
expect(compactWithRows(["a", "", " ", "d"])).toEqual({ items: ["a", "d"], rowIndices: [0, 3] })
|
|
47
|
+
})
|
|
48
|
+
|
|
49
|
+
it("is the identity (with 0..n-1 rows) for a list with no holes", () => {
|
|
50
|
+
expect(compactWithRows(["a", "b"])).toEqual({ items: ["a", "b"], rowIndices: [0, 1] })
|
|
51
|
+
})
|
|
52
|
+
|
|
53
|
+
it("keeps the ORIGINAL text of a kept value (no trimming of content)", () => {
|
|
54
|
+
expect(compactWithRows([" a ", "b\nc"]).items).toEqual([" a ", "b\nc"])
|
|
55
|
+
})
|
|
56
|
+
})
|
|
57
|
+
|
|
58
|
+
describe("liveRowColumn", () => {
|
|
59
|
+
it("drops a row only when EVERY cell of it is blank, and keeps an empty cell of a live row in place", () => {
|
|
60
|
+
const rows = [["a", "x"], ["b", ""], ["", ""], ["", "z"]]
|
|
61
|
+
expect(liveRowColumn(rows, 0)).toEqual(["a", "b", ""])
|
|
62
|
+
expect(liveRowColumn(rows, 1)).toEqual(["x", "", "z"])
|
|
63
|
+
})
|
|
64
|
+
|
|
65
|
+
it("a single-column list with a trailing blank row does not grow", () => {
|
|
66
|
+
expect(liveRowColumn([["a"], ["b"], [""]], 0)).toEqual(["a", "b"])
|
|
67
|
+
})
|
|
68
|
+
|
|
69
|
+
it("trims cells and tolerates ragged rows", () => {
|
|
70
|
+
expect(liveRowColumn([[" a "], ["b", "y"]], 1)).toEqual(["", "y"])
|
|
71
|
+
})
|
|
72
|
+
})
|
|
73
|
+
|
|
74
|
+
describe("resolveListFanOut", () => {
|
|
75
|
+
const cand = (targetHandle: string, aligned: string[]): FanOutCandidate => ({ targetHandle, aligned })
|
|
76
|
+
|
|
77
|
+
it("a single list: its values drive, on the rows they sit in", () => {
|
|
78
|
+
const only = cand("prompt", ["p1", "", "p3"])
|
|
79
|
+
expect(resolveListFanOut([only], "generate-image")).toEqual({ items: ["p1", "p3"], rowIndices: [0, 2], targetHandle: "prompt" })
|
|
80
|
+
})
|
|
81
|
+
|
|
82
|
+
it("the list that feeds the prompt drives — whichever wire the engine met first", () => {
|
|
83
|
+
const negative = cand("negative", ["n1", "n2", "n3"])
|
|
84
|
+
const prompt = cand("prompt", ["p1", "p2", "p3"])
|
|
85
|
+
const expected = { items: ["p1", "p2", "p3"], rowIndices: [0, 1, 2], targetHandle: "prompt" }
|
|
86
|
+
expect(resolveListFanOut([negative, prompt], "generate-image")).toEqual(expected)
|
|
87
|
+
expect(resolveListFanOut([prompt, negative], "generate-image")).toEqual(expected)
|
|
88
|
+
})
|
|
89
|
+
|
|
90
|
+
it("a list wired to negative ALONE drives through negative (so nothing writes it into the prompt)", () => {
|
|
91
|
+
const negative = cand("negative", ["n1", "n2"])
|
|
92
|
+
expect(resolveListFanOut([negative], "generate-image")?.targetHandle).toBe("negative")
|
|
93
|
+
})
|
|
94
|
+
|
|
95
|
+
it("a row runs when ANY column has a value in it; an empty driver cell overrides nothing", () => {
|
|
96
|
+
const negative = cand("negative", ["n1", "n2", "n3"])
|
|
97
|
+
const prompt = cand("prompt", ["p1", "", "p3"])
|
|
98
|
+
const expected = { items: ["p1", "", "p3"], rowIndices: [0, 1, 2], targetHandle: "prompt" }
|
|
99
|
+
expect(resolveListFanOut([negative, prompt], "generate-image")).toEqual(expected)
|
|
100
|
+
expect(resolveListFanOut([prompt, negative], "generate-image")).toEqual(expected)
|
|
101
|
+
})
|
|
102
|
+
|
|
103
|
+
it("a row that is empty in EVERY column does not run", () => {
|
|
104
|
+
const negative = cand("negative", ["n1", "", "n3"])
|
|
105
|
+
const prompt = cand("prompt", ["p1", "", "p3"])
|
|
106
|
+
expect(resolveListFanOut([prompt, negative], "generate-image")?.rowIndices).toEqual([0, 2])
|
|
107
|
+
})
|
|
108
|
+
|
|
109
|
+
it("the list holding the MOST values sets the rows — in either wire order — and one of another length stays out of it", () => {
|
|
110
|
+
const negative = cand("negative", ["n1", "n2", "n3"])
|
|
111
|
+
const prompt = cand("prompt", ["p1", "p2"])
|
|
112
|
+
const expected = { items: ["n1", "n2", "n3"], rowIndices: [0, 1, 2], targetHandle: "negative" }
|
|
113
|
+
expect(resolveListFanOut([negative, prompt], "generate-image")).toEqual(expected)
|
|
114
|
+
expect(resolveListFanOut([prompt, negative], "generate-image")).toEqual(expected)
|
|
115
|
+
})
|
|
116
|
+
|
|
117
|
+
it("values held decide the primary, not the row count (a long column that is mostly empty does not win)", () => {
|
|
118
|
+
const sparse = cand("negative", ["n1", "", "", "", "n5"])
|
|
119
|
+
const prompts = cand("prompt", ["p1", "p2", "p3"])
|
|
120
|
+
expect(resolveListFanOut([sparse, prompts], "generate-image")).toEqual({
|
|
121
|
+
items: ["p1", "p2", "p3"],
|
|
122
|
+
rowIndices: [0, 1, 2],
|
|
123
|
+
targetHandle: "prompt",
|
|
124
|
+
})
|
|
125
|
+
})
|
|
126
|
+
|
|
127
|
+
it("no candidates, no fan-out", () => {
|
|
128
|
+
expect(resolveListFanOut([], "generate-image")).toBeUndefined()
|
|
129
|
+
})
|
|
130
|
+
|
|
131
|
+
it("a media list never supplies the prompt: beside images, the prompts drive and every image row still runs", () => {
|
|
132
|
+
const images = cand("image", ["https://cdn/a.png", "https://cdn/b.png", "https://cdn/c.png"])
|
|
133
|
+
const prompts = cand("prompt", ["p1", "", "p3"])
|
|
134
|
+
const expected = { items: ["p1", "", "p3"], rowIndices: [0, 1, 2], targetHandle: "prompt" }
|
|
135
|
+
expect(resolveListFanOut([images, prompts], "image-to-video")).toEqual(expected)
|
|
136
|
+
expect(resolveListFanOut([prompts, images], "image-to-video")).toEqual(expected)
|
|
137
|
+
})
|
|
138
|
+
})
|
|
139
|
+
|
|
140
|
+
describe("planFanOut", () => {
|
|
141
|
+
const fanOut = { items: ["a", "c"], rowIndices: [0, 2], targetHandle: "prompt" }
|
|
142
|
+
|
|
143
|
+
it("maps every iteration to the row its item came from", () => {
|
|
144
|
+
expect(planFanOut(fanOut, "generate-image", {})).toEqual({
|
|
145
|
+
items: ["a", "c"],
|
|
146
|
+
rows: [0, 2],
|
|
147
|
+
targetHandle: "prompt",
|
|
148
|
+
})
|
|
149
|
+
})
|
|
150
|
+
|
|
151
|
+
it("repeat xN keeps each repeated iteration on ITS OWN row (the sibling columns stay paired)", () => {
|
|
152
|
+
expect(planFanOut(fanOut, "generate-image", { repeatCount: 2 })).toEqual({
|
|
153
|
+
items: ["a", "a", "c", "c"],
|
|
154
|
+
rows: [0, 0, 2, 2],
|
|
155
|
+
targetHandle: "prompt",
|
|
156
|
+
})
|
|
157
|
+
})
|
|
158
|
+
|
|
159
|
+
it("repeat-only and provider-only runs have no rows (nothing is list-driven)", () => {
|
|
160
|
+
expect(planFanOut(undefined, "generate-image", { repeatCount: 3 })).toEqual({
|
|
161
|
+
items: [REPEAT_PLACEHOLDER, REPEAT_PLACEHOLDER, REPEAT_PLACEHOLDER],
|
|
162
|
+
rows: [undefined, undefined, undefined],
|
|
163
|
+
targetHandle: undefined,
|
|
164
|
+
})
|
|
165
|
+
expect(planFanOut(undefined, "generate-image", { providers: ["a", "b"] })?.items).toEqual([
|
|
166
|
+
encodeProviderItem("a"),
|
|
167
|
+
encodeProviderItem("b"),
|
|
168
|
+
])
|
|
169
|
+
})
|
|
170
|
+
|
|
171
|
+
it("returns null when there is nothing to fan out (single run)", () => {
|
|
172
|
+
expect(planFanOut(undefined, "generate-image", {})).toBeNull()
|
|
173
|
+
expect(planFanOut({ items: ["only"], rowIndices: [0], targetHandle: "prompt" }, "generate-image", {})).toBeNull()
|
|
174
|
+
})
|
|
175
|
+
})
|
|
176
|
+
|
|
177
|
+
describe("alignedFieldList", () => {
|
|
178
|
+
const concepts = [
|
|
179
|
+
{ prompt: "p1", negative: "n1" },
|
|
180
|
+
{ prompt: "p2", negative: "" },
|
|
181
|
+
{ prompt: "p3", negative: "n3" },
|
|
182
|
+
{ prompt: "p4" },
|
|
183
|
+
{ prompt: "p5", negative: null },
|
|
184
|
+
]
|
|
185
|
+
|
|
186
|
+
it("emits one entry per element of a root array, empty where the element has no value", () => {
|
|
187
|
+
expect(alignedFieldList(concepts, "negative")).toEqual(["n1", "", "n3", "", ""])
|
|
188
|
+
expect(alignedFieldList(concepts, "prompt")).toEqual(["p1", "p2", "p3", "p4", "p5"])
|
|
189
|
+
})
|
|
190
|
+
|
|
191
|
+
it("stringifies like Extract Field does (numbers, booleans, objects)", () => {
|
|
192
|
+
expect(alignedFieldList([{ v: 1 }, { v: false }, { v: { a: 1 } }], "v")).toEqual(["1", "false", '{"a":1}'])
|
|
193
|
+
})
|
|
194
|
+
|
|
195
|
+
it("supports nested dot paths and whole-item mode", () => {
|
|
196
|
+
expect(alignedFieldList([{ a: { b: "x" } }, { a: {} }], "a.b")).toEqual(["x", ""])
|
|
197
|
+
expect(alignedFieldList(["a", "b"], "")).toEqual(["a", "b"])
|
|
198
|
+
})
|
|
199
|
+
|
|
200
|
+
it("has no answer when rows cannot be defined — not a root array, or an element fans into several values", () => {
|
|
201
|
+
expect(alignedFieldList({ prompt: "p" }, "prompt")).toBeUndefined()
|
|
202
|
+
expect(alignedFieldList([{ pages: [{ url: "u1" }, { url: "u2" }] }], "pages.url")).toBeUndefined()
|
|
203
|
+
})
|
|
204
|
+
|
|
205
|
+
it("has no answer when NO element carries the field (an all-blank list is not a list)", () => {
|
|
206
|
+
expect(alignedFieldList([{ a: 1 }, { b: 2 }], "missing")).toBeUndefined()
|
|
207
|
+
})
|
|
208
|
+
})
|
|
@@ -0,0 +1,66 @@
|
|
|
1
|
+
import { describe, it, expect } from "vitest"
|
|
2
|
+
import {
|
|
3
|
+
INSTAGRAM_SCRAPE_CREDIT_COSTS,
|
|
4
|
+
INSTAGRAM_SCRAPE_FALLBACK_CREDIT_ID,
|
|
5
|
+
INSTAGRAM_SCRAPE_MAX_SOURCES,
|
|
6
|
+
INSTAGRAM_SCRAPE_TIERS,
|
|
7
|
+
buildInstagramScrapeCreditId,
|
|
8
|
+
clampInstagramFeaturedIndex,
|
|
9
|
+
featuredInstagramOutputs,
|
|
10
|
+
instagramScrapeCreditIdFromNode,
|
|
11
|
+
instagramScrapeSources,
|
|
12
|
+
resolveInstagramScrapeCreditId,
|
|
13
|
+
splitInstagramTargets,
|
|
14
|
+
} from "../instagram-scrape.js"
|
|
15
|
+
import { resolveEffectiveSourceType } from "../entity-image-handle.js"
|
|
16
|
+
|
|
17
|
+
describe("instagram-scrape vocabulary", () => {
|
|
18
|
+
it("splitInstagramTargets: one per line / comma, strips @ and #, dedupes, caps at MAX_SOURCES", () => {
|
|
19
|
+
expect(splitInstagramTargets("@nike\n#running, adidas")).toEqual(["nike", "running", "adidas"])
|
|
20
|
+
expect(splitInstagramTargets("nike\nNIKE")).toEqual(["nike"])
|
|
21
|
+
expect(splitInstagramTargets([" puma ", "", "@vans"])).toEqual(["puma", "vans"])
|
|
22
|
+
expect(splitInstagramTargets(Array.from({ length: 9 }, (_, i) => `b${i}`))).toHaveLength(INSTAGRAM_SCRAPE_MAX_SOURCES)
|
|
23
|
+
expect(splitInstagramTargets(undefined)).toEqual([])
|
|
24
|
+
})
|
|
25
|
+
|
|
26
|
+
it("credit ids: 1 credit per requested post, tiered on count × sources, analysis folds in", () => {
|
|
27
|
+
expect(buildInstagramScrapeCreditId({ count: 20, sources: 1 })).toBe("instagram-scrape:20")
|
|
28
|
+
expect(buildInstagramScrapeCreditId({ count: 30, sources: 2 })).toBe("instagram-scrape:100")
|
|
29
|
+
expect(buildInstagramScrapeCreditId({ count: 20, sources: 1, analysis: "economy" })).toBe("instagram-scrape:20:analysis:economy")
|
|
30
|
+
expect(buildInstagramScrapeCreditId({ count: 50, sources: 1, analysis: "standard" })).toBe("instagram-scrape:50:analysis")
|
|
31
|
+
for (const tier of INSTAGRAM_SCRAPE_TIERS) {
|
|
32
|
+
expect(INSTAGRAM_SCRAPE_CREDIT_COSTS[`instagram-scrape:${tier}`]).toBe(tier)
|
|
33
|
+
expect(INSTAGRAM_SCRAPE_CREDIT_COSTS[`instagram-scrape:${tier}:analysis:economy`]).toBe(tier * 2)
|
|
34
|
+
}
|
|
35
|
+
expect(INSTAGRAM_SCRAPE_CREDIT_COSTS[INSTAGRAM_SCRAPE_FALLBACK_CREDIT_ID]).toBeDefined()
|
|
36
|
+
})
|
|
37
|
+
|
|
38
|
+
it("the node quote and the pre-Zod guard land on the SAME identifier", () => {
|
|
39
|
+
const data = { mode: "profile", targets: "nike\nadidas", count: 30, analyze: true }
|
|
40
|
+
expect(instagramScrapeSources(data)).toBe(2)
|
|
41
|
+
expect(instagramScrapeCreditIdFromNode(data)).toBe("instagram-scrape:100:analysis:economy")
|
|
42
|
+
// The wire body the node sends (targets already split to an array).
|
|
43
|
+
expect(resolveInstagramScrapeCreditId({ targets: ["nike", "adidas"], count: 30, analyze: true })).toBe("instagram-scrape:100:analysis:economy")
|
|
44
|
+
expect(resolveInstagramScrapeCreditId({})).toBe("instagram-scrape:20") // omitted count = default, 1 source
|
|
45
|
+
})
|
|
46
|
+
|
|
47
|
+
it("featured outputs: caption / first image / first video, clamped", () => {
|
|
48
|
+
const posts = [
|
|
49
|
+
{ caption: "hi", images: ["https://i/1.jpg"], videos: [], videoPreviews: [] },
|
|
50
|
+
{ caption: "vid", images: [], videos: ["https://v/2.mp4"], videoPreviews: ["https://v/2.jpg"] },
|
|
51
|
+
]
|
|
52
|
+
expect(featuredInstagramOutputs(posts, 0)).toEqual({ text: "hi", imageUrl: "https://i/1.jpg" })
|
|
53
|
+
expect(featuredInstagramOutputs(posts, 1)).toEqual({ text: "vid", imageUrl: "https://v/2.jpg", videoUrl: "https://v/2.mp4" })
|
|
54
|
+
expect(featuredInstagramOutputs(posts, 9)).toEqual(featuredInstagramOutputs(posts, 1))
|
|
55
|
+
expect(featuredInstagramOutputs([], 0)).toEqual({})
|
|
56
|
+
expect(clampInstagramFeaturedIndex(-3, 2)).toBe(0)
|
|
57
|
+
})
|
|
58
|
+
|
|
59
|
+
it("typed handles resolve as the canonical single-media producers on canvas; json keeps the raw type", () => {
|
|
60
|
+
expect(resolveEffectiveSourceType("instagram-scrape", "text")).toBe("combine-text")
|
|
61
|
+
expect(resolveEffectiveSourceType("instagram-scrape", "image")).toBe("upload-image")
|
|
62
|
+
expect(resolveEffectiveSourceType("instagram-scrape", "video")).toBe("upload-video")
|
|
63
|
+
expect(resolveEffectiveSourceType("instagram-scrape", "json")).toBe("instagram-scrape")
|
|
64
|
+
expect(resolveEffectiveSourceType("instagram-scrape", "audio")).toBe("instagram-scrape")
|
|
65
|
+
})
|
|
66
|
+
})
|
|
@@ -18,6 +18,8 @@ import {
|
|
|
18
18
|
LLM_VENDOR_LABELS,
|
|
19
19
|
groupLlmModelsByVendor,
|
|
20
20
|
orderedLlmModels,
|
|
21
|
+
REASONING_OUTPUT_FLOOR,
|
|
22
|
+
reasoningOutputFloor,
|
|
21
23
|
} from "../llm-models.js"
|
|
22
24
|
import type { LlmModelDef, LlmTier, LlmFeature } from "../llm-models.js"
|
|
23
25
|
import { PIPELINE_PINNABLE_SCRIPT_LLMS } from "../pipeline-types.js"
|
|
@@ -388,6 +390,7 @@ describe("LLM_FEATURE_DEFAULTS", () => {
|
|
|
388
390
|
"lottie-overlay",
|
|
389
391
|
"3d-title",
|
|
390
392
|
"image-to-text",
|
|
393
|
+
"meta-ads-analysis",
|
|
391
394
|
"describe-to-picker",
|
|
392
395
|
"qa-check",
|
|
393
396
|
"generate-script",
|
|
@@ -399,8 +402,8 @@ describe("LLM_FEATURE_DEFAULTS", () => {
|
|
|
399
402
|
"3d-scene",
|
|
400
403
|
]
|
|
401
404
|
|
|
402
|
-
it("has entries for all
|
|
403
|
-
expect(Object.keys(LLM_FEATURE_DEFAULTS)).toHaveLength(
|
|
405
|
+
it("has entries for all 19 features", () => {
|
|
406
|
+
expect(Object.keys(LLM_FEATURE_DEFAULTS)).toHaveLength(19)
|
|
404
407
|
for (const feature of ALL_FEATURES) {
|
|
405
408
|
expect(LLM_FEATURE_DEFAULTS).toHaveProperty(feature)
|
|
406
409
|
}
|
|
@@ -577,6 +580,38 @@ describe("reasoning effort registry", () => {
|
|
|
577
580
|
}
|
|
578
581
|
})
|
|
579
582
|
|
|
583
|
+
it("every Gemini 3 model reasons by default and is floored within what its lanes take (#1588)", () => {
|
|
584
|
+
// Measured on 3.6 (#1588): with no thinking param sent, the direct lane
|
|
585
|
+
// spent ~1,060 of a node's 1,100-token cap reasoning and returned 120
|
|
586
|
+
// characters. Unflagged, the node's small cap reaches the wire untouched.
|
|
587
|
+
// The floor is the model's own, never the 32768 default: it rides the KIE
|
|
588
|
+
// endpoint too, and the flash ones are only known to take 8192.
|
|
589
|
+
const expected: Record<string, number> = {
|
|
590
|
+
"gemini-3-flash": 8192,
|
|
591
|
+
"gemini-3.6-flash": 8192,
|
|
592
|
+
"gemini-3.7-flash": 8192,
|
|
593
|
+
"gemini-3.8-flash": 16384,
|
|
594
|
+
"gemini-3.1-pro": 16384,
|
|
595
|
+
}
|
|
596
|
+
for (const [id, floor] of Object.entries(expected)) {
|
|
597
|
+
const m = getLlmModel(id)!
|
|
598
|
+
expect(m.thinkingDefaultOn, id).toBe(true)
|
|
599
|
+
expect(reasoningOutputFloor(m), id).toBe(floor)
|
|
600
|
+
expect(reasoningOutputFloor(m), `${id} floor above its KIE-safe cap`).toBeLessThanOrEqual(m.maxOutputTokens)
|
|
601
|
+
}
|
|
602
|
+
})
|
|
603
|
+
|
|
604
|
+
it("a reasoning floor never sits below the model's default cap, and is declared only below the default", () => {
|
|
605
|
+
for (const m of LLM_MODELS) {
|
|
606
|
+
// A floor under maxOutputTokens would LOWER a call's cap — not a floor.
|
|
607
|
+
expect(reasoningOutputFloor(m), m.id).toBeGreaterThanOrEqual(m.maxOutputTokens)
|
|
608
|
+
// Declaring the default is noise that hides which models are lane-limited.
|
|
609
|
+
if (m.reasoningOutputFloor !== undefined) {
|
|
610
|
+
expect(m.reasoningOutputFloor, m.id).toBeLessThan(REASONING_OUTPUT_FLOOR)
|
|
611
|
+
}
|
|
612
|
+
}
|
|
613
|
+
})
|
|
614
|
+
|
|
580
615
|
it("gemini-3.6-flash exposes the KIE low/high thinking levels; claude-fable-5 the full Claude ladder", () => {
|
|
581
616
|
expect(getLlmModel("gemini-3.6-flash")?.reasoningEfforts).toEqual(["low", "high"])
|
|
582
617
|
expect(getLlmModel("claude-fable-5")?.reasoningEfforts).toEqual(["low", "medium", "high", "xhigh", "max"])
|
|
@@ -737,17 +772,19 @@ describe("gpt-6-astra exposure", () => {
|
|
|
737
772
|
expect(buildLlmCreditIdentifier("llm-chat", "gpt-6-astra", "xhigh")).toBe("llm-chat:premium")
|
|
738
773
|
})
|
|
739
774
|
|
|
740
|
-
it("
|
|
775
|
+
it("names EXACTLY the models KIE's non-stream responses lane fails for — the condition is per model, not per lane", () => {
|
|
741
776
|
// KIE's non-stream responses endpoint 500s ~2 calls in 3 for astra
|
|
742
|
-
// (measured 2026-09-06: 2/6 non-stream vs 5/6 streaming)
|
|
743
|
-
// gpt-5.
|
|
744
|
-
//
|
|
745
|
-
//
|
|
746
|
-
//
|
|
747
|
-
// because SSE does not reliably
|
|
777
|
+
// (measured 2026-09-06: 2/6 non-stream vs 5/6 streaming), and on
|
|
778
|
+
// 2026-09-17 a live gpt-5.6-sol call returned the same `server_error`,
|
|
779
|
+
// retiring the 2026-07-14 "the GPT-5.6 family is fine non-stream" reading.
|
|
780
|
+
// So llm-client collapses the streaming wire for THESE models only. Adding
|
|
781
|
+
// a third is a deliberate act, never copy-paste: the flag moves a model's
|
|
782
|
+
// provider cost onto the rate-table estimate, because SSE does not reliably
|
|
783
|
+
// carry `credits_consumed`.
|
|
784
|
+
const collapsed = LLM_MODELS.filter((m) => m.kieCollapseStream !== undefined).map((m) => m.id)
|
|
785
|
+
expect(collapsed.sort()).toEqual(["gpt-5.6-sol", "gpt-6-astra"])
|
|
748
786
|
expect(getLlmModel("gpt-6-astra")?.kieCollapseStream).toBe(true)
|
|
749
|
-
|
|
750
|
-
expect(others.filter((m) => m.kieCollapseStream !== undefined).map((m) => m.id)).toEqual([])
|
|
787
|
+
expect(getLlmModel("gpt-5.6-sol")?.kieCollapseStream).toBe(true)
|
|
751
788
|
})
|
|
752
789
|
})
|
|
753
790
|
|