@frockbot/plugin-shell 0.3.22 → 0.3.23
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 +32 -32
- package/src/backend-recovery-integration.test.ts +134 -0
- package/src/backend.ts +93 -42
- package/src/client/AppletCanvas.vue +128 -30
- package/src/client/FrockBotApp.vue +111 -2
- package/src/client/applet-building-view.test.ts +69 -0
- package/src/client/applet-progress.test.ts +300 -0
- package/src/client/applet-progress.ts +339 -0
- package/src/client/deployment.test.ts +152 -0
- package/src/client/deployment.ts +138 -0
- package/src/client/index.test.ts +71 -0
- package/src/client/index.ts +58 -0
- package/src/client/styles.css +91 -4
- package/src/run-protocol.ts +10 -0
- package/src/shared.ts +14 -0
|
@@ -56,6 +56,7 @@ import {
|
|
|
56
56
|
} from "./turn-limits.js";
|
|
57
57
|
import SendPayloadView from "./SendPayloadView.vue";
|
|
58
58
|
import AppletCanvas from "./AppletCanvas.vue";
|
|
59
|
+
import { appletProgressToolsV1, appletProgressV1 } from "./applet-progress.js";
|
|
59
60
|
import PackageIframeHost from "./PackageIframeHost.vue";
|
|
60
61
|
import type { ClientSkillCatalogEntryV1 } from "../skill-protocol.js";
|
|
61
62
|
import {
|
|
@@ -67,6 +68,14 @@ import {
|
|
|
67
68
|
textWithoutSkillTriggerV1,
|
|
68
69
|
type SkillPopoverStateV1,
|
|
69
70
|
} from "./skill-invocation.js";
|
|
71
|
+
import {
|
|
72
|
+
DEPLOYMENT_RELOAD_LABEL_V1,
|
|
73
|
+
DEPLOYMENT_UPDATED_MESSAGE_V1,
|
|
74
|
+
deploymentFollowV1,
|
|
75
|
+
deploymentReloadStoreV1,
|
|
76
|
+
readDeploymentReloadV1,
|
|
77
|
+
writeDeploymentReloadV1,
|
|
78
|
+
} from "./deployment.js";
|
|
70
79
|
|
|
71
80
|
const injectedWeb = inject(frockBotWebDataKey);
|
|
72
81
|
if (!injectedWeb) throw new Error("shell client data was not provided");
|
|
@@ -264,6 +273,25 @@ const appletChip = computed(() =>
|
|
|
264
273
|
? (state.value.focusedApplet?.displayName ?? "Applet")
|
|
265
274
|
: undefined,
|
|
266
275
|
);
|
|
276
|
+
/*
|
|
277
|
+
* What the Bot is doing to it, on the phone.
|
|
278
|
+
*
|
|
279
|
+
* There is no room beside the conversation here, so the chip carries the line
|
|
280
|
+
* the canvas would have shown and opens the canvas over the whole screen. It
|
|
281
|
+
* is the same projection and the same words: a person who moves between their
|
|
282
|
+
* phone and a laptop reads one story, not two.
|
|
283
|
+
*/
|
|
284
|
+
const appletChipStatus = computed(() => {
|
|
285
|
+
if (!appletChip.value) return undefined;
|
|
286
|
+
const progress = appletProgressV1({
|
|
287
|
+
applet: state.value.focusedApplet ?? null,
|
|
288
|
+
source: state.value.appletSource,
|
|
289
|
+
build: state.value.appletBuild,
|
|
290
|
+
tools: appletProgressToolsV1(state.value.messages),
|
|
291
|
+
running: Boolean(state.value.activeRunId),
|
|
292
|
+
});
|
|
293
|
+
return progress && progress.stage !== "published" ? progress : undefined;
|
|
294
|
+
});
|
|
267
295
|
/*
|
|
268
296
|
* Skill invocation. `/` or `@` at a word boundary opens a popover over the
|
|
269
297
|
* Bot's catalog; choosing one attaches a ref chip and removes the trigger from
|
|
@@ -411,6 +439,57 @@ let voiceTail = "";
|
|
|
411
439
|
let voiceSendOnFinal = false;
|
|
412
440
|
|
|
413
441
|
const dictating = computed(() => voiceState.value !== "idle");
|
|
442
|
+
|
|
443
|
+
/*
|
|
444
|
+
* Following a release.
|
|
445
|
+
*
|
|
446
|
+
* A reload is destructive, so the shell only does it on its own when there is
|
|
447
|
+
* nothing to lose, and otherwise says so and waits. The bar is not an alert:
|
|
448
|
+
* nothing is wrong, there is just newer code to pick up.
|
|
449
|
+
*/
|
|
450
|
+
const reloadStore = deploymentReloadStoreV1();
|
|
451
|
+
const lastReloadedAt = readDeploymentReloadV1(reloadStore);
|
|
452
|
+
const updateBarVisible = ref(false);
|
|
453
|
+
|
|
454
|
+
function followDeployment(): void {
|
|
455
|
+
const decision = deploymentFollowV1({
|
|
456
|
+
stale: state.value.deploymentStale,
|
|
457
|
+
turnRunning: Boolean(state.value.runningRunId),
|
|
458
|
+
draft: draft.value,
|
|
459
|
+
overlayOpen: Boolean(overlaySurface.value),
|
|
460
|
+
listening: dictating.value,
|
|
461
|
+
holds: state.value.reloadHolds,
|
|
462
|
+
now: Date.now(),
|
|
463
|
+
...(lastReloadedAt === undefined ? {} : { reloadedAt: lastReloadedAt }),
|
|
464
|
+
});
|
|
465
|
+
updateBarVisible.value = decision === "offer";
|
|
466
|
+
if (decision !== "reload") return;
|
|
467
|
+
reloadNow();
|
|
468
|
+
}
|
|
469
|
+
|
|
470
|
+
function reloadNow(): void {
|
|
471
|
+
writeDeploymentReloadV1(reloadStore, Date.now());
|
|
472
|
+
/*
|
|
473
|
+
* `location.reload()` and nothing else. The Android shell runs this very
|
|
474
|
+
* bundle inside a WebView served from its own origin, so re-navigating to a
|
|
475
|
+
* URL this code built would leave that origin behind; reloading in place
|
|
476
|
+
* works the same way in both.
|
|
477
|
+
*/
|
|
478
|
+
window.location.reload();
|
|
479
|
+
}
|
|
480
|
+
|
|
481
|
+
watch(
|
|
482
|
+
[
|
|
483
|
+
() => state.value.deploymentStale,
|
|
484
|
+
() => state.value.runningRunId,
|
|
485
|
+
() => state.value.reloadHolds,
|
|
486
|
+
draft,
|
|
487
|
+
overlaySurface,
|
|
488
|
+
dictating,
|
|
489
|
+
],
|
|
490
|
+
() => followDeployment(),
|
|
491
|
+
{ immediate: true },
|
|
492
|
+
);
|
|
414
493
|
const voiceButtonLabel = computed(() => voiceButtonLabelV1(voiceState.value));
|
|
415
494
|
/**
|
|
416
495
|
* The wave button takes the slot only when the slot is otherwise idle: an
|
|
@@ -1574,6 +1653,23 @@ function handleComposerKeydown(event: KeyboardEvent): void {
|
|
|
1574
1653
|
</div>
|
|
1575
1654
|
</Transition>
|
|
1576
1655
|
|
|
1656
|
+
<Transition name="banner">
|
|
1657
|
+
<!--
|
|
1658
|
+
One bar at a time in this spot. A failed Turn is the more urgent
|
|
1659
|
+
thing to read, and newer code is still there once it is dealt with.
|
|
1660
|
+
-->
|
|
1661
|
+
<div
|
|
1662
|
+
v-if="updateBarVisible && !state.error && !state.activeRun"
|
|
1663
|
+
class="update-banner"
|
|
1664
|
+
role="status"
|
|
1665
|
+
>
|
|
1666
|
+
<span>{{ DEPLOYMENT_UPDATED_MESSAGE_V1 }}</span>
|
|
1667
|
+
<button type="button" @click="reloadNow()">
|
|
1668
|
+
{{ DEPLOYMENT_RELOAD_LABEL_V1 }}
|
|
1669
|
+
</button>
|
|
1670
|
+
</div>
|
|
1671
|
+
</Transition>
|
|
1672
|
+
|
|
1577
1673
|
<!--
|
|
1578
1674
|
No Bot, no composer. A disabled input under a made-up Bot name reads
|
|
1579
1675
|
as a broken Bot; the first-run pane above points at making one.
|
|
@@ -1618,11 +1714,24 @@ function handleComposerKeydown(event: KeyboardEvent): void {
|
|
|
1618
1714
|
v-if="appletChip"
|
|
1619
1715
|
type="button"
|
|
1620
1716
|
class="applet-chip"
|
|
1717
|
+
data-testid="applet-chip"
|
|
1621
1718
|
@click="toggleRightPanel"
|
|
1622
1719
|
>
|
|
1623
1720
|
<UiIcon name="applets" size="sm" />
|
|
1624
|
-
<span class="applet-chip-
|
|
1625
|
-
|
|
1721
|
+
<span class="applet-chip-text">
|
|
1722
|
+
<span class="applet-chip-name">Applet: {{ appletChip }}</span>
|
|
1723
|
+
<span v-if="appletChipStatus" class="applet-chip-status">
|
|
1724
|
+
<span
|
|
1725
|
+
v-if="appletChipStatus.working"
|
|
1726
|
+
class="applet-chip-dot"
|
|
1727
|
+
aria-hidden="true"
|
|
1728
|
+
/>
|
|
1729
|
+
{{ appletChipStatus.failure ?? appletChipStatus.label }}
|
|
1730
|
+
</span>
|
|
1731
|
+
</span>
|
|
1732
|
+
<span class="applet-chip-action">{{
|
|
1733
|
+
appletChipStatus ? "Watch" : "Open"
|
|
1734
|
+
}}</span>
|
|
1626
1735
|
</button>
|
|
1627
1736
|
<div class="composer-body">
|
|
1628
1737
|
<ul v-if="attachedSkills.length > 0" class="skill-chips">
|
|
@@ -0,0 +1,69 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* The building view, as the two surfaces that draw it are written.
|
|
3
|
+
*
|
|
4
|
+
* Bun has no single-file-component loader, so a `.vue` file is read as text
|
|
5
|
+
* here — the same thing `ComputerCard.test.ts` does. It proves the wiring the
|
|
6
|
+
* unit tests for the projection cannot: that the panel and the phone chip both
|
|
7
|
+
* read the one projection, and that neither still shows the fixed line it used
|
|
8
|
+
* to show for the whole of a build.
|
|
9
|
+
*/
|
|
10
|
+
import { describe, expect, test } from "bun:test";
|
|
11
|
+
import { parse } from "@vue/compiler-sfc";
|
|
12
|
+
|
|
13
|
+
const canvas = await Bun.file(
|
|
14
|
+
new URL("./AppletCanvas.vue", import.meta.url),
|
|
15
|
+
).text();
|
|
16
|
+
const app = await Bun.file(
|
|
17
|
+
new URL("./FrockBotApp.vue", import.meta.url),
|
|
18
|
+
).text();
|
|
19
|
+
const styles = await Bun.file(new URL("./styles.css", import.meta.url)).text();
|
|
20
|
+
|
|
21
|
+
describe("the Applet canvas while the Bot is still building", () => {
|
|
22
|
+
test("the file parses as a single-file component", () => {
|
|
23
|
+
expect(parse(canvas).errors).toEqual([]);
|
|
24
|
+
});
|
|
25
|
+
|
|
26
|
+
test("the panel draws the projection rather than a fixed line", () => {
|
|
27
|
+
expect(canvas).toContain('from "./applet-progress.js"');
|
|
28
|
+
expect(canvas).toContain('data-testid="applet-canvas-progress"');
|
|
29
|
+
expect(canvas).toContain("progress.label");
|
|
30
|
+
expect(canvas).toContain("progress.failure");
|
|
31
|
+
expect(canvas).toContain("progress.output");
|
|
32
|
+
// The line the panel used to show for minutes on end, whatever happened.
|
|
33
|
+
expect(canvas).not.toContain("Not published yet");
|
|
34
|
+
});
|
|
35
|
+
|
|
36
|
+
test("the progress block is announced and is gone once the Applet runs", () => {
|
|
37
|
+
expect(canvas).toContain('role="status"');
|
|
38
|
+
expect(canvas).toContain(
|
|
39
|
+
'v-if="applet && !loading && progress && beingBuilt"',
|
|
40
|
+
);
|
|
41
|
+
});
|
|
42
|
+
|
|
43
|
+
test("the build output is monospace and cannot grow without bound", () => {
|
|
44
|
+
expect(canvas).toContain("font-family: var(--frock-font-mono);");
|
|
45
|
+
expect(canvas).toContain("max-height: 180px;");
|
|
46
|
+
});
|
|
47
|
+
|
|
48
|
+
test("the working dot stops moving where motion is not wanted", () => {
|
|
49
|
+
expect(canvas).toContain("@media (prefers-reduced-motion: reduce)");
|
|
50
|
+
expect(canvas).toContain(
|
|
51
|
+
".applet-canvas-progress-dot {\n animation: none;",
|
|
52
|
+
);
|
|
53
|
+
});
|
|
54
|
+
});
|
|
55
|
+
|
|
56
|
+
describe("the phone's way in", () => {
|
|
57
|
+
test("the chip carries the same line and opens the same panel", () => {
|
|
58
|
+
expect(app).toContain('from "./applet-progress.js"');
|
|
59
|
+
expect(app).toContain("appletChipStatus");
|
|
60
|
+
// The one overlay: the right panel drawer, opened by the one toggle.
|
|
61
|
+
expect(app).toContain('class="applet-chip"');
|
|
62
|
+
expect(app).toContain('@click="toggleRightPanel"');
|
|
63
|
+
});
|
|
64
|
+
|
|
65
|
+
test("the chip has room for a second line", () => {
|
|
66
|
+
expect(styles).toContain(".applet-chip-status {");
|
|
67
|
+
expect(styles).toContain("min-height: var(--frock-control-md);");
|
|
68
|
+
});
|
|
69
|
+
});
|
|
@@ -0,0 +1,300 @@
|
|
|
1
|
+
import { describe, expect, test } from "bun:test";
|
|
2
|
+
import {
|
|
3
|
+
APPLET_PROGRESS_OUTPUT_LINES_V1,
|
|
4
|
+
appletCommandOutputV1,
|
|
5
|
+
appletIsBeingBuiltV1,
|
|
6
|
+
appletProgressV1,
|
|
7
|
+
} from "./applet-progress.js";
|
|
8
|
+
import type { WebToolActivity } from "../shared.js";
|
|
9
|
+
|
|
10
|
+
const draft = {
|
|
11
|
+
appletId: "u1abc.todo",
|
|
12
|
+
displayName: "Weekly Todos",
|
|
13
|
+
status: "draft" as const,
|
|
14
|
+
tools: [],
|
|
15
|
+
createdAt: "2026-09-05T00:00:00.000Z",
|
|
16
|
+
};
|
|
17
|
+
const published = {
|
|
18
|
+
...draft,
|
|
19
|
+
status: "published" as const,
|
|
20
|
+
currentGenerationId: "generation-2",
|
|
21
|
+
};
|
|
22
|
+
|
|
23
|
+
const sourceOf = (...paths: string[]) => ({
|
|
24
|
+
appletId: draft.appletId,
|
|
25
|
+
truncated: false,
|
|
26
|
+
files: paths.map((path) => ({
|
|
27
|
+
path,
|
|
28
|
+
text: "export default class {}",
|
|
29
|
+
generationId: "w-1",
|
|
30
|
+
changedAt: "2026-09-05T00:01:00.000Z",
|
|
31
|
+
})),
|
|
32
|
+
});
|
|
33
|
+
|
|
34
|
+
function tool(
|
|
35
|
+
name: string,
|
|
36
|
+
status: WebToolActivity["status"],
|
|
37
|
+
extra: Partial<WebToolActivity> = {},
|
|
38
|
+
): WebToolActivity {
|
|
39
|
+
return { id: `${name}-${status}`, name, status, ...extra };
|
|
40
|
+
}
|
|
41
|
+
|
|
42
|
+
describe("what the Bot is doing to the focused Applet", () => {
|
|
43
|
+
test("no Applet is no line at all", () => {
|
|
44
|
+
expect(appletProgressV1({})).toBeUndefined();
|
|
45
|
+
expect(appletProgressV1({ applet: null })).toBeUndefined();
|
|
46
|
+
});
|
|
47
|
+
|
|
48
|
+
test("a draft nothing has been said about yet reads as still being built", () => {
|
|
49
|
+
// The same words the Applets list uses, so the two surfaces agree.
|
|
50
|
+
expect(appletProgressV1({ applet: draft })).toMatchObject({
|
|
51
|
+
stage: "unknown",
|
|
52
|
+
label: "Still being built",
|
|
53
|
+
working: false,
|
|
54
|
+
});
|
|
55
|
+
});
|
|
56
|
+
|
|
57
|
+
test("the create call is the first thing worth saying", () => {
|
|
58
|
+
expect(
|
|
59
|
+
appletProgressV1({
|
|
60
|
+
applet: draft,
|
|
61
|
+
tools: [tool("applets/applet_create", "completed")],
|
|
62
|
+
})?.stage,
|
|
63
|
+
).toBe("created");
|
|
64
|
+
});
|
|
65
|
+
|
|
66
|
+
test("a native tool name counts the same as a namespaced one", () => {
|
|
67
|
+
expect(
|
|
68
|
+
appletProgressV1({
|
|
69
|
+
applet: draft,
|
|
70
|
+
tools: [tool("applet_create", "running")],
|
|
71
|
+
}),
|
|
72
|
+
).toMatchObject({ stage: "created", working: true });
|
|
73
|
+
});
|
|
74
|
+
|
|
75
|
+
test("source on the Workspace means the Bot has been writing", () => {
|
|
76
|
+
expect(
|
|
77
|
+
appletProgressV1({
|
|
78
|
+
applet: draft,
|
|
79
|
+
source: sourceOf("server.ts", "ui.tsx"),
|
|
80
|
+
tools: [tool("applets/applet_create", "completed")],
|
|
81
|
+
})?.label,
|
|
82
|
+
).toBe("Writing the code");
|
|
83
|
+
});
|
|
84
|
+
|
|
85
|
+
test("a check that passed moves the line on and shows what it printed", () => {
|
|
86
|
+
const progress = appletProgressV1({
|
|
87
|
+
applet: draft,
|
|
88
|
+
source: sourceOf("server.ts"),
|
|
89
|
+
tools: [
|
|
90
|
+
tool("computer_exec", "completed", {
|
|
91
|
+
text: "applet check: no problems found",
|
|
92
|
+
}),
|
|
93
|
+
],
|
|
94
|
+
});
|
|
95
|
+
expect(progress).toMatchObject({
|
|
96
|
+
stage: "checking",
|
|
97
|
+
label: "The code checks out",
|
|
98
|
+
done: true,
|
|
99
|
+
});
|
|
100
|
+
expect(progress?.output).toEqual(["applet check: no problems found"]);
|
|
101
|
+
expect(progress?.failure).toBeUndefined();
|
|
102
|
+
});
|
|
103
|
+
|
|
104
|
+
test("a check that found errors is a failure in plain words", () => {
|
|
105
|
+
const progress = appletProgressV1({
|
|
106
|
+
applet: draft,
|
|
107
|
+
tools: [
|
|
108
|
+
tool("computer_exec", "failed", {
|
|
109
|
+
text: "ui.tsx:3:1 Unexpected any\napplet check: 1 error(s)",
|
|
110
|
+
}),
|
|
111
|
+
],
|
|
112
|
+
});
|
|
113
|
+
expect(progress?.stage).toBe("checking");
|
|
114
|
+
expect(progress?.failure).toBe("The code has problems that need fixing.");
|
|
115
|
+
expect(progress?.output).toEqual([
|
|
116
|
+
"ui.tsx:3:1 Unexpected any",
|
|
117
|
+
"applet check: 1 error(s)",
|
|
118
|
+
]);
|
|
119
|
+
});
|
|
120
|
+
|
|
121
|
+
test("a build is recognised by the files it says it wrote", () => {
|
|
122
|
+
const progress = appletProgressV1({
|
|
123
|
+
applet: draft,
|
|
124
|
+
tools: [
|
|
125
|
+
tool("computer_exec", "completed", {
|
|
126
|
+
text: [
|
|
127
|
+
"dist/server.js 0a1b2c3d4e5f",
|
|
128
|
+
"dist/ui.html f5e4d3c2b1a0",
|
|
129
|
+
"dist/manifest.json 1 tool(s): add_todo",
|
|
130
|
+
].join("\n"),
|
|
131
|
+
}),
|
|
132
|
+
],
|
|
133
|
+
});
|
|
134
|
+
expect(progress).toMatchObject({
|
|
135
|
+
stage: "building",
|
|
136
|
+
label: "Built and ready to go live",
|
|
137
|
+
done: true,
|
|
138
|
+
});
|
|
139
|
+
expect(progress?.output).toHaveLength(3);
|
|
140
|
+
});
|
|
141
|
+
|
|
142
|
+
test("a shell command that is not the Applet CLI says nothing", () => {
|
|
143
|
+
expect(
|
|
144
|
+
appletCommandOutputV1("total 0\ndrwxr-xr-x 2 box box"),
|
|
145
|
+
).toBeUndefined();
|
|
146
|
+
expect(appletCommandOutputV1(undefined)).toBeUndefined();
|
|
147
|
+
expect(
|
|
148
|
+
appletProgressV1({
|
|
149
|
+
applet: draft,
|
|
150
|
+
source: sourceOf("server.ts"),
|
|
151
|
+
tools: [tool("computer_exec", "completed", { text: "hello" })],
|
|
152
|
+
})?.stage,
|
|
153
|
+
).toBe("writing");
|
|
154
|
+
});
|
|
155
|
+
|
|
156
|
+
test("a shell command still running is the Bot working, not a new step", () => {
|
|
157
|
+
const progress = appletProgressV1({
|
|
158
|
+
applet: draft,
|
|
159
|
+
source: sourceOf("server.ts"),
|
|
160
|
+
tools: [tool("computer_exec", "running")],
|
|
161
|
+
});
|
|
162
|
+
expect(progress).toMatchObject({ stage: "writing", working: true });
|
|
163
|
+
});
|
|
164
|
+
|
|
165
|
+
test("output is bounded, newest lines kept", () => {
|
|
166
|
+
const lines = Array.from(
|
|
167
|
+
{ length: 40 },
|
|
168
|
+
(_, index) => `ui.tsx:${index}:1 problem`,
|
|
169
|
+
);
|
|
170
|
+
const progress = appletProgressV1({
|
|
171
|
+
applet: draft,
|
|
172
|
+
tools: [
|
|
173
|
+
tool("computer_exec", "failed", {
|
|
174
|
+
text: [...lines, "applet check: 40 error(s)"].join("\n"),
|
|
175
|
+
}),
|
|
176
|
+
],
|
|
177
|
+
});
|
|
178
|
+
expect(progress?.output).toHaveLength(APPLET_PROGRESS_OUTPUT_LINES_V1);
|
|
179
|
+
expect(progress?.output?.at(-1)).toBe("applet check: 40 error(s)");
|
|
180
|
+
});
|
|
181
|
+
|
|
182
|
+
test("a very long line is cut rather than allowed to run off the panel", () => {
|
|
183
|
+
const progress = appletProgressV1({
|
|
184
|
+
applet: draft,
|
|
185
|
+
tools: [
|
|
186
|
+
tool("computer_exec", "failed", {
|
|
187
|
+
text: `applet check: 1 error(s)\n${"x".repeat(500)}`,
|
|
188
|
+
}),
|
|
189
|
+
],
|
|
190
|
+
});
|
|
191
|
+
expect(progress?.output?.at(-1)?.length).toBe(200);
|
|
192
|
+
expect(progress?.output?.at(-1)?.endsWith("…")).toBe(true);
|
|
193
|
+
});
|
|
194
|
+
|
|
195
|
+
test("a publish in flight is the last step, and clears an earlier complaint", () => {
|
|
196
|
+
const progress = appletProgressV1({
|
|
197
|
+
applet: draft,
|
|
198
|
+
tools: [
|
|
199
|
+
tool("computer_exec", "failed", { text: "applet check: 2 error(s)" }),
|
|
200
|
+
tool("applets/applet_publish", "running", {
|
|
201
|
+
input: { appletId: draft.appletId },
|
|
202
|
+
}),
|
|
203
|
+
],
|
|
204
|
+
});
|
|
205
|
+
expect(progress).toMatchObject({
|
|
206
|
+
stage: "publishing",
|
|
207
|
+
label: "Getting it ready to open",
|
|
208
|
+
working: true,
|
|
209
|
+
});
|
|
210
|
+
expect(progress?.failure).toBeUndefined();
|
|
211
|
+
});
|
|
212
|
+
|
|
213
|
+
test("a publish that was refused says why, in the words it was refused with", () => {
|
|
214
|
+
const progress = appletProgressV1({
|
|
215
|
+
applet: draft,
|
|
216
|
+
tools: [
|
|
217
|
+
tool("applets/applet_publish", "failed", {
|
|
218
|
+
input: { appletId: draft.appletId },
|
|
219
|
+
text: 'Publishing u1abc.todo failed: "add_todo" is already a tool of "Shopping"',
|
|
220
|
+
}),
|
|
221
|
+
],
|
|
222
|
+
});
|
|
223
|
+
expect(progress?.stage).toBe("publishing");
|
|
224
|
+
expect(progress?.failure).toBe(
|
|
225
|
+
'Publishing u1abc.todo failed: "add_todo" is already a tool of "Shopping"',
|
|
226
|
+
);
|
|
227
|
+
});
|
|
228
|
+
|
|
229
|
+
test("a publish of another Applet never moves this one's line", () => {
|
|
230
|
+
expect(
|
|
231
|
+
appletProgressV1({
|
|
232
|
+
applet: draft,
|
|
233
|
+
tools: [
|
|
234
|
+
tool("applets/applet_publish", "running", {
|
|
235
|
+
input: { appletId: "u1abc.other" },
|
|
236
|
+
}),
|
|
237
|
+
],
|
|
238
|
+
})?.stage,
|
|
239
|
+
).toBe("unknown");
|
|
240
|
+
});
|
|
241
|
+
|
|
242
|
+
test("a live Applet is ready, and the canvas stops showing progress", () => {
|
|
243
|
+
const progress = appletProgressV1({ applet: published });
|
|
244
|
+
expect(progress).toMatchObject({
|
|
245
|
+
stage: "published",
|
|
246
|
+
label: "Ready to use",
|
|
247
|
+
done: true,
|
|
248
|
+
});
|
|
249
|
+
expect(appletIsBeingBuiltV1(progress)).toBe(false);
|
|
250
|
+
expect(appletIsBeingBuiltV1(appletProgressV1({ applet: draft }))).toBe(
|
|
251
|
+
true,
|
|
252
|
+
);
|
|
253
|
+
expect(appletIsBeingBuiltV1(undefined)).toBe(false);
|
|
254
|
+
});
|
|
255
|
+
|
|
256
|
+
test("a step still going never claims it finished", () => {
|
|
257
|
+
const progress = appletProgressV1({
|
|
258
|
+
applet: draft,
|
|
259
|
+
tools: [
|
|
260
|
+
tool("computer_exec", "completed", {
|
|
261
|
+
text: "applet check: no problems found",
|
|
262
|
+
}),
|
|
263
|
+
tool("computer_exec", "running"),
|
|
264
|
+
],
|
|
265
|
+
});
|
|
266
|
+
expect(progress).toMatchObject({ done: false, working: true });
|
|
267
|
+
expect(progress?.label).toBe("Checking the code");
|
|
268
|
+
});
|
|
269
|
+
|
|
270
|
+
test("a Turn still running says so even when no Applet tool has been called", () => {
|
|
271
|
+
expect(appletProgressV1({ applet: draft, running: true })?.working).toBe(
|
|
272
|
+
true,
|
|
273
|
+
);
|
|
274
|
+
});
|
|
275
|
+
|
|
276
|
+
test("the outcome the Applet authority recorded is used when it has one", () => {
|
|
277
|
+
const progress = appletProgressV1({
|
|
278
|
+
applet: draft,
|
|
279
|
+
build: {
|
|
280
|
+
status: "failed",
|
|
281
|
+
command: "build",
|
|
282
|
+
summary: "dist/manifest.json is invalid",
|
|
283
|
+
diagnostics: ["hashes.server must be a sha-256 hex digest"],
|
|
284
|
+
},
|
|
285
|
+
});
|
|
286
|
+
expect(progress).toMatchObject({
|
|
287
|
+
stage: "building",
|
|
288
|
+
failure: "dist/manifest.json is invalid",
|
|
289
|
+
});
|
|
290
|
+
expect(progress?.output).toEqual([
|
|
291
|
+
"hashes.server must be a sha-256 hex digest",
|
|
292
|
+
]);
|
|
293
|
+
});
|
|
294
|
+
|
|
295
|
+
test("an unknown recorded outcome is not a step that happened", () => {
|
|
296
|
+
expect(
|
|
297
|
+
appletProgressV1({ applet: draft, build: { status: "unknown" } })?.stage,
|
|
298
|
+
).toBe("unknown");
|
|
299
|
+
});
|
|
300
|
+
});
|