@happyvertical/smrt-svelte 0.51.4 → 0.51.6

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/AGENTS.md CHANGED
@@ -16,6 +16,7 @@ subpath you are editing. This file keeps what holds in every module.
16
16
  | `src/components/settings/` (`./settings`) | `SettingsCatalog`, `paginateSettingsCatalog`, and the summary-vs-detail scalability contract | [agents/settings.md](agents/settings.md) |
17
17
  | `src/components/workspace/` (`./workspace` + `./web`) | the AdminShell family and its principles, the legacy ToolsDock surface, the `./web` activity-feed and `updateAvailable` adapters, and server-side dock gates | [agents/workspace.md](agents/workspace.md) |
18
18
  | `src/web/remote-query.svelte.ts` | Svelte 5 binding for query-shaped remote pages: rows, page, totals, loading/refreshing/stale/error, retry, last-updated, and query-scoped live subscriptions (#2445) | — |
19
+ | `src/web/list-data-surface.svelte.ts` (`mountListDataSurface`) | One-call registration of an existing, non-`DataTable` list (already mirroring a headless `DataTableController`) as a mounted `DataSurfaceDescriptor` — a same-behavior port of `registerContentListDataSurface`'s registration/translation logic (`@happyvertical/smrt-content/svelte`), generalized off ContentList's view-mode concept, so any custom list markup can adopt it directly instead of hand-mirroring the registry contract. Not yet unified behind a shared `@happyvertical/smrt-ui/data` implementation (#2917) — this copy is currently ahead; see its own module doc comment for the current, authoritative list of divergences #2917 must adopt rather than assume parity (#2906) | — |
19
20
  | `src/web/webmcp-provider.ts` | Provider config for generated data/model WebMCP tools: definitions, effect policy, namespace, budget, legacy/canonical filters, and fetcher seams (#2520) | — |
20
21
  | `src/web/webmcp-ui.ts` | Fixed, low-cardinality WebMCP adapter over the Provider's mounted form-control and data-surface registries (#2521) | — |
21
22
  | `src/web/webmcp.svelte.ts` (`useWebMcpTool`) | Component-owned bespoke WebMCP tool. Routes through `@happyvertical/smrt-web`'s `registerWebMcpBespokeTool`, so it shares the fail-closed effect classification and `effects` exposure policy of generated tools — undeclared `annotations` classify destructive and are excluded by default. `namespace` and `maxTools` never apply to a bespoke tool (#2586) | — |
package/README.md CHANGED
@@ -144,6 +144,44 @@ should be discoverable:
144
144
  </Provider>
145
145
  ```
146
146
 
147
+ A page with its own hand-rolled list markup — not `DataTable` — that already
148
+ mirrors a headless `DataTableController`'s search/filters/sort/page/selection
149
+ can register that same registry in one call with `mountListDataSurface`
150
+ (`@happyvertical/smrt-svelte/web`) instead of hand-writing the registration:
151
+
152
+ ```ts
153
+ import { createDataTableController } from '@happyvertical/smrt-ui/data';
154
+ import { mountListDataSurface } from '@happyvertical/smrt-svelte/web';
155
+
156
+ const controller = createDataTableController();
157
+ const handle = mountListDataSurface({
158
+ registry: surfaces,
159
+ descriptor: myListDescriptor,
160
+ controller,
161
+ context: { totalRows, queryFingerprint },
162
+ refresh: () => reload(),
163
+ });
164
+
165
+ // Only `controller` is observed automatically. App-owned `context` is a
166
+ // one-time snapshot at mount — publish a fresh one whenever totalRows,
167
+ // queryFingerprint, or freshness changes (an $effect keyed on those values
168
+ // is the usual place):
169
+ handle.update({ totalRows, queryFingerprint });
170
+
171
+ // on unmount:
172
+ handle.destroy();
173
+ ```
174
+
175
+ It mirrors `controller` into the registry, translates visible table commands
176
+ back into `controller.dispatch()` calls, and routes the fixed
177
+ `refresh`/`retry`/`focus`/`reveal`/`highlight` controls to callbacks; any
178
+ other `controlId` goes through an `onControl` escape hatch (denied by
179
+ default) — except a canonical table-control id (`set-filters`, `reset`,
180
+ `set-page`, … the full `DATA_TABLE_SURFACE_CONTROL_IDS` list from
181
+ `@happyvertical/smrt-ui/data`), which is always intercepted first and never
182
+ reaches `onControl`, even under a custom label. See
183
+ `docs/data-surface-conformance.md` for the full contract.
184
+
147
185
  The default prefix is `smrt_ui_`. Configure `ui.prefix` when multiple Providers
148
186
  must coexist in one document; the same prefix cannot be registered twice. The
149
187
  six derived names are reserved through the document-global tool-name lock, so
@@ -306,7 +344,7 @@ importable, even if it appears in `dist/`.
306
344
  | `@happyvertical/smrt-svelte/workspace/live` | `systemFeed` — the AdminShell system scope (jobs/schedules/dispatch) polled from an app status endpoint; deliberately carries no `smrt-web` dependency |
307
345
  | `@happyvertical/smrt-svelte/browser-ai` | Browser AI client (STT/TTS/LLM adapters, capability detection) |
308
346
  | `@happyvertical/smrt-svelte/browser-ai/svelte` | Svelte AI components (VoiceInput, CapabilityGate, etc.) |
309
- | `@happyvertical/smrt-svelte/web` | `smrt-web` live-query bindings (`liveCollection`, `activityFeed`, `useUpdateAvailable`) |
347
+ | `@happyvertical/smrt-svelte/web` | `smrt-web` live-query bindings (`liveCollection`, `activityFeed`, `useUpdateAvailable`) plus `mountListDataSurface` (custom-list data-surface registration) |
310
348
  | `@happyvertical/smrt-svelte/i18n/server` | Server-side i18n resolver (Node only) |
311
349
 
312
350
  Domain-agnostic UI lives in `@happyvertical/smrt-ui`. There is no `ui`,
@@ -35,6 +35,31 @@ identity and action content belongs in `topLeftCorner` / `topRightCorner`,
35
35
  wrapped in `ShellCorner side="left"` or `ShellCorner side="right"` so it follows
36
36
  the corresponding shell track as that edge expands and collapses.
37
37
 
38
+ ## AssistantDock recipe (#2904)
39
+
40
+ `ShellDockTool` also hosts `@happyvertical/smrt-chat/svelte`'s `AssistantDock`
41
+ — a route-aware assistant surface. This package has no runtime dependency on
42
+ `@happyvertical/smrt-chat` (only a devDependency, for tests); the composition
43
+ below is intentionally application code, not a `workspace/` export:
44
+
45
+ ```svelte
46
+ <script lang="ts">
47
+ import { ShellDockTool } from '@happyvertical/smrt-svelte/workspace';
48
+ import { AssistantDock } from '@happyvertical/smrt-chat/svelte';
49
+ </script>
50
+
51
+ <ShellDockTool id="assistant" label="Assistant" icon="bot">
52
+ {#snippet render()}
53
+ <AssistantDock {transport} {registry} />
54
+ {/snippet}
55
+ </ShellDockTool>
56
+ ```
57
+
58
+ `registry` is the same `DataSurfaceRegistry` instance mounted routes register
59
+ their descriptors on. See
60
+ [`docs/assistant-dock.md`](../../../../../docs/assistant-dock.md) for the full
61
+ design.
62
+
38
63
  ## Migration
39
64
 
40
65
  See [MIGRATION.md](./MIGRATION.md) for the first-generation workspace migration
@@ -0,0 +1,204 @@
1
+ // @vitest-environment jsdom
2
+ /**
3
+ * AssistantDock conformance-style integration test (#2904).
4
+ *
5
+ * Mirrors `data-surface-conformance.integration.svelte.test.ts` in this
6
+ * directory: a real `DataSurfaceRegistry` and a real
7
+ * `createAssistantDockController`/`AssistantDock` are composed together;
8
+ * only the chat transport and the action client are in-process test doubles
9
+ * (`createInMemoryAssistantTransport`, and a thin `actionClient` adapter over
10
+ * the registry's own `execute()`), matching that file's stated scope of
11
+ * faking only the transport boundary.
12
+ *
13
+ * Scope note (disclosed deviation from the original test plan): the action
14
+ * preview/apply path here is driven directly through
15
+ * `createAssistantDockController` rather than by having a fake model reply
16
+ * trigger it through the rendered `AssistantDock` DOM — `AssistantDock` has
17
+ * no built-in "parse this assistant message as an action proposal" step (no
18
+ * such parsing is specified anywhere in #2904's binding decisions), so the
19
+ * host application is expected to call `controller.previewAction(...)` from
20
+ * its own message-rendering logic. This test still exercises the full
21
+ * dock (registry discovery, rendering, send/poll) through the DOM, and
22
+ * separately proves the action pathway (preview → confirm → apply → registry
23
+ * "command" event) end-to-end against the real registry.
24
+ */
25
+ import { AssistantDock, createAssistantDockController, createInMemoryAssistantTransport, } from '@happyvertical/smrt-chat/svelte';
26
+ import { createDataSurfaceRegistry, } from '@happyvertical/smrt-ui/data-surface';
27
+ import { render, screen, waitFor } from '@testing-library/svelte';
28
+ import { describe, expect, it } from 'vitest';
29
+ const identity = {
30
+ surfaceId: 'assistant-dock-orders',
31
+ kind: 'table',
32
+ subject: { type: 'tenant', id: 'tenant-a' },
33
+ };
34
+ const descriptor = {
35
+ version: 1,
36
+ identity,
37
+ schemaVersion: 1,
38
+ label: 'Orders',
39
+ rowKey: 'id',
40
+ columns: [
41
+ {
42
+ id: 'id',
43
+ label: 'ID',
44
+ capabilities: ['read', 'project'],
45
+ role: 'row-key',
46
+ },
47
+ ],
48
+ query: {
49
+ modes: ['rows'],
50
+ projectableColumnIds: ['id'],
51
+ searchableColumnIds: [],
52
+ filterableColumnIds: [],
53
+ sortableColumnIds: [],
54
+ },
55
+ actions: [
56
+ {
57
+ id: 'archive',
58
+ label: 'Archive',
59
+ selectionScopes: ['explicit-ids'],
60
+ requiresConfirmation: true,
61
+ },
62
+ ],
63
+ controls: [{ id: 'data-surface.action.archive', label: 'Archive' }],
64
+ limits: { maxQueryRows: 100, maxQueryBytes: 100_000, maxSelectionSize: 100 },
65
+ };
66
+ function mountRegistryWithOneSurface() {
67
+ const registry = createDataSurfaceRegistry();
68
+ let revision = 1;
69
+ const state = { archived: false };
70
+ const unregister = registry.register({
71
+ descriptor,
72
+ getSnapshot: () => ({
73
+ version: 1,
74
+ descriptor,
75
+ revision,
76
+ state: { archived: state.archived },
77
+ }),
78
+ execute: async (command) => {
79
+ // A minimal real mutation: the "archive" action flips `archived` and
80
+ // bumps the snapshot revision, which is what makes the registry emit a
81
+ // 'command' event other mounted UI can react to.
82
+ state.archived = true;
83
+ revision += 1;
84
+ return {
85
+ version: 1,
86
+ commandId: command.commandId,
87
+ identity: command.identity,
88
+ ok: true,
89
+ revision,
90
+ };
91
+ },
92
+ });
93
+ return { registry, unregister, state: () => state.archived };
94
+ }
95
+ describe('AssistantDock integration (#2904)', () => {
96
+ it('fails closed and shows the no-surfaces notice with an empty registry', async () => {
97
+ const registry = createDataSurfaceRegistry();
98
+ const transport = createInMemoryAssistantTransport();
99
+ render(AssistantDock, { props: { transport, registry } });
100
+ await waitFor(() => {
101
+ expect(screen.getByText(/No data surfaces are mounted on this route/i)).toBeInTheDocument();
102
+ });
103
+ });
104
+ it('discovers a mounted surface via the registry and renders a sent conversation', async () => {
105
+ const { registry } = mountRegistryWithOneSurface();
106
+ const transport = createInMemoryAssistantTransport({
107
+ respond: (_threadId, userMessage) => ({
108
+ id: 'assistant-1',
109
+ threadId: userMessage.threadId,
110
+ content: 'Archived the order.',
111
+ role: 'assistant',
112
+ createdAt: new Date(),
113
+ }),
114
+ });
115
+ render(AssistantDock, { props: { transport, registry } });
116
+ await waitFor(() => {
117
+ expect(screen.queryByText(/No data surfaces are mounted on this route/i)).not.toBeInTheDocument();
118
+ });
119
+ const thread = await transport.createThread('Order question');
120
+ // Drive the same controller the component owns internally by exercising
121
+ // the transport directly (the composer's own send() path is covered at
122
+ // the unit level in packages/chat); here we assert the descriptor is
123
+ // visible to a controller built against this exact registry instance.
124
+ const controller = createAssistantDockController({ transport, registry });
125
+ await controller.openThread(thread.id);
126
+ // Cycle-3 second final finding 1: an attached file must round-trip onto
127
+ // the sent message, not just the message text — this is the surface
128
+ // that previously rendered nothing for message.attachments.
129
+ await controller.send('please archive this order', [
130
+ { id: 'att-1', name: 'order-details.pdf' },
131
+ ]);
132
+ await waitFor(() => {
133
+ expect(controller.messages.some((m) => m.content === 'Archived the order.')).toBe(true);
134
+ });
135
+ const sentMessage = controller.messages.find((m) => m.content === 'please archive this order');
136
+ expect(sentMessage?.attachments).toEqual([
137
+ { id: 'att-1', name: 'order-details.pdf' },
138
+ ]);
139
+ expect(controller.surfaces).toHaveLength(1);
140
+ expect(controller.surfaces[0].surfaceId).toBe('assistant-dock-orders');
141
+ controller.dispose();
142
+ });
143
+ it('drives preview → confirm → apply through the real registry and fires its "command" event', async () => {
144
+ const { registry, state } = mountRegistryWithOneSurface();
145
+ const events = [];
146
+ registry.subscribe((event) => events.push(event));
147
+ const actionClient = {
148
+ async preview(request) {
149
+ // A preview never mutates: assert the surface is untouched.
150
+ expect(state()).toBe(false);
151
+ return {
152
+ version: 1,
153
+ requestId: request.requestId,
154
+ identity: request.identity,
155
+ actionId: request.actionId,
156
+ phase: 'preview',
157
+ ok: true,
158
+ confirmationToken: 'token-1',
159
+ };
160
+ },
161
+ async apply(request) {
162
+ const result = await registry.execute({
163
+ version: 1,
164
+ commandId: request.requestId,
165
+ identity: request.identity,
166
+ expectedRevision: 1,
167
+ controlId: `data-surface.action.${request.actionId}`,
168
+ });
169
+ return {
170
+ version: 1,
171
+ requestId: request.requestId,
172
+ identity: request.identity,
173
+ actionId: request.actionId,
174
+ phase: 'apply',
175
+ ok: result.ok,
176
+ reason: result.ok ? undefined : result.reason,
177
+ };
178
+ },
179
+ };
180
+ const controller = createAssistantDockController({
181
+ transport: createInMemoryAssistantTransport(),
182
+ registry,
183
+ actionClient,
184
+ });
185
+ const requestId = 'archive-req-1';
186
+ await controller.previewAction({
187
+ version: 1,
188
+ requestId,
189
+ identity,
190
+ actionId: 'archive',
191
+ phase: 'preview',
192
+ selection: { scope: 'explicit-ids', rowIds: ['order-1'] },
193
+ });
194
+ expect(controller.actions.get(requestId)?.status).toBe('previewed');
195
+ expect(state()).toBe(false); // preview never mutated the surface
196
+ // applyAction now takes only requestId — it reuses the idempotencyKey
197
+ // minted once by previewAction (binding decision #2904, build phase 2).
198
+ await controller.applyAction(requestId);
199
+ expect(controller.actions.get(requestId)?.status).toBe('applied');
200
+ expect(state()).toBe(true);
201
+ expect(events.some((e) => e.type === 'command')).toBe(true);
202
+ controller.dispose();
203
+ });
204
+ });
@@ -0,0 +1,59 @@
1
+ <script lang="ts">
2
+ import type {
3
+ DataSurfaceDescriptor,
4
+ DataSurfaceRegistry,
5
+ DataTableController,
6
+ } from '@happyvertical/smrt-ui/data';
7
+ import {
8
+ type ListDataSurfaceContext,
9
+ mountListDataSurface,
10
+ } from '../list-data-surface.svelte.js';
11
+
12
+ let {
13
+ registry,
14
+ descriptor,
15
+ controller,
16
+ context,
17
+ onRefresh,
18
+ onStar,
19
+ }: {
20
+ registry: DataSurfaceRegistry;
21
+ descriptor: DataSurfaceDescriptor;
22
+ controller: DataTableController;
23
+ context: ListDataSurfaceContext;
24
+ onRefresh?: () => boolean | Promise<boolean>;
25
+ onStar?: (payload: unknown) => boolean;
26
+ } = $props();
27
+
28
+ let starredCount = $state(0);
29
+ let filterText = $state('');
30
+
31
+ $effect(() => {
32
+ const handle = mountListDataSurface({
33
+ registry,
34
+ descriptor,
35
+ controller,
36
+ context,
37
+ refresh: onRefresh,
38
+ onControl: (controlId, payload) => {
39
+ if (controlId === 'star') {
40
+ starredCount += 1;
41
+ return onStar ? onStar(payload) : true;
42
+ }
43
+ return false;
44
+ },
45
+ });
46
+ const unsubscribe = controller.subscribe(() => {
47
+ filterText = String(controller.getState().filters[0]?.value ?? '');
48
+ });
49
+ return () => {
50
+ unsubscribe();
51
+ handle.destroy();
52
+ };
53
+ });
54
+ </script>
55
+
56
+ <ul data-testid="custom-list">
57
+ <li data-testid="filter-text">{filterText}</li>
58
+ </ul>
59
+ <p data-testid="starred-count">{starredCount}</p>
@@ -0,0 +1,14 @@
1
+ import type { DataSurfaceDescriptor, DataSurfaceRegistry, DataTableController } from '@happyvertical/smrt-ui/data';
2
+ import { type ListDataSurfaceContext } from '../list-data-surface.svelte.js';
3
+ type $$ComponentProps = {
4
+ registry: DataSurfaceRegistry;
5
+ descriptor: DataSurfaceDescriptor;
6
+ controller: DataTableController;
7
+ context: ListDataSurfaceContext;
8
+ onRefresh?: () => boolean | Promise<boolean>;
9
+ onStar?: (payload: unknown) => boolean;
10
+ };
11
+ declare const ListDataSurface: import("svelte").Component<$$ComponentProps, {}, "">;
12
+ type ListDataSurface = ReturnType<typeof ListDataSurface>;
13
+ export default ListDataSurface;
14
+ //# sourceMappingURL=list-data-surface.fixture.svelte.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"list-data-surface.fixture.svelte.d.ts","sourceRoot":"","sources":["../../../src/web/__tests__/list-data-surface.fixture.svelte.ts"],"names":[],"mappings":"AAGA,OAAO,KAAK,EACV,qBAAqB,EACrB,mBAAmB,EACnB,mBAAmB,EACpB,MAAM,6BAA6B,CAAC;AACrC,OAAO,EACL,KAAK,sBAAsB,EAE5B,MAAM,gCAAgC,CAAC;AAEvC,KAAK,gBAAgB,GAAI;IACxB,QAAQ,EAAE,mBAAmB,CAAC;IAC9B,UAAU,EAAE,qBAAqB,CAAC;IAClC,UAAU,EAAE,mBAAmB,CAAC;IAChC,OAAO,EAAE,sBAAsB,CAAC;IAChC,SAAS,CAAC,EAAE,MAAM,OAAO,GAAG,OAAO,CAAC,OAAO,CAAC,CAAC;IAC7C,MAAM,CAAC,EAAE,CAAC,OAAO,EAAE,OAAO,KAAK,OAAO,CAAC;CACxC,CAAC;AAiDF,QAAA,MAAM,eAAe,sDAAwC,CAAC;AAC9D,KAAK,eAAe,GAAG,UAAU,CAAC,OAAO,eAAe,CAAC,CAAC;AAC1D,eAAe,eAAe,CAAC"}
@@ -0,0 +1,106 @@
1
+ /**
2
+ * Component-lifecycle proof for `mountListDataSurface` (#2906), in the same
3
+ * conformance style as `data-surface-conformance.integration.svelte.test.ts`:
4
+ * a real Svelte component mounts a custom (non-DataTable) list, the registry
5
+ * drives it with commands, and unmounting the component tears the
6
+ * registration down. The registration/translation logic itself is unit
7
+ * tested directly against the registry in `list-data-surface.test.ts`.
8
+ */
9
+ import { createDataSurfaceRegistry, createDataTableController, } from '@happyvertical/smrt-ui/data';
10
+ import { render, screen } from '@testing-library/svelte';
11
+ import { describe, expect, it, vi } from 'vitest';
12
+ import Fixture from './list-data-surface.fixture.svelte';
13
+ const identity = {
14
+ surfaceId: 'fixture-list',
15
+ kind: 'list',
16
+ };
17
+ function descriptor() {
18
+ return {
19
+ version: 1,
20
+ identity,
21
+ schemaVersion: 1,
22
+ label: 'Fixture list',
23
+ rowKey: 'id',
24
+ columns: [
25
+ { id: 'id', label: 'ID', capabilities: ['read', 'project'] },
26
+ {
27
+ id: 'title',
28
+ label: 'Title',
29
+ capabilities: ['read', 'search', 'filter', 'sort', 'project'],
30
+ operators: { filter: ['contains'] },
31
+ },
32
+ ],
33
+ query: {
34
+ modes: ['rows', 'count'],
35
+ projectableColumnIds: ['id', 'title'],
36
+ filterableColumnIds: ['title'],
37
+ sortableColumnIds: ['title'],
38
+ },
39
+ controls: [
40
+ { id: 'set-filters', label: 'Filter' },
41
+ { id: 'refresh', label: 'Refresh' },
42
+ { id: 'star', label: 'Star selected' },
43
+ ],
44
+ actions: [],
45
+ limits: { maxQueryRows: 50, maxQueryBytes: 10_000, maxSelectionSize: 5 },
46
+ };
47
+ }
48
+ describe('mountListDataSurface (component integration)', () => {
49
+ it('drives a mounted custom list from the registry and tears down on unmount', async () => {
50
+ const registry = createDataSurfaceRegistry();
51
+ const controller = createDataTableController();
52
+ const onRefresh = vi.fn().mockResolvedValue(true);
53
+ const { unmount } = render(Fixture, {
54
+ props: {
55
+ registry,
56
+ descriptor: descriptor(),
57
+ controller,
58
+ context: { totalRows: 3, queryFingerprint: 'fixture-1' },
59
+ onRefresh,
60
+ },
61
+ });
62
+ expect(screen.getByTestId('filter-text').textContent).toBe('');
63
+ const before = registry.inspect(identity);
64
+ expect(before?.state.totalRows).toBe(3);
65
+ const filtered = await registry.execute({
66
+ version: 1,
67
+ commandId: 'filter-1',
68
+ identity,
69
+ expectedRevision: before?.revision ?? 0,
70
+ controlId: 'set-filters',
71
+ payload: {
72
+ filters: [{ columnId: 'title', operator: 'contains', value: 'draft' }],
73
+ },
74
+ });
75
+ expect(filtered.ok).toBe(true);
76
+ expect(screen.getByTestId('filter-text').textContent).toBe('draft');
77
+ const refreshed = await registry.execute({
78
+ version: 1,
79
+ commandId: 'refresh-1',
80
+ identity,
81
+ expectedRevision: registry.inspect(identity)?.revision ?? 0,
82
+ controlId: 'refresh',
83
+ });
84
+ expect(refreshed.ok).toBe(true);
85
+ expect(onRefresh).toHaveBeenCalledOnce();
86
+ const starred = await registry.execute({
87
+ version: 1,
88
+ commandId: 'star-1',
89
+ identity,
90
+ expectedRevision: registry.inspect(identity)?.revision ?? 0,
91
+ controlId: 'star',
92
+ });
93
+ expect(starred.ok).toBe(true);
94
+ expect(screen.getByTestId('starred-count').textContent).toBe('1');
95
+ unmount();
96
+ // Unmounting tore down the mounted surface — the identity is gone.
97
+ expect(registry.inspect(identity)).toBeUndefined();
98
+ await expect(registry.execute({
99
+ version: 1,
100
+ commandId: 'after-unmount',
101
+ identity,
102
+ expectedRevision: 0,
103
+ controlId: 'refresh',
104
+ })).resolves.toMatchObject({ ok: false, reason: 'not_found' });
105
+ });
106
+ });