sounding 0.0.0 → 0.0.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/RESEARCH.md CHANGED
@@ -6,16 +6,16 @@ Sounding is a testing framework for Sails applications and The Boring JavaScript
6
6
 
7
7
  It should make it feel natural to test:
8
8
  - helpers and business logic
9
- - actions and endpoints
9
+ - actions, endpoints, and JSON APIs
10
10
  - Inertia responses
11
11
  - authentication flows
12
12
  - email flows
13
13
  - browser journeys
14
- - sockets, jobs, payments, and webhooks over time
14
+ - sockets, jobs, payments, uploads, and webhooks over time
15
15
 
16
16
  The key idea is simple:
17
17
 
18
- **Use the native Node.js test runner, use Playwright for browser work, and wrap both in a Sails-aware runtime that makes realistic tests easy to write and easy to trust.**
18
+ **Use the native Node.js test runner, use Playwright for browser work, and wrap both in a Sails-native runtime that makes realistic tests easy to write and easy to trust.**
19
19
 
20
20
  This should feel less like "yet another framework" and more like the missing test home for everything TBJS already does.
21
21
 
@@ -27,7 +27,7 @@ It is how mariners probe unknown waters, verify what is safe, and learn what lie
27
27
 
28
28
  That maps naturally to testing.
29
29
 
30
- Sounding gives us a name that suggests:
30
+ Sounding suggests:
31
31
  - probing the system
32
32
  - measuring the unknown
33
33
  - learning before committing
@@ -37,7 +37,7 @@ It is also:
37
37
  - one word
38
38
  - maritime without being too cute
39
39
  - broad enough for unit, integration, endpoint, and browser testing
40
- - distinct from Captain Vane, which can remain the data/scenario engine underneath
40
+ - broad enough to grow into the full Sails-native testing story
41
41
 
42
42
  ## The problem we are actually solving
43
43
 
@@ -46,7 +46,7 @@ The TBJS testing story is close, but still fragmented.
46
46
  Today we have pieces:
47
47
  - `node:test` for unit-style tests
48
48
  - Playwright for browser flows
49
- - `inertia-sails/test` for response assertions
49
+ - `inertia-sails/test` for response assertions today
50
50
  - `getSails()` patterns for loading the app in tests
51
51
  - ad hoc seeding and fixture code per application
52
52
 
@@ -57,370 +57,882 @@ Current pain points:
57
57
  - data setup is repetitive and not expressive enough
58
58
  - E2E and app boot orchestration can get ugly fast
59
59
  - `sails-disk` and multi-process test setups collide in painful ways
60
- - realistic auth/email/payment flows are still too manual to test cleanly
60
+ - realistic auth, email, and payment flows are still too manual to test cleanly
61
61
  - people are tempted to add app-code test hooks just to make tests possible
62
62
 
63
63
  The missing thing is not just a test runner.
64
64
 
65
65
  The missing thing is an **elegant testing story**.
66
66
 
67
+ ## Product goals
68
+
69
+ Sounding should be:
70
+ - unmistakably Sails-native
71
+ - expressive enough for product-behavior tests
72
+ - boring to maintain
73
+ - delightful to write
74
+ - credible for API-only apps, Inertia apps, and browser-heavy apps
75
+
67
76
  ## Design principles
68
77
 
69
78
  ### 1. Native first
70
79
  Sounding should build on the native Node.js test runner, not compete with it.
71
80
 
72
- ### 2. Sails-aware, not Sails-entangled
73
- It should understand helpers, actions, policies, sessions, Waterline, Inertia, mail, sockets, and jobs.
81
+ ### 2. Hook first
82
+ Sounding should be a Sails hook first and a CLI second.
83
+
84
+ ### 3. Sails-aware, not Sails-entangled
85
+ It should understand helpers, actions, policies, sessions, Waterline, Inertia, mail, sockets, uploads, and jobs.
74
86
  But it should not force awkward test-only app code.
75
87
 
76
- ### 3. Tests own test data
88
+ ### 4. Tests own test data
77
89
  Factories, traits, scenarios, and fixtures should live under `tests/`, not in the app runtime.
78
90
 
79
- ### 4. One live runtime per browser flow
80
- For E2E, there should be one real app instance and one isolated test database. No shadow app instances fighting the same datastore.
91
+ ### 5. One live runtime per browser flow
92
+ For E2E, there should be one real app instance and one isolated test datastore. No shadow app instances fighting the same datastore.
93
+
94
+ ### 6. Good datastore defaults
95
+ Sounding should respect normal Sails test configuration first.
81
96
 
82
- ### 5. Disposable databases by default
83
- For serious end-to-end or endpoint testing, the default should be a temporary SQLite database per run or per worker, stored under `/tmp`, not `sails-disk`.
97
+ By default, Sounding should manage a temporary `sails-sqlite` datastore under `.tmp/db`.
98
+ When teams want stronger isolation with less ceremony, Sounding should be able to manage a temporary `sails-sqlite` datastore per run or per worker.
84
99
 
85
- ### 6. Realistic over synthetic
100
+ The helper surface should mirror Sails itself: `sails.helpers.user.signupWithTeam(inputs)` should be the happy path.
101
+
102
+ ### 7. Realistic over synthetic
86
103
  The goal is not mocking everything. The goal is real flows with as little fake plumbing as possible.
87
104
 
88
- ### 7. Minimal magic
105
+ ### 8. Minimal magic
89
106
  The best APIs should feel obvious. The framework should save time, not hide too much.
