Skip to content

Commit a047053

Browse files
r1tsuuclaude
andcommitted
fix: decouple integration tests from production default inventory
Add optional createInventory factory to PlayerSystem and AuthoritativeWorld so tests can supply a stable fixed layout. Create tests/helpers/test-inventory.ts with a frozen layout (cobblestone at hotbar[8], glass at main[0]) used by authoritative-world and client-server tests. Update inventory.test.ts to reflect the current production default (glass at hotbar[8], main fully empty). Fix sky light propagation to treat non-full-occlusion blocks as light-transparent. Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
1 parent 5fc0319 commit a047053

9 files changed

Lines changed: 68 additions & 30 deletions

File tree

packages/core/src/server/authoritative-world.ts

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -127,12 +127,14 @@ export class AuthoritativeWorld {
127127
public constructor(
128128
private world: WorldSummary,
129129
private readonly storage: WorldStorage,
130+
options?: { createInventory?: () => InventorySnapshot },
130131
) {
131132
this.playerSystem = new PlayerSystem(
132133
this.world.name,
133134
this.storage,
134135
this.spawnPosition,
135136
this.entityState,
137+
options?.createInventory,
136138
)
137139
this.droppedItemSystem = new DroppedItemSystem(
138140
this.world.name,

packages/core/src/server/lighting-system.ts

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -9,7 +9,7 @@ import {
99
normalizeWorldTimeState,
1010
type WorldTimeState,
1111
} from '../shared/lighting.ts'
12-
import { BLOCK_IDS, getBlockEmittedLightLevel } from '../world/blocks.ts'
12+
import { Blocks, BLOCK_IDS, getBlockEmittedLightLevel } from '../world/blocks.ts'
1313
import { CHUNK_HEIGHT, CHUNK_SIZE, WORLD_MAX_BLOCK_Y } from '../world/constants.ts'
1414

1515
const LIGHT_DIRECTIONS = [
@@ -34,7 +34,7 @@ interface ChunkLightBuffers {
3434

3535
const chunkKey = ({ x, z }: ChunkCoord): string => `${x},${z}`
3636

37-
const isLightPassable = (blockId: BlockId): boolean => blockId === BLOCK_IDS.air
37+
const isLightPassable = (blockId: BlockId): boolean => Blocks[blockId].occlusion !== 'full'
3838

3939
const localIndex = (localX: number, localY: number, localZ: number): number =>
4040
localX + CHUNK_SIZE * (localZ + CHUNK_SIZE * localY)

packages/core/src/server/player-system.ts

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -62,6 +62,7 @@ export class PlayerSystem {
6262
private readonly storage: WorldStorage,
6363
private readonly spawnPosition: readonly [number, number, number],
6464
private readonly entities: WorldEntityState,
65+
private readonly createInventory: () => InventorySnapshot = createStarterInventory,
6566
) {}
6667

6768
public getPlayerName(entityId: EntityId): PlayerName | null {
@@ -416,7 +417,7 @@ export class PlayerSystem {
416417
flying: false,
417418
})
418419
this.entities.playerInventory.set(entityId, {
419-
inventory: createStarterInventory(),
420+
inventory: this.createInventory(),
420421
})
421422
this.entities.playerSession.set(entityId, { active: false })
422423
this.entities.playerPersistence.set(entityId, {

packages/core/src/world/content-spec.ts

Lines changed: 0 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -528,11 +528,6 @@ export const DEFAULT_STARTER_INVENTORY_STACK_SPECS = [
528528
},
529529
{
530530
slot: 8,
531-
itemKey: 'cobblestone',
532-
count: 64,
533-
},
534-
{
535-
slot: 9,
536531
itemKey: 'glass',
537532
count: 64,
538533
},

packages/core/src/world/generated/content-registry.ts

Lines changed: 1 addition & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -499,6 +499,5 @@ export const STARTER_INVENTORY_STACKS = [
499499
{ slot: 4, itemId: ITEM_IDS.log, count: 64 },
500500
{ slot: 5, itemId: ITEM_IDS.leaves, count: 64 },
501501
{ slot: 7, itemId: ITEM_IDS.planks, count: 64 },
502-
{ slot: 8, itemId: ITEM_IDS.cobblestone, count: 64 },
503-
{ slot: 9, itemId: ITEM_IDS.glass, count: 64 },
502+
{ slot: 8, itemId: ITEM_IDS.glass, count: 64 },
504503
] as const satisfies readonly StarterInventoryStackDefinition[];

tests/authoritative-world.test.ts

Lines changed: 10 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -7,6 +7,7 @@ import type { BlockId } from '../packages/core/src/types.ts'
77

88
import { AuthoritativeWorld } from '../packages/core/src/server/authoritative-world.ts'
99
import { BinaryWorldStorage } from '../packages/core/src/server/world-storage.ts'
10+
import { createTestStarterInventory } from './helpers/test-inventory.ts'
1011
import { BLOCK_IDS, getDroppedItemIdForBlock } from '../packages/core/src/world/blocks.ts'
1112
import { Chunk } from '../packages/core/src/world/chunk.ts'
1213
import { CHUNK_SIZE, WORLD_SEA_LEVEL } from '../packages/core/src/world/constants.ts'
@@ -32,7 +33,7 @@ test('authoritative world keeps per-player state separate and persists it by pla
3233

3334
try {
3435
const worldRecord = await storage.createWorld('Alpha', 42)
35-
const world = new AuthoritativeWorld(worldRecord, storage)
36+
const world = new AuthoritativeWorld(worldRecord, storage, { createInventory: createTestStarterInventory })
3637

3738
const joinedA = await world.joinPlayer(PLAYER_A)
3839
expect(joinedA.clientPlayer.name).toBe(PLAYER_A)
@@ -77,7 +78,7 @@ test('authoritative world keeps per-player state separate and persists it by pla
7778

7879
const reloadedRecord = await storage.getWorld('Alpha')
7980
expect(reloadedRecord).not.toBeNull()
80-
const reloadedWorld = new AuthoritativeWorld(reloadedRecord!, storage)
81+
const reloadedWorld = new AuthoritativeWorld(reloadedRecord!, storage, { createInventory: createTestStarterInventory })
8182

8283
const rejoinedA = await reloadedWorld.joinPlayer(PLAYER_A)
8384
expect(rejoinedA.clientPlayer.entityId).toBe(joinedA.clientPlayer.entityId)
@@ -107,7 +108,7 @@ test('authoritative world spawns and persists dropped items until players pick t
107108

108109
try {
109110
const worldRecord = await storage.createWorld('Drops', 42)
110-
const world = new AuthoritativeWorld(worldRecord, storage)
111+
const world = new AuthoritativeWorld(worldRecord, storage, { createInventory: createTestStarterInventory })
111112
const joined = await world.joinPlayer(PLAYER_A)
112113

113114
const targetX = 1
@@ -136,7 +137,7 @@ test('authoritative world spawns and persists dropped items until players pick t
136137

137138
const reloadedRecord = await storage.getWorld('Drops')
138139
expect(reloadedRecord).not.toBeNull()
139-
const reloadedWorld = new AuthoritativeWorld(reloadedRecord!, storage)
140+
const reloadedWorld = new AuthoritativeWorld(reloadedRecord!, storage, { createInventory: createTestStarterInventory })
140141
const rejoined = await reloadedWorld.joinPlayer(PLAYER_A)
141142
expect(rejoined.droppedItems).toHaveLength(1)
142143
expect(rejoined.droppedItems[0]?.itemId).toBe(droppedItemId!)
@@ -151,7 +152,7 @@ test('authoritative world pregenerates and persists the startup chunk set', asyn
151152

152153
try {
153154
const worldRecord = await storage.createWorld('Startup', 42)
154-
const world = new AuthoritativeWorld(worldRecord, storage)
155+
const world = new AuthoritativeWorld(worldRecord, storage, { createInventory: createTestStarterInventory })
155156
const expectedCoords = world.getStartupChunkCoords()
156157
const progress: Array<{ completedChunks: number; totalChunks: number }> = []
157158

@@ -195,7 +196,7 @@ test('survival cannot break bedrock but creative can', async () => {
195196

196197
try {
197198
const worldRecord = await storage.createWorld('Bedrock', 42)
198-
const world = new AuthoritativeWorld(worldRecord, storage)
199+
const world = new AuthoritativeWorld(worldRecord, storage, { createInventory: createTestStarterInventory })
199200
const joined = await world.joinPlayer(PLAYER_A)
200201
const targetIndex = 1 + 1 * CHUNK_SIZE + 0 * CHUNK_SIZE * CHUNK_SIZE
201202

@@ -224,7 +225,7 @@ test('creative block mutations neither spawn drops nor consume held items', asyn
224225

225226
try {
226227
const worldRecord = await storage.createWorld('CreativeItems', 42)
227-
const world = new AuthoritativeWorld(worldRecord, storage)
228+
const world = new AuthoritativeWorld(worldRecord, storage, { createInventory: createTestStarterInventory })
228229
const joined = await world.joinPlayer(PLAYER_A)
229230
await world.setPlayerGamemode(joined.clientPlayer.entityId, 1)
230231

@@ -276,7 +277,7 @@ test('placing a solid block into water replaces the water cell', async () => {
276277

277278
try {
278279
const worldRecord = await storage.createWorld('WaterReplace', 42)
279-
const world = new AuthoritativeWorld(worldRecord, storage)
280+
const world = new AuthoritativeWorld(worldRecord, storage, { createInventory: createTestStarterInventory })
280281
const joined = await world.joinPlayer(PLAYER_A)
281282

282283
let targetX = 0
@@ -341,7 +342,7 @@ test('block mutations relight only the nearby loaded chunk neighborhood', async
341342

342343
try {
343344
const worldRecord = await storage.createWorld('LocalRelight', 42)
344-
const world = new AuthoritativeWorld(worldRecord, storage)
345+
const world = new AuthoritativeWorld(worldRecord, storage, { createInventory: createTestStarterInventory })
345346
const joined = await world.joinPlayer(PLAYER_A)
346347
const targetY = WORLD_SEA_LEVEL + 8
347348
const targetChunk = worldToChunkCoord(1, targetY, 1).chunk

tests/client-server.test.ts

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -12,6 +12,7 @@ import type {
1212
import { PortClientAdapter } from '../apps/client/src/app/client-adapter.ts'
1313
import { ClientWorldRuntime } from '../apps/client/src/app/world-runtime.ts'
1414
import { AuthoritativeWorld } from '../packages/core/src/server/authoritative-world.ts'
15+
import { createTestStarterInventory } from './helpers/test-inventory.ts'
1516
import { ServerRuntime } from '../packages/core/src/server/runtime.ts'
1617
import { PortServerAdapter } from '../packages/core/src/server/server-adapter.ts'
1718
import { BinaryWorldStorage } from '../packages/core/src/server/world-storage.ts'
@@ -60,7 +61,7 @@ const createHarness = async (): Promise<{
6061
const storage = new BinaryWorldStorage(rootDir)
6162
const worldRecord = await storage.createWorld('Alpha', 42)
6263
let nowMs = 0
63-
const serverRuntime = new ServerRuntime(server, new AuthoritativeWorld(worldRecord, storage), {
64+
const serverRuntime = new ServerRuntime(server, new AuthoritativeWorld(worldRecord, storage, { createInventory: createTestStarterInventory }), {
6465
tickIntervalMs: 10,
6566
maxCatchUpTicks: 100,
6667
autoSaveIntervalTicks: 10,

tests/helpers/test-inventory.ts

Lines changed: 46 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,46 @@
1+
import type { InventorySnapshot } from '../../packages/core/src/types.ts'
2+
3+
import {
4+
createEmptyInventory,
5+
DEFAULT_INVENTORY_STACK_SIZE,
6+
HOTBAR_SLOT_COUNT,
7+
} from '../../packages/core/src/world/inventory.ts'
8+
import { ITEM_IDS } from '../../packages/core/src/world/items.ts'
9+
10+
/**
11+
* A stable, test-only starter inventory that does not change with the
12+
* production default. Integration tests reference specific slots and items
13+
* from this layout so that changes to DEFAULT_STARTER_INVENTORY_STACK_SPECS
14+
* do not break them.
15+
*
16+
* Layout:
17+
* Hotbar [0-8]: grass, glowstone, dirt, stone, log, leaves, <empty>, planks, cobblestone
18+
* Main [0] : glass (slot index 9)
19+
*/
20+
export const createTestStarterInventory = (): InventorySnapshot => {
21+
const inventory = createEmptyInventory()
22+
const slots = [...inventory.slots]
23+
24+
const hotbar: Array<keyof typeof ITEM_IDS> = [
25+
'grass',
26+
'glowstone',
27+
'dirt',
28+
'stone',
29+
'log',
30+
'leaves',
31+
'empty', // slot 6 intentionally empty
32+
'planks',
33+
'cobblestone',
34+
]
35+
36+
for (let i = 0; i < hotbar.length; i++) {
37+
const key = hotbar[i]!
38+
if (key === 'empty') continue
39+
slots[i] = { itemId: ITEM_IDS[key], count: DEFAULT_INVENTORY_STACK_SIZE }
40+
}
41+
42+
// main[0] = glass
43+
slots[HOTBAR_SLOT_COUNT] = { itemId: ITEM_IDS.glass, count: DEFAULT_INVENTORY_STACK_SIZE }
44+
45+
return { ...inventory, slots }
46+
}

tests/inventory.test.ts

Lines changed: 3 additions & 10 deletions
Original file line numberDiff line numberDiff line change
@@ -32,16 +32,10 @@ test('starter inventory creates a full hotbar plus a starter brick stack', () =>
3232
{ itemId: ITEM_IDS.leaves, count: DEFAULT_INVENTORY_STACK_SIZE },
3333
{ itemId: ITEM_IDS.empty, count: 0 },
3434
{ itemId: ITEM_IDS.planks, count: DEFAULT_INVENTORY_STACK_SIZE },
35-
{ itemId: ITEM_IDS.cobblestone, count: DEFAULT_INVENTORY_STACK_SIZE },
35+
{ itemId: ITEM_IDS.glass, count: DEFAULT_INVENTORY_STACK_SIZE },
3636
])
3737
expect(main).toHaveLength(MAIN_INVENTORY_SLOT_COUNT)
38-
expect(main[0]).toEqual({
39-
itemId: ITEM_IDS.glass,
40-
count: DEFAULT_INVENTORY_STACK_SIZE,
41-
})
42-
expect(main.slice(1).every((slot) => slot.itemId === ITEM_IDS.empty && slot.count === 0)).toBe(
43-
true,
44-
)
38+
expect(main.every((slot) => slot.itemId === ITEM_IDS.empty && slot.count === 0)).toBe(true)
4539
expect(inventory.selectedSlot).toBe(0)
4640
expect(inventory.cursor).toBeNull()
4741
})
@@ -102,8 +96,7 @@ test('adding items fills empty main-inventory stacks when hotbar stacks are full
10296

10397
expect(result.added).toBe(5)
10498
expect(result.remaining).toBe(0)
105-
expect(main[0]).toEqual({ itemId: ITEM_IDS.glass, count: 64 })
106-
expect(main[1]).toEqual({ itemId: ITEM_IDS.grass, count: 5 })
99+
expect(main[0]).toEqual({ itemId: ITEM_IDS.grass, count: 5 })
107100
})
108101

109102
test('inventory interaction picks up, places, and merges stacks', () => {

0 commit comments

Comments
 (0)