@agentworkforce/sage 1.1.2 → 1.2.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/dist/app.d.ts.map +1 -1
- package/dist/app.js +56 -23
- package/dist/e2e/e2e-harness.d.ts +36 -0
- package/dist/e2e/e2e-harness.d.ts.map +1 -0
- package/dist/e2e/e2e-harness.js +278 -0
- package/dist/e2e/mock-cloud-proxy-server.d.ts +25 -0
- package/dist/e2e/mock-cloud-proxy-server.d.ts.map +1 -0
- package/dist/e2e/mock-cloud-proxy-server.js +149 -0
- package/dist/e2e/mock-relayfile-server.d.ts +35 -0
- package/dist/e2e/mock-relayfile-server.d.ts.map +1 -0
- package/dist/e2e/mock-relayfile-server.js +488 -0
- package/dist/integrations/cloud-proxy-provider.js +1 -1
- package/dist/integrations/freshness-envelope.js +1 -1
- package/dist/integrations/github.d.ts +24 -1
- package/dist/integrations/github.d.ts.map +1 -1
- package/dist/integrations/github.js +116 -1
- package/dist/integrations/linear-ingress.d.ts +30 -0
- package/dist/integrations/linear-ingress.d.ts.map +1 -0
- package/dist/integrations/linear-ingress.js +58 -0
- package/dist/integrations/notion-ingress.d.ts +26 -0
- package/dist/integrations/notion-ingress.d.ts.map +1 -0
- package/dist/integrations/notion-ingress.js +70 -0
- package/dist/integrations/provider-ingress-dedup.d.ts +14 -0
- package/dist/integrations/provider-ingress-dedup.d.ts.map +1 -0
- package/dist/integrations/provider-ingress-dedup.js +35 -0
- package/dist/integrations/provider-ingress-dedup.test.d.ts +2 -0
- package/dist/integrations/provider-ingress-dedup.test.d.ts.map +1 -0
- package/dist/integrations/provider-ingress-dedup.test.js +55 -0
- package/dist/integrations/provider-write-facade.d.ts +80 -0
- package/dist/integrations/provider-write-facade.d.ts.map +1 -0
- package/dist/integrations/provider-write-facade.js +417 -0
- package/dist/integrations/provider-write-facade.test.d.ts +2 -0
- package/dist/integrations/provider-write-facade.test.d.ts.map +1 -0
- package/dist/integrations/provider-write-facade.test.js +247 -0
- package/dist/integrations/read-your-writes.test.d.ts +2 -0
- package/dist/integrations/read-your-writes.test.d.ts.map +1 -0
- package/dist/integrations/read-your-writes.test.js +170 -0
- package/dist/integrations/recent-actions-overlay.d.ts +1 -0
- package/dist/integrations/recent-actions-overlay.d.ts.map +1 -1
- package/dist/integrations/recent-actions-overlay.js +3 -0
- package/dist/integrations/relayfile-reader-envelope.test.d.ts +2 -0
- package/dist/integrations/relayfile-reader-envelope.test.d.ts.map +1 -0
- package/dist/integrations/relayfile-reader-envelope.test.js +198 -0
- package/dist/integrations/relayfile-reader.d.ts +20 -1
- package/dist/integrations/relayfile-reader.d.ts.map +1 -1
- package/dist/integrations/relayfile-reader.js +334 -48
- package/dist/integrations/slack-egress.d.ts +2 -1
- package/dist/integrations/slack-egress.d.ts.map +1 -1
- package/dist/integrations/slack-egress.js +28 -1
- package/dist/observability.e2e.test.d.ts +2 -0
- package/dist/observability.e2e.test.d.ts.map +1 -0
- package/dist/observability.e2e.test.js +411 -0
- package/package.json +9 -4
|
@@ -0,0 +1,411 @@
|
|
|
1
|
+
import { afterEach, beforeEach, describe, expect, it, vi } from "vitest";
|
|
2
|
+
import { buildHarness, makeGitHubPRComment, makeRelayfileSeedEntry, makeSlackMessage, } from "./e2e/e2e-harness.js";
|
|
3
|
+
const POLL_INTERVAL_MS = 10;
|
|
4
|
+
const POLL_TIMEOUT_MS = 2_000;
|
|
5
|
+
function delay(ms) {
|
|
6
|
+
return new Promise((resolve) => {
|
|
7
|
+
setTimeout(resolve, ms);
|
|
8
|
+
});
|
|
9
|
+
}
|
|
10
|
+
function expectEnvelopeMeta(envelope, status, source) {
|
|
11
|
+
expect(envelope.status).toBe(status);
|
|
12
|
+
expect(envelope.source).toBe(source);
|
|
13
|
+
expect(envelope.asOf).toEqual(expect.any(Number));
|
|
14
|
+
expect(envelope.asOf).toBeGreaterThan(0);
|
|
15
|
+
expect(envelope.ageMs).toEqual(expect.any(Number));
|
|
16
|
+
expect(envelope.ageMs).toBeGreaterThanOrEqual(0);
|
|
17
|
+
}
|
|
18
|
+
describe("observability E2E", () => {
|
|
19
|
+
let harness;
|
|
20
|
+
beforeEach(async () => {
|
|
21
|
+
vi.useRealTimers();
|
|
22
|
+
harness = await buildHarness();
|
|
23
|
+
});
|
|
24
|
+
afterEach(async () => {
|
|
25
|
+
vi.useRealTimers();
|
|
26
|
+
await harness.teardown();
|
|
27
|
+
});
|
|
28
|
+
async function rebuildHarness(options) {
|
|
29
|
+
await harness.teardown();
|
|
30
|
+
harness = await buildHarness(options);
|
|
31
|
+
}
|
|
32
|
+
async function waitForSettledStatus(actionId, timeoutMs = POLL_TIMEOUT_MS) {
|
|
33
|
+
const startedAt = Date.now();
|
|
34
|
+
while (Date.now() - startedAt < timeoutMs) {
|
|
35
|
+
const status = harness.facade.getStatus(actionId);
|
|
36
|
+
if (status && status.state !== "pending") {
|
|
37
|
+
return status;
|
|
38
|
+
}
|
|
39
|
+
await delay(POLL_INTERVAL_MS);
|
|
40
|
+
}
|
|
41
|
+
const status = harness.facade.getStatus(actionId);
|
|
42
|
+
throw new Error(`Timed out waiting for write status to settle: ${JSON.stringify(status)}`);
|
|
43
|
+
}
|
|
44
|
+
function replaceSlackEgress(slackEgress) {
|
|
45
|
+
harness.facade.slackEgress = slackEgress;
|
|
46
|
+
}
|
|
47
|
+
describe("happy paths", () => {
|
|
48
|
+
it("slack read-your-writes", async () => {
|
|
49
|
+
const msg = makeSlackMessage();
|
|
50
|
+
harness.mockCloudProxy.setSlackResponse("/chat.postMessage", {
|
|
51
|
+
body: { ok: true, ts: "1234567890.000100" },
|
|
52
|
+
});
|
|
53
|
+
const writeResult = await harness.facade.write({
|
|
54
|
+
kind: "slack:postMessage",
|
|
55
|
+
channel: msg.channel,
|
|
56
|
+
text: msg.text,
|
|
57
|
+
});
|
|
58
|
+
expect(writeResult).toMatchObject({
|
|
59
|
+
ok: true,
|
|
60
|
+
mode: "sync",
|
|
61
|
+
providerPath: `/slack/channels/${msg.channel}/messages/1234567890.000100.json`,
|
|
62
|
+
});
|
|
63
|
+
expect(writeResult.data).toMatchObject({ ts: "1234567890.000100" });
|
|
64
|
+
const envelope = await harness.reader.readFileEnveloped(`/slack/channels/${msg.channel}/messages/1234567890.000100.json`);
|
|
65
|
+
expectEnvelopeMeta(envelope, "hit", "overlay");
|
|
66
|
+
expect(envelope.data).toEqual(expect.stringContaining(msg.text));
|
|
67
|
+
expect(envelope.data).toEqual(expect.stringContaining("1234567890.000100"));
|
|
68
|
+
});
|
|
69
|
+
it("github read-your-writes", async () => {
|
|
70
|
+
const comment = makeGitHubPRComment();
|
|
71
|
+
harness.mockCloudProxy.setGithubResponse(`/repos/${comment.owner}/${comment.repo}/issues/${comment.number}/comments`, { body: { id: 999, url: "https://github.com/test/999" } });
|
|
72
|
+
const writeResult = await harness.facade.write({
|
|
73
|
+
kind: "github:createComment",
|
|
74
|
+
owner: comment.owner,
|
|
75
|
+
repo: comment.repo,
|
|
76
|
+
number: comment.number,
|
|
77
|
+
body: comment.body,
|
|
78
|
+
});
|
|
79
|
+
expect(writeResult).toMatchObject({
|
|
80
|
+
ok: true,
|
|
81
|
+
mode: "sync",
|
|
82
|
+
providerPath: `/github/repos/${comment.owner}/${comment.repo}/issues/${comment.number}/comments/999.json`,
|
|
83
|
+
});
|
|
84
|
+
expect(writeResult.data).toMatchObject({ id: 999 });
|
|
85
|
+
const envelope = await harness.reader.readGitHubPREnveloped(comment.owner, comment.repo, comment.number);
|
|
86
|
+
expectEnvelopeMeta(envelope, "hit", "overlay");
|
|
87
|
+
expect(envelope.data).toEqual(expect.stringContaining(comment.body));
|
|
88
|
+
expect(envelope.data).toEqual(expect.stringContaining("999"));
|
|
89
|
+
});
|
|
90
|
+
it("relayfile hit passthrough", async () => {
|
|
91
|
+
const filePath = "/slack/channels/C-test/messages/ts1.json";
|
|
92
|
+
harness.mockRelayfile.seedFile(filePath, makeRelayfileSeedEntry(filePath, {
|
|
93
|
+
content: JSON.stringify({ text: "archived message" }),
|
|
94
|
+
}));
|
|
95
|
+
const envelope = await harness.reader.readFileEnveloped(filePath);
|
|
96
|
+
expectEnvelopeMeta(envelope, "hit", "relayfile");
|
|
97
|
+
expect(envelope.data).toEqual(expect.stringContaining("archived message"));
|
|
98
|
+
});
|
|
99
|
+
});
|
|
100
|
+
describe("staleness", () => {
|
|
101
|
+
it("TTL expiry falls through to relayfile", async () => {
|
|
102
|
+
await rebuildHarness({ overlayTtlMs: 50 });
|
|
103
|
+
const filePath = "/slack/channels/C1/messages/ts1.json";
|
|
104
|
+
harness.mockRelayfile.seedFile(filePath, makeRelayfileSeedEntry(filePath, {
|
|
105
|
+
content: JSON.stringify({ text: "relayfile data" }),
|
|
106
|
+
}));
|
|
107
|
+
vi.useFakeTimers();
|
|
108
|
+
vi.setSystemTime(new Date("2026-04-15T12:00:00.000Z"));
|
|
109
|
+
harness.overlay.record({
|
|
110
|
+
path: filePath,
|
|
111
|
+
data: { text: "overlay data" },
|
|
112
|
+
provider: "slack",
|
|
113
|
+
action: "postMessage",
|
|
114
|
+
});
|
|
115
|
+
vi.advanceTimersByTime(100);
|
|
116
|
+
expect(harness.overlay.lookup(filePath)).toBeNull();
|
|
117
|
+
vi.useRealTimers();
|
|
118
|
+
const envelope = await harness.reader.readFileEnveloped(filePath);
|
|
119
|
+
expectEnvelopeMeta(envelope, "hit", "relayfile");
|
|
120
|
+
expect(envelope.data).toEqual(expect.stringContaining("relayfile data"));
|
|
121
|
+
expect(envelope.data).not.toEqual(expect.stringContaining("overlay data"));
|
|
122
|
+
});
|
|
123
|
+
it("overlay wins over stale relayfile", async () => {
|
|
124
|
+
const filePath = "/slack/channels/C1/messages/ts2.json";
|
|
125
|
+
harness.mockRelayfile.seedFile(filePath, makeRelayfileSeedEntry(filePath, {
|
|
126
|
+
content: JSON.stringify({ text: "old relayfile" }),
|
|
127
|
+
}));
|
|
128
|
+
harness.overlay.record({
|
|
129
|
+
path: filePath,
|
|
130
|
+
data: { text: "fresh overlay" },
|
|
131
|
+
provider: "slack",
|
|
132
|
+
action: "postMessage",
|
|
133
|
+
});
|
|
134
|
+
const envelope = await harness.reader.readFileEnveloped(filePath);
|
|
135
|
+
expectEnvelopeMeta(envelope, "hit", "overlay");
|
|
136
|
+
expect(envelope.data).toEqual(expect.stringContaining("fresh overlay"));
|
|
137
|
+
expect(envelope.data).not.toEqual(expect.stringContaining("old relayfile"));
|
|
138
|
+
});
|
|
139
|
+
it("LRU eviction under pressure", async () => {
|
|
140
|
+
await rebuildHarness({ overlayMaxEntries: 3 });
|
|
141
|
+
vi.useFakeTimers();
|
|
142
|
+
vi.setSystemTime(new Date("2026-04-15T12:00:00.000Z"));
|
|
143
|
+
for (const path of ["/a", "/b", "/c", "/d"]) {
|
|
144
|
+
harness.overlay.record({
|
|
145
|
+
path,
|
|
146
|
+
data: { path },
|
|
147
|
+
provider: "slack",
|
|
148
|
+
action: "test",
|
|
149
|
+
});
|
|
150
|
+
vi.advanceTimersByTime(1);
|
|
151
|
+
}
|
|
152
|
+
expect(harness.overlay.size).toBeLessThanOrEqual(3);
|
|
153
|
+
expect(harness.overlay.lookup("/a")).toBeNull();
|
|
154
|
+
expect(harness.overlay.lookup("/d")).toMatchObject({
|
|
155
|
+
path: "/d",
|
|
156
|
+
data: { path: "/d" },
|
|
157
|
+
});
|
|
158
|
+
vi.useRealTimers();
|
|
159
|
+
});
|
|
160
|
+
});
|
|
161
|
+
describe("error distinguishability", () => {
|
|
162
|
+
it("relayfile 404 yields miss envelope", async () => {
|
|
163
|
+
const envelope = await harness.reader.readFileEnveloped("/nonexistent/path.json");
|
|
164
|
+
expectEnvelopeMeta(envelope, "miss", "relayfile");
|
|
165
|
+
expect(envelope.data).toBeNull();
|
|
166
|
+
expect(envelope.error).toBeUndefined();
|
|
167
|
+
});
|
|
168
|
+
it("relayfile 500 yields error envelope", async () => {
|
|
169
|
+
harness.mockRelayfile.setResponse("/failing/path.json", {
|
|
170
|
+
status: 500,
|
|
171
|
+
body: { code: "internal_error", message: "boom" },
|
|
172
|
+
});
|
|
173
|
+
const envelope = await harness.reader.readFileEnveloped("/failing/path.json");
|
|
174
|
+
expectEnvelopeMeta(envelope, "error", "relayfile");
|
|
175
|
+
expect(envelope.data).toBeNull();
|
|
176
|
+
expect(envelope.error).toEqual(expect.any(String));
|
|
177
|
+
expect(envelope.error?.length).toBeGreaterThan(0);
|
|
178
|
+
});
|
|
179
|
+
it("relayfile timeout yields error envelope", async () => {
|
|
180
|
+
harness.mockRelayfile.setResponse("/slow/path.json", { delayMs: 60_000 });
|
|
181
|
+
const envelope = await harness.reader.readFileEnveloped("/slow/path.json");
|
|
182
|
+
expectEnvelopeMeta(envelope, "error", "relayfile");
|
|
183
|
+
expect(envelope.data).toBeNull();
|
|
184
|
+
expect(envelope.error).toEqual(expect.any(String));
|
|
185
|
+
expect(envelope.error?.length).toBeGreaterThan(0);
|
|
186
|
+
}, 4_000);
|
|
187
|
+
it("overlay source is observable", async () => {
|
|
188
|
+
harness.overlay.record({
|
|
189
|
+
path: "/test/observable.json",
|
|
190
|
+
data: { key: "value" },
|
|
191
|
+
provider: "slack",
|
|
192
|
+
action: "postMessage",
|
|
193
|
+
});
|
|
194
|
+
const envelope = await harness.reader.readFileEnveloped("/test/observable.json");
|
|
195
|
+
expectEnvelopeMeta(envelope, "hit", "overlay");
|
|
196
|
+
expect(envelope.data).toEqual(expect.stringContaining("value"));
|
|
197
|
+
});
|
|
198
|
+
});
|
|
199
|
+
describe("write modes", () => {
|
|
200
|
+
it("sync write success populates overlay", async () => {
|
|
201
|
+
harness.mockCloudProxy.setSlackResponse("/chat.postMessage", {
|
|
202
|
+
body: { ok: true, ts: "1111.2222" },
|
|
203
|
+
});
|
|
204
|
+
const result = await harness.facade.write({ kind: "slack:postMessage", channel: "C-sync", text: "sync test" }, "sync");
|
|
205
|
+
expect(result).toMatchObject({
|
|
206
|
+
ok: true,
|
|
207
|
+
mode: "sync",
|
|
208
|
+
providerPath: "/slack/channels/C-sync/messages/1111.2222.json",
|
|
209
|
+
});
|
|
210
|
+
const entry = harness.overlay.lookup("/slack/channels/C-sync/messages/1111.2222.json");
|
|
211
|
+
expect(entry).toMatchObject({
|
|
212
|
+
provider: "slack",
|
|
213
|
+
action: "postMessage",
|
|
214
|
+
data: {
|
|
215
|
+
channel: "C-sync",
|
|
216
|
+
text: "sync test",
|
|
217
|
+
ts: "1111.2222",
|
|
218
|
+
},
|
|
219
|
+
});
|
|
220
|
+
});
|
|
221
|
+
it("sync write provider error skips overlay", async () => {
|
|
222
|
+
harness.mockCloudProxy.setSlackResponse("/chat.postMessage", {
|
|
223
|
+
ok: false,
|
|
224
|
+
error: "channel_not_found",
|
|
225
|
+
status: 200,
|
|
226
|
+
});
|
|
227
|
+
const result = await harness.facade.write({ kind: "slack:postMessage", channel: "C-bad", text: "fail test" }, "sync");
|
|
228
|
+
expect(result).toMatchObject({
|
|
229
|
+
ok: false,
|
|
230
|
+
mode: "sync",
|
|
231
|
+
error: "channel_not_found",
|
|
232
|
+
});
|
|
233
|
+
expect(result.providerPath).toMatch(/^\/slack\/channels\/C-bad\/messages\/\d+\.json$/);
|
|
234
|
+
expect(harness.overlay.search({ provider: "slack", pathPrefix: "/slack/channels/C-bad/" })).toHaveLength(0);
|
|
235
|
+
});
|
|
236
|
+
it("fire-and-forget status transitions pending to settled", async () => {
|
|
237
|
+
vi.useRealTimers();
|
|
238
|
+
harness.mockCloudProxy.setSlackResponse("/chat.postMessage", {
|
|
239
|
+
body: { ok: true, ts: "3333.4444" },
|
|
240
|
+
delayMs: 100,
|
|
241
|
+
});
|
|
242
|
+
const result = await harness.facade.write({ kind: "slack:postMessage", channel: "C-faf", text: "faf test" }, "fire-and-forget");
|
|
243
|
+
expect(result).toMatchObject({
|
|
244
|
+
ok: true,
|
|
245
|
+
mode: "fire-and-forget",
|
|
246
|
+
actionId: expect.any(String),
|
|
247
|
+
});
|
|
248
|
+
const pendingStatus = harness.facade.getStatus(result.actionId);
|
|
249
|
+
expect(pendingStatus).toMatchObject({
|
|
250
|
+
actionId: result.actionId,
|
|
251
|
+
state: "pending",
|
|
252
|
+
});
|
|
253
|
+
const settledStatus = await waitForSettledStatus(result.actionId);
|
|
254
|
+
expect(settledStatus).toMatchObject({
|
|
255
|
+
actionId: result.actionId,
|
|
256
|
+
state: "success",
|
|
257
|
+
result: {
|
|
258
|
+
ok: true,
|
|
259
|
+
mode: "fire-and-forget",
|
|
260
|
+
providerPath: "/slack/channels/C-faf/messages/3333.4444.json",
|
|
261
|
+
},
|
|
262
|
+
});
|
|
263
|
+
expect(settledStatus.result?.data).toMatchObject({ ts: "3333.4444" });
|
|
264
|
+
});
|
|
265
|
+
it("fire-and-forget async failure surfaces in status", async () => {
|
|
266
|
+
vi.useRealTimers();
|
|
267
|
+
harness.mockCloudProxy.setSlackResponse("/chat.postMessage", {
|
|
268
|
+
ok: false,
|
|
269
|
+
error: "network failure",
|
|
270
|
+
status: 500,
|
|
271
|
+
});
|
|
272
|
+
const result = await harness.facade.write({ kind: "slack:postMessage", channel: "C-faf-fail", text: "will fail" }, "fire-and-forget");
|
|
273
|
+
expect(result).toMatchObject({
|
|
274
|
+
ok: true,
|
|
275
|
+
mode: "fire-and-forget",
|
|
276
|
+
});
|
|
277
|
+
const settledStatus = await waitForSettledStatus(result.actionId);
|
|
278
|
+
expect(settledStatus.state).toBe("failed");
|
|
279
|
+
expect(settledStatus.result).toMatchObject({
|
|
280
|
+
ok: false,
|
|
281
|
+
mode: "fire-and-forget",
|
|
282
|
+
});
|
|
283
|
+
expect(settledStatus.result?.error).toEqual(expect.stringContaining("network failure"));
|
|
284
|
+
});
|
|
285
|
+
});
|
|
286
|
+
describe("concurrency", () => {
|
|
287
|
+
it("concurrent writes to same path last-write-wins", async () => {
|
|
288
|
+
vi.useRealTimers();
|
|
289
|
+
harness.mockCloudProxy.setSlackResponse("/chat.postMessage", {
|
|
290
|
+
body: { ok: true, ts: "5555.6666" },
|
|
291
|
+
delayMs: 80,
|
|
292
|
+
});
|
|
293
|
+
const writes = await Promise.all([
|
|
294
|
+
harness.facade.write({ kind: "slack:postMessage", channel: "C-race", text: "write-1" }, "sync"),
|
|
295
|
+
(async () => {
|
|
296
|
+
await delay(20);
|
|
297
|
+
return harness.facade.write({ kind: "slack:postMessage", channel: "C-race", text: "write-2" }, "sync");
|
|
298
|
+
})(),
|
|
299
|
+
]);
|
|
300
|
+
expect(writes).toEqual([
|
|
301
|
+
expect.objectContaining({ ok: true, providerPath: "/slack/channels/C-race/messages/5555.6666.json" }),
|
|
302
|
+
expect.objectContaining({ ok: true, providerPath: "/slack/channels/C-race/messages/5555.6666.json" }),
|
|
303
|
+
]);
|
|
304
|
+
const entries = harness.overlay.search({
|
|
305
|
+
provider: "slack",
|
|
306
|
+
pathPrefix: "/slack/channels/C-race/messages/",
|
|
307
|
+
});
|
|
308
|
+
expect(entries).toHaveLength(1);
|
|
309
|
+
expect(entries[0]).toMatchObject({
|
|
310
|
+
path: "/slack/channels/C-race/messages/5555.6666.json",
|
|
311
|
+
data: {
|
|
312
|
+
channel: "C-race",
|
|
313
|
+
text: "write-2",
|
|
314
|
+
ts: "5555.6666",
|
|
315
|
+
},
|
|
316
|
+
});
|
|
317
|
+
const requests = harness.mockCloudProxy
|
|
318
|
+
.getRequestLog()
|
|
319
|
+
.filter((entry) => entry.provider === "slack" && entry.endpoint === "/chat.postMessage");
|
|
320
|
+
expect(requests).toHaveLength(2);
|
|
321
|
+
expect(requests[0].timestamp).toBeLessThanOrEqual(requests[1].timestamp);
|
|
322
|
+
expect(entries[0].writtenAt).toBeGreaterThanOrEqual(requests[1].timestamp);
|
|
323
|
+
});
|
|
324
|
+
it("read during in-flight fire-and-forget", async () => {
|
|
325
|
+
vi.useRealTimers();
|
|
326
|
+
harness.mockCloudProxy.setSlackResponse("/chat.postMessage", {
|
|
327
|
+
body: { ok: true, ts: "7777.8888" },
|
|
328
|
+
delayMs: 200,
|
|
329
|
+
});
|
|
330
|
+
const writeResult = await harness.facade.write({ kind: "slack:postMessage", channel: "C-inflight", text: "in-flight" }, "fire-and-forget");
|
|
331
|
+
expect(writeResult).toMatchObject({
|
|
332
|
+
ok: true,
|
|
333
|
+
mode: "fire-and-forget",
|
|
334
|
+
});
|
|
335
|
+
const entries = harness.overlay.search({
|
|
336
|
+
provider: "slack",
|
|
337
|
+
pathPrefix: "/slack/channels/C-inflight/",
|
|
338
|
+
});
|
|
339
|
+
expect(entries.length).toBeGreaterThanOrEqual(1);
|
|
340
|
+
expect(entries[0].data).toMatchObject({
|
|
341
|
+
channel: "C-inflight",
|
|
342
|
+
text: "in-flight",
|
|
343
|
+
pending: true,
|
|
344
|
+
});
|
|
345
|
+
const settledStatus = await waitForSettledStatus(writeResult.actionId);
|
|
346
|
+
expect(settledStatus.state).toBe("success");
|
|
347
|
+
expect(settledStatus.result?.providerPath).toBe("/slack/channels/C-inflight/messages/7777.8888.json");
|
|
348
|
+
});
|
|
349
|
+
it("parallel reads of same overlay entry", async () => {
|
|
350
|
+
harness.overlay.record({
|
|
351
|
+
path: "/parallel/read.json",
|
|
352
|
+
data: { value: "shared" },
|
|
353
|
+
provider: "slack",
|
|
354
|
+
action: "test",
|
|
355
|
+
});
|
|
356
|
+
const results = await Promise.all([
|
|
357
|
+
harness.reader.readFileEnveloped("/parallel/read.json"),
|
|
358
|
+
harness.reader.readFileEnveloped("/parallel/read.json"),
|
|
359
|
+
harness.reader.readFileEnveloped("/parallel/read.json"),
|
|
360
|
+
]);
|
|
361
|
+
for (const envelope of results) {
|
|
362
|
+
expectEnvelopeMeta(envelope, "hit", "overlay");
|
|
363
|
+
expect(envelope.data).toEqual(expect.stringContaining("shared"));
|
|
364
|
+
}
|
|
365
|
+
});
|
|
366
|
+
});
|
|
367
|
+
describe("graceful degradation", () => {
|
|
368
|
+
it("overlay lookup error yields error envelope", async () => {
|
|
369
|
+
harness.overlay.record({
|
|
370
|
+
path: "/poisoned/file.json",
|
|
371
|
+
data: { x: 1 },
|
|
372
|
+
provider: "slack",
|
|
373
|
+
action: "test",
|
|
374
|
+
});
|
|
375
|
+
const originalLookup = harness.overlay.lookup.bind(harness.overlay);
|
|
376
|
+
harness.overlay.lookup = () => {
|
|
377
|
+
throw new Error("overlay corrupted");
|
|
378
|
+
};
|
|
379
|
+
try {
|
|
380
|
+
const envelope = await harness.reader.readFileEnveloped("/poisoned/file.json");
|
|
381
|
+
expectEnvelopeMeta(envelope, "error", "overlay");
|
|
382
|
+
expect(envelope.data).toBeNull();
|
|
383
|
+
expect(envelope.error).toEqual(expect.stringContaining("overlay corrupted"));
|
|
384
|
+
}
|
|
385
|
+
finally {
|
|
386
|
+
harness.overlay.lookup = originalLookup;
|
|
387
|
+
}
|
|
388
|
+
});
|
|
389
|
+
it("fire-and-forget handler throw is captured", async () => {
|
|
390
|
+
vi.useRealTimers();
|
|
391
|
+
replaceSlackEgress({
|
|
392
|
+
postMessage: () => {
|
|
393
|
+
throw new Error("sync kaboom");
|
|
394
|
+
},
|
|
395
|
+
addReaction: async () => undefined,
|
|
396
|
+
});
|
|
397
|
+
const result = await harness.facade.write({ kind: "slack:postMessage", channel: "C-throw", text: "boom" }, "fire-and-forget");
|
|
398
|
+
expect(result).toMatchObject({
|
|
399
|
+
ok: true,
|
|
400
|
+
mode: "fire-and-forget",
|
|
401
|
+
});
|
|
402
|
+
const status = await waitForSettledStatus(result.actionId);
|
|
403
|
+
expect(status.state).toBe("failed");
|
|
404
|
+
expect(status.result).toMatchObject({
|
|
405
|
+
ok: false,
|
|
406
|
+
mode: "fire-and-forget",
|
|
407
|
+
});
|
|
408
|
+
expect(status.result?.error).toEqual(expect.stringContaining("sync kaboom"));
|
|
409
|
+
});
|
|
410
|
+
});
|
|
411
|
+
});
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@agentworkforce/sage",
|
|
3
|
-
"version": "1.
|
|
3
|
+
"version": "1.2.0",
|
|
4
4
|
"repository": {
|
|
5
5
|
"type": "git",
|
|
6
6
|
"url": "git+https://github.com/AgentWorkforce/sage.git"
|
|
@@ -22,7 +22,7 @@
|
|
|
22
22
|
"dev": "tsx watch --env-file=.env src/dev-server.ts",
|
|
23
23
|
"clean": "node --input-type=module -e \"import { rmSync } from 'node:fs'; rmSync('dist', { recursive: true, force: true });\"",
|
|
24
24
|
"build": "npm run clean && tsc -p tsconfig.build.json",
|
|
25
|
-
"test": "vitest run",
|
|
25
|
+
"test": "npm run build --workspace @agentworkforce/sage-client && vitest run --workspace vitest.workspace.ts",
|
|
26
26
|
"prepack": "npm run build",
|
|
27
27
|
"workflows:cli-mac": "node scripts/run-cli-mac-workflows.mjs",
|
|
28
28
|
"workflows:cli-mac:dry": "node scripts/run-cli-mac-workflows.mjs --dry-run",
|
|
@@ -31,13 +31,16 @@
|
|
|
31
31
|
"workflows:qa-meta-master": "agent-relay run workflows/v4/34-qa-meta-master-executor.ts"
|
|
32
32
|
},
|
|
33
33
|
"dependencies": {
|
|
34
|
-
"@agent-assistant/core": "^0.
|
|
35
|
-
"@agent-assistant/
|
|
34
|
+
"@agent-assistant/core": "^0.2.0",
|
|
35
|
+
"@agent-assistant/surfaces": "^0.2.0",
|
|
36
|
+
"@agent-assistant/traits": "^0.2.0",
|
|
36
37
|
"@agent-relay/memory": "^4.0.0",
|
|
37
38
|
"@agent-relay/sdk": "^4.0.0",
|
|
38
39
|
"@agentcron/sdk": "^0.1.0",
|
|
39
40
|
"@nangohq/node": "^0.42.0",
|
|
40
41
|
"@nangohq/types": "^0.69.48",
|
|
42
|
+
"@relayfile/adapter-linear": "^0.1.3",
|
|
43
|
+
"@relayfile/adapter-notion": "^0.1.2",
|
|
41
44
|
"@relayfile/adapter-slack": "^0.1.3",
|
|
42
45
|
"@relayfile/provider-nango": "^0.2.1",
|
|
43
46
|
"agent-trajectories": "^0.5.0",
|
|
@@ -46,7 +49,9 @@
|
|
|
46
49
|
"devDependencies": {
|
|
47
50
|
"@cloudflare/workers-types": "^4.20250408.0",
|
|
48
51
|
"@hono/node-server": "^1.0.0",
|
|
52
|
+
"@types/express": "^5.0.6",
|
|
49
53
|
"@types/node": "^20.0.0",
|
|
54
|
+
"express": "^5.2.1",
|
|
50
55
|
"tsx": "^4.0.0",
|
|
51
56
|
"typescript": "^5.7.0",
|
|
52
57
|
"vitest": "^2.0.0"
|