90
107
 
91
- ### 8. Great failure output
108
+ ### 9. Great failure output
92
109
  When a test fails, the developer should know:
93
110
  - what world was created
94
111
  - what request or browser step failed
112
+ - what actor was involved
95
113
  - what the relevant app state was
96
114
 
97
- ## What Sounding should cover
115
+ ## Hook-first architecture
98
116
 
99
- ### Unit / helper tests
100
- - helpers
101
- - pure business logic
102
- - model-adjacent logic
103
- - policies when run in isolation
117
+ Sounding should be a **Sails hook first** and a **CLI second**.
104
118
 
105
- ### Endpoint / action tests
106
- - guest vs authenticated access
107
- - redirects
108
- - JSON and HTML responses
109
- - action inputs/exits
110
- - policy interaction
119
+ ### Canonical runtime surfaces
120
+ - `sails.hooks.sounding` - internal hook runtime
121
+ - `sails.sounding` - ergonomic public alias for app and test usage
122
+ - `config/sounding.js` - the primary configuration surface
111
123
 
112
- ### Inertia integration tests
113
- - component name assertions
114
- - prop assertions
115
- - partial reload behavior
116
- - validation and redirect behavior
124
+ ### What we should not do
125
+ - we should **not** split the mental model between `sails.test` and `sails.sounding`
126
+ - we should **not** use `config/test.js` as the main Sounding config namespace
117
127
 
118
- ### Browser / E2E tests
119
- - sign in flows
120
- - onboarding
121
- - editor flows
122
- - gated-content flows
123
- - checkout and subscription handoff
124
- - mobile navigation
128
+ `config/sounding.js` is the Sails-native answer because it behaves like every other serious subsystem in the ecosystem:
129
+ - `config/mail.js`
130
+ - `config/quest.js`
131
+ - `config/clearance.js`
132
+ - `config/shipwright.js`
125
133
 
126
- ### Mail tests
127
- - magic link emails
128
- - password reset emails
129
- - invite emails
130
- - webhook-triggered notifications
134
+ ### Config shape
131
135
 
132
- ### Future layers
133
- - sockets
134
- - quest jobs
135
- - webhook simulation
136
- - uploads
137
- - passkey/WebAuthn flows
136
+ ```js
137
+ // config/sounding.js
138
+ module.exports.sounding = {
139
+ world: {
140
+ factories: 'tests/factories',
141
+ scenarios: 'tests/scenarios',
142
+ seed: 1337,
143
+ },
144
+
145
+ datastore: {
146
+ mode: 'managed',
147
+ identity: 'default',
148
+
149
+ managed: {
150
+ adapter: 'sails-sqlite',
151
+ isolation: 'worker',
152
+ },
153
+ },
154
+
155
+ browser: {
156
+ enabled: true,
157
+ baseUrl: 'http://127.0.0.1:3333',
158
+ projects: ['desktop'],
159
+ },
160
+
161
+ mail: {
162
+ capture: true,
163
+ },
164
+
165
+ request: {
166
+ transport: 'virtual',
167
+ },
168
+
169
+ auth: {
170
+ defaultActor: 'guest',
171
+ },
172
+ }
173
+ ```
174
+
175
+ ### Default behavior
176
+
177
+ Sounding should ship with calm, predictable defaults:
178
+ - `datastore.mode = 'managed'`
179
+ - `datastore.identity = 'default'`
180
+ - `datastore.adapter = 'sails-sqlite'`
181
+ - `datastore.isolation = 'worker'`
182
+ - `world.factories = 'tests/factories'`
183
+ - `world.scenarios = 'tests/scenarios'`
184
+ - `mail.capture = true`
185
+ - `request.transport = 'virtual'`
186
+ - `browser.projects = ['desktop']`
187
+
188
+ Environment-specific overrides should still live in `config/env/test.js`, but `config/sounding.js` should be the home of the Sounding subsystem itself.
138
189
 
139
190
  ## The core mental model
140
191
 
141
192
  Sounding should feel like this:
142
193
 
143
194
  - **App**: a booted Sails application under test
144
- - **World**: a realistic set of data created for a test
195
+ - **World**: a realistic, named set of data created for a test
145
196
  - **Actor**: a user role in that world
146
197
  - **Trial**: the test itself
147
198
  - **Mailbox**: captured outbound mail for assertions
148
- - **Browser**: Playwright page/context helpers
199
+ - **Browser**: Playwright page and context helpers
149
200
 
150
201
  The tests should read like behavior, not setup plumbing.
151
202
 
152
- ## What Captain Vane should become
153
203
 
