@happyvertical/smrt-svelte 0.43.4 → 0.43.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 +23 -0
- package/README.md +23 -1
- package/dist/components/forms/AddressInput.svelte +155 -42
- package/dist/components/forms/AddressInput.svelte.d.ts.map +1 -1
- package/dist/components/forms/CheckboxInput.svelte +40 -13
- package/dist/components/forms/CheckboxInput.svelte.d.ts.map +1 -1
- package/dist/components/forms/DateRangeInput.svelte +132 -16
- package/dist/components/forms/DateRangeInput.svelte.d.ts.map +1 -1
- package/dist/components/forms/DateTimeInput.svelte +48 -14
- package/dist/components/forms/DateTimeInput.svelte.d.ts.map +1 -1
- package/dist/components/forms/Form.svelte +599 -70
- package/dist/components/forms/Form.svelte.d.ts +6 -2
- package/dist/components/forms/Form.svelte.d.ts.map +1 -1
- package/dist/components/forms/MeasurementInput.svelte +137 -31
- package/dist/components/forms/MeasurementInput.svelte.d.ts.map +1 -1
- package/dist/components/forms/MoneyInput.svelte +42 -23
- package/dist/components/forms/MoneyInput.svelte.d.ts.map +1 -1
- package/dist/components/forms/NumberInput.svelte +59 -27
- package/dist/components/forms/NumberInput.svelte.d.ts.map +1 -1
- package/dist/components/forms/PhoneInput.svelte +51 -22
- package/dist/components/forms/PhoneInput.svelte.d.ts.map +1 -1
- package/dist/components/forms/SelectInput.svelte +38 -22
- package/dist/components/forms/SelectInput.svelte.d.ts.map +1 -1
- package/dist/components/forms/TextInput.svelte +32 -18
- package/dist/components/forms/TextInput.svelte.d.ts.map +1 -1
- package/dist/components/forms/TextareaInput.svelte +25 -18
- package/dist/components/forms/TextareaInput.svelte.d.ts.map +1 -1
- package/dist/components/forms/__tests__/AddressInput.behavior.test.js +94 -0
- package/dist/components/forms/__tests__/Form.behavior.test.js +1 -1
- package/dist/components/forms/__tests__/Form.extraction.test.js +68 -0
- package/dist/components/forms/__tests__/Form.test.js +28 -0
- package/dist/components/forms/__tests__/Form.webmcp.test.js +2123 -114
- package/dist/components/forms/__tests__/FormMicButton.test.js +0 -1
- package/dist/components/forms/__tests__/async-invalid-field.fixture.svelte +30 -0
- package/dist/components/forms/__tests__/async-invalid-field.fixture.svelte.d.ts +7 -0
- package/dist/components/forms/__tests__/async-invalid-field.fixture.svelte.d.ts.map +1 -0
- package/dist/components/forms/__tests__/async-validation-form.fixture.svelte +19 -0
- package/dist/components/forms/__tests__/async-validation-form.fixture.svelte.d.ts +9 -0
- package/dist/components/forms/__tests__/async-validation-form.fixture.svelte.d.ts.map +1 -0
- package/dist/components/forms/__tests__/async-validation-snapshot-form.fixture.svelte +21 -0
- package/dist/components/forms/__tests__/async-validation-snapshot-form.fixture.svelte.d.ts +8 -0
- package/dist/components/forms/__tests__/async-validation-snapshot-form.fixture.svelte.d.ts.map +1 -0
- package/dist/components/forms/__tests__/compound-registration-wrapper.fixture.svelte +27 -0
- package/dist/components/forms/__tests__/compound-registration-wrapper.fixture.svelte.d.ts +9 -0
- package/dist/components/forms/__tests__/compound-registration-wrapper.fixture.svelte.d.ts.map +1 -0
- package/dist/components/forms/__tests__/custom-registration-field.fixture.svelte +48 -0
- package/dist/components/forms/__tests__/custom-registration-field.fixture.svelte.d.ts +10 -0
- package/dist/components/forms/__tests__/custom-registration-field.fixture.svelte.d.ts.map +1 -0
- package/dist/components/forms/__tests__/form-extraction-values.fixture.svelte +26 -0
- package/dist/components/forms/__tests__/form-extraction-values.fixture.svelte.d.ts +13 -0
- package/dist/components/forms/__tests__/form-extraction-values.fixture.svelte.d.ts.map +1 -0
- package/dist/components/forms/__tests__/form-registration-lifecycle.fixture.svelte +52 -0
- package/dist/components/forms/__tests__/form-registration-lifecycle.fixture.svelte.d.ts +16 -0
- package/dist/components/forms/__tests__/form-registration-lifecycle.fixture.svelte.d.ts.map +1 -0
- package/dist/components/forms/__tests__/form-with-fields.fixture.svelte +108 -5
- package/dist/components/forms/__tests__/form-with-fields.fixture.svelte.d.ts +34 -0
- package/dist/components/forms/__tests__/form-with-fields.fixture.svelte.d.ts.map +1 -1
- package/dist/components/forms/__tests__/form-with-policy-field.fixture.svelte +24 -0
- package/dist/components/forms/__tests__/form-with-policy-field.fixture.svelte.d.ts +11 -0
- package/dist/components/forms/__tests__/form-with-policy-field.fixture.svelte.d.ts.map +1 -0
- package/dist/components/forms/__tests__/form-with-structured-fields.fixture.svelte +69 -4
- package/dist/components/forms/__tests__/form-with-structured-fields.fixture.svelte.d.ts +32 -0
- package/dist/components/forms/__tests__/form-with-structured-fields.fixture.svelte.d.ts.map +1 -1
- package/dist/components/forms/__tests__/legacy-form-context.fixture.svelte +44 -0
- package/dist/components/forms/__tests__/legacy-form-context.fixture.svelte.d.ts +9 -0
- package/dist/components/forms/__tests__/legacy-form-context.fixture.svelte.d.ts.map +1 -0
- package/dist/components/forms/__tests__/policy-field.fixture.svelte +35 -0
- package/dist/components/forms/__tests__/policy-field.fixture.svelte.d.ts +8 -0
- package/dist/components/forms/__tests__/policy-field.fixture.svelte.d.ts.map +1 -0
- package/dist/components/forms/prepare-field-value.d.ts +5 -0
- package/dist/components/forms/prepare-field-value.d.ts.map +1 -0
- package/dist/components/forms/prepare-field-value.js +15 -0
- package/dist/state/form-context.d.ts +28 -6
- package/dist/state/form-context.d.ts.map +1 -1
- package/dist/state/form-context.js +75 -7
- package/dist/web/__tests__/webmcp-composed.child.fixture.svelte +62 -0
- package/dist/web/__tests__/webmcp-composed.child.fixture.svelte.d.ts +8 -0
- package/dist/web/__tests__/webmcp-composed.child.fixture.svelte.d.ts.map +1 -0
- package/dist/web/__tests__/webmcp-composed.fixture.svelte +52 -0
- package/dist/web/__tests__/webmcp-composed.fixture.svelte.d.ts +12 -0
- package/dist/web/__tests__/webmcp-composed.fixture.svelte.d.ts.map +1 -0
- package/dist/web/__tests__/webmcp-composed.integration.svelte.test.js +247 -0
- package/dist/web/__tests__/webmcp-ssr.fixture.svelte +13 -0
- package/dist/web/__tests__/webmcp-ssr.fixture.svelte.d.ts +19 -0
- package/dist/web/__tests__/webmcp-ssr.fixture.svelte.d.ts.map +1 -0
- package/dist/web/__tests__/webmcp-ui.test.js +209 -33
- package/dist/web/__tests__/webmcp.test.js +20 -0
- package/dist/web/webmcp-ui.d.ts.map +1 -1
- package/dist/web/webmcp-ui.js +50 -31
- package/dist/web/webmcp.svelte.d.ts.map +1 -1
- package/dist/web/webmcp.svelte.js +6 -1
- package/package.json +8 -6
|
@@ -0,0 +1,247 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Mounted UI half of the application composition fixture (#2523).
|
|
3
|
+
*
|
|
4
|
+
* A real Provider owns a Form and DataTable at once. This catches lifecycle
|
|
5
|
+
* mistakes that isolated registry tests miss: controls must be discoverable,
|
|
6
|
+
* the DataSurface must be present, and an agent stage request must not apply a
|
|
7
|
+
* value without a human gesture.
|
|
8
|
+
*/
|
|
9
|
+
var __decorate = (this && this.__decorate) || function (decorators, target, key, desc) {
|
|
10
|
+
var c = arguments.length, r = c < 3 ? target : desc === null ? desc = Object.getOwnPropertyDescriptor(target, key) : desc, d;
|
|
11
|
+
if (typeof Reflect === "object" && typeof Reflect.decorate === "function") r = Reflect.decorate(decorators, target, key, desc);
|
|
12
|
+
else for (var i = decorators.length - 1; i >= 0; i--) if (d = decorators[i]) r = (c < 3 ? d(r) : c > 3 ? d(target, key, r) : d(target, key)) || r;
|
|
13
|
+
return c > 3 && r && Object.defineProperty(target, key, r), r;
|
|
14
|
+
};
|
|
15
|
+
import { APIGenerator, field, ObjectRegistry, SmrtCollection, SmrtObject, smrt, } from '@happyvertical/smrt-core';
|
|
16
|
+
import { ManifestGenerator } from '@happyvertical/smrt-core/scanner';
|
|
17
|
+
import { getTestDatabase } from '@happyvertical/smrt-core/testing';
|
|
18
|
+
import { ManifestAdapter, OxcScanner } from '@happyvertical/smrt-scanner';
|
|
19
|
+
import { createDataSurfaceRegistry } from '@happyvertical/smrt-ui/data';
|
|
20
|
+
import { svelte } from '@sveltejs/vite-plugin-svelte';
|
|
21
|
+
import { render } from '@testing-library/svelte';
|
|
22
|
+
import { tick } from 'svelte';
|
|
23
|
+
import { createServer } from 'vite';
|
|
24
|
+
import { afterAll, afterEach, beforeAll, describe, expect, it, vi, } from 'vitest';
|
|
25
|
+
import { buildWebMcpToolDefinitions } from '../../../../core/src/vite-plugin/web-collections.js';
|
|
26
|
+
import Fixture from './webmcp-composed.fixture.svelte';
|
|
27
|
+
import SsrFixture from './webmcp-ssr.fixture.svelte';
|
|
28
|
+
let WebMcpComposedFixtureItem = class WebMcpComposedFixtureItem extends SmrtObject {
|
|
29
|
+
name = '';
|
|
30
|
+
};
|
|
31
|
+
__decorate([
|
|
32
|
+
field({ type: 'text' })
|
|
33
|
+
], WebMcpComposedFixtureItem.prototype, "name", void 0);
|
|
34
|
+
WebMcpComposedFixtureItem = __decorate([
|
|
35
|
+
smrt({ api: { include: ['get'] } })
|
|
36
|
+
], WebMcpComposedFixtureItem);
|
|
37
|
+
class WebMcpComposedFixtureItemCollection extends SmrtCollection {
|
|
38
|
+
static _itemClass = WebMcpComposedFixtureItem;
|
|
39
|
+
}
|
|
40
|
+
ObjectRegistry.registerCollection('WebMcpComposedFixtureItem', WebMcpComposedFixtureItemCollection);
|
|
41
|
+
afterEach(() => {
|
|
42
|
+
delete document.modelContext;
|
|
43
|
+
});
|
|
44
|
+
describe('composed Provider WebMCP surface (#2523)', () => {
|
|
45
|
+
let generatedReadDefinition;
|
|
46
|
+
let generatedRowId = '';
|
|
47
|
+
let db;
|
|
48
|
+
let handler;
|
|
49
|
+
const baseUrl = 'http://fixture.local/api/v1';
|
|
50
|
+
beforeAll(async () => {
|
|
51
|
+
const scanner = new OxcScanner({
|
|
52
|
+
cwd: process.cwd(),
|
|
53
|
+
include: ['src/web/__tests__/webmcp-composed.integration.svelte.test.ts'],
|
|
54
|
+
exclude: [],
|
|
55
|
+
followImports: true,
|
|
56
|
+
baseClasses: ['SmrtObject', 'SmrtCollection'],
|
|
57
|
+
includeStaticMethods: true,
|
|
58
|
+
});
|
|
59
|
+
const { results, resolved } = await scanner.scanAndResolve();
|
|
60
|
+
const manifest = new ManifestAdapter().toManifest(resolved, {
|
|
61
|
+
packageName: '@fixture/smrt-svelte',
|
|
62
|
+
typeAliases: results.typeAliases,
|
|
63
|
+
});
|
|
64
|
+
new ManifestGenerator().applyGenerationPasses(manifest, {
|
|
65
|
+
packageName: '@fixture/smrt-svelte',
|
|
66
|
+
});
|
|
67
|
+
const generated = buildWebMcpToolDefinitions(manifest).find((definition) => definition.className === 'WebMcpComposedFixtureItem' &&
|
|
68
|
+
definition.action === 'get');
|
|
69
|
+
if (!generated)
|
|
70
|
+
throw new Error('scanner did not emit the fixture get tool');
|
|
71
|
+
generatedReadDefinition = generated;
|
|
72
|
+
db = await getTestDatabase({
|
|
73
|
+
type: 'sqlite',
|
|
74
|
+
url: ':memory:',
|
|
75
|
+
classes: ['WebMcpComposedFixtureItem'],
|
|
76
|
+
});
|
|
77
|
+
const collection = await WebMcpComposedFixtureItemCollection.create({ db });
|
|
78
|
+
const row = await collection.create({ name: 'generated read' });
|
|
79
|
+
generatedRowId = row.id;
|
|
80
|
+
const api = new APIGenerator({
|
|
81
|
+
basePath: '/api/v1',
|
|
82
|
+
authMiddleware: () => async (request) => {
|
|
83
|
+
if (request.headers.get('authorization') !== 'Bearer fixture-user') {
|
|
84
|
+
return new Response('auth required', { status: 401 });
|
|
85
|
+
}
|
|
86
|
+
return request;
|
|
87
|
+
},
|
|
88
|
+
}, { db });
|
|
89
|
+
api.registerCollection('webmcpcomposedfixtureitems', collection);
|
|
90
|
+
handler = api.generateHandler();
|
|
91
|
+
});
|
|
92
|
+
afterAll(async () => {
|
|
93
|
+
await db?.close?.();
|
|
94
|
+
});
|
|
95
|
+
function authenticatedFetch() {
|
|
96
|
+
return (async (input, init) => {
|
|
97
|
+
const requestUrl = new URL(String(input), `${baseUrl}/`);
|
|
98
|
+
const headers = new Headers(init?.headers);
|
|
99
|
+
headers.set('authorization', 'Bearer fixture-user');
|
|
100
|
+
return handler(new Request(requestUrl, { ...init, headers }));
|
|
101
|
+
});
|
|
102
|
+
}
|
|
103
|
+
it('keeps the composed UI render safe without browser modelContext support', async () => {
|
|
104
|
+
const dataSurfaceRegistry = createDataSurfaceRegistry();
|
|
105
|
+
const view = render(Fixture, { props: { dataSurfaceRegistry } });
|
|
106
|
+
await tick();
|
|
107
|
+
expect(dataSurfaceRegistry.list()).toHaveLength(1);
|
|
108
|
+
view.unmount();
|
|
109
|
+
expect(dataSurfaceRegistry.list()).toEqual([]);
|
|
110
|
+
});
|
|
111
|
+
it('renders the Provider composition through the server runtime', async () => {
|
|
112
|
+
const vite = await createServer({
|
|
113
|
+
appType: 'custom',
|
|
114
|
+
configFile: false,
|
|
115
|
+
plugins: [svelte()],
|
|
116
|
+
root: process.cwd(),
|
|
117
|
+
server: { middlewareMode: true },
|
|
118
|
+
});
|
|
119
|
+
const originalDocument = globalThis.document;
|
|
120
|
+
try {
|
|
121
|
+
delete globalThis.document;
|
|
122
|
+
const { default: ssrFixture } = await vite.ssrLoadModule('/src/web/__tests__/webmcp-ssr.fixture.svelte');
|
|
123
|
+
const { render: renderSsr } = await vite.ssrLoadModule('svelte/server');
|
|
124
|
+
const result = renderSsr(ssrFixture);
|
|
125
|
+
expect(result.body).toContain('webmcp-ssr-fixture');
|
|
126
|
+
}
|
|
127
|
+
finally {
|
|
128
|
+
if (originalDocument === undefined) {
|
|
129
|
+
delete globalThis.document;
|
|
130
|
+
}
|
|
131
|
+
else {
|
|
132
|
+
globalThis.document = originalDocument;
|
|
133
|
+
}
|
|
134
|
+
await vite.close();
|
|
135
|
+
}
|
|
136
|
+
});
|
|
137
|
+
it('registers the mounted Form and DataTable together and preserves consent', async () => {
|
|
138
|
+
const registered = [];
|
|
139
|
+
document.modelContext = {
|
|
140
|
+
async registerTool(tool, options) {
|
|
141
|
+
registered.push({ ...tool, signal: options?.signal });
|
|
142
|
+
},
|
|
143
|
+
};
|
|
144
|
+
const dataSurfaceRegistry = createDataSurfaceRegistry();
|
|
145
|
+
const fetchFn = authenticatedFetch();
|
|
146
|
+
const generatedDefinitions = [generatedReadDefinition];
|
|
147
|
+
const view = render(Fixture, {
|
|
148
|
+
props: {
|
|
149
|
+
dataSurfaceRegistry,
|
|
150
|
+
generatedDefinitions,
|
|
151
|
+
fetchFn,
|
|
152
|
+
},
|
|
153
|
+
});
|
|
154
|
+
await tick();
|
|
155
|
+
await tick();
|
|
156
|
+
await vi.waitFor(() => expect(registered.map((tool) => tool.name)).toContain(generatedReadDefinition.name));
|
|
157
|
+
expect(registered.map((tool) => tool.name)).toEqual(expect.arrayContaining([
|
|
158
|
+
'smrt_ui_list_form_controls',
|
|
159
|
+
'smrt_ui_inspect_form_control',
|
|
160
|
+
'smrt_ui_execute_form_control',
|
|
161
|
+
'smrt_ui_list_data_surfaces',
|
|
162
|
+
'smrt_ui_inspect_data_surface',
|
|
163
|
+
'smrt_ui_execute_data_surface_control',
|
|
164
|
+
'fixture_component_preview',
|
|
165
|
+
generatedReadDefinition.name,
|
|
166
|
+
]));
|
|
167
|
+
expect(registered).toHaveLength(8);
|
|
168
|
+
expect(dataSurfaceRegistry.list()).toHaveLength(1);
|
|
169
|
+
const titleInput = view.container.querySelector('input[name="title"]');
|
|
170
|
+
expect(titleInput?.value).toBe('');
|
|
171
|
+
const listControls = registered.find((tool) => tool.name === 'smrt_ui_list_form_controls');
|
|
172
|
+
if (!listControls)
|
|
173
|
+
throw new Error('list form-controls tool was not registered');
|
|
174
|
+
expect(JSON.parse(await listControls.execute({}))).toMatchObject({
|
|
175
|
+
ok: true,
|
|
176
|
+
result: [{ identity: { formId: 'fixture-form', controlId: 'title' } }],
|
|
177
|
+
});
|
|
178
|
+
const executeForm = registered.find((tool) => tool.name === 'smrt_ui_execute_form_control');
|
|
179
|
+
if (!executeForm)
|
|
180
|
+
throw new Error('execute form-control tool was not registered');
|
|
181
|
+
const staged = JSON.parse(await executeForm.execute({
|
|
182
|
+
action: 'stage',
|
|
183
|
+
identity: { formId: 'fixture-form', controlId: 'title' },
|
|
184
|
+
value: 'agent proposal',
|
|
185
|
+
}));
|
|
186
|
+
expect(staged).toMatchObject({ result: { ok: true, action: 'stage' } });
|
|
187
|
+
expect(titleInput?.value).toBe('');
|
|
188
|
+
const applied = JSON.parse(await executeForm.execute({
|
|
189
|
+
action: 'apply',
|
|
190
|
+
identity: { formId: 'fixture-form', controlId: 'title' },
|
|
191
|
+
}));
|
|
192
|
+
expect(applied).toMatchObject({
|
|
193
|
+
result: { ok: false, reason: 'human_confirmation_required' },
|
|
194
|
+
});
|
|
195
|
+
expect(titleInput?.value).toBe('');
|
|
196
|
+
const generated = registered.find((tool) => tool.name === generatedReadDefinition.name);
|
|
197
|
+
if (!generated)
|
|
198
|
+
throw new Error('generated Provider tool was not registered');
|
|
199
|
+
expect(JSON.parse(await generated.execute({ id: generatedRowId }))).toMatchObject({ name: 'generated read' });
|
|
200
|
+
const bespoke = registered.find((tool) => tool.name === 'fixture_component_preview');
|
|
201
|
+
if (!bespoke)
|
|
202
|
+
throw new Error('bespoke component tool was not registered');
|
|
203
|
+
expect(JSON.parse(await bespoke.execute({}))).toEqual({
|
|
204
|
+
ok: true,
|
|
205
|
+
source: 'bespoke-component',
|
|
206
|
+
});
|
|
207
|
+
await view.rerender({
|
|
208
|
+
dataSurfaceRegistry,
|
|
209
|
+
generatedDefinitions,
|
|
210
|
+
fetchFn,
|
|
211
|
+
showChild: false,
|
|
212
|
+
});
|
|
213
|
+
await tick();
|
|
214
|
+
await tick();
|
|
215
|
+
expect(dataSurfaceRegistry.list()).toEqual([]);
|
|
216
|
+
const firstRegistration = registered.slice();
|
|
217
|
+
expect(firstRegistration
|
|
218
|
+
.filter((tool) => !tool.signal?.aborted)
|
|
219
|
+
.map((tool) => tool.name)).toEqual([
|
|
220
|
+
'smrt_ui_list_form_controls',
|
|
221
|
+
'smrt_ui_inspect_form_control',
|
|
222
|
+
'smrt_ui_execute_form_control',
|
|
223
|
+
'smrt_ui_list_data_surfaces',
|
|
224
|
+
'smrt_ui_inspect_data_surface',
|
|
225
|
+
'smrt_ui_execute_data_surface_control',
|
|
226
|
+
generatedReadDefinition.name,
|
|
227
|
+
]);
|
|
228
|
+
await view.rerender({
|
|
229
|
+
dataSurfaceRegistry,
|
|
230
|
+
generatedDefinitions,
|
|
231
|
+
fetchFn,
|
|
232
|
+
showChild: true,
|
|
233
|
+
});
|
|
234
|
+
await tick();
|
|
235
|
+
await tick();
|
|
236
|
+
await vi.waitFor(() => expect(registered).toHaveLength(9));
|
|
237
|
+
expect(dataSurfaceRegistry.list()).toHaveLength(1);
|
|
238
|
+
const secondBespoke = registered.filter((tool) => tool.name === 'fixture_component_preview')[1];
|
|
239
|
+
if (!secondBespoke)
|
|
240
|
+
throw new Error('remounted bespoke tool was not registered');
|
|
241
|
+
expect(secondBespoke.name).toBe('fixture_component_preview');
|
|
242
|
+
expect(secondBespoke.signal?.aborted).toBe(false);
|
|
243
|
+
expect(registered.filter((tool) => tool.name === 'fixture_component_preview')).toHaveLength(2);
|
|
244
|
+
view.unmount();
|
|
245
|
+
expect(registered.every((tool) => tool.signal?.aborted)).toBe(true);
|
|
246
|
+
});
|
|
247
|
+
});
|
|
@@ -0,0 +1,13 @@
|
|
|
1
|
+
<script lang="ts">
|
|
2
|
+
import { createDataSurfaceRegistry } from '@happyvertical/smrt-ui/data';
|
|
3
|
+
import Provider from '../../Provider.svelte';
|
|
4
|
+
import Child from './webmcp-composed.child.fixture.svelte';
|
|
5
|
+
|
|
6
|
+
const dataSurfaceRegistry = createDataSurfaceRegistry();
|
|
7
|
+
</script>
|
|
8
|
+
|
|
9
|
+
<Provider webmcp={{ definitions: [], basePath: '/api', effects: ['read'] }}>
|
|
10
|
+
<div data-testid="webmcp-ssr-fixture">
|
|
11
|
+
<Child {dataSurfaceRegistry} />
|
|
12
|
+
</div>
|
|
13
|
+
</Provider>
|
|
@@ -0,0 +1,19 @@
|
|
|
1
|
+
interface $$__sveltets_2_IsomorphicComponent<Props extends Record<string, any> = any, Events extends Record<string, any> = any, Slots extends Record<string, any> = any, Exports = {}, Bindings = string> {
|
|
2
|
+
new (options: import('svelte').ComponentConstructorOptions<Props>): import('svelte').SvelteComponent<Props, Events, Slots> & {
|
|
3
|
+
$$bindings?: Bindings;
|
|
4
|
+
} & Exports;
|
|
5
|
+
(internal: unknown, props: {
|
|
6
|
+
$$events?: Events;
|
|
7
|
+
$$slots?: Slots;
|
|
8
|
+
}): Exports & {
|
|
9
|
+
$set?: any;
|
|
10
|
+
$on?: any;
|
|
11
|
+
};
|
|
12
|
+
z_$$bindings?: Bindings;
|
|
13
|
+
}
|
|
14
|
+
declare const WebmcpSsr: $$__sveltets_2_IsomorphicComponent<Record<string, never>, {
|
|
15
|
+
[evt: string]: CustomEvent<any>;
|
|
16
|
+
}, {}, {}, string>;
|
|
17
|
+
type WebmcpSsr = InstanceType<typeof WebmcpSsr>;
|
|
18
|
+
export default WebmcpSsr;
|
|
19
|
+
//# sourceMappingURL=webmcp-ssr.fixture.svelte.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"webmcp-ssr.fixture.svelte.d.ts","sourceRoot":"","sources":["../../../src/web/__tests__/webmcp-ssr.fixture.svelte.ts"],"names":[],"mappings":"AAuBA,UAAU,kCAAkC,CAAC,KAAK,SAAS,MAAM,CAAC,MAAM,EAAE,GAAG,CAAC,GAAG,GAAG,EAAE,MAAM,SAAS,MAAM,CAAC,MAAM,EAAE,GAAG,CAAC,GAAG,GAAG,EAAE,KAAK,SAAS,MAAM,CAAC,MAAM,EAAE,GAAG,CAAC,GAAG,GAAG,EAAE,OAAO,GAAG,EAAE,EAAE,QAAQ,GAAG,MAAM;IACpM,KAAK,OAAO,EAAE,OAAO,QAAQ,EAAE,2BAA2B,CAAC,KAAK,CAAC,GAAG,OAAO,QAAQ,EAAE,eAAe,CAAC,KAAK,EAAE,MAAM,EAAE,KAAK,CAAC,GAAG;QAAE,UAAU,CAAC,EAAE,QAAQ,CAAA;KAAE,GAAG,OAAO,CAAC;IACjK,CAAC,QAAQ,EAAE,OAAO,EAAE,KAAK,EAAE;QAAC,QAAQ,CAAC,EAAE,MAAM,CAAC;QAAC,OAAO,CAAC,EAAE,KAAK,CAAA;KAAC,GAAG,OAAO,GAAG;QAAE,IAAI,CAAC,EAAE,GAAG,CAAC;QAAC,GAAG,CAAC,EAAE,GAAG,CAAA;KAAE,CAAC;IACtG,YAAY,CAAC,EAAE,QAAQ,CAAC;CAC3B;AAKD,QAAA,MAAM,SAAS;;kBAA+E,CAAC;AAC7E,KAAK,SAAS,GAAG,YAAY,CAAC,OAAO,SAAS,CAAC,CAAC;AAClD,eAAe,SAAS,CAAC"}
|
|
@@ -114,7 +114,7 @@ describe('registerWebMcpUiTools', () => {
|
|
|
114
114
|
const identity = { formId: 'profile', controlId: 'name' };
|
|
115
115
|
expect((await parse(execute.execute({ action: 'stage', identity, value: 'Grace' }))).result).toMatchObject({ ok: true, action: 'stage' });
|
|
116
116
|
expect(value).toBe('Ada');
|
|
117
|
-
expect((await parse(execute.execute({ action: 'apply', identity }))).result).toMatchObject({ ok: false, reason: '
|
|
117
|
+
expect((await parse(execute.execute({ action: 'apply', identity }))).result).toMatchObject({ ok: false, reason: 'human_confirmation_required' });
|
|
118
118
|
expect(value).toBe('Ada');
|
|
119
119
|
expect(await parse(execute.execute({
|
|
120
120
|
action: 'apply',
|
|
@@ -123,6 +123,70 @@ describe('registerWebMcpUiTools', () => {
|
|
|
123
123
|
}))).toEqual({ ok: false, reason: 'invalid_request', details: 'confirmed' });
|
|
124
124
|
expect(value).toBe('Ada');
|
|
125
125
|
});
|
|
126
|
+
it('returns the canonical values staged by general form-control tools', async () => {
|
|
127
|
+
const browser = modelContext();
|
|
128
|
+
const controls = createControlInteractionRegistry();
|
|
129
|
+
const identities = {
|
|
130
|
+
checkbox: { formId: 'base', controlId: 'enabled' },
|
|
131
|
+
number: { formId: 'base', controlId: 'count' },
|
|
132
|
+
option: { formId: 'base', controlId: 'role' },
|
|
133
|
+
};
|
|
134
|
+
controls.register({
|
|
135
|
+
identity: identities.checkbox,
|
|
136
|
+
metadata: { kind: 'checkbox' },
|
|
137
|
+
getValue: () => false,
|
|
138
|
+
setValue: () => undefined,
|
|
139
|
+
prepareValue: (next) => {
|
|
140
|
+
if (typeof next !== 'boolean')
|
|
141
|
+
throw new Error('staged_value_invalid');
|
|
142
|
+
return next;
|
|
143
|
+
},
|
|
144
|
+
});
|
|
145
|
+
controls.register({
|
|
146
|
+
identity: identities.number,
|
|
147
|
+
metadata: { kind: 'number' },
|
|
148
|
+
getValue: () => 0,
|
|
149
|
+
setValue: () => undefined,
|
|
150
|
+
prepareValue: (next) => Number(next),
|
|
151
|
+
});
|
|
152
|
+
controls.register({
|
|
153
|
+
identity: identities.option,
|
|
154
|
+
metadata: {
|
|
155
|
+
kind: 'listbox',
|
|
156
|
+
options: [{ value: 1, label: 'Admin' }],
|
|
157
|
+
},
|
|
158
|
+
getValue: () => 1,
|
|
159
|
+
setValue: () => undefined,
|
|
160
|
+
prepareValue: (next) => {
|
|
161
|
+
if (String(next) !== '1')
|
|
162
|
+
throw new Error('staged_value_invalid');
|
|
163
|
+
return 1;
|
|
164
|
+
},
|
|
165
|
+
});
|
|
166
|
+
registerWebMcpUiTools({
|
|
167
|
+
controlRegistry: controls,
|
|
168
|
+
dataSurfaceRegistry: createDataSurfaceRegistry(),
|
|
169
|
+
document: browser.document,
|
|
170
|
+
});
|
|
171
|
+
const execute = findTool(browser.registered, 'smrt_ui_execute_form_control');
|
|
172
|
+
expect(await parse(execute.execute({
|
|
173
|
+
action: 'stage',
|
|
174
|
+
identity: identities.checkbox,
|
|
175
|
+
value: {},
|
|
176
|
+
}))).toMatchObject({
|
|
177
|
+
ok: true,
|
|
178
|
+
result: { ok: false, reason: 'execution_failed' },
|
|
179
|
+
});
|
|
180
|
+
expect(controls.get(identities.checkbox)?.state.staged).toBeUndefined();
|
|
181
|
+
for (const [identity, value, staged] of [
|
|
182
|
+
[identities.number, '42', 42],
|
|
183
|
+
[identities.option, '1', 1],
|
|
184
|
+
]) {
|
|
185
|
+
const response = await parse(execute.execute({ action: 'stage', identity, value }));
|
|
186
|
+
expect(response).toMatchObject({ ok: true, result: { ok: true } });
|
|
187
|
+
expect(response.result.snapshot.state.staged.value).toEqual(staged);
|
|
188
|
+
}
|
|
189
|
+
});
|
|
126
190
|
it('keeps secret values redacted from list, inspect, and execute responses', async () => {
|
|
127
191
|
const browser = modelContext();
|
|
128
192
|
const controls = createControlInteractionRegistry();
|
|
@@ -188,38 +252,86 @@ describe('registerWebMcpUiTools', () => {
|
|
|
188
252
|
expect(result).toEqual({ ok: false, reason: 'execution_failed' });
|
|
189
253
|
expect(JSON.stringify(result)).not.toContain('customer 4711');
|
|
190
254
|
});
|
|
191
|
-
it('redacts
|
|
192
|
-
const
|
|
193
|
-
|
|
194
|
-
|
|
195
|
-
|
|
196
|
-
|
|
197
|
-
|
|
198
|
-
|
|
199
|
-
|
|
200
|
-
|
|
201
|
-
|
|
202
|
-
|
|
203
|
-
|
|
204
|
-
|
|
205
|
-
|
|
206
|
-
|
|
207
|
-
|
|
208
|
-
|
|
209
|
-
|
|
210
|
-
|
|
211
|
-
|
|
212
|
-
|
|
213
|
-
|
|
214
|
-
|
|
215
|
-
|
|
216
|
-
|
|
217
|
-
|
|
218
|
-
|
|
219
|
-
|
|
220
|
-
|
|
221
|
-
|
|
222
|
-
|
|
255
|
+
it('redacts sensitive injected snapshots and result text despite inconsistent flags', async () => {
|
|
256
|
+
for (const sensitivity of ['secret', 'sensitive']) {
|
|
257
|
+
const marker = `injected-${sensitivity}-value`;
|
|
258
|
+
const browser = modelContext();
|
|
259
|
+
const snapshot = {
|
|
260
|
+
identity: { formId: 'injected', controlId: sensitivity },
|
|
261
|
+
metadata: {
|
|
262
|
+
kind: 'password',
|
|
263
|
+
sensitivity,
|
|
264
|
+
capabilities: ['discard'],
|
|
265
|
+
},
|
|
266
|
+
state: {
|
|
267
|
+
value: marker,
|
|
268
|
+
valueRedacted: false,
|
|
269
|
+
stagedValue: `${marker}-staged`,
|
|
270
|
+
stagedValueRedacted: false,
|
|
271
|
+
validationMessage: `${marker} validation`,
|
|
272
|
+
staged: {
|
|
273
|
+
value: `${marker}-nested`,
|
|
274
|
+
valueRedacted: false,
|
|
275
|
+
provenance: { source: 'agent' },
|
|
276
|
+
stagedAt: 1,
|
|
277
|
+
revision: 1,
|
|
278
|
+
stale: false,
|
|
279
|
+
validationMessage: `${marker} nested validation`,
|
|
280
|
+
},
|
|
281
|
+
},
|
|
282
|
+
};
|
|
283
|
+
const controls = {
|
|
284
|
+
register: () => () => { },
|
|
285
|
+
unregister: () => { },
|
|
286
|
+
list: () => [snapshot],
|
|
287
|
+
get: () => snapshot,
|
|
288
|
+
execute: async (command) => ({
|
|
289
|
+
ok: false,
|
|
290
|
+
action: command.action,
|
|
291
|
+
identity: command.identity,
|
|
292
|
+
reason: `${marker} reason`,
|
|
293
|
+
validationMessage: `${marker} result validation`,
|
|
294
|
+
snapshot,
|
|
295
|
+
}),
|
|
296
|
+
subscribe: () => () => { },
|
|
297
|
+
};
|
|
298
|
+
registerWebMcpUiTools({
|
|
299
|
+
controlRegistry: controls,
|
|
300
|
+
dataSurfaceRegistry: createDataSurfaceRegistry(),
|
|
301
|
+
document: browser.document,
|
|
302
|
+
});
|
|
303
|
+
const identity = { formId: 'injected', controlId: sensitivity };
|
|
304
|
+
const list = await parse(findTool(browser.registered, 'smrt_ui_list_form_controls').execute({}));
|
|
305
|
+
const inspect = await parse(findTool(browser.registered, 'smrt_ui_inspect_form_control').execute({
|
|
306
|
+
identity,
|
|
307
|
+
}));
|
|
308
|
+
const executed = await parse(findTool(browser.registered, 'smrt_ui_execute_form_control').execute({
|
|
309
|
+
action: 'focus',
|
|
310
|
+
identity,
|
|
311
|
+
}));
|
|
312
|
+
expect(JSON.stringify([list, inspect, executed])).not.toContain(marker);
|
|
313
|
+
for (const result of [
|
|
314
|
+
list.result[0],
|
|
315
|
+
inspect.result,
|
|
316
|
+
executed.result.snapshot,
|
|
317
|
+
]) {
|
|
318
|
+
expect(result.metadata.capabilities).toEqual(['discard']);
|
|
319
|
+
expect(result.state).toMatchObject({
|
|
320
|
+
valueRedacted: true,
|
|
321
|
+
stagedValueRedacted: true,
|
|
322
|
+
staged: {
|
|
323
|
+
valueRedacted: true,
|
|
324
|
+
},
|
|
325
|
+
});
|
|
326
|
+
expect(result.state).not.toHaveProperty('value');
|
|
327
|
+
expect(result.state).not.toHaveProperty('stagedValue');
|
|
328
|
+
expect(result.state).not.toHaveProperty('validationMessage');
|
|
329
|
+
expect(result.state.staged).not.toHaveProperty('value');
|
|
330
|
+
expect(result.state.staged).not.toHaveProperty('validationMessage');
|
|
331
|
+
}
|
|
332
|
+
expect(executed.result.reason).toBe('execution_failed');
|
|
333
|
+
expect(executed.result).not.toHaveProperty('validationMessage');
|
|
334
|
+
}
|
|
223
335
|
});
|
|
224
336
|
it('filters hidden columns and preserves surface revision and replay failures', async () => {
|
|
225
337
|
const browser = modelContext();
|
|
@@ -407,4 +519,68 @@ describe('registerWebMcpUiTools', () => {
|
|
|
407
519
|
expect(() => registerWebMcpUiTools({ ...registries, document })).not.toThrow();
|
|
408
520
|
expect(calls).toBe(6);
|
|
409
521
|
});
|
|
522
|
+
it('observes asynchronous host rejection, aborts tools, and releases only its own lock', async () => {
|
|
523
|
+
let rejectFirstRegistration;
|
|
524
|
+
const signals = [];
|
|
525
|
+
let calls = 0;
|
|
526
|
+
const document = {
|
|
527
|
+
modelContext: {
|
|
528
|
+
registerTool(_tool, options) {
|
|
529
|
+
calls += 1;
|
|
530
|
+
if (options?.signal)
|
|
531
|
+
signals.push(options.signal);
|
|
532
|
+
if (calls === 1) {
|
|
533
|
+
return new Promise((_resolve, reject) => {
|
|
534
|
+
rejectFirstRegistration = reject;
|
|
535
|
+
});
|
|
536
|
+
}
|
|
537
|
+
},
|
|
538
|
+
},
|
|
539
|
+
};
|
|
540
|
+
const registries = {
|
|
541
|
+
controlRegistry: createControlInteractionRegistry(),
|
|
542
|
+
dataSurfaceRegistry: createDataSurfaceRegistry(),
|
|
543
|
+
};
|
|
544
|
+
const disposeFirst = registerWebMcpUiTools({ ...registries, document });
|
|
545
|
+
disposeFirst();
|
|
546
|
+
const disposeReplacement = registerWebMcpUiTools({
|
|
547
|
+
...registries,
|
|
548
|
+
document,
|
|
549
|
+
});
|
|
550
|
+
rejectFirstRegistration?.(new Error('late host collision'));
|
|
551
|
+
await Promise.resolve();
|
|
552
|
+
await Promise.resolve();
|
|
553
|
+
expect(signals.slice(0, 6).every((signal) => signal.aborted)).toBe(true);
|
|
554
|
+
expect(signals.slice(6).every((signal) => !signal.aborted)).toBe(true);
|
|
555
|
+
expect(() => registerWebMcpUiTools({ ...registries, document })).toThrow('already registered');
|
|
556
|
+
disposeReplacement();
|
|
557
|
+
expect(() => registerWebMcpUiTools({ ...registries, document })).not.toThrow();
|
|
558
|
+
});
|
|
559
|
+
it('releases an active prefix when asynchronous host registration fails', async () => {
|
|
560
|
+
const signals = [];
|
|
561
|
+
let rejectRegistration;
|
|
562
|
+
const document = {
|
|
563
|
+
modelContext: {
|
|
564
|
+
registerTool(_tool, options) {
|
|
565
|
+
if (options?.signal)
|
|
566
|
+
signals.push(options.signal);
|
|
567
|
+
if (!rejectRegistration) {
|
|
568
|
+
return new Promise((_resolve, reject) => {
|
|
569
|
+
rejectRegistration = reject;
|
|
570
|
+
});
|
|
571
|
+
}
|
|
572
|
+
},
|
|
573
|
+
},
|
|
574
|
+
};
|
|
575
|
+
const registries = {
|
|
576
|
+
controlRegistry: createControlInteractionRegistry(),
|
|
577
|
+
dataSurfaceRegistry: createDataSurfaceRegistry(),
|
|
578
|
+
};
|
|
579
|
+
registerWebMcpUiTools({ ...registries, document });
|
|
580
|
+
rejectRegistration?.(new Error('host collision'));
|
|
581
|
+
await Promise.resolve();
|
|
582
|
+
await Promise.resolve();
|
|
583
|
+
expect(signals.every((signal) => signal.aborted)).toBe(true);
|
|
584
|
+
expect(() => registerWebMcpUiTools({ ...registries, document })).not.toThrow();
|
|
585
|
+
});
|
|
410
586
|
});
|
|
@@ -30,6 +30,26 @@ describe('useWebMcpTool', () => {
|
|
|
30
30
|
view.unmount();
|
|
31
31
|
expect(registered[1].signal?.aborted).toBe(true);
|
|
32
32
|
});
|
|
33
|
+
it('observes asynchronous host rejection and aborts the failed registration', async () => {
|
|
34
|
+
let rejectRegistration;
|
|
35
|
+
let signal;
|
|
36
|
+
document.modelContext = {
|
|
37
|
+
registerTool(_tool, options) {
|
|
38
|
+
signal = options?.signal;
|
|
39
|
+
return new Promise((_resolve, reject) => {
|
|
40
|
+
rejectRegistration = reject;
|
|
41
|
+
});
|
|
42
|
+
},
|
|
43
|
+
};
|
|
44
|
+
const view = render(Harness);
|
|
45
|
+
await tick();
|
|
46
|
+
expect(signal?.aborted).toBe(false);
|
|
47
|
+
rejectRegistration?.(new Error('host collision'));
|
|
48
|
+
await Promise.resolve();
|
|
49
|
+
await Promise.resolve();
|
|
50
|
+
expect(signal?.aborted).toBe(true);
|
|
51
|
+
view.unmount();
|
|
52
|
+
});
|
|
33
53
|
it('is a no-op when WebMCP is unavailable', async () => {
|
|
34
54
|
const view = render(Harness);
|
|
35
55
|
await tick();
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"webmcp-ui.d.ts","sourceRoot":"","sources":["../../src/web/webmcp-ui.ts"],"names":[],"mappings":"AAAA,OAAO,EAKL,KAAK,mBAAmB,EAGzB,MAAM,6BAA6B,CAAC;AACrC,OAAO,KAAK,
|
|
1
|
+
{"version":3,"file":"webmcp-ui.d.ts","sourceRoot":"","sources":["../../src/web/webmcp-ui.ts"],"names":[],"mappings":"AAAA,OAAO,EAKL,KAAK,mBAAmB,EAGzB,MAAM,6BAA6B,CAAC;AACrC,OAAO,KAAK,EAKV,0BAA0B,EAE3B,MAAM,8BAA8B,CAAC;AACtC,OAAO,KAAK,EAAE,cAAc,EAAE,MAAM,oBAAoB,CAAC;AA8CzD,MAAM,WAAW,4BAA4B;IAC3C,eAAe,EAAE,0BAA0B,CAAC;IAC5C,mBAAmB,EAAE,mBAAmB,CAAC;IACzC,MAAM,CAAC,EAAE,MAAM,CAAC;IAChB,sEAAsE;IACtE,QAAQ,CAAC,EAAE;QAAE,YAAY,CAAC,EAAE,sBAAsB,CAAA;KAAE,CAAC;CACtD;AAED,UAAU,sBAAsB;IAC9B,YAAY,CACV,IAAI,EAAE,cAAc,EACpB,OAAO,CAAC,EAAE;QAAE,MAAM,CAAC,EAAE,WAAW,CAAA;KAAE,GACjC,IAAI,GAAG,OAAO,CAAC,IAAI,CAAC,CAAC;CACzB;AA6fD,4EAA4E;AAC5E,wBAAgB,qBAAqB,CACnC,OAAO,EAAE,4BAA4B,GACpC,MAAM,IAAI,CAoDZ"}
|