@frockbot/plugin-computer 0.0.0 → 0.1.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 +25 -0
- package/package.json +54 -6
- package/src/agent.test.ts +271 -0
- package/src/agent.ts +1419 -0
- package/src/backend.test.ts +149 -0
- package/src/backend.ts +163 -0
- package/src/bot.test.ts +411 -0
- package/src/bot.ts +831 -0
- package/src/client/ComputerCard.test.ts +96 -0
- package/src/client/ComputerCard.vue +60 -0
- package/src/client/ComputerStrip.test.ts +54 -0
- package/src/client/ComputerStrip.vue +55 -0
- package/src/client/ComputerViewerOverlay.vue +252 -0
- package/src/client/application.test.ts +373 -0
- package/src/client/application.ts +340 -0
- package/src/client/cordis-client-shim.d.ts +16 -0
- package/src/client/dialog-focus.ts +13 -0
- package/src/client/index.ts +28 -0
- package/src/client/state-machine.test.ts +200 -0
- package/src/client/state-machine.ts +172 -0
- package/src/client/styles.css +594 -0
- package/src/client/viewer.ts +58 -0
- package/src/control-record.ts +57 -0
- package/src/doctor.test.ts +247 -0
- package/src/env.d.ts +12 -0
- package/src/index.ts +6 -0
- package/src/manifest.ts +3 -0
- package/src/process-records.test.ts +178 -0
- package/src/process-records.ts +278 -0
- package/src/process-store.ts +96 -0
- package/src/processes.test.ts +388 -0
- package/src/protocol.ts +405 -0
- package/src/roots.ts +6 -0
- package/src/screenshot.test.ts +253 -0
- package/src/shared-provider.test.ts +56 -0
- package/src/shared-provider.ts +121 -0
- package/src/shared.ts +54 -0
- package/src/sync.test.ts +255 -0
- package/src/workspace-fixture.ts +126 -0
- package/tsconfig.json +19 -0
- package/vite.config.ts +24 -0
- package/README.md +0 -3
|
@@ -0,0 +1,247 @@
|
|
|
1
|
+
// `computer_doctor` (parity row 27) and the GUI policy at the exec seam (row
|
|
2
|
+
// 33).
|
|
3
|
+
//
|
|
4
|
+
// What is asserted here is what the Package does with the Computer's answer:
|
|
5
|
+
// the report is filed through the Workspace so the Bot is recorded as its
|
|
6
|
+
// writer, the self-check runs once for the Computer this instance opened, and
|
|
7
|
+
// a `chromium …` command is refused with the sentence that names the tool to
|
|
8
|
+
// use instead.
|
|
9
|
+
import { describe, expect, test } from "bun:test";
|
|
10
|
+
import { SystemPromptRegistry } from "@frockbot/plugin-prompt";
|
|
11
|
+
import { ToolRegistry } from "@frockbot/plugin-tools";
|
|
12
|
+
import {
|
|
13
|
+
ComputerRegistry,
|
|
14
|
+
computerBotPathKeyV1,
|
|
15
|
+
type ComputerDoctorReportV1,
|
|
16
|
+
type ComputerHandle,
|
|
17
|
+
type ComputerProvider,
|
|
18
|
+
} from "@frockbot/computer-core";
|
|
19
|
+
import { createPluginHarness } from "@frockbot/plugin-testkit";
|
|
20
|
+
import { SessionStore } from "@frockbot/kernel-contracts";
|
|
21
|
+
import { createComputerAgentPlugin } from "./agent.js";
|
|
22
|
+
import { FakeWorkspace } from "./workspace-fixture.js";
|
|
23
|
+
|
|
24
|
+
const REPORT: ComputerDoctorReportV1 = {
|
|
25
|
+
schemaVersion: 2,
|
|
26
|
+
generation: 3,
|
|
27
|
+
capturedAt: "2026-09-01T00:00:00.000Z",
|
|
28
|
+
checks: [
|
|
29
|
+
{ name: "disk-root", status: "pass", detail: "12% full, 90 GiB free" },
|
|
30
|
+
{ name: "dns", status: "fail", detail: "api.fly.io does not resolve" },
|
|
31
|
+
],
|
|
32
|
+
// Parity row 34b: what the browser announced itself as, filed with the rest
|
|
33
|
+
// of the report so the measurement is readable while the Computer sleeps.
|
|
34
|
+
browserIdentity: {
|
|
35
|
+
userAgent: "Mozilla/5.0 … Chrome/141.0.0.0 Safari/537.36",
|
|
36
|
+
webdriver: false,
|
|
37
|
+
brands: ["Chromium/141"],
|
|
38
|
+
},
|
|
39
|
+
summary: "2 checks, 1 passed, 1 failed",
|
|
40
|
+
};
|
|
41
|
+
|
|
42
|
+
interface Fixture {
|
|
43
|
+
provider: ComputerProvider;
|
|
44
|
+
runs: number;
|
|
45
|
+
execs: string[];
|
|
46
|
+
}
|
|
47
|
+
|
|
48
|
+
function fixture(workspace: FakeWorkspace): Fixture {
|
|
49
|
+
const state: Fixture = {
|
|
50
|
+
runs: 0,
|
|
51
|
+
execs: [],
|
|
52
|
+
provider: {
|
|
53
|
+
id: "fixture",
|
|
54
|
+
open: (identity, tenant, assignment): Promise<ComputerHandle> =>
|
|
55
|
+
Promise.resolve({
|
|
56
|
+
assignment,
|
|
57
|
+
identity,
|
|
58
|
+
tenant,
|
|
59
|
+
workspace,
|
|
60
|
+
doctor: {
|
|
61
|
+
run: () => {
|
|
62
|
+
state.runs += 1;
|
|
63
|
+
return Promise.resolve(REPORT);
|
|
64
|
+
},
|
|
65
|
+
},
|
|
66
|
+
exec: {
|
|
67
|
+
execute: (request) => {
|
|
68
|
+
state.execs.push(request.args?.at(-1) ?? "");
|
|
69
|
+
return Promise.resolve({
|
|
70
|
+
exitCode: 0,
|
|
71
|
+
stdout: new TextEncoder().encode("ran"),
|
|
72
|
+
stderr: new Uint8Array(),
|
|
73
|
+
outputTruncated: false,
|
|
74
|
+
});
|
|
75
|
+
},
|
|
76
|
+
},
|
|
77
|
+
close: () => Promise.resolve(),
|
|
78
|
+
}),
|
|
79
|
+
},
|
|
80
|
+
};
|
|
81
|
+
return state;
|
|
82
|
+
}
|
|
83
|
+
|
|
84
|
+
async function mount(provider: ComputerProvider) {
|
|
85
|
+
const harness = await createPluginHarness([
|
|
86
|
+
ComputerRegistry,
|
|
87
|
+
ToolRegistry,
|
|
88
|
+
SystemPromptRegistry,
|
|
89
|
+
SessionStore,
|
|
90
|
+
]);
|
|
91
|
+
harness.root.computers.register(provider);
|
|
92
|
+
await harness.mount(
|
|
93
|
+
createComputerAgentPlugin({
|
|
94
|
+
userId: "user-1",
|
|
95
|
+
defaultProviderId: "fixture",
|
|
96
|
+
writer: { sessionId: "session-1", turnId: "run-9", runId: "run-9" },
|
|
97
|
+
}),
|
|
98
|
+
);
|
|
99
|
+
return harness;
|
|
100
|
+
}
|
|
101
|
+
|
|
102
|
+
function context(effectId = "tool:1:1:0") {
|
|
103
|
+
return {
|
|
104
|
+
botId: "bot-1",
|
|
105
|
+
agentId: "run-9",
|
|
106
|
+
compositionGenerationId: "bootstrap",
|
|
107
|
+
turnType: "chat" as const,
|
|
108
|
+
sessionId: "session-1",
|
|
109
|
+
effectId,
|
|
110
|
+
signal: new AbortController().signal,
|
|
111
|
+
};
|
|
112
|
+
}
|
|
113
|
+
|
|
114
|
+
async function call(
|
|
115
|
+
harness: Awaited<ReturnType<typeof createPluginHarness>>,
|
|
116
|
+
name: string,
|
|
117
|
+
input: unknown,
|
|
118
|
+
effectId?: string,
|
|
119
|
+
) {
|
|
120
|
+
const execution = context(effectId);
|
|
121
|
+
const prepared = await harness.root.tools.prepare(
|
|
122
|
+
{ id: crypto.randomUUID(), name, input },
|
|
123
|
+
execution,
|
|
124
|
+
);
|
|
125
|
+
if (prepared.kind !== "ready") return prepared.result;
|
|
126
|
+
return harness.root.tools.executePrepared(prepared, execution);
|
|
127
|
+
}
|
|
128
|
+
|
|
129
|
+
describe("computer_doctor", () => {
|
|
130
|
+
test("answers the report and files it with the Bot as its writer", async () => {
|
|
131
|
+
const workspace = new FakeWorkspace();
|
|
132
|
+
const state = fixture(workspace);
|
|
133
|
+
const harness = await mount(state.provider);
|
|
134
|
+
|
|
135
|
+
const result = await call(harness, "computer_doctor", {});
|
|
136
|
+
|
|
137
|
+
expect(result.isError).toBe(false);
|
|
138
|
+
const answer = JSON.parse(result.content) as Record<string, unknown>;
|
|
139
|
+
expect(answer).toMatchObject({
|
|
140
|
+
schemaVersion: 2,
|
|
141
|
+
generation: 3,
|
|
142
|
+
summary: "2 checks, 1 passed, 1 failed",
|
|
143
|
+
rootId: "doctor",
|
|
144
|
+
});
|
|
145
|
+
const botKey = computerBotPathKeyV1("bot-1");
|
|
146
|
+
expect(answer.path).toBe(`${botKey}/latest.json`);
|
|
147
|
+
expect(answer.checks).toHaveLength(2);
|
|
148
|
+
|
|
149
|
+
// Through the Workspace, never left on the Computer: a report that
|
|
150
|
+
// reached object storage by a shell write would arrive `unattributed`,
|
|
151
|
+
// which is data and never provenance.
|
|
152
|
+
const written = workspace.writes.find((write) =>
|
|
153
|
+
write.path.path.endsWith("latest.json"),
|
|
154
|
+
);
|
|
155
|
+
expect(written?.path.root).toMatchObject({
|
|
156
|
+
kind: "package-declared",
|
|
157
|
+
packageId: "computer",
|
|
158
|
+
rootId: "doctor",
|
|
159
|
+
});
|
|
160
|
+
expect(written?.writer).toEqual({
|
|
161
|
+
kind: "bot",
|
|
162
|
+
botId: "bot-1",
|
|
163
|
+
sessionId: "session-1",
|
|
164
|
+
turnId: "run-9",
|
|
165
|
+
runId: "run-9",
|
|
166
|
+
});
|
|
167
|
+
const filed = JSON.parse(
|
|
168
|
+
new TextDecoder().decode(written!.bytes),
|
|
169
|
+
) as ComputerDoctorReportV1;
|
|
170
|
+
expect(filed).toEqual(REPORT);
|
|
171
|
+
});
|
|
172
|
+
|
|
173
|
+
test("is offered on every turn type, so a Routine can diagnose too", async () => {
|
|
174
|
+
// A Routine that finds a Computer misbehaving has to be able to say what
|
|
175
|
+
// is wrong with it, and a read-only call is admissible wherever a Turn is.
|
|
176
|
+
const state = fixture(new FakeWorkspace());
|
|
177
|
+
const harness = await mount(state.provider);
|
|
178
|
+
|
|
179
|
+
for (const turnType of ["chat", "automation", "subagent"] as const) {
|
|
180
|
+
const names = harness.root.tools
|
|
181
|
+
.schemas({ turnType })
|
|
182
|
+
.map((schema) => schema.name);
|
|
183
|
+
expect(names, turnType).toContain("computer_doctor");
|
|
184
|
+
}
|
|
185
|
+
});
|
|
186
|
+
|
|
187
|
+
test("runs the self-check once for the Computer this instance opened", async () => {
|
|
188
|
+
// "box-doctor runs at startup and on demand". Startup is the first time
|
|
189
|
+
// this Bot reaches its Computer after this Package loaded — the first Turn
|
|
190
|
+
// after a cold provisioning. Repeating it costs a read-only exec and no
|
|
191
|
+
// effect, so the guard is against waste and never against damage.
|
|
192
|
+
const state = fixture(new FakeWorkspace());
|
|
193
|
+
const harness = await mount(state.provider);
|
|
194
|
+
|
|
195
|
+
await call(harness, "computer_exec", { command: "ls" }, "tool:1:1:0");
|
|
196
|
+
await call(harness, "computer_exec", { command: "pwd" }, "tool:1:1:1");
|
|
197
|
+
|
|
198
|
+
expect(state.runs).toBe(1);
|
|
199
|
+
});
|
|
200
|
+
});
|
|
201
|
+
|
|
202
|
+
describe("the GUI is never driven from the shell", () => {
|
|
203
|
+
test("refuses a command that reaches for the browser or the X tools", async () => {
|
|
204
|
+
const state = fixture(new FakeWorkspace());
|
|
205
|
+
const harness = await mount(state.provider);
|
|
206
|
+
|
|
207
|
+
for (const command of [
|
|
208
|
+
"chromium --headless https://example.com",
|
|
209
|
+
"cd /tmp && scrot shot.png",
|
|
210
|
+
"xdotool key Return",
|
|
211
|
+
"sudo x11vnc -display :1",
|
|
212
|
+
]) {
|
|
213
|
+
const result = await call(harness, "computer_exec", { command });
|
|
214
|
+
expect(result.isError, command).toBe(true);
|
|
215
|
+
expect(result.content).toContain("never driven from the shell");
|
|
216
|
+
expect(result.content).toContain("computer_browser");
|
|
217
|
+
expect(result.content).toContain("computer_screenshot");
|
|
218
|
+
expect(result.content).toContain("frockbot-chrome");
|
|
219
|
+
}
|
|
220
|
+
// Refused at the seam: none of them reached the Computer at all.
|
|
221
|
+
expect(state.execs).toEqual([]);
|
|
222
|
+
});
|
|
223
|
+
|
|
224
|
+
test("lets a command that merely mentions one through", async () => {
|
|
225
|
+
const state = fixture(new FakeWorkspace());
|
|
226
|
+
const harness = await mount(state.provider);
|
|
227
|
+
|
|
228
|
+
const result = await call(harness, "computer_exec", {
|
|
229
|
+
command: "grep -c chromium /home/box/.frockbot/bots/x/chromium.log",
|
|
230
|
+
});
|
|
231
|
+
|
|
232
|
+
expect(result.isError).toBe(false);
|
|
233
|
+
expect(state.execs).toHaveLength(1);
|
|
234
|
+
});
|
|
235
|
+
|
|
236
|
+
test("says where the shared scratch is, and that it is not durable", async () => {
|
|
237
|
+
const state = fixture(new FakeWorkspace());
|
|
238
|
+
const harness = await mount(state.provider);
|
|
239
|
+
const description = harness.root.tools
|
|
240
|
+
.schemas({ turnType: "chat" })
|
|
241
|
+
.find((schema) => schema.name === "computer_exec")?.description;
|
|
242
|
+
|
|
243
|
+
expect(description).toContain("/workspace");
|
|
244
|
+
expect(description).toContain("not durable");
|
|
245
|
+
expect(description).toContain("never driven from the shell");
|
|
246
|
+
});
|
|
247
|
+
});
|
package/src/env.d.ts
ADDED
package/src/index.ts
ADDED
|
@@ -0,0 +1,6 @@
|
|
|
1
|
+
export * from "./agent.js";
|
|
2
|
+
export * from "./process-records.js";
|
|
3
|
+
export * from "./process-store.js";
|
|
4
|
+
export { default as computerClientPlugin } from "./client/application.js";
|
|
5
|
+
export * from "./shared.js";
|
|
6
|
+
export { default as computerManifest } from "./manifest.js";
|
package/src/manifest.ts
ADDED
|
@@ -0,0 +1,178 @@
|
|
|
1
|
+
// The durable process record, its codec, and the one rule that decides what a
|
|
2
|
+
// process is after the Computer moved under it.
|
|
3
|
+
import { describe, expect, test } from "bun:test";
|
|
4
|
+
import {
|
|
5
|
+
COMPUTER_PROCESS_LIMIT_PER_BOT,
|
|
6
|
+
computerProcessKeyV1,
|
|
7
|
+
computerProcessStatusV1,
|
|
8
|
+
ComputerProcessDecodeError,
|
|
9
|
+
decodeComputerProcessRecordV1,
|
|
10
|
+
type ComputerProcessRecordV1,
|
|
11
|
+
} from "./process-records.js";
|
|
12
|
+
import {
|
|
13
|
+
ComputerProcessLimitError,
|
|
14
|
+
ComputerProcessStore,
|
|
15
|
+
type ComputerProcessStorageV1,
|
|
16
|
+
} from "./process-store.js";
|
|
17
|
+
|
|
18
|
+
const record: ComputerProcessRecordV1 = {
|
|
19
|
+
schemaVersion: 1,
|
|
20
|
+
processId: "p-tool-1-1-0",
|
|
21
|
+
botId: "bot-1",
|
|
22
|
+
sessionId: "session-1",
|
|
23
|
+
turnId: "run-9",
|
|
24
|
+
command: "npm run build",
|
|
25
|
+
cwd: "/workspaces/bot-1",
|
|
26
|
+
startedAt: "2026-08-31T00:00:00.000Z",
|
|
27
|
+
status: "running",
|
|
28
|
+
generation: 3,
|
|
29
|
+
effectId: "tool:1:1:0",
|
|
30
|
+
pid: 4321,
|
|
31
|
+
logPath: "/home/box/.frockbot/bots/bot-1/processes/p-tool-1-1-0/log",
|
|
32
|
+
};
|
|
33
|
+
|
|
34
|
+
/** Storage enough for the store: a map with a prefix listing. */
|
|
35
|
+
function storage(): ComputerProcessStorageV1 & { map: Map<string, unknown> } {
|
|
36
|
+
const map = new Map<string, unknown>();
|
|
37
|
+
return {
|
|
38
|
+
map,
|
|
39
|
+
get: <T>(key: string) => Promise.resolve(map.get(key) as T | undefined),
|
|
40
|
+
put: (key, value) => {
|
|
41
|
+
map.set(key, value);
|
|
42
|
+
return Promise.resolve();
|
|
43
|
+
},
|
|
44
|
+
delete: (key) => Promise.resolve(map.delete(key)),
|
|
45
|
+
list: <T>(options: { prefix: string; limit?: number }) => {
|
|
46
|
+
const held = new Map<string, T>();
|
|
47
|
+
for (const [key, value] of map) {
|
|
48
|
+
if (!key.startsWith(options.prefix)) continue;
|
|
49
|
+
if (options.limit !== undefined && held.size >= options.limit) break;
|
|
50
|
+
held.set(key, value as T);
|
|
51
|
+
}
|
|
52
|
+
return Promise.resolve(held);
|
|
53
|
+
},
|
|
54
|
+
};
|
|
55
|
+
}
|
|
56
|
+
|
|
57
|
+
describe("the Computer process record codec", () => {
|
|
58
|
+
test("round-trips an exact record and keys it by its id", () => {
|
|
59
|
+
expect(decodeComputerProcessRecordV1(record)).toEqual(record);
|
|
60
|
+
expect(computerProcessKeyV1(record.processId)).toBe(
|
|
61
|
+
"computer-process:p-tool-1-1-0",
|
|
62
|
+
);
|
|
63
|
+
});
|
|
64
|
+
|
|
65
|
+
test("refuses a record it does not exactly recognise", () => {
|
|
66
|
+
// No migrations: a record the codec refuses is a visible failure rather
|
|
67
|
+
// than something reshaped into a guess.
|
|
68
|
+
expect(() =>
|
|
69
|
+
decodeComputerProcessRecordV1({ ...record, schemaVersion: 2 }),
|
|
70
|
+
).toThrow(ComputerProcessDecodeError);
|
|
71
|
+
expect(() =>
|
|
72
|
+
decodeComputerProcessRecordV1({ ...record, extra: true }),
|
|
73
|
+
).toThrow(/unknown field "extra"/);
|
|
74
|
+
expect(() =>
|
|
75
|
+
decodeComputerProcessRecordV1({ ...record, status: "paused" }),
|
|
76
|
+
).toThrow(/status is invalid/);
|
|
77
|
+
expect(() =>
|
|
78
|
+
decodeComputerProcessRecordV1({ ...record, generation: -1 }),
|
|
79
|
+
).toThrow(/generation/);
|
|
80
|
+
const { pid: _pid, ...withoutPid } = record;
|
|
81
|
+
expect(decodeComputerProcessRecordV1(withoutPid)).toEqual(withoutPid);
|
|
82
|
+
});
|
|
83
|
+
});
|
|
84
|
+
|
|
85
|
+
describe("the process store", () => {
|
|
86
|
+
test("writes intent, reads it back, and bounds how many a Bot may hold", async () => {
|
|
87
|
+
const held = storage();
|
|
88
|
+
const store = new ComputerProcessStore(held);
|
|
89
|
+
|
|
90
|
+
await store.record(record);
|
|
91
|
+
expect(await store.read(record.processId)).toEqual(record);
|
|
92
|
+
expect(await store.read("p-absent")).toBeUndefined();
|
|
93
|
+
|
|
94
|
+
for (let index = 1; index < COMPUTER_PROCESS_LIMIT_PER_BOT; index += 1) {
|
|
95
|
+
await store.record({ ...record, processId: `p-${index}` });
|
|
96
|
+
}
|
|
97
|
+
await expect(
|
|
98
|
+
store.record({ ...record, processId: "p-one-too-many" }),
|
|
99
|
+
).rejects.toBeInstanceOf(ComputerProcessLimitError);
|
|
100
|
+
// Updating one that already exists is not a new record and is admitted.
|
|
101
|
+
await store.update({ ...record, status: "exited", exitCode: 0 });
|
|
102
|
+
expect((await store.read(record.processId))?.status).toBe("exited");
|
|
103
|
+
});
|
|
104
|
+
|
|
105
|
+
test("drops a stored value the codec refuses rather than failing the listing", async () => {
|
|
106
|
+
const held = storage();
|
|
107
|
+
const store = new ComputerProcessStore(held);
|
|
108
|
+
await store.record(record);
|
|
109
|
+
held.map.set("computer-process:p-broken", { schemaVersion: 9 });
|
|
110
|
+
|
|
111
|
+
expect((await store.list()).map((entry) => entry.processId)).toEqual([
|
|
112
|
+
record.processId,
|
|
113
|
+
]);
|
|
114
|
+
});
|
|
115
|
+
});
|
|
116
|
+
|
|
117
|
+
describe("the status a process holds", () => {
|
|
118
|
+
test("is running only when the same Computer still holds a live pid", () => {
|
|
119
|
+
expect(
|
|
120
|
+
computerProcessStatusV1({
|
|
121
|
+
recorded: record,
|
|
122
|
+
currentGeneration: 3,
|
|
123
|
+
observed: { alive: true },
|
|
124
|
+
}),
|
|
125
|
+
).toEqual({ status: "running" });
|
|
126
|
+
});
|
|
127
|
+
|
|
128
|
+
test("is exited with its code once the Computer recorded one", () => {
|
|
129
|
+
expect(
|
|
130
|
+
computerProcessStatusV1({
|
|
131
|
+
recorded: record,
|
|
132
|
+
currentGeneration: 3,
|
|
133
|
+
observed: { alive: false, exitCode: 2 },
|
|
134
|
+
}),
|
|
135
|
+
).toEqual({ status: "exited", exitCode: 2 });
|
|
136
|
+
});
|
|
137
|
+
|
|
138
|
+
test("is unknown, never running, once the generation moved", () => {
|
|
139
|
+
// "other processes are assumed dead after a cold pause": a rebuilt
|
|
140
|
+
// Computer is a different Computer, and a live pid on it belongs to
|
|
141
|
+
// whatever is running there now.
|
|
142
|
+
expect(
|
|
143
|
+
computerProcessStatusV1({
|
|
144
|
+
recorded: record,
|
|
145
|
+
currentGeneration: 4,
|
|
146
|
+
observed: { alive: true },
|
|
147
|
+
}),
|
|
148
|
+
).toEqual({ status: "unknown" });
|
|
149
|
+
// An exit file written before the rebuild is still evidence.
|
|
150
|
+
expect(
|
|
151
|
+
computerProcessStatusV1({
|
|
152
|
+
recorded: record,
|
|
153
|
+
currentGeneration: 4,
|
|
154
|
+
observed: { alive: false, exitCode: 0 },
|
|
155
|
+
}),
|
|
156
|
+
).toEqual({ status: "exited", exitCode: 0 });
|
|
157
|
+
});
|
|
158
|
+
|
|
159
|
+
test("is unknown when the pid is gone with no recorded exit", () => {
|
|
160
|
+
expect(
|
|
161
|
+
computerProcessStatusV1({
|
|
162
|
+
recorded: record,
|
|
163
|
+
currentGeneration: 3,
|
|
164
|
+
observed: { alive: false },
|
|
165
|
+
}),
|
|
166
|
+
).toEqual({ status: "unknown" });
|
|
167
|
+
});
|
|
168
|
+
|
|
169
|
+
test("keeps a settled outcome settled", () => {
|
|
170
|
+
expect(
|
|
171
|
+
computerProcessStatusV1({
|
|
172
|
+
recorded: { ...record, status: "exited", exitCode: 7 },
|
|
173
|
+
currentGeneration: 99,
|
|
174
|
+
observed: { alive: true },
|
|
175
|
+
}),
|
|
176
|
+
).toEqual({ status: "exited", exitCode: 7 });
|
|
177
|
+
});
|
|
178
|
+
});
|