1import * as AWS from "@/AWS";
2import { Lexicon } from "@/AWS/Polly/Lexicon.ts";
3import * as Provider from "@/Provider";
4import * as Test from "@/Test/Alchemy";
5import * as polly from "@distilled.cloud/aws/polly";
6import { expect } from "alchemy-test";
7import * as Effect from "effect/Effect";
8import * as Redacted from "effect/Redacted";
9
10const { test } = Test.make({ providers: AWS.providers() });
11
12// Lexicon names must match [0-9A-Za-z]{1,20}.
13const lexiconName = "alchemyTestLexicon";
14
15// Checked-in W3C PLS fixtures — never generated at test time.
16const plsDocument = (alias: string) => `<?xml version="1.0" encoding="UTF-8"?>
17<lexicon version="1.0"
18 xmlns="http://www.w3.org/2005/01/pronunciation-lexicon"
19 alphabet="ipa" xml:lang="en-US">
20 <lexeme><grapheme>IaC</grapheme><alias>${alias}</alias></lexeme>
21</lexicon>`;
22
23const PLS_V1 = plsDocument("infrastructure as code");
24const PLS_V2 = plsDocument("infrastructure as effects");
25
26const unredact = (value: string | Redacted.Redacted<string> | undefined) =>
27 value === undefined
28 ? undefined
29 : Redacted.isRedacted(value)
30 ? Redacted.value(value)
31 : value;
32
33const getLexicon = (name: string) =>
34 polly
35 .getLexicon({ Name: name })
36 .pipe(
37 Effect.catchTag("LexiconNotFoundException", () =>
38 Effect.succeed(undefined),
39 ),
40 );
41
42const waitForContent = (name: string, content: string) =>
43 Effect.gen(function* () {
44 let found: polly.GetLexiconOutput | undefined;
45 for (let attempt = 0; attempt < 20; attempt++) {
46 found = yield* getLexicon(name);
47 if (unredact(found?.Lexicon?.Content)?.includes(content)) return found;
48 yield* Effect.sleep("250 millis");
49 }
50 return found;
51 });
52
53const waitUntilGone = (name: string) =>
54 Effect.gen(function* () {
55 let found: polly.GetLexiconOutput | undefined;
56 for (let attempt = 0; attempt < 20; attempt++) {
57 found = yield* getLexicon(name);
58 if (found === undefined) return found;
59 yield* Effect.sleep("250 millis");
60 }
61 return found;
62 });
63
64test.provider(
65 "lifecycle: create named lexicon, update content, destroy",
66 (stack) =>
67 Effect.gen(function* () {
68 yield* stack.destroy();
69
70 // Create — a named lexicon with the v1 PLS document.
71 const deployed = yield* stack.deploy(
72 Effect.gen(function* () {
73 return yield* Lexicon("TestLexicon", {
74 lexiconName,
75 content: PLS_V1,
76 });
77 }),
78 );
79 expect(deployed.lexiconName).toBe(lexiconName);
80 expect(deployed.lexiconArn).toContain(`:lexicon/${lexiconName}`);
81 expect(deployed.languageCode).toBe("en-US");
82 expect(deployed.alphabet).toBe("ipa");
83 expect(deployed.lexemesCount).toBe(1);
84
85 // Out-of-band verification via distilled.
86 const created = yield* waitForContent(
87 lexiconName,
88 "infrastructure as code",
89 );
90 expect(unredact(created?.Lexicon?.Content)).toContain(
91 "infrastructure as code",
92 );
93
94 // Canonical list() coverage.
95 const provider = yield* Provider.findProvider(Lexicon);
96 const all = yield* provider.list();
97 expect(all.some((l) => l.lexiconName === lexiconName)).toBe(true);
98
99 // Update — the PLS content is updatable in place (PutLexicon upsert).
100 yield* stack.deploy(
101 Effect.gen(function* () {
102 return yield* Lexicon("TestLexicon", {
103 lexiconName,
104 content: PLS_V2,
105 });
106 }),
107 );
108 const updated = yield* waitForContent(
109 lexiconName,
110 "infrastructure as effects",
111 );
112 expect(unredact(updated?.Lexicon?.Content)).toContain(
113 "infrastructure as effects",
114 );
115
116 // Destroy — the lexicon is gone.
117 yield* stack.destroy();
118 const after = yield* waitUntilGone(lexiconName);
119 expect(after).toBeUndefined();
120 }),
121 { tags: ["provider:aws", "provider:aws:polly", "live"], timeout: 180_000 },
122);
123
124test.provider(
125 "generates a valid alphanumeric name when lexiconName is omitted",
126 (stack) =>
127 Effect.gen(function* () {
128 yield* stack.destroy();
129
130 const deployed = yield* stack.deploy(
131 Effect.gen(function* () {
132 return yield* Lexicon("GeneratedName", { content: PLS_V1 });
133 }),
134 );
135 // Polly constrains names to [0-9A-Za-z]{1,20}.
136 expect(deployed.lexiconName).toMatch(/^[0-9A-Za-z]{1,20}$/);
137
138 const created = yield* getLexicon(deployed.lexiconName);
139 expect(created).toBeDefined();
140
141 yield* stack.destroy();
142 const after = yield* waitUntilGone(deployed.lexiconName);
143 expect(after).toBeUndefined();
144 }),
145 { tags: ["provider:aws", "provider:aws:polly", "live"], timeout: 180_000 },
146);