@frockbot/plugin-machine-messages 0.0.0 → 0.1.1
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 +43 -0
- package/package.json +34 -6
- package/src/agent.test.ts +380 -0
- package/src/agent.ts +548 -0
- package/src/device.test.ts +321 -0
- package/src/device.ts +413 -0
- package/src/gate.test.ts +99 -0
- package/src/gate.ts +79 -0
- package/src/index.ts +3 -0
- package/src/manifest.ts +3 -0
- package/tsconfig.json +15 -0
- package/README.md +0 -3
package/frockbot.json
ADDED
|
@@ -0,0 +1,43 @@
|
|
|
1
|
+
{
|
|
2
|
+
"schemaVersion": 4,
|
|
3
|
+
"id": "machine-messages",
|
|
4
|
+
"displayName": "Messages on your Mac",
|
|
5
|
+
"version": "0.0.1",
|
|
6
|
+
"compatibility": {
|
|
7
|
+
"frockbot": ">=0.0.1"
|
|
8
|
+
},
|
|
9
|
+
"dependencies": {
|
|
10
|
+
"user-machine": ">=0.0.1"
|
|
11
|
+
},
|
|
12
|
+
"contributions": {
|
|
13
|
+
"runtime": {
|
|
14
|
+
"entry": "./agent"
|
|
15
|
+
}
|
|
16
|
+
},
|
|
17
|
+
"configuration": {
|
|
18
|
+
"settings": [
|
|
19
|
+
{
|
|
20
|
+
"id": "messages-enabled",
|
|
21
|
+
"schemaVersion": 1,
|
|
22
|
+
"scopes": ["user"],
|
|
23
|
+
"schema": {
|
|
24
|
+
"type": "boolean",
|
|
25
|
+
"title": "Messages.app tools",
|
|
26
|
+
"description": "Let a bot read and send iMessages through Messages.app on a registered Mac of yours. Off by default. Reading needs Full Disk Access and sending needs Automation rights, both granted on the Mac itself; sending always asks you to approve it first."
|
|
27
|
+
}
|
|
28
|
+
}
|
|
29
|
+
],
|
|
30
|
+
"connectionTypes": [],
|
|
31
|
+
"capabilities": [
|
|
32
|
+
{
|
|
33
|
+
"id": "machine-messages",
|
|
34
|
+
"kind": "tool",
|
|
35
|
+
"connectionTypes": [],
|
|
36
|
+
"admission": {
|
|
37
|
+
"turnTypes": ["chat"]
|
|
38
|
+
}
|
|
39
|
+
}
|
|
40
|
+
]
|
|
41
|
+
},
|
|
42
|
+
"permissions": []
|
|
43
|
+
}
|
package/package.json
CHANGED
|
@@ -1,14 +1,42 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@frockbot/plugin-machine-messages",
|
|
3
|
-
"version": "0.
|
|
4
|
-
"
|
|
5
|
-
"
|
|
3
|
+
"version": "0.1.1",
|
|
4
|
+
"private": false,
|
|
5
|
+
"type": "module",
|
|
6
|
+
"exports": {
|
|
7
|
+
".": "./src/index.ts",
|
|
8
|
+
"./agent": "./src/agent.ts",
|
|
9
|
+
"./device": "./src/device.ts",
|
|
10
|
+
"./gate": "./src/gate.ts",
|
|
11
|
+
"./manifest": "./src/manifest.ts",
|
|
12
|
+
"./frockbot.json": "./frockbot.json",
|
|
13
|
+
"./package.json": "./package.json"
|
|
14
|
+
},
|
|
15
|
+
"frockbot": {
|
|
16
|
+
"manifest": "./frockbot.json"
|
|
17
|
+
},
|
|
18
|
+
"scripts": {
|
|
19
|
+
"test": "bun test src",
|
|
20
|
+
"typecheck": "tsc --noEmit -p tsconfig.json"
|
|
21
|
+
},
|
|
22
|
+
"dependencies": {
|
|
23
|
+
"@frockbot/kernel-agent-loop": "0.1.1",
|
|
24
|
+
"@frockbot/kernel-contracts": "0.1.1",
|
|
25
|
+
"@frockbot/machine-protocol": "0.1.1",
|
|
26
|
+
"@frockbot/plugin-user-machine": "0.1.1",
|
|
27
|
+
"cordis": "4.0.0-rc.8"
|
|
28
|
+
},
|
|
29
|
+
"devDependencies": {
|
|
30
|
+
"@frockbot/plugin-tools": "0.1.1",
|
|
31
|
+
"@types/bun": "1.4.0",
|
|
32
|
+
"typescript": "^7.0.2"
|
|
33
|
+
},
|
|
34
|
+
"publishConfig": {
|
|
35
|
+
"access": "public"
|
|
36
|
+
},
|
|
6
37
|
"repository": {
|
|
7
38
|
"type": "git",
|
|
8
39
|
"url": "git+https://github.com/timoconnellaus/frockbot.git",
|
|
9
40
|
"directory": "packages/plugin-machine-messages"
|
|
10
|
-
},
|
|
11
|
-
"publishConfig": {
|
|
12
|
-
"access": "public"
|
|
13
41
|
}
|
|
14
42
|
}
|
|
@@ -0,0 +1,380 @@
|
|
|
1
|
+
// The seven Messages tools: what they refuse, what they queue, and the one of
|
|
2
|
+
// them that asks a person first.
|
|
3
|
+
import { describe, expect, test } from "bun:test";
|
|
4
|
+
import {
|
|
5
|
+
SessionStore,
|
|
6
|
+
type Session,
|
|
7
|
+
type ToolCall,
|
|
8
|
+
type ToolExecutionContext,
|
|
9
|
+
type TurnTypeV1,
|
|
10
|
+
} from "@frockbot/kernel-contracts";
|
|
11
|
+
import { ToolRegistry } from "@frockbot/plugin-tools";
|
|
12
|
+
import { Context } from "cordis";
|
|
13
|
+
import type {
|
|
14
|
+
MachineCommandV1,
|
|
15
|
+
MachineListEntryV1,
|
|
16
|
+
MachineListViewV1,
|
|
17
|
+
MachineMessagesPermissionsV1,
|
|
18
|
+
} from "@frockbot/machine-protocol";
|
|
19
|
+
import type { MachineRuntimeHostV1 } from "@frockbot/plugin-user-machine/agent";
|
|
20
|
+
import {
|
|
21
|
+
decodeMachineIntentRecordV1,
|
|
22
|
+
machineIntentKeyV1,
|
|
23
|
+
} from "@frockbot/plugin-user-machine/intent";
|
|
24
|
+
import type { MachineTargetViewV1 } from "@frockbot/plugin-user-machine/target";
|
|
25
|
+
import {
|
|
26
|
+
MACHINE_MESSAGES_TOOL_NAMES_V1,
|
|
27
|
+
MESSAGES_ACTIVITY_TOOL_V1,
|
|
28
|
+
MESSAGES_CHECK_PERMISSIONS_TOOL_V1,
|
|
29
|
+
MESSAGES_FIND_CHATS_TOOL_V1,
|
|
30
|
+
MESSAGES_SEND_TOOL_V1,
|
|
31
|
+
createMachineMessagesRuntimePlugin,
|
|
32
|
+
machineMessagesAdmissionCeilingV1,
|
|
33
|
+
machineMessagesTurnOfV1,
|
|
34
|
+
type MachineMessagesRuntimeHostV1,
|
|
35
|
+
} from "./agent.js";
|
|
36
|
+
|
|
37
|
+
const SESSION_ID = "user-1:bot-1";
|
|
38
|
+
const NOW = "2026-09-01T00:00:00.000Z";
|
|
39
|
+
const EFFECT_ID = "tool:4:2:0";
|
|
40
|
+
const COMMAND_ID = "tool.4.2.0";
|
|
41
|
+
|
|
42
|
+
const granted: MachineMessagesPermissionsV1 = {
|
|
43
|
+
schemaVersion: 1,
|
|
44
|
+
fullDiskAccess: true,
|
|
45
|
+
automation: true,
|
|
46
|
+
checkedAt: NOW,
|
|
47
|
+
};
|
|
48
|
+
|
|
49
|
+
function entry(
|
|
50
|
+
overrides: Partial<MachineListEntryV1> = {},
|
|
51
|
+
): MachineListEntryV1 {
|
|
52
|
+
return {
|
|
53
|
+
machineId: "mac-1",
|
|
54
|
+
label: "Tims-M5-MacBook-Pro.local",
|
|
55
|
+
platform: "macos",
|
|
56
|
+
capabilities: ["exec", "files", "messages"],
|
|
57
|
+
connected: true,
|
|
58
|
+
lastSeenAt: NOW,
|
|
59
|
+
registeredAt: NOW,
|
|
60
|
+
messagesPermissions: granted,
|
|
61
|
+
...overrides,
|
|
62
|
+
};
|
|
63
|
+
}
|
|
64
|
+
|
|
65
|
+
interface Harness {
|
|
66
|
+
root: Context;
|
|
67
|
+
session: Session;
|
|
68
|
+
storage: Map<string, unknown>;
|
|
69
|
+
dispatched: MachineCommandV1[];
|
|
70
|
+
dispose(): Promise<void>;
|
|
71
|
+
}
|
|
72
|
+
|
|
73
|
+
async function mount(
|
|
74
|
+
options: { entry?: MachineListEntryV1 | undefined; refuse?: string } = {},
|
|
75
|
+
): Promise<Harness> {
|
|
76
|
+
const root = new Context();
|
|
77
|
+
await root.plugin(SessionStore);
|
|
78
|
+
await root.plugin(ToolRegistry);
|
|
79
|
+
const session = root.sessions.create(SESSION_ID);
|
|
80
|
+
session.appendBatch([
|
|
81
|
+
{ type: "turn/start", turn: 4 },
|
|
82
|
+
{ type: "step/start", turn: 4, step: 2 },
|
|
83
|
+
]);
|
|
84
|
+
const store = new Map<string, unknown>();
|
|
85
|
+
const resolved = "entry" in options ? options.entry : entry();
|
|
86
|
+
const target: MachineTargetViewV1 = {
|
|
87
|
+
schemaVersion: 1,
|
|
88
|
+
machineId: "mac-1",
|
|
89
|
+
...(resolved === undefined ? {} : { entry: resolved }),
|
|
90
|
+
queuedCommands: 0,
|
|
91
|
+
commandsToday: 0,
|
|
92
|
+
serverTime: NOW,
|
|
93
|
+
};
|
|
94
|
+
const view: MachineListViewV1 = {
|
|
95
|
+
schemaVersion: 1,
|
|
96
|
+
machines: resolved ? [resolved] : [],
|
|
97
|
+
serverTime: NOW,
|
|
98
|
+
};
|
|
99
|
+
const machines: MachineRuntimeHostV1 & {
|
|
100
|
+
writer: { sessionId: string; turnId: string; runId: string };
|
|
101
|
+
} = {
|
|
102
|
+
botId: "bot-1",
|
|
103
|
+
writer: { sessionId: SESSION_ID, turnId: "turn-4", runId: "run-1" },
|
|
104
|
+
storage: {
|
|
105
|
+
get: async <T>(key: string) => store.get(key) as T | undefined,
|
|
106
|
+
put: async (key: string, value: unknown) => {
|
|
107
|
+
store.set(key, value);
|
|
108
|
+
},
|
|
109
|
+
},
|
|
110
|
+
list: async () => view,
|
|
111
|
+
describeTarget: async () => target,
|
|
112
|
+
readResult: async () => undefined,
|
|
113
|
+
};
|
|
114
|
+
const dispatched: MachineCommandV1[] = [];
|
|
115
|
+
const host: MachineMessagesRuntimeHostV1 = {
|
|
116
|
+
machines,
|
|
117
|
+
now: () => NOW,
|
|
118
|
+
dispatch: async (command) => {
|
|
119
|
+
if (options.refuse) {
|
|
120
|
+
return { status: "refused", reason: options.refuse };
|
|
121
|
+
}
|
|
122
|
+
dispatched.push(command);
|
|
123
|
+
return { status: "queued", command };
|
|
124
|
+
},
|
|
125
|
+
};
|
|
126
|
+
await root.plugin(createMachineMessagesRuntimePlugin(host));
|
|
127
|
+
return {
|
|
128
|
+
root,
|
|
129
|
+
session,
|
|
130
|
+
storage: store,
|
|
131
|
+
dispatched,
|
|
132
|
+
dispose: () => root.fiber.dispose(),
|
|
133
|
+
};
|
|
134
|
+
}
|
|
135
|
+
|
|
136
|
+
function contextFor(turnType: TurnTypeV1): ToolExecutionContext {
|
|
137
|
+
return {
|
|
138
|
+
botId: "bot-1",
|
|
139
|
+
agentId: "bot-1",
|
|
140
|
+
sessionId: SESSION_ID,
|
|
141
|
+
compositionGenerationId: "2026-09-01T00:00:00.000Z:0123456789abcdef",
|
|
142
|
+
turnType,
|
|
143
|
+
effectId: EFFECT_ID,
|
|
144
|
+
signal: new AbortController().signal,
|
|
145
|
+
};
|
|
146
|
+
}
|
|
147
|
+
|
|
148
|
+
async function invoke(
|
|
149
|
+
harness: Harness,
|
|
150
|
+
name: string,
|
|
151
|
+
input: unknown,
|
|
152
|
+
turnType: TurnTypeV1 = "chat",
|
|
153
|
+
) {
|
|
154
|
+
const call: ToolCall = { id: "call-1", name, input };
|
|
155
|
+
const context = contextFor(turnType);
|
|
156
|
+
const preparation = await harness.root.tools.prepare(call, context);
|
|
157
|
+
if (preparation.kind === "denied") return preparation.result;
|
|
158
|
+
return harness.root.tools.executePrepared(preparation, context);
|
|
159
|
+
}
|
|
160
|
+
|
|
161
|
+
describe("admission", () => {
|
|
162
|
+
test("all seven tools are chat-only, and the ceiling is the manifest's", async () => {
|
|
163
|
+
const harness = await mount();
|
|
164
|
+
try {
|
|
165
|
+
const names = (turnType: TurnTypeV1) =>
|
|
166
|
+
harness.root.tools.schemas({ turnType }).map((tool) => tool.name);
|
|
167
|
+
for (const tool of MACHINE_MESSAGES_TOOL_NAMES_V1) {
|
|
168
|
+
expect(names("chat")).toContain(tool);
|
|
169
|
+
for (const turnType of ["automation", "subagent"] as const) {
|
|
170
|
+
expect(names(turnType)).not.toContain(tool);
|
|
171
|
+
}
|
|
172
|
+
}
|
|
173
|
+
expect(machineMessagesAdmissionCeilingV1()).toEqual(["chat"]);
|
|
174
|
+
} finally {
|
|
175
|
+
await harness.dispose();
|
|
176
|
+
}
|
|
177
|
+
});
|
|
178
|
+
|
|
179
|
+
test("the Turn an effect belongs to is read off its own id", () => {
|
|
180
|
+
expect(machineMessagesTurnOfV1("tool:4:2:0")).toBe(4);
|
|
181
|
+
expect(machineMessagesTurnOfV1("nonsense")).toBe(0);
|
|
182
|
+
});
|
|
183
|
+
});
|
|
184
|
+
|
|
185
|
+
describe("the permission gate", () => {
|
|
186
|
+
test("a machine that never reported permissions refuses every read but the check", async () => {
|
|
187
|
+
const harness = await mount({
|
|
188
|
+
entry: entry({ messagesPermissions: undefined }),
|
|
189
|
+
});
|
|
190
|
+
try {
|
|
191
|
+
const refused = await invoke(harness, MESSAGES_ACTIVITY_TOOL_V1, {
|
|
192
|
+
machineId: "mac-1",
|
|
193
|
+
});
|
|
194
|
+
expect(refused.isError).toBe(true);
|
|
195
|
+
expect(String(refused.content)).toContain("Refused:");
|
|
196
|
+
expect(String(refused.content)).toContain(
|
|
197
|
+
MESSAGES_CHECK_PERMISSIONS_TOOL_V1,
|
|
198
|
+
);
|
|
199
|
+
// Nothing was queued: the refusal happens before the effect, not after.
|
|
200
|
+
expect(harness.dispatched).toEqual([]);
|
|
201
|
+
// The check itself is how a machine stops being unknown, so it runs.
|
|
202
|
+
const checked = await invoke(
|
|
203
|
+
harness,
|
|
204
|
+
MESSAGES_CHECK_PERMISSIONS_TOOL_V1,
|
|
205
|
+
{ machineId: "mac-1" },
|
|
206
|
+
);
|
|
207
|
+
expect(checked.isError).toBe(false);
|
|
208
|
+
expect(harness.dispatched).toHaveLength(1);
|
|
209
|
+
} finally {
|
|
210
|
+
await harness.dispose();
|
|
211
|
+
}
|
|
212
|
+
});
|
|
213
|
+
|
|
214
|
+
test("denied Full Disk Access refuses with the remediation, and queues nothing", async () => {
|
|
215
|
+
const harness = await mount({
|
|
216
|
+
entry: entry({
|
|
217
|
+
messagesPermissions: { ...granted, fullDiskAccess: false },
|
|
218
|
+
}),
|
|
219
|
+
});
|
|
220
|
+
try {
|
|
221
|
+
const result = await invoke(harness, MESSAGES_FIND_CHATS_TOOL_V1, {
|
|
222
|
+
machineId: "mac-1",
|
|
223
|
+
});
|
|
224
|
+
expect(result.isError).toBe(true);
|
|
225
|
+
expect(String(result.content)).toContain("Full Disk Access");
|
|
226
|
+
expect(String(result.content)).toContain("System Settings");
|
|
227
|
+
expect(harness.dispatched).toEqual([]);
|
|
228
|
+
} finally {
|
|
229
|
+
await harness.dispose();
|
|
230
|
+
}
|
|
231
|
+
});
|
|
232
|
+
|
|
233
|
+
test("a read runs without Automation; only the send needs it", async () => {
|
|
234
|
+
const harness = await mount({
|
|
235
|
+
entry: entry({ messagesPermissions: { ...granted, automation: false } }),
|
|
236
|
+
});
|
|
237
|
+
try {
|
|
238
|
+
expect(
|
|
239
|
+
(
|
|
240
|
+
await invoke(harness, MESSAGES_ACTIVITY_TOOL_V1, {
|
|
241
|
+
machineId: "mac-1",
|
|
242
|
+
})
|
|
243
|
+
).isError,
|
|
244
|
+
).toBe(false);
|
|
245
|
+
const send = await invoke(harness, MESSAGES_SEND_TOOL_V1, {
|
|
246
|
+
machineId: "mac-1",
|
|
247
|
+
to: "+61400000000",
|
|
248
|
+
text: "hi",
|
|
249
|
+
});
|
|
250
|
+
expect(send.isError).toBe(true);
|
|
251
|
+
expect(String(send.content)).toContain("Automation");
|
|
252
|
+
} finally {
|
|
253
|
+
await harness.dispose();
|
|
254
|
+
}
|
|
255
|
+
});
|
|
256
|
+
});
|
|
257
|
+
|
|
258
|
+
describe("the machine gate, at the tool boundary", () => {
|
|
259
|
+
test("unknown, offline and capability-less machines refuse visibly", async () => {
|
|
260
|
+
for (const [options, expected] of [
|
|
261
|
+
[{ entry: undefined }, "no machine"],
|
|
262
|
+
[{ entry: entry({ connected: false }) }, "not connected"],
|
|
263
|
+
[
|
|
264
|
+
{ entry: entry({ capabilities: ["exec", "files"] }) },
|
|
265
|
+
"does not report the messages capability",
|
|
266
|
+
],
|
|
267
|
+
[{ entry: entry({ revokedAt: NOW }) }, "was revoked"],
|
|
268
|
+
] as const) {
|
|
269
|
+
const harness = await mount(options as { entry?: MachineListEntryV1 });
|
|
270
|
+
try {
|
|
271
|
+
const result = await invoke(harness, MESSAGES_ACTIVITY_TOOL_V1, {
|
|
272
|
+
machineId: "mac-1",
|
|
273
|
+
});
|
|
274
|
+
expect(result.isError).toBe(true);
|
|
275
|
+
expect(String(result.content)).toContain(expected);
|
|
276
|
+
expect(harness.dispatched).toEqual([]);
|
|
277
|
+
} finally {
|
|
278
|
+
await harness.dispose();
|
|
279
|
+
}
|
|
280
|
+
}
|
|
281
|
+
});
|
|
282
|
+
});
|
|
283
|
+
|
|
284
|
+
describe("a read", () => {
|
|
285
|
+
test("records intent, queues one messages command, and ends no Turn", async () => {
|
|
286
|
+
const harness = await mount();
|
|
287
|
+
try {
|
|
288
|
+
const result = await invoke(harness, MESSAGES_FIND_CHATS_TOOL_V1, {
|
|
289
|
+
machineId: "mac-1",
|
|
290
|
+
query: "mum",
|
|
291
|
+
limit: 5,
|
|
292
|
+
});
|
|
293
|
+
expect(result.isError).toBe(false);
|
|
294
|
+
// A read is exempt from the card, so it must not end the Turn: the model
|
|
295
|
+
// says what it asked for and carries on.
|
|
296
|
+
expect(result.endsTurn).toBeUndefined();
|
|
297
|
+
expect(String(result.content)).toContain(COMMAND_ID);
|
|
298
|
+
expect(harness.dispatched).toHaveLength(1);
|
|
299
|
+
const command = harness.dispatched[0]!;
|
|
300
|
+
expect(command.commandId).toBe(COMMAND_ID);
|
|
301
|
+
expect(command.op).toEqual({
|
|
302
|
+
kind: "messages",
|
|
303
|
+
call: { kind: "find-chats", query: "mum", limit: 5 },
|
|
304
|
+
});
|
|
305
|
+
// Intent before the effect, and it is the record `machine_command_check`
|
|
306
|
+
// reads back: dispatched, with no decision, because nobody was asked.
|
|
307
|
+
const intent = decodeMachineIntentRecordV1(
|
|
308
|
+
harness.storage.get(machineIntentKeyV1(COMMAND_ID)),
|
|
309
|
+
);
|
|
310
|
+
expect(intent.decision).toBeUndefined();
|
|
311
|
+
expect(intent.outcome).toBe("dispatched");
|
|
312
|
+
expect(intent.turn).toBe(4);
|
|
313
|
+
// Nothing was put on the session log: a read shows the user no card.
|
|
314
|
+
expect(
|
|
315
|
+
harness.session.events.filter((event) => event.type === "send/to-user"),
|
|
316
|
+
).toEqual([]);
|
|
317
|
+
} finally {
|
|
318
|
+
await harness.dispose();
|
|
319
|
+
}
|
|
320
|
+
});
|
|
321
|
+
|
|
322
|
+
test("a queue that refuses is a visible refusal, and the intent says so", async () => {
|
|
323
|
+
const harness = await mount({ refuse: "Refused: over quota." });
|
|
324
|
+
try {
|
|
325
|
+
const result = await invoke(harness, MESSAGES_ACTIVITY_TOOL_V1, {
|
|
326
|
+
machineId: "mac-1",
|
|
327
|
+
});
|
|
328
|
+
expect(result.isError).toBe(true);
|
|
329
|
+
expect(String(result.content)).toContain("over quota");
|
|
330
|
+
expect(
|
|
331
|
+
decodeMachineIntentRecordV1(
|
|
332
|
+
harness.storage.get(machineIntentKeyV1(COMMAND_ID)),
|
|
333
|
+
).outcome,
|
|
334
|
+
).toBe("refused");
|
|
335
|
+
} finally {
|
|
336
|
+
await harness.dispose();
|
|
337
|
+
}
|
|
338
|
+
});
|
|
339
|
+
});
|
|
340
|
+
|
|
341
|
+
describe("the send", () => {
|
|
342
|
+
test("asks for approval on the landed path, ends the Turn, and queues nothing", async () => {
|
|
343
|
+
const harness = await mount();
|
|
344
|
+
try {
|
|
345
|
+
const result = await invoke(harness, MESSAGES_SEND_TOOL_V1, {
|
|
346
|
+
machineId: "mac-1",
|
|
347
|
+
to: "+61400000000",
|
|
348
|
+
text: 'tell them "yes"',
|
|
349
|
+
});
|
|
350
|
+
expect(result.isError).toBe(false);
|
|
351
|
+
expect(result.endsTurn).toBe(true);
|
|
352
|
+
// Nothing reached the queue: the settlement dispatches, not the tool.
|
|
353
|
+
expect(harness.dispatched).toEqual([]);
|
|
354
|
+
const sends = harness.session.events.filter(
|
|
355
|
+
(event) => event.type === "send/to-user",
|
|
356
|
+
);
|
|
357
|
+
expect(sends).toHaveLength(1);
|
|
358
|
+
const payload = (sends[0] as { payload: Record<string, unknown> })
|
|
359
|
+
.payload;
|
|
360
|
+
expect(payload.type).toBe("approval");
|
|
361
|
+
expect(payload.approvalId).toBe(COMMAND_ID);
|
|
362
|
+
expect(payload.risk).toBe("high");
|
|
363
|
+
// The card carries the exact text: approving a message you have not read
|
|
364
|
+
// is not approving anything.
|
|
365
|
+
expect(String(payload.action)).toContain('tell them "yes"');
|
|
366
|
+
expect(String(payload.action)).toContain("+61400000000");
|
|
367
|
+
const intent = decodeMachineIntentRecordV1(
|
|
368
|
+
harness.storage.get(machineIntentKeyV1(COMMAND_ID)),
|
|
369
|
+
);
|
|
370
|
+
expect(intent.decision).toBeUndefined();
|
|
371
|
+
expect(intent.outcome).toBeUndefined();
|
|
372
|
+
expect(intent.op).toEqual({
|
|
373
|
+
kind: "messages",
|
|
374
|
+
call: { kind: "send", to: "+61400000000", text: 'tell them "yes"' },
|
|
375
|
+
});
|
|
376
|
+
} finally {
|
|
377
|
+
await harness.dispose();
|
|
378
|
+
}
|
|
379
|
+
});
|
|
380
|
+
});
|