run-dmcp 0.1.0 → 0.2.0
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.md +76 -10
- package/dist/bin/run-dmcp.d.ts +2 -0
- package/dist/bin/run-dmcp.js +55 -0
- package/dist/db/connection.d.ts +32 -0
- package/dist/db/connection.js +38 -16
- package/dist/db/schema.d.ts +29 -1
- package/dist/db/schema.js +439 -7
- package/dist/http/server.js +3 -3
- package/dist/index.d.ts +36 -2
- package/dist/index.js +184 -92
- package/dist/mcp-server.d.ts +49 -0
- package/dist/mcp-server.js +127 -0
- package/dist/reader/turnReader.d.ts +185 -0
- package/dist/reader/turnReader.js +288 -0
- package/dist/register/batch.js +5 -79
- package/dist/register/mcp-resources.d.ts +9 -0
- package/dist/register/mcp-resources.js +16 -62
- package/dist/register/render.d.ts +17 -0
- package/dist/register/render.js +50 -0
- package/dist/register/resolve.d.ts +14 -0
- package/dist/register/resolve.js +102 -0
- package/dist/register/resources.js +11 -4
- package/dist/register/timeline.d.ts +2 -0
- package/dist/register/timeline.js +311 -0
- package/dist/rpg/index.d.ts +29 -0
- package/dist/rpg/index.js +55 -0
- package/dist/rpg/register/abilities.d.ts +2 -0
- package/dist/rpg/register/abilities.js +165 -0
- package/dist/rpg/register/batch.d.ts +2 -0
- package/dist/rpg/register/batch.js +92 -0
- package/dist/rpg/register/combat.d.ts +2 -0
- package/dist/rpg/register/combat.js +207 -0
- package/dist/rpg/register/mcp-prompts.d.ts +2 -0
- package/dist/rpg/register/mcp-prompts.js +684 -0
- package/dist/rpg/register/mcp-resources.d.ts +2 -0
- package/dist/rpg/register/mcp-resources.js +61 -0
- package/dist/rpg/register/quests.d.ts +2 -0
- package/dist/rpg/register/quests.js +118 -0
- package/dist/rpg/register/status.d.ts +2 -0
- package/dist/rpg/register/status.js +130 -0
- package/dist/rpg/register/tables.d.ts +2 -0
- package/dist/rpg/register/tables.js +146 -0
- package/dist/rpg/tools/ability.d.ts +48 -0
- package/dist/rpg/tools/ability.js +238 -0
- package/dist/rpg/tools/combat.d.ts +13 -0
- package/dist/rpg/tools/combat.js +195 -0
- package/dist/rpg/tools/dice.d.ts +23 -0
- package/dist/rpg/tools/dice.js +111 -0
- package/dist/rpg/tools/quest.d.ts +34 -0
- package/dist/rpg/tools/quest.js +164 -0
- package/dist/rpg/tools/status.d.ts +36 -0
- package/dist/rpg/tools/status.js +218 -0
- package/dist/rpg/tools/tables.d.ts +33 -0
- package/dist/rpg/tools/tables.js +209 -0
- package/dist/schemas/index.d.ts +12 -12
- package/dist/timeline/adjudication.d.ts +150 -0
- package/dist/timeline/adjudication.js +174 -0
- package/dist/timeline/changes.d.ts +100 -0
- package/dist/timeline/changes.js +161 -0
- package/dist/timeline/checkpoint.d.ts +69 -0
- package/dist/timeline/checkpoint.js +131 -0
- package/dist/timeline/clock.d.ts +89 -0
- package/dist/timeline/clock.js +173 -0
- package/dist/timeline/constrained.d.ts +220 -0
- package/dist/timeline/constrained.js +671 -0
- package/dist/timeline/export.d.ts +171 -0
- package/dist/timeline/export.js +329 -0
- package/dist/timeline/irreversible.d.ts +85 -0
- package/dist/timeline/irreversible.js +108 -0
- package/dist/timeline/kinds.d.ts +14 -0
- package/dist/timeline/kinds.js +22 -0
- package/dist/timeline/narration.d.ts +175 -0
- package/dist/timeline/narration.js +259 -0
- package/dist/timeline/projection.d.ts +97 -0
- package/dist/timeline/projection.js +330 -0
- package/dist/timeline/provenance.d.ts +66 -0
- package/dist/timeline/provenance.js +45 -0
- package/dist/timeline/registry.d.ts +95 -0
- package/dist/timeline/registry.js +124 -0
- package/dist/timeline/render.d.ts +121 -0
- package/dist/timeline/render.js +187 -0
- package/dist/timeline/replay.d.ts +64 -0
- package/dist/timeline/replay.js +104 -0
- package/dist/timeline/resolve.d.ts +262 -0
- package/dist/timeline/resolve.js +226 -0
- package/dist/timeline/schema.d.ts +13 -0
- package/dist/timeline/schema.js +262 -0
- package/dist/timeline/t.d.ts +80 -0
- package/dist/timeline/t.js +37 -0
- package/dist/tools/constraint.d.ts +44 -80
- package/dist/tools/constraint.js +115 -124
- package/dist/tools/relationship.d.ts +83 -2
- package/dist/tools/relationship.js +139 -62
- package/dist/tools/resource.d.ts +31 -6
- package/dist/tools/resource.js +106 -153
- package/dist/types/index.d.ts +19 -1
- package/dist/utils/output-schemas.d.ts +593 -2
- package/dist/utils/output-schemas.js +3 -0
- package/dist/utils/webui.d.ts +32 -0
- package/dist/utils/webui.js +54 -1
- package/package.json +20 -4
- package/dist/__tests__/engineVocabulary.test.d.ts +0 -1
- package/dist/__tests__/engineVocabulary.test.js +0 -147
- package/dist/db/__tests__/connection.test.d.ts +0 -1
- package/dist/db/__tests__/connection.test.js +0 -72
- package/dist/db/__tests__/testDb.d.ts +0 -33
- package/dist/db/__tests__/testDb.js +0 -41
- package/dist/test-setup.d.ts +0 -1
- package/dist/test-setup.js +0 -13
- package/dist/tools/__tests__/audio.test.d.ts +0 -1
- package/dist/tools/__tests__/audio.test.js +0 -59
- package/dist/tools/__tests__/conserved.test.d.ts +0 -1
- package/dist/tools/__tests__/conserved.test.js +0 -488
- package/dist/tools/__tests__/constraint.test.d.ts +0 -1
- package/dist/tools/__tests__/constraint.test.js +0 -212
- package/dist/tools/__tests__/expiry-consequences.test.d.ts +0 -1
- package/dist/tools/__tests__/expiry-consequences.test.js +0 -110
- package/dist/tools/__tests__/images.test.d.ts +0 -1
- package/dist/tools/__tests__/images.test.js +0 -59
- package/dist/tools/__tests__/relationship.test.d.ts +0 -1
- package/dist/tools/__tests__/relationship.test.js +0 -132
- package/dist/tools/__tests__/resource-constraints.test.d.ts +0 -1
- package/dist/tools/__tests__/resource-constraints.test.js +0 -131
- package/dist/tools/__tests__/resource.test.d.ts +0 -1
- package/dist/tools/__tests__/resource.test.js +0 -190
- package/dist/tools/__tests__/time.test.d.ts +0 -1
- package/dist/tools/__tests__/time.test.js +0 -404
- package/dist/tools/__tests__/timers.test.d.ts +0 -1
- package/dist/tools/__tests__/timers.test.js +0 -426
- package/dist/tools/__tests__/world.test.d.ts +0 -1
- package/dist/tools/__tests__/world.test.js +0 -70
- package/dist/utils/__tests__/json.test.d.ts +0 -1
- package/dist/utils/__tests__/json.test.js +0 -55
- package/dist/utils/__tests__/validation.test.d.ts +0 -1
- package/dist/utils/__tests__/validation.test.js +0 -90
|
@@ -1,426 +0,0 @@
|
|
|
1
|
-
import { describe, it, expect, beforeEach, afterEach } from 'vitest';
|
|
2
|
-
import { createTestDb, destroyTestDb } from '../../db/__tests__/testDb.js';
|
|
3
|
-
import { getDatabase } from '../../db/connection.js';
|
|
4
|
-
import { createGame } from '../game.js';
|
|
5
|
-
import { createResource, getResource, getResourceHistory, deleteResource } from '../resource.js';
|
|
6
|
-
import { createTimer, getTimer, listTimers, tickTimer, resetTimer, modifyTimerState } from '../timers.js';
|
|
7
|
-
describe('timer tools', () => {
|
|
8
|
-
let gameId;
|
|
9
|
-
beforeEach(() => {
|
|
10
|
-
createTestDb();
|
|
11
|
-
gameId = createGame({ name: 'Test Game', setting: 'Test Setting', style: 'Test Style' }).id;
|
|
12
|
-
});
|
|
13
|
-
afterEach(() => {
|
|
14
|
-
destroyTestDb();
|
|
15
|
-
});
|
|
16
|
-
describe('tickTimer baseline (no consequence)', () => {
|
|
17
|
-
it('returns null for an unknown timer', () => {
|
|
18
|
-
expect(tickTimer('does-not-exist')).toBeNull();
|
|
19
|
-
});
|
|
20
|
-
it('counts a countdown timer down by the tick amount', () => {
|
|
21
|
-
const timer = createTimer({ gameId, name: 'fuse', timerType: 'countdown', currentValue: 5 });
|
|
22
|
-
const result = tickTimer(timer.id, 1);
|
|
23
|
-
expect(result?.timer.currentValue).toBe(4);
|
|
24
|
-
expect(result?.previousValue).toBe(5);
|
|
25
|
-
});
|
|
26
|
-
it('counts a stopwatch timer up by the tick amount', () => {
|
|
27
|
-
const timer = createTimer({ gameId, name: 'ritual', timerType: 'stopwatch', direction: 'up', maxValue: 10 });
|
|
28
|
-
const result = tickTimer(timer.id, 3);
|
|
29
|
-
expect(result?.timer.currentValue).toBe(3);
|
|
30
|
-
});
|
|
31
|
-
it('flags justTriggered the first time a countdown reaches its triggerAt', () => {
|
|
32
|
-
const timer = createTimer({ gameId, name: 'fuse', timerType: 'countdown', currentValue: 1, triggerAt: 0 });
|
|
33
|
-
const result = tickTimer(timer.id, 1);
|
|
34
|
-
expect(result?.justTriggered).toBe(true);
|
|
35
|
-
expect(result?.timer.triggered).toBe(true);
|
|
36
|
-
});
|
|
37
|
-
it('does not flag justTriggered again on a subsequent tick after already triggered', () => {
|
|
38
|
-
const timer = createTimer({ gameId, name: 'fuse', timerType: 'countdown', currentValue: 1, triggerAt: 0 });
|
|
39
|
-
tickTimer(timer.id, 1);
|
|
40
|
-
const second = tickTimer(timer.id, 1);
|
|
41
|
-
expect(second?.justTriggered).toBe(false);
|
|
42
|
-
});
|
|
43
|
-
});
|
|
44
|
-
describe('createTimer consequence persistence (opt-in field)', () => {
|
|
45
|
-
it('defaults consequence to null when not provided', () => {
|
|
46
|
-
const timer = createTimer({ gameId, name: 'fuse', timerType: 'countdown', currentValue: 3 });
|
|
47
|
-
expect(timer.consequence).toBeNull();
|
|
48
|
-
});
|
|
49
|
-
it('persists and round-trips a provided consequence through createTimer', () => {
|
|
50
|
-
const resource = createResource({ gameId, ownerType: 'game', name: 'grain', value: 10 });
|
|
51
|
-
const timer = createTimer({
|
|
52
|
-
gameId,
|
|
53
|
-
name: 'fuse',
|
|
54
|
-
timerType: 'countdown',
|
|
55
|
-
currentValue: 3,
|
|
56
|
-
consequence: { resourceId: resource.id, delta: -5 },
|
|
57
|
-
});
|
|
58
|
-
expect(timer.consequence).toEqual({ resourceId: resource.id, delta: -5 });
|
|
59
|
-
});
|
|
60
|
-
it('round-trips through getTimer', () => {
|
|
61
|
-
const resource = createResource({ gameId, ownerType: 'game', name: 'grain', value: 10 });
|
|
62
|
-
const created = createTimer({
|
|
63
|
-
gameId,
|
|
64
|
-
name: 'fuse',
|
|
65
|
-
timerType: 'countdown',
|
|
66
|
-
currentValue: 3,
|
|
67
|
-
consequence: { resourceId: resource.id, delta: -5 },
|
|
68
|
-
});
|
|
69
|
-
expect(getTimer(created.id)?.consequence).toEqual({ resourceId: resource.id, delta: -5 });
|
|
70
|
-
});
|
|
71
|
-
it('round-trips through listTimers', () => {
|
|
72
|
-
const resource = createResource({ gameId, ownerType: 'game', name: 'grain', value: 10 });
|
|
73
|
-
createTimer({
|
|
74
|
-
gameId,
|
|
75
|
-
name: 'fuse',
|
|
76
|
-
timerType: 'countdown',
|
|
77
|
-
currentValue: 3,
|
|
78
|
-
consequence: { resourceId: resource.id, delta: -5 },
|
|
79
|
-
});
|
|
80
|
-
const [listed] = listTimers(gameId);
|
|
81
|
-
expect(listed.consequence).toEqual({ resourceId: resource.id, delta: -5 });
|
|
82
|
-
});
|
|
83
|
-
});
|
|
84
|
-
describe('consequences applied by tickTimer -- no LLM in the loop', () => {
|
|
85
|
-
it('applies the consequence the moment the timer crosses its triggerAt', () => {
|
|
86
|
-
const resource = createResource({ gameId, ownerType: 'game', name: 'grain', value: 100 });
|
|
87
|
-
const timer = createTimer({
|
|
88
|
-
gameId,
|
|
89
|
-
name: 'fuse',
|
|
90
|
-
timerType: 'countdown',
|
|
91
|
-
currentValue: 1,
|
|
92
|
-
triggerAt: 0,
|
|
93
|
-
consequence: { resourceId: resource.id, delta: -30 },
|
|
94
|
-
});
|
|
95
|
-
// Nothing reads justTriggered or acts on it -- tickTimer alone must
|
|
96
|
-
// be enough for the resource to change.
|
|
97
|
-
tickTimer(timer.id, 1);
|
|
98
|
-
expect(getResource(resource.id)?.value).toBe(70);
|
|
99
|
-
});
|
|
100
|
-
it('applies a positive delta consequence', () => {
|
|
101
|
-
const resource = createResource({ gameId, ownerType: 'game', name: 'treasury', value: 10 });
|
|
102
|
-
const timer = createTimer({
|
|
103
|
-
gameId,
|
|
104
|
-
name: 'ritual completes',
|
|
105
|
-
timerType: 'stopwatch',
|
|
106
|
-
direction: 'up',
|
|
107
|
-
maxValue: 3,
|
|
108
|
-
triggerAt: 3,
|
|
109
|
-
consequence: { resourceId: resource.id, delta: 25 },
|
|
110
|
-
});
|
|
111
|
-
tickTimer(timer.id, 3);
|
|
112
|
-
expect(getResource(resource.id)?.value).toBe(35);
|
|
113
|
-
});
|
|
114
|
-
it('clamps the consequence delta the same way any other delta update is clamped', () => {
|
|
115
|
-
const resource = createResource({ gameId, ownerType: 'game', name: 'population', value: 5, minValue: 0 });
|
|
116
|
-
const timer = createTimer({
|
|
117
|
-
gameId,
|
|
118
|
-
name: 'plague',
|
|
119
|
-
timerType: 'countdown',
|
|
120
|
-
currentValue: 1,
|
|
121
|
-
triggerAt: 0,
|
|
122
|
-
consequence: { resourceId: resource.id, delta: -100 },
|
|
123
|
-
});
|
|
124
|
-
tickTimer(timer.id, 1);
|
|
125
|
-
expect(getResource(resource.id)?.value).toBe(0);
|
|
126
|
-
});
|
|
127
|
-
it('records a resource_history row whose reason references the triggering timer', () => {
|
|
128
|
-
const resource = createResource({ gameId, ownerType: 'game', name: 'grain', value: 100 });
|
|
129
|
-
const timer = createTimer({
|
|
130
|
-
gameId,
|
|
131
|
-
name: 'fuse',
|
|
132
|
-
timerType: 'countdown',
|
|
133
|
-
currentValue: 1,
|
|
134
|
-
triggerAt: 0,
|
|
135
|
-
consequence: { resourceId: resource.id, delta: -10 },
|
|
136
|
-
});
|
|
137
|
-
tickTimer(timer.id, 1);
|
|
138
|
-
const history = getResourceHistory(resource.id);
|
|
139
|
-
expect(history).toHaveLength(1);
|
|
140
|
-
expect(history[0]).toMatchObject({ previousValue: 100, newValue: 90, delta: -10 });
|
|
141
|
-
expect(history[0].reason).toContain('fuse');
|
|
142
|
-
});
|
|
143
|
-
it('reports consequenceApplied=true on the TickResult when the consequence lands', () => {
|
|
144
|
-
const resource = createResource({ gameId, ownerType: 'game', name: 'grain', value: 100 });
|
|
145
|
-
const timer = createTimer({
|
|
146
|
-
gameId,
|
|
147
|
-
name: 'fuse',
|
|
148
|
-
timerType: 'countdown',
|
|
149
|
-
currentValue: 1,
|
|
150
|
-
triggerAt: 0,
|
|
151
|
-
consequence: { resourceId: resource.id, delta: -10 },
|
|
152
|
-
});
|
|
153
|
-
const result = tickTimer(timer.id, 1);
|
|
154
|
-
expect(result?.consequenceApplied).toBe(true);
|
|
155
|
-
});
|
|
156
|
-
it('reports consequenceApplied=false when a tick does not cross the trigger', () => {
|
|
157
|
-
const resource = createResource({ gameId, ownerType: 'game', name: 'grain', value: 100 });
|
|
158
|
-
const timer = createTimer({
|
|
159
|
-
gameId,
|
|
160
|
-
name: 'fuse',
|
|
161
|
-
timerType: 'countdown',
|
|
162
|
-
currentValue: 5,
|
|
163
|
-
triggerAt: 0,
|
|
164
|
-
consequence: { resourceId: resource.id, delta: -10 },
|
|
165
|
-
});
|
|
166
|
-
const result = tickTimer(timer.id, 1);
|
|
167
|
-
expect(result?.consequenceApplied).toBe(false);
|
|
168
|
-
expect(getResource(resource.id)?.value).toBe(100);
|
|
169
|
-
});
|
|
170
|
-
it('reports consequenceApplied=false when justTriggered fires but no consequence is declared', () => {
|
|
171
|
-
const timer = createTimer({ gameId, name: 'fuse', timerType: 'countdown', currentValue: 1, triggerAt: 0 });
|
|
172
|
-
const result = tickTimer(timer.id, 1);
|
|
173
|
-
expect(result?.justTriggered).toBe(true);
|
|
174
|
-
expect(result?.consequenceApplied).toBe(false);
|
|
175
|
-
});
|
|
176
|
-
it('leaves timers with no declared consequence behaving exactly as before (opt-in)', () => {
|
|
177
|
-
const timer = createTimer({ gameId, name: 'plain fuse', timerType: 'countdown', currentValue: 1, triggerAt: 0 });
|
|
178
|
-
const result = tickTimer(timer.id, 1);
|
|
179
|
-
expect(result?.timer.currentValue).toBe(0);
|
|
180
|
-
expect(result?.timer.triggered).toBe(true);
|
|
181
|
-
});
|
|
182
|
-
});
|
|
183
|
-
describe('exactly-once guarantees', () => {
|
|
184
|
-
it('does not re-apply the consequence on a later tick after the timer already triggered', () => {
|
|
185
|
-
const resource = createResource({ gameId, ownerType: 'game', name: 'grain', value: 100 });
|
|
186
|
-
const timer = createTimer({
|
|
187
|
-
gameId,
|
|
188
|
-
name: 'fuse',
|
|
189
|
-
timerType: 'countdown',
|
|
190
|
-
currentValue: 2,
|
|
191
|
-
triggerAt: 0,
|
|
192
|
-
consequence: { resourceId: resource.id, delta: -10 },
|
|
193
|
-
});
|
|
194
|
-
tickTimer(timer.id, 1); // 2 -> 1, not yet triggered
|
|
195
|
-
tickTimer(timer.id, 1); // 1 -> 0, triggers, applies once
|
|
196
|
-
tickTimer(timer.id, 1); // already triggered, stays at 0 (floor), must not re-apply
|
|
197
|
-
expect(getResource(resource.id)?.value).toBe(90);
|
|
198
|
-
});
|
|
199
|
-
it('applies exactly once even when a single tick amount jumps straight past the trigger boundary', () => {
|
|
200
|
-
const resource = createResource({ gameId, ownerType: 'game', name: 'grain', value: 100 });
|
|
201
|
-
const timer = createTimer({
|
|
202
|
-
gameId,
|
|
203
|
-
name: 'fuse',
|
|
204
|
-
timerType: 'countdown',
|
|
205
|
-
currentValue: 10,
|
|
206
|
-
triggerAt: 5,
|
|
207
|
-
consequence: { resourceId: resource.id, delta: -10 },
|
|
208
|
-
});
|
|
209
|
-
const result = tickTimer(timer.id, 8); // 10 -> 2, jumps past triggerAt=5 in one tick
|
|
210
|
-
expect(result?.justTriggered).toBe(true);
|
|
211
|
-
expect(getResource(resource.id)?.value).toBe(90);
|
|
212
|
-
tickTimer(timer.id, 1); // further ticks must not re-apply
|
|
213
|
-
expect(getResource(resource.id)?.value).toBe(90);
|
|
214
|
-
});
|
|
215
|
-
it('applies a consequence when the new value lands exactly on triggerAt (boundary, countdown)', () => {
|
|
216
|
-
const resource = createResource({ gameId, ownerType: 'game', name: 'grain', value: 100 });
|
|
217
|
-
const timer = createTimer({
|
|
218
|
-
gameId,
|
|
219
|
-
name: 'fuse',
|
|
220
|
-
timerType: 'countdown',
|
|
221
|
-
currentValue: 5,
|
|
222
|
-
triggerAt: 3,
|
|
223
|
-
consequence: { resourceId: resource.id, delta: -10 },
|
|
224
|
-
});
|
|
225
|
-
const result = tickTimer(timer.id, 2); // 5 -> 3, exactly on triggerAt
|
|
226
|
-
expect(result?.justTriggered).toBe(true);
|
|
227
|
-
expect(getResource(resource.id)?.value).toBe(90);
|
|
228
|
-
});
|
|
229
|
-
it('applies a consequence when the new value lands exactly on triggerAt (boundary, stopwatch)', () => {
|
|
230
|
-
const resource = createResource({ gameId, ownerType: 'game', name: 'grain', value: 100 });
|
|
231
|
-
const timer = createTimer({
|
|
232
|
-
gameId,
|
|
233
|
-
name: 'ritual',
|
|
234
|
-
timerType: 'stopwatch',
|
|
235
|
-
direction: 'up',
|
|
236
|
-
maxValue: 6,
|
|
237
|
-
triggerAt: 6,
|
|
238
|
-
consequence: { resourceId: resource.id, delta: -10 },
|
|
239
|
-
});
|
|
240
|
-
const result = tickTimer(timer.id, 6);
|
|
241
|
-
expect(result?.justTriggered).toBe(true);
|
|
242
|
-
expect(getResource(resource.id)?.value).toBe(90);
|
|
243
|
-
});
|
|
244
|
-
it('re-fires the consequence after an explicit reset and a subsequent re-trigger', () => {
|
|
245
|
-
const resource = createResource({ gameId, ownerType: 'game', name: 'grain', value: 100 });
|
|
246
|
-
const timer = createTimer({
|
|
247
|
-
gameId,
|
|
248
|
-
name: 'fuse',
|
|
249
|
-
timerType: 'countdown',
|
|
250
|
-
currentValue: 1,
|
|
251
|
-
triggerAt: 0,
|
|
252
|
-
consequence: { resourceId: resource.id, delta: -10 },
|
|
253
|
-
});
|
|
254
|
-
tickTimer(timer.id, 1); // triggers, -10
|
|
255
|
-
expect(getResource(resource.id)?.value).toBe(90);
|
|
256
|
-
resetTimer(timer.id); // explicit reset clears the triggered flag
|
|
257
|
-
tickTimer(timer.id, 1); // fires again -- this is a deliberate re-arm, not a double-fire of the same event
|
|
258
|
-
expect(getResource(resource.id)?.value).toBe(80);
|
|
259
|
-
});
|
|
260
|
-
it('resetTimer itself never applies a consequence, even though it clears the triggered flag', () => {
|
|
261
|
-
const resource = createResource({ gameId, ownerType: 'game', name: 'grain', value: 100 });
|
|
262
|
-
const timer = createTimer({
|
|
263
|
-
gameId,
|
|
264
|
-
name: 'fuse',
|
|
265
|
-
timerType: 'countdown',
|
|
266
|
-
currentValue: 1,
|
|
267
|
-
triggerAt: 0,
|
|
268
|
-
consequence: { resourceId: resource.id, delta: -10 },
|
|
269
|
-
});
|
|
270
|
-
tickTimer(timer.id, 1);
|
|
271
|
-
expect(getResource(resource.id)?.value).toBe(90);
|
|
272
|
-
resetTimer(timer.id);
|
|
273
|
-
expect(getResource(resource.id)?.value).toBe(90); // unchanged by reset alone
|
|
274
|
-
});
|
|
275
|
-
it('modifyTimerState({mode: "reset"}) does not apply a consequence', () => {
|
|
276
|
-
const resource = createResource({ gameId, ownerType: 'game', name: 'grain', value: 100 });
|
|
277
|
-
const timer = createTimer({
|
|
278
|
-
gameId,
|
|
279
|
-
name: 'fuse',
|
|
280
|
-
timerType: 'countdown',
|
|
281
|
-
currentValue: 1,
|
|
282
|
-
triggerAt: 0,
|
|
283
|
-
consequence: { resourceId: resource.id, delta: -10 },
|
|
284
|
-
});
|
|
285
|
-
tickTimer(timer.id, 1);
|
|
286
|
-
modifyTimerState(timer.id, { mode: 'reset' });
|
|
287
|
-
expect(getResource(resource.id)?.value).toBe(90);
|
|
288
|
-
});
|
|
289
|
-
it('modifyTimerState({mode: "tick"}) applies the consequence exactly like tickTimer', () => {
|
|
290
|
-
const resource = createResource({ gameId, ownerType: 'game', name: 'grain', value: 100 });
|
|
291
|
-
const timer = createTimer({
|
|
292
|
-
gameId,
|
|
293
|
-
name: 'fuse',
|
|
294
|
-
timerType: 'countdown',
|
|
295
|
-
currentValue: 1,
|
|
296
|
-
triggerAt: 0,
|
|
297
|
-
consequence: { resourceId: resource.id, delta: -10 },
|
|
298
|
-
});
|
|
299
|
-
const result = modifyTimerState(timer.id, { mode: 'tick', amount: 1 });
|
|
300
|
-
expect(result?.consequenceApplied).toBe(true);
|
|
301
|
-
expect(getResource(resource.id)?.value).toBe(90);
|
|
302
|
-
});
|
|
303
|
-
});
|
|
304
|
-
describe('a consequence that itself fails', () => {
|
|
305
|
-
it('throws when the target resource never existed, and does not tick the timer at all', () => {
|
|
306
|
-
const timer = createTimer({
|
|
307
|
-
gameId,
|
|
308
|
-
name: 'fuse',
|
|
309
|
-
timerType: 'countdown',
|
|
310
|
-
currentValue: 1,
|
|
311
|
-
triggerAt: 0,
|
|
312
|
-
consequence: { resourceId: 'does-not-exist', delta: -10 },
|
|
313
|
-
});
|
|
314
|
-
expect(() => tickTimer(timer.id, 1)).toThrow();
|
|
315
|
-
const reloaded = getTimer(timer.id);
|
|
316
|
-
expect(reloaded?.currentValue).toBe(1);
|
|
317
|
-
expect(reloaded?.triggered).toBe(false);
|
|
318
|
-
});
|
|
319
|
-
it('throws when the target resource was deleted after the timer was created', () => {
|
|
320
|
-
const resource = createResource({ gameId, ownerType: 'game', name: 'grain', value: 100 });
|
|
321
|
-
const timer = createTimer({
|
|
322
|
-
gameId,
|
|
323
|
-
name: 'fuse',
|
|
324
|
-
timerType: 'countdown',
|
|
325
|
-
currentValue: 1,
|
|
326
|
-
triggerAt: 0,
|
|
327
|
-
consequence: { resourceId: resource.id, delta: -10 },
|
|
328
|
-
});
|
|
329
|
-
deleteResource(resource.id);
|
|
330
|
-
expect(() => tickTimer(timer.id, 1)).toThrow();
|
|
331
|
-
const reloaded = getTimer(timer.id);
|
|
332
|
-
expect(reloaded?.currentValue).toBe(1);
|
|
333
|
-
expect(reloaded?.triggered).toBe(false);
|
|
334
|
-
});
|
|
335
|
-
it('rolls back the value and triggered-flag change together with the consequence on a mid-transaction failure', () => {
|
|
336
|
-
const resource = createResource({ gameId, ownerType: 'game', name: 'grain', value: 100 });
|
|
337
|
-
const timer = createTimer({
|
|
338
|
-
gameId,
|
|
339
|
-
name: 'fuse',
|
|
340
|
-
timerType: 'countdown',
|
|
341
|
-
currentValue: 1,
|
|
342
|
-
triggerAt: 0,
|
|
343
|
-
consequence: { resourceId: resource.id, delta: -10 },
|
|
344
|
-
});
|
|
345
|
-
const db = getDatabase();
|
|
346
|
-
db.exec(`
|
|
347
|
-
CREATE TRIGGER fail_resource_history_insert_timer
|
|
348
|
-
BEFORE INSERT ON resource_history
|
|
349
|
-
BEGIN
|
|
350
|
-
SELECT RAISE(ABORT, 'simulated failure');
|
|
351
|
-
END;
|
|
352
|
-
`);
|
|
353
|
-
expect(() => tickTimer(timer.id, 1)).toThrow();
|
|
354
|
-
expect(getResource(resource.id)?.value).toBe(100);
|
|
355
|
-
expect(getResourceHistory(resource.id)).toHaveLength(0);
|
|
356
|
-
const reloaded = getTimer(timer.id);
|
|
357
|
-
expect(reloaded?.currentValue).toBe(1);
|
|
358
|
-
expect(reloaded?.triggered).toBe(false);
|
|
359
|
-
});
|
|
360
|
-
it('permits ticking again after fixing the underlying resource problem', () => {
|
|
361
|
-
const timer = createTimer({
|
|
362
|
-
gameId,
|
|
363
|
-
name: 'fuse',
|
|
364
|
-
timerType: 'countdown',
|
|
365
|
-
currentValue: 1,
|
|
366
|
-
triggerAt: 0,
|
|
367
|
-
consequence: { resourceId: 'does-not-exist', delta: -10 },
|
|
368
|
-
});
|
|
369
|
-
expect(() => tickTimer(timer.id, 1)).toThrow();
|
|
370
|
-
// Retrying with the same broken consequence keeps failing safely --
|
|
371
|
-
// it never silently "succeeds" by skipping the consequence.
|
|
372
|
-
expect(() => tickTimer(timer.id, 1)).toThrow();
|
|
373
|
-
const reloaded = getTimer(timer.id);
|
|
374
|
-
expect(reloaded?.currentValue).toBe(1);
|
|
375
|
-
expect(reloaded?.triggered).toBe(false);
|
|
376
|
-
});
|
|
377
|
-
});
|
|
378
|
-
describe('interleaving with unrelated timers', () => {
|
|
379
|
-
it('applies consequences independently across multiple timers sharing no state', () => {
|
|
380
|
-
const grain = createResource({ gameId, ownerType: 'game', name: 'grain', value: 100 });
|
|
381
|
-
const treasury = createResource({ gameId, ownerType: 'game', name: 'treasury', value: 100 });
|
|
382
|
-
const fuseA = createTimer({
|
|
383
|
-
gameId,
|
|
384
|
-
name: 'fuse A',
|
|
385
|
-
timerType: 'countdown',
|
|
386
|
-
currentValue: 1,
|
|
387
|
-
triggerAt: 0,
|
|
388
|
-
consequence: { resourceId: grain.id, delta: -10 },
|
|
389
|
-
});
|
|
390
|
-
const fuseB = createTimer({
|
|
391
|
-
gameId,
|
|
392
|
-
name: 'fuse B',
|
|
393
|
-
timerType: 'countdown',
|
|
394
|
-
currentValue: 1,
|
|
395
|
-
triggerAt: 0,
|
|
396
|
-
consequence: { resourceId: treasury.id, delta: -25 },
|
|
397
|
-
});
|
|
398
|
-
tickTimer(fuseA.id, 1);
|
|
399
|
-
tickTimer(fuseB.id, 1);
|
|
400
|
-
expect(getResource(grain.id)?.value).toBe(90);
|
|
401
|
-
expect(getResource(treasury.id)?.value).toBe(75);
|
|
402
|
-
});
|
|
403
|
-
it('one timer failing to apply its consequence does not affect an unrelated timer ticked afterward', () => {
|
|
404
|
-
const treasury = createResource({ gameId, ownerType: 'game', name: 'treasury', value: 100 });
|
|
405
|
-
const brokenFuse = createTimer({
|
|
406
|
-
gameId,
|
|
407
|
-
name: 'broken fuse',
|
|
408
|
-
timerType: 'countdown',
|
|
409
|
-
currentValue: 1,
|
|
410
|
-
triggerAt: 0,
|
|
411
|
-
consequence: { resourceId: 'does-not-exist', delta: -10 },
|
|
412
|
-
});
|
|
413
|
-
const healthyFuse = createTimer({
|
|
414
|
-
gameId,
|
|
415
|
-
name: 'healthy fuse',
|
|
416
|
-
timerType: 'countdown',
|
|
417
|
-
currentValue: 1,
|
|
418
|
-
triggerAt: 0,
|
|
419
|
-
consequence: { resourceId: treasury.id, delta: -25 },
|
|
420
|
-
});
|
|
421
|
-
expect(() => tickTimer(brokenFuse.id, 1)).toThrow();
|
|
422
|
-
tickTimer(healthyFuse.id, 1);
|
|
423
|
-
expect(getResource(treasury.id)?.value).toBe(75);
|
|
424
|
-
});
|
|
425
|
-
});
|
|
426
|
-
});
|
|
@@ -1 +0,0 @@
|
|
|
1
|
-
export {};
|
|
@@ -1,70 +0,0 @@
|
|
|
1
|
-
import { describe, it, expect, beforeEach, afterEach } from 'vitest';
|
|
2
|
-
import { createTestDb, destroyTestDb } from '../../db/__tests__/testDb.js';
|
|
3
|
-
import { getDatabase } from '../../db/connection.js';
|
|
4
|
-
import { createGame } from '../game.js';
|
|
5
|
-
import { connectLocations, createLocation, getLocation } from '../world.js';
|
|
6
|
-
describe('connectLocations', () => {
|
|
7
|
-
let gameId;
|
|
8
|
-
beforeEach(() => {
|
|
9
|
-
createTestDb();
|
|
10
|
-
gameId = createGame({ name: 'Test Game', setting: 'Test Setting', style: 'Test Style' }).id;
|
|
11
|
-
});
|
|
12
|
-
afterEach(() => {
|
|
13
|
-
destroyTestDb();
|
|
14
|
-
});
|
|
15
|
-
it('adds a one-way exit when bidirectional is false', () => {
|
|
16
|
-
const a = createLocation({ gameId, name: 'Field', description: 'An open field' });
|
|
17
|
-
const b = createLocation({ gameId, name: 'Barn', description: 'A wooden barn' });
|
|
18
|
-
const result = connectLocations({
|
|
19
|
-
fromLocationId: a.id,
|
|
20
|
-
toLocationId: b.id,
|
|
21
|
-
fromDirection: 'north',
|
|
22
|
-
toDirection: 'south',
|
|
23
|
-
bidirectional: false,
|
|
24
|
-
});
|
|
25
|
-
expect(result?.success).toBe(true);
|
|
26
|
-
expect(result?.reverseExitCreated).toBeNull();
|
|
27
|
-
expect(getLocation(a.id)?.properties.exits).toHaveLength(1);
|
|
28
|
-
expect(getLocation(b.id)?.properties.exits).toHaveLength(0);
|
|
29
|
-
});
|
|
30
|
-
it('adds exits to both locations when bidirectional', () => {
|
|
31
|
-
const a = createLocation({ gameId, name: 'Field', description: 'An open field' });
|
|
32
|
-
const b = createLocation({ gameId, name: 'Barn', description: 'A wooden barn' });
|
|
33
|
-
connectLocations({
|
|
34
|
-
fromLocationId: a.id,
|
|
35
|
-
toLocationId: b.id,
|
|
36
|
-
fromDirection: 'north',
|
|
37
|
-
toDirection: 'south',
|
|
38
|
-
});
|
|
39
|
-
expect(getLocation(a.id)?.properties.exits).toHaveLength(1);
|
|
40
|
-
expect(getLocation(b.id)?.properties.exits).toHaveLength(1);
|
|
41
|
-
});
|
|
42
|
-
it('leaves NO partial write when the second (reverse-exit) update fails mid-operation', () => {
|
|
43
|
-
const a = createLocation({ gameId, name: 'Field', description: 'An open field' });
|
|
44
|
-
const b = createLocation({ gameId, name: 'Barn', description: 'A wooden barn' });
|
|
45
|
-
// Fault injection: make the UPDATE against location `b` (the second
|
|
46
|
-
// write connectLocations performs) fail, simulating a mid-operation
|
|
47
|
-
// failure such as a disk error or constraint violation. If the two
|
|
48
|
-
// writes aren't wrapped in a real transaction, location `a` will have
|
|
49
|
-
// already been updated by the time this trigger fires.
|
|
50
|
-
const db = getDatabase();
|
|
51
|
-
db.exec(`
|
|
52
|
-
CREATE TRIGGER fail_second_update
|
|
53
|
-
BEFORE UPDATE ON locations
|
|
54
|
-
WHEN NEW.id = '${b.id}'
|
|
55
|
-
BEGIN
|
|
56
|
-
SELECT RAISE(ABORT, 'simulated failure');
|
|
57
|
-
END;
|
|
58
|
-
`);
|
|
59
|
-
expect(() => connectLocations({
|
|
60
|
-
fromLocationId: a.id,
|
|
61
|
-
toLocationId: b.id,
|
|
62
|
-
fromDirection: 'north',
|
|
63
|
-
toDirection: 'south',
|
|
64
|
-
})).toThrow();
|
|
65
|
-
// Neither location should show any exit -- the forward-exit write to
|
|
66
|
-
// `a` must have been rolled back along with the failed write to `b`.
|
|
67
|
-
expect(getLocation(a.id)?.properties.exits).toHaveLength(0);
|
|
68
|
-
expect(getLocation(b.id)?.properties.exits).toHaveLength(0);
|
|
69
|
-
});
|
|
70
|
-
});
|
|
@@ -1 +0,0 @@
|
|
|
1
|
-
export {};
|
|
@@ -1,55 +0,0 @@
|
|
|
1
|
-
import { describe, it, expect } from 'vitest';
|
|
2
|
-
import { safeJsonParse, safeJsonParseOrNull } from '../json.js';
|
|
3
|
-
describe('safeJsonParse', () => {
|
|
4
|
-
it('should parse valid JSON', () => {
|
|
5
|
-
const result = safeJsonParse('{"name": "test"}', {});
|
|
6
|
-
expect(result).toEqual({ name: 'test' });
|
|
7
|
-
});
|
|
8
|
-
it('should return fallback for invalid JSON', () => {
|
|
9
|
-
const fallback = { default: true };
|
|
10
|
-
const result = safeJsonParse('not valid json', fallback);
|
|
11
|
-
expect(result).toEqual(fallback);
|
|
12
|
-
});
|
|
13
|
-
it('should return fallback for empty string', () => {
|
|
14
|
-
const fallback = [];
|
|
15
|
-
const result = safeJsonParse('', fallback);
|
|
16
|
-
expect(result).toEqual(fallback);
|
|
17
|
-
});
|
|
18
|
-
it('should parse arrays', () => {
|
|
19
|
-
const result = safeJsonParse('["a", "b", "c"]', []);
|
|
20
|
-
expect(result).toEqual(['a', 'b', 'c']);
|
|
21
|
-
});
|
|
22
|
-
it('should parse primitive values', () => {
|
|
23
|
-
expect(safeJsonParse('42', 0)).toBe(42);
|
|
24
|
-
expect(safeJsonParse('"hello"', '')).toBe('hello');
|
|
25
|
-
expect(safeJsonParse('true', false)).toBe(true);
|
|
26
|
-
expect(safeJsonParse('null', 'fallback')).toBe(null);
|
|
27
|
-
});
|
|
28
|
-
it('should return fallback for undefined JSON text', () => {
|
|
29
|
-
const result = safeJsonParse(undefined, { fallback: true });
|
|
30
|
-
expect(result).toEqual({ fallback: true });
|
|
31
|
-
});
|
|
32
|
-
});
|
|
33
|
-
describe('safeJsonParseOrNull', () => {
|
|
34
|
-
it('should parse valid JSON', () => {
|
|
35
|
-
const result = safeJsonParseOrNull('{"name": "test"}');
|
|
36
|
-
expect(result).toEqual({ name: 'test' });
|
|
37
|
-
});
|
|
38
|
-
it('should return null for invalid JSON', () => {
|
|
39
|
-
const result = safeJsonParseOrNull('not valid json');
|
|
40
|
-
expect(result).toBeNull();
|
|
41
|
-
});
|
|
42
|
-
it('should return null for empty string', () => {
|
|
43
|
-
const result = safeJsonParseOrNull('');
|
|
44
|
-
expect(result).toBeNull();
|
|
45
|
-
});
|
|
46
|
-
it('should parse arrays', () => {
|
|
47
|
-
const result = safeJsonParseOrNull('[1, 2, 3]');
|
|
48
|
-
expect(result).toEqual([1, 2, 3]);
|
|
49
|
-
});
|
|
50
|
-
it('should return null for malformed JSON', () => {
|
|
51
|
-
expect(safeJsonParseOrNull('{name: "test"}')).toBeNull();
|
|
52
|
-
expect(safeJsonParseOrNull('{"name": test}')).toBeNull();
|
|
53
|
-
expect(safeJsonParseOrNull('{incomplete')).toBeNull();
|
|
54
|
-
});
|
|
55
|
-
});
|
|
@@ -1 +0,0 @@
|
|
|
1
|
-
export {};
|
|
@@ -1,90 +0,0 @@
|
|
|
1
|
-
import { describe, it, expect } from 'vitest';
|
|
2
|
-
import { LIMITS, validatedSchemas, boundedString, boundedArray } from '../validation.js';
|
|
3
|
-
import { z } from 'zod';
|
|
4
|
-
describe('LIMITS', () => {
|
|
5
|
-
it('should have correct values', () => {
|
|
6
|
-
expect(LIMITS.NAME_MAX).toBe(200);
|
|
7
|
-
expect(LIMITS.DESCRIPTION_MAX).toBe(5000);
|
|
8
|
-
expect(LIMITS.CONTENT_MAX).toBe(50000);
|
|
9
|
-
expect(LIMITS.NARRATIVE_MAX).toBe(200000);
|
|
10
|
-
expect(LIMITS.ARRAY_MAX).toBe(100);
|
|
11
|
-
expect(LIMITS.MAX_DEPTH).toBe(10);
|
|
12
|
-
});
|
|
13
|
-
});
|
|
14
|
-
describe('validatedSchemas', () => {
|
|
15
|
-
describe('name', () => {
|
|
16
|
-
it('should accept valid names', () => {
|
|
17
|
-
expect(validatedSchemas.name.parse('Test Name')).toBe('Test Name');
|
|
18
|
-
});
|
|
19
|
-
it('should reject empty names', () => {
|
|
20
|
-
expect(() => validatedSchemas.name.parse('')).toThrow();
|
|
21
|
-
});
|
|
22
|
-
it('should reject names exceeding max length', () => {
|
|
23
|
-
const longName = 'a'.repeat(LIMITS.NAME_MAX + 1);
|
|
24
|
-
expect(() => validatedSchemas.name.parse(longName)).toThrow();
|
|
25
|
-
});
|
|
26
|
-
it('should accept names at max length', () => {
|
|
27
|
-
const maxName = 'a'.repeat(LIMITS.NAME_MAX);
|
|
28
|
-
expect(validatedSchemas.name.parse(maxName)).toBe(maxName);
|
|
29
|
-
});
|
|
30
|
-
});
|
|
31
|
-
describe('description', () => {
|
|
32
|
-
it('should accept valid descriptions', () => {
|
|
33
|
-
expect(validatedSchemas.description.parse('A description')).toBe('A description');
|
|
34
|
-
});
|
|
35
|
-
it('should accept empty descriptions', () => {
|
|
36
|
-
expect(validatedSchemas.description.parse('')).toBe('');
|
|
37
|
-
});
|
|
38
|
-
it('should reject descriptions exceeding max length', () => {
|
|
39
|
-
const longDesc = 'a'.repeat(LIMITS.DESCRIPTION_MAX + 1);
|
|
40
|
-
expect(() => validatedSchemas.description.parse(longDesc)).toThrow();
|
|
41
|
-
});
|
|
42
|
-
});
|
|
43
|
-
describe('id', () => {
|
|
44
|
-
it('should accept valid UUIDs', () => {
|
|
45
|
-
const uuid = '550e8400-e29b-41d4-a716-446655440000';
|
|
46
|
-
expect(validatedSchemas.id.parse(uuid)).toBe(uuid);
|
|
47
|
-
});
|
|
48
|
-
it('should reject IDs exceeding 100 characters', () => {
|
|
49
|
-
const longId = 'a'.repeat(101);
|
|
50
|
-
expect(() => validatedSchemas.id.parse(longId)).toThrow();
|
|
51
|
-
});
|
|
52
|
-
});
|
|
53
|
-
describe('stringArray', () => {
|
|
54
|
-
it('should accept valid arrays', () => {
|
|
55
|
-
const arr = ['one', 'two', 'three'];
|
|
56
|
-
expect(validatedSchemas.stringArray.parse(arr)).toEqual(arr);
|
|
57
|
-
});
|
|
58
|
-
it('should accept empty arrays', () => {
|
|
59
|
-
expect(validatedSchemas.stringArray.parse([])).toEqual([]);
|
|
60
|
-
});
|
|
61
|
-
it('should reject arrays exceeding max items', () => {
|
|
62
|
-
const longArr = Array(LIMITS.ARRAY_MAX + 1).fill('item');
|
|
63
|
-
expect(() => validatedSchemas.stringArray.parse(longArr)).toThrow();
|
|
64
|
-
});
|
|
65
|
-
});
|
|
66
|
-
});
|
|
67
|
-
describe('boundedString', () => {
|
|
68
|
-
it('should create a schema with default max length', () => {
|
|
69
|
-
const schema = boundedString();
|
|
70
|
-
expect(schema.parse('test')).toBe('test');
|
|
71
|
-
expect(() => schema.parse('a'.repeat(LIMITS.NAME_MAX + 1))).toThrow();
|
|
72
|
-
});
|
|
73
|
-
it('should create a schema with custom max length', () => {
|
|
74
|
-
const schema = boundedString(10);
|
|
75
|
-
expect(schema.parse('test')).toBe('test');
|
|
76
|
-
expect(() => schema.parse('a'.repeat(11))).toThrow();
|
|
77
|
-
});
|
|
78
|
-
});
|
|
79
|
-
describe('boundedArray', () => {
|
|
80
|
-
it('should create an array schema with default max items', () => {
|
|
81
|
-
const schema = boundedArray(z.string());
|
|
82
|
-
expect(schema.parse(['a', 'b'])).toEqual(['a', 'b']);
|
|
83
|
-
expect(() => schema.parse(Array(LIMITS.ARRAY_MAX + 1).fill('x'))).toThrow();
|
|
84
|
-
});
|
|
85
|
-
it('should create an array schema with custom max items', () => {
|
|
86
|
-
const schema = boundedArray(z.number(), 3);
|
|
87
|
-
expect(schema.parse([1, 2, 3])).toEqual([1, 2, 3]);
|
|
88
|
-
expect(() => schema.parse([1, 2, 3, 4])).toThrow();
|
|
89
|
-
});
|
|
90
|
-
});
|