154
- Captain Vane should not disappear.
204
+ ## What a trial means
205
+
206
+ A **trial** is the smallest meaningful behavior Sounding asks your app to prove.
207
+
208
+ It is one named claim about how the product should behave in a real Sails runtime.
209
+
210
+ Examples:
211
+
212
+ - a guest is redirected from the dashboard
213
+ - a subscriber can read a members-only issue
214
+ - a publisher can save a draft
215
+ - requesting a magic link sends a usable email
216
+
217
+ This stays close to the mental model developers already know from Jest, Pest, and the native Node test runner: one file groups related checks, and each named check proves one thing.
218
+
219
+ Sounding keeps the familiar `test()` API, but uses **trial** as the conceptual word because the framework is designed around product behaviors, worlds, actors, and realistic runtime conditions.
220
+
221
+ A good trial should be:
222
+
223
+ - named after behavior, not implementation
224
+ - small enough to understand quickly
225
+ - real enough to trust
226
+ - written at the right layer for what it is proving
227
+
228
+ ## What a trial context means
229
+
230
+ A **trial context** is the single object passed into `test()`.
231
+
232
+ It should always have one clear center of gravity: `sails`.
233
+
234
+ That means:
235
+ - `sails` is the primary runtime object
236
+ - app-native surfaces stay where Sails developers expect them
237
+ - Sounding capabilities live under `sails.sounding`
238
+ - a few top-level aliases like `get()` and `post()` can exist for convenience
239
+ - `expect` is always present
240
+
241
+ This is important because Sounding should not invent a second pretend app model.
242
+ The trial context should feel like a real Sails app that has been furnished for testing.
243
+
244
+ ## Design patterns for Sounding
245
+
246
+ These are the patterns that should keep Sounding elegant as it grows.
247
+
248
+ ### 1. Runtime-rooted context
249
+
250
+ Every trial should start from the real app runtime:
251
+
252
+ - `sails` is the primary object
253
+ - `sails.helpers`, `sails.models`, `sails.config`, and `sails.hooks` stay canonical
254
+ - Sounding-specific capabilities hang off `sails.sounding`
255
+
256
+ This keeps Sounding from inventing a second fake app model.
257
+
258
+ ### 2. Capability aliases, not parallel abstractions
259
+
260
+ Top-level helpers like `get()`, `post()`, and `visit()` should exist as ergonomic shortcuts.
155
261
 
156
- Captain Vane should become the data and scenario engine that powers Sounding.
262
+ But they should always map back to a canonical home like:
157
263
 
158
- ### Captain Vane should own
264
+ - `sails.sounding.request.get()`
265
+ - `sails.sounding.request.post()`
266
+ - `sails.sounding.visit()`
267
+
268
+ That gives us convenience without splitting the mental model.
269
+
270
+ ### 3. Worlds as business situations
271
+
272
+ Worlds should describe business state, not just rows in a datastore.
273
+
274
+ That means:
275
+
276
+ - scenarios should read like product situations
277
+ - actors should be role-based
278
+ - tests should load a world instead of assembling ten unrelated records
279
+
280
+ ### 4. Calm defaults, explicit escalation
281
+
282
+ Sounding should respect the app before it tries to be clever:
283
+
284
+ - manage a temporary `sails-sqlite` datastore by default
285
+ - use `config/env/test.js` as the app's truth
286
+ - let teams opt into `inherit` or `external` only when they truly need them
287
+
288
+ This keeps the first experience simple and the advanced experience deliberate.
289
+
290
+ ### 5. Progressive disclosure
291
+
292
+ The beginner path should be tiny:
293
+
294
+ - `test()`
295
+ - `sails`
296
+ - `expect`
297
+
298
+ Then as the need grows, the trial can reach for:
299
+
300
+ - `get()` / `post()`
301
+ - `sails.sounding.world`
302
+ - `sails.sounding.mailbox`
303
+ - `page`
304
+
305
+ We should not force complexity on the first test.
306
+
307
+ ### 6. Lazy heavyweight surfaces
308
+
309
+ The heaviest tools should only boot when a trial really needs them.
310
+
311
+ That includes:
312
+
313
+ - Playwright browser state
314
+ - mailbox capture integrations
315
+ - richer Inertia helpers
316
+
317
+ This keeps helper and endpoint trials fast without creating a separate API universe.
318
+
319
+ ### 7. One assertion style
320
+
321
+ `expect` should be the primary assertion API everywhere.
322
+
323
+ That means the same mental style for:
324
+
325
+ - helpers
326
+ - JSON APIs
327
+ - Inertia responses
328
+ - mail
329
+ - browser assertions
330
+
331
+ ## What is a world?
332
+
333
+ A world should be documented and taught as one of Sounding's signature ideas, not a side feature.
334
+
335
+ The docs need to make these distinctions obvious:
336
+
337
+ - a **factory** builds one kind of record
338
+ - a **scenario** composes factories into a named business situation
339
+ - an **actor** is the role a trial operates through inside that situation
340
+ - the resulting **world** is the readable state the trial uses
341
+
342
+ The best worlds should feel like product language, not seed-script language.
343
+
344
+
345
+ A **world** is the named, deterministic business state that a trial lives inside.
346
+
347
+ A world includes:
348
+ - actors such as guests, publishers, subscribers, or admins
349
+ - records like issues, subscriptions, teams, unlocks, invoices, or comments
350
+ - the relationships between those records
351
+ - the current business situation the trial cares about
352
+
353
+ A world is not just a fixture.
354
+
355
+ It is a reusable description of a product situation.
356
+
357
+ That lets tests say:
358
+ - "load the subscriber who has access to a gated issue"
359
+ - "load the publisher with a draft issue"
360
+ - "load the guest who requested a magic link"
361
+
362
+ instead of rewriting ten lines of setup every time.
363
+
364
+ ## The built-in world engine
365
+
366
+ Sounding should own its own world engine.
367
+
368
+ That means factories, traits, states, scenarios, seeds, and world composition should live inside Sounding itself.
369
+
370
+ This keeps the testing story elegant:
371
+ - one package
372
+ - one mental model
373
+ - one configuration surface
374
+ - one documentation story
375
+ - one runtime that understands app boot, data setup, mail capture, and browser execution together
376
+
377
+ ### The world engine should own
159
378
  - factories
