@rhost/testkit 0.1.0 → 1.3.1

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 (110) hide show
  1. package/README.md +393 -5
  2. package/ROADMAP.md +241 -0
  3. package/dist/benchmark.d.ts +44 -0
  4. package/dist/benchmark.d.ts.map +1 -0
  5. package/dist/benchmark.js +118 -0
  6. package/dist/benchmark.js.map +1 -0
  7. package/dist/cli/deploy.d.ts +2 -0
  8. package/dist/cli/deploy.d.ts.map +1 -0
  9. package/dist/cli/deploy.js +120 -0
  10. package/dist/cli/deploy.js.map +1 -0
  11. package/dist/cli/fmt.d.ts +2 -0
  12. package/dist/cli/fmt.d.ts.map +1 -0
  13. package/dist/cli/fmt.js +119 -0
  14. package/dist/cli/fmt.js.map +1 -0
  15. package/dist/cli/index.d.ts +3 -0
  16. package/dist/cli/index.d.ts.map +1 -0
  17. package/dist/cli/index.js +81 -0
  18. package/dist/cli/index.js.map +1 -0
  19. package/dist/cli/init.d.ts +2 -0
  20. package/dist/cli/init.d.ts.map +1 -0
  21. package/dist/cli/init.js +210 -0
  22. package/dist/cli/init.js.map +1 -0
  23. package/dist/cli/validate.d.ts +2 -0
  24. package/dist/cli/validate.d.ts.map +1 -0
  25. package/dist/cli/validate.js +126 -0
  26. package/dist/cli/validate.js.map +1 -0
  27. package/dist/cli/watch.d.ts +2 -0
  28. package/dist/cli/watch.d.ts.map +1 -0
  29. package/dist/cli/watch.js +136 -0
  30. package/dist/cli/watch.js.map +1 -0
  31. package/dist/client.d.ts +48 -0
  32. package/dist/client.d.ts.map +1 -1
  33. package/dist/client.js +113 -30
  34. package/dist/client.js.map +1 -1
  35. package/dist/container.d.ts.map +1 -1
  36. package/dist/container.js +1 -1
  37. package/dist/container.js.map +1 -1
  38. package/dist/deployer.d.ts +86 -0
  39. package/dist/deployer.d.ts.map +1 -0
  40. package/dist/deployer.js +154 -0
  41. package/dist/deployer.js.map +1 -0
  42. package/dist/expect.d.ts +27 -1
  43. package/dist/expect.d.ts.map +1 -1
  44. package/dist/expect.js +47 -2
  45. package/dist/expect.js.map +1 -1
  46. package/dist/index.d.ts +10 -3
  47. package/dist/index.d.ts.map +1 -1
  48. package/dist/index.js +39 -1
  49. package/dist/index.js.map +1 -1
  50. package/dist/preflight.d.ts +70 -0
  51. package/dist/preflight.d.ts.map +1 -0
  52. package/dist/preflight.js +121 -0
  53. package/dist/preflight.js.map +1 -0
  54. package/dist/reporter.d.ts +2 -0
  55. package/dist/reporter.d.ts.map +1 -1
  56. package/dist/reporter.js +24 -1
  57. package/dist/reporter.js.map +1 -1
  58. package/dist/runner.d.ts +83 -2
  59. package/dist/runner.d.ts.map +1 -1
  60. package/dist/runner.js +137 -8
  61. package/dist/runner.js.map +1 -1
  62. package/dist/snapshots.d.ts +84 -0
  63. package/dist/snapshots.d.ts.map +1 -0
  64. package/dist/snapshots.js +230 -0
  65. package/dist/snapshots.js.map +1 -0
  66. package/dist/validator/builtins.d.ts +18 -0
  67. package/dist/validator/builtins.d.ts.map +1 -0
  68. package/dist/validator/builtins.js +265 -0
  69. package/dist/validator/builtins.js.map +1 -0
  70. package/dist/validator/checker.d.ts +13 -0
  71. package/dist/validator/checker.d.ts.map +1 -0
  72. package/dist/validator/checker.js +111 -0
  73. package/dist/validator/checker.js.map +1 -0
  74. package/dist/validator/clobber.d.ts +7 -0
  75. package/dist/validator/clobber.d.ts.map +1 -0
  76. package/dist/validator/clobber.js +102 -0
  77. package/dist/validator/clobber.js.map +1 -0
  78. package/dist/validator/compat.d.ts +19 -0
  79. package/dist/validator/compat.d.ts.map +1 -0
  80. package/dist/validator/compat.js +58 -0
  81. package/dist/validator/compat.js.map +1 -0
  82. package/dist/validator/formatter.d.ts +21 -0
  83. package/dist/validator/formatter.d.ts.map +1 -0
  84. package/dist/validator/formatter.js +120 -0
  85. package/dist/validator/formatter.js.map +1 -0
  86. package/dist/validator/index.d.ts +57 -0
  87. package/dist/validator/index.d.ts.map +1 -0
  88. package/dist/validator/index.js +133 -0
  89. package/dist/validator/index.js.map +1 -0
  90. package/dist/validator/parser.d.ts +16 -0
  91. package/dist/validator/parser.d.ts.map +1 -0
  92. package/dist/validator/parser.js +260 -0
  93. package/dist/validator/parser.js.map +1 -0
  94. package/dist/validator/tokenizer.d.ts +28 -0
  95. package/dist/validator/tokenizer.d.ts.map +1 -0
  96. package/dist/validator/tokenizer.js +174 -0
  97. package/dist/validator/tokenizer.js.map +1 -0
  98. package/dist/validator/types.d.ts +55 -0
  99. package/dist/validator/types.d.ts.map +1 -0
  100. package/dist/validator/types.js +6 -0
  101. package/dist/validator/types.js.map +1 -0
  102. package/dist/watcher.d.ts +44 -0
  103. package/dist/watcher.d.ts.map +1 -0
  104. package/dist/watcher.js +297 -0
  105. package/dist/watcher.js.map +1 -0
  106. package/dist/world.d.ts +79 -0
  107. package/dist/world.d.ts.map +1 -1
  108. package/dist/world.js +167 -1
  109. package/dist/world.js.map +1 -1
  110. package/package.json +19 -3
