a2a-exposed 0.8.17 → 0.8.18
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/lib/commands.mjs +4 -1
- package/lib/main.mjs +9 -6
- package/package.json +1 -1
- package/worker/migrations/0012_conversation_peer_owner.sql +13 -0
- package/worker/src/index.ts +146 -40
- package/worker/src/mcp.ts +2 -1
- package/worker/src/migrations.ts +2 -0
- package/worker/src/owner-rpc.ts +11 -5
- package/worker/src/version.ts +1 -1
- package/worker/src/wake.ts +4 -2
- package/worker/test/context-peer.test.ts +198 -79
- package/worker/test/facade.test.ts +6 -2
- package/worker/test/list-tasks.test.ts +120 -0
- package/worker/test/migrations.test.ts +18 -6
- package/worker/test/owner-rpc.test.ts +4 -0
- package/worker/test/push-config.test.ts +161 -0
- package/worker/test/wake.test.ts +2 -2
|
@@ -1,4 +1,5 @@
|
|
|
1
|
-
// A
|
|
1
|
+
// A context id is one conversation per peer. A client-supplied id this peer does not already hold
|
|
2
|
+
// starts that peer's own conversation, whether or not another peer already uses the same string.
|
|
2
3
|
// Run: npm test
|
|
3
4
|
import { test } from "node:test";
|
|
4
5
|
import assert from "node:assert/strict";
|
|
@@ -6,7 +7,6 @@ import worker from "../src/index.ts";
|
|
|
6
7
|
import { d1 } from "./d1.ts";
|
|
7
8
|
|
|
8
9
|
const BASE = "https://agent.example.com";
|
|
9
|
-
const UNKNOWN = "Invalid params: unknown contextId (omit contextId to start a new conversation, then use the one returned)";
|
|
10
10
|
|
|
11
11
|
function setup() {
|
|
12
12
|
const DB = d1(new URL("../migrations/", import.meta.url));
|
|
@@ -32,91 +32,191 @@ function setup() {
|
|
|
32
32
|
jsonrpc: "2.0", id: 1, method,
|
|
33
33
|
params: { message: { kind: "message", role: "user", messageId: crypto.randomUUID(), ...(contextId ? { contextId } : {}), parts: [{ kind: "text", text }] } },
|
|
34
34
|
}, `Bearer ${token}`);
|
|
35
|
-
return { DB, owner, send, restore: () => { globalThis.fetch = origFetch; } };
|
|
35
|
+
return { DB, owner, send, call, restore: () => { globalThis.fetch = origFetch; } };
|
|
36
36
|
}
|
|
37
37
|
|
|
38
|
-
|
|
38
|
+
/** The 0.3 task, or the 1.0 `{ task }` wrapper, minus fields that are new on every call. */
|
|
39
|
+
function opened(data: any) {
|
|
40
|
+
const task = data?.result?.kind === "task" || data?.result?.status ? data.result : data?.result?.task;
|
|
41
|
+
const history = Array.isArray(task?.history) ? task.history : [];
|
|
42
|
+
return {
|
|
43
|
+
contextId: task?.contextId,
|
|
44
|
+
state: task?.status?.state,
|
|
45
|
+
texts: history.map((m: { parts?: { text?: string }[] }) => (m.parts || []).map((p) => p.text || "").join("\n")),
|
|
46
|
+
roles: history.map((m: { role?: string }) => m.role),
|
|
47
|
+
};
|
|
48
|
+
}
|
|
49
|
+
|
|
50
|
+
function hides(body: string, secret: string) {
|
|
51
|
+
assert.equal(body.includes(secret), false);
|
|
52
|
+
}
|
|
53
|
+
|
|
54
|
+
test("a foreign contextId opens the caller's own conversation and does not reveal the other one", async (t) => {
|
|
39
55
|
const s = setup();
|
|
40
56
|
t.after(s.restore);
|
|
41
57
|
const ada = (await s.owner("POST", "/owner/peers", { label: "ada" })).data.token as string;
|
|
42
58
|
const bob = (await s.owner("POST", "/owner/peers", { label: "bob" })).data.token as string;
|
|
43
59
|
assert.match(ada, /^a2aow_/);
|
|
44
60
|
assert.match(bob, /^a2aow_/);
|
|
61
|
+
const pushToken = `pt_${crypto.randomUUID()}`;
|
|
45
62
|
|
|
46
63
|
const first = await s.send(ada, "y-one", "ctx-y");
|
|
47
|
-
assert.equal(first.status, 200
|
|
64
|
+
assert.equal(first.status, 200);
|
|
48
65
|
assert.equal(first.data.result.contextId, "ctx-y");
|
|
49
66
|
const second = await s.send(ada, "y-two", "ctx-y");
|
|
50
|
-
assert.equal(second.status, 200
|
|
67
|
+
assert.equal(second.status, 200);
|
|
51
68
|
assert.equal(second.data.result.contextId, "ctx-y");
|
|
69
|
+
const adaTask = second.data.result.id as string;
|
|
70
|
+
s.DB.db.prepare("UPDATE tasks SET push_configs_json = ? WHERE id = ?").run(JSON.stringify([{ id: "cfg-ada", url: "https://hooks.example.net/ada", token: pushToken, _version: "0.3" }]), adaTask);
|
|
52
71
|
|
|
53
|
-
const history = () => s.owner("GET",
|
|
54
|
-
const beforeHist = await history();
|
|
55
|
-
assert.equal(beforeHist.status, 200
|
|
56
|
-
|
|
57
|
-
|
|
58
|
-
const wake = () => s.DB.db.prepare("SELECT context_id, peer, last_sent_ms, pending_json, pending_since_ms FROM wakes WHERE context_id = 'ctx-y' AND peer = 'ada'").get() as {
|
|
72
|
+
const history = (from?: string) => s.owner("GET", `/owner/history/ctx-y${from ? `?from=${from}` : ""}`);
|
|
73
|
+
const beforeHist = await history("ada");
|
|
74
|
+
assert.equal(beforeHist.status, 200);
|
|
75
|
+
assert.deepEqual((beforeHist.data as { text: string; peer: string }[]).map((r) => [r.peer, r.text]), [["ada", "y-one"], ["ada", "y-two"]]);
|
|
76
|
+
const wake = (peer: string) => s.DB.db.prepare("SELECT context_id, peer, last_sent_ms, pending_json, pending_since_ms FROM wakes WHERE context_id = 'ctx-y' AND peer = ?").get(peer) as {
|
|
59
77
|
pending_json: string; peer: string; last_sent_ms: number; pending_since_ms: number;
|
|
60
|
-
};
|
|
61
|
-
const beforeWake = wake();
|
|
62
|
-
assert.equal(beforeWake
|
|
63
|
-
const pending = JSON.parse(beforeWake
|
|
78
|
+
} | undefined;
|
|
79
|
+
const beforeWake = wake("ada");
|
|
80
|
+
assert.equal(beforeWake?.peer, "ada");
|
|
81
|
+
const pending = JSON.parse(beforeWake!.pending_json);
|
|
64
82
|
assert.equal(pending.from, "ada");
|
|
65
83
|
assert.equal(pending.preview, "y-two");
|
|
66
|
-
assert.equal(pending.taskIds.includes(
|
|
84
|
+
assert.equal(pending.taskIds.includes(adaTask), true);
|
|
85
|
+
|
|
86
|
+
const listedForeign = await s.call("POST", "/", {
|
|
87
|
+
jsonrpc: "2.0", id: 1, method: "ListTasks", params: { contextId: "ctx-y", pageSize: 50 },
|
|
88
|
+
}, `Bearer ${bob}`);
|
|
89
|
+
const listedFresh = await s.call("POST", "/", {
|
|
90
|
+
jsonrpc: "2.0", id: 1, method: "ListTasks", params: { contextId: "ctx-never-seen", pageSize: 50 },
|
|
91
|
+
}, `Bearer ${bob}`);
|
|
92
|
+
assert.equal(listedForeign.status, 200);
|
|
93
|
+
assert.equal(listedFresh.status, 200);
|
|
94
|
+
assert.deepEqual(listedForeign.data.result, listedFresh.data.result);
|
|
95
|
+
assert.deepEqual(listedForeign.data.result, { tasks: [], nextPageToken: "", pageSize: 50, totalSize: 0 });
|
|
96
|
+
hides(listedForeign.text, "y-one");
|
|
97
|
+
hides(listedForeign.text, "y-two");
|
|
98
|
+
hides(listedForeign.text, pushToken);
|
|
99
|
+
hides(listedForeign.text, ada);
|
|
67
100
|
|
|
68
101
|
const foreign = await s.send(bob, "x-inject", "ctx-y");
|
|
102
|
+
const fresh = await s.send(bob, "x-inject", "ctx-new");
|
|
103
|
+
assert.equal(foreign.status, 200);
|
|
104
|
+
assert.equal(fresh.status, 200);
|
|
105
|
+
assert.equal(foreign.data.error, undefined);
|
|
106
|
+
assert.equal(fresh.data.error, undefined);
|
|
107
|
+
assert.deepEqual(opened(foreign.data), { contextId: "ctx-y", state: "submitted", texts: ["x-inject"], roles: ["user"] });
|
|
108
|
+
assert.deepEqual(opened(fresh.data), { contextId: "ctx-new", state: "submitted", texts: ["x-inject"], roles: ["user"] });
|
|
109
|
+
hides(foreign.text, "y-one");
|
|
110
|
+
hides(foreign.text, "y-two");
|
|
111
|
+
hides(foreign.text, pushToken);
|
|
112
|
+
hides(foreign.text, ada);
|
|
113
|
+
hides(foreign.text, bob);
|
|
114
|
+
|
|
69
115
|
const foreignV1 = await s.send(bob, "x-inject-v1", "ctx-y", "SendMessage");
|
|
70
|
-
assert.
|
|
71
|
-
assert.
|
|
72
|
-
|
|
73
|
-
|
|
74
|
-
|
|
75
|
-
|
|
76
|
-
|
|
77
|
-
assert.
|
|
78
|
-
assert.
|
|
79
|
-
assert.equal(body.includes(bob), false);
|
|
80
|
-
|
|
81
|
-
const afterHist = await history();
|
|
82
|
-
assert.deepEqual(afterHist.data, beforeHist.data);
|
|
83
|
-
assert.deepEqual(wake(), beforeWake);
|
|
116
|
+
assert.equal(foreignV1.status, 200);
|
|
117
|
+
assert.equal(foreignV1.data.result.task.contextId, "ctx-y");
|
|
118
|
+
assert.deepEqual((foreignV1.data.result.task.history || []).map((m: { parts: { text: string }[] }) => m.parts.map((p) => p.text).join("\n")), ["x-inject-v1"]);
|
|
119
|
+
hides(foreignV1.text, "y-one");
|
|
120
|
+
hides(foreignV1.text, pushToken);
|
|
121
|
+
|
|
122
|
+
const adaRows = await history("ada");
|
|
123
|
+
assert.deepEqual(adaRows.data, beforeHist.data);
|
|
124
|
+
assert.deepEqual(wake("ada"), beforeWake);
|
|
84
125
|
const db = s.DB.db;
|
|
85
|
-
assert.equal((db.prepare("SELECT COUNT(*) AS n FROM tasks WHERE context_id = 'ctx-y' AND peer = '
|
|
86
|
-
assert.equal((db.prepare("SELECT COUNT(*) AS n FROM
|
|
87
|
-
assert.equal((db.prepare("SELECT COUNT(*) AS n FROM
|
|
88
|
-
|
|
126
|
+
assert.equal((db.prepare("SELECT COUNT(*) AS n FROM tasks WHERE context_id = 'ctx-y' AND peer = 'ada'").get() as { n: number }).n, 2);
|
|
127
|
+
assert.equal((db.prepare("SELECT COUNT(*) AS n FROM messages WHERE context_id = 'ctx-y' AND message_json LIKE '%y-one%'").get() as { n: number }).n, 1);
|
|
128
|
+
assert.equal((db.prepare("SELECT COUNT(*) AS n FROM messages WHERE task_id = ? AND message_json LIKE '%x-inject%'").get(adaTask) as { n: number }).n, 0);
|
|
129
|
+
const bobWake = wake("bob");
|
|
130
|
+
assert.equal(bobWake?.peer, "bob");
|
|
131
|
+
assert.equal(JSON.parse(bobWake!.pending_json).from, "bob");
|
|
132
|
+
assert.equal(JSON.parse(bobWake!.pending_json).preview, "x-inject-v1");
|
|
133
|
+
assert.equal(JSON.parse(beforeWake!.pending_json).preview, "y-two");
|
|
89
134
|
|
|
90
|
-
|
|
135
|
+
const owners = db.prepare("SELECT peer FROM conversation_owners WHERE context_id = 'ctx-y' ORDER BY peer").all() as { peer: string }[];
|
|
136
|
+
assert.deepEqual(owners.map((r) => r.peer), ["ada", "bob"]);
|
|
137
|
+
|
|
138
|
+
const steal = await s.call("POST", "/", {
|
|
139
|
+
jsonrpc: "2.0", id: 1, method: "message/send",
|
|
140
|
+
params: { message: { kind: "message", role: "user", messageId: crypto.randomUUID(), taskId: adaTask, contextId: "ctx-y", parts: [{ kind: "text", text: "steal" }] } },
|
|
141
|
+
}, `Bearer ${bob}`);
|
|
142
|
+
assert.deepEqual(steal.data.error, { code: -32001, message: "Task not found" });
|
|
143
|
+
hides(steal.text, "y-one");
|
|
144
|
+
hides(steal.text, pushToken);
|
|
145
|
+
const got = await s.call("POST", "/", { jsonrpc: "2.0", id: 1, method: "tasks/get", params: { id: adaTask } }, `Bearer ${bob}`);
|
|
146
|
+
assert.deepEqual(got.data.error, { code: -32001, message: "Task not found" });
|
|
147
|
+
hides(got.text, pushToken);
|
|
148
|
+
hides(got.text, "y-two");
|
|
91
149
|
|
|
92
150
|
const follow = await s.send(ada, "y-three", "ctx-y");
|
|
93
|
-
assert.equal(follow.status, 200
|
|
151
|
+
assert.equal(follow.status, 200);
|
|
94
152
|
assert.equal(follow.data.result.contextId, "ctx-y");
|
|
95
|
-
|
|
96
|
-
assert.
|
|
97
|
-
const
|
|
153
|
+
assert.deepEqual(((await history("ada")).data as { text: string; peer: string }[]).map((r) => r.text), ["y-one", "y-two", "y-three"]);
|
|
154
|
+
assert.equal(((await history("ada")).data as { peer: string }[]).every((r) => r.peer === "ada"), true);
|
|
155
|
+
const bobOnly = (await history("bob")).data as { text: string; peer: string }[];
|
|
156
|
+
assert.deepEqual(bobOnly.map((r) => r.text), ["x-inject", "x-inject-v1"]);
|
|
157
|
+
assert.equal(bobOnly.every((r) => r.peer === "bob"), true);
|
|
158
|
+
const mixed = (await history()).data as { text: string; peer: string }[];
|
|
159
|
+
assert.deepEqual(mixed.map((r) => r.peer), ["ada", "ada", "bob", "bob", "ada"]);
|
|
160
|
+
const after = JSON.parse(wake("ada")!.pending_json);
|
|
98
161
|
assert.equal(after.from, "ada");
|
|
99
162
|
assert.equal(after.preview, "y-three");
|
|
100
163
|
assert.equal(String(after.preview).includes("x-inject"), false);
|
|
101
|
-
|
|
102
|
-
|
|
164
|
+
|
|
165
|
+
const adaInbox = await s.owner("GET", "/owner/inbox?context=ctx-y&from=ada&all=1");
|
|
166
|
+
const bobInbox = await s.owner("GET", "/owner/inbox?context=ctx-y&from=bob&all=1");
|
|
167
|
+
assert.equal((adaInbox.data as { from: string; text: string }[]).every((r) => r.from === "ada"), true);
|
|
168
|
+
assert.equal((bobInbox.data as { from: string; text: string }[]).every((r) => r.from === "bob"), true);
|
|
169
|
+
assert.equal((adaInbox.data as { text: string }[]).some((r) => r.text === "x-inject"), false);
|
|
170
|
+
assert.equal((bobInbox.data as { text: string }[]).some((r) => r.text === "y-one"), false);
|
|
171
|
+
|
|
172
|
+
const adaList = await s.call("POST", "/", { jsonrpc: "2.0", id: 1, method: "ListTasks", params: { contextId: "ctx-y" } }, `Bearer ${ada}`);
|
|
173
|
+
const bobList = await s.call("POST", "/", { jsonrpc: "2.0", id: 1, method: "ListTasks", params: { contextId: "ctx-y" } }, `Bearer ${bob}`);
|
|
174
|
+
assert.equal(adaList.data.result.totalSize, 3);
|
|
175
|
+
assert.equal(bobList.data.result.totalSize, 2);
|
|
176
|
+
assert.equal((adaList.data.result.tasks as { id: string }[]).some((t) => t.id === adaTask), true);
|
|
177
|
+
assert.equal((bobList.data.result.tasks as { id: string }[]).some((t) => t.id === adaTask), false);
|
|
178
|
+
hides(bobList.text, pushToken);
|
|
179
|
+
hides(bobList.text, "y-one");
|
|
103
180
|
|
|
104
181
|
const own = await s.send(bob, "x-own", "ctx-x");
|
|
105
|
-
assert.equal(own.status, 200
|
|
182
|
+
assert.equal(own.status, 200);
|
|
106
183
|
assert.equal(own.data.result.contextId, "ctx-x");
|
|
107
184
|
const again = await s.send(bob, "x-again", "ctx-x");
|
|
108
|
-
assert.equal(again.status, 200
|
|
185
|
+
assert.equal(again.status, 200);
|
|
109
186
|
assert.equal(again.data.result.contextId, "ctx-x");
|
|
110
|
-
const
|
|
187
|
+
const omitted = await s.send(bob, "x-omit");
|
|
188
|
+
assert.equal(omitted.status, 200);
|
|
189
|
+
assert.match(omitted.data.result.contextId, /^[0-9a-f-]{36}$/);
|
|
190
|
+
assert.equal(omitted.data.result.contextId === "ctx-y", false);
|
|
191
|
+
const bobHist = await s.owner("GET", "/owner/history/ctx-x?from=bob");
|
|
111
192
|
assert.deepEqual((bobHist.data as { text: string }[]).map((r) => r.text), ["x-own", "x-again"]);
|
|
112
|
-
assert.deepEqual(((await history()).data as { text: string }[]).map((r) => r.text), ["y-one", "y-two", "y-three"]);
|
|
113
|
-
assert.equal(JSON.parse(wake().pending_json).from, "ada");
|
|
114
|
-
assert.equal(JSON.parse(wake().pending_json).preview, "y-three");
|
|
115
|
-
assert.equal((db.prepare("SELECT peer FROM conversation_owners WHERE context_id = 'ctx-x'").get() as { peer: string }).peer, "bob");
|
|
116
|
-
assert.equal((db.prepare("SELECT COUNT(*) AS n FROM conversation_owners WHERE context_id = 'ctx-y'").get() as { n: number }).n, 1);
|
|
117
193
|
});
|
|
118
194
|
|
|
119
|
-
test("
|
|
195
|
+
test("an outbound row on the same context id is not joined into the other peer's task", async (t) => {
|
|
196
|
+
const s = setup();
|
|
197
|
+
t.after(s.restore);
|
|
198
|
+
const ada = (await s.owner("POST", "/owner/peers", { label: "ada" })).data.token as string;
|
|
199
|
+
const bob = (await s.owner("POST", "/owner/peers", { label: "bob" })).data.token as string;
|
|
200
|
+
const aliasText = "alias-outbound-secret";
|
|
201
|
+
await s.send(ada, "y-one", "ctx-y");
|
|
202
|
+
const db = s.DB.db;
|
|
203
|
+
db.prepare("INSERT INTO outbound (task_id, context_id, peer, sent_at, updates_json) VALUES ('out-1', 'ctx-y', 'alias', '2026-01-01T00:00:00.000Z', '[]')").run();
|
|
204
|
+
db.prepare("INSERT INTO history (context_id, ts, dir, peer, task_id, text) VALUES ('ctx-y', '2026-01-01T00:00:00.000Z', 'in', 'alias', 'out-1', ?)").run(aliasText);
|
|
205
|
+
|
|
206
|
+
const sent = await s.send(bob, "from-bob", "ctx-y");
|
|
207
|
+
assert.equal(sent.status, 200);
|
|
208
|
+
assert.deepEqual(opened(sent.data).texts, ["from-bob"]);
|
|
209
|
+
hides(sent.text, aliasText);
|
|
210
|
+
const bobHist = await s.owner("GET", "/owner/history/ctx-y?from=bob");
|
|
211
|
+
assert.equal((bobHist.data as { text: string }[]).some((r) => r.text === aliasText), false);
|
|
212
|
+
const adaHist = await s.owner("GET", "/owner/history/ctx-y?from=ada");
|
|
213
|
+
assert.equal((adaHist.data as { text: string }[]).some((r) => r.text === aliasText), false);
|
|
214
|
+
assert.deepEqual((adaHist.data as { text: string }[]).map((r) => r.text), ["y-one"]);
|
|
215
|
+
assert.equal((db.prepare("SELECT COUNT(*) AS n FROM tasks WHERE context_id = 'ctx-y' AND peer = 'ada'").get() as { n: number }).n, 1);
|
|
216
|
+
assert.equal((db.prepare("SELECT text FROM history WHERE task_id = 'out-1'").get() as { text: string }).text, aliasText);
|
|
217
|
+
});
|
|
218
|
+
|
|
219
|
+
test("two peers can both open one unused contextId without sharing history or wakes", async (t) => {
|
|
120
220
|
const s = setup();
|
|
121
221
|
t.after(s.restore);
|
|
122
222
|
const ada = (await s.owner("POST", "/owner/peers", { label: "ada" })).data.token as string;
|
|
@@ -138,34 +238,53 @@ test("two peers cannot both claim one unused contextId", async (t) => {
|
|
|
138
238
|
s.send(bob, "from-bob", "ctx-race"),
|
|
139
239
|
]);
|
|
140
240
|
assert.equal(arrived, 2);
|
|
141
|
-
|
|
142
|
-
|
|
143
|
-
assert.equal(
|
|
144
|
-
assert.equal(
|
|
145
|
-
assert.
|
|
146
|
-
assert.equal(
|
|
147
|
-
|
|
148
|
-
|
|
241
|
+
assert.equal(left.status, 200);
|
|
242
|
+
assert.equal(right.status, 200);
|
|
243
|
+
assert.equal(left.data.error, undefined);
|
|
244
|
+
assert.equal(right.data.error, undefined);
|
|
245
|
+
assert.equal(left.data.result.contextId, "ctx-race");
|
|
246
|
+
assert.equal(right.data.result.contextId, "ctx-race");
|
|
247
|
+
assert.deepEqual(opened(left.data).texts, ["from-ada"]);
|
|
248
|
+
assert.deepEqual(opened(right.data).texts, ["from-bob"]);
|
|
249
|
+
hides(left.text, "from-bob");
|
|
250
|
+
hides(right.text, "from-ada");
|
|
251
|
+
hides(left.text, bob);
|
|
252
|
+
hides(right.text, ada);
|
|
149
253
|
|
|
150
254
|
const db = s.DB.db;
|
|
151
|
-
const owners = db.prepare("SELECT peer FROM conversation_owners WHERE context_id = 'ctx-race'").all() as { peer: string }[];
|
|
152
|
-
assert.
|
|
153
|
-
|
|
154
|
-
|
|
155
|
-
assert.equal(
|
|
156
|
-
assert.equal((db.prepare("SELECT COUNT(*) AS n FROM
|
|
157
|
-
assert.equal((db.prepare("SELECT COUNT(*) AS n FROM
|
|
158
|
-
assert.equal((db.prepare("SELECT COUNT(*) AS n FROM
|
|
159
|
-
assert.equal((db.prepare("SELECT COUNT(*) AS n FROM
|
|
160
|
-
|
|
161
|
-
|
|
162
|
-
|
|
163
|
-
assert.equal(
|
|
164
|
-
assert.equal(
|
|
165
|
-
|
|
166
|
-
|
|
167
|
-
|
|
168
|
-
const
|
|
169
|
-
assert.
|
|
170
|
-
assert.equal(
|
|
255
|
+
const owners = db.prepare("SELECT peer FROM conversation_owners WHERE context_id = 'ctx-race' ORDER BY peer").all() as { peer: string }[];
|
|
256
|
+
assert.deepEqual(owners.map((r) => r.peer), ["ada", "bob"]);
|
|
257
|
+
assert.equal((db.prepare("SELECT COUNT(*) AS n FROM tasks WHERE context_id = 'ctx-race'").get() as { n: number }).n, 2);
|
|
258
|
+
assert.equal((db.prepare("SELECT COUNT(*) AS n FROM tasks WHERE context_id = 'ctx-race' AND peer = 'ada'").get() as { n: number }).n, 1);
|
|
259
|
+
assert.equal((db.prepare("SELECT COUNT(*) AS n FROM tasks WHERE context_id = 'ctx-race' AND peer = 'bob'").get() as { n: number }).n, 1);
|
|
260
|
+
assert.equal((db.prepare("SELECT COUNT(*) AS n FROM history WHERE context_id = 'ctx-race' AND peer = 'ada' AND text = 'from-bob'").get() as { n: number }).n, 0);
|
|
261
|
+
assert.equal((db.prepare("SELECT COUNT(*) AS n FROM history WHERE context_id = 'ctx-race' AND peer = 'bob' AND text = 'from-ada'").get() as { n: number }).n, 0);
|
|
262
|
+
assert.equal((db.prepare("SELECT COUNT(*) AS n FROM messages WHERE context_id = 'ctx-race' AND message_json LIKE '%from-ada%' AND message_json LIKE '%from-bob%'").get() as { n: number }).n, 0);
|
|
263
|
+
assert.equal((db.prepare("SELECT COUNT(*) AS n FROM wakes WHERE context_id = 'ctx-race'").get() as { n: number }).n, 2);
|
|
264
|
+
const wakeRow = (peer: string) => db.prepare("SELECT pending_json, last_sent_ms FROM wakes WHERE context_id = 'ctx-race' AND peer = ?").get(peer) as { pending_json: string | null; last_sent_ms: number };
|
|
265
|
+
// The first message of a conversation is sent at once, so neither row is holding the other's text.
|
|
266
|
+
assert.equal(wakeRow("ada").pending_json, null);
|
|
267
|
+
assert.equal(wakeRow("bob").pending_json, null);
|
|
268
|
+
assert.equal(wakeRow("ada").last_sent_ms > 0 && wakeRow("bob").last_sent_ms > 0, true);
|
|
269
|
+
|
|
270
|
+
const followAda = await s.send(ada, "follow-ada", "ctx-race");
|
|
271
|
+
const adaPendingJson = wakeRow("ada").pending_json;
|
|
272
|
+
const followBob = await s.send(bob, "follow-bob", "ctx-race");
|
|
273
|
+
assert.equal(followAda.data.result.contextId, "ctx-race");
|
|
274
|
+
assert.equal(followBob.data.result.contextId, "ctx-race");
|
|
275
|
+
assert.equal(wakeRow("ada").pending_json, adaPendingJson);
|
|
276
|
+
const adaPending = JSON.parse(adaPendingJson || "null");
|
|
277
|
+
const bobPending = JSON.parse(wakeRow("bob").pending_json || "null");
|
|
278
|
+
assert.equal(adaPending.preview, "follow-ada");
|
|
279
|
+
assert.equal(bobPending.preview, "follow-bob");
|
|
280
|
+
assert.equal(adaPending.taskIds.includes(right.data.result.id), false);
|
|
281
|
+
assert.equal(adaPending.taskIds.includes(followBob.data.result.id), false);
|
|
282
|
+
assert.equal(bobPending.taskIds.includes(left.data.result.id), false);
|
|
283
|
+
assert.equal(bobPending.taskIds.includes(followAda.data.result.id), false);
|
|
284
|
+
const adaHist = await s.owner("GET", "/owner/history/ctx-race?from=ada");
|
|
285
|
+
const bobHist = await s.owner("GET", "/owner/history/ctx-race?from=bob");
|
|
286
|
+
assert.deepEqual((adaHist.data as { text: string; peer: string }[]).map((r) => r.text), ["from-ada", "follow-ada"]);
|
|
287
|
+
assert.deepEqual((bobHist.data as { text: string; peer: string }[]).map((r) => r.text), ["from-bob", "follow-bob"]);
|
|
288
|
+
assert.equal((adaHist.data as { peer: string }[]).every((r) => r.peer === "ada"), true);
|
|
289
|
+
assert.equal((bobHist.data as { peer: string }[]).every((r) => r.peer === "bob"), true);
|
|
171
290
|
});
|
|
@@ -326,7 +326,9 @@ test("proxy mode: peers sharing the upstream identity can't touch each other's t
|
|
|
326
326
|
for (const [method, params] of [
|
|
327
327
|
["GetTask", { id: taskId }], ["CancelTask", { id: taskId }], ["tasks/get", { id: taskId }],
|
|
328
328
|
["GetTaskPushNotificationConfig", { taskId, id: "x" }], ["ListTaskPushNotificationConfig", { taskId }],
|
|
329
|
-
["
|
|
329
|
+
["ListTaskPushNotificationConfigs", { taskId }], ["DeleteTaskPushNotificationConfig", { taskId, id: "x" }],
|
|
330
|
+
["tasks/pushNotificationConfig/list", { id: taskId }], ["tasks/pushNotificationConfig/delete", { id: taskId, pushNotificationConfigId: "x" }],
|
|
331
|
+
["GetTask", { id: "never-seen" }],
|
|
330
332
|
] as const) {
|
|
331
333
|
const x = await s.rpc(b, method, params);
|
|
332
334
|
assert.equal(x.data.error?.code, -32001, `${method}: ${JSON.stringify(x.data)}`);
|
|
@@ -390,7 +392,9 @@ test("proxy mode: push configs never reach the upstream (it would call them from
|
|
|
390
392
|
["CreateTaskPushNotificationConfig", { taskId, url: "https://hooks.example.org/b" }],
|
|
391
393
|
["tasks/pushNotificationConfig/set", { taskId, pushNotificationConfig: { url: "https://hooks.example.org/b" } }],
|
|
392
394
|
["GetTaskPushNotificationConfig", { taskId, id: "x" }], ["ListTaskPushNotificationConfig", { taskId }],
|
|
393
|
-
["
|
|
395
|
+
["ListTaskPushNotificationConfigs", { taskId }], ["DeleteTaskPushNotificationConfig", { taskId, id: "x" }],
|
|
396
|
+
["tasks/pushNotificationConfig/list", { id: taskId }], ["tasks/pushNotificationConfig/delete", { id: taskId, pushNotificationConfigId: "x" }],
|
|
397
|
+
["tasks/pushNotificationConfig/get", { id: taskId }],
|
|
394
398
|
] as const) {
|
|
395
399
|
const r = await s.rpc(a, method, params);
|
|
396
400
|
assert.equal(r.data.error?.code, -32003, method);
|
|
@@ -0,0 +1,120 @@
|
|
|
1
|
+
// ListTasks returns a cursor. The page is this peer's tasks, newest update first. Run: npm test
|
|
2
|
+
import { test } from "node:test";
|
|
3
|
+
import assert from "node:assert/strict";
|
|
4
|
+
import worker from "../src/index.ts";
|
|
5
|
+
import { d1 } from "./d1.ts";
|
|
6
|
+
|
|
7
|
+
const BASE = "https://agent.example.com";
|
|
8
|
+
|
|
9
|
+
function setup() {
|
|
10
|
+
const DB = d1(new URL("../migrations/", import.meta.url));
|
|
11
|
+
const env: any = {
|
|
12
|
+
DB, PUBLIC_URL: BASE, OWNER_TOKEN: "owner-secret", AGENT_NAME: "Test Inbox", WAKE_PRESET: "generic",
|
|
13
|
+
};
|
|
14
|
+
const call = async (token: string, method: string, params: unknown) => {
|
|
15
|
+
const res = await worker.fetch(new Request(BASE + "/", {
|
|
16
|
+
method: "POST",
|
|
17
|
+
headers: { authorization: `Bearer ${token}`, "content-type": "application/json" },
|
|
18
|
+
body: JSON.stringify({ jsonrpc: "2.0", id: 1, method, params }),
|
|
19
|
+
}) as any, env, { waitUntil() {}, passThroughOnException() {} } as any);
|
|
20
|
+
const text = await res.text();
|
|
21
|
+
let data: any = text;
|
|
22
|
+
try { data = text ? JSON.parse(text) : null; } catch { /* not json */ }
|
|
23
|
+
return { status: res.status, data, text };
|
|
24
|
+
};
|
|
25
|
+
const owner = async (method: string, path: string, json?: unknown) => {
|
|
26
|
+
const res = await worker.fetch(new Request(BASE + path, {
|
|
27
|
+
method, headers: { authorization: "Bearer owner-secret", "content-type": "application/json" },
|
|
28
|
+
body: json === undefined ? undefined : JSON.stringify(json),
|
|
29
|
+
}) as any, env, { waitUntil() {}, passThroughOnException() {} } as any);
|
|
30
|
+
return res.json() as Promise<any>;
|
|
31
|
+
};
|
|
32
|
+
return { DB, call, owner };
|
|
33
|
+
}
|
|
34
|
+
|
|
35
|
+
function stamp(n: number): string {
|
|
36
|
+
return `2026-01-01T00:00:${String(n).padStart(2, "0")}.000Z`;
|
|
37
|
+
}
|
|
38
|
+
|
|
39
|
+
function decodeCursor(token: string): { updatedAt: string; id: string } {
|
|
40
|
+
const pad = token.length % 4 === 0 ? "" : "=".repeat(4 - (token.length % 4));
|
|
41
|
+
const text = Buffer.from(token.replace(/-/g, "+").replace(/_/g, "/") + pad, "base64").toString();
|
|
42
|
+
const nl = text.indexOf("\n");
|
|
43
|
+
return { updatedAt: text.slice(0, nl), id: text.slice(nl + 1) };
|
|
44
|
+
}
|
|
45
|
+
|
|
46
|
+
test("ListTasks pages with nextPageToken and stays on the caller", async () => {
|
|
47
|
+
const s = setup();
|
|
48
|
+
const ada = (await s.owner("POST", "/owner/peers", { label: "ada" })).token as string;
|
|
49
|
+
const bob = (await s.owner("POST", "/owner/peers", { label: "bob" })).token as string;
|
|
50
|
+
const insert = s.DB.db.prepare(`INSERT INTO tasks (id, context_id, peer, state, protocol, created_at, updated_at, status_json, artifacts_json, push_configs_json)
|
|
51
|
+
VALUES (?, 'ctx-page', ?, 'submitted', '1.0', ?, ?, ?, '[]', '[]')`);
|
|
52
|
+
const ids: string[] = [];
|
|
53
|
+
for (let n = 1; n <= 16; n++) {
|
|
54
|
+
const id = `t-${String(n).padStart(2, "0")}`;
|
|
55
|
+
ids.push(id);
|
|
56
|
+
// t-16 and t-15 share a timestamp so the tie breaks on id descending.
|
|
57
|
+
const at = stamp(n === 15 ? 16 : n);
|
|
58
|
+
insert.run(id, "ada", at, at, JSON.stringify({ state: "submitted", timestamp: at }));
|
|
59
|
+
}
|
|
60
|
+
insert.run("t-bob", "bob", stamp(30), stamp(30), JSON.stringify({ state: "submitted", timestamp: stamp(30) }));
|
|
61
|
+
const expected = [...ids].reverse();
|
|
62
|
+
assert.deepEqual(expected.slice(0, 2), ["t-16", "t-15"]);
|
|
63
|
+
|
|
64
|
+
const seen: string[] = [];
|
|
65
|
+
let token = "";
|
|
66
|
+
let pages = 0;
|
|
67
|
+
let total = -1;
|
|
68
|
+
do {
|
|
69
|
+
const page = await s.call(ada, "ListTasks", { pageSize: 5, ...(token ? { pageToken: token } : {}) });
|
|
70
|
+
assert.equal(page.status, 200);
|
|
71
|
+
assert.equal(page.data.error, undefined);
|
|
72
|
+
const result = page.data.result;
|
|
73
|
+
assert.equal(result.pageSize, 5);
|
|
74
|
+
assert.equal(result.totalSize, 16);
|
|
75
|
+
total = result.totalSize;
|
|
76
|
+
const pageIds = (result.tasks as { id: string; contextId: string }[]).map((task) => task.id);
|
|
77
|
+
assert.equal(pageIds.includes("t-bob"), false);
|
|
78
|
+
assert.equal((result.tasks as { contextId: string }[]).every((task) => task.contextId === "ctx-page"), true);
|
|
79
|
+
if (result.nextPageToken) {
|
|
80
|
+
const cursor = decodeCursor(result.nextPageToken);
|
|
81
|
+
assert.equal(cursor.id, pageIds[pageIds.length - 1]);
|
|
82
|
+
assert.equal(result.tasks.length, 5);
|
|
83
|
+
} else {
|
|
84
|
+
assert.equal(result.nextPageToken, "");
|
|
85
|
+
}
|
|
86
|
+
seen.push(...pageIds);
|
|
87
|
+
token = result.nextPageToken;
|
|
88
|
+
pages++;
|
|
89
|
+
} while (token);
|
|
90
|
+
assert.equal(pages, 4);
|
|
91
|
+
assert.equal(total, 16);
|
|
92
|
+
assert.deepEqual(seen, expected);
|
|
93
|
+
assert.equal(new Set(seen).size, 16);
|
|
94
|
+
|
|
95
|
+
const bobPage = await s.call(bob, "ListTasks", { pageSize: 5 });
|
|
96
|
+
assert.deepEqual((bobPage.data.result.tasks as { id: string }[]).map((task) => task.id), ["t-bob"]);
|
|
97
|
+
assert.equal(bobPage.data.result.totalSize, 1);
|
|
98
|
+
assert.equal(bobPage.data.result.nextPageToken, "");
|
|
99
|
+
|
|
100
|
+
const filtered = await s.call(ada, "ListTasks", { contextId: "ctx-page", pageSize: 100 });
|
|
101
|
+
assert.equal(filtered.data.result.totalSize, 16);
|
|
102
|
+
assert.equal(filtered.data.result.pageSize, 100);
|
|
103
|
+
assert.equal(filtered.data.result.nextPageToken, "");
|
|
104
|
+
const empty = await s.call(ada, "ListTasks", { contextId: "ctx-none" });
|
|
105
|
+
assert.deepEqual(empty.data.result, { tasks: [], nextPageToken: "", pageSize: 50, totalSize: 0 });
|
|
106
|
+
const badContext = await s.call(ada, "ListTasks", { contextId: "bad id" });
|
|
107
|
+
assert.equal(badContext.data.error?.code, -32602);
|
|
108
|
+
|
|
109
|
+
const bad = "page-token-secret-value";
|
|
110
|
+
const rejected = await s.call(ada, "ListTasks", { pageToken: bad });
|
|
111
|
+
assert.deepEqual(rejected.data.error, { code: -32602, message: "Invalid params: pageToken" });
|
|
112
|
+
assert.equal(rejected.text.includes(bad), false);
|
|
113
|
+
const garbage = Buffer.from("not-a-cursor", "utf8").toString("base64url");
|
|
114
|
+
const rejectedGarbage = await s.call(ada, "ListTasks", { pageToken: garbage });
|
|
115
|
+
assert.equal(rejectedGarbage.data.error?.message, "Invalid params: pageToken");
|
|
116
|
+
assert.equal(rejectedGarbage.text.includes(garbage), false);
|
|
117
|
+
|
|
118
|
+
const absent = await s.call(ada, "tasks/list", {});
|
|
119
|
+
assert.equal(absent.data.error?.code, -32601);
|
|
120
|
+
});
|
|
@@ -67,15 +67,15 @@ test("fresh database: every migration applies in order; a second run applies not
|
|
|
67
67
|
test("database from the 0002 fix (0001-0003 recorded): only 0004 and later apply; pending requests keep working", () => {
|
|
68
68
|
const db = existingDb([sql("0001_init.sql"), sql("0002_wake_budget.sql"), sql("0003_device_pairing.sql")], ["0001_init.sql", "0002_wake_budget.sql", "0003_device_pairing.sql"]);
|
|
69
69
|
db.prepare("INSERT INTO device_requests (device_hash, user_code, status, created_ms, expires_ms, interval_s) VALUES ('h', 'WDJB4827', 'pending', 1, 2, 5)").run();
|
|
70
|
-
assert.deepEqual(applyMigrations(db), ["0004_pairing_replace.sql", "0005_facade_owners.sql", "0006_mcp.sql", "0007_cimd.sql", "0008_oidc_txns.sql", "0009_outbound_seen.sql", "0010_conversation_peer.sql", "0011_conversation_owner.sql"]);
|
|
70
|
+
assert.deepEqual(applyMigrations(db), ["0004_pairing_replace.sql", "0005_facade_owners.sql", "0006_mcp.sql", "0007_cimd.sql", "0008_oidc_txns.sql", "0009_outbound_seen.sql", "0010_conversation_peer.sql", "0011_conversation_owner.sql", "0012_conversation_peer_owner.sql"]);
|
|
71
71
|
assert.equal((db.prepare("SELECT replaces_label FROM device_requests WHERE device_hash = 'h'").get() as any).replaces_label, null);
|
|
72
72
|
});
|
|
73
73
|
|
|
74
74
|
test("database from v0.2.0 (0001 and 0003 recorded): 0002 applies as a no-op and keeps the data", () => {
|
|
75
75
|
const db = existingDb([sql("0001_init.sql"), sql("0003_device_pairing.sql")], ["0001_init.sql", "0003_device_pairing.sql"]);
|
|
76
76
|
db.prepare("INSERT INTO wake_budget (hour, count) VALUES (1, 7)").run();
|
|
77
|
-
assert.deepEqual(applyMigrations(db), ["0002_wake_budget.sql", "0004_pairing_replace.sql", "0005_facade_owners.sql", "0006_mcp.sql", "0007_cimd.sql", "0008_oidc_txns.sql", "0009_outbound_seen.sql", "0010_conversation_peer.sql", "0011_conversation_owner.sql"]);
|
|
78
|
-
assert.deepEqual(recorded(db), ["0001_init.sql", "0003_device_pairing.sql", "0002_wake_budget.sql", "0004_pairing_replace.sql", "0005_facade_owners.sql", "0006_mcp.sql", "0007_cimd.sql", "0008_oidc_txns.sql", "0009_outbound_seen.sql", "0010_conversation_peer.sql", "0011_conversation_owner.sql"]);
|
|
77
|
+
assert.deepEqual(applyMigrations(db), ["0002_wake_budget.sql", "0004_pairing_replace.sql", "0005_facade_owners.sql", "0006_mcp.sql", "0007_cimd.sql", "0008_oidc_txns.sql", "0009_outbound_seen.sql", "0010_conversation_peer.sql", "0011_conversation_owner.sql", "0012_conversation_peer_owner.sql"]);
|
|
78
|
+
assert.deepEqual(recorded(db), ["0001_init.sql", "0003_device_pairing.sql", "0002_wake_budget.sql", "0004_pairing_replace.sql", "0005_facade_owners.sql", "0006_mcp.sql", "0007_cimd.sql", "0008_oidc_txns.sql", "0009_outbound_seen.sql", "0010_conversation_peer.sql", "0011_conversation_owner.sql", "0012_conversation_peer_owner.sql"]);
|
|
79
79
|
assert.equal((db.prepare("SELECT count FROM wake_budget WHERE hour = 1").get() as any).count, 7);
|
|
80
80
|
});
|
|
81
81
|
|
|
@@ -107,7 +107,7 @@ test("database from an earlier build without wake_budget (0001 and 0003 recorded
|
|
|
107
107
|
assert.equal(before.data.error?.code, -32603, JSON.stringify(before.data));
|
|
108
108
|
assert.equal(wakes.length, 0);
|
|
109
109
|
|
|
110
|
-
assert.deepEqual(applyMigrations(db), ["0002_wake_budget.sql", "0004_pairing_replace.sql", "0005_facade_owners.sql", "0006_mcp.sql", "0007_cimd.sql", "0008_oidc_txns.sql", "0009_outbound_seen.sql", "0010_conversation_peer.sql", "0011_conversation_owner.sql"]);
|
|
110
|
+
assert.deepEqual(applyMigrations(db), ["0002_wake_budget.sql", "0004_pairing_replace.sql", "0005_facade_owners.sql", "0006_mcp.sql", "0007_cimd.sql", "0008_oidc_txns.sql", "0009_outbound_seen.sql", "0010_conversation_peer.sql", "0011_conversation_owner.sql", "0012_conversation_peer_owner.sql"]);
|
|
111
111
|
assert.deepEqual(columns(db, "wake_budget"), ["hour", "count"]);
|
|
112
112
|
assert.deepEqual(applyMigrations(db), []);
|
|
113
113
|
|
|
@@ -141,7 +141,7 @@ test("0010 keys an existing wake by the peer in its payload and allows a second
|
|
|
141
141
|
});
|
|
142
142
|
|
|
143
143
|
test("0011 keeps one owner per context from the earliest task, then history, then outbound", () => {
|
|
144
|
-
const prior = [...FILES].sort(byNumber).filter((f) => f !== "0011_conversation_owner.sql");
|
|
144
|
+
const prior = [...FILES].sort(byNumber).filter((f) => f !== "0011_conversation_owner.sql" && f !== "0012_conversation_peer_owner.sql");
|
|
145
145
|
const db = existingDb(prior.map(sql), prior);
|
|
146
146
|
const task = `INSERT INTO tasks (id, context_id, peer, state, protocol, created_at, updated_at, status_json, artifacts_json, push_configs_json)
|
|
147
147
|
VALUES (?, ?, ?, 'submitted', '0.3', ?, ?, '{}', '[]', '[]')`;
|
|
@@ -149,7 +149,7 @@ test("0011 keeps one owner per context from the earliest task, then history, the
|
|
|
149
149
|
db.prepare(task).run("t-ada", "ctx-y", "ada", "2026-01-01T00:00:00.000Z", "2026-01-01T00:00:00.000Z");
|
|
150
150
|
db.prepare("INSERT INTO history (context_id, ts, dir, peer, text) VALUES ('ctx-h', '2026-01-01T00:00:00.000Z', 'local', 'ada', 'note')").run();
|
|
151
151
|
db.prepare("INSERT INTO outbound (task_id, context_id, peer, sent_at) VALUES ('o1', 'ctx-o', 'alias', '2026-01-01T00:00:00.000Z')").run();
|
|
152
|
-
assert.deepEqual(applyMigrations(db), ["0011_conversation_owner.sql"]);
|
|
152
|
+
assert.deepEqual(applyMigrations(db, ["0011_conversation_owner.sql"]), ["0011_conversation_owner.sql"]);
|
|
153
153
|
const owner = (id: string) => (db.prepare("SELECT peer FROM conversation_owners WHERE context_id = ?").get(id) as { peer: string }).peer;
|
|
154
154
|
assert.equal(owner("ctx-y"), "ada");
|
|
155
155
|
assert.equal((db.prepare("SELECT COUNT(*) AS n FROM conversation_owners WHERE context_id = 'ctx-y'").get() as { n: number }).n, 1);
|
|
@@ -157,3 +157,15 @@ test("0011 keeps one owner per context from the earliest task, then history, the
|
|
|
157
157
|
assert.equal(owner("ctx-o"), "alias");
|
|
158
158
|
assert.throws(() => db.prepare("INSERT INTO conversation_owners (context_id, peer, created_at) VALUES ('ctx-y', 'bob', 'later')").run());
|
|
159
159
|
});
|
|
160
|
+
|
|
161
|
+
test("0012 lets each peer own the same context id and keeps the first row", () => {
|
|
162
|
+
const prior = [...FILES].sort(byNumber).filter((f) => f !== "0012_conversation_peer_owner.sql");
|
|
163
|
+
const db = existingDb(prior.map(sql), prior);
|
|
164
|
+
db.prepare("INSERT INTO conversation_owners (context_id, peer, created_at) VALUES ('ctx-y', 'ada', '2026-01-01T00:00:00.000Z')").run();
|
|
165
|
+
assert.throws(() => db.prepare("INSERT INTO conversation_owners (context_id, peer, created_at) VALUES ('ctx-y', 'bob', 'later')").run());
|
|
166
|
+
assert.deepEqual(applyMigrations(db), ["0012_conversation_peer_owner.sql"]);
|
|
167
|
+
assert.equal((db.prepare("SELECT created_at FROM conversation_owners WHERE context_id = 'ctx-y' AND peer = 'ada'").get() as { created_at: string }).created_at, "2026-01-01T00:00:00.000Z");
|
|
168
|
+
db.prepare("INSERT INTO conversation_owners (context_id, peer, created_at) VALUES ('ctx-y', 'bob', 'later')").run();
|
|
169
|
+
const peers = db.prepare("SELECT peer FROM conversation_owners WHERE context_id = 'ctx-y' ORDER BY peer").all() as { peer: string }[];
|
|
170
|
+
assert.deepEqual(peers.map((r) => r.peer), ["ada", "bob"]);
|
|
171
|
+
});
|
|
@@ -12,9 +12,11 @@ test("owner RPC allowlist is the management reads and the calls that do not retu
|
|
|
12
12
|
assert.equal(ok("GET", "/owner/inbox")?.name, "GET /owner/inbox");
|
|
13
13
|
assert.equal(ok("GET", "/owner/summary")?.name, "GET /owner/summary");
|
|
14
14
|
assert.equal(ok("GET", "/owner/inbox", { context: "ctx-1", all: "1" })?.search, "?context=ctx-1&all=1");
|
|
15
|
+
assert.equal(ok("GET", "/owner/inbox", { from: "ada" })?.search, "?from=ada");
|
|
15
16
|
assert.equal(ok("GET", "/owner/inbox", { limit: "20", cursor: "abc_DEF-012" })?.search, "?limit=20&cursor=abc_DEF-012");
|
|
16
17
|
assert.equal(ok("GET", "/owner/tasks/task-1")?.path, "/owner/tasks/task-1");
|
|
17
18
|
assert.equal(ok("GET", "/owner/history/ctx-1", { n: "50" })?.search, "?n=50");
|
|
19
|
+
assert.equal(ok("GET", "/owner/history/ctx-1", { n: "50", from: "ada" })?.search, "?n=50&from=ada");
|
|
18
20
|
assert.equal(ok("GET", "/owner/outbound/task-1")?.name, "GET /owner/outbound/:id");
|
|
19
21
|
assert.equal(ok("GET", "/owner/peers")?.name, "GET /owner/peers");
|
|
20
22
|
assert.equal(ok("DELETE", "/owner/peers/ada")?.path, "/owner/peers/ada");
|
|
@@ -63,6 +65,8 @@ test("owner RPC allowlist is the management reads and the calls that do not retu
|
|
|
63
65
|
assert.deepEqual(refused("GET", "/owner/inbox", { limit: "201" }), { ok: false, reason: "query" });
|
|
64
66
|
assert.deepEqual(refused("GET", "/owner/inbox", { limit: "0" }), { ok: false, reason: "query" });
|
|
65
67
|
assert.deepEqual(refused("GET", "/owner/inbox", { cursor: "bad+cursor" }), { ok: false, reason: "query" });
|
|
68
|
+
assert.deepEqual(refused("GET", "/owner/inbox", { from: "bad peer" }), { ok: false, reason: "query" });
|
|
69
|
+
assert.deepEqual(refused("GET", "/owner/history/ctx-1", { from: "../x" }), { ok: false, reason: "query" });
|
|
66
70
|
assert.deepEqual(refused("GET", "/owner/summary", { all: "1" }), { ok: false, reason: "query" });
|
|
67
71
|
assert.deepEqual(refused("GET", "/owner/history/ctx-1", { n: "1001" }), { ok: false, reason: "query" });
|
|
68
72
|
assert.deepEqual(refused("GET", "/owner/history/.."), { ok: false, reason: "path" });
|