160
- - traits / states
379
+ - traits and states
161
380
  - sequences
162
381
  - deterministic seeds
163
382
  - build vs create APIs
164
383
  - relationship graphs
165
384
  - scenarios that return readable world objects
166
385
 
167
- ### Captain Vane v2 should support
386
+ ### The world engine should support
168
387
  - `tests/factories`
169
388
  - `tests/scenarios`
389
+ - `defineFactory()`
390
+ - `defineScenario()`
170
391
  - `build()`
171
392
  - `buildMany()`
172
393
  - `create()`
173
394
  - `createMany()`
174
- - `state()` / `trait()`
395
+ - `trait()` / `state()`
175
396
  - `seed()`
176
397
  - `afterBuild()` / `afterCreate()`
398
+ - `world.use()`
177
399
 
178
- ### Captain Vane should not own
400
+ ### The world engine should not own
179
401
  - Sails app boot lifecycle
180
402
  - request clients
181
403
  - Playwright lifecycle
182
404
  - mail capture runtime
183
- - worker/database orchestration
405
+ - worker and datastore orchestration
184
406
 
185
- That is Sounding’s job.
407
+ Those remain the job of the Sounding runtime around it.
186
408
 
187
- ## What Sounding should own
409
+ ## What Sounding should cover
188
410
 
189
- ### App lifecycle
190
- - boot Sails once for the test mode being used
191
- - manage ports and process lifecycle
192
- - expose helpers for in-process and browser-driven testing
411
+ ### Helper trials
412
+ - helpers
413
+ - pure business logic
414
+ - policy-like checks in isolation
415
+ - model-adjacent rules
193
416
 
194
- ### Database lifecycle
195
- - create one isolated SQLite database per run or per worker by default
196
- - configure Sails to use it automatically in test mode
197
- - tear it down cleanly
198
- - support Postgres later for heavier projects
417
+ ### Endpoint and action trials
418
+ - guest vs authenticated access
419
+ - redirects
420
+ - JSON and HTML responses
421
+ - status codes and headers
422
+ - action inputs and exits
423
+ - policy interaction
424
+ - webhooks and provider callbacks
199
425
 
200
- ### Runtime adapters
201
- - request client for endpoint/action tests
202
- - Inertia assertion helpers
203
- - Playwright integration for browser tests
204
- - mailbox capture
205
- - auth/session helpers
206
- - socket client helpers later
207
- - job helpers later
426
+ ### Inertia trials
427
+ - component name assertions
428
+ - prop assertions
429
+ - nested prop paths
430
+ - shared props
431
+ - validation and redirect behavior
432
+ - partial reload behavior
208
433
 
209
- ### Ergonomic API surface
210
- The top-level API should make Sails concepts first-class without inventing a giant DSL.
434
+ ### Browser trials
435
+ - sign in flows
436
+ - onboarding
437
+ - editor flows
438
+ - gated-content flows
439
+ - checkout and subscription handoff
440
+ - mobile navigation
441
+
442
+ ### Mail trials
443
+ - magic link emails
444
+ - password reset emails
445
+ - invite emails
446
+ - billing and transactional notifications
447
+
448
+ ### Future layers
449
+ - sockets
450
+ - quest jobs
451
+ - uploads
452
+ - passkey/WebAuthn flows
453
+ - payment simulation
454
+
455
+ ## The API surface we want
456
+
457
+ ### Core
458
+ - `defineConfig()`
459
+ - `test()`
460
+ - `describe()`
461
+ - `beforeEach()`
462
+ - `afterEach()`
463
+ - `beforeAll()`
464
+ - `afterAll()`
465
+ - `dataset()`
466
+ - `expect()`
467
+
468
+ ### One trial API
469
+ - `test()` is the primary public entrypoint
470
+ - the callback receives a single context object
471
+ - `sails` is the canonical runtime surface
472
+ - transport aliases like `get()`, `post()`, and later `visit()` are convenience helpers
473
+ - browser-capable trials can additionally destructure `page` when needed
474
+
475
+ ### Runtime surfaces
476
+ - `sails.sounding.boot()`
477
+ - `sails.sounding.lower()`
478
+ - `sails.sounding.world.use()`
479
+ - `sails.helpers.user.signupWithTeam()`
480
+ - `sails.sounding.mailbox.latest()`
481
+ - `sails.sounding.mailbox.clear()`
482
+
483
+ ## Assertion style
484
+
485
+ Sounding should prefer `expect` as the primary assertion API.
486
+
487
+ That choice matters because it gives the framework one clear, readable style across helper, endpoint, Inertia, mail, and browser trials.
488
+
489
+ `assert` from Node can remain available as an escape hatch, but it should not be the main story.
490
+
491
+ ### Core matchers
492
+ - `toBe()`
493
+ - `toEqual()`
494
+ - `toContain()`
495
+ - `toMatch()`
496
+ - `toBeTruthy()`
497
+ - `toBeFalsy()`
498
+ - `toBeDefined()`
499
+
500
+ ### Sails-native matchers
501
+ - `toHaveStatus()`
502
+ - `toRedirectTo()`
503
+ - `toHaveJsonPath()`
504
+ - `toHaveHeader()`
505
+ - `toExit()`
506
+ - `toBeInertiaPage()`
507
+ - `toHaveProp()`
508
+ - `toHaveValidationError()`
509
+ - `toHaveSentMail()`
510
+
511
+ ## The API-only testing story
512
+
513
+ Sounding has to be excellent for JSON and endpoint-heavy apps.
211
514
 
