|
| 1 | +import { describe, expect, it, vi } from "vitest"; |
| 2 | +import { |
| 3 | + GATEWAY_CLIENT_IDS, |
| 4 | + GATEWAY_CLIENT_MODES, |
| 5 | + type GatewayClientInfo, |
| 6 | +} from "../../../packages/gateway-protocol/src/client-info.js"; |
| 7 | +import type { MsgContext } from "../../auto-reply/templating.js"; |
| 8 | +import type { UserTurnInput } from "../../sessions/user-turn-transcript.js"; |
| 9 | +import { applyChatSendManagedMediaFields, prepareChatSendUserTurn } from "./chat-send-user-turn.js"; |
| 10 | + |
| 11 | +function createUserTurnInputController() { |
| 12 | + const baseInput: UserTurnInput = { |
| 13 | + text: "raw message", |
| 14 | + timestamp: 1, |
| 15 | + idempotencyKey: "run-1:user", |
| 16 | + }; |
| 17 | + let inputPromise = Promise.resolve(baseInput); |
| 18 | + return { |
| 19 | + controller: { |
| 20 | + baseInput, |
| 21 | + setInputPromise: (input: Promise<UserTurnInput>) => { |
| 22 | + inputPromise = input; |
| 23 | + }, |
| 24 | + }, |
| 25 | + readInput: () => inputPromise, |
| 26 | + }; |
| 27 | +} |
| 28 | + |
| 29 | +function createClientInfo(overrides: Partial<GatewayClientInfo> = {}): GatewayClientInfo { |
| 30 | + return { |
| 31 | + id: GATEWAY_CLIENT_IDS.CLI, |
| 32 | + version: "test", |
| 33 | + platform: "test", |
| 34 | + mode: GATEWAY_CLIENT_MODES.CLI, |
| 35 | + ...overrides, |
| 36 | + }; |
| 37 | +} |
| 38 | + |
| 39 | +function createAttachments( |
| 40 | + overrides: Partial<{ |
| 41 | + explicitOriginTargetsPlugin: boolean; |
| 42 | + mediaPathOffloadPaths: string[]; |
| 43 | + mediaPathOffloadTypes: string[]; |
| 44 | + mediaPathOffloadWorkspaceDir: string | undefined; |
| 45 | + parsedMessage: string; |
| 46 | + }> = {}, |
| 47 | +) { |
| 48 | + return { |
| 49 | + explicitOriginTargetsPlugin: false, |
| 50 | + imageOrder: [], |
| 51 | + mediaPathOffloadPaths: [], |
| 52 | + mediaPathOffloadTypes: [], |
| 53 | + mediaPathOffloadWorkspaceDir: undefined, |
| 54 | + offloadedRefs: [], |
| 55 | + parsedImages: [], |
| 56 | + parsedMessage: "hello", |
| 57 | + prepareAttachmentsMs: undefined, |
| 58 | + ...overrides, |
| 59 | + }; |
| 60 | +} |
| 61 | + |
| 62 | +describe("prepareChatSendUserTurn", () => { |
| 63 | + it("assembles command, provenance, sender, and origin facts", async () => { |
| 64 | + const { controller, readInput } = createUserTurnInputController(); |
| 65 | + const prepared = prepareChatSendUserTurn({ |
| 66 | + request: { |
| 67 | + clientInfo: createClientInfo({ displayName: "Gateway CLI" }), |
| 68 | + normalizedAttachments: [], |
| 69 | + suppressCommandInterpretation: false, |
| 70 | + systemInputProvenance: { kind: "internal_system", sourceTool: "test" }, |
| 71 | + systemProvenanceReceipt: "[System receipt]", |
| 72 | + }, |
| 73 | + session: { |
| 74 | + agentId: "main", |
| 75 | + clientRunId: "run-1", |
| 76 | + sessionKey: "agent:main:main", |
| 77 | + }, |
| 78 | + admission: { |
| 79 | + originatingRoute: { |
| 80 | + originatingChannel: "discord", |
| 81 | + originatingTo: "channel:1", |
| 82 | + accountId: "account-1", |
| 83 | + messageThreadId: "thread-1", |
| 84 | + explicitDeliverRoute: true, |
| 85 | + }, |
| 86 | + }, |
| 87 | + attachments: createAttachments({ parsedMessage: "/status" }), |
| 88 | + client: null, |
| 89 | + logGateway: { warn: vi.fn() } as never, |
| 90 | + userTurn: controller, |
| 91 | + }); |
| 92 | + |
| 93 | + expect(prepared.ctx).toMatchObject({ |
| 94 | + Body: "[System receipt]\n\n/status", |
| 95 | + BodyForAgent: "[System receipt]\n\n/status", |
| 96 | + BodyForCommands: "/status", |
| 97 | + RawBody: "/status", |
| 98 | + CommandSource: "text", |
| 99 | + CommandAuthorized: true, |
| 100 | + CommandTurn: { |
| 101 | + kind: "text-slash", |
| 102 | + source: "text", |
| 103 | + authorized: true, |
| 104 | + body: "/status", |
| 105 | + }, |
| 106 | + InputProvenance: { kind: "internal_system", sourceTool: "test" }, |
| 107 | + OriginatingChannel: "discord", |
| 108 | + OriginatingTo: "channel:1", |
| 109 | + AccountId: "account-1", |
| 110 | + MessageThreadId: "thread-1", |
| 111 | + ExplicitDeliverRoute: true, |
| 112 | + SenderId: GATEWAY_CLIENT_IDS.CLI, |
| 113 | + SenderName: "Gateway CLI", |
| 114 | + SenderUsername: "Gateway CLI", |
| 115 | + }); |
| 116 | + expect(prepared.accountId).toBe("account-1"); |
| 117 | + expect(prepared.isInternalTextSlashCommandTurn).toBe(true); |
| 118 | + expect(prepared.queuedFollowupOwnerKey).toBeUndefined(); |
| 119 | + expect(prepared.replyOptionImages).toBeUndefined(); |
| 120 | + await expect(prepared.pluginBoundMediaFieldsPromise).resolves.toEqual({}); |
| 121 | + await expect(readInput()).resolves.toEqual(controller.baseInput); |
| 122 | + }); |
| 123 | + |
| 124 | + it("carries pre-staged media and device ownership without UI sender decoration", async () => { |
| 125 | + const { controller, readInput } = createUserTurnInputController(); |
| 126 | + const prepared = prepareChatSendUserTurn({ |
| 127 | + request: { |
| 128 | + clientInfo: createClientInfo({ |
| 129 | + id: GATEWAY_CLIENT_IDS.CONTROL_UI, |
| 130 | + mode: GATEWAY_CLIENT_MODES.UI, |
| 131 | + }), |
| 132 | + normalizedAttachments: [{}], |
| 133 | + suppressCommandInterpretation: true, |
| 134 | + systemInputProvenance: undefined, |
| 135 | + systemProvenanceReceipt: undefined, |
| 136 | + }, |
| 137 | + session: { |
| 138 | + agentId: "main", |
| 139 | + clientRunId: "run-1", |
| 140 | + sessionKey: "agent:main:main", |
| 141 | + }, |
| 142 | + admission: { |
| 143 | + originatingRoute: { |
| 144 | + originatingChannel: "webchat", |
| 145 | + explicitDeliverRoute: false, |
| 146 | + }, |
| 147 | + }, |
| 148 | + attachments: createAttachments({ |
| 149 | + mediaPathOffloadPaths: ["uploads/report.pdf"], |
| 150 | + mediaPathOffloadTypes: ["application/pdf"], |
| 151 | + mediaPathOffloadWorkspaceDir: "/workspace", |
| 152 | + }), |
| 153 | + client: { |
| 154 | + connId: "conn-1", |
| 155 | + connect: { |
| 156 | + device: { id: "device-1" }, |
| 157 | + scopes: ["operator.admin"], |
| 158 | + caps: ["tool-events"], |
| 159 | + }, |
| 160 | + } as never, |
| 161 | + logGateway: { warn: vi.fn() } as never, |
| 162 | + userTurn: controller, |
| 163 | + }); |
| 164 | + |
| 165 | + expect(prepared.ctx).toMatchObject({ |
| 166 | + CommandAuthorized: false, |
| 167 | + CommandTurn: { |
| 168 | + kind: "normal", |
| 169 | + source: "message", |
| 170 | + authorized: false, |
| 171 | + body: "hello", |
| 172 | + }, |
| 173 | + ApprovalReviewerDeviceId: "device-1", |
| 174 | + MediaPath: "uploads/report.pdf", |
| 175 | + MediaPaths: ["uploads/report.pdf"], |
| 176 | + MediaType: "application/pdf", |
| 177 | + MediaTypes: ["application/pdf"], |
| 178 | + MediaWorkspaceDir: "/workspace", |
| 179 | + MediaStaged: true, |
| 180 | + GatewayClientScopes: ["operator.admin"], |
| 181 | + GatewayClientCaps: ["tool-events"], |
| 182 | + }); |
| 183 | + expect(prepared.ctx).not.toHaveProperty("SenderId"); |
| 184 | + expect(prepared.queuedFollowupOwnerKey).toBe("device:device-1"); |
| 185 | + await expect(readInput()).resolves.toEqual(controller.baseInput); |
| 186 | + }); |
| 187 | +}); |
| 188 | + |
| 189 | +describe("applyChatSendManagedMediaFields", () => { |
| 190 | + it("fills missing staged fields without replacing pre-staged paths", () => { |
| 191 | + const ctx = { |
| 192 | + MediaStaged: true, |
| 193 | + MediaPath: "uploads/report.pdf", |
| 194 | + } as MsgContext; |
| 195 | + |
| 196 | + applyChatSendManagedMediaFields(ctx, { |
| 197 | + MediaPath: "managed/image.png", |
| 198 | + MediaPaths: ["managed/image.png"], |
| 199 | + MediaType: "image/png", |
| 200 | + MediaTypes: ["image/png"], |
| 201 | + }); |
| 202 | + |
| 203 | + expect(ctx).toMatchObject({ |
| 204 | + MediaPath: "uploads/report.pdf", |
| 205 | + MediaPaths: ["managed/image.png"], |
| 206 | + MediaType: "image/png", |
| 207 | + MediaTypes: ["image/png"], |
| 208 | + }); |
| 209 | + }); |
| 210 | +}); |
0 commit comments