package/README.md CHANGED
@@ -23,13 +23,20 @@ npm install @rhost/testkit
23
23
  - [Quick start](#quick-start)
24
24
  - [API reference — RhostRunner](#api-reference--rhostrunner)
25
25
  - [API reference — RhostExpect](#api-reference--rhostexpect)
26
+ - [Snapshot testing](#snapshot-testing)
26
27
  - [API reference — RhostWorld](#api-reference--rhostworld)
27
28
  - [API reference — RhostClient](#api-reference--rhostclient)
28
29
  - [API reference — RhostContainer](#api-reference--rhostcontainer)
30
+ - [Offline validator](#offline-validator)
31
+ - [Softcode formatter](#softcode-formatter)
32
+ - [Benchmark mode](#benchmark-mode)
33
+ - [Watch mode](#watch-mode)
34
+ - [CI/CD templates](#cicd-templates)
29
35
  - [MUSH output format](#mush-output-format)
30
36
  - [Using with LLM skills](#using-with-llm-skills)
31
37
  - [Environment variables](#environment-variables)
32
38
  - [Examples](#examples)
39
+ - [Roadmap](#roadmap)
33
40
 
34
41
  ---
35
42
 
@@ -39,7 +46,14 @@ npm install @rhost/testkit
39
46
 
40
47
  - **Eval** softcode expressions and capture their output
41
48
  - **Assert** results with a Jest-like `expect()` API and MUSH-aware matchers
42
- - **Manage fixtures** — create objects, set attributes, and auto-destroy everything after each test
49
+ - **Snapshot test** softcode output first run writes, subsequent runs compare with a diff on mismatch
50
+ - **Preview** raw server output exactly as a MUSH client sees it — ANSI colours and all
51
+ - **Manage fixtures** — create/destroy objects, set attributes, force commands, send mail, and more
52
+ - **Validate softcode offline** — catch syntax errors, wrong arg counts, register clobber risks, and dialect compatibility without a server
53
+ - **Format softcode** — normalize whitespace and optionally indent nested calls (`rhost-testkit fmt`)
54
+ - **Benchmark** expressions — measure median / p95 / p99 latency per softcode call against a live server
55
+ - **Watch mode** — re-run tests on save with a 300ms debounce
56
+ - **Generate CI/CD workflows** — one command to get GitHub Actions or GitLab CI configured
43
57
  - **Spin up RhostMUSH in Docker** for isolated, reproducible CI runs
44
58
  - **Report** results with a pretty, indented pass/fail tree
45
59
 
@@ -251,9 +265,10 @@ interface SuiteContext {
251
265
 
252
266
  ```typescript
253
267
  interface TestContext {
254
- expect(expression: string): RhostExpect // create an expect for a softcode expression
255
- client: RhostClient // the live MUSH connection
256
- world: RhostWorld // fresh per-test fixture manager (auto-cleaned)
268
+ expect(expression: string): RhostExpect // create an expect for a softcode expression
269
+ preview(input: string, opts?: PreviewOptions): Promise<string> // print raw server output
270
+ client: RhostClient // the live MUSH connection
271
+ world: RhostWorld // fresh per-test fixture manager (auto-cleaned)
257
272
  }
258
273
  ```
259
274
 
@@ -323,6 +338,7 @@ await expect('add(1,1)').not.toBeError();
323
338
  | `.toBeDbref()` | Result matches `/^#\d+$/` (a positive object reference). |
324
339
  | `.toContainWord(word: string, sep?: string)` | Word is present in the space-delimited list (or custom separator). |
325
340
  | `.toHaveWordCount(n: number, sep?: string)` | List has exactly `n` words. Empty string has 0. |
341
+ | `.toMatchSnapshot()` | Compare output against a stored snapshot; writes on first run. |
326
342
 
327
343
  ### Failure message format
328
344
 
@@ -353,6 +369,45 @@ await client.disconnect();
353
369
 
354
370
  ---
355
371
 
372
+ ## Snapshot testing
373
+
374
+ Snapshot tests lock in the output of a softcode expression. The first run writes the value to a `.snap` file; subsequent runs compare against it and diff on mismatch.
375
+
376
+ ```typescript
377
+ runner.describe('iter output', ({ it }) => {
378
+ it('produces the right sequence', async ({ expect }) => {
379
+ await expect('iter(lnum(1,5),##)').toMatchSnapshot();
380
+ });
381
+ });
382
+ ```
383
+
384
+ On first run, a file like `__snapshots__/my-tests.test.ts.snap` is created:
385
+ ```json
386
+ {
387
+ "iter output > produces the right sequence: 1": "1 2 3 4 5"
388
+ }
389
+ ```
390
+
391
+ On subsequent runs the stored value is compared. If it differs, the test fails with a line-by-line diff:
392
+ ```
393
+ - 1 2 3 4 5
394
+ + 1 2 3 4 5 6
395
+ ```
396
+
397
+ To update all snapshots (e.g. after an intentional change):
398
+
399
+ ```bash
400
+ RHOST_UPDATE_SNAPSHOTS=1 npx ts-node my-tests.test.ts
401
+ ```
402
+
403
+ Or pass `updateSnapshots: true` to `runner.run()`.
404
+
405
+ The runner prints a snapshot summary after each run: `Snapshots: 3 passed, 1 written, 0 updated`.
406
+
407
+ **Snapshot key format:** `"Suite > Sub-suite > Test name: N"` — where `N` is the 1-based call count within the test. Calling `.toMatchSnapshot()` twice in one test produces two separate keys.
408
+
409
+ ---
410
+
356
411
  ## API reference — RhostWorld
357
412
 
358
413
  `RhostWorld` manages MUSH object fixtures. Objects created through `world` are registered and destroyed automatically in `world.cleanup()`. In the runner, `cleanup()` is called after every `it()` test — even on failure.
@@ -400,6 +455,51 @@ await world.trigger(dbref: string, attr: string, args?: string): Promise<string[
400
455
  ```
401
456
  Triggers `@trigger dbref/attr=args`. Returns all output lines captured before the sentinel.
402
457
 
458
+ ```typescript
459
+ await world.pemit(target: string, msg: string): Promise<void>
460
+ ```
461
+ Emits a message to a target: `@pemit target=msg`.
462
+
463
+ ```typescript
464
+ await world.remit(room: string, msg: string): Promise<void>
465
+ ```
466
+ Emits a message to all objects in a room: `@remit room=msg`.
467
+
468
+ ```typescript
469
+ await world.force(actor: string, cmd: string): Promise<void>
470
+ ```
471
+ Forces an object to run a command: `@force actor=cmd`.
472
+
473
+ ```typescript
474
+ await world.parent(child: string, parent: string): Promise<void>
475
+ ```
476
+ Sets a parent object: `@parent child=parent`.
477
+
478
+ ```typescript
479
+ await world.zone(name: string): Promise<string>
480
+ ```
481
+ Creates a zone room via `@dig name` and sets `INHERIT_ZONE`. Returns the room dbref. Registers for cleanup.
482
+
483
+ ```typescript
484
+ await world.addToChannel(dbref: string, chan: string): Promise<void>
485
+ ```
486
+ Adds an object to a channel: `@channel/add chan=dbref`.
487
+
488
+ ```typescript
489
+ await world.grantQuota(dbref: string, n: number): Promise<void>
490
+ ```
491
+ Sets the build quota for an object: `@quota/set dbref=n`.
492
+
493
+ ```typescript
494
+ await world.wait(ms: number): Promise<void>
495
+ ```
496
+ Pauses the test for `ms` milliseconds. Plain JavaScript delay — not a MUSH `@wait`.
497
+
498
+ ```typescript
499
+ await world.mail(to: string, subj: string, body: string): Promise<void>
500
+ ```
501
+ Sends in-game mail: `@mail to=subj/body`.
502
+
403
503
  ```typescript
404
504
  await world.destroy(dbref: string): Promise<void>
405
505
  ```
@@ -466,7 +566,7 @@ Establishes the TCP connection and drains the welcome banner.
466
566
  ```typescript
467
567
  await client.login(username: string, password: string): Promise<void>
468
568
  ```
469
- Sends `connect <username> <password>` and waits for the login sentinel. Throws `RangeError` if either credential contains `\n` or `\r`.
569
+ Sends `connect <username> <password>` and waits for the login sentinel. Throws `RangeError` if the username contains `\n`, `\r`, spaces, or tabs (which would split the MUSH connect command), or if the password contains `\n` or `\r`.
470
570
 
471
571
  ```typescript
472
572
  await client.eval(expression: string, timeout?: number): Promise<string>
@@ -478,6 +578,32 @@ await client.command(cmd: string, timeout?: number): Promise<string[]>
478
578
  ```
479
579
  Sends a MUSH command and captures all output lines until the sentinel. Returns an array of lines (may be empty).
480
580
 
581
+ ```typescript
582
+ await client.preview(input: string, options?: PreviewOptions): Promise<string>
583
+ ```
584
+ Evaluate an expression or run a command and print the raw server output to stdout exactly as a MUSH client receives it — ANSI colours, formatting codes, and all. Output is rendered in a labelled frame. Returns the raw string so you can still assert on it.
585
+
586
+ **`PreviewOptions`:**
587
+
588
+ | Option | Type | Default | Description |
589
+ |--------|------|---------|-------------|
590
+ | `mode` | `'eval' \| 'command'` | `'eval'` | `'eval'` wraps in `think`; `'command'` sends as a raw MUSH command |
591
+ | `label` | `string` | the input string | Custom frame header label |
592
+ | `timeout` | `number` | client default | Per-call timeout (ms) |
593
+ | `print` | `boolean` | `true` | Set `false` to suppress stdout and only use the return value |
594
+
595
+ ```typescript
596
+ // Softcode expression — see the raw colour output
597
+ await preview('ansi(rh,CRITICAL HIT!)');
598
+
599
+ // Room description as a player sees it
600
+ await preview('look here', { mode: 'command' });
601
+
602
+ // Assert without printing
603
+ const raw = await preview('ansi(b,test)', { print: false });
604
+ expect(stripAnsi(raw)).toBe('test');
605
+ ```
606
+
481
607
  ```typescript
482
608
  client.onLine(handler: (line: string) => void): void
483
609
  client.offLine(handler: (line: string) => void): void
@@ -579,6 +705,242 @@ process.exit(result.failed > 0 ? 1 : 0);
579
705
 
580
706
  ---
581
707
 
708
+ ## Offline validator
709
+
710
+ Validate softcode expressions without a server connection. Catches structural errors (unbalanced parens/brackets), wrong argument counts, and unknown built-in functions.
711
+
712
+ ### Programmatic API
713
+
714
+ ```typescript
715
+ import { validate, validateFile } from '@rhost/testkit/validator';
716
+
717
+ const result = validate('add(2,3)');
718
+ // result.valid => true
719
+ // result.diagnostics => []
720
+
721
+ const bad = validate('add(2,3');
722
+ // bad.valid => false
723
+ // bad.diagnostics[0].code => 'E001'
724
+ // bad.diagnostics[0].message => "Unclosed '(' ..."
725
+ ```
726
+
727
+ ```typescript
728
+ const result = validateFile('./mycode.mush');
729
+ ```
730
+
731
+ ### CLI
732
+
733
+ ```bash
734
+ npx rhost-testkit validate "add(2,3)"
735
+ npx rhost-testkit validate --file mycode.mush
736
+ npx rhost-testkit validate --json "abs(1,2)" # machine-readable output
737
+ ```
738
+
739
+ Exit code `0` = valid (warnings allowed), `1` = one or more errors.
740
+
741
+ ### Diagnostic codes
742
+
743
+ | Code | Severity | Meaning |
744
+ |------|----------|---------|
745
+ | `E001` | error | Unclosed `(` |
746
+ | `E002` | error | Unexpected `)` |
747
+ | `E003` | error | Unclosed `[` |
748
+ | `E004` | error | Unexpected `]` |
749
+ | `E006` | error | Too few arguments for known built-in |
750
+ | `E007` | error | Too many arguments for known built-in |
751
+ | `W001` | warning | Empty expression |
752
+ | `W002` | warning | Empty argument (e.g. `add(,3)`) |
753
+ | `W003` | warning | Deprecated function |
754
+ | `W005` | warning | Unknown function name (may be a UDF) |
755
+ | `W006` | warning | Register clobber risk — `setq()` inside a loop body may overwrite registers in concurrent iterations |
756
+
757
+ ### Dialect compatibility report
758
+
759
+ Report which functions are portable across MUSH platforms (RhostMUSH, PennMUSH, TinyMUX):
760
+
761
+ ```typescript
762
+ import { compatibilityReport } from '@rhost/testkit';
763
+
764
+ const report = compatibilityReport('json(get,key)');
765
+ // report.portable => false
766
+ // report.restricted => [{ name: 'json', platforms: ['rhost'] }]
767
+ ```
768
+
769
+ ```bash
770
+ npx rhost-testkit validate --compat mycode.mush
771
+ ```
772
+
773
+ ### Register clobber analysis
774
+
775
+ `%q0`–`%q9` registers are scoped per queue entry. The validator warns when `setq()` appears inside a loop body (`iter()`, `parse()`, `map()`, etc.) where concurrent invocations can silently overwrite each other's registers:
776
+
777
+ ```bash
778
+ $ rhost-testkit validate --file combat.mush
779
+
780
+ WARNING W006: register clobber risk
781
+ setq() writes %q0 inside iter() at offset 14.
782
+ Wrap in localize() to scope registers to each iteration.
783
+ ```
784
+
785
+ ---
786
+
787
+ ## Softcode formatter
788
+
789
+ Normalize whitespace in softcode files — strips extra spaces around `(`, `,`, `)` while preserving interior argument text.
790
+
791
+ ### CLI
792
+
793
+ ```bash
794
+ # Format a file in-place
795
+ npx rhost-testkit fmt mycode.mush
796
+
797
+ # Check without writing (exit 1 if not formatted — useful in CI)
798
+ npx rhost-testkit fmt --check mycode.mush
799
+
800
+ # Indent nested function calls for human readability
801
+ npx rhost-testkit fmt --pretty mycode.mush
802
+
803
+ # Normalize function names to lowercase
804
+ npx rhost-testkit fmt --lowercase mycode.mush
805
+
806
+ # Format from stdin
807
+ echo "add( 2, 3 )" | npx rhost-testkit fmt
808
+ ```
809
+
810
+ ### Programmatic API
811
+
812
+ ```typescript
813
+ import { format } from '@rhost/testkit';
814
+
815
+ const result = format('add( 2, 3 )');
816
+ // result.formatted => 'add(2,3)'
817
+ // result.changed => true
818
+
819
+ // Pretty mode — indents nested calls for readability (not for upload)
820
+ const pretty = format('add(mul(2,3),4)', { pretty: true });
821
+ // pretty.formatted => 'add(\n mul(2,3),\n 4\n)'
822
+
823
+ // Lowercase function names
824
+ const lower = format('ADD(2,3)', { lowercase: true });
825
+ // lower.formatted => 'add(2,3)'
826
+ ```
827
+
828
+ Interior whitespace within argument text is preserved — `pemit(%#,hello world)` is not changed.
829
+
830
+ ---
831
+
832
+ ## Benchmark mode
833
+
834
+ Profile softcode performance against a live server. Reports median, p95, and p99 latency per expression.
835
+
836
+ ### Programmatic API
837
+
838
+ ```typescript
839
+ import { RhostBenchmark, formatBenchResults } from '@rhost/testkit';
840
+
841
+ const bench = new RhostBenchmark(client);
842
+
843
+ bench
844
+ .add('add(2,3)', { name: 'addition', iterations: 100, warmup: 10 })
845
+ .add('iter(lnum(1,100),##)', { name: 'heavy iter', iterations: 50, warmup: 5 });
846
+
847
+ const results = await bench.run();
848
+ console.log(formatBenchResults(results));
849
+ ```
850
+
851
+ Output:
852
+
853
+ ```
854
+ Benchmark Results
855
+ ────────────────────────────────────────────────────────────────────────
856
+ addition
857
+ iterations: 100 warmup: 10
858
+ median: 4.231ms mean: 4.512ms p95: 7.820ms p99: 12.003ms
859
+ min: 3.901ms max: 14.221ms
860
+
861
+ heavy iter
862
+ iterations: 50 warmup: 5
863
+ median: 18.440ms mean: 19.201ms p95: 31.100ms p99: 38.500ms
864
+ min: 16.002ms max: 41.300ms
865
+ ────────────────────────────────────────────────────────────────────────
866
+ ```
867
+
868
+ ### `runBench` — single expression
869
+
870
+ ```typescript
871
+ import { runBench } from '@rhost/testkit';
872
+
873
+ const result = await runBench(client, 'iter(lnum(1,1000),##)', {
874
+ name: 'heavy iter',
875
+ iterations: 100,
876
+ warmup: 10,
877
+ });
878
+
879
+ console.log(`median: ${result.median.toFixed(2)}ms`);
880
+ console.log(`p95: ${result.p95.toFixed(2)}ms`);
881
+ ```
882
+
883
+ ### `BenchmarkResult` shape
884
+
885
+ ```typescript
886
+ interface BenchmarkResult {
887
+ name: string;
888
+ iterations: number;
889
+ warmup: number;
890
+ samples: number[]; // raw timings in ms, in run order
891
+ mean: number;
892
+ median: number;
893
+ p95: number;
894
+ p99: number;
895
+ min: number;
896
+ max: number;
897
+ }
898
+ ```
899
+
900
+ ---
901
+
902
+ ## Watch mode
903
+
904
+ Re-run test files automatically on save.
905
+
906
+ ```bash
907
+ # Auto-discover *.test.ts / *.spec.ts under the current directory
908
+ npx rhost-testkit watch
909
+
910
+ # Watch specific files
911
+ npx rhost-testkit watch src/__tests__/math.test.ts
912
+
913
+ # Options
914
+ npx rhost-testkit watch --debounce 500 # longer debounce (default: 300ms)
915
+ npx rhost-testkit watch --no-clear # don't clear terminal between runs
916
+ ```
917
+
918
+ TypeScript files are run with `ts-node --transpile-only`. Plain JS files are run with Node directly. Watch mode exits cleanly on Ctrl+C.
919
+
920
+ ---
921
+
922
+ ## CI/CD templates
923
+
924
+ Generate a ready-to-use workflow file for your CI platform with one command:
925
+
926
+ ```bash
927
+ # GitHub Actions → .github/workflows/mush-tests.yml
928
+ npx rhost-testkit init --ci github
929
+
930
+ # GitLab CI → .gitlab-ci.yml
931
+ npx rhost-testkit init --ci gitlab
932
+
933
+ # Overwrite an existing file
934
+ npx rhost-testkit init --ci github --force
935
+ ```
936
+
937
+ The generated file includes:
938
+ - Node.js 20 setup
939
+ - `npm ci` + `npm test`
940
+ - A commented-out block for optional integration tests against a `rhostmush/rhostmush` Docker container — uncomment and set `RHOST_PASS` in your secrets to enable
941
+
942
+ ---
943
+
582
944
  ## MUSH output format
583
945
 
584
946
  Understanding what `client.eval()` returns is essential for writing correct assertions.
@@ -856,6 +1218,32 @@ npm run example:01
856
1218
 
857
1219
  ---
858
1220
 
1221
+ ## Roadmap
1222
+
1223
+ See [ROADMAP.md](./ROADMAP.md) for full details and implementation notes.
1224
+
1225
+ ### ✅ Shipped
1226
+
1227
+ | Version | Features |
1228
+ |---------|----------|
1229
+ | v0.2.0 | Offline validator · Watch mode · Snapshot testing |
1230
+ | v1.0.0 | Extended world API · CI/CD templates |
1231
+ | v1.1.0 | Server pre-flight assertions · Multi-persona test matrix · Side-effect assertion mode |
1232
+ | v1.2.0 | Register clobber analyzer · Deploy pipeline with rollback · Dialect compatibility report |
1233
+ | v1.3.0 | **Softcode formatter** (`rhost-testkit fmt`) · **Benchmark mode** (`RhostBenchmark`) |
1234
+
1235
+ ### Planned
1236
+
1237
+ **Test coverage tracking** — report which attributes on which objects were exercised during a run.
1238
+
1239
+ **Interactive REPL** — persistent connection with readline history and tab-complete for built-in functions.
1240
+
1241
+ **Parallel test execution** — run independent `describe` blocks concurrently on separate connections.
1242
+
1243
+ **Recursion depth profiler** — track max call depth per expression, warn before hitting server recursion limits.
1244
+
1245
+ ---
1246
+
859
1247
  ## License
860
1248
 
861
1249
  MIT