Fix Story 1.3: StateStore spec compliance and minor cleanup

Critical Fix:
- StateStore now uses global Hooks.callAll directly (per spec)
- Removed hooks parameter from StateStore constructor
- Updated module.js to pass only adapter.settings
- Updated tests to stub globalThis.Hooks

Minor Cleanup:
- Fixed misleading warning in SocketHandler.registerPendingOp
- Added clarifying comment for setMatrix _revision behavior

Generated by Mistral Vibe.
Co-Authored-By: Mistral Vibe <vibe@mistral.ai>
This commit is contained in:
2026-05-22 11:38:45 +02:00
parent 110b295a7b
commit 5ba7717ecd
17 changed files with 2391 additions and 55 deletions
+94
View File
@@ -0,0 +1,94 @@
/**
* tests/fixtures/foundry-adapter.js
*
* GAME_STUB — frozen minimal game object for FoundryAdapter constructor tests.
*
* Mirrors the six game sub-objects that FoundryAdapter surfaces delegate to:
* game.settings, game.socket, game.users, game.user, game.scenes
*
* `Hooks` is a standalone global — not part of game — and is stubbed
* via `vi.stubGlobal('Hooks', HOOKS_STUB)` in individual test files.
* `ui.notifications` is similarly a standalone global.
*/
import { vi } from 'vitest';
/** Minimal game.settings stub. All methods are vi.fn(). */
const SETTINGS_STUB = {
register: vi.fn(),
get: vi.fn().mockReturnValue(null),
set: vi.fn().mockResolvedValue(undefined),
};
/** Minimal game.socket stub. */
const SOCKET_STUB = {
emit: vi.fn(),
on: vi.fn(),
off: vi.fn(),
};
/** A representative user object. */
const GM_USER = Object.freeze({ id: 'gm-user-1', name: 'GM', isGM: true });
const PLAYER_USER = Object.freeze({ id: 'player-user-1', name: 'Player', isGM: false });
/** Minimal game.users map-like stub. */
const USERS_STUB = {
get: vi.fn((id) => {
if (id === GM_USER.id) return GM_USER;
if (id === PLAYER_USER.id) return PLAYER_USER;
return null;
}),
[Symbol.iterator]: vi.fn(function* () {
yield GM_USER;
yield PLAYER_USER;
}),
};
/** Minimal game.user (current user) stub. */
const USER_STUB = Object.freeze({ id: GM_USER.id, name: GM_USER.name, isGM: true });
/** A representative scene object. */
const ACTIVE_SCENE = Object.freeze({ id: 'scene-1', name: 'Test Scene' });
/** Minimal game.scenes stub. */
const SCENES_STUB = {
active: ACTIVE_SCENE,
get: vi.fn((id) => (id === ACTIVE_SCENE.id ? ACTIVE_SCENE : null)),
};
/**
* Frozen minimal game object for FoundryAdapter constructor tests.
* Reset individual stubs between tests with `vi.clearAllMocks()` or per-spy.
*/
export const GAME_STUB = Object.freeze({
settings: SETTINGS_STUB,
socket: SOCKET_STUB,
users: USERS_STUB,
user: USER_STUB,
scenes: SCENES_STUB,
});
/** Exported stubs for targeted assertions in tests. */
export { SETTINGS_STUB, SOCKET_STUB, USERS_STUB, USER_STUB, SCENES_STUB, GM_USER, PLAYER_USER, ACTIVE_SCENE };
/**
* Minimal Hooks stub for vi.stubGlobal('Hooks', HOOKS_STUB).
* Each method is a vi.fn().
*/
export const HOOKS_STUB = {
on: vi.fn(),
once: vi.fn(),
off: vi.fn(),
callAll: vi.fn(),
};
/**
* Minimal ui.notifications stub for vi.stubGlobal('ui', UI_STUB).
*/
export const UI_STUB = Object.freeze({
notifications: {
info: vi.fn(),
warn: vi.fn(),
error: vi.fn(),
},
});
+47
View File
@@ -15,6 +15,7 @@ export const SOCKET_PAYLOADS = Object.freeze({
opId: "op-001",
userId: "user-abc",
targetState: "hidden",
baseRevision: 0,
}),
}),
@@ -35,6 +36,7 @@ export const SOCKET_PAYLOADS = Object.freeze({
payload: Object.freeze({
userId: "user-abc",
targetState: "hidden",
baseRevision: 0,
// opId intentionally omitted
}),
}),
@@ -46,6 +48,7 @@ export const SOCKET_PAYLOADS = Object.freeze({
opId: "op-002",
userId: "user-abc",
targetState: "invisible", // not a valid VisibilityState
baseRevision: 0,
}),
}),
@@ -56,6 +59,7 @@ export const SOCKET_PAYLOADS = Object.freeze({
opId: "op-003",
userId: "user-abc",
targetState: "hidden",
baseRevision: 0,
extraField: "should-not-be-here",
}),
}),
@@ -76,4 +80,47 @@ export const SOCKET_PAYLOADS = Object.freeze({
// revision intentionally omitted
}),
}),
// ── Stale ACK — opId not in pendingOps ───────────────────────────────────
staleEcho: Object.freeze({
event: "scrying-pool.visibility.updated",
payload: Object.freeze({
opId: "op-stale-999", // not registered in any pendingOps map
userId: "user-abc",
state: "hidden",
revision: 5,
}),
}),
// ── Timeout/retry sequence: intent that will time out ────────────────────
timeoutIntent: Object.freeze({
event: "scrying-pool.visibility.set",
payload: Object.freeze({
opId: "op-timeout-001",
userId: "user-abc",
targetState: "hidden",
baseRevision: 0,
}),
}),
// ── Hydration payload (persisted world setting shape) ────────────────────
hydrationPayload: Object.freeze({
_version: 1,
matrix: Object.freeze({
"user-001": "active",
"user-002": "hidden",
}),
}),
// ── Malformed: missing baseRevision in intent ─────────────────────────────
missingBaseRevision: Object.freeze({
event: "scrying-pool.visibility.set",
payload: Object.freeze({
opId: "op-006",
userId: "user-abc",
targetState: "hidden",
// baseRevision intentionally omitted
}),
}),
});