@wingman-ai/gateway 0.2.4 → 0.3.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/.wingman/agents/coding/agent.md +5 -0
- package/.wingman/agents/coding-v2/agent.md +58 -0
- package/.wingman/agents/game-dev/agent.md +94 -0
- package/.wingman/agents/game-dev/art-generation.md +37 -0
- package/.wingman/agents/game-dev/asset-refinement.md +17 -0
- package/.wingman/agents/game-dev/planning-idea.md +17 -0
- package/.wingman/agents/game-dev/ui-specialist.md +17 -0
- package/.wingman/agents/main/agent.md +2 -0
- package/README.md +1 -0
- package/dist/agent/config/agentConfig.d.ts +4 -0
- package/dist/agent/config/mcpClientManager.cjs +44 -10
- package/dist/agent/config/mcpClientManager.d.ts +6 -2
- package/dist/agent/config/mcpClientManager.js +44 -10
- package/dist/agent/config/toolRegistry.cjs +3 -1
- package/dist/agent/config/toolRegistry.js +3 -1
- package/dist/agent/tests/mcpClientManager.test.cjs +124 -0
- package/dist/agent/tests/mcpClientManager.test.d.ts +1 -0
- package/dist/agent/tests/mcpClientManager.test.js +118 -0
- package/dist/agent/tools/command_execute.cjs +1 -1
- package/dist/agent/tools/command_execute.js +1 -1
- package/dist/cli/config/schema.d.ts +2 -0
- package/dist/cli/core/agentInvoker.cjs +55 -66
- package/dist/cli/core/agentInvoker.d.ts +10 -13
- package/dist/cli/core/agentInvoker.js +42 -62
- package/dist/cli/core/imagePersistence.cjs +125 -0
- package/dist/cli/core/imagePersistence.d.ts +24 -0
- package/dist/cli/core/imagePersistence.js +85 -0
- package/dist/cli/core/sessionManager.cjs +297 -40
- package/dist/cli/core/sessionManager.d.ts +9 -0
- package/dist/cli/core/sessionManager.js +297 -40
- package/dist/debug/terminalProbe.cjs +57 -0
- package/dist/debug/terminalProbe.d.ts +10 -0
- package/dist/debug/terminalProbe.js +20 -0
- package/dist/debug/terminalProbeAuth.cjs +140 -0
- package/dist/debug/terminalProbeAuth.d.ts +20 -0
- package/dist/debug/terminalProbeAuth.js +97 -0
- package/dist/gateway/http/fs.cjs +19 -0
- package/dist/gateway/http/fs.js +19 -0
- package/dist/gateway/http/sessions.cjs +25 -5
- package/dist/gateway/http/sessions.js +25 -5
- package/dist/gateway/server.cjs +112 -11
- package/dist/gateway/server.d.ts +2 -0
- package/dist/gateway/server.js +112 -11
- package/dist/providers/codex.cjs +230 -37
- package/dist/providers/codex.d.ts +2 -0
- package/dist/providers/codex.js +231 -38
- package/dist/tests/agentInvokerSummarization.test.cjs +56 -37
- package/dist/tests/agentInvokerSummarization.test.js +58 -39
- package/dist/tests/agentInvokerWorkdir.test.cjs +50 -0
- package/dist/tests/agentInvokerWorkdir.test.js +52 -2
- package/dist/tests/cli-init.test.cjs +36 -0
- package/dist/tests/cli-init.test.js +36 -0
- package/dist/tests/codex-provider.test.cjs +173 -0
- package/dist/tests/codex-provider.test.js +174 -1
- package/dist/tests/falRuntime.test.cjs +78 -0
- package/dist/tests/falRuntime.test.d.ts +1 -0
- package/dist/tests/falRuntime.test.js +72 -0
- package/dist/tests/falSummary.test.cjs +51 -0
- package/dist/tests/falSummary.test.d.ts +1 -0
- package/dist/tests/falSummary.test.js +45 -0
- package/dist/tests/gateway.test.cjs +109 -1
- package/dist/tests/gateway.test.js +109 -1
- package/dist/tests/imagePersistence.test.cjs +143 -0
- package/dist/tests/imagePersistence.test.d.ts +1 -0
- package/dist/tests/imagePersistence.test.js +137 -0
- package/dist/tests/sessionMessageAttachments.test.cjs +30 -0
- package/dist/tests/sessionMessageAttachments.test.js +30 -0
- package/dist/tests/sessionStateMessages.test.cjs +126 -0
- package/dist/tests/sessionStateMessages.test.js +126 -0
- package/dist/tests/sessions-api.test.cjs +117 -3
- package/dist/tests/sessions-api.test.js +118 -4
- package/dist/tests/terminalProbe.test.cjs +45 -0
- package/dist/tests/terminalProbe.test.d.ts +1 -0
- package/dist/tests/terminalProbe.test.js +39 -0
- package/dist/tests/terminalProbeAuth.test.cjs +85 -0
- package/dist/tests/terminalProbeAuth.test.d.ts +1 -0
- package/dist/tests/terminalProbeAuth.test.js +79 -0
- package/dist/tools/fal/runtime.cjs +103 -0
- package/dist/tools/fal/runtime.d.ts +10 -0
- package/dist/tools/fal/runtime.js +60 -0
- package/dist/tools/fal/summary.cjs +78 -0
- package/dist/tools/fal/summary.d.ts +22 -0
- package/dist/tools/fal/summary.js +41 -0
- package/dist/tools/mcp-fal-ai.cjs +1041 -0
- package/dist/tools/mcp-fal-ai.d.ts +1 -0
- package/dist/tools/mcp-fal-ai.js +1025 -0
- package/dist/types/mcp.cjs +2 -0
- package/dist/types/mcp.d.ts +8 -0
- package/dist/types/mcp.js +3 -1
- package/dist/webui/assets/index-0nUBsUUq.js +278 -0
- package/dist/webui/assets/index-kk7OrD-G.css +11 -0
- package/dist/webui/index.html +2 -2
- package/package.json +16 -13
- package/dist/webui/assets/index-DVWQluit.css +0 -11
- package/dist/webui/assets/index-Dlyzwalc.js +0 -270
|
@@ -0,0 +1,51 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
var __webpack_exports__ = {};
|
|
3
|
+
const external_vitest_namespaceObject = require("vitest");
|
|
4
|
+
const summary_cjs_namespaceObject = require("../tools/fal/summary.cjs");
|
|
5
|
+
(0, external_vitest_namespaceObject.describe)("fal summary", ()=>{
|
|
6
|
+
(0, external_vitest_namespaceObject.it)("formats summary text with status, model, and files", ()=>{
|
|
7
|
+
const summary = (0, summary_cjs_namespaceObject.buildFalGenerationSummary)({
|
|
8
|
+
toolName: "generate_image_or_texture",
|
|
9
|
+
jobId: "job-123",
|
|
10
|
+
status: "completed",
|
|
11
|
+
modelId: "fal-ai/nano-banana-pro",
|
|
12
|
+
reviewState: "accepted",
|
|
13
|
+
media: [
|
|
14
|
+
{
|
|
15
|
+
modality: "image",
|
|
16
|
+
path: "/repo/apps/wingman/generated/images/asset.png",
|
|
17
|
+
mimeType: "image/png"
|
|
18
|
+
}
|
|
19
|
+
],
|
|
20
|
+
cwd: "/repo/apps/wingman"
|
|
21
|
+
});
|
|
22
|
+
(0, external_vitest_namespaceObject.expect)(summary).toContain("FAL job `job-123`");
|
|
23
|
+
(0, external_vitest_namespaceObject.expect)(summary).toContain("**completed**");
|
|
24
|
+
(0, external_vitest_namespaceObject.expect)(summary).toContain("- Model: `fal-ai/nano-banana-pro`");
|
|
25
|
+
(0, external_vitest_namespaceObject.expect)(summary).toContain("- Assets: 1");
|
|
26
|
+
(0, external_vitest_namespaceObject.expect)(summary).toContain("[image] ./generated/images/asset.png image/png");
|
|
27
|
+
});
|
|
28
|
+
(0, external_vitest_namespaceObject.it)("includes review instruction when pending", ()=>{
|
|
29
|
+
const summary = (0, summary_cjs_namespaceObject.buildFalGenerationSummary)({
|
|
30
|
+
toolName: "generate_video_from_image",
|
|
31
|
+
jobId: "job-456",
|
|
32
|
+
status: "awaiting_review",
|
|
33
|
+
modelId: "fal-ai/kling-video/o3/standard/image-to-video",
|
|
34
|
+
reviewState: "pending",
|
|
35
|
+
media: []
|
|
36
|
+
});
|
|
37
|
+
(0, external_vitest_namespaceObject.expect)(summary).toContain("Review required");
|
|
38
|
+
(0, external_vitest_namespaceObject.expect)(summary).toContain("fal_generation_status");
|
|
39
|
+
});
|
|
40
|
+
(0, external_vitest_namespaceObject.it)("formats home paths with tilde when outside cwd", ()=>{
|
|
41
|
+
const formatted = (0, summary_cjs_namespaceObject.formatFalPath)("/Users/demo/Projects/output.mp4", {
|
|
42
|
+
cwd: "/repo/apps/wingman",
|
|
43
|
+
homeDir: "/Users/demo"
|
|
44
|
+
});
|
|
45
|
+
(0, external_vitest_namespaceObject.expect)(formatted).toBe("~/Projects/output.mp4");
|
|
46
|
+
});
|
|
47
|
+
});
|
|
48
|
+
for(var __rspack_i in __webpack_exports__)exports[__rspack_i] = __webpack_exports__[__rspack_i];
|
|
49
|
+
Object.defineProperty(exports, '__esModule', {
|
|
50
|
+
value: true
|
|
51
|
+
});
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
export {};
|
|
@@ -0,0 +1,45 @@
|
|
|
1
|
+
import { describe, expect, it } from "vitest";
|
|
2
|
+
import { buildFalGenerationSummary, formatFalPath } from "../tools/fal/summary.js";
|
|
3
|
+
describe("fal summary", ()=>{
|
|
4
|
+
it("formats summary text with status, model, and files", ()=>{
|
|
5
|
+
const summary = buildFalGenerationSummary({
|
|
6
|
+
toolName: "generate_image_or_texture",
|
|
7
|
+
jobId: "job-123",
|
|
8
|
+
status: "completed",
|
|
9
|
+
modelId: "fal-ai/nano-banana-pro",
|
|
10
|
+
reviewState: "accepted",
|
|
11
|
+
media: [
|
|
12
|
+
{
|
|
13
|
+
modality: "image",
|
|
14
|
+
path: "/repo/apps/wingman/generated/images/asset.png",
|
|
15
|
+
mimeType: "image/png"
|
|
16
|
+
}
|
|
17
|
+
],
|
|
18
|
+
cwd: "/repo/apps/wingman"
|
|
19
|
+
});
|
|
20
|
+
expect(summary).toContain("FAL job `job-123`");
|
|
21
|
+
expect(summary).toContain("**completed**");
|
|
22
|
+
expect(summary).toContain("- Model: `fal-ai/nano-banana-pro`");
|
|
23
|
+
expect(summary).toContain("- Assets: 1");
|
|
24
|
+
expect(summary).toContain("[image] ./generated/images/asset.png image/png");
|
|
25
|
+
});
|
|
26
|
+
it("includes review instruction when pending", ()=>{
|
|
27
|
+
const summary = buildFalGenerationSummary({
|
|
28
|
+
toolName: "generate_video_from_image",
|
|
29
|
+
jobId: "job-456",
|
|
30
|
+
status: "awaiting_review",
|
|
31
|
+
modelId: "fal-ai/kling-video/o3/standard/image-to-video",
|
|
32
|
+
reviewState: "pending",
|
|
33
|
+
media: []
|
|
34
|
+
});
|
|
35
|
+
expect(summary).toContain("Review required");
|
|
36
|
+
expect(summary).toContain("fal_generation_status");
|
|
37
|
+
});
|
|
38
|
+
it("formats home paths with tilde when outside cwd", ()=>{
|
|
39
|
+
const formatted = formatFalPath("/Users/demo/Projects/output.mp4", {
|
|
40
|
+
cwd: "/repo/apps/wingman",
|
|
41
|
+
homeDir: "/Users/demo"
|
|
42
|
+
});
|
|
43
|
+
expect(formatted).toBe("~/Projects/output.mp4");
|
|
44
|
+
});
|
|
45
|
+
});
|
|
@@ -1,5 +1,8 @@
|
|
|
1
1
|
"use strict";
|
|
2
2
|
var __webpack_exports__ = {};
|
|
3
|
+
const external_node_fs_namespaceObject = require("node:fs");
|
|
4
|
+
const external_node_os_namespaceObject = require("node:os");
|
|
5
|
+
const external_node_path_namespaceObject = require("node:path");
|
|
3
6
|
const external_vitest_namespaceObject = require("vitest");
|
|
4
7
|
const index_cjs_namespaceObject = require("../gateway/index.cjs");
|
|
5
8
|
function _define_property(obj, key, value) {
|
|
@@ -38,6 +41,9 @@ external_vitest_namespaceObject.vi.mock("@/cli/core/agentInvoker.js", ()=>({
|
|
|
38
41
|
cancelled: true
|
|
39
42
|
};
|
|
40
43
|
}
|
|
44
|
+
if ("return-no-event" === content) return {
|
|
45
|
+
streaming: true
|
|
46
|
+
};
|
|
41
47
|
this.outputManager?.emitAgentComplete?.({
|
|
42
48
|
streaming: true
|
|
43
49
|
});
|
|
@@ -54,7 +60,9 @@ external_vitest_namespaceObject.vi.mock("@/cli/core/agentInvoker.js", ()=>({
|
|
|
54
60
|
describeIfBun("Gateway", ()=>{
|
|
55
61
|
let server;
|
|
56
62
|
let port = 0;
|
|
63
|
+
let testWorkspace;
|
|
57
64
|
(0, external_vitest_namespaceObject.beforeAll)(async ()=>{
|
|
65
|
+
testWorkspace = (0, external_node_fs_namespaceObject.mkdtempSync)((0, external_node_path_namespaceObject.join)((0, external_node_os_namespaceObject.tmpdir)(), "wingman-gateway-test-"));
|
|
58
66
|
const instance = new index_cjs_namespaceObject.GatewayServer({
|
|
59
67
|
port: 0,
|
|
60
68
|
host: "localhost",
|
|
@@ -62,7 +70,10 @@ describeIfBun("Gateway", ()=>{
|
|
|
62
70
|
auth: {
|
|
63
71
|
mode: "none"
|
|
64
72
|
},
|
|
65
|
-
logLevel: "silent"
|
|
73
|
+
logLevel: "silent",
|
|
74
|
+
workspace: testWorkspace,
|
|
75
|
+
configDir: ".wingman-test-config",
|
|
76
|
+
stateDir: ".wingman-test-state"
|
|
66
77
|
});
|
|
67
78
|
await instance.start();
|
|
68
79
|
server = instance;
|
|
@@ -72,6 +83,10 @@ describeIfBun("Gateway", ()=>{
|
|
|
72
83
|
});
|
|
73
84
|
(0, external_vitest_namespaceObject.afterAll)(async ()=>{
|
|
74
85
|
if (server) await server.stop();
|
|
86
|
+
if (testWorkspace) (0, external_node_fs_namespaceObject.rmSync)(testWorkspace, {
|
|
87
|
+
recursive: true,
|
|
88
|
+
force: true
|
|
89
|
+
});
|
|
75
90
|
});
|
|
76
91
|
const connectClient = (instanceId, clientType = "test")=>new Promise((resolve, reject)=>{
|
|
77
92
|
const ws = new WebSocket(`ws://localhost:${port}/ws`);
|
|
@@ -117,6 +132,24 @@ describeIfBun("Gateway", ()=>{
|
|
|
117
132
|
};
|
|
118
133
|
ws.addEventListener("message", handler);
|
|
119
134
|
});
|
|
135
|
+
const collectMessages = (ws, predicate, durationMs = 600)=>new Promise((resolve)=>{
|
|
136
|
+
const matches = [];
|
|
137
|
+
const handler = (event)=>{
|
|
138
|
+
let msg;
|
|
139
|
+
try {
|
|
140
|
+
msg = JSON.parse(event.data);
|
|
141
|
+
} catch {
|
|
142
|
+
return;
|
|
143
|
+
}
|
|
144
|
+
if (!predicate(msg)) return;
|
|
145
|
+
matches.push(msg);
|
|
146
|
+
};
|
|
147
|
+
ws.addEventListener("message", handler);
|
|
148
|
+
setTimeout(()=>{
|
|
149
|
+
ws.removeEventListener("message", handler);
|
|
150
|
+
resolve(matches);
|
|
151
|
+
}, durationMs);
|
|
152
|
+
});
|
|
120
153
|
(0, external_vitest_namespaceObject.it)("should start the gateway server", async ()=>{
|
|
121
154
|
const response = await fetch(`http://localhost:${port}/health`);
|
|
122
155
|
(0, external_vitest_namespaceObject.expect)(response.ok).toBe(true);
|
|
@@ -354,6 +387,49 @@ describeIfBun("Gateway", ()=>{
|
|
|
354
387
|
(0, external_vitest_namespaceObject.expect)(errorMsg.payload?.agentId).toBe("main");
|
|
355
388
|
requester.close();
|
|
356
389
|
});
|
|
390
|
+
(0, external_vitest_namespaceObject.it)("should emit agent-complete when invocation returns without terminal output events", async ()=>{
|
|
391
|
+
const requester = await connectClient("session-complete-fallback-requester");
|
|
392
|
+
const requestId = "req-complete-fallback";
|
|
393
|
+
const sessionId = "session-complete-fallback";
|
|
394
|
+
requester.send(JSON.stringify({
|
|
395
|
+
type: "req:agent",
|
|
396
|
+
id: requestId,
|
|
397
|
+
payload: {
|
|
398
|
+
agentId: "main",
|
|
399
|
+
sessionKey: sessionId,
|
|
400
|
+
content: "return-no-event"
|
|
401
|
+
},
|
|
402
|
+
timestamp: Date.now()
|
|
403
|
+
}));
|
|
404
|
+
const completeMsg = await waitForMessage(requester, (msg)=>"event:agent" === msg.type && msg.id === requestId && msg.payload?.type === "agent-complete");
|
|
405
|
+
(0, external_vitest_namespaceObject.expect)(completeMsg.payload?.sessionId).toBe(sessionId);
|
|
406
|
+
(0, external_vitest_namespaceObject.expect)(completeMsg.payload?.agentId).toBe("main");
|
|
407
|
+
(0, external_vitest_namespaceObject.expect)(completeMsg.payload?.result).toEqual({
|
|
408
|
+
streaming: true
|
|
409
|
+
});
|
|
410
|
+
requester.close();
|
|
411
|
+
});
|
|
412
|
+
(0, external_vitest_namespaceObject.it)("should emit a single agent-complete terminal event per request", async ()=>{
|
|
413
|
+
const requester = await connectClient("session-single-complete-requester");
|
|
414
|
+
const requestId = "req-single-complete";
|
|
415
|
+
const sessionId = "session-single-complete";
|
|
416
|
+
const completionEventsPromise = collectMessages(requester, (msg)=>"event:agent" === msg.type && msg.id === requestId && msg.payload?.type === "agent-complete", 1000);
|
|
417
|
+
requester.send(JSON.stringify({
|
|
418
|
+
type: "req:agent",
|
|
419
|
+
id: requestId,
|
|
420
|
+
payload: {
|
|
421
|
+
agentId: "main",
|
|
422
|
+
sessionKey: sessionId,
|
|
423
|
+
content: "single-complete"
|
|
424
|
+
},
|
|
425
|
+
timestamp: Date.now()
|
|
426
|
+
}));
|
|
427
|
+
const completionEvents = await completionEventsPromise;
|
|
428
|
+
(0, external_vitest_namespaceObject.expect)(completionEvents).toHaveLength(1);
|
|
429
|
+
(0, external_vitest_namespaceObject.expect)(completionEvents[0].payload?.sessionId).toBe(sessionId);
|
|
430
|
+
(0, external_vitest_namespaceObject.expect)(completionEvents[0].payload?.agentId).toBe("main");
|
|
431
|
+
requester.close();
|
|
432
|
+
});
|
|
357
433
|
(0, external_vitest_namespaceObject.it)("should cancel an in-flight agent request", async ()=>{
|
|
358
434
|
const requester = await connectClient("session-cancel-requester");
|
|
359
435
|
const requestId = "req-cancel-test";
|
|
@@ -460,6 +536,9 @@ describeIfBun("Gateway", ()=>{
|
|
|
460
536
|
}));
|
|
461
537
|
const cancelAck = await waitForMessage(requester, (msg)=>"ack" === msg.type && msg.payload?.action === "req:agent:cancel" && msg.payload?.requestId === secondRequestId && msg.payload?.status === "cancelled_queued", 10000);
|
|
462
538
|
(0, external_vitest_namespaceObject.expect)(cancelAck.payload?.status).toBe("cancelled_queued");
|
|
539
|
+
const cancelEvent = await waitForMessage(requester, (msg)=>"event:agent" === msg.type && msg.id === secondRequestId && msg.payload?.type === "agent-error" && /cancel/i.test(String(msg.payload?.error || "")), 10000);
|
|
540
|
+
(0, external_vitest_namespaceObject.expect)(cancelEvent.payload?.sessionId).toBe(sessionId);
|
|
541
|
+
(0, external_vitest_namespaceObject.expect)(cancelEvent.payload?.agentId).toBe("main");
|
|
463
542
|
await waitForMessage(requester, (msg)=>"event:agent" === msg.type && msg.id === firstRequestId && msg.payload?.type === "agent-complete", 10000);
|
|
464
543
|
const queuedRequests = server.queuedSessionRequests;
|
|
465
544
|
const isStillQueued = [
|
|
@@ -496,6 +575,35 @@ describeIfBun("Gateway", ()=>{
|
|
|
496
575
|
(0, external_vitest_namespaceObject.expect)(updated?.messageCount).toBe(0);
|
|
497
576
|
(0, external_vitest_namespaceObject.expect)(updated?.lastMessagePreview).toBeNull();
|
|
498
577
|
});
|
|
578
|
+
(0, external_vitest_namespaceObject.it)("persists failed first-turn messages so the thread survives reload", async ()=>{
|
|
579
|
+
const requester = await connectClient("persist-failed-turn-requester");
|
|
580
|
+
const sessionId = `session-persist-failed-${Date.now()}`;
|
|
581
|
+
const requestId = `req-persist-failed-${Date.now()}`;
|
|
582
|
+
requester.send(JSON.stringify({
|
|
583
|
+
type: "req:agent",
|
|
584
|
+
id: requestId,
|
|
585
|
+
payload: {
|
|
586
|
+
agentId: "main",
|
|
587
|
+
sessionKey: sessionId,
|
|
588
|
+
content: "throw-no-event"
|
|
589
|
+
},
|
|
590
|
+
timestamp: Date.now()
|
|
591
|
+
}));
|
|
592
|
+
await waitForMessage(requester, (msg)=>"event:agent" === msg.type && msg.id === requestId && msg.payload?.type === "agent-error", 10000);
|
|
593
|
+
const sessionsRes = await fetch(`http://localhost:${port}/api/sessions?limit=100`);
|
|
594
|
+
(0, external_vitest_namespaceObject.expect)(sessionsRes.ok).toBe(true);
|
|
595
|
+
const sessions = await sessionsRes.json();
|
|
596
|
+
const created = sessions.find((session)=>session.id === sessionId);
|
|
597
|
+
(0, external_vitest_namespaceObject.expect)(created).toBeTruthy();
|
|
598
|
+
(0, external_vitest_namespaceObject.expect)(created?.messageCount).toBe(1);
|
|
599
|
+
const messagesRes = await fetch(`http://localhost:${port}/api/sessions/${encodeURIComponent(sessionId)}/messages?agentId=main`);
|
|
600
|
+
(0, external_vitest_namespaceObject.expect)(messagesRes.ok).toBe(true);
|
|
601
|
+
const messages = await messagesRes.json();
|
|
602
|
+
(0, external_vitest_namespaceObject.expect)(messages.some((message)=>"user" === message.role)).toBe(true);
|
|
603
|
+
(0, external_vitest_namespaceObject.expect)(messages.some((message)=>"user" === message.role && message.content.includes("throw-no-event"))).toBe(true);
|
|
604
|
+
(0, external_vitest_namespaceObject.expect)(messages.some((message)=>"assistant" === message.role && message.content.includes("Synthetic invocation failure"))).toBe(true);
|
|
605
|
+
requester.close();
|
|
606
|
+
});
|
|
499
607
|
});
|
|
500
608
|
for(var __rspack_i in __webpack_exports__)exports[__rspack_i] = __webpack_exports__[__rspack_i];
|
|
501
609
|
Object.defineProperty(exports, '__esModule', {
|
|
@@ -1,3 +1,6 @@
|
|
|
1
|
+
import { mkdtempSync, rmSync } from "node:fs";
|
|
2
|
+
import { tmpdir } from "node:os";
|
|
3
|
+
import { join } from "node:path";
|
|
1
4
|
import { afterAll, beforeAll, describe, expect, it, vi } from "vitest";
|
|
2
5
|
import { GatewayClient, GatewayServer } from "../gateway/index.js";
|
|
3
6
|
function _define_property(obj, key, value) {
|
|
@@ -36,6 +39,9 @@ vi.mock("@/cli/core/agentInvoker.js", ()=>({
|
|
|
36
39
|
cancelled: true
|
|
37
40
|
};
|
|
38
41
|
}
|
|
42
|
+
if ("return-no-event" === content) return {
|
|
43
|
+
streaming: true
|
|
44
|
+
};
|
|
39
45
|
this.outputManager?.emitAgentComplete?.({
|
|
40
46
|
streaming: true
|
|
41
47
|
});
|
|
@@ -52,7 +58,9 @@ vi.mock("@/cli/core/agentInvoker.js", ()=>({
|
|
|
52
58
|
describeIfBun("Gateway", ()=>{
|
|
53
59
|
let server;
|
|
54
60
|
let port = 0;
|
|
61
|
+
let testWorkspace;
|
|
55
62
|
beforeAll(async ()=>{
|
|
63
|
+
testWorkspace = mkdtempSync(join(tmpdir(), "wingman-gateway-test-"));
|
|
56
64
|
const instance = new GatewayServer({
|
|
57
65
|
port: 0,
|
|
58
66
|
host: "localhost",
|
|
@@ -60,7 +68,10 @@ describeIfBun("Gateway", ()=>{
|
|
|
60
68
|
auth: {
|
|
61
69
|
mode: "none"
|
|
62
70
|
},
|
|
63
|
-
logLevel: "silent"
|
|
71
|
+
logLevel: "silent",
|
|
72
|
+
workspace: testWorkspace,
|
|
73
|
+
configDir: ".wingman-test-config",
|
|
74
|
+
stateDir: ".wingman-test-state"
|
|
64
75
|
});
|
|
65
76
|
await instance.start();
|
|
66
77
|
server = instance;
|
|
@@ -70,6 +81,10 @@ describeIfBun("Gateway", ()=>{
|
|
|
70
81
|
});
|
|
71
82
|
afterAll(async ()=>{
|
|
72
83
|
if (server) await server.stop();
|
|
84
|
+
if (testWorkspace) rmSync(testWorkspace, {
|
|
85
|
+
recursive: true,
|
|
86
|
+
force: true
|
|
87
|
+
});
|
|
73
88
|
});
|
|
74
89
|
const connectClient = (instanceId, clientType = "test")=>new Promise((resolve, reject)=>{
|
|
75
90
|
const ws = new WebSocket(`ws://localhost:${port}/ws`);
|
|
@@ -115,6 +130,24 @@ describeIfBun("Gateway", ()=>{
|
|
|
115
130
|
};
|
|
116
131
|
ws.addEventListener("message", handler);
|
|
117
132
|
});
|
|
133
|
+
const collectMessages = (ws, predicate, durationMs = 600)=>new Promise((resolve)=>{
|
|
134
|
+
const matches = [];
|
|
135
|
+
const handler = (event)=>{
|
|
136
|
+
let msg;
|
|
137
|
+
try {
|
|
138
|
+
msg = JSON.parse(event.data);
|
|
139
|
+
} catch {
|
|
140
|
+
return;
|
|
141
|
+
}
|
|
142
|
+
if (!predicate(msg)) return;
|
|
143
|
+
matches.push(msg);
|
|
144
|
+
};
|
|
145
|
+
ws.addEventListener("message", handler);
|
|
146
|
+
setTimeout(()=>{
|
|
147
|
+
ws.removeEventListener("message", handler);
|
|
148
|
+
resolve(matches);
|
|
149
|
+
}, durationMs);
|
|
150
|
+
});
|
|
118
151
|
it("should start the gateway server", async ()=>{
|
|
119
152
|
const response = await fetch(`http://localhost:${port}/health`);
|
|
120
153
|
expect(response.ok).toBe(true);
|
|
@@ -352,6 +385,49 @@ describeIfBun("Gateway", ()=>{
|
|
|
352
385
|
expect(errorMsg.payload?.agentId).toBe("main");
|
|
353
386
|
requester.close();
|
|
354
387
|
});
|
|
388
|
+
it("should emit agent-complete when invocation returns without terminal output events", async ()=>{
|
|
389
|
+
const requester = await connectClient("session-complete-fallback-requester");
|
|
390
|
+
const requestId = "req-complete-fallback";
|
|
391
|
+
const sessionId = "session-complete-fallback";
|
|
392
|
+
requester.send(JSON.stringify({
|
|
393
|
+
type: "req:agent",
|
|
394
|
+
id: requestId,
|
|
395
|
+
payload: {
|
|
396
|
+
agentId: "main",
|
|
397
|
+
sessionKey: sessionId,
|
|
398
|
+
content: "return-no-event"
|
|
399
|
+
},
|
|
400
|
+
timestamp: Date.now()
|
|
401
|
+
}));
|
|
402
|
+
const completeMsg = await waitForMessage(requester, (msg)=>"event:agent" === msg.type && msg.id === requestId && msg.payload?.type === "agent-complete");
|
|
403
|
+
expect(completeMsg.payload?.sessionId).toBe(sessionId);
|
|
404
|
+
expect(completeMsg.payload?.agentId).toBe("main");
|
|
405
|
+
expect(completeMsg.payload?.result).toEqual({
|
|
406
|
+
streaming: true
|
|
407
|
+
});
|
|
408
|
+
requester.close();
|
|
409
|
+
});
|
|
410
|
+
it("should emit a single agent-complete terminal event per request", async ()=>{
|
|
411
|
+
const requester = await connectClient("session-single-complete-requester");
|
|
412
|
+
const requestId = "req-single-complete";
|
|
413
|
+
const sessionId = "session-single-complete";
|
|
414
|
+
const completionEventsPromise = collectMessages(requester, (msg)=>"event:agent" === msg.type && msg.id === requestId && msg.payload?.type === "agent-complete", 1000);
|
|
415
|
+
requester.send(JSON.stringify({
|
|
416
|
+
type: "req:agent",
|
|
417
|
+
id: requestId,
|
|
418
|
+
payload: {
|
|
419
|
+
agentId: "main",
|
|
420
|
+
sessionKey: sessionId,
|
|
421
|
+
content: "single-complete"
|
|
422
|
+
},
|
|
423
|
+
timestamp: Date.now()
|
|
424
|
+
}));
|
|
425
|
+
const completionEvents = await completionEventsPromise;
|
|
426
|
+
expect(completionEvents).toHaveLength(1);
|
|
427
|
+
expect(completionEvents[0].payload?.sessionId).toBe(sessionId);
|
|
428
|
+
expect(completionEvents[0].payload?.agentId).toBe("main");
|
|
429
|
+
requester.close();
|
|
430
|
+
});
|
|
355
431
|
it("should cancel an in-flight agent request", async ()=>{
|
|
356
432
|
const requester = await connectClient("session-cancel-requester");
|
|
357
433
|
const requestId = "req-cancel-test";
|
|
@@ -458,6 +534,9 @@ describeIfBun("Gateway", ()=>{
|
|
|
458
534
|
}));
|
|
459
535
|
const cancelAck = await waitForMessage(requester, (msg)=>"ack" === msg.type && msg.payload?.action === "req:agent:cancel" && msg.payload?.requestId === secondRequestId && msg.payload?.status === "cancelled_queued", 10000);
|
|
460
536
|
expect(cancelAck.payload?.status).toBe("cancelled_queued");
|
|
537
|
+
const cancelEvent = await waitForMessage(requester, (msg)=>"event:agent" === msg.type && msg.id === secondRequestId && msg.payload?.type === "agent-error" && /cancel/i.test(String(msg.payload?.error || "")), 10000);
|
|
538
|
+
expect(cancelEvent.payload?.sessionId).toBe(sessionId);
|
|
539
|
+
expect(cancelEvent.payload?.agentId).toBe("main");
|
|
461
540
|
await waitForMessage(requester, (msg)=>"event:agent" === msg.type && msg.id === firstRequestId && msg.payload?.type === "agent-complete", 10000);
|
|
462
541
|
const queuedRequests = server.queuedSessionRequests;
|
|
463
542
|
const isStillQueued = [
|
|
@@ -494,4 +573,33 @@ describeIfBun("Gateway", ()=>{
|
|
|
494
573
|
expect(updated?.messageCount).toBe(0);
|
|
495
574
|
expect(updated?.lastMessagePreview).toBeNull();
|
|
496
575
|
});
|
|
576
|
+
it("persists failed first-turn messages so the thread survives reload", async ()=>{
|
|
577
|
+
const requester = await connectClient("persist-failed-turn-requester");
|
|
578
|
+
const sessionId = `session-persist-failed-${Date.now()}`;
|
|
579
|
+
const requestId = `req-persist-failed-${Date.now()}`;
|
|
580
|
+
requester.send(JSON.stringify({
|
|
581
|
+
type: "req:agent",
|
|
582
|
+
id: requestId,
|
|
583
|
+
payload: {
|
|
584
|
+
agentId: "main",
|
|
585
|
+
sessionKey: sessionId,
|
|
586
|
+
content: "throw-no-event"
|
|
587
|
+
},
|
|
588
|
+
timestamp: Date.now()
|
|
589
|
+
}));
|
|
590
|
+
await waitForMessage(requester, (msg)=>"event:agent" === msg.type && msg.id === requestId && msg.payload?.type === "agent-error", 10000);
|
|
591
|
+
const sessionsRes = await fetch(`http://localhost:${port}/api/sessions?limit=100`);
|
|
592
|
+
expect(sessionsRes.ok).toBe(true);
|
|
593
|
+
const sessions = await sessionsRes.json();
|
|
594
|
+
const created = sessions.find((session)=>session.id === sessionId);
|
|
595
|
+
expect(created).toBeTruthy();
|
|
596
|
+
expect(created?.messageCount).toBe(1);
|
|
597
|
+
const messagesRes = await fetch(`http://localhost:${port}/api/sessions/${encodeURIComponent(sessionId)}/messages?agentId=main`);
|
|
598
|
+
expect(messagesRes.ok).toBe(true);
|
|
599
|
+
const messages = await messagesRes.json();
|
|
600
|
+
expect(messages.some((message)=>"user" === message.role)).toBe(true);
|
|
601
|
+
expect(messages.some((message)=>"user" === message.role && message.content.includes("throw-no-event"))).toBe(true);
|
|
602
|
+
expect(messages.some((message)=>"assistant" === message.role && message.content.includes("Synthetic invocation failure"))).toBe(true);
|
|
603
|
+
requester.close();
|
|
604
|
+
});
|
|
497
605
|
});
|
|
@@ -0,0 +1,143 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
var __webpack_exports__ = {};
|
|
3
|
+
const external_node_fs_namespaceObject = require("node:fs");
|
|
4
|
+
const external_node_os_namespaceObject = require("node:os");
|
|
5
|
+
const external_node_path_namespaceObject = require("node:path");
|
|
6
|
+
const external_vitest_namespaceObject = require("vitest");
|
|
7
|
+
const imagePersistence_cjs_namespaceObject = require("../cli/core/imagePersistence.cjs");
|
|
8
|
+
const PNG_DATA_URL = "data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAAAEAAAABCAQAAAC1HAwCAAAAC0lEQVR42mP8/x8AAwMCAO3X6S0AAAAASUVORK5CYII=";
|
|
9
|
+
(0, external_vitest_namespaceObject.describe)("imagePersistence", ()=>{
|
|
10
|
+
const tempDirs = [];
|
|
11
|
+
(0, external_vitest_namespaceObject.afterEach)(()=>{
|
|
12
|
+
for (const dir of tempDirs)(0, external_node_fs_namespaceObject.rmSync)(dir, {
|
|
13
|
+
recursive: true,
|
|
14
|
+
force: true
|
|
15
|
+
});
|
|
16
|
+
tempDirs.length = 0;
|
|
17
|
+
});
|
|
18
|
+
(0, external_vitest_namespaceObject.it)("persists assistant image data URLs to disk", ()=>{
|
|
19
|
+
const tempDir = (0, external_node_fs_namespaceObject.mkdtempSync)((0, external_node_path_namespaceObject.join)((0, external_node_os_namespaceObject.tmpdir)(), "wingman-image-store-"));
|
|
20
|
+
tempDirs.push(tempDir);
|
|
21
|
+
const dbPath = (0, external_node_path_namespaceObject.join)(tempDir, "wingman.db");
|
|
22
|
+
const messages = [
|
|
23
|
+
{
|
|
24
|
+
role: "assistant",
|
|
25
|
+
attachments: [
|
|
26
|
+
{
|
|
27
|
+
kind: "image",
|
|
28
|
+
dataUrl: PNG_DATA_URL
|
|
29
|
+
}
|
|
30
|
+
]
|
|
31
|
+
}
|
|
32
|
+
];
|
|
33
|
+
(0, imagePersistence_cjs_namespaceObject.persistAssistantImagesToDisk)({
|
|
34
|
+
dbPath,
|
|
35
|
+
sessionId: "session-123",
|
|
36
|
+
messages
|
|
37
|
+
});
|
|
38
|
+
const attachment = messages[0].attachments?.[0];
|
|
39
|
+
(0, external_vitest_namespaceObject.expect)(typeof attachment?.path).toBe("string");
|
|
40
|
+
(0, external_vitest_namespaceObject.expect)(attachment?.mimeType).toBe("image/png");
|
|
41
|
+
(0, external_vitest_namespaceObject.expect)(typeof attachment?.size).toBe("number");
|
|
42
|
+
(0, external_vitest_namespaceObject.expect)(attachment?.size).toBeGreaterThan(0);
|
|
43
|
+
(0, external_vitest_namespaceObject.expect)((0, external_node_fs_namespaceObject.existsSync)(attachment.path)).toBe(true);
|
|
44
|
+
(0, external_vitest_namespaceObject.expect)((0, external_node_fs_namespaceObject.readFileSync)(attachment.path).length).toBe(attachment?.size);
|
|
45
|
+
});
|
|
46
|
+
(0, external_vitest_namespaceObject.it)("does not persist non-assistant images", ()=>{
|
|
47
|
+
const tempDir = (0, external_node_fs_namespaceObject.mkdtempSync)((0, external_node_path_namespaceObject.join)((0, external_node_os_namespaceObject.tmpdir)(), "wingman-image-store-"));
|
|
48
|
+
tempDirs.push(tempDir);
|
|
49
|
+
const dbPath = (0, external_node_path_namespaceObject.join)(tempDir, "wingman.db");
|
|
50
|
+
const messages = [
|
|
51
|
+
{
|
|
52
|
+
role: "user",
|
|
53
|
+
attachments: [
|
|
54
|
+
{
|
|
55
|
+
kind: "image",
|
|
56
|
+
dataUrl: PNG_DATA_URL
|
|
57
|
+
}
|
|
58
|
+
]
|
|
59
|
+
}
|
|
60
|
+
];
|
|
61
|
+
(0, imagePersistence_cjs_namespaceObject.persistAssistantImagesToDisk)({
|
|
62
|
+
dbPath,
|
|
63
|
+
sessionId: "session-123",
|
|
64
|
+
messages
|
|
65
|
+
});
|
|
66
|
+
(0, external_vitest_namespaceObject.expect)(messages[0].attachments?.[0].path).toBeUndefined();
|
|
67
|
+
});
|
|
68
|
+
(0, external_vitest_namespaceObject.it)("keeps remote image URLs untouched", ()=>{
|
|
69
|
+
const tempDir = (0, external_node_fs_namespaceObject.mkdtempSync)((0, external_node_path_namespaceObject.join)((0, external_node_os_namespaceObject.tmpdir)(), "wingman-image-store-"));
|
|
70
|
+
tempDirs.push(tempDir);
|
|
71
|
+
const dbPath = (0, external_node_path_namespaceObject.join)(tempDir, "wingman.db");
|
|
72
|
+
const messages = [
|
|
73
|
+
{
|
|
74
|
+
role: "assistant",
|
|
75
|
+
attachments: [
|
|
76
|
+
{
|
|
77
|
+
kind: "image",
|
|
78
|
+
dataUrl: "https://example.com/image.png"
|
|
79
|
+
}
|
|
80
|
+
]
|
|
81
|
+
}
|
|
82
|
+
];
|
|
83
|
+
(0, imagePersistence_cjs_namespaceObject.persistAssistantImagesToDisk)({
|
|
84
|
+
dbPath,
|
|
85
|
+
sessionId: "session-123",
|
|
86
|
+
messages
|
|
87
|
+
});
|
|
88
|
+
(0, external_vitest_namespaceObject.expect)(messages[0].attachments?.[0].path).toBeUndefined();
|
|
89
|
+
});
|
|
90
|
+
(0, external_vitest_namespaceObject.it)("derives deterministic file paths for repeated image payloads", ()=>{
|
|
91
|
+
const tempDir = (0, external_node_fs_namespaceObject.mkdtempSync)((0, external_node_path_namespaceObject.join)((0, external_node_os_namespaceObject.tmpdir)(), "wingman-image-store-"));
|
|
92
|
+
tempDirs.push(tempDir);
|
|
93
|
+
const dbPath = (0, external_node_path_namespaceObject.join)(tempDir, "wingman.db");
|
|
94
|
+
const messages = [
|
|
95
|
+
{
|
|
96
|
+
role: "assistant",
|
|
97
|
+
attachments: [
|
|
98
|
+
{
|
|
99
|
+
kind: "image",
|
|
100
|
+
dataUrl: PNG_DATA_URL
|
|
101
|
+
}
|
|
102
|
+
]
|
|
103
|
+
}
|
|
104
|
+
];
|
|
105
|
+
(0, imagePersistence_cjs_namespaceObject.persistAssistantImagesToDisk)({
|
|
106
|
+
dbPath,
|
|
107
|
+
sessionId: "session-123",
|
|
108
|
+
messages
|
|
109
|
+
});
|
|
110
|
+
const firstPath = messages[0].attachments?.[0].path;
|
|
111
|
+
const nextMessages = [
|
|
112
|
+
{
|
|
113
|
+
role: "assistant",
|
|
114
|
+
attachments: [
|
|
115
|
+
{
|
|
116
|
+
kind: "image",
|
|
117
|
+
dataUrl: PNG_DATA_URL
|
|
118
|
+
}
|
|
119
|
+
]
|
|
120
|
+
}
|
|
121
|
+
];
|
|
122
|
+
(0, imagePersistence_cjs_namespaceObject.persistAssistantImagesToDisk)({
|
|
123
|
+
dbPath,
|
|
124
|
+
sessionId: "session-123",
|
|
125
|
+
messages: nextMessages
|
|
126
|
+
});
|
|
127
|
+
const secondPath = nextMessages[0].attachments?.[0].path;
|
|
128
|
+
(0, external_vitest_namespaceObject.expect)(firstPath).toBeTruthy();
|
|
129
|
+
(0, external_vitest_namespaceObject.expect)(secondPath).toBe(firstPath);
|
|
130
|
+
});
|
|
131
|
+
(0, external_vitest_namespaceObject.it)("parses base64 data URLs and resolves extensions", ()=>{
|
|
132
|
+
const parsed = (0, imagePersistence_cjs_namespaceObject.parseBase64DataUrl)(PNG_DATA_URL);
|
|
133
|
+
(0, external_vitest_namespaceObject.expect)(parsed?.mimeType).toBe("image/png");
|
|
134
|
+
(0, external_vitest_namespaceObject.expect)(typeof parsed?.data).toBe("string");
|
|
135
|
+
(0, external_vitest_namespaceObject.expect)((0, imagePersistence_cjs_namespaceObject.resolveImageExtension)("image/jpeg")).toBe("jpg");
|
|
136
|
+
(0, external_vitest_namespaceObject.expect)((0, imagePersistence_cjs_namespaceObject.resolveImageExtension)("image/svg+xml")).toBe("svg");
|
|
137
|
+
(0, external_vitest_namespaceObject.expect)((0, imagePersistence_cjs_namespaceObject.resolveImageExtension)("image/custom+format")).toBe("customformat");
|
|
138
|
+
});
|
|
139
|
+
});
|
|
140
|
+
for(var __rspack_i in __webpack_exports__)exports[__rspack_i] = __webpack_exports__[__rspack_i];
|
|
141
|
+
Object.defineProperty(exports, '__esModule', {
|
|
142
|
+
value: true
|
|
143
|
+
});
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
export {};
|