@nodaro/prompts 1.0.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/LICENSE +105 -0
- package/README.md +27 -0
- package/dist/index.cjs +21240 -0
- package/dist/index.cjs.map +1 -0
- package/dist/index.d.cts +3599 -0
- package/dist/index.d.ts +3599 -0
- package/dist/index.js +20854 -0
- package/dist/index.js.map +1 -0
- package/package.json +49 -0
- package/src/__tests__/__snapshots__/prompt-builder-segments.test.ts.snap +65 -0
- package/src/__tests__/action-fx.test.ts +155 -0
- package/src/__tests__/apply-picker-json.test.ts +50 -0
- package/src/__tests__/assemble-image-input.test.ts +248 -0
- package/src/__tests__/assemble-suno-input.test.ts +283 -0
- package/src/__tests__/brand-tokens.test.ts +81 -0
- package/src/__tests__/build-image-prompt-element-injection.test.ts +118 -0
- package/src/__tests__/build-image-prompt-hybrid-format.test.ts +131 -0
- package/src/__tests__/build-image-prompt-mentions.test.ts +809 -0
- package/src/__tests__/build-image-prompt-reference-cap.test.ts +57 -0
- package/src/__tests__/build-image-prompt-reference-numbering.test.ts +237 -0
- package/src/__tests__/build-image-prompt-reference-order.test.ts +313 -0
- package/src/__tests__/camera-motions-from-connections.test.ts +71 -0
- package/src/__tests__/catalog-gapfill.test.ts +53 -0
- package/src/__tests__/character-convergence-image.test.ts +217 -0
- package/src/__tests__/character-default-role-image.test.ts +167 -0
- package/src/__tests__/character-default-role-video.test.ts +168 -0
- package/src/__tests__/character-default-role.test.ts +70 -0
- package/src/__tests__/character-fx.test.ts +200 -0
- package/src/__tests__/entity-prompts-location.test.ts +97 -0
- package/src/__tests__/entity-prompts.test.ts +240 -0
- package/src/__tests__/expand-extra-refs-role.test.ts +50 -0
- package/src/__tests__/factory-presets.test.ts +1045 -0
- package/src/__tests__/factory-snippets.test.ts +68 -0
- package/src/__tests__/framing-multi.test.ts +71 -0
- package/src/__tests__/framing-vantage.test.ts +39 -0
- package/src/__tests__/i18n-entry-completeness.test.ts +269 -0
- package/src/__tests__/identity-lock.test.ts +46 -0
- package/src/__tests__/instrumentation.test.ts +68 -0
- package/src/__tests__/lighting-multi.test.ts +68 -0
- package/src/__tests__/location-convergence-image.test.ts +124 -0
- package/src/__tests__/mention-lock-flag.test.ts +499 -0
- package/src/__tests__/multi-picker-spec.test.ts +71 -0
- package/src/__tests__/music-genre.test.ts +103 -0
- package/src/__tests__/music-mood.test.ts +87 -0
- package/src/__tests__/object-creature-convergence-image.test.ts +105 -0
- package/src/__tests__/parameter-prompt-hint.test.ts +270 -0
- package/src/__tests__/parameter-registry-sync.test.ts +243 -0
- package/src/__tests__/person-age.test.ts +80 -0
- package/src/__tests__/person-analyzer-invariants.test.ts +47 -0
- package/src/__tests__/person-body-axes.test.ts +67 -0
- package/src/__tests__/person-facial-geometry.test.ts +156 -0
- package/src/__tests__/person-regional-aesthetic.test.ts +161 -0
- package/src/__tests__/person-sections.test.ts +18 -0
- package/src/__tests__/picker-analyzer-registry.test.ts +108 -0
- package/src/__tests__/picker-catalogs-project.test.ts +85 -0
- package/src/__tests__/picker-catalogs.test.ts +53 -0
- package/src/__tests__/picker-limits.test.ts +20 -0
- package/src/__tests__/prompt-builder-segments.test.ts +183 -0
- package/src/__tests__/prompt-builder-structured-fields.test.ts +40 -0
- package/src/__tests__/prompt-builder.test.ts +1773 -0
- package/src/__tests__/prompt-wizard-categories.test.ts +31 -0
- package/src/__tests__/provider-prompt-doctrine.test.ts +49 -0
- package/src/__tests__/resolve-prompt-append.test.ts +58 -0
- package/src/__tests__/resolve-prompt.test.ts +44 -0
- package/src/__tests__/role-picker-shared.test.ts +208 -0
- package/src/__tests__/seedance-2-inputs.test.ts +173 -0
- package/src/__tests__/seedance-extend.test.ts +44 -0
- package/src/__tests__/sound-aggregator.test.ts +350 -0
- package/src/__tests__/style-presets.test.ts +34 -0
- package/src/__tests__/temporal-multi.test.ts +74 -0
- package/src/__tests__/transitions.test.ts +213 -0
- package/src/__tests__/video-reference-features.test.ts +41 -0
- package/src/__tests__/video-reference-leading-refs.test.ts +90 -0
- package/src/__tests__/video-reference-resolver.test.ts +233 -0
- package/src/__tests__/video-reference-roles.test.ts +96 -0
- package/src/__tests__/voice-character.test.ts +48 -0
- package/src/__tests__/voice-delivery.test.ts +41 -0
- package/src/__tests__/wardrobe.test.ts +25 -0
- package/src/action-fx.ts +255 -0
- package/src/aesthetic.ts +435 -0
- package/src/assemble-image-input.ts +236 -0
- package/src/assemble-suno-input.ts +147 -0
- package/src/atmosphere.ts +104 -0
- package/src/backdrop.ts +131 -0
- package/src/brand-tokens.ts +154 -0
- package/src/camera-format.ts +76 -0
- package/src/camera-motions.ts +615 -0
- package/src/character-fx.ts +274 -0
- package/src/color-look.ts +103 -0
- package/src/composition-effects.ts +67 -0
- package/src/entity-prompts.ts +231 -0
- package/src/era.ts +292 -0
- package/src/exposure-settings.ts +142 -0
- package/src/factory-presets/generate-image.ts +1644 -0
- package/src/factory-presets/generate-video.ts +1116 -0
- package/src/factory-presets/index.ts +46 -0
- package/src/factory-presets/lottie-overlay.ts +166 -0
- package/src/factory-presets/motion-graphics.ts +350 -0
- package/src/factory-presets/music.ts +734 -0
- package/src/factory-presets/sfx.ts +136 -0
- package/src/factory-presets/shared-image.ts +207 -0
- package/src/factory-presets/switchx.ts +65 -0
- package/src/factory-presets/text.ts +292 -0
- package/src/factory-presets/types.ts +51 -0
- package/src/factory-presets/video-edit.ts +136 -0
- package/src/factory-presets/voice.ts +172 -0
- package/src/factory-presets.ts +2 -0
- package/src/factory-snippets/catalog.ts +105 -0
- package/src/factory-snippets/index.ts +16 -0
- package/src/factory-snippets/types.ts +33 -0
- package/src/framing.ts +634 -0
- package/src/held-prop.ts +187 -0
- package/src/identity-lock.ts +213 -0
- package/src/index.ts +66 -0
- package/src/instrumentation.ts +337 -0
- package/src/lens.ts +59 -0
- package/src/lighting.ts +229 -0
- package/src/loop-subject.ts +276 -0
- package/src/materials.ts +184 -0
- package/src/mood.ts +186 -0
- package/src/music-genre.ts +662 -0
- package/src/music-mood.ts +137 -0
- package/src/object-asset-presets.ts +81 -0
- package/src/parameter-prompt-hint.ts +281 -0
- package/src/person.ts +1368 -0
- package/src/photo-genre.ts +151 -0
- package/src/photographer.ts +612 -0
- package/src/picker-analyzer-registry.ts +374 -0
- package/src/picker-catalogs.ts +858 -0
- package/src/pose.ts +246 -0
- package/src/post-process-effects.ts +94 -0
- package/src/prompt-builder-structured-fields.ts +116 -0
- package/src/prompt-builder.ts +2964 -0
- package/src/prompt-templates.ts +50 -0
- package/src/prompt-wizard-categories.ts +334 -0
- package/src/provider-prompt-doctrine.ts +85 -0
- package/src/render-quality.ts +89 -0
- package/src/resolve-prompt.ts +120 -0
- package/src/seedance-2-inputs.ts +100 -0
- package/src/setting.ts +130 -0
- package/src/sound-aggregator.ts +241 -0
- package/src/style-presets.ts +162 -0
- package/src/style.ts +99 -0
- package/src/styling.ts +585 -0
- package/src/temporal.ts +151 -0
- package/src/transitions.ts +333 -0
- package/src/video-reference-resolver.ts +823 -0
- package/src/voice-character.ts +237 -0
- package/src/voice-delivery.ts +142 -0
- package/src/wardrobe.ts +179 -0
|
@@ -0,0 +1,70 @@
|
|
|
1
|
+
// packages/shared/src/__tests__/character-default-role.test.ts
|
|
2
|
+
import { describe, it, expect } from "vitest"
|
|
3
|
+
import { resolveDefaultRole, sanitizeRole } from "@nodaro/shared"
|
|
4
|
+
import { characterLockToRefLock, getIdentityLockClause } from "../identity-lock.js"
|
|
5
|
+
|
|
6
|
+
describe("resolveDefaultRole", () => {
|
|
7
|
+
it("prefers an explicit defaultRole verbatim (Custom survives)", () => {
|
|
8
|
+
expect(resolveDefaultRole("clothes", undefined, "wired-character")).toBe("clothes")
|
|
9
|
+
// A Custom role wins over a defaultUsageMode-derived role.
|
|
10
|
+
expect(resolveDefaultRole("earrings", "face", "wired-character")).toBe("earrings")
|
|
11
|
+
})
|
|
12
|
+
|
|
13
|
+
it("falls back to the defaultUsageMode-derived role when no defaultRole", () => {
|
|
14
|
+
expect(resolveDefaultRole(undefined, "face", "wired-character")).toBe("face")
|
|
15
|
+
expect(resolveDefaultRole(undefined, "pose", "wired-character")).toBe("pose")
|
|
16
|
+
expect(resolveDefaultRole(undefined, "style", "wired-character")).toBe("style")
|
|
17
|
+
})
|
|
18
|
+
|
|
19
|
+
it("collapses non-preset usage modes to the source default", () => {
|
|
20
|
+
expect(resolveDefaultRole(undefined, "identical", "wired-character")).toBe("person")
|
|
21
|
+
expect(resolveDefaultRole(undefined, "face-pose", "wired-character")).toBe("person")
|
|
22
|
+
expect(resolveDefaultRole(undefined, "emotion", "wired-character")).toBe("person")
|
|
23
|
+
expect(resolveDefaultRole(undefined, undefined, "wired-character")).toBe("person")
|
|
24
|
+
})
|
|
25
|
+
|
|
26
|
+
it("ignores a blank/whitespace defaultRole", () => {
|
|
27
|
+
expect(resolveDefaultRole(" ", "style", "wired-character")).toBe("style")
|
|
28
|
+
expect(resolveDefaultRole("", undefined, "wired-character")).toBe("person")
|
|
29
|
+
})
|
|
30
|
+
})
|
|
31
|
+
|
|
32
|
+
describe("characterLockToRefLock", () => {
|
|
33
|
+
it("maps off -> disabled (no line)", () => {
|
|
34
|
+
expect(characterLockToRefLock("off")).toEqual({ enabled: false })
|
|
35
|
+
})
|
|
36
|
+
|
|
37
|
+
it("maps soft -> enabled with a {ref}-BOUND mild clause (per-reference attribution)", () => {
|
|
38
|
+
const lock = characterLockToRefLock("soft")
|
|
39
|
+
expect(lock.enabled).toBe(true)
|
|
40
|
+
expect(lock.text).toContain("{ref}")
|
|
41
|
+
expect(lock.text).toContain("overall facial likeness")
|
|
42
|
+
})
|
|
43
|
+
|
|
44
|
+
it("maps strict -> enabled with a {ref}-BOUND strong clause", () => {
|
|
45
|
+
const lock = characterLockToRefLock("strict")
|
|
46
|
+
expect(lock.enabled).toBe(true)
|
|
47
|
+
expect(lock.text).toContain("{ref}")
|
|
48
|
+
expect(lock.text).toContain("must match exactly")
|
|
49
|
+
})
|
|
50
|
+
|
|
51
|
+
it("coerces undefined -> soft (DEFAULT_IDENTITY_LOCK, the accepted back-compat behavior)", () => {
|
|
52
|
+
expect(characterLockToRefLock(undefined)).toEqual(characterLockToRefLock("soft"))
|
|
53
|
+
})
|
|
54
|
+
|
|
55
|
+
it("soft and strict escalate like getIdentityLockClause but stay reference-bound", () => {
|
|
56
|
+
// The node ladder mirrors the config panel's semantics; the emitted text is
|
|
57
|
+
// the {ref}-bound variant so multi-character lock blocks stay attributable.
|
|
58
|
+
expect(getIdentityLockClause("soft")).toBeTruthy()
|
|
59
|
+
expect(getIdentityLockClause("strict")).toBeTruthy()
|
|
60
|
+
expect(characterLockToRefLock("soft").text).not.toBe(getIdentityLockClause("soft"))
|
|
61
|
+
expect(characterLockToRefLock("strict").text).not.toBe(getIdentityLockClause("strict"))
|
|
62
|
+
})
|
|
63
|
+
})
|
|
64
|
+
|
|
65
|
+
describe("sanitizeRole (hoisted to shared)", () => {
|
|
66
|
+
it("slugifies a Custom role", () => {
|
|
67
|
+
expect(sanitizeRole("Gold Ring")).toBe("gold-ring")
|
|
68
|
+
expect(sanitizeRole(" freckles ")).toBe("freckles")
|
|
69
|
+
})
|
|
70
|
+
})
|
|
@@ -0,0 +1,200 @@
|
|
|
1
|
+
import { describe, expect, it } from "vitest"
|
|
2
|
+
import { CHARACTER_FX, CHARACTER_FX_IDS, CHARACTER_FX_CATEGORY_ORDER, CHARACTER_FX_CATEGORY_LABELS, composeCharacterFxHintFromConnections, getCharacterFx, getCharacterFxLabel, getCharacterFxPromptHint } from "../character-fx.js"
|
|
3
|
+
|
|
4
|
+
describe("character-fx catalog", () => {
|
|
5
|
+
it("ships 57 unique entries", () => {
|
|
6
|
+
expect(CHARACTER_FX).toHaveLength(57)
|
|
7
|
+
expect(new Set(CHARACTER_FX_IDS).size).toBe(57)
|
|
8
|
+
})
|
|
9
|
+
|
|
10
|
+
it("every entry has a non-empty id, label, and description", () => {
|
|
11
|
+
for (const c of CHARACTER_FX) {
|
|
12
|
+
expect(c.id).toMatch(/^[a-z][a-z0-9-]*$/)
|
|
13
|
+
expect(c.label.length).toBeGreaterThan(0)
|
|
14
|
+
expect(c.description.length).toBeGreaterThan(0)
|
|
15
|
+
}
|
|
16
|
+
})
|
|
17
|
+
|
|
18
|
+
it("category counts match spec §3.3", () => {
|
|
19
|
+
const counts: Record<string, number> = {}
|
|
20
|
+
for (const c of CHARACTER_FX) counts[c.category] = (counts[c.category] ?? 0) + 1
|
|
21
|
+
expect(counts).toEqual({
|
|
22
|
+
"transformation": 14, "power": 12, "body-mod": 9, "face-expression": 8, "aura-ambient": 14,
|
|
23
|
+
})
|
|
24
|
+
})
|
|
25
|
+
|
|
26
|
+
it("category order covers every category and matches labels", () => {
|
|
27
|
+
expect(new Set(CHARACTER_FX_CATEGORY_ORDER)).toEqual(
|
|
28
|
+
new Set(Object.keys(CHARACTER_FX_CATEGORY_LABELS)),
|
|
29
|
+
)
|
|
30
|
+
})
|
|
31
|
+
|
|
32
|
+
it("auto + none have empty promptHint by design", () => {
|
|
33
|
+
expect(getCharacterFxPromptHint("auto")).toBe("")
|
|
34
|
+
expect(getCharacterFxPromptHint("none")).toBe("")
|
|
35
|
+
})
|
|
36
|
+
|
|
37
|
+
it("every non-empty promptHint contains 'the subject' (regex substitution depends on it)", () => {
|
|
38
|
+
const empty = ["auto", "none"]
|
|
39
|
+
for (const c of CHARACTER_FX) {
|
|
40
|
+
if (empty.includes(c.id)) {
|
|
41
|
+
expect(c.promptHint).toBe("")
|
|
42
|
+
} else {
|
|
43
|
+
expect(c.promptHint, `${c.id} promptHint missing 'the subject'`)
|
|
44
|
+
.toMatch(/\bthe subject\b/)
|
|
45
|
+
}
|
|
46
|
+
}
|
|
47
|
+
})
|
|
48
|
+
})
|
|
49
|
+
|
|
50
|
+
describe("getCharacterFx / getCharacterFxLabel / getCharacterFxPromptHint", () => {
|
|
51
|
+
it("returns the entry for a known id", () => {
|
|
52
|
+
expect(getCharacterFx("werewolf")?.label).toBe("Werewolf")
|
|
53
|
+
})
|
|
54
|
+
|
|
55
|
+
it("returns undefined for nullish + unknown ids", () => {
|
|
56
|
+
expect(getCharacterFx(undefined)).toBeUndefined()
|
|
57
|
+
expect(getCharacterFx(null)).toBeUndefined()
|
|
58
|
+
expect(getCharacterFx("nonexistent")).toBeUndefined()
|
|
59
|
+
})
|
|
60
|
+
|
|
61
|
+
it("getCharacterFxLabel falls back to title-cased id", () => {
|
|
62
|
+
expect(getCharacterFxLabel("nonexistent-id")).toBe("Nonexistent Id")
|
|
63
|
+
})
|
|
64
|
+
|
|
65
|
+
it("getCharacterFxLabel returns empty for null/undefined", () => {
|
|
66
|
+
expect(getCharacterFxLabel(null)).toBe("")
|
|
67
|
+
expect(getCharacterFxLabel(undefined)).toBe("")
|
|
68
|
+
})
|
|
69
|
+
})
|
|
70
|
+
|
|
71
|
+
describe("composeCharacterFxHintFromConnections — single-pick", () => {
|
|
72
|
+
it("returns bare hint when no target + no timing", () => {
|
|
73
|
+
const r = composeCharacterFxHintFromConnections("werewolf", [])
|
|
74
|
+
expect(r).toBe(getCharacterFxPromptHint("werewolf"))
|
|
75
|
+
})
|
|
76
|
+
|
|
77
|
+
it("returns empty for undefined / unknown / 'auto'", () => {
|
|
78
|
+
expect(composeCharacterFxHintFromConnections(undefined, ["Aria"])).toBe("")
|
|
79
|
+
expect(composeCharacterFxHintFromConnections("unknown-id", ["Aria"])).toBe("")
|
|
80
|
+
expect(composeCharacterFxHintFromConnections("auto", ["Aria"])).toBe("")
|
|
81
|
+
})
|
|
82
|
+
|
|
83
|
+
it("treats null like undefined (falsy guard)", () => {
|
|
84
|
+
expect(composeCharacterFxHintFromConnections(null as unknown as undefined, ["Aria"])).toBe("")
|
|
85
|
+
})
|
|
86
|
+
|
|
87
|
+
it("substitutes 'the subject' globally with the target name", () => {
|
|
88
|
+
const r = composeCharacterFxHintFromConnections("werewolf", ["Aria"])
|
|
89
|
+
expect(r).not.toContain("the subject")
|
|
90
|
+
expect(r).toContain("Aria")
|
|
91
|
+
})
|
|
92
|
+
|
|
93
|
+
it("joins multiple target names with ' and '", () => {
|
|
94
|
+
const r = composeCharacterFxHintFromConnections("werewolf", ["Aria", "Sam"])
|
|
95
|
+
expect(r).toContain("Aria and Sam")
|
|
96
|
+
})
|
|
97
|
+
|
|
98
|
+
it("preserves apostrophe-s constructions: 'the subject's body' → 'Aria's body'", () => {
|
|
99
|
+
// Pick an entry that uses "the subject's" — e.g., werewolf or glow-trace
|
|
100
|
+
const r = composeCharacterFxHintFromConnections("glow-trace", ["Aria"])
|
|
101
|
+
expect(r).toContain("Aria")
|
|
102
|
+
// Confirm no leftover "the subject"s anywhere:
|
|
103
|
+
expect(r).not.toMatch(/\bthe subject\b/)
|
|
104
|
+
})
|
|
105
|
+
})
|
|
106
|
+
|
|
107
|
+
describe("composeCharacterFxHintFromConnections — timing clauses", () => {
|
|
108
|
+
it("appends position clause when non-auto", () => {
|
|
109
|
+
const r = composeCharacterFxHintFromConnections("werewolf", [], { position: "middle" })
|
|
110
|
+
expect(r).toContain("the effect occurs in the middle of the clip")
|
|
111
|
+
})
|
|
112
|
+
|
|
113
|
+
it("appends duration clause when non-auto", () => {
|
|
114
|
+
const r = composeCharacterFxHintFromConnections("werewolf", [], { duration: "long" })
|
|
115
|
+
expect(r).toContain("manifesting over approximately 3 seconds")
|
|
116
|
+
})
|
|
117
|
+
|
|
118
|
+
it("appends intensity clause when non-auto", () => {
|
|
119
|
+
const r = composeCharacterFxHintFromConnections("werewolf", [], { intensity: "dynamic" })
|
|
120
|
+
expect(r).toContain("with dynamic energy and assertive flourish")
|
|
121
|
+
})
|
|
122
|
+
|
|
123
|
+
it("appends all three timing clauses in position/duration/intensity order", () => {
|
|
124
|
+
const r = composeCharacterFxHintFromConnections(
|
|
125
|
+
"werewolf",
|
|
126
|
+
[],
|
|
127
|
+
{ position: "middle", duration: "long", intensity: "dynamic" },
|
|
128
|
+
)
|
|
129
|
+
const idxPos = r.indexOf("the effect occurs")
|
|
130
|
+
const idxDur = r.indexOf("manifesting over")
|
|
131
|
+
const idxInt = r.indexOf("with dynamic energy")
|
|
132
|
+
expect(idxPos).toBeGreaterThan(-1)
|
|
133
|
+
expect(idxDur).toBeGreaterThan(idxPos)
|
|
134
|
+
expect(idxInt).toBeGreaterThan(idxDur)
|
|
135
|
+
})
|
|
136
|
+
|
|
137
|
+
it("composes target + timing in spec §4.2 worked-example order", () => {
|
|
138
|
+
const r = composeCharacterFxHintFromConnections(
|
|
139
|
+
"werewolf",
|
|
140
|
+
["Aria Voss"],
|
|
141
|
+
{ position: "middle", duration: "long", intensity: "dynamic" },
|
|
142
|
+
)
|
|
143
|
+
expect(r).toContain("Aria Voss")
|
|
144
|
+
expect(r).not.toContain("the subject")
|
|
145
|
+
expect(r).toContain("the effect occurs in the middle of the clip")
|
|
146
|
+
expect(r).toContain("manifesting over approximately 3 seconds")
|
|
147
|
+
expect(r).toContain("with dynamic energy and assertive flourish")
|
|
148
|
+
})
|
|
149
|
+
})
|
|
150
|
+
|
|
151
|
+
describe("composeCharacterFxHintFromConnections — multi-pick", () => {
|
|
152
|
+
it("scalar form is identical to single-element array", () => {
|
|
153
|
+
const a = composeCharacterFxHintFromConnections("werewolf", ["Aria"])
|
|
154
|
+
const b = composeCharacterFxHintFromConnections(["werewolf"], ["Aria"])
|
|
155
|
+
expect(a).toBe(b)
|
|
156
|
+
})
|
|
157
|
+
|
|
158
|
+
it("joins two base hints with ', and ', substituting target per id", () => {
|
|
159
|
+
const r = composeCharacterFxHintFromConnections(["werewolf", "fire-breathe"], ["Aria"])
|
|
160
|
+
// Both entries should mention Aria; "the subject" should NOT appear anywhere
|
|
161
|
+
expect(r).toContain("Aria")
|
|
162
|
+
expect(r).not.toMatch(/\bthe subject\b/)
|
|
163
|
+
expect(r).toContain(", and ")
|
|
164
|
+
})
|
|
165
|
+
|
|
166
|
+
it("dedupes duplicate ids", () => {
|
|
167
|
+
const r1 = composeCharacterFxHintFromConnections(["werewolf", "werewolf"], [])
|
|
168
|
+
const r2 = composeCharacterFxHintFromConnections("werewolf", [])
|
|
169
|
+
expect(r1).toBe(r2)
|
|
170
|
+
})
|
|
171
|
+
|
|
172
|
+
it("caps at 2 — extras dropped", () => {
|
|
173
|
+
const r = composeCharacterFxHintFromConnections(
|
|
174
|
+
["werewolf", "fire-breathe", "levitation", "wings-grow"],
|
|
175
|
+
[],
|
|
176
|
+
)
|
|
177
|
+
expect(r).toContain(getCharacterFxPromptHint("werewolf"))
|
|
178
|
+
expect(r).toContain(getCharacterFxPromptHint("fire-breathe"))
|
|
179
|
+
expect(r).not.toContain(getCharacterFxPromptHint("levitation"))
|
|
180
|
+
})
|
|
181
|
+
|
|
182
|
+
it("returns '' on empty array", () => {
|
|
183
|
+
expect(composeCharacterFxHintFromConnections([], ["Aria"])).toBe("")
|
|
184
|
+
})
|
|
185
|
+
|
|
186
|
+
it("filters out 'auto' (empty hint) entries silently", () => {
|
|
187
|
+
const justWerewolf = composeCharacterFxHintFromConnections("werewolf", ["Aria"])
|
|
188
|
+
const withAuto = composeCharacterFxHintFromConnections(["auto", "werewolf"], ["Aria"])
|
|
189
|
+
expect(withAuto).toBe(justWerewolf)
|
|
190
|
+
})
|
|
191
|
+
|
|
192
|
+
it("applies timing clauses once at outer layer (not per id)", () => {
|
|
193
|
+
const r = composeCharacterFxHintFromConnections(
|
|
194
|
+
["werewolf", "fire-breathe"],
|
|
195
|
+
["Aria"],
|
|
196
|
+
{ intensity: "dynamic" },
|
|
197
|
+
)
|
|
198
|
+
expect((r.match(/with dynamic energy/g) ?? []).length).toBe(1)
|
|
199
|
+
})
|
|
200
|
+
})
|
|
@@ -0,0 +1,97 @@
|
|
|
1
|
+
import { describe, it, expect } from "vitest"
|
|
2
|
+
import { LOCATION_ASSET_TYPES, LOCATION_ATTACH_COLUMNS, type LocationAssetType, type LocationAttachColumn } from "@nodaro/shared"
|
|
3
|
+
import { buildLocationMotionPrompt, buildLocationRefinePrompt } from "../entity-prompts.js"
|
|
4
|
+
|
|
5
|
+
describe("location entity-prompts constants", () => {
|
|
6
|
+
it("LOCATION_ASSET_TYPES has 6 values", () => {
|
|
7
|
+
expect(LOCATION_ASSET_TYPES).toEqual([
|
|
8
|
+
"timeOfDay", "weather", "seasons", "angles", "lighting", "custom"
|
|
9
|
+
])
|
|
10
|
+
})
|
|
11
|
+
it("LOCATION_ATTACH_COLUMNS has the snake_case DB column names (incl. reference-sheet buckets)", () => {
|
|
12
|
+
expect(LOCATION_ATTACH_COLUMNS).toEqual([
|
|
13
|
+
"time_of_day", "weather", "seasons", "angles", "lighting", "atmosphere_motions",
|
|
14
|
+
// Reference-sheet buckets (migration 200) — locations get sheets + detail_closeups.
|
|
15
|
+
"sheets", "detail_closeups",
|
|
16
|
+
])
|
|
17
|
+
})
|
|
18
|
+
it("LocationAssetType union derives from constant", () => {
|
|
19
|
+
const x: LocationAssetType = "lighting"
|
|
20
|
+
expect(LOCATION_ASSET_TYPES.includes(x)).toBe(true)
|
|
21
|
+
})
|
|
22
|
+
it("LocationAttachColumn union derives from constant", () => {
|
|
23
|
+
const x: LocationAttachColumn = "atmosphere_motions"
|
|
24
|
+
expect(LOCATION_ATTACH_COLUMNS.includes(x)).toBe(true)
|
|
25
|
+
})
|
|
26
|
+
})
|
|
27
|
+
|
|
28
|
+
describe("buildLocationMotionPrompt", () => {
|
|
29
|
+
it("uses canonicalDescription when present", () => {
|
|
30
|
+
const result = buildLocationMotionPrompt({
|
|
31
|
+
name: "Old Library",
|
|
32
|
+
motionPrompt: "slow dolly-in",
|
|
33
|
+
canonicalDescription: "A dimly-lit Victorian library with leather-bound books, brass fixtures, warm golden light.",
|
|
34
|
+
})
|
|
35
|
+
expect(result).toContain("A dimly-lit Victorian library")
|
|
36
|
+
expect(result).toContain("Camera move: slow dolly-in")
|
|
37
|
+
expect(result).toContain("Slow, ambient, cinematic")
|
|
38
|
+
})
|
|
39
|
+
|
|
40
|
+
it("falls back to category + name when canonicalDescription absent", () => {
|
|
41
|
+
const result = buildLocationMotionPrompt({
|
|
42
|
+
name: "Subway Tunnel",
|
|
43
|
+
category: "interior",
|
|
44
|
+
motionPrompt: "static atmospheric",
|
|
45
|
+
})
|
|
46
|
+
expect(result).toContain("interior")
|
|
47
|
+
expect(result).toContain("Subway Tunnel")
|
|
48
|
+
expect(result).toContain("Camera move: static atmospheric")
|
|
49
|
+
})
|
|
50
|
+
|
|
51
|
+
it("uses 'A generic location' fallback when all scene fields empty", () => {
|
|
52
|
+
const result = buildLocationMotionPrompt({
|
|
53
|
+
name: "",
|
|
54
|
+
motionPrompt: "drone fly-over",
|
|
55
|
+
})
|
|
56
|
+
expect(result).toContain("A generic location")
|
|
57
|
+
expect(result).toContain("Camera move: drone fly-over")
|
|
58
|
+
})
|
|
59
|
+
|
|
60
|
+
it("defaults style to 'realistic'", () => {
|
|
61
|
+
const result = buildLocationMotionPrompt({
|
|
62
|
+
name: "X",
|
|
63
|
+
motionPrompt: "y",
|
|
64
|
+
})
|
|
65
|
+
expect(result).toContain("realistic style")
|
|
66
|
+
})
|
|
67
|
+
|
|
68
|
+
it("honors explicit style", () => {
|
|
69
|
+
const result = buildLocationMotionPrompt({
|
|
70
|
+
name: "X",
|
|
71
|
+
motionPrompt: "y",
|
|
72
|
+
style: "anime",
|
|
73
|
+
})
|
|
74
|
+
expect(result).toContain("anime style")
|
|
75
|
+
})
|
|
76
|
+
})
|
|
77
|
+
|
|
78
|
+
describe("buildLocationRefinePrompt", () => {
|
|
79
|
+
it("leads with the trimmed edit instruction", () => {
|
|
80
|
+
const result = buildLocationRefinePrompt({ editPrompt: " make it night, add fog " })
|
|
81
|
+
expect(result.startsWith("make it night, add fog,")).toBe(true)
|
|
82
|
+
})
|
|
83
|
+
|
|
84
|
+
it("frames the instruction as an establishing shot without weaving in a persisted description", () => {
|
|
85
|
+
const result = buildLocationRefinePrompt({ editPrompt: "add neon signage" })
|
|
86
|
+
expect(result).toContain("add neon signage")
|
|
87
|
+
expect(result).toContain("wide establishing shot")
|
|
88
|
+
})
|
|
89
|
+
|
|
90
|
+
it("defaults style to 'realistic'", () => {
|
|
91
|
+
expect(buildLocationRefinePrompt({ editPrompt: "x" })).toContain("realistic art style")
|
|
92
|
+
})
|
|
93
|
+
|
|
94
|
+
it("honors explicit style", () => {
|
|
95
|
+
expect(buildLocationRefinePrompt({ editPrompt: "x", style: "anime" })).toContain("anime art style")
|
|
96
|
+
})
|
|
97
|
+
})
|
|
@@ -0,0 +1,240 @@
|
|
|
1
|
+
import { describe, it, expect } from "vitest"
|
|
2
|
+
import { PLACEHOLDER_CHARACTER_NAME, LOCATION_REFERENCE_PHOTO_KINDS, LOCATION_REFERENCE_PHOTO_KIND_LABELS, locationReferencePhotoKindLabel, OBJECT_ASSET_TYPES, OBJECT_ATTACH_COLUMNS } from "@nodaro/shared"
|
|
3
|
+
import { buildCharacterPrompt, buildObjectPrompt, buildLocationPrompt, buildFaceTemplateInputs, buildMotionPrompt, buildObjectMotionPrompt } from "../entity-prompts.js"
|
|
4
|
+
|
|
5
|
+
describe("buildCharacterPrompt", () => {
|
|
6
|
+
it("combines name, gender, description with outfit and style", () => {
|
|
7
|
+
const prompt = buildCharacterPrompt({
|
|
8
|
+
name: "Aria",
|
|
9
|
+
gender: "female",
|
|
10
|
+
description: "silver hair",
|
|
11
|
+
baseOutfit: "leather armour",
|
|
12
|
+
style: "anime",
|
|
13
|
+
})
|
|
14
|
+
expect(prompt).toContain("Aria, female, silver hair")
|
|
15
|
+
expect(prompt).toContain("wearing leather armour")
|
|
16
|
+
expect(prompt).toContain("anime style")
|
|
17
|
+
expect(prompt).toContain("front view, looking at camera")
|
|
18
|
+
expect(prompt).toContain("full body portrait")
|
|
19
|
+
})
|
|
20
|
+
|
|
21
|
+
it("defaults style to realistic when unset", () => {
|
|
22
|
+
const prompt = buildCharacterPrompt({ name: "Bob" })
|
|
23
|
+
expect(prompt).toContain("realistic style")
|
|
24
|
+
})
|
|
25
|
+
|
|
26
|
+
it("omits outfit clause when baseOutfit is empty", () => {
|
|
27
|
+
const prompt = buildCharacterPrompt({ name: "Bob" })
|
|
28
|
+
expect(prompt).not.toContain("wearing")
|
|
29
|
+
})
|
|
30
|
+
|
|
31
|
+
it("skips missing gender/description fields in description block", () => {
|
|
32
|
+
const prompt = buildCharacterPrompt({ name: "Solo" })
|
|
33
|
+
// Should just have "Solo," — no extra commas for missing fields
|
|
34
|
+
expect(prompt).toMatch(/^Solo,/)
|
|
35
|
+
})
|
|
36
|
+
|
|
37
|
+
it("drops the auto-assigned placeholder name from the prompt", () => {
|
|
38
|
+
// Character Studio auto-assigns PLACEHOLDER_CHARACTER_NAME when the user
|
|
39
|
+
// clicks Generate before naming. The string must not reach the model.
|
|
40
|
+
const prompt = buildCharacterPrompt({
|
|
41
|
+
name: PLACEHOLDER_CHARACTER_NAME,
|
|
42
|
+
gender: "female",
|
|
43
|
+
description: "red hair",
|
|
44
|
+
})
|
|
45
|
+
expect(prompt).not.toContain(PLACEHOLDER_CHARACTER_NAME)
|
|
46
|
+
expect(prompt).toMatch(/^female, red hair,/)
|
|
47
|
+
})
|
|
48
|
+
})
|
|
49
|
+
|
|
50
|
+
describe("buildObjectPrompt", () => {
|
|
51
|
+
it("uses category and product-photography suffix", () => {
|
|
52
|
+
const prompt = buildObjectPrompt({
|
|
53
|
+
name: "pistol",
|
|
54
|
+
category: "weapon",
|
|
55
|
+
description: "brass fittings",
|
|
56
|
+
style: "realistic",
|
|
57
|
+
})
|
|
58
|
+
expect(prompt).toContain("Single weapon pistol")
|
|
59
|
+
expect(prompt).toContain("brass fittings")
|
|
60
|
+
expect(prompt).toContain("realistic art style")
|
|
61
|
+
expect(prompt).toContain("product photography style")
|
|
62
|
+
})
|
|
63
|
+
|
|
64
|
+
it("defaults category to 'object' when unset", () => {
|
|
65
|
+
const prompt = buildObjectPrompt({ name: "widget" })
|
|
66
|
+
expect(prompt).toContain("Single object widget")
|
|
67
|
+
})
|
|
68
|
+
})
|
|
69
|
+
|
|
70
|
+
describe("buildLocationPrompt", () => {
|
|
71
|
+
it("uses scene framing with establishing-shot suffix", () => {
|
|
72
|
+
const prompt = buildLocationPrompt({
|
|
73
|
+
name: "Neo Tokyo",
|
|
74
|
+
category: "urban",
|
|
75
|
+
description: "rain-slick streets",
|
|
76
|
+
style: "3d-pixar",
|
|
77
|
+
})
|
|
78
|
+
expect(prompt).toContain("urban scene")
|
|
79
|
+
expect(prompt).toContain("Neo Tokyo")
|
|
80
|
+
expect(prompt).toContain("rain-slick streets")
|
|
81
|
+
expect(prompt).toContain("3d-pixar art style")
|
|
82
|
+
expect(prompt).toContain("wide establishing shot")
|
|
83
|
+
expect(prompt).toContain("no people")
|
|
84
|
+
})
|
|
85
|
+
|
|
86
|
+
it("defaults category to 'location' when unset", () => {
|
|
87
|
+
const prompt = buildLocationPrompt({ name: "The Grove" })
|
|
88
|
+
expect(prompt).toContain("location scene")
|
|
89
|
+
})
|
|
90
|
+
})
|
|
91
|
+
|
|
92
|
+
describe("buildFaceTemplateInputs", () => {
|
|
93
|
+
it("combines name and description into one description string", () => {
|
|
94
|
+
const inputs = buildFaceTemplateInputs({
|
|
95
|
+
name: "Elena",
|
|
96
|
+
description: "high cheekbones",
|
|
97
|
+
style: "realistic",
|
|
98
|
+
})
|
|
99
|
+
expect(inputs.description).toBe("Elena, high cheekbones")
|
|
100
|
+
expect(inputs.style).toBe("realistic")
|
|
101
|
+
})
|
|
102
|
+
|
|
103
|
+
it("omits missing description fields", () => {
|
|
104
|
+
const inputs = buildFaceTemplateInputs({ name: "Solo" })
|
|
105
|
+
expect(inputs.description).toBe("Solo")
|
|
106
|
+
expect(inputs.style).toBe("realistic")
|
|
107
|
+
})
|
|
108
|
+
})
|
|
109
|
+
|
|
110
|
+
describe("buildMotionPrompt", () => {
|
|
111
|
+
it("composes character base + motion + style", () => {
|
|
112
|
+
const out = buildMotionPrompt({
|
|
113
|
+
name: "Alex",
|
|
114
|
+
gender: "male",
|
|
115
|
+
description: "tall, dark hair",
|
|
116
|
+
style: "realistic",
|
|
117
|
+
baseOutfit: "leather jacket",
|
|
118
|
+
motionPrompt: "walking confidently",
|
|
119
|
+
})
|
|
120
|
+
expect(out).toBe("Alex, male, tall, dark hair, wearing leather jacket, walking confidently. realistic style.")
|
|
121
|
+
})
|
|
122
|
+
|
|
123
|
+
it("defaults style to realistic and omits outfit when missing", () => {
|
|
124
|
+
const out = buildMotionPrompt({ name: "Mia", motionPrompt: "waving" })
|
|
125
|
+
expect(out).toBe("Mia, waving. realistic style.")
|
|
126
|
+
})
|
|
127
|
+
|
|
128
|
+
it("drops the placeholder name; bare motion prompt is the result", () => {
|
|
129
|
+
const out = buildMotionPrompt({ name: PLACEHOLDER_CHARACTER_NAME, motionPrompt: "waving" })
|
|
130
|
+
expect(out).not.toContain(PLACEHOLDER_CHARACTER_NAME)
|
|
131
|
+
expect(out).toBe("waving. realistic style.")
|
|
132
|
+
})
|
|
133
|
+
})
|
|
134
|
+
|
|
135
|
+
// ---------------------------------------------------------------------------
|
|
136
|
+
// Phase 2 #3: kind-tagged reference-photo subject-line annotation. The map
|
|
137
|
+
// is the single source of truth for the human-friendly label each photo kind
|
|
138
|
+
// renders as in the assembled prompt; the helper is a typed wrapper.
|
|
139
|
+
// ---------------------------------------------------------------------------
|
|
140
|
+
|
|
141
|
+
describe("locationReferencePhotoKindLabel", () => {
|
|
142
|
+
it("returns the expected label for each kind", () => {
|
|
143
|
+
expect(locationReferencePhotoKindLabel("wide")).toBe("wide-angle reference")
|
|
144
|
+
expect(locationReferencePhotoKindLabel("interior")).toBe("interior reference")
|
|
145
|
+
expect(locationReferencePhotoKindLabel("exterior")).toBe("exterior reference")
|
|
146
|
+
expect(locationReferencePhotoKindLabel("detail")).toBe("detail reference")
|
|
147
|
+
expect(locationReferencePhotoKindLabel("moodBoard")).toBe("mood-board reference")
|
|
148
|
+
expect(locationReferencePhotoKindLabel("other")).toBe("reference")
|
|
149
|
+
})
|
|
150
|
+
|
|
151
|
+
it("LOCATION_REFERENCE_PHOTO_KIND_LABELS covers every kind in LOCATION_REFERENCE_PHOTO_KINDS", () => {
|
|
152
|
+
// Guard against the kind enum drifting from the label map. The map is a
|
|
153
|
+
// `Record<LocationReferencePhotoKind, string>` so TS would already catch
|
|
154
|
+
// missing keys, but this runtime check also verifies labels are non-empty.
|
|
155
|
+
for (const kind of LOCATION_REFERENCE_PHOTO_KINDS) {
|
|
156
|
+
expect(LOCATION_REFERENCE_PHOTO_KIND_LABELS[kind]).toBeDefined()
|
|
157
|
+
expect(LOCATION_REFERENCE_PHOTO_KIND_LABELS[kind].length).toBeGreaterThan(0)
|
|
158
|
+
}
|
|
159
|
+
expect(Object.keys(LOCATION_REFERENCE_PHOTO_KIND_LABELS).sort()).toEqual(
|
|
160
|
+
[...LOCATION_REFERENCE_PHOTO_KINDS].sort(),
|
|
161
|
+
)
|
|
162
|
+
})
|
|
163
|
+
})
|
|
164
|
+
|
|
165
|
+
describe("OBJECT_ASSET_TYPES", () => {
|
|
166
|
+
it("contains the 5 expected values", () => {
|
|
167
|
+
expect([...OBJECT_ASSET_TYPES].sort()).toEqual([
|
|
168
|
+
"angles", "custom", "materials", "motion", "variations",
|
|
169
|
+
])
|
|
170
|
+
})
|
|
171
|
+
})
|
|
172
|
+
|
|
173
|
+
describe("OBJECT_ATTACH_COLUMNS", () => {
|
|
174
|
+
it("aligns with the append_object_asset RPC whitelist (migration 147 + 200 sheet buckets)", () => {
|
|
175
|
+
expect([...OBJECT_ATTACH_COLUMNS].sort()).toEqual([
|
|
176
|
+
// migration 147 originals + migration 200 reference-sheet buckets
|
|
177
|
+
// (sheets, detail_closeups) — objects do NOT get outfit_variations.
|
|
178
|
+
"angles", "detail_closeups", "materials", "motion_clips", "sheets", "variations",
|
|
179
|
+
])
|
|
180
|
+
})
|
|
181
|
+
})
|
|
182
|
+
|
|
183
|
+
describe("buildObjectMotionPrompt", () => {
|
|
184
|
+
it("uses canonicalDescription when present", () => {
|
|
185
|
+
const out = buildObjectMotionPrompt({
|
|
186
|
+
name: "Magic Sword",
|
|
187
|
+
motionPrompt: "slow rotation",
|
|
188
|
+
canonicalDescription: "A glowing katana with ancient runes",
|
|
189
|
+
})
|
|
190
|
+
expect(out).toMatch(/A glowing katana/)
|
|
191
|
+
expect(out).toMatch(/Motion: slow rotation/)
|
|
192
|
+
expect(out).toMatch(/realistic style/)
|
|
193
|
+
expect(out).toMatch(/product-showcase quality/)
|
|
194
|
+
})
|
|
195
|
+
|
|
196
|
+
it("falls back to category + name when no canonicalDescription", () => {
|
|
197
|
+
const out = buildObjectMotionPrompt({
|
|
198
|
+
name: "Magic Sword",
|
|
199
|
+
category: "weapon",
|
|
200
|
+
motionPrompt: "hover",
|
|
201
|
+
})
|
|
202
|
+
expect(out).toMatch(/weapon, Magic Sword/)
|
|
203
|
+
expect(out).toMatch(/Motion: hover/)
|
|
204
|
+
})
|
|
205
|
+
|
|
206
|
+
it("uses the generic-object placeholder when all identity fields are empty", () => {
|
|
207
|
+
const out = buildObjectMotionPrompt({ name: "", motionPrompt: "spin" })
|
|
208
|
+
expect(out).toMatch(/A generic object/)
|
|
209
|
+
expect(out).toMatch(/Motion: spin/)
|
|
210
|
+
})
|
|
211
|
+
|
|
212
|
+
it("appends seedPromptHint when provided", () => {
|
|
213
|
+
const out = buildObjectMotionPrompt({
|
|
214
|
+
name: "Statue",
|
|
215
|
+
motionPrompt: "rotate-360",
|
|
216
|
+
seedPromptHint: "marble surface with veining",
|
|
217
|
+
})
|
|
218
|
+
expect(out).toMatch(/Motion: rotate-360/)
|
|
219
|
+
expect(out).toMatch(/marble surface with veining\.$/)
|
|
220
|
+
})
|
|
221
|
+
|
|
222
|
+
it("omits seedSuffix when seedPromptHint is empty/whitespace", () => {
|
|
223
|
+
const out = buildObjectMotionPrompt({
|
|
224
|
+
name: "Statue",
|
|
225
|
+
motionPrompt: "rotate",
|
|
226
|
+
seedPromptHint: " ",
|
|
227
|
+
})
|
|
228
|
+
expect(out).not.toMatch(/\.\s+\.$/) // no awkward ".. ." pattern
|
|
229
|
+
expect(out).toMatch(/product-showcase quality\.$/)
|
|
230
|
+
})
|
|
231
|
+
|
|
232
|
+
it("honors a custom style override", () => {
|
|
233
|
+
const out = buildObjectMotionPrompt({
|
|
234
|
+
name: "Toy",
|
|
235
|
+
motionPrompt: "spin",
|
|
236
|
+
style: "3d-pixar",
|
|
237
|
+
})
|
|
238
|
+
expect(out).toMatch(/3d-pixar style/)
|
|
239
|
+
})
|
|
240
|
+
})
|
|
@@ -0,0 +1,50 @@
|
|
|
1
|
+
// packages/shared/src/__tests__/expand-extra-refs-role.test.ts
|
|
2
|
+
//
|
|
3
|
+
// `expandExtraRefsToConnectedReferences` must stamp the character node's
|
|
4
|
+
// hybrid `defaultRole` + mapped `identityLock` (from ExtraRefCharacterContext)
|
|
5
|
+
// onto character-sourced extras — with the per-ref `usageMode` override
|
|
6
|
+
// SUPPRESSING the node defaultRole (the override wins), while the lock is
|
|
7
|
+
// orthogonal (always inherited unless the ref itself overrides downstream).
|
|
8
|
+
import { describe, it, expect } from "vitest"
|
|
9
|
+
import { expandExtraRefsToConnectedReferences } from "@nodaro/shared"
|
|
10
|
+
import { characterLockToRefLock } from "../identity-lock.js"
|
|
11
|
+
|
|
12
|
+
const lookup = (ctx: Record<string, unknown>) => (slug: string) =>
|
|
13
|
+
slug === "kira" ? (ctx as never) : undefined
|
|
14
|
+
|
|
15
|
+
describe("expandExtraRefsToConnectedReferences — defaultRole + identityLock stamping", () => {
|
|
16
|
+
it("stamps ctx.defaultRole when the extra has no per-ref usageMode override", () => {
|
|
17
|
+
const [ref] = expandExtraRefsToConnectedReferences(
|
|
18
|
+
[{ url: "u", characterSlug: "kira", variantSlug: "look" }],
|
|
19
|
+
lookup({ displayName: "Kira", defaultRole: "hair" }),
|
|
20
|
+
)
|
|
21
|
+
expect(ref.defaultRole).toBe("hair")
|
|
22
|
+
})
|
|
23
|
+
|
|
24
|
+
it("a per-ref usageMode override suppresses the node defaultRole (override wins)", () => {
|
|
25
|
+
const [ref] = expandExtraRefsToConnectedReferences(
|
|
26
|
+
[{ url: "u", characterSlug: "kira", variantSlug: "look", usageMode: "face" }],
|
|
27
|
+
lookup({ displayName: "Kira", defaultRole: "hair" }),
|
|
28
|
+
)
|
|
29
|
+
expect(ref.defaultRole).toBeUndefined()
|
|
30
|
+
expect(ref.defaultUsageMode).toBe("face")
|
|
31
|
+
})
|
|
32
|
+
|
|
33
|
+
it("stamps ctx.identityLock (node's mapped lock) onto the character extra", () => {
|
|
34
|
+
const lock = characterLockToRefLock("strict")
|
|
35
|
+
const [ref] = expandExtraRefsToConnectedReferences(
|
|
36
|
+
[{ url: "u", characterSlug: "kira", variantSlug: "look" }],
|
|
37
|
+
lookup({ displayName: "Kira", identityLock: lock }),
|
|
38
|
+
)
|
|
39
|
+
expect(ref.identityLock).toEqual(lock)
|
|
40
|
+
})
|
|
41
|
+
|
|
42
|
+
it("manual extras (no characterSlug) get neither field", () => {
|
|
43
|
+
const [ref] = expandExtraRefsToConnectedReferences(
|
|
44
|
+
[{ url: "u", description: "a brass lantern" }],
|
|
45
|
+
lookup({ defaultRole: "hair", identityLock: characterLockToRefLock("soft") }),
|
|
46
|
+
)
|
|
47
|
+
expect(ref.defaultRole).toBeUndefined()
|
|
48
|
+
expect(ref.identityLock).toBeUndefined()
|
|
49
|
+
})
|
|
50
|
+
})
|