212
- ## Proposed API direction
515
+ This is not a side quest.
516
+ It is part of the core product.
517
+
518
+ ### One API, multiple transports
519
+
520
+ Sounding should keep one public request story while supporting more than one transport underneath.
521
+
522
+ That means `get()`, `post()`, `visit()`, and `sails.sounding.request` should feel stable even if the underlying transport changes.
523
+
524
+ The two important transports are:
525
+
526
+ - **virtual** requests, powered by `sails.request()`
527
+ - **HTTP** requests, powered by a real listening app over the network
528
+
529
+ ### Why `sails.request()` matters
530
+
531
+ `sails.request()` is one of the most interesting native building blocks Sounding can lean on.
532
+
533
+ It already gives Sails a virtual request interpreter, and its documented sweet spot is faster-running unit and integration tests.
534
+
535
+ That makes it a strong fit for:
536
+
537
+ - fast endpoint trials
538
+ - action-like request flows
539
+ - Inertia response assertions that care about server-side contracts
540
+ - situations where lifting a full HTTP server is unnecessary
541
+
542
+ ### Where virtual requests should not be the whole story
543
+
544
+ The Sails docs are also clear that virtual requests are not identical to true HTTP requests.
545
+
546
+ That matters because:
547
+
548
+ - body parsing is simpler
549
+ - Express HTTP middleware is not fully in play
550
+ - static assets are not involved
551
+ - some middleware-sensitive behaviors need real HTTP parity
552
+
553
+ So Sounding should not pretend `sails.request()` is the answer to everything.
554
+
555
+ ### The right transport strategy
556
+
557
+ The elegant answer is:
558
+
559
+ - keep one request API
560
+ - choose the transport underneath based on the kind of trial
561
+ - let the app or the trial opt into stricter parity when needed
562
+ - make the override order obvious and boring
563
+
564
+ A good switching story looks like:
565
+
566
+ 1. per-call override, such as `get('/health', { transport: 'http' })`
567
+ 2. per-trial override, such as `test('...', { transport: 'http' }, ...)`
568
+ 3. the default from `config/sounding.js`
569
+ 4. a scoped client when a trial wants to stay explicit: `sails.sounding.request.using('http')`
570
+
571
+ So a good default would be:
572
+
573
+ - use **virtual transport** for fast app-aware endpoint and Inertia-style trials when that is sufficient
574
+ - use **HTTP transport** for browser flows and for endpoint trials that need true HTTP behavior
575
+
576
+ That gives Sounding speed without lying about what is actually being exercised.
577
+
578
+ ### `test()` for endpoint behavior
579
+ Use `test()` for endpoint behavior:
580
+ - status codes
581
+ - headers
582
+ - redirects
583
+ - JSON bodies
584
+ - policy interaction
585
+ - guest vs authenticated access
213
586
 
214
- ### Helper test
215
587
  ```js
216
- import { test } from 'drydock'
588
+ import { test } from 'sounding'
217
589
 
218
- test.helper('signupWithTeam creates a team and membership', async ({ helper, expect }) => {
219
- const result = await helper('user.signupWithTeam', {
220
- fullName: 'Kelvin O',
221
- email: 'kelvin@example.com',
222
- tosAcceptedByIp: '127.0.0.1',
223
- })
590
+ test('guest gets 401 on a private JSON endpoint', async ({
591
+ get,
592
+ expect,
593
+ }) => {
594
+ const response = await get('/api/issues')
224
595
 
225
- expect(result.user.email).toBe('kelvin@example.com')
596
+ expect(response).toHaveStatus(401)
226
597
  })
227
598
  ```
228
599
 
229
- ### Endpoint test
600
+ ### `test()` for action contracts
601
+ Use `test()` when the action contract matters more than raw HTTP.
602
+
230
603
  ```js
231
- import { test } from 'drydock'
604
+ import { test } from 'sounding'
605
+
606
+ test('issues/publish rejects incomplete drafts', async ({
607
+ action,
608
+ expect,
609
+ }) => {
610
+ const result = await action('issues/publish', { id: 12 })
232
611
 
233
- test.endpoint('guest is redirected from dashboard', async ({ request, expect }) => {
234
- const response = await request.get('/dashboard')
235
- expect(response).toRedirectTo('/login')
612
+ expect(result).toExit('invalid')
236
613
  })
237
614
  ```
238
615
 
239
- ### Inertia test
616
+ ## The Inertia testing story
617
+
618
+ Inertia responses deserve their own first-class lane.
619
+
620
+ They are not just JSON and not just browser pages.
621
+
622
+ ### `test()` for Inertia responses
623
+ Use `test()` with `visit()` and Inertia-aware matchers for:
624
+ - component assertions
625
+ - prop assertions
626
+ - nested prop paths
627
+ - shared props
628
+ - validation and redirect behavior
629
+ - partial reloads
630
+
240
631
  ```js
241
- import { test } from 'drydock'
632
+ import { test } from 'sounding'
242
633
 
243
- test.inertia('pricing page returns the expected component and props', async ({ visit, expect }) => {
634
+ test('pricing page returns the correct component and props', async ({
635
+ visit,
636
+ expect,
637
+ }) => {
244
638
  const page = await visit('/pricing')
639
+
245
640
  expect(page).toBeInertiaPage('billing/pricing')
641
+ expect(page).toHaveProp('plans')
642
+ expect(page).toHaveProp('auth.user', null)
246
643
  })
247
644
  ```
