1/**
2 * True `alchemy dev` end-to-end for the Job app — the regression test for
3 * the dev-mode apply failure where the SNS→Lambda glue resources
4 * (AWS.SNS.Subscription and the AWS.Lambda.Permission behind
5 * `consumeTopicNotifications`) were created against REAL AWS with floci
6 * ARNs and died with `InvalidParameterException: Invalid parameter:
7 * TopicArn` / `ResourceNotFoundException: Function not found`.
8 *
9 * This stack is also the mixed-mode case: the DynamoDB table, SQS queue,
10 * SNS topic, Lambda function, event source mapping, subscription, and
11 * permission are all local (floci), while the mode-agnostic CloudWatch
12 * Dashboard + Alarm deploy live — so the run needs AWS credentials, and a
13 * green apply proves local glue and live mode-agnostic resources coexist.
14 *
15 * Mirrored from examples/aws-dev/test/dev.test.ts (which owns the broader
16 * per-binding + hot-reload coverage); this suite drives the real app:
17 * POST a job over the emulator-served Function URL, read it back, and let
18 * the notification publish ride the local Subscription.
19 */
20import { afterAll, expect, test } from "bun:test";
21import { DevCli, fetchOk } from "alchemy-test/DevCli";
22import { spawnSync } from "node:child_process";
23import * as path from "node:path";
24
25const root = path.resolve(import.meta.dirname, "..");
26const STAGE = "dev-cli-test";
27const cli = new DevCli({ root, stage: STAGE });
28
29// The whole suite needs docker (floci runs as a container).
30const dockerAvailable =
31 spawnSync("docker", ["info"], { stdio: "ignore" }).status === 0;
32
33afterAll(async () => {
34 await cli.stop();
35 if (!process.env.NO_DESTROY && dockerAvailable) {
36 cli.destroy();
37 }
38}, 180_000);
39
40test.skipIf(!dockerAvailable)(
41 "alchemy dev applies the mixed local/live Job stack and serves jobs",
42 async () => {
43 cli.start();
44
45 // The first dev deploy may pull the floci image and provision the
46 // local data plane before printing stack outputs.
47 const url = await cli.pollUntil(
48 "url in stack outputs",
49 () => cli.outputUrl("url"),
50 { tries: 300, delayMs: 1000 },
51 );
52
53 // Dev identity: the function URL is served locally, not by AWS
54 // (e.g. http://<id>.lambda-url.us-east-1.localhost:<port>/). The port
55 // is whatever the emulator bound — never hard-code it, only the URL
56 // captured from the CLI's stdout is authoritative.
57 expect(new URL(url).hostname).toEndWith("localhost");
58 expect(url).not.toContain("amazonaws.com");
59
60 // The bug failed the APPLY itself: Subscription/Permission creation
61 // against real AWS with floci ARNs. Outputs printing means apply
62 // succeeded, but pin the failure banner too so a partial apply that
63 // still prints outputs can never sneak past.
64 expect(cli.output).not.toContain("apply failed");
65
66 const api = new URL(url);
67 api.search = "";
68
69 // POST a job: stores it in the local table AND publishes the
70 // job.created notification over the local topic — which only works
71 // when the Subscription + invoke Permission were created on floci.
72 const content = `hello from alchemy dev ${crypto.randomUUID()}`;
73 const created = (await (
74 await fetchOk(api, {
75 method: "POST",
76 headers: { "content-type": "text/plain" },
77 body: content,
78 })
79 ).json()) as { jobId: string };
80 expect(created.jobId).toBeString();
81
82 // Read it back through the local DynamoDB storage.
83 const job = (await (
84 await fetchOk(new URL(`/?jobId=${created.jobId}`, api))
85 ).json()) as { id: string; content: string };
86 expect(job.id).toBe(created.jobId);
87 expect(job.content).toBe(content);
88 },
89 { timeout: 600_000 },
90);