@frockbot/plugin-shell 0.1.2 → 0.1.4
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/package.json +28 -28
- package/src/backend-composition.ts +4 -4
- package/src/backend-configuration.test.ts +366 -1451
- package/src/backend-execution.ts +0 -4
- package/src/backend-image.test.ts +8 -8
- package/src/backend-image.ts +13 -13
- package/src/backend-isolate.test.ts +181 -35
- package/src/backend-isolate.ts +110 -72
- package/src/backend-recovery-integration.test.ts +17 -67
- package/src/backend-routines.ts +1 -1
- package/src/backend.ts +335 -1659
- package/src/client/FrockBotApp.vue +6 -73
- package/src/client/index.test.ts +320 -380
- package/src/client/index.ts +85 -256
- package/src/client/model-presentation.test.ts +21 -8
- package/src/client/model-presentation.ts +14 -7
- package/src/client/styles.css +0 -27
- package/src/settings-links.test.ts +2 -10
- package/src/settings-links.ts +1 -13
- package/src/shared.ts +0 -13
- package/src/backend-assignment.test.ts +0 -161
- package/src/backend-assignment.ts +0 -274
package/src/backend-execution.ts
CHANGED
|
@@ -6,7 +6,6 @@ import type {
|
|
|
6
6
|
import type {
|
|
7
7
|
BotExecutionPlanV1,
|
|
8
8
|
BotSettingsViewV1,
|
|
9
|
-
ConnectionView,
|
|
10
9
|
} from "@frockbot/configuration-core";
|
|
11
10
|
import type { BotTurnCommand, BotTurnCompletion } from "./backend-contracts.js";
|
|
12
11
|
|
|
@@ -17,9 +16,6 @@ export interface BotResidentProjection {
|
|
|
17
16
|
settings: BotSettingsViewV1;
|
|
18
17
|
executionPlan: BotExecutionPlanV1;
|
|
19
18
|
systemPromptSection: string;
|
|
20
|
-
authorizeConnection(
|
|
21
|
-
assignment: BotSettingsViewV1["assignments"][number],
|
|
22
|
-
): Promise<ConnectionView>;
|
|
23
19
|
}
|
|
24
20
|
|
|
25
21
|
export interface BotResidentTurnExecution {
|
|
@@ -5,8 +5,8 @@ import {
|
|
|
5
5
|
} from "@frockbot/plugin-image/testing";
|
|
6
6
|
import {
|
|
7
7
|
createBotImageHost,
|
|
8
|
-
|
|
9
|
-
|
|
8
|
+
createNativeAiImageModelV1,
|
|
9
|
+
decodeNativeAiImageV1,
|
|
10
10
|
} from "./backend-image.ts";
|
|
11
11
|
|
|
12
12
|
const IDENTITY = { userId: "user-1", botId: "bot-1" };
|
|
@@ -56,7 +56,7 @@ describe("normalizing what Workers AI answered", () => {
|
|
|
56
56
|
const png = fakePngBytesV1(64, 32);
|
|
57
57
|
|
|
58
58
|
test("accepts the base64 envelope the FLUX models answer", async () => {
|
|
59
|
-
const buffer = await
|
|
59
|
+
const buffer = await decodeNativeAiImageV1({ image: base64(png) });
|
|
60
60
|
expect([...new Uint8Array(buffer)]).toEqual([...png]);
|
|
61
61
|
});
|
|
62
62
|
|
|
@@ -68,23 +68,23 @@ describe("normalizing what Workers AI answered", () => {
|
|
|
68
68
|
controller.close();
|
|
69
69
|
},
|
|
70
70
|
});
|
|
71
|
-
expect([...new Uint8Array(await
|
|
71
|
+
expect([...new Uint8Array(await decodeNativeAiImageV1(stream))]).toEqual([
|
|
72
72
|
...png,
|
|
73
73
|
]);
|
|
74
74
|
});
|
|
75
75
|
|
|
76
76
|
test("accepts a raw buffer or view", async () => {
|
|
77
77
|
expect([
|
|
78
|
-
...new Uint8Array(await
|
|
78
|
+
...new Uint8Array(await decodeNativeAiImageV1(png.slice().buffer)),
|
|
79
79
|
]).toEqual([...png]);
|
|
80
|
-
expect([...new Uint8Array(await
|
|
80
|
+
expect([...new Uint8Array(await decodeNativeAiImageV1(png))]).toEqual([
|
|
81
81
|
...png,
|
|
82
82
|
]);
|
|
83
83
|
});
|
|
84
84
|
|
|
85
85
|
test("refuses anything else rather than storing it", async () => {
|
|
86
86
|
for (const answer of [undefined, null, 7, "hello", { image: 3 }, {}]) {
|
|
87
|
-
await expect(
|
|
87
|
+
await expect(decodeNativeAiImageV1(answer)).rejects.toThrow(
|
|
88
88
|
"not an image",
|
|
89
89
|
);
|
|
90
90
|
}
|
|
@@ -92,7 +92,7 @@ describe("normalizing what Workers AI answered", () => {
|
|
|
92
92
|
|
|
93
93
|
test("passes the requested size through to the binding", async () => {
|
|
94
94
|
const calls: Array<[string, Record<string, unknown>]> = [];
|
|
95
|
-
const model =
|
|
95
|
+
const model = createNativeAiImageModelV1({
|
|
96
96
|
run: (name, input) => {
|
|
97
97
|
calls.push([name, input]);
|
|
98
98
|
return Promise.resolve({ image: base64(png) });
|
package/src/backend-image.ts
CHANGED
|
@@ -1,15 +1,15 @@
|
|
|
1
1
|
// The Bot Durable Object's half of the image-generation seam.
|
|
2
2
|
//
|
|
3
3
|
// The Image Package consumes a narrow `ImageModelV1` — one method, answering
|
|
4
|
-
// raw image bytes — and knows nothing about
|
|
4
|
+
// raw image bytes — and knows nothing about Cloudflare's native AI binding. This module is the
|
|
5
5
|
// adapter: it takes the `AI` binding off the Durable Object's environment,
|
|
6
|
-
// normalizes the three shapes a
|
|
6
|
+
// normalizes the three shapes a native text-to-image model answers in, and
|
|
7
7
|
// hands the Package a seam with no platform vocabulary in it. "Electron,
|
|
8
8
|
// Cloudflare, provider SDK, and Computer implementation types remain inside
|
|
9
9
|
// their adapters."
|
|
10
10
|
//
|
|
11
11
|
// The binding is optional, exactly as `PACKAGE_BUNDLER` is. A deployment with
|
|
12
|
-
// no
|
|
12
|
+
// no native AI binding still mounts the Package, and `generate_image` then
|
|
13
13
|
// refuses visibly on the Turn that calls it — which is a better answer than a
|
|
14
14
|
// tool that silently vanishes from the catalog, and a far better one than a
|
|
15
15
|
// `TypeError` inside the Agent loop.
|
|
@@ -38,11 +38,11 @@ export interface BotImageTurn {
|
|
|
38
38
|
}
|
|
39
39
|
|
|
40
40
|
/**
|
|
41
|
-
* The
|
|
41
|
+
* The native AI binding, declared structurally so this module names no
|
|
42
42
|
* Cloudflare type it does not have to. `run` is the whole of what an image
|
|
43
43
|
* model needs from it.
|
|
44
44
|
*/
|
|
45
|
-
export interface
|
|
45
|
+
export interface NativeAiBindingV1 {
|
|
46
46
|
run(model: string, input: Record<string, unknown>): Promise<unknown>;
|
|
47
47
|
}
|
|
48
48
|
|
|
@@ -51,7 +51,7 @@ export interface WorkersAiBindingV1 {
|
|
|
51
51
|
* as its own type so the binding's absence is a typed state, not a cast.
|
|
52
52
|
*/
|
|
53
53
|
export interface BotImageEnv {
|
|
54
|
-
AI?:
|
|
54
|
+
AI?: NativeAiBindingV1;
|
|
55
55
|
WORKSPACE_FILES?: WorkspaceFilesV1;
|
|
56
56
|
}
|
|
57
57
|
|
|
@@ -88,7 +88,7 @@ async function readStream(
|
|
|
88
88
|
}
|
|
89
89
|
|
|
90
90
|
/**
|
|
91
|
-
* The bytes inside whatever a
|
|
91
|
+
* The bytes inside whatever a native text-to-image model answered.
|
|
92
92
|
*
|
|
93
93
|
* The catalog does not agree with itself: `flux-1-schnell` and the FLUX.2
|
|
94
94
|
* models answer `{ image: "<base64>" }`, the Stable Diffusion models answer a
|
|
@@ -96,7 +96,7 @@ async function readStream(
|
|
|
96
96
|
* directly. Every one of those is an image; none of them is the Package's
|
|
97
97
|
* problem.
|
|
98
98
|
*/
|
|
99
|
-
export async function
|
|
99
|
+
export async function decodeNativeAiImageV1(
|
|
100
100
|
answer: unknown,
|
|
101
101
|
): Promise<ArrayBuffer> {
|
|
102
102
|
if (answer instanceof ArrayBuffer) return answer;
|
|
@@ -123,15 +123,15 @@ export async function decodeWorkersAiImageV1(
|
|
|
123
123
|
}
|
|
124
124
|
|
|
125
125
|
/**
|
|
126
|
-
* The
|
|
126
|
+
* The native AI binding as an {@link ImageModelV1}.
|
|
127
127
|
*
|
|
128
128
|
* `width` and `height` are passed through: the Stable Diffusion models accept
|
|
129
129
|
* them, and the FLUX models ignore unknown fields rather than refusing. The
|
|
130
130
|
* Package never trusts them to have been honoured — it reads the dimensions
|
|
131
131
|
* back out of the bytes.
|
|
132
132
|
*/
|
|
133
|
-
export function
|
|
134
|
-
binding:
|
|
133
|
+
export function createNativeAiImageModelV1(
|
|
134
|
+
binding: NativeAiBindingV1,
|
|
135
135
|
): ImageModelV1 {
|
|
136
136
|
return {
|
|
137
137
|
async run(model: string, input: ImageModelInputV1): Promise<ArrayBuffer> {
|
|
@@ -140,7 +140,7 @@ export function createWorkersAiImageModelV1(
|
|
|
140
140
|
width: input.width,
|
|
141
141
|
height: input.height,
|
|
142
142
|
});
|
|
143
|
-
return await
|
|
143
|
+
return await decodeNativeAiImageV1(answer);
|
|
144
144
|
},
|
|
145
145
|
};
|
|
146
146
|
}
|
|
@@ -174,7 +174,7 @@ export function createBotImageHost(
|
|
|
174
174
|
runId: turn.runId,
|
|
175
175
|
},
|
|
176
176
|
files,
|
|
177
|
-
...(bindings.AI ? { model:
|
|
177
|
+
...(bindings.AI ? { model: createNativeAiImageModelV1(bindings.AI) } : {}),
|
|
178
178
|
...(modelId ? { modelId } : {}),
|
|
179
179
|
};
|
|
180
180
|
}
|
|
@@ -5,26 +5,24 @@ import type {
|
|
|
5
5
|
} from "@frockbot/kernel-contracts";
|
|
6
6
|
import {
|
|
7
7
|
createIsolateCapabilityHost,
|
|
8
|
+
isolateBindingDigestV1,
|
|
8
9
|
isolateModelEventStreamV1,
|
|
9
10
|
ISOLATE_MODEL_FAILURE_MESSAGE,
|
|
10
11
|
ISOLATE_MODEL_REQUEST_PREFIX,
|
|
11
|
-
|
|
12
|
-
type
|
|
12
|
+
matchingModelCapabilityV1,
|
|
13
|
+
type IsolateCapabilityV1,
|
|
13
14
|
type IsolateModelBindingV1,
|
|
14
15
|
type IsolateModelRequestRecordV1,
|
|
15
16
|
} from "./backend-isolate.ts";
|
|
16
17
|
|
|
17
|
-
const
|
|
18
|
-
assignmentId: "model-assignment",
|
|
18
|
+
const CAPABILITY: IsolateCapabilityV1 = {
|
|
19
19
|
packageId: "provider-ollama-cloud",
|
|
20
20
|
capabilityId: "ollama-cloud-models",
|
|
21
21
|
kind: "model",
|
|
22
22
|
connectionId: "connection-1",
|
|
23
|
-
providerModelId: "glm-5.3-flash:cloud",
|
|
24
23
|
};
|
|
25
24
|
|
|
26
25
|
const BINDING: IsolateModelBindingV1 = {
|
|
27
|
-
assignmentId: "model-assignment",
|
|
28
26
|
packageId: "provider-ollama-cloud",
|
|
29
27
|
capabilityId: "ollama-cloud-models",
|
|
30
28
|
connectionId: "connection-1",
|
|
@@ -69,14 +67,19 @@ function memoryStorage() {
|
|
|
69
67
|
|
|
70
68
|
function host(
|
|
71
69
|
options: {
|
|
72
|
-
binding?: IsolateModelBindingV1;
|
|
70
|
+
binding?: IsolateModelBindingV1 | null;
|
|
71
|
+
capabilities?: IsolateCapabilityV1[];
|
|
72
|
+
unavailableModelBinding?: {
|
|
73
|
+
provider?: string;
|
|
74
|
+
providerModelId: string;
|
|
75
|
+
};
|
|
73
76
|
stream?: (request: NormalizedModelRequest) => AsyncIterable<LlmStreamEvent>;
|
|
74
77
|
} = {},
|
|
75
78
|
) {
|
|
76
79
|
const storage = memoryStorage();
|
|
77
80
|
const forwarded: NormalizedModelRequest[] = [];
|
|
78
81
|
let minted = 0;
|
|
79
|
-
const binding = options.binding
|
|
82
|
+
const binding = options.binding === undefined ? BINDING : options.binding;
|
|
80
83
|
return {
|
|
81
84
|
storage,
|
|
82
85
|
forwarded,
|
|
@@ -85,55 +88,65 @@ function host(
|
|
|
85
88
|
botId: "bot-1",
|
|
86
89
|
packageId: "bot-authored",
|
|
87
90
|
generationId: "generation-1",
|
|
88
|
-
|
|
89
|
-
|
|
90
|
-
|
|
91
|
-
|
|
92
|
-
|
|
93
|
-
|
|
94
|
-
|
|
95
|
-
|
|
96
|
-
|
|
97
|
-
|
|
98
|
-
|
|
99
|
-
|
|
100
|
-
|
|
91
|
+
capabilities: options.capabilities ?? [CAPABILITY],
|
|
92
|
+
...(binding
|
|
93
|
+
? {
|
|
94
|
+
modelBinding: binding,
|
|
95
|
+
modelPath: {
|
|
96
|
+
stream: (value: NormalizedModelRequest) => {
|
|
97
|
+
forwarded.push(structuredClone(value));
|
|
98
|
+
return (
|
|
99
|
+
options.stream?.(value) ??
|
|
100
|
+
(async function* () {
|
|
101
|
+
yield {
|
|
102
|
+
type: "text-delta",
|
|
103
|
+
text: "hi",
|
|
104
|
+
} as LlmStreamEvent;
|
|
105
|
+
})()
|
|
106
|
+
);
|
|
107
|
+
},
|
|
108
|
+
},
|
|
109
|
+
}
|
|
110
|
+
: {}),
|
|
111
|
+
...(options.unavailableModelBinding
|
|
112
|
+
? { unavailableModelBinding: options.unavailableModelBinding }
|
|
113
|
+
: {}),
|
|
101
114
|
newId: () => `minted-${(minted += 1)}`,
|
|
102
115
|
now: () => new Date("2026-08-31T00:00:00.000Z"),
|
|
103
116
|
}),
|
|
104
117
|
};
|
|
105
118
|
}
|
|
106
119
|
|
|
107
|
-
describe("an isolate model request is bound to its
|
|
108
|
-
test("matches only the exact
|
|
109
|
-
expect(
|
|
110
|
-
|
|
111
|
-
)
|
|
120
|
+
describe("an isolate model request is bound to its enabled Capability", () => {
|
|
121
|
+
test("matches only the exact authoritative provider and model", () => {
|
|
122
|
+
expect(matchingModelCapabilityV1([CAPABILITY], BINDING, request())).toEqual(
|
|
123
|
+
CAPABILITY,
|
|
124
|
+
);
|
|
112
125
|
expect(
|
|
113
|
-
|
|
114
|
-
[
|
|
126
|
+
matchingModelCapabilityV1(
|
|
127
|
+
[CAPABILITY],
|
|
115
128
|
BINDING,
|
|
116
129
|
request({ provider: "foundation" }),
|
|
117
130
|
),
|
|
118
131
|
).toBeUndefined();
|
|
119
132
|
expect(
|
|
120
|
-
|
|
121
|
-
[
|
|
133
|
+
matchingModelCapabilityV1(
|
|
134
|
+
[CAPABILITY],
|
|
122
135
|
BINDING,
|
|
123
136
|
request({ model: "some-other-model" }),
|
|
124
137
|
),
|
|
125
138
|
).toBeUndefined();
|
|
126
|
-
// No
|
|
139
|
+
// No authoritative binding at all: an enabled Capability on its own authorizes
|
|
127
140
|
// nothing.
|
|
128
141
|
expect(
|
|
129
|
-
|
|
142
|
+
matchingModelCapabilityV1([CAPABILITY], undefined, request()),
|
|
130
143
|
).toBeUndefined();
|
|
131
144
|
});
|
|
132
145
|
|
|
133
146
|
test("ignores a Bot-supplied model binding", () => {
|
|
134
147
|
expect(
|
|
135
|
-
|
|
136
|
-
[
|
|
148
|
+
matchingModelCapabilityV1(
|
|
149
|
+
[CAPABILITY],
|
|
137
150
|
BINDING,
|
|
138
151
|
request({
|
|
139
152
|
provider: "foundation",
|
|
@@ -144,7 +157,7 @@ describe("an isolate model request is bound to its Assignment", () => {
|
|
|
144
157
|
).toBeUndefined();
|
|
145
158
|
});
|
|
146
159
|
|
|
147
|
-
test("a request
|
|
160
|
+
test("a request outside the effective binding is a pending decision", async () => {
|
|
148
161
|
const subject = host();
|
|
149
162
|
const outcome = await subject.host.invokeModel(
|
|
150
163
|
request({ provider: "foundation", model: "deterministic-v1" }),
|
|
@@ -155,6 +168,53 @@ describe("an isolate model request is bound to its Assignment", () => {
|
|
|
155
168
|
expect(subject.forwarded).toHaveLength(0);
|
|
156
169
|
});
|
|
157
170
|
|
|
171
|
+
test("a request with no durable model binding is a pending decision", async () => {
|
|
172
|
+
const subject = host({ binding: null, capabilities: [] });
|
|
173
|
+
|
|
174
|
+
const outcome = await subject.host.invokeModel(request());
|
|
175
|
+
|
|
176
|
+
expect(outcome).toMatchObject({ status: "pending-user-decision" });
|
|
177
|
+
expect(await subject.host.pendingDecisions()).toHaveLength(1);
|
|
178
|
+
expect(await subject.host.recordedModelRequests()).toHaveLength(0);
|
|
179
|
+
expect(subject.forwarded).toHaveLength(0);
|
|
180
|
+
});
|
|
181
|
+
|
|
182
|
+
test("a held model binding with an unavailable Connection is unavailable", async () => {
|
|
183
|
+
const subject = host({
|
|
184
|
+
binding: null,
|
|
185
|
+
capabilities: [],
|
|
186
|
+
unavailableModelBinding: {
|
|
187
|
+
provider: BINDING.provider,
|
|
188
|
+
providerModelId: BINDING.providerModelId,
|
|
189
|
+
},
|
|
190
|
+
});
|
|
191
|
+
|
|
192
|
+
const outcome = await subject.host.invokeModel(request());
|
|
193
|
+
|
|
194
|
+
expect(outcome).toMatchObject({ status: "unavailable" });
|
|
195
|
+
expect(await subject.host.pendingDecisions()).toHaveLength(0);
|
|
196
|
+
expect(await subject.host.recordedModelRequests()).toHaveLength(0);
|
|
197
|
+
expect(subject.forwarded).toHaveLength(0);
|
|
198
|
+
});
|
|
199
|
+
|
|
200
|
+
test("an unavailable binding does not cover another provider", async () => {
|
|
201
|
+
const subject = host({
|
|
202
|
+
binding: null,
|
|
203
|
+
capabilities: [],
|
|
204
|
+
unavailableModelBinding: {
|
|
205
|
+
provider: BINDING.provider,
|
|
206
|
+
providerModelId: BINDING.providerModelId,
|
|
207
|
+
},
|
|
208
|
+
});
|
|
209
|
+
|
|
210
|
+
const outcome = await subject.host.invokeModel(
|
|
211
|
+
request({ provider: "foundation" }),
|
|
212
|
+
);
|
|
213
|
+
|
|
214
|
+
expect(outcome).toMatchObject({ status: "pending-user-decision" });
|
|
215
|
+
expect(await subject.host.pendingDecisions()).toHaveLength(1);
|
|
216
|
+
});
|
|
217
|
+
|
|
158
218
|
test("forwards the authority's binding, never the Bot's", async () => {
|
|
159
219
|
const subject = host();
|
|
160
220
|
await subject.host.invokeModel(
|
|
@@ -173,6 +233,92 @@ describe("an isolate model request is bound to its Assignment", () => {
|
|
|
173
233
|
});
|
|
174
234
|
});
|
|
175
235
|
|
|
236
|
+
describe("User-enabled isolate bindings", () => {
|
|
237
|
+
test("list projects the complete enabled set", async () => {
|
|
238
|
+
const subject = host();
|
|
239
|
+
|
|
240
|
+
await expect(subject.host.list()).resolves.toEqual([
|
|
241
|
+
{ capabilityId: "ollama-cloud-models", kind: "model" },
|
|
242
|
+
]);
|
|
243
|
+
});
|
|
244
|
+
|
|
245
|
+
test("identity, generation, and the enabled set are binding digest inputs", async () => {
|
|
246
|
+
const first = await isolateBindingDigestV1({
|
|
247
|
+
userId: "user-1",
|
|
248
|
+
botId: "bot-1",
|
|
249
|
+
generationId: "generation-1",
|
|
250
|
+
capabilities: [CAPABILITY],
|
|
251
|
+
});
|
|
252
|
+
const same = await isolateBindingDigestV1({
|
|
253
|
+
userId: "user-1",
|
|
254
|
+
botId: "bot-1",
|
|
255
|
+
generationId: "generation-1",
|
|
256
|
+
capabilities: [structuredClone(CAPABILITY)],
|
|
257
|
+
});
|
|
258
|
+
const otherUser = await isolateBindingDigestV1({
|
|
259
|
+
userId: "user-2",
|
|
260
|
+
botId: "bot-1",
|
|
261
|
+
generationId: "generation-1",
|
|
262
|
+
capabilities: [CAPABILITY],
|
|
263
|
+
});
|
|
264
|
+
const otherBot = await isolateBindingDigestV1({
|
|
265
|
+
userId: "user-1",
|
|
266
|
+
botId: "bot-2",
|
|
267
|
+
generationId: "generation-1",
|
|
268
|
+
capabilities: [CAPABILITY],
|
|
269
|
+
});
|
|
270
|
+
const otherGeneration = await isolateBindingDigestV1({
|
|
271
|
+
userId: "user-1",
|
|
272
|
+
botId: "bot-1",
|
|
273
|
+
generationId: "generation-2",
|
|
274
|
+
capabilities: [CAPABILITY],
|
|
275
|
+
});
|
|
276
|
+
const widened = await isolateBindingDigestV1({
|
|
277
|
+
userId: "user-1",
|
|
278
|
+
botId: "bot-1",
|
|
279
|
+
generationId: "generation-1",
|
|
280
|
+
capabilities: [
|
|
281
|
+
CAPABILITY,
|
|
282
|
+
{
|
|
283
|
+
packageId: "clock",
|
|
284
|
+
capabilityId: "clock",
|
|
285
|
+
kind: "tool",
|
|
286
|
+
},
|
|
287
|
+
],
|
|
288
|
+
});
|
|
289
|
+
|
|
290
|
+
expect(same).toBe(first);
|
|
291
|
+
expect(otherUser).not.toBe(first);
|
|
292
|
+
expect(otherBot).not.toBe(first);
|
|
293
|
+
expect(otherGeneration).not.toBe(first);
|
|
294
|
+
expect(widened).not.toBe(first);
|
|
295
|
+
});
|
|
296
|
+
|
|
297
|
+
test("ordering does not change the digest", async () => {
|
|
298
|
+
const clock: IsolateCapabilityV1 = {
|
|
299
|
+
packageId: "clock",
|
|
300
|
+
capabilityId: "clock",
|
|
301
|
+
kind: "tool",
|
|
302
|
+
};
|
|
303
|
+
|
|
304
|
+
await expect(
|
|
305
|
+
isolateBindingDigestV1({
|
|
306
|
+
userId: "user-1",
|
|
307
|
+
botId: "bot-1",
|
|
308
|
+
generationId: "generation-1",
|
|
309
|
+
capabilities: [CAPABILITY, clock],
|
|
310
|
+
}),
|
|
311
|
+
).resolves.toBe(
|
|
312
|
+
await isolateBindingDigestV1({
|
|
313
|
+
userId: "user-1",
|
|
314
|
+
botId: "bot-1",
|
|
315
|
+
generationId: "generation-1",
|
|
316
|
+
capabilities: [clock, CAPABILITY],
|
|
317
|
+
}),
|
|
318
|
+
);
|
|
319
|
+
});
|
|
320
|
+
});
|
|
321
|
+
|
|
176
322
|
describe("the isolate model request record", () => {
|
|
177
323
|
test("two invocations reusing one Bot request id produce two records", async () => {
|
|
178
324
|
const subject = host();
|