@linxin666/dsh-pet 0.1.14 → 0.1.16
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.i18n.yaml +2 -2
- package/README.md +73 -38
- package/README.zh.md +91 -55
- package/assets/whale/pet.json +12 -1
- package/cordis.patch.yml +4 -4
- package/lib/client.js +296 -347
- package/lib/client.js.map +1 -1
- package/lib/index.js +975 -317
- package/lib/invariant.js +1 -1
- package/lib/{state-P-wsVJ6O.js → state-CFyJv0sQ.js} +15 -1
- package/lib/types/affinity.d.ts +15 -0
- package/lib/types/affinity.d.ts.map +1 -1
- package/lib/types/affinity.js +14 -0
- package/lib/types/client/PetDockEntry.d.ts +14 -12
- package/lib/types/client/PetDockEntry.d.ts.map +1 -1
- package/lib/types/client/PetDockEntry.js +13 -8
- package/lib/types/client/PetSettingsCard.d.ts +22 -9
- package/lib/types/client/PetSettingsCard.d.ts.map +1 -1
- package/lib/types/client/PetSettingsCard.js +42 -7
- package/lib/types/client/{WhalePet.d.ts → PetSprite.d.ts} +21 -20
- package/lib/types/client/PetSprite.d.ts.map +1 -0
- package/lib/types/client/{WhalePet.js → PetSprite.js} +44 -67
- package/lib/types/client/PluginSettingsCard.d.ts +26 -11
- package/lib/types/client/PluginSettingsCard.d.ts.map +1 -1
- package/lib/types/client/PluginSettingsCard.js +30 -8
- package/lib/types/client/index.d.ts +11 -8
- package/lib/types/client/index.d.ts.map +1 -1
- package/lib/types/client/index.js +32 -14
- package/lib/types/client/locales.d.ts +10 -10
- package/lib/types/client/locales.js +10 -10
- package/lib/types/client/pet-store.d.ts +5 -0
- package/lib/types/client/pet-store.d.ts.map +1 -1
- package/lib/types/client/pet-store.js +4 -0
- package/lib/types/client/settings-form.d.ts +76 -5
- package/lib/types/client/settings-form.d.ts.map +1 -1
- package/lib/types/client/settings-form.js +97 -20
- package/lib/types/client/spritesheet.d.ts +14 -48
- package/lib/types/client/spritesheet.d.ts.map +1 -1
- package/lib/types/client/spritesheet.js +13 -81
- package/lib/types/dsh-home.d.ts +17 -0
- package/lib/types/dsh-home.d.ts.map +1 -0
- package/lib/types/dsh-home.js +34 -0
- package/lib/types/event-projection.d.ts +36 -0
- package/lib/types/event-projection.d.ts.map +1 -0
- package/lib/types/event-projection.js +98 -0
- package/lib/types/index.d.ts +24 -11
- package/lib/types/index.d.ts.map +1 -1
- package/lib/types/index.js +53 -33
- package/lib/types/ledger.d.ts +79 -0
- package/lib/types/ledger.d.ts.map +1 -0
- package/lib/types/ledger.js +146 -0
- package/lib/types/persist.d.ts +15 -4
- package/lib/types/persist.d.ts.map +1 -1
- package/lib/types/persist.js +39 -8
- package/lib/types/registry.d.ts +158 -0
- package/lib/types/registry.d.ts.map +1 -0
- package/lib/types/registry.js +276 -0
- package/lib/types/routes.d.ts +10 -10
- package/lib/types/routes.d.ts.map +1 -1
- package/lib/types/routes.js +169 -51
- package/lib/types/service.d.ts +59 -57
- package/lib/types/service.d.ts.map +1 -1
- package/lib/types/service.js +111 -211
- package/package.json +2 -2
- package/src/affinity.ts +33 -0
- package/src/client/PetDockEntry.tsx +34 -26
- package/src/client/PetSettingsCard.tsx +67 -22
- package/src/client/{WhalePet.test.tsx → PetSprite.test.tsx} +56 -8
- package/src/client/{WhalePet.tsx → PetSprite.tsx} +55 -73
- package/src/client/PluginSettingsCard.tsx +90 -18
- package/src/client/index.ts +38 -16
- package/src/client/locales.ts +10 -10
- package/src/client/pet-store.ts +9 -0
- package/src/client/settings-card.module.css +9 -0
- package/src/client/settings-form.ts +153 -19
- package/src/client/spritesheet.ts +16 -92
- package/src/dsh-home.test.ts +35 -0
- package/src/dsh-home.ts +36 -0
- package/src/event-projection.ts +128 -0
- package/src/index.test.ts +23 -0
- package/src/index.ts +89 -45
- package/src/ledger.test.ts +69 -0
- package/src/ledger.ts +190 -0
- package/src/persist.test.ts +52 -3
- package/src/persist.ts +49 -11
- package/src/registry.test.ts +132 -0
- package/src/registry.ts +400 -0
- package/src/routes.ts +166 -53
- package/src/service.ts +153 -285
- package/lib/types/client/WhalePet.d.ts.map +0 -1
package/src/persist.test.ts
CHANGED
|
@@ -5,7 +5,7 @@ import { join } from 'node:path'
|
|
|
5
5
|
import { AFFINITY_MAX, emptyAffinity } from './affinity.ts'
|
|
6
6
|
import { defaultTreatConfig, emptyTreatLedger } from './treats.ts'
|
|
7
7
|
import {
|
|
8
|
-
|
|
8
|
+
DEFAULT_PET_ID,
|
|
9
9
|
DISPLAY_INSET_MAX,
|
|
10
10
|
DISPLAY_SIZE_MAX,
|
|
11
11
|
DISPLAY_SIZE_MIN,
|
|
@@ -43,7 +43,8 @@ describe('loadPetPersist', () => {
|
|
|
43
43
|
const dir = tempDir()
|
|
44
44
|
try {
|
|
45
45
|
const data = {
|
|
46
|
-
|
|
46
|
+
petId: 'otter',
|
|
47
|
+
names: { otter: '泡泡', 'whale-girl': '鲸鱼娘' },
|
|
47
48
|
affinity: { ...emptyAffinity(), points: 42, pets: 3, feeds: 1, turns: 10 },
|
|
48
49
|
treats: { ...emptyTreatLedger(), treats: 7, lastTreatGrantAt: 1234, turnsAtLastTreatGrant: 9 },
|
|
49
50
|
display: { visible: false, size: 200, right: 10, bottom: 40 },
|
|
@@ -55,17 +56,65 @@ describe('loadPetPersist', () => {
|
|
|
55
56
|
}
|
|
56
57
|
})
|
|
57
58
|
|
|
59
|
+
it('migrates the legacy flat name onto the legacy pet id', () => {
|
|
60
|
+
const dir = tempDir()
|
|
61
|
+
try {
|
|
62
|
+
writeFileSync(join(dir, 'pet.json'), JSON.stringify({
|
|
63
|
+
name: '泡泡',
|
|
64
|
+
affinity: { points: 5 },
|
|
65
|
+
}), 'utf8')
|
|
66
|
+
const loaded = loadPetPersist(dir)
|
|
67
|
+
expect(loaded.petId).toBe(DEFAULT_PET_ID)
|
|
68
|
+
expect(loaded.names).toEqual({ [DEFAULT_PET_ID]: '泡泡' })
|
|
69
|
+
expect(loaded.affinity.points).toBe(5)
|
|
70
|
+
expect(loaded.display).toEqual(defaultDisplayConfig)
|
|
71
|
+
} finally {
|
|
72
|
+
rmSync(dir, { recursive: true, force: true })
|
|
73
|
+
}
|
|
74
|
+
})
|
|
75
|
+
|
|
76
|
+
it('migrates the legacy flat name onto the persisted selection when both exist', () => {
|
|
77
|
+
const dir = tempDir()
|
|
78
|
+
try {
|
|
79
|
+
writeFileSync(join(dir, 'pet.json'), JSON.stringify({
|
|
80
|
+
petId: 'otter',
|
|
81
|
+
name: '水獭',
|
|
82
|
+
}), 'utf8')
|
|
83
|
+
const loaded = loadPetPersist(dir)
|
|
84
|
+
expect(loaded.petId).toBe('otter')
|
|
85
|
+
expect(loaded.names).toEqual({ otter: '水獭' })
|
|
86
|
+
} finally {
|
|
87
|
+
rmSync(dir, { recursive: true, force: true })
|
|
88
|
+
}
|
|
89
|
+
})
|
|
90
|
+
|
|
91
|
+
it('keeps stored per-pet names when the legacy name is also present', () => {
|
|
92
|
+
const dir = tempDir()
|
|
93
|
+
try {
|
|
94
|
+
writeFileSync(join(dir, 'pet.json'), JSON.stringify({
|
|
95
|
+
name: '旧名字',
|
|
96
|
+
names: { [DEFAULT_PET_ID]: '新名字' },
|
|
97
|
+
}), 'utf8')
|
|
98
|
+
const loaded = loadPetPersist(dir)
|
|
99
|
+
expect(loaded.names[DEFAULT_PET_ID]).toBe('新名字')
|
|
100
|
+
} finally {
|
|
101
|
+
rmSync(dir, { recursive: true, force: true })
|
|
102
|
+
}
|
|
103
|
+
})
|
|
104
|
+
|
|
58
105
|
it('clamps out-of-range and non-numeric fields', () => {
|
|
59
106
|
const dir = tempDir()
|
|
60
107
|
try {
|
|
61
108
|
writeFileSync(join(dir, 'pet.json'), JSON.stringify({
|
|
62
109
|
name: ' ',
|
|
110
|
+
names: { bad: ' ' },
|
|
63
111
|
affinity: { points: 9999, lastPetAt: -5, lastFeedAt: 'x', pets: -1, feeds: 1.5, turns: 0 },
|
|
64
112
|
treats: { treats: 150, lastTreatGrantAt: -1, turnsAtLastTreatGrant: 0 },
|
|
65
113
|
display: { visible: 'yes', size: -10, right: 1e12, bottom: 20 },
|
|
66
114
|
}), 'utf8')
|
|
67
115
|
const loaded = loadPetPersist(dir)
|
|
68
|
-
expect(loaded.
|
|
116
|
+
expect(loaded.petId).toBe(DEFAULT_PET_ID)
|
|
117
|
+
expect(loaded.names).toEqual({})
|
|
69
118
|
expect(loaded.affinity.points).toBe(AFFINITY_MAX)
|
|
70
119
|
expect(loaded.affinity.lastPetAt).toBe(0)
|
|
71
120
|
expect(loaded.affinity.lastFeedAt).toBe(0)
|
package/src/persist.ts
CHANGED
|
@@ -7,7 +7,7 @@
|
|
|
7
7
|
|
|
8
8
|
import { mkdirSync, readFileSync, renameSync, writeFileSync } from 'node:fs'
|
|
9
9
|
import { join } from 'node:path'
|
|
10
|
-
import {
|
|
10
|
+
import { dshHome } from './dsh-home.ts'
|
|
11
11
|
import { AFFINITY_MAX, emptyAffinity, type AffinityState } from './affinity.ts'
|
|
12
12
|
import { defaultTreatConfig, emptyTreatLedger, type TreatLedger } from './treats.ts'
|
|
13
13
|
|
|
@@ -37,15 +37,23 @@ export const DISPLAY_INSET_MAX = 10_000
|
|
|
37
37
|
|
|
38
38
|
/** Everything persisted for the pet. */
|
|
39
39
|
export interface PetPersist {
|
|
40
|
-
/**
|
|
41
|
-
|
|
40
|
+
/** Selected pet id (a registry entry; clamped at service startup). */
|
|
41
|
+
petId: string
|
|
42
|
+
/**
|
|
43
|
+
* Per-pet display names keyed by pet id. A pet without an entry falls back
|
|
44
|
+
* to its manifest displayName, so only user renames are stored here.
|
|
45
|
+
*/
|
|
46
|
+
names: Record<string, string>
|
|
42
47
|
affinity: AffinityState
|
|
43
48
|
/** Treat (小鱼干) stock ledger. */
|
|
44
49
|
treats: TreatLedger
|
|
45
50
|
display: PetDisplayConfig
|
|
46
51
|
}
|
|
47
52
|
|
|
48
|
-
/**
|
|
53
|
+
/** Pet id the legacy single-pet installs resolve to on migration. */
|
|
54
|
+
export const DEFAULT_PET_ID = 'whale-girl'
|
|
55
|
+
|
|
56
|
+
/** Default pet name (used only when a manifest carries no displayName). */
|
|
49
57
|
export const DEFAULT_PET_NAME = '鲸鱼娘'
|
|
50
58
|
|
|
51
59
|
/** Name constraints. */
|
|
@@ -53,16 +61,21 @@ export const PET_NAME_MAX_LENGTH = 20
|
|
|
53
61
|
|
|
54
62
|
export function emptyPersist(): PetPersist {
|
|
55
63
|
return {
|
|
56
|
-
|
|
64
|
+
petId: DEFAULT_PET_ID,
|
|
65
|
+
names: {},
|
|
57
66
|
affinity: emptyAffinity(),
|
|
58
67
|
treats: emptyTreatLedger(),
|
|
59
68
|
display: { ...defaultDisplayConfig },
|
|
60
69
|
}
|
|
61
70
|
}
|
|
62
71
|
|
|
63
|
-
/**
|
|
72
|
+
/**
|
|
73
|
+
* Resolve the persistence directory ($DSH_HOME or ~/.dsh). Delegates to the
|
|
74
|
+
* shared {@link dshHome} resolution so the plugin family keeps one DSH_HOME
|
|
75
|
+
* definition (env override, ~ expansion, cwd-joined relative values).
|
|
76
|
+
*/
|
|
64
77
|
export function petHomeDir(): string {
|
|
65
|
-
return
|
|
78
|
+
return dshHome()
|
|
66
79
|
}
|
|
67
80
|
|
|
68
81
|
/** Numeric field guard: finite numbers only, else the fallback. */
|
|
@@ -70,6 +83,22 @@ function finiteNum(value: unknown, fallback: number): number {
|
|
|
70
83
|
return typeof value === 'number' && Number.isFinite(value) ? value : fallback
|
|
71
84
|
}
|
|
72
85
|
|
|
86
|
+
/** A persisted document from any schema era (legacy carried a flat `name`). */
|
|
87
|
+
type PetPersistDocument = Partial<PetPersist> & { name?: unknown }
|
|
88
|
+
|
|
89
|
+
/** Sanitize the per-pet names map (string keys, non-empty trimmed values). */
|
|
90
|
+
function loadPetNames(parsed: PetPersistDocument): Record<string, string> {
|
|
91
|
+
const names: Record<string, string> = {}
|
|
92
|
+
if (typeof parsed.names !== 'object' || parsed.names === null) return names
|
|
93
|
+
for (const [id, value] of Object.entries(parsed.names as Record<string, unknown>)) {
|
|
94
|
+
if (id === '' || typeof value !== 'string') continue
|
|
95
|
+
const name = value.trim()
|
|
96
|
+
if (name === '') continue
|
|
97
|
+
names[id] = name.slice(0, PET_NAME_MAX_LENGTH)
|
|
98
|
+
}
|
|
99
|
+
return names
|
|
100
|
+
}
|
|
101
|
+
|
|
73
102
|
/** Clamp one count/score into [0, max]. */
|
|
74
103
|
function clamp(value: number, max: number): number {
|
|
75
104
|
return Math.min(max, Math.max(0, value))
|
|
@@ -79,7 +108,7 @@ function clamp(value: number, max: number): number {
|
|
|
79
108
|
export function loadPetPersist(dir: string = petHomeDir()): PetPersist {
|
|
80
109
|
try {
|
|
81
110
|
const raw = readFileSync(join(dir, 'pet.json'), 'utf8')
|
|
82
|
-
const parsed = JSON.parse(raw) as
|
|
111
|
+
const parsed = JSON.parse(raw) as PetPersistDocument
|
|
83
112
|
const base = emptyPersist()
|
|
84
113
|
const rawAffinity = (parsed.affinity ?? {}) as Partial<AffinityState>
|
|
85
114
|
const affinity: AffinityState = {
|
|
@@ -105,10 +134,19 @@ export function loadPetPersist(dir: string = petHomeDir()): PetPersist {
|
|
|
105
134
|
right: Math.round(clamp(finiteNum(rawDisplay.right, base.display.right), DISPLAY_INSET_MAX)),
|
|
106
135
|
bottom: Math.round(clamp(finiteNum(rawDisplay.bottom, base.display.bottom), DISPLAY_INSET_MAX)),
|
|
107
136
|
}
|
|
137
|
+
const petId = typeof parsed.petId === 'string' && parsed.petId.trim() !== ''
|
|
138
|
+
? parsed.petId.trim()
|
|
139
|
+
: base.petId
|
|
140
|
+
const names = loadPetNames(parsed)
|
|
141
|
+
// Legacy migration: pre-registry installs persisted one flat `name`
|
|
142
|
+
// field. Move it onto the selected pet (the legacy whale-girl unless the
|
|
143
|
+
// file already names another pet) so renames survive the upgrade.
|
|
144
|
+
if (typeof parsed.name === 'string' && parsed.name.trim() !== '' && names[petId] === undefined) {
|
|
145
|
+
names[petId] = parsed.name.trim().slice(0, PET_NAME_MAX_LENGTH)
|
|
146
|
+
}
|
|
108
147
|
return {
|
|
109
|
-
|
|
110
|
-
|
|
111
|
-
: base.name,
|
|
148
|
+
petId,
|
|
149
|
+
names,
|
|
112
150
|
affinity,
|
|
113
151
|
treats,
|
|
114
152
|
display,
|
|
@@ -0,0 +1,132 @@
|
|
|
1
|
+
import { describe, expect, it } from 'vitest'
|
|
2
|
+
import { mkdirSync, mkdtempSync, rmSync, writeFileSync } from 'node:fs'
|
|
3
|
+
import { tmpdir } from 'node:os'
|
|
4
|
+
import { join } from 'node:path'
|
|
5
|
+
import {
|
|
6
|
+
DEFAULT_FRAME_COUNTS,
|
|
7
|
+
DEFAULT_PET_CELL,
|
|
8
|
+
codexPetsDir,
|
|
9
|
+
loadPetRegistry,
|
|
10
|
+
resolvePetManifest,
|
|
11
|
+
} from './registry.ts'
|
|
12
|
+
|
|
13
|
+
function tempDir(): string {
|
|
14
|
+
return mkdtempSync(join(tmpdir(), 'dsh-pet-registry-'))
|
|
15
|
+
}
|
|
16
|
+
|
|
17
|
+
describe('resolvePetManifest', () => {
|
|
18
|
+
it('resolves a bare Codex manifest onto the hatch-pet contract defaults', () => {
|
|
19
|
+
const entry = resolvePetManifest({
|
|
20
|
+
id: 'otter',
|
|
21
|
+
displayName: '水獭',
|
|
22
|
+
spritesheetPath: 'spritesheet.webp',
|
|
23
|
+
}, join(tmpdir(), 'otter'))
|
|
24
|
+
expect(entry).toBeDefined()
|
|
25
|
+
expect(entry!.id).toBe('otter')
|
|
26
|
+
expect(entry!.cell).toEqual(DEFAULT_PET_CELL)
|
|
27
|
+
expect(entry!.columns).toBe(8)
|
|
28
|
+
expect(entry!.rows).toEqual([...DEFAULT_FRAME_COUNTS])
|
|
29
|
+
expect(entry!.atlasUrl).toBe('/pet/otter/spritesheet.webp')
|
|
30
|
+
expect(entry!.manifestUrl).toBe('/pet/otter/pet.json')
|
|
31
|
+
// Every track's frames/durations line up with its row count.
|
|
32
|
+
expect(entry!.tracks.idle.frames.length).toBe(entry!.rows[0])
|
|
33
|
+
expect(entry!.tracks.idle.durations.length).toBe(entry!.rows[0])
|
|
34
|
+
expect(entry!.tracks.jumping.loop).toBe(false)
|
|
35
|
+
expect(entry!.tracks.jumping.fallback).toBe('idle')
|
|
36
|
+
expect(entry!.tracks.failed.loop).toBe(false)
|
|
37
|
+
expect(entry!.tracks.running.loop).toBe(true)
|
|
38
|
+
})
|
|
39
|
+
|
|
40
|
+
it('keeps the legacy whale-girl frame counts and its own durations', () => {
|
|
41
|
+
const entry = resolvePetManifest({
|
|
42
|
+
id: 'whale-girl',
|
|
43
|
+
displayName: '鲸鱼娘',
|
|
44
|
+
spritesheetPath: 'spritesheet.webp',
|
|
45
|
+
frames: [6, 8, 8, 4, 5, 8, 6, 6, 6],
|
|
46
|
+
tracks: { idle: { durations: [400, 400, 500, 400, 400, 500] } },
|
|
47
|
+
}, join(tmpdir(), 'whale'))
|
|
48
|
+
expect(entry!.rows).toEqual([6, 8, 8, 4, 5, 8, 6, 6, 6])
|
|
49
|
+
expect(entry!.tracks.idle.durations).toEqual([400, 400, 500, 400, 400, 500])
|
|
50
|
+
// Non-overridden tracks keep the contract rhythm.
|
|
51
|
+
expect(entry!.tracks['running-right'].durations.length).toBe(8)
|
|
52
|
+
})
|
|
53
|
+
|
|
54
|
+
it('cycles short override durations up to the row frame count', () => {
|
|
55
|
+
const entry = resolvePetManifest({
|
|
56
|
+
id: 'fox',
|
|
57
|
+
displayName: '狐狸',
|
|
58
|
+
spritesheetPath: 'atlas.png',
|
|
59
|
+
frames: [4, 4, 4, 4, 4, 4, 4, 4, 4],
|
|
60
|
+
tracks: { idle: { durations: [200, 300] } },
|
|
61
|
+
}, join(tmpdir(), 'fox'))
|
|
62
|
+
expect(entry!.tracks.idle.durations).toEqual([200, 300, 200, 300])
|
|
63
|
+
expect(entry!.tracks.idle.frames).toEqual([0, 1, 2, 3])
|
|
64
|
+
})
|
|
65
|
+
|
|
66
|
+
it('rejects unsafe ids and spritesheet paths with warnings', () => {
|
|
67
|
+
const warnings: string[] = []
|
|
68
|
+
expect(resolvePetManifest({ id: 'Bad Id', displayName: 'x', spritesheetPath: 'a.webp' }, '/tmp', { warnings })).toBeUndefined()
|
|
69
|
+
expect(resolvePetManifest({ id: 'ok', displayName: 'x', spritesheetPath: '../etc/passwd' }, '/tmp', { warnings })).toBeUndefined()
|
|
70
|
+
expect(resolvePetManifest({ id: 'ok', displayName: 'x', spritesheetPath: '/absolute.webp' }, '/tmp', { warnings })).toBeUndefined()
|
|
71
|
+
expect(warnings.length).toBe(3)
|
|
72
|
+
})
|
|
73
|
+
})
|
|
74
|
+
|
|
75
|
+
describe('loadPetRegistry', () => {
|
|
76
|
+
it('scans built-in assets, the custom pets dir, and composed extras with precedence', () => {
|
|
77
|
+
const root = tempDir()
|
|
78
|
+
try {
|
|
79
|
+
const assets = join(root, 'assets')
|
|
80
|
+
mkdirSync(join(assets, 'whale'), { recursive: true })
|
|
81
|
+
writeFileSync(join(assets, 'whale', 'pet.json'), JSON.stringify({
|
|
82
|
+
id: 'whale-girl', displayName: '鲸鱼娘', spritesheetPath: 'spritesheet.webp',
|
|
83
|
+
}), 'utf8')
|
|
84
|
+
const petsDir = join(root, 'pets')
|
|
85
|
+
mkdirSync(join(petsDir, 'otter'), { recursive: true })
|
|
86
|
+
writeFileSync(join(petsDir, 'otter', 'pet.json'), JSON.stringify({
|
|
87
|
+
id: 'otter', displayName: '水獭', spritesheetPath: 'spritesheet.webp',
|
|
88
|
+
}), 'utf8')
|
|
89
|
+
// A broken manifest is skipped with a warning, never thrown.
|
|
90
|
+
mkdirSync(join(petsDir, 'broken'), { recursive: true })
|
|
91
|
+
writeFileSync(join(petsDir, 'broken', 'pet.json'), '{ not json', 'utf8')
|
|
92
|
+
|
|
93
|
+
const registry = loadPetRegistry({ packageRoot: root, petsDir })
|
|
94
|
+
expect(registry.entries.map(entry => entry.id)).toEqual(['whale-girl', 'otter'])
|
|
95
|
+
expect(registry.defaultEntry().id).toBe('whale-girl')
|
|
96
|
+
expect(registry.warnings.some(warning => warning.includes('broken'))).toBe(true)
|
|
97
|
+
|
|
98
|
+
// A composed extra with the same id overrides the earlier sources.
|
|
99
|
+
const overridden = loadPetRegistry({
|
|
100
|
+
packageRoot: root,
|
|
101
|
+
petsDir,
|
|
102
|
+
extra: [{ id: 'whale-girl', displayName: '替换鲸', spritesheetPath: 'spritesheet.webp' }],
|
|
103
|
+
})
|
|
104
|
+
expect(overridden.byId('whale-girl')!.displayName).toBe('替换鲸')
|
|
105
|
+
} finally {
|
|
106
|
+
rmSync(root, { recursive: true, force: true })
|
|
107
|
+
}
|
|
108
|
+
})
|
|
109
|
+
|
|
110
|
+
it('defaults to the built-in pet even when custom pets sort first', () => {
|
|
111
|
+
const root = tempDir()
|
|
112
|
+
try {
|
|
113
|
+
const petsDir = join(root, 'pets')
|
|
114
|
+
mkdirSync(join(petsDir, 'aardvark'), { recursive: true })
|
|
115
|
+
writeFileSync(join(petsDir, 'aardvark', 'pet.json'), JSON.stringify({
|
|
116
|
+
id: 'aardvark', displayName: '土豚', spritesheetPath: 'spritesheet.webp',
|
|
117
|
+
}), 'utf8')
|
|
118
|
+
const registry = loadPetRegistry({ packageRoot: join(root, 'no-assets'), petsDir })
|
|
119
|
+
expect(registry.defaultEntry().id).toBe('aardvark')
|
|
120
|
+
} finally {
|
|
121
|
+
rmSync(root, { recursive: true, force: true })
|
|
122
|
+
}
|
|
123
|
+
})
|
|
124
|
+
})
|
|
125
|
+
|
|
126
|
+
describe('codexPetsDir', () => {
|
|
127
|
+
it('honors CODEX_HOME and expands a leading tilde', () => {
|
|
128
|
+
expect(codexPetsDir({ CODEX_HOME: '/opt/codex' }, '/home/user')).toBe('/opt/codex/pets')
|
|
129
|
+
expect(codexPetsDir({ CODEX_HOME: '~/codex' }, '/home/user')).toBe('/home/user/codex/pets')
|
|
130
|
+
expect(codexPetsDir({}, '/home/user')).toBe('/home/user/.codex/pets')
|
|
131
|
+
})
|
|
132
|
+
})
|