@sharpee/transcript-tester 0.9.61-beta

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.
Files changed (137) hide show
  1. package/.turbo/turbo-build.log +4 -0
  2. package/LICENSE +21 -0
  3. package/dist/cli.d.ts +11 -0
  4. package/dist/cli.d.ts.map +1 -0
  5. package/dist/cli.js +367 -0
  6. package/dist/cli.js.map +1 -0
  7. package/dist/condition-evaluator.d.ts +30 -0
  8. package/dist/condition-evaluator.d.ts.map +1 -0
  9. package/dist/condition-evaluator.js +314 -0
  10. package/dist/condition-evaluator.js.map +1 -0
  11. package/dist/fast-cli.d.ts +13 -0
  12. package/dist/fast-cli.d.ts.map +1 -0
  13. package/dist/fast-cli.js +363 -0
  14. package/dist/fast-cli.js.map +1 -0
  15. package/dist/index.d.ts +17 -0
  16. package/dist/index.d.ts.map +1 -0
  17. package/dist/index.js +48 -0
  18. package/dist/index.js.map +1 -0
  19. package/dist/navigator.d.ts +27 -0
  20. package/dist/navigator.d.ts.map +1 -0
  21. package/dist/navigator.js +303 -0
  22. package/dist/navigator.js.map +1 -0
  23. package/dist/parser.d.ts +19 -0
  24. package/dist/parser.d.ts.map +1 -0
  25. package/dist/parser.js +453 -0
  26. package/dist/parser.js.map +1 -0
  27. package/dist/reporter.d.ts +41 -0
  28. package/dist/reporter.d.ts.map +1 -0
  29. package/dist/reporter.js +386 -0
  30. package/dist/reporter.js.map +1 -0
  31. package/dist/runner.d.ts +44 -0
  32. package/dist/runner.d.ts.map +1 -0
  33. package/dist/runner.js +977 -0
  34. package/dist/runner.js.map +1 -0
  35. package/dist/story-loader.d.ts +31 -0
  36. package/dist/story-loader.d.ts.map +1 -0
  37. package/dist/story-loader.js +169 -0
  38. package/dist/story-loader.js.map +1 -0
  39. package/dist/types.d.ts +204 -0
  40. package/dist/types.d.ts.map +1 -0
  41. package/dist/types.js +8 -0
  42. package/dist/types.js.map +1 -0
  43. package/dist-esm/cli.d.ts +11 -0
  44. package/dist-esm/cli.d.ts.map +1 -0
  45. package/dist-esm/cli.js +332 -0
  46. package/dist-esm/cli.js.map +1 -0
  47. package/dist-esm/condition-evaluator.d.ts +30 -0
  48. package/dist-esm/condition-evaluator.d.ts.map +1 -0
  49. package/dist-esm/condition-evaluator.js +311 -0
  50. package/dist-esm/condition-evaluator.js.map +1 -0
  51. package/dist-esm/fast-cli.d.ts +13 -0
  52. package/dist-esm/fast-cli.d.ts.map +1 -0
  53. package/dist-esm/fast-cli.js +328 -0
  54. package/dist-esm/fast-cli.js.map +1 -0
  55. package/dist-esm/index.d.ts +17 -0
  56. package/dist-esm/index.d.ts.map +1 -0
  57. package/dist-esm/index.js +21 -0
  58. package/dist-esm/index.js.map +1 -0
  59. package/dist-esm/navigator.d.ts +27 -0
  60. package/dist-esm/navigator.d.ts.map +1 -0
  61. package/dist-esm/navigator.js +300 -0
  62. package/dist-esm/navigator.js.map +1 -0
  63. package/dist-esm/parser.d.ts +19 -0
  64. package/dist-esm/parser.d.ts.map +1 -0
  65. package/dist-esm/parser.js +415 -0
  66. package/dist-esm/parser.js.map +1 -0
  67. package/dist-esm/reporter.d.ts +41 -0
  68. package/dist-esm/reporter.d.ts.map +1 -0
  69. package/dist-esm/reporter.js +342 -0
  70. package/dist-esm/reporter.js.map +1 -0
  71. package/dist-esm/runner.d.ts +44 -0
  72. package/dist-esm/runner.d.ts.map +1 -0
  73. package/dist-esm/runner.js +941 -0
  74. package/dist-esm/runner.js.map +1 -0
  75. package/dist-esm/story-loader.d.ts +31 -0
  76. package/dist-esm/story-loader.d.ts.map +1 -0
  77. package/dist-esm/story-loader.js +131 -0
  78. package/dist-esm/story-loader.js.map +1 -0
  79. package/dist-esm/types.d.ts +204 -0
  80. package/dist-esm/types.d.ts.map +1 -0
  81. package/dist-esm/types.js +7 -0
  82. package/dist-esm/types.js.map +1 -0
  83. package/dist-npm/cli.d.ts +11 -0
  84. package/dist-npm/cli.d.ts.map +1 -0
  85. package/dist-npm/cli.js +367 -0
  86. package/dist-npm/cli.js.map +1 -0
  87. package/dist-npm/condition-evaluator.d.ts +30 -0
  88. package/dist-npm/condition-evaluator.d.ts.map +1 -0
  89. package/dist-npm/condition-evaluator.js +314 -0
  90. package/dist-npm/condition-evaluator.js.map +1 -0
  91. package/dist-npm/fast-cli.d.ts +13 -0
  92. package/dist-npm/fast-cli.d.ts.map +1 -0
  93. package/dist-npm/fast-cli.js +363 -0
  94. package/dist-npm/fast-cli.js.map +1 -0
  95. package/dist-npm/index.d.ts +17 -0
  96. package/dist-npm/index.d.ts.map +1 -0
  97. package/dist-npm/index.js +48 -0
  98. package/dist-npm/index.js.map +1 -0
  99. package/dist-npm/navigator.d.ts +27 -0
  100. package/dist-npm/navigator.d.ts.map +1 -0
  101. package/dist-npm/navigator.js +303 -0
  102. package/dist-npm/navigator.js.map +1 -0
  103. package/dist-npm/parser.d.ts +19 -0
  104. package/dist-npm/parser.d.ts.map +1 -0
  105. package/dist-npm/parser.js +453 -0
  106. package/dist-npm/parser.js.map +1 -0
  107. package/dist-npm/reporter.d.ts +41 -0
  108. package/dist-npm/reporter.d.ts.map +1 -0
  109. package/dist-npm/reporter.js +386 -0
  110. package/dist-npm/reporter.js.map +1 -0
  111. package/dist-npm/runner.d.ts +44 -0
  112. package/dist-npm/runner.d.ts.map +1 -0
  113. package/dist-npm/runner.js +977 -0
  114. package/dist-npm/runner.js.map +1 -0
  115. package/dist-npm/story-loader.d.ts +31 -0
  116. package/dist-npm/story-loader.d.ts.map +1 -0
  117. package/dist-npm/story-loader.js +169 -0
  118. package/dist-npm/story-loader.js.map +1 -0
  119. package/dist-npm/types.d.ts +204 -0
  120. package/dist-npm/types.d.ts.map +1 -0
  121. package/dist-npm/types.js +8 -0
  122. package/dist-npm/types.js.map +1 -0
  123. package/package.json +49 -0
  124. package/src/cli.ts +385 -0
  125. package/src/condition-evaluator.ts +382 -0
  126. package/src/fast-cli.ts +403 -0
  127. package/src/index.ts +26 -0
  128. package/src/navigator.ts +365 -0
  129. package/src/parser.ts +488 -0
  130. package/src/reporter.ts +409 -0
  131. package/src/runner.ts +1152 -0
  132. package/src/story-loader.ts +168 -0
  133. package/src/types.ts +244 -0
  134. package/tsconfig.esm.json +11 -0
  135. package/tsconfig.esm.tsbuildinfo +1 -0
  136. package/tsconfig.json +22 -0
  137. package/tsconfig.tsbuildinfo +1 -0
