@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.
- package/README.md +393 -5
- package/ROADMAP.md +241 -0
- package/dist/benchmark.d.ts +44 -0
- package/dist/benchmark.d.ts.map +1 -0
- package/dist/benchmark.js +118 -0
- package/dist/benchmark.js.map +1 -0
- package/dist/cli/deploy.d.ts +2 -0
- package/dist/cli/deploy.d.ts.map +1 -0
- package/dist/cli/deploy.js +120 -0
- package/dist/cli/deploy.js.map +1 -0
- package/dist/cli/fmt.d.ts +2 -0
- package/dist/cli/fmt.d.ts.map +1 -0
- package/dist/cli/fmt.js +119 -0
- package/dist/cli/fmt.js.map +1 -0
- package/dist/cli/index.d.ts +3 -0
- package/dist/cli/index.d.ts.map +1 -0
- package/dist/cli/index.js +81 -0
- package/dist/cli/index.js.map +1 -0
- package/dist/cli/init.d.ts +2 -0
- package/dist/cli/init.d.ts.map +1 -0
- package/dist/cli/init.js +210 -0
- package/dist/cli/init.js.map +1 -0
- package/dist/cli/validate.d.ts +2 -0
- package/dist/cli/validate.d.ts.map +1 -0
- package/dist/cli/validate.js +126 -0
- package/dist/cli/validate.js.map +1 -0
- package/dist/cli/watch.d.ts +2 -0
- package/dist/cli/watch.d.ts.map +1 -0
- package/dist/cli/watch.js +136 -0
- package/dist/cli/watch.js.map +1 -0
- package/dist/client.d.ts +48 -0
- package/dist/client.d.ts.map +1 -1
- package/dist/client.js +113 -30
- package/dist/client.js.map +1 -1
- package/dist/container.d.ts.map +1 -1
- package/dist/container.js +1 -1
- package/dist/container.js.map +1 -1
- package/dist/deployer.d.ts +86 -0
- package/dist/deployer.d.ts.map +1 -0
- package/dist/deployer.js +154 -0
- package/dist/deployer.js.map +1 -0
- package/dist/expect.d.ts +27 -1
- package/dist/expect.d.ts.map +1 -1
- package/dist/expect.js +47 -2
- package/dist/expect.js.map +1 -1
- package/dist/index.d.ts +10 -3
- package/dist/index.d.ts.map +1 -1
- package/dist/index.js +39 -1
- package/dist/index.js.map +1 -1
- package/dist/preflight.d.ts +70 -0
- package/dist/preflight.d.ts.map +1 -0
- package/dist/preflight.js +121 -0
- package/dist/preflight.js.map +1 -0
- package/dist/reporter.d.ts +2 -0
- package/dist/reporter.d.ts.map +1 -1
- package/dist/reporter.js +24 -1
- package/dist/reporter.js.map +1 -1
- package/dist/runner.d.ts +83 -2
- package/dist/runner.d.ts.map +1 -1
- package/dist/runner.js +137 -8
- package/dist/runner.js.map +1 -1
- package/dist/snapshots.d.ts +84 -0
- package/dist/snapshots.d.ts.map +1 -0
- package/dist/snapshots.js +230 -0
- package/dist/snapshots.js.map +1 -0
- package/dist/validator/builtins.d.ts +18 -0
- package/dist/validator/builtins.d.ts.map +1 -0
- package/dist/validator/builtins.js +265 -0
- package/dist/validator/builtins.js.map +1 -0
- package/dist/validator/checker.d.ts +13 -0
- package/dist/validator/checker.d.ts.map +1 -0
- package/dist/validator/checker.js +111 -0
- package/dist/validator/checker.js.map +1 -0
- package/dist/validator/clobber.d.ts +7 -0
- package/dist/validator/clobber.d.ts.map +1 -0
- package/dist/validator/clobber.js +102 -0
- package/dist/validator/clobber.js.map +1 -0
- package/dist/validator/compat.d.ts +19 -0
- package/dist/validator/compat.d.ts.map +1 -0
- package/dist/validator/compat.js +58 -0
- package/dist/validator/compat.js.map +1 -0
- package/dist/validator/formatter.d.ts +21 -0
- package/dist/validator/formatter.d.ts.map +1 -0
- package/dist/validator/formatter.js +120 -0
- package/dist/validator/formatter.js.map +1 -0
- package/dist/validator/index.d.ts +57 -0
- package/dist/validator/index.d.ts.map +1 -0
- package/dist/validator/index.js +133 -0
- package/dist/validator/index.js.map +1 -0
- package/dist/validator/parser.d.ts +16 -0
- package/dist/validator/parser.d.ts.map +1 -0
- package/dist/validator/parser.js +260 -0
- package/dist/validator/parser.js.map +1 -0
- package/dist/validator/tokenizer.d.ts +28 -0
- package/dist/validator/tokenizer.d.ts.map +1 -0
- package/dist/validator/tokenizer.js +174 -0
- package/dist/validator/tokenizer.js.map +1 -0
- package/dist/validator/types.d.ts +55 -0
- package/dist/validator/types.d.ts.map +1 -0
- package/dist/validator/types.js +6 -0
- package/dist/validator/types.js.map +1 -0
- package/dist/watcher.d.ts +44 -0
- package/dist/watcher.d.ts.map +1 -0
- package/dist/watcher.js +297 -0
- package/dist/watcher.js.map +1 -0
- package/dist/world.d.ts +79 -0
- package/dist/world.d.ts.map +1 -1
- package/dist/world.js +167 -1
- package/dist/world.js.map +1 -1
- 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
|
-
- **
|
|
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
|
|
255
|
-
|
|
256
|
-
|
|
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
|
|
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
|