t3-code-android-nightly/.repos/alchemy-effect/packages/alchemy/test/AWS/Personalize/Bindings.test.ts
Julius Marminge 6f9cea00ae
chore(refs): sync Effect and Alchemy references to 4.0.1 and beta.80 (#16170)
Co-authored-by: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
2026-10-05 13:22:30 -07:00

461 lines
15 KiB
TypeScript

import * as AWS from "@/AWS";
import { AWSEnvironment } from "@/AWS/Environment";
import * as Core from "@/Test/Core";
import * as Output from "@/Output";
import * as Test from "@/Test/Alchemy";
import * as personalize from "@distilled.cloud/aws/personalize";
import { describe, expect } from "alchemy-test";
import * as Effect from "effect/Effect";
import * as Schedule from "effect/Schedule";
import * as HttpClient from "effect/http/HttpClient";
import PersonalizeTestFunctionLive, {
PersonalizeTestFunction,
} from "./handler";
const testOptions = { providers: AWS.providers() };
const { test, beforeAll, afterAll } = Test.make(testOptions);
const sharedStack = Core.scratchStack(testOptions, "PersonalizeBindings");
// Lambda function URL cold-start (DNS, IAM propagation, init) can take well
// over 60s on a fresh deploy.
const readinessPolicy = Schedule.max([
Schedule.fixed("2 seconds"),
Schedule.recurs(75),
]);
let baseUrl: string;
let resourceArns:
| {
group: string;
tracker: string;
datasets: string[];
schemas: string[];
}
| undefined;
// Well-formed-but-nonexistent ARNs the probe routes are driven against —
// training a solution/campaign takes ~an hour of paid compute, so the
// campaign/solution routes only prove the IAM bind + typed error decode.
const probeArns = Effect.gen(function* () {
const { accountId, region } = yield* AWSEnvironment.current;
return {
dataset: `arn:aws:personalize:${region}:${accountId}:dataset/alchemy-probe/INTERACTIONS`,
importJob: `arn:aws:personalize:${region}:${accountId}:dataset-import-job/alchemy-probe`,
datasetGroup: `arn:aws:personalize:${region}:${accountId}:dataset-group/alchemy-probe`,
solution: `arn:aws:personalize:${region}:${accountId}:solution/alchemy-probe`,
solutionVersion: `arn:aws:personalize:${region}:${accountId}:solution/alchemy-probe/0123456789abcdef`,
campaign: `arn:aws:personalize:${region}:${accountId}:campaign/alchemy-probe`,
batchJob: `arn:aws:personalize:${region}:${accountId}:batch-inference-job/alchemy-probe`,
};
});
// A nonexistent target answers ResourceNotFoundException; malformed-but-
// plausible inputs can surface InvalidInputException. Both are typed tags
// decoded inside the Lambda — an IAM gap or schema break would surface a
// different tag (AccessDeniedException / UnknownAwsError) and fail.
const EXPECTED_PROBE_TAGS = [
"ResourceNotFoundException",
"InvalidInputException",
];
const getJson = (path: string) =>
HttpClient.get(`${baseUrl}${path}`).pipe(
Effect.flatMap((response) =>
response.status >= 500
? Effect.fail(new Error(`transient upstream ${response.status}`))
: Effect.succeed(response),
),
Effect.retry({
schedule: Schedule.max([
Schedule.exponential("500 millis"),
Schedule.recurs(6),
]),
}),
Effect.flatMap((r) => r.json),
);
// IAM policy statements on a freshly-created role can take up to ~a minute to
// propagate to the Personalize data plane, surfacing as a transient
// AccessDeniedException tag. Retry (bounded) until a different tag appears —
// a genuine IAM gap still fails the assertion with the AccessDeniedException
// tag after the retries are exhausted.
const routeTag = (path: string) =>
getJson(path).pipe(
Effect.map((response) => (response as { tag: string }).tag),
Effect.repeat({
schedule: Schedule.spaced("3 seconds"),
until: (tag: string): boolean => tag !== "AccessDeniedException",
times: 20,
}),
);
const probeTag = (path: string, arn: string) =>
routeTag(`${path}?arn=${encodeURIComponent(arn)}`);
// Create-probes that pass a data-access role: use a nonexistent role in OUR
// account so the service validates (and 404s) the primary resource rather
// than tripping cross-account PassRole checks.
const probeTagWithRole = (path: string, arn: string) =>
Effect.gen(function* () {
const { accountId } = yield* AWSEnvironment.current;
const role = `arn:aws:iam::${accountId}:role/alchemy-nonexistent-probe-role`;
return yield* routeTag(
`${path}?arn=${encodeURIComponent(arn)}&role=${encodeURIComponent(role)}`,
);
});
describe.sequential(
"Personalize Bindings",
{
tags: [
"provider:aws",
"provider:aws:lambda",
"provider:aws:personalize",
"live",
],
},
() => {
beforeAll(
Effect.gen(function* () {
yield* Effect.logInfo(
"Personalize test setup: destroying previous resources",
);
yield* sharedStack.destroy();
yield* Effect.logInfo("Personalize test setup: deploying fixture");
const { functionUrl } = yield* sharedStack.deploy(
Effect.gen(function* () {
return yield* PersonalizeTestFunction;
}).pipe(Effect.provide(PersonalizeTestFunctionLive)),
);
// Resolve this generation's references before destroy removes its state.
const refOptions = {
stack: sharedStack.name,
stage: sharedStack.stage,
};
const group = yield* AWS.Personalize.DatasetGroup.ref(
"BindingsGroup",
refOptions,
);
const tracker = yield* AWS.Personalize.EventTracker.ref(
"Tracker",
refOptions,
);
const datasets = yield* Effect.forEach(
["Interactions", "Items", "Users"],
(id) => AWS.Personalize.Dataset.ref(id, refOptions),
);
const schemas = yield* Effect.forEach(
["InteractionsSchema", "ItemsSchema", "UsersSchema"],
(id) => AWS.Personalize.Schema.ref(id, refOptions),
);
resourceArns = yield* Effect.all({
group: Output.evaluate(group.datasetGroupArn, {}),
tracker: Output.evaluate(tracker.eventTrackerArn, {}),
datasets: Effect.forEach(datasets, (dataset) =>
Output.evaluate(dataset.datasetArn, {}),
),
schemas: Effect.forEach(schemas, (schema) =>
Output.evaluate(schema.schemaArn, {}),
),
}).pipe(Effect.provide(sharedStack.state));
expect(functionUrl).toBeTruthy();
baseUrl = functionUrl!.replace(/\/+$/, "");
const readinessUrl = `${baseUrl}/bindings`;
yield* HttpClient.get(readinessUrl).pipe(
Effect.flatMap((response) =>
response.status === 200
? Effect.succeed(response)
: Effect.fail(
new Error(`Function not ready: ${response.status}`),
),
),
Effect.retry({ schedule: readinessPolicy }),
);
}),
{ timeout: 480_000 },
);
// Verify only resources owned by this fixture generation. Old runs with the
// same stack-name prefix must not hold this run's cleanup open.
const assertPersonalizeResourcesGone = Core.withProviders(
Effect.gen(function* () {
if (!resourceArns) return;
yield* Effect.forEach(
[
personalize
.describeDatasetGroup({
datasetGroupArn: resourceArns.group,
})
.pipe(Effect.asVoid),
personalize
.describeEventTracker({
eventTrackerArn: resourceArns.tracker,
})
.pipe(Effect.asVoid),
...resourceArns.datasets.map((datasetArn) =>
personalize.describeDataset({ datasetArn }).pipe(Effect.asVoid),
),
...resourceArns.schemas.map((schemaArn) =>
personalize.describeSchema({ schemaArn }).pipe(Effect.asVoid),
),
],
(describe) =>
describe.pipe(
Effect.as(false),
Effect.catchTag("ResourceNotFoundException", () =>
Effect.succeed(true),
),
Effect.repeat({
schedule: Schedule.spaced("2 seconds"),
until: (gone) => gone,
times: 10,
}),
Effect.tap((gone) => Effect.sync(() => expect(gone).toBe(true))),
),
{ concurrency: 4 },
);
}),
testOptions,
sharedStack.name,
);
afterAll(
sharedStack
.destroy()
.pipe(Effect.andThen(assertPersonalizeResourcesGone)),
{ timeout: 420_000 },
);
describe("binding registration", () => {
test.provider("all nineteen capabilities initialize in the runtime", () =>
Effect.gen(function* () {
const response = (yield* getJson("/bindings")) as { bound: string[] };
expect(response.bound).toHaveLength(19);
}),
);
});
describe("PutEvents", () => {
test.provider("records a real event through the event tracker", () =>
Effect.gen(function* () {
const tag = yield* routeTag("/put-events");
expect(tag).toBe("Recorded");
}),
);
});
describe("PutItems", () => {
test.provider("upserts a real item into the Items dataset", () =>
Effect.gen(function* () {
const tag = yield* routeTag("/put-items");
expect(tag).toBe("Recorded");
}),
);
});
describe("PutUsers", () => {
test.provider("upserts a real user into the Users dataset", () =>
Effect.gen(function* () {
const tag = yield* routeTag("/put-users");
expect(tag).toBe("Recorded");
}),
);
});
describe("PutActions", () => {
test.provider(
"surfaces a typed tag when the bound dataset is not an Actions dataset",
() =>
Effect.gen(function* () {
const tag = yield* routeTag("/put-actions-probe");
expect(EXPECTED_PROBE_TAGS).toContain(tag);
}),
);
});
describe("PutActionInteractions", () => {
test.provider(
"surfaces a typed tag without an Action interactions dataset",
() =>
Effect.gen(function* () {
const tag = yield* routeTag("/put-action-interactions-probe");
expect(EXPECTED_PROBE_TAGS).toContain(tag);
}),
);
});
describe("GetRecommendations", () => {
test.provider("surfaces a typed tag for a nonexistent campaign", () =>
Effect.gen(function* () {
const arns = yield* probeArns;
const tag = yield* probeTag("/recommendations-probe", arns.campaign);
expect(EXPECTED_PROBE_TAGS).toContain(tag);
}),
);
});
describe("GetPersonalizedRanking", () => {
test.provider("surfaces a typed tag for a nonexistent campaign", () =>
Effect.gen(function* () {
const arns = yield* probeArns;
const tag = yield* probeTag("/ranking-probe", arns.campaign);
expect(EXPECTED_PROBE_TAGS).toContain(tag);
}),
);
});
describe("GetActionRecommendations", () => {
test.provider("surfaces a typed tag for a nonexistent campaign", () =>
Effect.gen(function* () {
const arns = yield* probeArns;
const tag = yield* probeTag(
"/action-recommendations-probe",
arns.campaign,
);
expect(EXPECTED_PROBE_TAGS).toContain(tag);
}),
);
});
describe("CreateDatasetImportJob", () => {
test.provider("rejects a nonexistent dataset with a typed tag", () =>
Effect.gen(function* () {
const arns = yield* probeArns;
const tag = yield* probeTagWithRole(
"/import-create-probe",
arns.dataset,
);
expect(EXPECTED_PROBE_TAGS).toContain(tag);
}),
);
});
describe("DescribeDatasetImportJob", () => {
test.provider("surfaces a typed tag for a nonexistent import job", () =>
Effect.gen(function* () {
const arns = yield* probeArns;
const tag = yield* probeTag("/import-probe", arns.importJob);
expect(EXPECTED_PROBE_TAGS).toContain(tag);
}),
);
});
describe("CreateSolution", () => {
test.provider(
"rejects a nonexistent dataset group with a typed tag",
() =>
Effect.gen(function* () {
const arns = yield* probeArns;
const tag = yield* probeTag(
"/solution-create-probe",
arns.datasetGroup,
);
expect(EXPECTED_PROBE_TAGS).toContain(tag);
}),
);
});
describe("CreateSolutionVersion", () => {
test.provider("rejects a nonexistent solution with a typed tag", () =>
Effect.gen(function* () {
const arns = yield* probeArns;
const tag = yield* probeTag(
"/solution-version-create-probe",
arns.solution,
);
expect(EXPECTED_PROBE_TAGS).toContain(tag);
}),
);
});
describe("DescribeSolutionVersion", () => {
test.provider("surfaces a typed tag for a nonexistent version", () =>
Effect.gen(function* () {
const arns = yield* probeArns;
const tag = yield* probeTag(
"/solution-version-probe",
arns.solutionVersion,
);
expect(EXPECTED_PROBE_TAGS).toContain(tag);
}),
);
});
describe("GetSolutionMetrics", () => {
test.provider("surfaces a typed tag for a nonexistent version", () =>
Effect.gen(function* () {
const arns = yield* probeArns;
const tag = yield* probeTag(
"/solution-metrics-probe",
arns.solutionVersion,
);
expect(EXPECTED_PROBE_TAGS).toContain(tag);
}),
);
});
describe("CreateCampaign", () => {
test.provider(
"rejects a nonexistent solution version with a typed tag",
() =>
Effect.gen(function* () {
const arns = yield* probeArns;
const tag = yield* probeTag(
"/campaign-create-probe",
arns.solutionVersion,
);
expect(EXPECTED_PROBE_TAGS).toContain(tag);
}),
);
});
describe("UpdateCampaign", () => {
test.provider("surfaces a typed tag for a nonexistent campaign", () =>
Effect.gen(function* () {
const arns = yield* probeArns;
const tag = yield* probeTag("/campaign-update-probe", arns.campaign);
expect(EXPECTED_PROBE_TAGS).toContain(tag);
}),
);
});
describe("DescribeCampaign", () => {
test.provider("surfaces a typed tag for a nonexistent campaign", () =>
Effect.gen(function* () {
const arns = yield* probeArns;
const tag = yield* probeTag("/campaign-probe", arns.campaign);
expect(EXPECTED_PROBE_TAGS).toContain(tag);
}),
);
});
describe("CreateBatchInferenceJob", () => {
test.provider(
"rejects a nonexistent solution version with a typed tag",
() =>
Effect.gen(function* () {
const arns = yield* probeArns;
const tag = yield* probeTagWithRole(
"/batch-create-probe",
arns.solutionVersion,
);
expect(EXPECTED_PROBE_TAGS).toContain(tag);
}),
);
});
describe("DescribeBatchInferenceJob", () => {
test.provider("surfaces a typed tag for a nonexistent batch job", () =>
Effect.gen(function* () {
const arns = yield* probeArns;
const tag = yield* probeTag("/batch-probe", arns.batchJob);
expect(EXPECTED_PROBE_TAGS).toContain(tag);
}),
);
});
},
);