@frockbot/plugin-shell 0.1.3 → 0.2.0
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/frockbot.json +4 -0
- package/package.json +30 -28
- package/src/agent.ts +10 -13
- package/src/backend-authoring.test.ts +356 -2
- package/src/backend-authoring.ts +585 -20
- package/src/backend-composition-input.test.ts +65 -0
- package/src/backend-composition-input.ts +58 -0
- package/src/backend-composition.ts +23 -5
- package/src/backend-configuration.test.ts +549 -1468
- package/src/backend-contracts.test.ts +28 -0
- package/src/backend-contracts.ts +3 -1
- package/src/backend-execution.ts +0 -4
- package/src/backend-iframe-ui.test.ts +131 -0
- package/src/backend-image.test.ts +8 -8
- package/src/backend-image.ts +13 -13
- package/src/backend-isolate.test.ts +230 -89
- package/src/backend-isolate.ts +103 -200
- package/src/backend-package-catalog.test.ts +451 -0
- package/src/backend-package-catalog.ts +923 -0
- package/src/backend-recovery-integration.test.ts +17 -67
- package/src/backend-routines.ts +1 -1
- package/src/backend-runner-iframe.test.ts +74 -0
- package/src/backend-runner.ts +192 -0
- package/src/backend.ts +1198 -1781
- package/src/client/FrockBotApp.vue +44 -73
- package/src/client/PackageIframeHost.vue +218 -0
- package/src/client/PackageIframeSettings.vue +52 -0
- package/src/client/SendPayloadView.vue +0 -60
- package/src/client/index.test.ts +439 -380
- package/src/client/index.ts +163 -257
- package/src/client/model-presentation.test.ts +21 -8
- package/src/client/model-presentation.ts +14 -7
- package/src/client/package-iframe-host-message.test.ts +41 -0
- package/src/client/package-iframe-host-message.ts +27 -0
- package/src/client/styles.css +0 -27
- package/src/composition-views.ts +54 -0
- package/src/settings-links.test.ts +2 -10
- package/src/settings-links.ts +1 -13
- package/src/shared.ts +10 -13
- package/src/backend-assignment.test.ts +0 -161
- package/src/backend-assignment.ts +0 -274
|
@@ -1,19 +1,26 @@
|
|
|
1
1
|
/**
|
|
2
|
-
* The model line the shell shows above the composer.
|
|
3
|
-
*
|
|
4
|
-
*
|
|
5
|
-
*
|
|
2
|
+
* The model line the shell shows above the composer. Platform choices read as
|
|
3
|
+
* the model itself; opting into an account choice or Bot override makes that
|
|
4
|
+
* distinction visible. A resolver failure is already the backend's complete,
|
|
5
|
+
* repairable explanation, so the client presents it verbatim.
|
|
6
6
|
*/
|
|
7
7
|
export function modelRuntimeLabel(input: {
|
|
8
|
+
source: "bot" | "account" | "platform" | "none";
|
|
8
9
|
modelDisplayName?: string;
|
|
9
10
|
providerModelId?: string;
|
|
10
11
|
packageDisplayName?: string;
|
|
11
12
|
connectionDisplayName?: string;
|
|
12
|
-
|
|
13
|
+
failure?: string;
|
|
13
14
|
}): string {
|
|
14
|
-
if (
|
|
15
|
+
if (input.failure) return input.failure;
|
|
16
|
+
if (input.source === "none" || !input.providerModelId) {
|
|
17
|
+
return "Model unavailable";
|
|
18
|
+
}
|
|
15
19
|
const model =
|
|
16
20
|
input.modelDisplayName ?? input.providerModelId ?? "Connected model";
|
|
17
21
|
const provider = input.packageDisplayName ?? input.connectionDisplayName;
|
|
18
|
-
|
|
22
|
+
const runtime = provider ? `${model} · ${provider}` : model;
|
|
23
|
+
if (input.source === "bot") return `${runtime} · Bot override`;
|
|
24
|
+
if (input.source === "account") return `${runtime} · Account model`;
|
|
25
|
+
return runtime;
|
|
19
26
|
}
|
|
@@ -0,0 +1,41 @@
|
|
|
1
|
+
import { describe, expect, test } from "bun:test";
|
|
2
|
+
import type { PackageIframeHostMessageV1 } from "@frockbot/kernel-contracts";
|
|
3
|
+
import { postPackageIframeHostMessage } from "./package-iframe-host-message.js";
|
|
4
|
+
|
|
5
|
+
describe("Package iframe host messages", () => {
|
|
6
|
+
test("posts unchanged state once and posts changed state again", () => {
|
|
7
|
+
const posted: PackageIframeHostMessageV1[] = [];
|
|
8
|
+
const target = {
|
|
9
|
+
postMessage(message: PackageIframeHostMessageV1): void {
|
|
10
|
+
posted.push(message);
|
|
11
|
+
},
|
|
12
|
+
} as Pick<Window, "postMessage">;
|
|
13
|
+
const lastStateWireByName = new Map<string, string>();
|
|
14
|
+
|
|
15
|
+
const postSettings = (value: unknown): void =>
|
|
16
|
+
postPackageIframeHostMessage(
|
|
17
|
+
target,
|
|
18
|
+
{ schemaVersion: 1, type: "state", name: "settings", value },
|
|
19
|
+
lastStateWireByName,
|
|
20
|
+
);
|
|
21
|
+
|
|
22
|
+
postSettings({ temperatureUnit: "celsius" });
|
|
23
|
+
postSettings({ temperatureUnit: "celsius" });
|
|
24
|
+
postSettings({ temperatureUnit: "fahrenheit" });
|
|
25
|
+
|
|
26
|
+
expect(posted).toEqual([
|
|
27
|
+
{
|
|
28
|
+
schemaVersion: 1,
|
|
29
|
+
type: "state",
|
|
30
|
+
name: "settings",
|
|
31
|
+
value: { temperatureUnit: "celsius" },
|
|
32
|
+
},
|
|
33
|
+
{
|
|
34
|
+
schemaVersion: 1,
|
|
35
|
+
type: "state",
|
|
36
|
+
name: "settings",
|
|
37
|
+
value: { temperatureUnit: "fahrenheit" },
|
|
38
|
+
},
|
|
39
|
+
]);
|
|
40
|
+
});
|
|
41
|
+
});
|
|
@@ -0,0 +1,27 @@
|
|
|
1
|
+
import type { PackageIframeHostMessageV1 } from "@frockbot/kernel-contracts";
|
|
2
|
+
|
|
3
|
+
type MessageTarget = Pick<Window, "postMessage">;
|
|
4
|
+
|
|
5
|
+
export function postPackageIframeHostMessage(
|
|
6
|
+
target: MessageTarget,
|
|
7
|
+
message: PackageIframeHostMessageV1,
|
|
8
|
+
lastStateWireByName: Map<string, string>,
|
|
9
|
+
): void {
|
|
10
|
+
const wire = JSON.stringify(message);
|
|
11
|
+
if (new TextEncoder().encode(wire).byteLength > 64 * 1024) {
|
|
12
|
+
throw new Error("Package page state exceeds the bridge limit");
|
|
13
|
+
}
|
|
14
|
+
if (
|
|
15
|
+
message.type === "state" &&
|
|
16
|
+
lastStateWireByName.get(message.name) === wire
|
|
17
|
+
) {
|
|
18
|
+
return;
|
|
19
|
+
}
|
|
20
|
+
|
|
21
|
+
// Vue settings values may be reactive proxies, which structured clone
|
|
22
|
+
// rejects. JSON is also the bridge's declared value domain.
|
|
23
|
+
target.postMessage(JSON.parse(wire) as PackageIframeHostMessageV1, "*");
|
|
24
|
+
if (message.type === "state") {
|
|
25
|
+
lastStateWireByName.set(message.name, wire);
|
|
26
|
+
}
|
|
27
|
+
}
|
package/src/client/styles.css
CHANGED
|
@@ -175,27 +175,6 @@
|
|
|
175
175
|
text-overflow: ellipsis;
|
|
176
176
|
}
|
|
177
177
|
|
|
178
|
-
.model-setup-link {
|
|
179
|
-
width: max-content;
|
|
180
|
-
max-width: 100%;
|
|
181
|
-
margin-top: 3px;
|
|
182
|
-
overflow: hidden;
|
|
183
|
-
border: 0;
|
|
184
|
-
padding: 0;
|
|
185
|
-
color: var(--frock-action-primary);
|
|
186
|
-
background: transparent;
|
|
187
|
-
font: inherit;
|
|
188
|
-
font-size: var(--frock-text-sm);
|
|
189
|
-
text-align: left;
|
|
190
|
-
text-overflow: ellipsis;
|
|
191
|
-
white-space: nowrap;
|
|
192
|
-
cursor: pointer;
|
|
193
|
-
}
|
|
194
|
-
|
|
195
|
-
.model-setup-link:hover {
|
|
196
|
-
color: var(--frock-action-primary-hover);
|
|
197
|
-
}
|
|
198
|
-
|
|
199
178
|
/*
|
|
200
179
|
* The panel collapse control is pinned to the window's trailing edge. Feature
|
|
201
180
|
* actions live in the panel's own header.
|
|
@@ -570,12 +549,6 @@
|
|
|
570
549
|
0 0 0 3px var(--frock-focus-ring);
|
|
571
550
|
}
|
|
572
551
|
|
|
573
|
-
.composer-model-setup {
|
|
574
|
-
min-width: 0;
|
|
575
|
-
flex: 1;
|
|
576
|
-
align-self: center;
|
|
577
|
-
}
|
|
578
|
-
|
|
579
552
|
.composer textarea {
|
|
580
553
|
box-sizing: border-box;
|
|
581
554
|
width: 100%;
|
package/src/composition-views.ts
CHANGED
|
@@ -9,6 +9,11 @@ import {
|
|
|
9
9
|
type CompositionMemberViewV1,
|
|
10
10
|
type CompositionProvenanceViewV1,
|
|
11
11
|
} from "@frockbot/configuration-core";
|
|
12
|
+
import type { PackageIframeCompositionV1 } from "@frockbot/kernel-contracts";
|
|
13
|
+
import {
|
|
14
|
+
isClientIframeContribution,
|
|
15
|
+
type FrockBotManifest,
|
|
16
|
+
} from "@frockbot/kernel-composition";
|
|
12
17
|
import type {
|
|
13
18
|
CompositionFailureV1,
|
|
14
19
|
CompositionQuarantineV1,
|
|
@@ -29,11 +34,57 @@ export type CompositionMemberSourceReaderV1 = (
|
|
|
29
34
|
member: CompositionMemberV1,
|
|
30
35
|
) => Promise<string | undefined>;
|
|
31
36
|
|
|
37
|
+
/** One decoded manifest lookup shared by isolate mount and hosted UI views. */
|
|
38
|
+
export type CompositionMemberManifestReaderV1 = (
|
|
39
|
+
member: CompositionMemberV1,
|
|
40
|
+
) => Promise<FrockBotManifest | undefined>;
|
|
41
|
+
|
|
42
|
+
/** Project iframe Contributions from either Bot or Catalog provenance. */
|
|
43
|
+
export async function projectPackageIframeCompositionV1(input: {
|
|
44
|
+
botId: string;
|
|
45
|
+
generation: CompositionGenerationV1;
|
|
46
|
+
readMemberManifest: CompositionMemberManifestReaderV1;
|
|
47
|
+
}): Promise<PackageIframeCompositionV1> {
|
|
48
|
+
const contributions: PackageIframeCompositionV1["contributions"] = [];
|
|
49
|
+
for (const member of input.generation.members) {
|
|
50
|
+
if (member.provenance.kind === "first-party") continue;
|
|
51
|
+
const manifest = await input.readMemberManifest(member);
|
|
52
|
+
if (!manifest) continue;
|
|
53
|
+
const client = manifest.contributions.client;
|
|
54
|
+
if (!client || !isClientIframeContribution(client)) continue;
|
|
55
|
+
contributions.push({
|
|
56
|
+
packageId: member.packageId,
|
|
57
|
+
displayName: manifest.displayName,
|
|
58
|
+
provenance:
|
|
59
|
+
member.provenance.kind === "bot" ? "Bot-authored" : "User-installed",
|
|
60
|
+
artifact: { ...client.artifact },
|
|
61
|
+
mounts: client.mounts.map((mount) => ({ ...mount })),
|
|
62
|
+
declaredTools: (manifest.tools ?? []).map((tool) => tool.name),
|
|
63
|
+
});
|
|
64
|
+
}
|
|
65
|
+
contributions.sort((left, right) =>
|
|
66
|
+
left.packageId.localeCompare(right.packageId),
|
|
67
|
+
);
|
|
68
|
+
return {
|
|
69
|
+
schemaVersion: 1,
|
|
70
|
+
botId: input.botId,
|
|
71
|
+
generationId: input.generation.generationId,
|
|
72
|
+
contributions,
|
|
73
|
+
};
|
|
74
|
+
}
|
|
75
|
+
|
|
32
76
|
function provenanceView(
|
|
33
77
|
member: CompositionMemberV1,
|
|
34
78
|
): CompositionProvenanceViewV1 {
|
|
35
79
|
const provenance = member.provenance;
|
|
36
80
|
if (provenance.kind === "first-party") return { kind: "first-party" };
|
|
81
|
+
if (provenance.kind === "catalog") {
|
|
82
|
+
return {
|
|
83
|
+
kind: "catalog",
|
|
84
|
+
catalogId: provenance.catalogId,
|
|
85
|
+
catalogGeneration: provenance.catalogGeneration,
|
|
86
|
+
};
|
|
87
|
+
}
|
|
37
88
|
if (provenance.kind === "user") {
|
|
38
89
|
return {
|
|
39
90
|
kind: "user",
|
|
@@ -102,6 +153,9 @@ export async function projectCompositionGenerationV1(
|
|
|
102
153
|
isCurrent: input.generation.generationId === input.currentGenerationId,
|
|
103
154
|
members,
|
|
104
155
|
failures,
|
|
156
|
+
...(input.generation.summary === undefined
|
|
157
|
+
? {}
|
|
158
|
+
: { summary: input.generation.summary }),
|
|
105
159
|
...(input.quarantine === undefined
|
|
106
160
|
? {}
|
|
107
161
|
: {
|
|
@@ -23,11 +23,11 @@ describe("settings link scheme", () => {
|
|
|
23
23
|
test("renders against an origin when one is supplied", () => {
|
|
24
24
|
expect(
|
|
25
25
|
settingsLinkV1({
|
|
26
|
-
anchor: "bot-
|
|
26
|
+
anchor: "bot-name",
|
|
27
27
|
botId: "alpha",
|
|
28
28
|
origin: "https://app.example/",
|
|
29
29
|
}),
|
|
30
|
-
).toBe("https://app.example/?bot=alpha&settings=bot-settings#bot-
|
|
30
|
+
).toBe("https://app.example/?bot=alpha&settings=bot-settings#bot-name");
|
|
31
31
|
});
|
|
32
32
|
|
|
33
33
|
test("refuses an anchor this build does not ship", () => {
|
|
@@ -36,14 +36,6 @@ describe("settings link scheme", () => {
|
|
|
36
36
|
);
|
|
37
37
|
});
|
|
38
38
|
|
|
39
|
-
test("renders a Markdown citation a payload can carry verbatim", () => {
|
|
40
|
-
expect(
|
|
41
|
-
renderSettingsLinkV1({ anchor: "bot-capabilities", botId: "alpha" }),
|
|
42
|
-
).toBe(
|
|
43
|
-
"[Capability Assignments](/?bot=alpha&settings=bot-settings#bot-capabilities)",
|
|
44
|
-
);
|
|
45
|
-
});
|
|
46
|
-
|
|
47
39
|
test("decodes an absolute link back to its surface, anchor and Bot", () => {
|
|
48
40
|
expect(
|
|
49
41
|
decodeSettingsLinkV1(
|
package/src/settings-links.ts
CHANGED
|
@@ -108,18 +108,6 @@ export const SETTINGS_ANCHORS_V1: readonly SettingsAnchorV1[] = [
|
|
|
108
108
|
label: "Waiting on you",
|
|
109
109
|
scope: "bot",
|
|
110
110
|
},
|
|
111
|
-
{
|
|
112
|
-
anchor: "bot-model",
|
|
113
|
-
surface: "bot-settings",
|
|
114
|
-
label: "Model",
|
|
115
|
-
scope: "bot",
|
|
116
|
-
},
|
|
117
|
-
{
|
|
118
|
-
anchor: "bot-capabilities",
|
|
119
|
-
surface: "bot-settings",
|
|
120
|
-
label: "Capability Assignments",
|
|
121
|
-
scope: "bot",
|
|
122
|
-
},
|
|
123
111
|
{
|
|
124
112
|
anchor: "bot-routines",
|
|
125
113
|
surface: "bot-settings",
|
|
@@ -194,7 +182,7 @@ export const SETTINGS_ANCHORS_V1: readonly SettingsAnchorV1[] = [
|
|
|
194
182
|
{
|
|
195
183
|
anchor: "user-connections",
|
|
196
184
|
surface: "connections",
|
|
197
|
-
label: "
|
|
185
|
+
label: "Connectors",
|
|
198
186
|
scope: "user",
|
|
199
187
|
},
|
|
200
188
|
{
|
package/src/shared.ts
CHANGED
|
@@ -7,8 +7,6 @@ import type {
|
|
|
7
7
|
BotProfile,
|
|
8
8
|
BotProfilePatchV1,
|
|
9
9
|
BotSettingsViewV1,
|
|
10
|
-
CapabilityAssignmentView,
|
|
11
|
-
ModelAssignment,
|
|
12
10
|
UserSettingsViewV1,
|
|
13
11
|
} from "@frockbot/configuration-core";
|
|
14
12
|
import type {
|
|
@@ -16,6 +14,8 @@ import type {
|
|
|
16
14
|
CatalogIndexEntryV1,
|
|
17
15
|
} from "@frockbot/catalog-core";
|
|
18
16
|
import type {
|
|
17
|
+
PackageIframeCatalogV1,
|
|
18
|
+
PackageIframeContributionViewV1,
|
|
19
19
|
SendToUserPayloadV1,
|
|
20
20
|
SkillRefV1,
|
|
21
21
|
} from "@frockbot/kernel-contracts";
|
|
@@ -189,6 +189,8 @@ export interface FrockBotWebData {
|
|
|
189
189
|
* without the run's own events being rewritten.
|
|
190
190
|
*/
|
|
191
191
|
tasks: TaskViewV1[];
|
|
192
|
+
/** Sandboxed pages in the selected Bot's active fail-closed Composition. */
|
|
193
|
+
packageUi?: PackageIframeCatalogV1;
|
|
192
194
|
/**
|
|
193
195
|
* The User's MCP servers: state, tool count, last handshake, instructions,
|
|
194
196
|
* failure, and the durable refusal ledger. Absent until it is loaded, and
|
|
@@ -206,19 +208,8 @@ export interface FrockBotWebData {
|
|
|
206
208
|
namedBy?: BotNameProvenanceV1,
|
|
207
209
|
): Promise<void>;
|
|
208
210
|
saveBotNotifications(notifications: BotNotificationPolicy): Promise<void>;
|
|
209
|
-
assignCapability(
|
|
210
|
-
assignment: Omit<CapabilityAssignmentView, "state">,
|
|
211
|
-
): Promise<void>;
|
|
212
|
-
replaceCapability(
|
|
213
|
-
assignment: Omit<CapabilityAssignmentView, "state">,
|
|
214
|
-
): Promise<void>;
|
|
215
|
-
unassignCapability(assignmentId: string): Promise<void>;
|
|
216
|
-
saveBotModel(model: ModelAssignment): Promise<void>;
|
|
217
|
-
clearBotModel(): Promise<void>;
|
|
218
211
|
loadUserSettings(): Promise<void>;
|
|
219
212
|
saveUserProfile(profile: { name: string; email?: string }): Promise<void>;
|
|
220
|
-
/** The model every Bot uses unless it overrides it. */
|
|
221
|
-
saveDefaultModel(model: ModelAssignment | undefined): Promise<void>;
|
|
222
213
|
loadPluginCatalog(): Promise<void>;
|
|
223
214
|
/** Refreshes {@link FrockBotWebData.mcpServers}. */
|
|
224
215
|
loadMcpServers(): Promise<void>;
|
|
@@ -253,6 +244,12 @@ export interface FrockBotWebData {
|
|
|
253
244
|
loadApprovals(): Promise<void>;
|
|
254
245
|
/** Refreshes {@link FrockBotWebData.tasks} for the active Bot. */
|
|
255
246
|
loadTasks(): Promise<void>;
|
|
247
|
+
loadPackageUi(): Promise<void>;
|
|
248
|
+
callPackageUiTool(
|
|
249
|
+
contribution: PackageIframeContributionViewV1,
|
|
250
|
+
name: string,
|
|
251
|
+
input: unknown,
|
|
252
|
+
): Promise<unknown>;
|
|
256
253
|
/**
|
|
257
254
|
* Cancels one subagent, explicitly and with the User's authentication. The
|
|
258
255
|
* backend is the authority: the task this replaces in the list is the record
|
|
@@ -1,161 +0,0 @@
|
|
|
1
|
-
import { describe, expect, test } from "bun:test";
|
|
2
|
-
import {
|
|
3
|
-
nextAssignmentPhase,
|
|
4
|
-
requireStoredAssignmentSaga,
|
|
5
|
-
settleAssignmentSaga,
|
|
6
|
-
type AssignmentSagaEffects,
|
|
7
|
-
type StoredAssignmentSaga,
|
|
8
|
-
} from "./backend-assignment.js";
|
|
9
|
-
|
|
10
|
-
function saga(
|
|
11
|
-
phase: StoredAssignmentSaga["phase"],
|
|
12
|
-
input: Partial<StoredAssignmentSaga> = {},
|
|
13
|
-
): StoredAssignmentSaga {
|
|
14
|
-
return {
|
|
15
|
-
schemaVersion: 1,
|
|
16
|
-
commandId: "command-1",
|
|
17
|
-
commandFingerprint: "configuration-command-v1:test",
|
|
18
|
-
userId: "user-1",
|
|
19
|
-
botId: "bot-1",
|
|
20
|
-
operation: "replacing",
|
|
21
|
-
assignmentId: "mail",
|
|
22
|
-
generation: "generation-1",
|
|
23
|
-
phase,
|
|
24
|
-
target: {
|
|
25
|
-
assignmentId: "mail",
|
|
26
|
-
packageId: "mail",
|
|
27
|
-
capabilityId: "send",
|
|
28
|
-
connectionId: "new-connection",
|
|
29
|
-
},
|
|
30
|
-
previous: {
|
|
31
|
-
assignmentId: "mail",
|
|
32
|
-
packageId: "mail",
|
|
33
|
-
capabilityId: "send",
|
|
34
|
-
connectionId: "old-connection",
|
|
35
|
-
state: "enabled",
|
|
36
|
-
},
|
|
37
|
-
previousGeneration: "old-generation",
|
|
38
|
-
deadlineAt: Date.now() + 60_000,
|
|
39
|
-
...input,
|
|
40
|
-
acceptedReceipt: input.acceptedReceipt ?? {
|
|
41
|
-
schemaVersion: 1,
|
|
42
|
-
commandId: "command-1",
|
|
43
|
-
revision: 0,
|
|
44
|
-
status: "pending",
|
|
45
|
-
},
|
|
46
|
-
};
|
|
47
|
-
}
|
|
48
|
-
|
|
49
|
-
function effects(log: string[], acknowledge = true): AssignmentSagaEffects {
|
|
50
|
-
return {
|
|
51
|
-
acknowledge: () => {
|
|
52
|
-
log.push("acknowledge");
|
|
53
|
-
return Promise.resolve(acknowledge);
|
|
54
|
-
},
|
|
55
|
-
compensate: () => {
|
|
56
|
-
log.push("compensate");
|
|
57
|
-
return Promise.resolve();
|
|
58
|
-
},
|
|
59
|
-
release: () => {
|
|
60
|
-
log.push("release");
|
|
61
|
-
return Promise.resolve(true);
|
|
62
|
-
},
|
|
63
|
-
rejectCommitted: () => {
|
|
64
|
-
log.push("reject-committed");
|
|
65
|
-
return Promise.resolve();
|
|
66
|
-
},
|
|
67
|
-
};
|
|
68
|
-
}
|
|
69
|
-
|
|
70
|
-
describe("Assignment saga transitions", () => {
|
|
71
|
-
test("orders Replace as claim, commit, acknowledge, release", () => {
|
|
72
|
-
const committed = nextAssignmentPhase(saga("claiming"), "claimed")!;
|
|
73
|
-
const acknowledged = nextAssignmentPhase(committed, "committed")!;
|
|
74
|
-
const releasing = nextAssignmentPhase(acknowledged, "acknowledged")!;
|
|
75
|
-
expect([committed.phase, acknowledged.phase, releasing.phase]).toEqual([
|
|
76
|
-
"committing",
|
|
77
|
-
"acknowledging",
|
|
78
|
-
"releasing",
|
|
79
|
-
]);
|
|
80
|
-
expect(nextAssignmentPhase(releasing, "released")).toBeUndefined();
|
|
81
|
-
});
|
|
82
|
-
|
|
83
|
-
test("finishes a connection-free Assign after commit", () => {
|
|
84
|
-
expect(
|
|
85
|
-
nextAssignmentPhase(
|
|
86
|
-
saga("committing", {
|
|
87
|
-
operation: "assigning",
|
|
88
|
-
target: {
|
|
89
|
-
assignmentId: "clock",
|
|
90
|
-
packageId: "clock",
|
|
91
|
-
capabilityId: "time",
|
|
92
|
-
},
|
|
93
|
-
previous: undefined,
|
|
94
|
-
previousGeneration: undefined,
|
|
95
|
-
}),
|
|
96
|
-
"committed",
|
|
97
|
-
),
|
|
98
|
-
).toBeUndefined();
|
|
99
|
-
});
|
|
100
|
-
|
|
101
|
-
test("strictly decodes durable saga state", () => {
|
|
102
|
-
expect(requireStoredAssignmentSaga(saga("claiming"))).toMatchObject({
|
|
103
|
-
operation: "replacing",
|
|
104
|
-
phase: "claiming",
|
|
105
|
-
});
|
|
106
|
-
expect(() =>
|
|
107
|
-
requireStoredAssignmentSaga({ ...saga("claiming"), extra: true }),
|
|
108
|
-
).toThrow("invalid fields");
|
|
109
|
-
const hidden = saga("claiming") as StoredAssignmentSaga & { hidden?: true };
|
|
110
|
-
Object.defineProperty(hidden, "hidden", { value: true });
|
|
111
|
-
expect(() => requireStoredAssignmentSaga(hidden)).toThrow("invalid fields");
|
|
112
|
-
expect(() =>
|
|
113
|
-
requireStoredAssignmentSaga({
|
|
114
|
-
...saga("claiming"),
|
|
115
|
-
[Symbol("extra")]: true,
|
|
116
|
-
}),
|
|
117
|
-
).toThrow("invalid fields");
|
|
118
|
-
expect(() =>
|
|
119
|
-
requireStoredAssignmentSaga({
|
|
120
|
-
...saga("claiming"),
|
|
121
|
-
acceptedReceipt: {
|
|
122
|
-
schemaVersion: 1,
|
|
123
|
-
commandId: "command-1",
|
|
124
|
-
revision: 0,
|
|
125
|
-
status: "applied",
|
|
126
|
-
},
|
|
127
|
-
}),
|
|
128
|
-
).toThrow("accepted receipt is invalid");
|
|
129
|
-
});
|
|
130
|
-
|
|
131
|
-
test("rejects out-of-order advancement", () => {
|
|
132
|
-
expect(() => nextAssignmentPhase(saga("claiming"), "released")).toThrow(
|
|
133
|
-
"cannot apply released while claiming",
|
|
134
|
-
);
|
|
135
|
-
});
|
|
136
|
-
|
|
137
|
-
test("compensates a claiming saga and rejects an unacknowledged commit", async () => {
|
|
138
|
-
const compensated: string[] = [];
|
|
139
|
-
await expect(
|
|
140
|
-
settleAssignmentSaga(saga("claiming"), effects(compensated)),
|
|
141
|
-
).resolves.toBe("compensated");
|
|
142
|
-
expect(compensated).toEqual(["compensate"]);
|
|
143
|
-
|
|
144
|
-
const acknowledged: string[] = [];
|
|
145
|
-
await expect(
|
|
146
|
-
settleAssignmentSaga(saga("acknowledging"), effects(acknowledged)),
|
|
147
|
-
).resolves.toBe("acknowledged");
|
|
148
|
-
expect(acknowledged).toEqual(["acknowledge"]);
|
|
149
|
-
|
|
150
|
-
const log: string[] = [];
|
|
151
|
-
await expect(
|
|
152
|
-
settleAssignmentSaga(saga("acknowledging"), effects(log, false)),
|
|
153
|
-
).resolves.toBe("rejected");
|
|
154
|
-
expect(log).toEqual([
|
|
155
|
-
"acknowledge",
|
|
156
|
-
"compensate",
|
|
157
|
-
"release",
|
|
158
|
-
"reject-committed",
|
|
159
|
-
]);
|
|
160
|
-
});
|
|
161
|
-
});
|