@@ -0,0 +1,977 @@
1
+ "use strict";
2
+ /**
3
+ * Transcript Runner
4
+ *
5
+ * Executes transcript commands against a loaded story and checks results.
6
+ */
7
+ var __createBinding = (this && this.__createBinding) || (Object.create ? (function(o, m, k, k2) {
8
+ if (k2 === undefined) k2 = k;
9
+ var desc = Object.getOwnPropertyDescriptor(m, k);
10
+ if (!desc || ("get" in desc ? !m.__esModule : desc.writable || desc.configurable)) {
11
+ desc = { enumerable: true, get: function() { return m[k]; } };
12
+ }
13
+ Object.defineProperty(o, k2, desc);
14
+ }) : (function(o, m, k, k2) {
15
+ if (k2 === undefined) k2 = k;
16
+ o[k2] = m[k];
17
+ }));
18
+ var __setModuleDefault = (this && this.__setModuleDefault) || (Object.create ? (function(o, v) {
19
+ Object.defineProperty(o, "default", { enumerable: true, value: v });
20
+ }) : function(o, v) {
21
+ o["default"] = v;
22
+ });
23
+ var __importStar = (this && this.__importStar) || (function () {
24
+ var ownKeys = function(o) {
25
+ ownKeys = Object.getOwnPropertyNames || function (o) {
26
+ var ar = [];
27
+ for (var k in o) if (Object.prototype.hasOwnProperty.call(o, k)) ar[ar.length] = k;
28
+ return ar;
29
+ };
30
+ return ownKeys(o);
31
+ };
32
+ return function (mod) {
33
+ if (mod && mod.__esModule) return mod;
34
+ var result = {};
35
+ if (mod != null) for (var k = ownKeys(mod), i = 0; i < k.length; i++) if (k[i] !== "default") __createBinding(result, mod, k[i]);
36
+ __setModuleDefault(result, mod);
37
+ return result;
38
+ };
39
+ })();
40
+ Object.defineProperty(exports, "__esModule", { value: true });
41
+ exports.runTranscript = runTranscript;
42
+ const fs = __importStar(require("fs"));
43
+ const path = __importStar(require("path"));
44
+ const condition_evaluator_1 = require("./condition-evaluator");
45
+ const navigator_1 = require("./navigator");
46
+ // Constants for directive execution
47
+ const MAX_WHILE_ITERATIONS = 100;
48
+ const MAX_BLOCK_DEPTH = 10;
49
+ /**
50
+ * Run a single transcript against an engine
51
+ *
52
+ * If transcript has items (with directives), use the smart runner.
53
+ * Otherwise, fall back to legacy command-only execution.
54
+ */
55
+ async function runTranscript(transcript, engine, options = {}) {
56
+ // Use smart runner if we have items with directives
57
+ if (transcript.items && transcript.items.length > 0) {
58
+ const hasDirectives = transcript.items.some(i => i.type === 'directive');
59
+ if (hasDirectives) {
60
+ return runSmartTranscript(transcript, engine, options);
61
+ }
62
+ }
63
+ // Legacy: command-only execution
64
+ const startTime = Date.now();
65
+ const results = [];
66
+ for (const command of transcript.commands) {
67
+ const result = await runCommand(command, engine, options);
68
+ results.push(result);
69
+ if (options.stopOnFailure && !result.passed && !result.expectedFailure && !result.skipped) {
70
+ break;
71
+ }
72
+ }
73
+ const passed = results.filter(r => r.passed && !r.skipped).length;
74
+ const failed = results.filter(r => !r.passed && !r.expectedFailure && !r.skipped).length;
75
+ const expectedFailures = results.filter(r => r.expectedFailure).length;
76
+ const skipped = results.filter(r => r.skipped).length;
77
+ return {
78
+ transcript,
79
+ commands: results,
80
+ passed,
81
+ failed,
82
+ expectedFailures,
83
+ skipped,
84
+ duration: Date.now() - startTime
85
+ };
86
+ }
87
+ /**
88
+ * Run a transcript with smart directives (IF/WHILE/NAVIGATE/GOAL)
89
+ */
90
+ async function runSmartTranscript(transcript, engine, options = {}) {
91
+ const startTime = Date.now();
92
+ const results = [];
93
+ const items = transcript.items;
94
+ // Get player ID for condition evaluation
95
+ const playerId = getPlayerId(engine);
96
+ // Block state stack for control flow
97
+ const blockStack = [];
98
+ // Main execution loop
99
+ let i = 0;
100
+ while (i < items.length) {
101
+ const item = items[i];
102
+ // Check if we should skip this item due to inactive block
103
+ if (blockStack.length > 0 && !blockStack[blockStack.length - 1].active) {
104
+ // Skip this item, but process END directives to close blocks
105
+ if (item.type === 'directive') {
106
+ const handled = await handleEndDirective(item.directive, blockStack, i, items, engine, playerId, options);
107
+ if (handled.consumed) {
108
+ i = handled.nextIndex;
109
+ continue;
110
+ }
111
+ }
112
+ i++;
113
+ continue;
114
+ }
115
+ if (item.type === 'command') {
116
+ // Execute command
117
+ const result = await runCommand(item.command, engine, options);
118
+ results.push(result);
119
+ // Update annotation context for ext-testing
120
+ if (options.testingExtension?.setCommandContext) {
121
+ options.testingExtension.setCommandContext(result.command.input, result.actualOutput);
122
+ }
123
+ if (options.stopOnFailure && !result.passed && !result.expectedFailure && !result.skipped) {
124
+ break;
125
+ }
126
+ i++;
127
+ }
128
+ else if (item.type === 'comment') {
129
+ // Handle comment annotation for ext-testing
130
+ if (options.testingExtension?.addAnnotation && item.comment) {
131
+ options.testingExtension.addAnnotation('comment', item.comment.text, engine.world);
132
+ }
133
+ i++;
134
+ }
135
+ else if (item.type === 'directive') {
136
+ const directive = item.directive;
137
+ const directiveResult = await handleDirective(directive, blockStack, i, items, engine, playerId, options);
138
+ if (directiveResult.error && options.stopOnFailure) {
139
+ // Create a synthetic failed result for the directive
140
+ results.push(createDirectiveFailResult(directive, directiveResult.error));
141
+ break;
142
+ }
143
+ // Add any command results from directive execution (e.g., NAVIGATE)
144
+ if (directiveResult.commandResults) {
145
+ results.push(...directiveResult.commandResults);
146
+ }
147
+ i = directiveResult.nextIndex;
148
+ }
149
+ else {
150
+ i++;
151
+ }
152
+ }
153
+ const passed = results.filter(r => r.passed && !r.skipped).length;
154
+ const failed = results.filter(r => !r.passed && !r.expectedFailure && !r.skipped).length;
155
+ const expectedFailures = results.filter(r => r.expectedFailure).length;
156
+ const skipped = results.filter(r => r.skipped).length;
157
+ return {
158
+ transcript,
159
+ commands: results,
160
+ passed,
161
+ failed,
162
+ expectedFailures,
163
+ skipped,
164
+ duration: Date.now() - startTime
165
+ };
166
+ }
167
+ /**
168
+ * Get player entity ID from engine
169
+ */
170
+ function getPlayerId(engine) {
171
+ if (engine.world?.getPlayer) {
172
+ const player = engine.world.getPlayer();
173
+ return player?.id || 'player';
174
+ }
175
+ return 'player';
176
+ }
177
+ /**
178
+ * Handle a directive (control flow, navigation, goals)
179
+ */
180
+ async function handleDirective(directive, blockStack, currentIndex, items, engine, playerId, options) {
181
+ const world = engine.world;
182
+ const verbose = options.verbose || false;
183
+ switch (directive.type) {
184
+ case 'goal': {
185
+ // Start a goal block
186
+ if (blockStack.length >= MAX_BLOCK_DEPTH) {
187
+ return { nextIndex: currentIndex + 1, error: 'Max block depth exceeded' };
188
+ }
189
+ // Find REQUIRES and ENSURES for this goal
190
+ const requires = [];
191
+ const ensures = [];
192
+ let j = currentIndex + 1;
193
+ while (j < items.length) {
194
+ const nextItem = items[j];
195
+ if (nextItem.type !== 'directive')
196
+ break;
197
+ const nextDir = nextItem.directive;
198
+ if (nextDir.type === 'requires' && nextDir.condition) {
199
+ requires.push(nextDir.condition);
200
+ j++;
201
+ }
202
+ else if (nextDir.type === 'ensures' && nextDir.condition) {
203
+ ensures.push(nextDir.condition);
204
+ j++;
205
+ }
206
+ else {
207
+ break;
208
+ }
209
+ }
210
+ // Check REQUIRES preconditions
211
+ let allRequiresMet = true;
212
+ if (world) {
213
+ for (const req of requires) {
214
+ const result = (0, condition_evaluator_1.evaluateCondition)(req, world, playerId);
215
+ if (verbose) {
216
+ console.log(` [GOAL "${directive.goalName}"] REQUIRES: ${req} -> ${result.met ? 'OK' : 'FAILED'}`);
217
+ }
218
+ if (!result.met) {
219
+ allRequiresMet = false;
220
+ break;
221
+ }
222
+ }
223
+ }
224
+ if (!allRequiresMet) {
225
+ return {
226
+ nextIndex: currentIndex + 1,
227
+ error: `Goal "${directive.goalName}" preconditions not met`
228
+ };
229
+ }
230
+ if (verbose) {
231
+ console.log(`[GOAL: ${directive.goalName}]`);
232
+ }
233
+ blockStack.push({
234
+ type: 'goal',
235
+ startIndex: j, // Skip past REQUIRES/ENSURES
236
+ active: true,
237
+ iterations: 0,
238
+ goalName: directive.goalName,
239
+ ensures
240
+ });
241
+ return { nextIndex: j };
242
+ }
243
+ case 'end_goal': {
244
+ // End goal block and check ENSURES
245
+ const block = blockStack.pop();
246
+ if (!block || block.type !== 'goal') {
247
+ return { nextIndex: currentIndex + 1, error: 'END GOAL without matching GOAL' };
248
+ }
249
+ // Check ENSURES postconditions
250
+ if (world && block.ensures) {
251
+ for (const ens of block.ensures) {
252
+ const result = (0, condition_evaluator_1.evaluateCondition)(ens, world, playerId);
253
+ if (verbose) {
254
+ console.log(` [END GOAL "${block.goalName}"] ENSURES: ${ens} -> ${result.met ? 'OK' : 'FAILED'}`);
255
+ }
256
+ if (!result.met) {
257
+ return {
258
+ nextIndex: currentIndex + 1,
259
+ error: `Goal "${block.goalName}" postcondition failed: ${ens}`
260
+ };
261
+ }
262
+ }
263
+ }
264
+ if (verbose) {
265
+ console.log(`[END GOAL: ${block.goalName}] - Success`);
266
+ }
267
+ return { nextIndex: currentIndex + 1 };
268
+ }
269
+ case 'requires':
270
+ case 'ensures':
271
+ // These are handled as part of GOAL processing
272
+ return { nextIndex: currentIndex + 1 };
273
+ case 'if': {
274
+ if (blockStack.length >= MAX_BLOCK_DEPTH) {
275
+ return { nextIndex: currentIndex + 1, error: 'Max block depth exceeded' };
276
+ }
277
+ let conditionMet = true;
278
+ if (world && directive.condition) {
279
+ const result = (0, condition_evaluator_1.evaluateCondition)(directive.condition, world, playerId);
280
+ conditionMet = result.met;
281
+ if (verbose) {
282
+ console.log(` [IF: ${directive.condition}] -> ${conditionMet ? 'TRUE' : 'FALSE'} (${result.reason})`);
283
+ }
284
+ }
285
+ blockStack.push({
286
+ type: 'if',
287
+ condition: directive.condition,
288
+ startIndex: currentIndex,
289
+ active: conditionMet,
290
+ iterations: 0
291
+ });
292
+ return { nextIndex: currentIndex + 1 };
293
+ }
294
+ case 'end_if': {
295
+ const block = blockStack.pop();
296
+ if (!block || block.type !== 'if') {
297
+ return { nextIndex: currentIndex + 1, error: 'END IF without matching IF' };
298
+ }
299
+ return { nextIndex: currentIndex + 1 };
300
+ }
301
+ case 'while': {
302
+ if (blockStack.length >= MAX_BLOCK_DEPTH) {
303
+ return { nextIndex: currentIndex + 1, error: 'Max block depth exceeded' };
304
+ }
305
+ let conditionMet = true;
306
+ if (world && directive.condition) {
307
+ const result = (0, condition_evaluator_1.evaluateCondition)(directive.condition, world, playerId);
308
+ conditionMet = result.met;
309
+ if (verbose) {
310
+ console.log(` [WHILE: ${directive.condition}] -> ${conditionMet ? 'TRUE (entering loop)' : 'FALSE (skipping loop)'}`);
311
+ }
312
+ }
313
+ blockStack.push({
314
+ type: 'while',
315
+ condition: directive.condition,
316
+ startIndex: currentIndex,
317
+ active: conditionMet,
318
+ iterations: 0
319
+ });
320
+ return { nextIndex: currentIndex + 1 };
321
+ }
322
+ case 'end_while': {
323
+ const block = blockStack[blockStack.length - 1];
324
+ if (!block || block.type !== 'while') {
325
+ blockStack.pop();
326
+ return { nextIndex: currentIndex + 1, error: 'END WHILE without matching WHILE' };
327
+ }
328
+ block.iterations++;
329
+ if (block.iterations >= MAX_WHILE_ITERATIONS) {
330
+ blockStack.pop();
331
+ return { nextIndex: currentIndex + 1, error: `WHILE loop exceeded ${MAX_WHILE_ITERATIONS} iterations` };
332
+ }
333
+ // Re-evaluate condition
334
+ let conditionMet = false;
335
+ if (world && block.condition) {
336
+ const result = (0, condition_evaluator_1.evaluateCondition)(block.condition, world, playerId);
337
+ conditionMet = result.met;
338
+ if (verbose) {
339
+ console.log(` [END WHILE iteration ${block.iterations}] ${block.condition} -> ${conditionMet ? 'TRUE (continue)' : 'FALSE (exit loop)'}`);
340
+ }
341
+ }
342
+ if (conditionMet) {
343
+ // Loop back to after WHILE directive
344
+ return { nextIndex: block.startIndex + 1 };
345
+ }
346
+ else {
347
+ // Exit loop
348
+ blockStack.pop();
349
+ return { nextIndex: currentIndex + 1 };
350
+ }
351
+ }
352
+ case 'navigate': {
353
+ if (!world || !directive.target) {
354
+ return { nextIndex: currentIndex + 1, error: 'NAVIGATE requires world model and target' };
355
+ }
356
+ if (verbose) {
357
+ console.log(`[NAVIGATE TO: "${directive.target}"]`);
358
+ }
359
+ const navResult = await (0, navigator_1.executeNavigate)(directive.target, world, engine, playerId, verbose);
360
+ if (!navResult.success) {
361
+ return {
362
+ nextIndex: currentIndex + 1,
363
+ error: navResult.error || `Navigation to "${directive.target}" failed`
364
+ };
365
+ }
366
+ // Create synthetic command results for the navigation commands
367
+ const commandResults = navResult.commands.map((cmd, idx) => ({
368
+ command: {
369
+ lineNumber: directive.lineNumber,
370
+ input: cmd,
371
+ expectedOutput: [],
372
+ assertions: []
373
+ },
374
+ actualOutput: `(navigated: ${navResult.path[idx] || '?'} -> ${navResult.path[idx + 1] || directive.target})`,
375
+ actualEvents: [],
376
+ passed: true,
377
+ expectedFailure: false,
378
+ skipped: false,
379
+ assertionResults: []
380
+ }));
381
+ return { nextIndex: currentIndex + 1, commandResults };
382
+ }
383
+ case 'save': {
384
+ if (!world || !directive.saveName) {
385
+ return { nextIndex: currentIndex + 1, error: 'SAVE requires world model and save name' };
386
+ }
387
+ if (verbose) {
388
+ console.log(`[$save ${directive.saveName}]`);
389
+ }
390
+ try {
391
+ // Get save directory from options or use default
392
+ const savesDir = options.savesDirectory || './saves';
393
+ // Ensure directory exists
394
+ if (!fs.existsSync(savesDir)) {
395
+ fs.mkdirSync(savesDir, { recursive: true });
396
+ }
397
+ // Serialize world state
398
+ const worldState = world.toJSON();
399
+ // Write to file
400
+ const savePath = path.join(savesDir, `${directive.saveName}.json`);
401
+ fs.writeFileSync(savePath, worldState, 'utf-8');
402
+ if (verbose) {
403
+ console.log(` Saved to: ${savePath}`);
404
+ }
405
+ }
406
+ catch (e) {
407
+ return {
408
+ nextIndex: currentIndex + 1,
409
+ error: `Failed to save "${directive.saveName}": ${e instanceof Error ? e.message : String(e)}`
410
+ };
411
+ }
412
+ return { nextIndex: currentIndex + 1 };
413
+ }
414
+ case 'restore': {
415
+ if (!world || !directive.saveName) {
416
+ return { nextIndex: currentIndex + 1, error: 'RESTORE requires world model and save name' };
417
+ }
418
+ if (verbose) {
419
+ console.log(`[$restore ${directive.saveName}]`);
420
+ }
421
+ try {
422
+ // Get save directory from options or use default
423
+ const savesDir = options.savesDirectory || './saves';
424
+ const savePath = path.join(savesDir, `${directive.saveName}.json`);
425
+ // Check file exists
426
+ if (!fs.existsSync(savePath)) {
427
+ return {
428
+ nextIndex: currentIndex + 1,
429
+ error: `Save file not found: ${savePath}`
430
+ };
431
+ }
432
+ // Read and restore world state
433
+ const worldState = fs.readFileSync(savePath, 'utf-8');
434
+ world.loadJSON(worldState);
435
+ if (verbose) {
436
+ console.log(` Restored from: ${savePath}`);
437
+ }
438
+ }
439
+ catch (e) {
440
+ return {
441
+ nextIndex: currentIndex + 1,
442
+ error: `Failed to restore "${directive.saveName}": ${e instanceof Error ? e.message : String(e)}`
443
+ };
444
+ }
445
+ return { nextIndex: currentIndex + 1 };
446
+ }
447
+ case 'test-command': {
448
+ // Execute ext-testing command ($teleport, $take, $kill, etc.)
449
+ if (!directive.testCommand) {
450
+ return { nextIndex: currentIndex + 1, error: 'Test command missing' };
451
+ }
452
+ if (!options.testingExtension) {
453
+ // No testing extension available - warn and skip
454
+ if (verbose) {
455
+ console.log(` [${directive.testCommand}] - skipped (no testing extension)`);
456
+ }
457
+ return { nextIndex: currentIndex + 1 };
458
+ }
459
+ if (!world) {
460
+ return { nextIndex: currentIndex + 1, error: 'World model not available for test command' };
461
+ }
462
+ if (verbose) {
463
+ console.log(`[${directive.testCommand}]`);
464
+ }
465
+ try {
466
+ const result = options.testingExtension.executeTestCommand(directive.testCommand, world);
467
+ if (verbose) {
468
+ if (result.output.length > 0) {
469
+ for (const line of result.output) {
470
+ console.log(` ${line}`);
471
+ }
472
+ }
473
+ if (result.error) {
474
+ console.log(` ERROR: ${result.error}`);
475
+ }
476
+ }
477
+ if (!result.success) {
478
+ return {
479
+ nextIndex: currentIndex + 1,
480
+ error: result.error || `Test command failed: ${directive.testCommand}`
481
+ };
482
+ }
483
+ }
484
+ catch (e) {
485
+ return {
486
+ nextIndex: currentIndex + 1,
487
+ error: `Test command error: ${e instanceof Error ? e.message : String(e)}`
488
+ };
489
+ }
490
+ return { nextIndex: currentIndex + 1 };
491
+ }
492
+ default:
493
+ return { nextIndex: currentIndex + 1 };
494
+ }
495
+ }
496
+ /**
497
+ * Handle END directives when in inactive block (for proper nesting)
498
+ */
499
+ async function handleEndDirective(directive, blockStack, currentIndex, items, engine, playerId, options) {
500
+ switch (directive.type) {
501
+ case 'end_if':
502
+ if (blockStack.length > 0 && blockStack[blockStack.length - 1].type === 'if') {
503
+ blockStack.pop();
504
+ return { consumed: true, nextIndex: currentIndex + 1 };
505
+ }
506
+ break;
507
+ case 'end_while':
508
+ if (blockStack.length > 0 && blockStack[blockStack.length - 1].type === 'while') {
509
+ blockStack.pop();
510
+ return { consumed: true, nextIndex: currentIndex + 1 };
511
+ }
512
+ break;
513
+ case 'end_goal':
514
+ if (blockStack.length > 0 && blockStack[blockStack.length - 1].type === 'goal') {
515
+ blockStack.pop();
516
+ return { consumed: true, nextIndex: currentIndex + 1 };
517
+ }
518
+ break;
519
+ // Handle nested block starts within inactive blocks
520
+ case 'if':
521
+ case 'while':
522
+ case 'goal':
523
+ // Push inactive block to maintain proper nesting
524
+ blockStack.push({
525
+ type: directive.type === 'goal' ? 'goal' : directive.type,
526
+ startIndex: currentIndex,
527
+ active: false,
528
+ iterations: 0
529
+ });
530
+ return { consumed: true, nextIndex: currentIndex + 1 };
531
+ }
532
+ return { consumed: false, nextIndex: currentIndex + 1 };
533
+ }
534
+ /**
535
+ * Create a synthetic failed result for a directive error
536
+ */
537
+ function createDirectiveFailResult(directive, error) {
538
+ return {
539
+ command: {
540
+ lineNumber: directive.lineNumber,
541
+ input: `[${directive.type.toUpperCase()}${directive.condition ? ': ' + directive.condition : ''}${directive.target ? ': "' + directive.target + '"' : ''}]`,
542
+ expectedOutput: [],
543
+ assertions: []
544
+ },
545
+ actualOutput: '',
546
+ actualEvents: [],
547
+ passed: false,
548
+ expectedFailure: false,
549
+ skipped: false,
550
+ assertionResults: [],
551
+ error
552
+ };
553
+ }
554
+ /**
555
+ * Run a single command and check assertions
556
+ */
557
+ async function runCommand(command, engine, options) {
558
+ // Check for skip/todo first
559
+ const skipAssertion = command.assertions.find(a => a.type === 'skip' || a.type === 'todo');
560
+ if (skipAssertion) {
561
+ return {
562
+ command,
563
+ actualOutput: '',
564
+ actualEvents: [],
565
+ passed: true,
566
+ expectedFailure: false,
567
+ skipped: true,
568
+ assertionResults: [{
569
+ assertion: skipAssertion,
570
+ passed: true,
571
+ message: skipAssertion.reason || 'Skipped'
572
+ }]
573
+ };
574
+ }
575
+ // Execute the command
576
+ let actualOutput;
577
+ let actualEvents = [];
578
+ let error;
579
+ try {
580
+ const result = await engine.executeCommand(command.input);
581
+ actualOutput = typeof result === 'string' ? result : (engine.getOutput?.() || '');
582
+ // Capture events from the engine (filter out system.* debug events)
583
+ if (engine.lastEvents) {
584
+ actualEvents = engine.lastEvents
585
+ .filter(e => !e.type.startsWith('system.'))
586
+ .map(e => ({
587
+ type: e.type,
588
+ data: e.data || {}
589
+ }));
590
+ }
591
+ }
592
+ catch (e) {
593
+ actualOutput = '';
594
+ error = e instanceof Error ? e.message : String(e);
595
+ }
596
+ // Normalize output for comparison
597
+ const normalizedActual = normalizeOutput(actualOutput);
598
+ const normalizedExpected = normalizeOutput(command.expectedOutput.join('\n'));
599
+ // Check all assertions
600
+ const assertionResults = [];
601
+ let allPassed = true;
602
+ for (const assertion of command.assertions) {
603
+ const result = checkAssertion(assertion, normalizedActual, normalizedExpected, actualEvents, engine.world);
604
+ assertionResults.push(result);
605
+ if (!result.passed) {
606
+ allPassed = false;
607
+ }
608
+ }
609
+ // Check for expected failure
610
+ const failAssertion = command.assertions.find(a => a.type === 'fail');
611
+ const expectedFailure = failAssertion !== undefined;
612
+ // For [FAIL] assertions, invert the logic
613
+ if (expectedFailure) {
614
+ return {
615
+ command,
616
+ actualOutput,
617
+ actualEvents,
618
+ passed: !allPassed, // Pass if assertions failed (as expected)
619
+ expectedFailure: true,
620
+ skipped: false,
621
+ assertionResults,
622
+ error
623
+ };
624
+ }
625
+ return {
626
+ command,
627
+ actualOutput,
628
+ actualEvents,
629
+ passed: allPassed && !error,
630
+ expectedFailure: false,
631
+ skipped: false,
632
+ assertionResults,
633
+ error
634
+ };
635
+ }
636
+ /**
637
+ * Check a single assertion against actual output, events, and world state
638
+ */
639
+ function checkAssertion(assertion, actualOutput, expectedOutput, events, world) {
640
+ switch (assertion.type) {
641
+ case 'ok':
642
+ // Exact match (after normalization)
643
+ const matches = actualOutput === expectedOutput;
644
+ return {
645
+ assertion,
646
+ passed: matches,
647
+ message: matches ? undefined : `Output did not match expected`
648
+ };
649
+ case 'ok-contains':
650
+ const contains = actualOutput.toLowerCase().includes(assertion.value.toLowerCase());
651
+ return {
652
+ assertion,
653
+ passed: contains,
654
+ message: contains ? undefined : `Output does not contain "${assertion.value}"`
655
+ };
656
+ case 'ok-not-contains':
657
+ const notContains = !actualOutput.toLowerCase().includes(assertion.value.toLowerCase());
658
+ return {
659
+ assertion,
660
+ passed: notContains,
661
+ message: notContains ? undefined : `Output should not contain "${assertion.value}"`
662
+ };
663
+ case 'ok-matches':
664
+ const regexMatches = assertion.pattern.test(actualOutput);
665
+ return {
666
+ assertion,
667
+ passed: regexMatches,
668
+ message: regexMatches ? undefined : `Output does not match pattern ${assertion.pattern}`
669
+ };
670
+ case 'fail':
671
+ // This is handled at the command level
672
+ return {
673
+ assertion,
674
+ passed: false,
675
+ message: assertion.reason
676
+ };
677
+ case 'skip':
678
+ case 'todo':
679
+ return {
680
+ assertion,
681
+ passed: true,
682
+ message: assertion.reason
683
+ };
684
+ case 'event-count': {
685
+ const expected = assertion.eventCount || 0;
686
+ const actual = events.length;
687
+ const countMatches = actual === expected;
688
+ return {
689
+ assertion,
690
+ passed: countMatches,
691
+ message: countMatches ? undefined : `Expected ${expected} events, got ${actual}`
692
+ };
693
+ }
694
+ case 'event-assert': {
695
+ return checkEventAssertion(assertion, events);
696
+ }
697
+ case 'state-assert': {
698
+ return checkStateAssertion(assertion, world);
699
+ }
700
+ default:
701
+ return {
702
+ assertion,
703
+ passed: false,
704
+ message: `Unknown assertion type: ${assertion.type}`
705
+ };
706
+ }
707
+ }
708
+ /**
709
+ * Check an event assertion (assertTrue/assertFalse for event existence/properties)
710
+ */
711
+ function checkEventAssertion(assertion, events) {
712
+ const { assertTrue, eventPosition, eventType, eventData } = assertion;
713
+ // Helper to check if an event matches
714
+ const eventMatches = (event) => {
715
+ if (event.type !== eventType)
716
+ return false;
717
+ if (eventData) {
718
+ for (const [key, value] of Object.entries(eventData)) {
719
+ if (event.data[key] !== value)
720
+ return false;
721
+ }
722
+ }
723
+ return true;
724
+ };
725
+ let found = false;
726
+ let actualEvent;
727
+ if (eventPosition !== undefined) {
728
+ // Check specific position (1-based)
729
+ const index = eventPosition - 1;
730
+ if (index >= 0 && index < events.length) {
731
+ actualEvent = events[index];
732
+ found = eventMatches(actualEvent);
733
+ }
734
+ }
735
+ else {
736
+ // Check any position
737
+ actualEvent = events.find(eventMatches);
738
+ found = actualEvent !== undefined;
739
+ }
740
+ // Apply assertTrue/assertFalse logic
741
+ const passed = assertTrue ? found : !found;
742
+ // Build message
743
+ let message;
744
+ if (!passed) {
745
+ if (assertTrue) {
746
+ // Expected to find but didn't
747
+ if (eventPosition !== undefined) {
748
+ const actualAtPos = events[eventPosition - 1];
749
+ if (actualAtPos) {
750
+ message = `Event ${eventPosition}: expected ${eventType}, got ${actualAtPos.type}`;
751
+ if (eventData) {
752
+ message += `. Expected data: ${JSON.stringify(eventData)}, got: ${JSON.stringify(actualAtPos.data)}`;
753
+ }
754
+ }
755
+ else {
756
+ message = `Event ${eventPosition}: position out of range (${events.length} events total)`;
757
+ }
758
+ }
759
+ else {
760
+ message = `No event matching ${eventType}`;
761
+ if (eventData) {
762
+ message += ` with ${JSON.stringify(eventData)}`;
763
+ }
764
+ message += `. Events: ${events.map(e => e.type).join(', ')}`;
765
+ }
766
+ }
767
+ else {
768
+ // Expected NOT to find but did
769
+ message = `Event ${eventType} should not exist but was found`;
770
+ if (eventData) {
771
+ message += ` with matching data ${JSON.stringify(eventData)}`;
772
+ }
773
+ }
774
+ }
775
+ return { assertion, passed, message };
776
+ }
777
+ /**
778
+ * Check a state assertion against the world model
779
+ */
780
+ function checkStateAssertion(assertion, world) {
781
+ const { assertTrue, stateExpression } = assertion;
782
+ if (!world) {
783
+ return {
784
+ assertion,
785
+ passed: false,
786
+ message: 'World model not available for state assertions'
787
+ };
788
+ }
789
+ if (!stateExpression) {
790
+ return {
791
+ assertion,
792
+ passed: false,
793
+ message: 'No state expression provided'
794
+ };
795
+ }
796
+ try {
797
+ const result = evaluateStateExpression(stateExpression, world);
798
+ const passed = assertTrue ? result.matches : !result.matches;
799
+ let message;
800
+ if (!passed) {
801
+ if (assertTrue) {
802
+ message = `State assertion failed: ${stateExpression}. ${result.details || ''}`;
803
+ }
804
+ else {
805
+ message = `State assertion should be false but was true: ${stateExpression}`;
806
+ }
807
+ }
808
+ return { assertion, passed, message };
809
+ }
810
+ catch (e) {
811
+ return {
812
+ assertion,
813
+ passed: false,
814
+ message: `Error evaluating state expression: ${e instanceof Error ? e.message : String(e)}`
815
+ };
816
+ }
817
+ }
818
+ /**
819
+ * Evaluate a state expression against the world model
820
+ * Supports: entity.property = value, entity.property != value,
821
+ * collection contains item, collection not-contains item
822
+ */
823
+ function evaluateStateExpression(expression, world) {
824
+ // Parse "entity.property = value" or "entity.property != value"
825
+ const equalityMatch = expression.match(/^(\w+)\.(\w+)\s*(=|!=)\s*(.+)$/);
826
+ if (equalityMatch) {
827
+ const [, entityName, property, operator, expectedValue] = equalityMatch;
828
+ // Find entity by name
829
+ const entity = findEntity(entityName, world);
830
+ if (!entity) {
831
+ return { matches: false, details: `Entity "${entityName}" not found` };
832
+ }
833
+ // Get property value
834
+ const actualValue = getEntityProperty(entity, property, world);
835
+ const expectedResolved = resolveValue(expectedValue.trim(), world);
836
+ const isEqual = actualValue === expectedResolved ||
837
+ (actualValue?.id && actualValue.id === expectedResolved) ||
838
+ (typeof expectedResolved === 'string' && actualValue?.id === expectedResolved);
839
+ if (operator === '=') {
840
+ return {
841
+ matches: isEqual,
842
+ details: isEqual ? undefined : `${entityName}.${property} is "${actualValue?.id || actualValue}", expected "${expectedResolved}"`
843
+ };
844
+ }
845
+ else {
846
+ return {
847
+ matches: !isEqual,
848
+ details: !isEqual ? undefined : `${entityName}.${property} should not be "${expectedResolved}"`
849
+ };
850
+ }
851
+ }
852
+ // Parse "collection contains item" or "collection not-contains item"
853
+ const containsMatch = expression.match(/^(\w+)\.(\w+)\s+(contains|not-contains)\s+(.+)$/);
854
+ if (containsMatch) {
855
+ const [, entityName, property, operator, itemName] = containsMatch;
856
+ const entity = findEntity(entityName, world);
857
+ if (!entity) {
858
+ return { matches: false, details: `Entity "${entityName}" not found` };
859
+ }
860
+ const collection = getEntityProperty(entity, property, world);
861
+ if (!Array.isArray(collection)) {
862
+ return { matches: false, details: `${entityName}.${property} is not a collection` };
863
+ }
864
+ const item = findEntity(itemName.trim(), world);
865
+ const itemId = item?.id || itemName.trim();
866
+ const hasItem = collection.some((c) => c === itemId || c?.id === itemId);
867
+ if (operator === 'contains') {
868
+ return { matches: hasItem, details: hasItem ? undefined : `${entityName}.${property} does not contain "${itemName}"` };
869
+ }
870
+ else {
871
+ return { matches: !hasItem, details: !hasItem ? undefined : `${entityName}.${property} should not contain "${itemName}"` };
872
+ }
873
+ }
874
+ return { matches: false, details: `Could not parse expression: ${expression}` };
875
+ }
876
+ /**
877
+ * Find an entity by name in the world model
878
+ */
879
+ function findEntity(name, world) {
880
+ // Try findEntityByName first
881
+ if (world.findEntityByName) {
882
+ const entity = world.findEntityByName(name);
883
+ if (entity)
884
+ return entity;
885
+ }
886
+ // Try getEntity/getEntityById
887
+ if (world.getEntity) {
888
+ const entity = world.getEntity(name);
889
+ if (entity)
890
+ return entity;
891
+ }
892
+ if (world.getEntityById) {
893
+ const entity = world.getEntityById(name);
894
+ if (entity)
895
+ return entity;
896
+ }
897
+ // Search all entities by name/alias
898
+ if (world.getAllEntities) {
899
+ const entities = world.getAllEntities();
900
+ for (const entity of entities) {
901
+ if (entity.name === name || entity.id === name)
902
+ return entity;
903
+ // Check identity trait for aliases
904
+ const identity = entity.traits?.identity || entity.get?.('IdentityTrait');
905
+ if (identity) {
906
+ if (identity.name === name)
907
+ return entity;
908
+ if (identity.aliases?.includes(name))
909
+ return entity;
910
+ }
911
+ }
912
+ }
913
+ return null;
914
+ }
915
+ /**
916
+ * Get a property from an entity (world needed for spatial queries)
917
+ */
918
+ function getEntityProperty(entity, property, world) {
919
+ // Special handling for location - use world.getLocation()
920
+ if (property === 'location') {
921
+ if (world?.getLocation) {
922
+ return world.getLocation(entity.id);
923
+ }
924
+ return entity.location || entity.containerId;
925
+ }
926
+ // Special handling for contents - use world.getContents()
927
+ if (property === 'contents' || property === 'inventory') {
928
+ if (world?.getContents) {
929
+ return world.getContents(entity.id);
930
+ }
931
+ return entity.contents || entity.inventory || [];
932
+ }
933
+ // Direct property access
934
+ if (property in entity) {
935
+ return entity[property];
936
+ }
937
+ // Check traits
938
+ if (entity.traits && property in entity.traits) {
939
+ return entity.traits[property];
940
+ }
941
+ return undefined;
942
+ }
943
+ /**
944
+ * Resolve a value (could be entity name, literal, etc.)
945
+ */
946
+ function resolveValue(value, world) {
947
+ // Check if it's an entity reference
948
+ const entity = findEntity(value, world);
949
+ if (entity) {
950
+ return entity.id;
951
+ }
952
+ // Check for special values
953
+ if (value === 'null' || value === 'undefined' || value === 'nowhere') {
954
+ return undefined;
955
+ }
956
+ if (value === 'true')
957
+ return true;
958
+ if (value === 'false')
959
+ return false;
960
+ // Return as string
961
+ return value;
962
+ }
963
+ /**
964
+ * Normalize output for comparison
965
+ * - Trim whitespace
966
+ * - Normalize line endings
967
+ * - Collapse multiple spaces
968
+ */
969
+ function normalizeOutput(output) {
970
+ return output
971
+ .replace(/\r\n/g, '\n')
972
+ .split('\n')
973
+ .map(line => line.trim())
974
+ .join('\n')
975
+ .trim();
976
+ }
977
+ //# sourceMappingURL=runner.js.map