248
645
 
249
- ### Browser test
250
- ```js
251
- import { test } from 'drydock'
252
-
253
- test.browser('subscriber can read a members-only issue', async ({ page, world, login, expect }) => {
254
- await world.use('issue-access')
255
- await login.as('subscriber', page)
646
+ And for partial reloads:
256
647
 
257
- await page.goto(world.issues.gated.url)
648
+ ```js
649
+ test('dashboard can reload only notifications', async ({ visit, expect }) => {
650
+ const page = await visit('/dashboard', {
651
+ component: 'dashboard/index',
652
+ only: ['notifications'],
653
+ reset: ['sidebar'],
654
+ })
258
655
 
259
- await expect(page.getByText(world.issues.gated.fullText)).toBeVisible()
656
+ expect(page).toBeInertiaPage('dashboard/index')
657
+ expect(page).toHaveProp('notifications')
260
658
  })
261
659
  ```
262
660
 
263
- ### Mail test
661
+ ## The mail testing story
662
+
663
+ Sounding should integrate cleanly with the Sails mail story and make mailbox capture feel native.
664
+
665
+ For `0.0.1`, the right implementation is simple and honest:
666
+ - wrap `sails.helpers.mail.send` when a trial boots
667
+ - capture the real inputs that flow through `sails-hook-mail`
668
+ - render the template preview when a template is used
669
+ - store normalized messages in `sails.sounding.mailbox`
670
+ - restore the original helper when the trial ends
671
+
672
+ That keeps the story Sails-native without inventing a fake mail subsystem.
673
+
674
+ A mail trial should let a developer say:
675
+
264
676
  ```js
265
- import { test } from 'drydock'
677
+ import { test } from 'sounding'
266
678
 
