@optimizely-opal/opal-tools-sdk 0.1.6-dev → 0.1.8-dev
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +201 -216
- package/dist/decorators.d.ts +2 -0
- package/dist/decorators.js +2 -1
- package/dist/index.d.ts +3 -2
- package/dist/index.js +5 -4
- package/dist/models.d.ts +22 -1
- package/dist/models.js +28 -2
- package/dist/proteus.d.ts +1451 -0
- package/dist/proteus.js +86 -0
- package/dist/registerResource.d.ts +60 -0
- package/dist/registerResource.js +59 -0
- package/dist/registerTool.d.ts +18 -7
- package/dist/registerTool.js +52 -3
- package/dist/service.d.ts +15 -3
- package/dist/service.js +80 -21
- package/package.json +2 -2
- package/scripts/generate-proteus.ts +135 -0
- package/src/decorators.ts +4 -0
- package/src/index.ts +3 -2
- package/src/models.ts +27 -0
- package/src/proteus.ts +2129 -0
- package/src/registerResource.ts +82 -0
- package/src/registerTool.ts +19 -29
- package/src/service.ts +110 -23
- package/tests/integration.test.ts +252 -73
- package/tests/proteus.test.ts +122 -0
- package/dist/block.d.ts +0 -4760
- package/dist/block.js +0 -104
- package/scripts/generate-block.ts +0 -167
- package/src/block.ts +0 -11761
- package/tests/block.test.ts +0 -115
|
@@ -7,7 +7,14 @@ import request from "supertest";
|
|
|
7
7
|
import { afterEach, beforeEach, describe, expect, test } from "vitest";
|
|
8
8
|
import { z } from "zod/v4";
|
|
9
9
|
|
|
10
|
-
import {
|
|
10
|
+
import {
|
|
11
|
+
ParameterType,
|
|
12
|
+
registerResource,
|
|
13
|
+
registerTool,
|
|
14
|
+
tool,
|
|
15
|
+
ToolsService,
|
|
16
|
+
UI,
|
|
17
|
+
} from "../src";
|
|
11
18
|
import { registry } from "../src/registry";
|
|
12
19
|
|
|
13
20
|
// Clear registry before each test
|
|
@@ -49,63 +56,6 @@ describe("Integration Tests", () => {
|
|
|
49
56
|
expect(response.body).toBe("Hello, Alice!");
|
|
50
57
|
});
|
|
51
58
|
|
|
52
|
-
test("block tool endpoint correctly serializes all fields", async () => {
|
|
53
|
-
const app = express();
|
|
54
|
-
app.use(express.json());
|
|
55
|
-
new ToolsService(app);
|
|
56
|
-
|
|
57
|
-
registerTool(
|
|
58
|
-
"create_task",
|
|
59
|
-
{
|
|
60
|
-
description: "Create a task",
|
|
61
|
-
inputSchema: {
|
|
62
|
-
name: z.string().describe("Task name"),
|
|
63
|
-
},
|
|
64
|
-
type: "block",
|
|
65
|
-
},
|
|
66
|
-
async (params) => {
|
|
67
|
-
return {
|
|
68
|
-
artifact: {
|
|
69
|
-
data: { name: params.name },
|
|
70
|
-
id: "task-123",
|
|
71
|
-
type: "task",
|
|
72
|
-
},
|
|
73
|
-
content: Block.Document({
|
|
74
|
-
children: Block.Text({ children: `Created task: ${params.name}` }),
|
|
75
|
-
}),
|
|
76
|
-
data: { created_at: "2024-01-01T00:00:00Z" },
|
|
77
|
-
rollback: {
|
|
78
|
-
config: { endpoint: "/api/tasks/task-123", method: "DELETE" },
|
|
79
|
-
label: "Delete Task",
|
|
80
|
-
type: "endpoint",
|
|
81
|
-
},
|
|
82
|
-
};
|
|
83
|
-
},
|
|
84
|
-
);
|
|
85
|
-
|
|
86
|
-
const response = await request(app)
|
|
87
|
-
.post("/tools/create-task")
|
|
88
|
-
.send({ parameters: { name: "My Task" } });
|
|
89
|
-
|
|
90
|
-
expect(response.status).toBe(200);
|
|
91
|
-
expect(response.headers["content-type"]).toBe(
|
|
92
|
-
"application/vnd.opal.block+json; charset=utf-8",
|
|
93
|
-
);
|
|
94
|
-
expect(response.body).toEqual({
|
|
95
|
-
artifact: { data: { name: "My Task" }, id: "task-123", type: "task" },
|
|
96
|
-
content: {
|
|
97
|
-
$type: "Block.Document",
|
|
98
|
-
children: { $type: "Block.Text", children: "Created task: My Task" },
|
|
99
|
-
},
|
|
100
|
-
data: { created_at: "2024-01-01T00:00:00Z" },
|
|
101
|
-
rollback: {
|
|
102
|
-
config: { endpoint: "/api/tasks/task-123", method: "DELETE" },
|
|
103
|
-
label: "Delete Task",
|
|
104
|
-
type: "endpoint",
|
|
105
|
-
},
|
|
106
|
-
});
|
|
107
|
-
});
|
|
108
|
-
|
|
109
59
|
test("environment parameter is correctly passed to tool", async () => {
|
|
110
60
|
const app = express();
|
|
111
61
|
app.use(express.json());
|
|
@@ -210,12 +160,13 @@ describe("Integration Tests", () => {
|
|
|
210
160
|
inputSchema: {
|
|
211
161
|
title: z.string().describe("Task title"),
|
|
212
162
|
},
|
|
213
|
-
type: "block",
|
|
214
163
|
},
|
|
215
164
|
async (params) => {
|
|
216
165
|
return {
|
|
217
|
-
content:
|
|
218
|
-
|
|
166
|
+
content: UI.Document({
|
|
167
|
+
appName: "Task Manager",
|
|
168
|
+
body: UI.Text({ children: `Task: ${params.title}` }),
|
|
169
|
+
title: "Create Task",
|
|
219
170
|
}),
|
|
220
171
|
};
|
|
221
172
|
},
|
|
@@ -286,33 +237,261 @@ describe("Integration Tests", () => {
|
|
|
286
237
|
});
|
|
287
238
|
});
|
|
288
239
|
|
|
289
|
-
test("
|
|
240
|
+
test("resource endpoint returns correct content via POST /resources/read", async () => {
|
|
241
|
+
const app = express();
|
|
242
|
+
app.use(express.json());
|
|
243
|
+
new ToolsService(app);
|
|
244
|
+
|
|
245
|
+
registerResource(
|
|
246
|
+
"test-form",
|
|
247
|
+
{
|
|
248
|
+
description: "A test form",
|
|
249
|
+
mimeType: "application/vnd.opal.proteus+json",
|
|
250
|
+
uri: "ui://test-app/form",
|
|
251
|
+
},
|
|
252
|
+
async () => {
|
|
253
|
+
return JSON.stringify({
|
|
254
|
+
fields: [{ name: "title", type: "text" }],
|
|
255
|
+
type: "form",
|
|
256
|
+
});
|
|
257
|
+
},
|
|
258
|
+
);
|
|
259
|
+
|
|
260
|
+
// Call the POST /resources/read endpoint
|
|
261
|
+
const response = await request(app)
|
|
262
|
+
.post("/resources/read")
|
|
263
|
+
.send({ uri: "ui://test-app/form" });
|
|
264
|
+
|
|
265
|
+
expect(response.status).toBe(200);
|
|
266
|
+
expect(response.body).toEqual({
|
|
267
|
+
mimeType: "application/vnd.opal.proteus+json",
|
|
268
|
+
text: JSON.stringify({
|
|
269
|
+
fields: [{ name: "title", type: "text" }],
|
|
270
|
+
type: "form",
|
|
271
|
+
}),
|
|
272
|
+
uri: "ui://test-app/form",
|
|
273
|
+
});
|
|
274
|
+
});
|
|
275
|
+
|
|
276
|
+
test("tool with uiResource reference", async () => {
|
|
290
277
|
const app = express();
|
|
291
278
|
app.use(express.json());
|
|
292
279
|
new ToolsService(app);
|
|
293
280
|
|
|
294
|
-
|
|
281
|
+
registerResource(
|
|
282
|
+
"create-form",
|
|
283
|
+
{
|
|
284
|
+
description: "Form for creating items",
|
|
285
|
+
mimeType: "application/vnd.opal.proteus+json",
|
|
286
|
+
uri: "ui://test-app/create-form",
|
|
287
|
+
},
|
|
288
|
+
async () => {
|
|
289
|
+
return "{}";
|
|
290
|
+
},
|
|
291
|
+
);
|
|
292
|
+
|
|
295
293
|
registerTool(
|
|
296
|
-
"
|
|
294
|
+
"create_item",
|
|
297
295
|
{
|
|
298
|
-
description: "
|
|
296
|
+
description: "Create a new item",
|
|
299
297
|
inputSchema: {
|
|
300
|
-
|
|
298
|
+
title: z.string().describe("The title"),
|
|
301
299
|
},
|
|
302
|
-
|
|
300
|
+
uiResource: "ui://test-app/create-form",
|
|
303
301
|
},
|
|
304
302
|
async (params) => {
|
|
305
|
-
|
|
306
|
-
return `This should fail: ${params.name}`;
|
|
303
|
+
return { id: "item-123", title: params.title };
|
|
307
304
|
},
|
|
308
305
|
);
|
|
309
306
|
|
|
307
|
+
// Call discovery endpoint
|
|
308
|
+
const response = await request(app).get("/discovery");
|
|
309
|
+
|
|
310
|
+
expect(response.status).toBe(200);
|
|
311
|
+
expect(response.body).toEqual({
|
|
312
|
+
functions: [
|
|
313
|
+
{
|
|
314
|
+
description: "Create a new item",
|
|
315
|
+
endpoint: "/tools/create-item",
|
|
316
|
+
http_method: "POST",
|
|
317
|
+
name: "create_item",
|
|
318
|
+
parameters: [
|
|
319
|
+
{
|
|
320
|
+
description: "The title",
|
|
321
|
+
name: "title",
|
|
322
|
+
required: true,
|
|
323
|
+
type: "string",
|
|
324
|
+
},
|
|
325
|
+
],
|
|
326
|
+
ui_resource: "ui://test-app/create-form",
|
|
327
|
+
},
|
|
328
|
+
],
|
|
329
|
+
});
|
|
330
|
+
});
|
|
331
|
+
|
|
332
|
+
test("POST /resources/read with missing URI returns 400", async () => {
|
|
333
|
+
const app = express();
|
|
334
|
+
app.use(express.json());
|
|
335
|
+
new ToolsService(app);
|
|
336
|
+
|
|
337
|
+
registerResource(
|
|
338
|
+
"minimal-resource",
|
|
339
|
+
{
|
|
340
|
+
uri: "ui://test-app/minimal",
|
|
341
|
+
},
|
|
342
|
+
async () => {
|
|
343
|
+
return "minimal";
|
|
344
|
+
},
|
|
345
|
+
);
|
|
346
|
+
|
|
347
|
+
// Call without URI
|
|
348
|
+
const response = await request(app).post("/resources/read").send({});
|
|
349
|
+
|
|
350
|
+
expect(response.status).toBe(400);
|
|
351
|
+
expect(response.body.error).toBe("Missing required field: uri");
|
|
352
|
+
});
|
|
353
|
+
|
|
354
|
+
test("POST /resources/read with unknown URI returns 404", async () => {
|
|
355
|
+
const app = express();
|
|
356
|
+
app.use(express.json());
|
|
357
|
+
new ToolsService(app);
|
|
358
|
+
|
|
359
|
+
// Call with unknown URI
|
|
310
360
|
const response = await request(app)
|
|
311
|
-
.post("/
|
|
312
|
-
.send({
|
|
361
|
+
.post("/resources/read")
|
|
362
|
+
.send({ uri: "ui://unknown/resource" });
|
|
363
|
+
|
|
364
|
+
expect(response.status).toBe(404);
|
|
365
|
+
expect(response.body.error).toBe(
|
|
366
|
+
"Resource not found: ui://unknown/resource",
|
|
367
|
+
);
|
|
368
|
+
});
|
|
369
|
+
|
|
370
|
+
test("POST /resources/read with non-string handler result returns 500", async () => {
|
|
371
|
+
const app = express();
|
|
372
|
+
app.use(express.json());
|
|
373
|
+
new ToolsService(app);
|
|
374
|
+
|
|
375
|
+
registerResource(
|
|
376
|
+
"invalid-resource",
|
|
377
|
+
{
|
|
378
|
+
uri: "ui://test-app/invalid",
|
|
379
|
+
},
|
|
380
|
+
// @ts-expect-error - intentionally returning wrong type for test
|
|
381
|
+
async () => {
|
|
382
|
+
return { invalid: "object" };
|
|
383
|
+
},
|
|
384
|
+
);
|
|
385
|
+
|
|
386
|
+
// Call the resource
|
|
387
|
+
const response = await request(app)
|
|
388
|
+
.post("/resources/read")
|
|
389
|
+
.send({ uri: "ui://test-app/invalid" });
|
|
313
390
|
|
|
314
|
-
// Should return 500 error because block tools must return BlockResponse
|
|
315
391
|
expect(response.status).toBe(500);
|
|
316
|
-
expect(response.body.error).toContain(
|
|
392
|
+
expect(response.body.error).toContain(
|
|
393
|
+
"Resource handler for 'ui://test-app/invalid' must return a string or ProteusDocument",
|
|
394
|
+
);
|
|
395
|
+
expect(response.body.error).toContain("but returned object");
|
|
396
|
+
});
|
|
397
|
+
|
|
398
|
+
test("resource with ProteusDocument return is auto-serialized", async () => {
|
|
399
|
+
const app = express();
|
|
400
|
+
app.use(express.json());
|
|
401
|
+
new ToolsService(app);
|
|
402
|
+
|
|
403
|
+
registerResource(
|
|
404
|
+
"dynamic-form",
|
|
405
|
+
{
|
|
406
|
+
description: "A dynamic form using UI.Document",
|
|
407
|
+
uri: "ui://test-app/dynamic-form",
|
|
408
|
+
},
|
|
409
|
+
async () => {
|
|
410
|
+
return UI.Document({
|
|
411
|
+
actions: [
|
|
412
|
+
UI.Action({ appearance: "primary", children: "Save" }),
|
|
413
|
+
UI.CancelAction({ children: "Cancel" }),
|
|
414
|
+
],
|
|
415
|
+
appName: "Item Manager",
|
|
416
|
+
body: [
|
|
417
|
+
UI.Heading({ children: "Create Item" }),
|
|
418
|
+
UI.Field({
|
|
419
|
+
children: UI.Input({
|
|
420
|
+
name: "item_name",
|
|
421
|
+
placeholder: "Enter item name",
|
|
422
|
+
}),
|
|
423
|
+
label: "Item Name",
|
|
424
|
+
}),
|
|
425
|
+
UI.Field({
|
|
426
|
+
children: UI.Textarea({
|
|
427
|
+
name: "description",
|
|
428
|
+
placeholder: "Enter description",
|
|
429
|
+
}),
|
|
430
|
+
label: "Description",
|
|
431
|
+
}),
|
|
432
|
+
],
|
|
433
|
+
title: "Create New Item",
|
|
434
|
+
});
|
|
435
|
+
},
|
|
436
|
+
);
|
|
437
|
+
|
|
438
|
+
// Call the resource endpoint
|
|
439
|
+
const response = await request(app)
|
|
440
|
+
.post("/resources/read")
|
|
441
|
+
.send({ uri: "ui://test-app/dynamic-form" });
|
|
442
|
+
|
|
443
|
+
expect(response.status).toBe(200);
|
|
444
|
+
|
|
445
|
+
// Verify URI
|
|
446
|
+
expect(response.body.uri).toBe("ui://test-app/dynamic-form");
|
|
447
|
+
|
|
448
|
+
// Verify MIME type was auto-set
|
|
449
|
+
expect(response.body.mimeType).toBe("application/vnd.opal.proteus+json");
|
|
450
|
+
|
|
451
|
+
// Verify the text is a serialized ProteusDocument
|
|
452
|
+
const parsedDoc = JSON.parse(response.body.text);
|
|
453
|
+
expect(parsedDoc.$type).toBe("Document");
|
|
454
|
+
expect(parsedDoc.appName).toBe("Item Manager");
|
|
455
|
+
expect(parsedDoc.title).toBe("Create New Item");
|
|
456
|
+
expect(parsedDoc.body).toHaveLength(3);
|
|
457
|
+
expect(parsedDoc.body[0].$type).toBe("Heading");
|
|
458
|
+
expect(parsedDoc.body[0].children).toBe("Create Item");
|
|
459
|
+
expect(parsedDoc.body[1].$type).toBe("Field");
|
|
460
|
+
expect(parsedDoc.body[1].label).toBe("Item Name");
|
|
461
|
+
expect(parsedDoc.actions).toHaveLength(2);
|
|
462
|
+
expect(parsedDoc.actions[0].$type).toBe("Action");
|
|
463
|
+
expect(parsedDoc.actions[1].$type).toBe("CancelAction");
|
|
464
|
+
});
|
|
465
|
+
|
|
466
|
+
test("resource with ProteusDocument and explicit MIME type preserves MIME type", async () => {
|
|
467
|
+
const app = express();
|
|
468
|
+
app.use(express.json());
|
|
469
|
+
new ToolsService(app);
|
|
470
|
+
|
|
471
|
+
registerResource(
|
|
472
|
+
"custom-mime",
|
|
473
|
+
{
|
|
474
|
+
description: "ProteusDocument with custom MIME type",
|
|
475
|
+
mimeType: "application/custom+json",
|
|
476
|
+
uri: "ui://test-app/custom-mime",
|
|
477
|
+
},
|
|
478
|
+
async () => {
|
|
479
|
+
return UI.Document({
|
|
480
|
+
appName: "Test App",
|
|
481
|
+
body: [UI.Text({ children: "Test" })],
|
|
482
|
+
title: "Test Document",
|
|
483
|
+
});
|
|
484
|
+
},
|
|
485
|
+
);
|
|
486
|
+
|
|
487
|
+
// Call the resource endpoint
|
|
488
|
+
const response = await request(app)
|
|
489
|
+
.post("/resources/read")
|
|
490
|
+
.send({ uri: "ui://test-app/custom-mime" });
|
|
491
|
+
|
|
492
|
+
expect(response.status).toBe(200);
|
|
493
|
+
|
|
494
|
+
// Verify explicit MIME type is preserved (not auto-set)
|
|
495
|
+
expect(response.body.mimeType).toBe("application/custom+json");
|
|
317
496
|
});
|
|
318
497
|
});
|
|
@@ -0,0 +1,122 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Tests for Adaptive Proteus components
|
|
3
|
+
*/
|
|
4
|
+
|
|
5
|
+
import { describe, expect, test } from "vitest";
|
|
6
|
+
|
|
7
|
+
import { UI } from "../src";
|
|
8
|
+
|
|
9
|
+
describe("Proteus Components", () => {
|
|
10
|
+
test("UI.Heading with children", () => {
|
|
11
|
+
const heading = UI.Heading({ children: "Test Heading" });
|
|
12
|
+
expect(heading.$type).toBe("Heading");
|
|
13
|
+
|
|
14
|
+
const doc = UI.Document({
|
|
15
|
+
appName: "Test App",
|
|
16
|
+
body: heading,
|
|
17
|
+
title: "Test Title",
|
|
18
|
+
});
|
|
19
|
+
expect(doc).toEqual({
|
|
20
|
+
$type: "Document",
|
|
21
|
+
appName: "Test App",
|
|
22
|
+
body: { $type: "Heading", children: "Test Heading" },
|
|
23
|
+
title: "Test Title",
|
|
24
|
+
});
|
|
25
|
+
});
|
|
26
|
+
|
|
27
|
+
test("UI.Text with children", () => {
|
|
28
|
+
const text = UI.Text({ children: "Test text content" });
|
|
29
|
+
expect(text.$type).toBe("Text");
|
|
30
|
+
|
|
31
|
+
const doc = UI.Document({
|
|
32
|
+
appName: "Test App",
|
|
33
|
+
body: text,
|
|
34
|
+
title: "Test Title",
|
|
35
|
+
});
|
|
36
|
+
expect(doc).toEqual({
|
|
37
|
+
$type: "Document",
|
|
38
|
+
appName: "Test App",
|
|
39
|
+
body: { $type: "Text", children: "Test text content" },
|
|
40
|
+
title: "Test Title",
|
|
41
|
+
});
|
|
42
|
+
});
|
|
43
|
+
|
|
44
|
+
test("UI.Heading with additional properties", () => {
|
|
45
|
+
const heading = UI.Heading({
|
|
46
|
+
children: "Styled Heading",
|
|
47
|
+
fontSize: "md",
|
|
48
|
+
fontWeight: "600",
|
|
49
|
+
level: "2",
|
|
50
|
+
});
|
|
51
|
+
|
|
52
|
+
const doc = UI.Document({
|
|
53
|
+
appName: "Test App",
|
|
54
|
+
body: heading,
|
|
55
|
+
title: "Test Title",
|
|
56
|
+
});
|
|
57
|
+
expect(doc).toEqual({
|
|
58
|
+
$type: "Document",
|
|
59
|
+
appName: "Test App",
|
|
60
|
+
body: {
|
|
61
|
+
$type: "Heading",
|
|
62
|
+
children: "Styled Heading",
|
|
63
|
+
fontSize: "md",
|
|
64
|
+
fontWeight: "600",
|
|
65
|
+
level: "2",
|
|
66
|
+
},
|
|
67
|
+
title: "Test Title",
|
|
68
|
+
});
|
|
69
|
+
});
|
|
70
|
+
|
|
71
|
+
test("UI.Group with children array", () => {
|
|
72
|
+
const group = UI.Group({
|
|
73
|
+
children: [
|
|
74
|
+
UI.Text({ children: "First item" }),
|
|
75
|
+
UI.Text({ children: "Second item" }),
|
|
76
|
+
],
|
|
77
|
+
flexDirection: "column",
|
|
78
|
+
gap: "16",
|
|
79
|
+
});
|
|
80
|
+
|
|
81
|
+
const doc = UI.Document({
|
|
82
|
+
appName: "Test App",
|
|
83
|
+
body: group,
|
|
84
|
+
title: "Test Title",
|
|
85
|
+
});
|
|
86
|
+
expect(doc).toEqual({
|
|
87
|
+
$type: "Document",
|
|
88
|
+
appName: "Test App",
|
|
89
|
+
body: {
|
|
90
|
+
$type: "Group",
|
|
91
|
+
children: [
|
|
92
|
+
{ $type: "Text", children: "First item" },
|
|
93
|
+
{ $type: "Text", children: "Second item" },
|
|
94
|
+
],
|
|
95
|
+
flexDirection: "column",
|
|
96
|
+
gap: "16",
|
|
97
|
+
},
|
|
98
|
+
title: "Test Title",
|
|
99
|
+
});
|
|
100
|
+
});
|
|
101
|
+
|
|
102
|
+
test("UI.Document", () => {
|
|
103
|
+
const doc = UI.Document({
|
|
104
|
+
appName: "Test App",
|
|
105
|
+
body: [
|
|
106
|
+
UI.Heading({ children: "Title", level: "2" }),
|
|
107
|
+
UI.Text({ children: "Description" }),
|
|
108
|
+
],
|
|
109
|
+
title: "Test Title",
|
|
110
|
+
});
|
|
111
|
+
|
|
112
|
+
expect(doc).toEqual({
|
|
113
|
+
$type: "Document",
|
|
114
|
+
appName: "Test App",
|
|
115
|
+
body: [
|
|
116
|
+
{ $type: "Heading", children: "Title", level: "2" },
|
|
117
|
+
{ $type: "Text", children: "Description" },
|
|
118
|
+
],
|
|
119
|
+
title: "Test Title",
|
|
120
|
+
});
|
|
121
|
+
});
|
|
122
|
+
});
|