1import * as AWS from "@/AWS";
2import { Application } from "@/AWS/AppRegistry";
3import * as Test from "@/Test/Alchemy";
4import * as appregistry from "@distilled.cloud/aws/service-catalog-appregistry";
5import { expect } from "alchemy-test";
6import * as Data from "effect/Data";
7import * as Effect from "effect/Effect";
8import * as Schedule from "effect/Schedule";
9import { makeAppRegistryTestLease } from "./TestLease.ts";
10
11const { test, beforeAll, afterAll } = Test.make({ providers: AWS.providers() });
12const serviceLease = makeAppRegistryTestLease();
13
14beforeAll(serviceLease.acquire, { timeout: 3_600_000 });
15afterAll(serviceLease.release);
16
17// AWS Service Catalog AppRegistry entered maintenance mode on July 30, 2026.
18// Accounts without pre-existing AppRegistry resources are treated as "new
19// customers" and every create is denied with AccessDeniedException. The full
20// lifecycle/binding suites are gated behind AWS_TEST_APPREGISTRY=1 for
21// accounts that retain access; the ungated probe below pins the typed
22// maintenance-mode rejection on everything else.
23const gated = !process.env.AWS_TEST_APPREGISTRY;
24
25// Probe only makes sense on an account WITHOUT AppRegistry access — an
26// entitled account (AWS_TEST_APPREGISTRY=1) would successfully create the
27// application instead, so the probe is skipped there and the real lifecycle
28// tests run in its place.
29test.provider.skipIf(!gated)(
30 "createApplication is denied with the typed maintenance-mode AccessDeniedException",
31 () =>
32 Effect.gen(function* () {
33 const error = yield* Effect.flip(
34 appregistry.createApplication({
35 name: "alchemy-appregistry-maintenance-probe",
36 clientToken: "alchemy-appregistry-maintenance-probe",
37 }),
38 );
39 expect(error._tag).toBe("AccessDeniedException");
40 if (error._tag === "AccessDeniedException") {
41 expect(error.message).toContain("maintenance mode");
42 }
43 }),
44 { tags: ["provider:aws", "provider:aws:appregistry", "live"] },
45);
46
47class ApplicationStillExists extends Data.TaggedError(
48 "ApplicationStillExists",
49)<{ specifier: string }> {}
50
51const assertApplicationGone = (specifier: string) =>
52 appregistry.getApplication({ application: specifier }).pipe(
53 Effect.flatMap(() =>
54 Effect.fail(new ApplicationStillExists({ specifier })),
55 ),
56 Effect.catchTag("ResourceNotFoundException", () => Effect.void),
57 Effect.retry({
58 while: (e) => e._tag === "ApplicationStillExists",
59 schedule: Schedule.max([
60 Schedule.fixed("2 seconds"),
61 Schedule.recurs(10),
62 ]),
63 }),
64 );
65
66test.provider.skipIf(gated)(
67 "creates, updates, replaces, and deletes an application",
68 (stack) =>
69 Effect.gen(function* () {
70 yield* stack.destroy();
71
72 const created = yield* stack.deploy(
73 Effect.gen(function* () {
74 const app = yield* Application("TestApp", {
75 description: "application lifecycle test",
76 tags: { purpose: "lifecycle" },
77 });
78 return {
79 applicationId: app.applicationId,
80 applicationArn: app.applicationArn,
81 applicationName: app.applicationName,
82 };
83 }),
84 );
85 expect(created.applicationArn).toContain(":servicecatalog:");
86
87 // out-of-band verify via distilled
88 const found = yield* appregistry.getApplication({
89 application: created.applicationId,
90 });
91 expect(found.name).toBe(created.applicationName);
92 expect(found.description).toBe("application lifecycle test");
93 expect(found.tags?.purpose).toBe("lifecycle");
94 expect(found.tags?.["alchemy::id"]).toBe("TestApp");
95
96 // in-place update of description + tags
97 yield* stack.deploy(
98 Effect.gen(function* () {
99 yield* Application("TestApp", {
100 description: "updated description",
101 tags: { purpose: "lifecycle-updated" },
102 });
103 }),
104 );
105 const updated = yield* appregistry.getApplication({
106 application: created.applicationId,
107 });
108 expect(updated.id).toBe(created.applicationId);
109 expect(updated.description).toBe("updated description");
110 expect(updated.tags?.purpose).toBe("lifecycle-updated");
111
112 // explicit name change → replacement (new application ID)
113 const replaced = yield* stack.deploy(
114 Effect.gen(function* () {
115 const app = yield* Application("TestApp", {
116 applicationName: `${created.applicationName}-renamed`,
117 description: "updated description",
118 });
119 return { applicationId: app.applicationId };
120 }),
121 );
122 expect(replaced.applicationId).not.toBe(created.applicationId);
123 yield* assertApplicationGone(created.applicationId);
124
125 yield* stack.destroy();
126 yield* assertApplicationGone(replaced.applicationId);
127 }),
128 {
129 tags: ["provider:aws", "provider:aws:appregistry", "live"],
130 timeout: 180_000,
131 },
132);