1import * as AWS from "@/AWS";
2import * as Effect from "effect/Effect";
3import * as Layer from "effect/Layer";
4import { HttpServerRequest } from "effect/http/HttpServerRequest";
5import * as HttpServerResponse from "effect/http/HttpServerResponse";
6
7export class QAppsTestFunction extends AWS.Lambda.Function<AWS.Lambda.Function>()(
8 "QAppsTestFunction",
9) {}
10
11/**
12 * Every route answers `{ …fields }` on success or `{ errorTag }` when the
13 * operation fails with a TYPED error — the test asserts on concrete fields
14 * (or a typed tag), which proves the binding wiring, the instance-id/app-id
15 * injection, and the IAM grants (app-ARN-scoped for the session/permission
16 * bindings, account-level for the instance-level bindings). An untyped
17 * error crashes into a 500.
18 */
19const errorTagged = <A, E extends { _tag: string }, R>(
20 effect: Effect.Effect<A, E, R>,
21): Effect.Effect<A | { errorTag: string }, never, R> =>
22 effect.pipe(
23 Effect.map((a): A | { errorTag: string } => a),
24 Effect.catch((e) => Effect.succeed({ errorTag: e._tag })),
25 );
26
27const TEXT_CARD_ID = "11111111-1111-4111-8111-111111111111";
28const QUERY_CARD_ID = "22222222-2222-4222-8222-222222222222";
29
30/**
31 * Q App binding fixture: deploys a Q App into the Q Business application
32 * environment instance named by QAPPS_INSTANCE_ID plus a Lambda bound to a
33 * representative set of Q Apps bindings (both builders: app/session-scoped
34 * and instance-level). Only deployed behind the AWS_TEST_QAPPS gate — the
35 * QApp resource itself cannot exist without an entitled Q Business instance.
36 */
37export default QAppsTestFunction.make(
38 { main: import.meta.url, functionUrl: true },
39 Effect.gen(function* () {
40 const instanceId = yield* Effect.sync(
41 () => process.env.QAPPS_INSTANCE_ID ?? "",
42 );
43
44 const app = yield* AWS.QApps.QApp("BindingsQApp", {
45 instanceId,
46 description: "alchemy QApps bindings fixture",
47 appDefinition: {
48 cards: [
49 {
50 textInput: {
51 id: TEXT_CARD_ID,
52 title: "Source Text",
53 type: "text-input",
54 },
55 },
56 {
57 qQuery: {
58 id: QUERY_CARD_ID,
59 title: "Summary",
60 type: "q-query",
61 prompt: "Summarize the following text: @Source Text",
62 },
63 },
64 ],
65 },
66 tags: { fixture: "qapps-bindings" },
67 });
68
69 const appVersion = yield* app.appVersion;
70
71 const startQAppSession = yield* AWS.QApps.StartQAppSession(app);
72 const getQAppSession = yield* AWS.QApps.GetQAppSession(app);
73 const stopQAppSession = yield* AWS.QApps.StopQAppSession(app);
74 const describeQAppPermissions =
75 yield* AWS.QApps.DescribeQAppPermissions(app);
76 const listQApps = yield* AWS.QApps.ListQApps(app);
77 const listCategories = yield* AWS.QApps.ListCategories(app);
78
79 const bound = {
80 startQAppSession,
81 getQAppSession,
82 stopQAppSession,
83 describeQAppPermissions,
84 listQApps,
85 listCategories,
86 };
87
88 return {
89 fetch: Effect.gen(function* () {
90 const request = yield* HttpServerRequest;
91 const url = new URL(request.originalUrl ?? request.url, "http://x");
92 const pathname = url.pathname;
93
94 if (pathname === "/bindings") {
95 return yield* HttpServerResponse.json({ bound: Object.keys(bound) });
96 }
97
98 if (pathname === "/session") {
99 // start → get → stop, proving app-id injection and the session
100 // sub-resource grant end-to-end.
101 const started = yield* errorTagged(
102 startQAppSession({ appVersion: yield* appVersion }),
103 );
104 if ("errorTag" in started) {
105 return yield* HttpServerResponse.json(started);
106 }
107 const state = yield* errorTagged(
108 getQAppSession({ sessionId: started.sessionId }),
109 );
110 const stopped = yield* errorTagged(
111 stopQAppSession({ sessionId: started.sessionId }),
112 );
113 return yield* HttpServerResponse.json({
114 sessionId: started.sessionId,
115 status: "errorTag" in state ? undefined : state.status,
116 getErrorTag: "errorTag" in state ? state.errorTag : undefined,
117 stopErrorTag: "errorTag" in stopped ? stopped.errorTag : undefined,
118 });
119 }
120
121 if (pathname === "/apps") {
122 const result = yield* errorTagged(listQApps({ limit: 10 }));
123 return yield* HttpServerResponse.json(
124 "errorTag" in result ? result : { count: result.apps.length },
125 );
126 }
127
128 if (pathname === "/categories") {
129 const result = yield* errorTagged(listCategories());
130 return yield* HttpServerResponse.json(
131 "errorTag" in result
132 ? result
133 : { count: result.categories?.length ?? 0 },
134 );
135 }
136
137 if (pathname === "/permissions") {
138 const result = yield* errorTagged(describeQAppPermissions());
139 return yield* HttpServerResponse.json(
140 "errorTag" in result
141 ? result
142 : {
143 resourceArn: result.resourceArn,
144 count: result.permissions?.length ?? 0,
145 },
146 );
147 }
148
149 return yield* HttpServerResponse.json(
150 { error: "Not found", pathname },
151 { status: 404 },
152 );
153 }).pipe(Effect.orDie),
154 };
155 }).pipe(
156 Effect.provide(
157 Layer.mergeAll(
158 AWS.QApps.StartQAppSessionHttp,
159 AWS.QApps.GetQAppSessionHttp,
160 AWS.QApps.StopQAppSessionHttp,
161 AWS.QApps.DescribeQAppPermissionsHttp,
162 AWS.QApps.ListQAppsHttp,
163 AWS.QApps.ListCategoriesHttp,
164 ),
165 ),
166 ),
167);