267
- test.mail('magic link sends a usable email', async ({ mailbox, expect }) => {
268
- const email = await mailbox.latest()
679
+ test('magic link sends a usable email', async ({
680
+ sails,
681
+ auth,
682
+ expect,
683
+ }) => {
684
+ await auth.requestMagicLink('reader@example.com')
685
+
686
+ const email = await sails.sounding.mailbox.latest()
687
+
688
+ expect(email.to).toContain('reader@example.com')
269
689
  expect(email.subject).toContain('Sign in')
270
- expect(email.ctaUrl).toMatch(/magic-link/)
690
+ expect(email.html).toContain('/magic-link/')
271
691
  })
272
692
  ```
273
693
 
274
- ## Database strategy
694
+ That is the level of clarity we want.
695
+
696
+ And the captured message should be rich enough to assert on:
697
+ - `to`, `cc`, and `bcc`
698
+ - `subject`, `from`, and `replyTo`
699
+ - rendered `html` and `text`
700
+ - `template` and `templateData`
701
+ - extracted links like `ctaUrl`
702
+ - `status` for sent vs failed deliveries
703
+ - `error` details when delivery fails
704
+
705
+ ## The browser testing story
706
+
707
+ Sounding should use Playwright for browser work, but it should make browser trials feel like the natural top layer of the same testing story.
708
+
709
+ A browser trial should have access to:
710
+ - Playwright `page`
711
+ - app-aware auth helpers like `login.as()`
712
+ - worlds and actors
713
+ - mobile projects as first-class citizens
714
+
715
+ ## What we borrow from Pest
716
+
717
+ The best thing to borrow from Pest is not PHP syntax.
718
+ It is the product feel.
719
+
720
+ We should borrow:
721
+ - one beautiful home for testing
722
+ - low ceremony
723
+ - coherent configuration
724
+ - first-class datasets and hooks
725
+ - browser testing as part of the same product, not a bolt-on
726
+ - reporting that feels like a feature
727
+
728
+ We should not borrow:
729
+ - too much syntax sugar
730
+ - magic that fights the host language
731
+ - abstractions that hide Node so much that debugging gets harder
732
+
733
+ Sounding should feel like:
734
+ - Pest philosophy
735
+ - Node honesty
736
+ - Sails-native ergonomics
737
+
738
+ ## The first credible release
739
+
740
+ `0.0.1` should prove the story is real, elegant, and useful.
741
+
742
+ It should include:
743
+ - hook loading and `sails.sounding`
744
+ - `config/sounding.js`
745
+ - datastore inheritance and managed `sails-sqlite` orchestration
746
+ - `test()` for helpers, endpoints, JSON, Inertia, and mail
747
+ - `test()` with `page` when the browser matters
748
+ - a built-in world engine
749
+ - mailbox capture
750
+ - a small example app and docs
751
+
752
+ It does not need to do everything at once.
753
+ It does need to make developers believe the rest of the vision is inevitable.
275
754
 
276
- This is the most important runtime choice.
755
+ ## Migration bar: replace the African Engineer test suite
277
756
 
278
- ### What we should avoid
279
- - `sails-disk` for serious endpoint/E2E tests
280
- - multi-process setups that touch the same datastore files
281
- - test-only HTTP routes just for seeding state
757
+ Sounding should be able to replace the current test story in `/Users/koo/Gringotts/687/africanengineer.com` without asking the app to invent more test plumbing.
282
758
 
283
- ### Recommended default
284
- For `0.0.1`, Sounding should default to:
285
- - **temporary SQLite**
286
- - one database file per run or per worker
287
- - stored under something like `/tmp/drydock/<run-id>/<worker-id>.sqlite`
759
+ ### What the current suite looks like
288
760
 
289
- Why SQLite first:
290
- - TBJS already leans on SQLite as a sensible default
291
- - no external services required
292
- - much more realistic than `sails-disk`
293
- - transactions and relational behavior are available
294
- - dramatically better for E2E orchestration
761
+ The current African Engineer test setup includes:
295
762
 
296
- Later, Sounding can support Postgres for teams that want parity with production.
763
+ - unit helper tests using `node:test` + `getSails()`
764
+ - Playwright page smoke tests for public pages
765
+ - guest protection tests for login redirects
766
+ - magic-link browser tests that manually issue tokens through a test helper module
767
+ - issue-access browser tests that seed users, subscriptions, unlocks, and bookmarks through a custom support file
768
+ - publisher editor tests that seed a draft issue and then drive the browser editor
297
769
 
298
- ## Test data location
770
+ Today, that setup relies on:
299
771
 
300
- A strong rule:
772
+ - `tests/util/get-sails.js`
773
+ - `tests/e2e/support/test-db.cjs`
774
+ - Playwright web-server orchestration in `playwright.config.js`
775
+ - custom fixture builders and explicit database cleanup
301
776
 
302
- **All test data definitions live under `tests/`.**
777
+ Sounding should absorb that burden.
303
778
 
304
- Suggested structure:
779
+ ### What Sounding must provide to replace it cleanly
305
780
 
306
- ```text
307
- tests/
308
- factories/
309
- user.js
310
- issue.js
311
- subscription.js
312
- scenarios/
313
- issue-access.js
314
- publisher-editor.js
315
- reader-dashboard.js
316
- e2e/
317
- integration/
318
- unit/
319
- ```
781
+ #### 1. One primary `test()` API
320
782
 
321
- This keeps product code clean and keeps the testing world owned by the tests.
783
+ The public entrypoint should stay:
322
784
 
323
- ## What 0.0.1 should actually ship
785
+ - `test()`
324
786
 
325
- The first release should be sharp, not huge.
787
+ And the trial context should be able to power:
326
788
 
327
- ### 0.0.1 goals
328
- - native Node test runner integration
329
- - Playwright browser integration
330
- - Sails app boot manager
331
- - temporary SQLite database lifecycle
332
- - `test.helper()`
333
- - `test.endpoint()`
334
- - `test.browser()`
335
- - basic auth helpers for common roles
336
- - mailbox capture for log mailers / test mailers
337
- - Captain Vane adapter for factories/scenarios
338
- - one reference TBJS example app
789
+ - helper tests
790
+ - endpoint tests
791
+ - Inertia tests
792
+ - mail assertions
793
+ - browser flows when `page` is needed
339
794
 
340
- ### 0.0.1 non-goals
341
- - custom assertion engine from scratch
342
- - sockets and jobs on day one
343
- - full payment/webhook simulation on day one
344
- - WebAuthn passkey coverage on day one
345
- - every possible Sails hook abstraction immediately
795
+ #### 2. A Sails-centered trial context
346
796
 
347
- The 0.0.1 bar is: **credible, elegant, useful, and real.**
797
+ Every trial should be able to reach for:
348
798
 
349
- ## What a good 0.0.1 feels like
799
+ - `sails`
800
+ - `expect`
801
+ - `get()`, `post()`, `put()`, `patch()`, `del()`
802
+ - `visit()`
803
+ - browser-capable trials should additionally be able to destructure `page`
350
804
 
351
- A developer should be able to:
352
- - install Sounding
353
- - point it at a Sails app
354
- - define factories and scenarios under `tests/`
355
- - run helper tests, endpoint tests, and browser tests with one coherent mental model
356
- - avoid touching product code to make tests possible
805
+ The canonical app surfaces should remain:
357
806
 
358
- If we achieve that, the story is already strong.
807
+ - `sails.helpers`
808
+ - `sails.models`
809
+ - `sails.config`
810
+ - `sails.hooks`
359
811
 
360
- ## Roadmap after 0.0.1
812
+ And Sounding-specific surfaces should remain under:
361
813
 
362
- ### 0.1.x
363
- - richer Captain Vane trait/state system
364
- - `test.inertia()`
365
- - storage-state auth helpers
366
- - mobile/browser project presets
367
- - better response and redirect assertions
814
+ - `sails.sounding.world`
815
+ - `sails.sounding.request`
816
+ - `sails.sounding.mailbox`
368
817
 
369
- ### 0.2.x
370
- - sockets
371
- - quest jobs
372
- - webhooks
373
- - upload helpers
374
- - payment flow harnesses
818
+ #### 3. Virtual request transport by default
819
+
820
+ For non-browser trials, Sounding should default to a request transport powered by `sails.request()`.
821
+
822
+ That gives us a Sails-native replacement for most current endpoint-style and Inertia-style needs without bringing in `supertest`.
823
+
824
+ It must normalize responses well enough for:
375
825
 
376
- ### 0.3.x
377
- - passkey/WebAuthn helpers
378
- - scenario debugger / world inspector
379
- - better watch mode and failure reports
380
- - richer CI output
826
+ - `toHaveStatus()`
827
+ - `toRedirectTo()`
828
+ - `toHaveHeader()`
829
+ - `toHaveJsonPath()`
830
+ - `toBeInertiaPage()`
831
+ - `toHaveProp()`
832
+
833
+ #### 4. True HTTP/browser capability when parity matters
834
+
835
+ Sounding still needs real browser support for the flows that genuinely require it:
836
+
837
+ - login journeys
838
+ - gated issue reading in the browser
839
+ - editor interactions
840
+ - mobile navigation
841
+
842
+ That means browser-capable trials need:
843
+
844
+ - `page`
845
+ - web-server orchestration
846
+ - a clear way to combine browser behavior with worlds and actors
847
+
848
+ #### 5. A built-in world engine strong enough to replace `tests/e2e/support/test-db.cjs`
849
+
850
+ Sounding should support factories and scenarios under `tests/` that can express the current African Engineer fixtures, including:
851
+
852
+ - users with roles like publisher, subscriber, unlocked reader, and guest
853
+ - teams and memberships
854
+ - subscriptions
855
+ - issue unlocks
856
+ - bookmarks
857
+ - published and draft issues
858
+
859
+ Concrete scenarios Sounding should be able to express early:
860
+
861
+ - `issue-access`
862
+ - `publisher-editor`
863
+ - `guest-protection`
864
+ - `magic-link-auth`
865
+
866
+ #### 6. Mailbox capture that removes the need for manual magic-link token helpers
867
+
868
+ African Engineer currently issues magic-link tokens by reaching into app internals from `tests/e2e/support/test-db.cjs`.
869
+
870
+ Sounding should replace that with a better story:
871
+
872
+ - request the magic link through the real app behavior
873
+ - capture the resulting mail through `sails.sounding.mailbox`
874
+ - extract the sign-in URL from the captured message
875
+ - continue the browser flow from there
876
+
877
+ That is more realistic and removes custom token-plumbing from app tests.
878
+
879
+ #### 7. Simple auth helpers for browser and endpoint trials
880
+
881
+ To replace the current repeated login setup, Sounding should offer lightweight auth helpers built around real app behavior.
882
+
883
+ At minimum, it should support:
884
+
885
+ - login via captured magic link
886
+ - acting as a known seeded actor in request-driven trials
887
+
888
+ #### 8. Enough ergonomics to replace the current unit helper tests too
889
+
890
+ The helper tests in African Engineer should become straightforward Sounding trials like:
891
+
892
+ ```js
893
+ const { test } = require('sounding')
894
+
895
+ test('capitalize helper works', async ({ sails, expect }) => {
896
+ expect(sails.helpers.capitalize('hello')).toBe('Hello')
897
+ })
898
+ ```
381
899
 
382
- ## Risks and sharp edges
900
+ That means Sounding should feel just as good for tiny tests as for browser journeys.
383
901
 
384
- ### 1. Too much magic
385
- If Sounding tries to hide too much, it will become hard to trust.
902
+ ### The concrete migration target
386
903
 
387
- ### 2. App boot complexity
388
- Sails boot and teardown need to be extremely predictable.
904
+ Sounding is ready to replace the current African Engineer suite when we can remove or obsolete:
389
905
 
390
- ### 3. Database abstraction drift
391
- We should not pretend SQLite and Postgres are identical. We should be honest about the tradeoffs.
906
+ - `/Users/koo/Gringotts/687/africanengineer.com/tests/util/get-sails.js`
907
+ - `/Users/koo/Gringotts/687/africanengineer.com/tests/e2e/support/test-db.cjs`
908
+ - the app-specific seeding and magic-link plumbing around Playwright
909
+ - the idea that helper, endpoint, Inertia, mail, and browser tests need separate mental models
392
910
 
393
- ### 4. Overcoupling to one stack shape
394
- It should be opinionated for TBJS, but still flexible enough for real Sails apps.
911
+ ### The first migration phases
395
912
 
396
- ### 5. Captain Vane boundary blur
397
- If Sounding and Captain Vane overlap too much, both products get muddy.
913
+ #### Phase 1
398
914
 
399
- ## Success criteria
915
+ Replace:
400
916
 
401
- Sounding is working if:
402
- - a new TBJS user can write meaningful tests quickly
403
- - endpoint and E2E tests do not require test-only app routes
404
- - browser tests can run against one isolated app + one isolated database cleanly
405
- - data setup reads like business scenarios, not SQL dumps
406
- - failures are easier to understand than today
917
+ - unit helper tests
918
+ - guest-protection endpoint-style tests
919
+ - public page smoke tests that do not need rich browser fixtures
407
920
 
408
- ## Short naming shortlist
921
+ #### Phase 2
409
922
 
410
- ### Chosen: Sounding
411
- Best balance of elegance, meaning, and distinctiveness.
923
+ Replace:
412
924
 
413
- ### Alternatives considered
414
- - **Drydock** strong and concrete, but more about environment than probing behavior
415
- - **Seatrial** very direct, but less elegant as a brand
416
- - **Harbor** — strong, but feels more like infra than testing
417
- - **Trials** — clear, but less distinctive
925
+ - issue-access tests
926
+ - magic-link auth tests
927
+ - basic dashboard/member flows
418
928
 
419
- ## Final take
929
+ #### Phase 3
420
930
 
421
- Sounding should become the elegant testing story for TBJS.
931
+ Replace:
422
932
 
423
- Captain Vane should power the world-building underneath it.
933
+ - publisher editor tests
934
+ - the remaining browser-heavy flows
935
+ - mobile navigation coverage
424
936
 
425
- If we get the boundaries right, we do not just end up with a nicer API.
426
- We end up with a testing system that actually matches how Sails applications are built.
937
+ If Sounding can carry that migration, then it is not just a nice idea.
938
+ It is a credible testing framework for real Sails applications.