t3-code-android-nightly/.repos/alchemy-effect/packages/alchemy/test/Fly/Postgres.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

330 lines
10 KiB
TypeScript

import * as machines from "@distilled.cloud/fly-io/machines";
import * as mpg from "@distilled.cloud/fly-io/mpg";
import * as Fly from "@/Fly";
import * as Provider from "@/Provider";
import * as Test from "@/Test/Alchemy";
import { expect } from "alchemy-test";
import * as Data from "effect/Data";
import * as Effect from "effect/Effect";
import { MinimumLogLevel } from "effect/References";
import * as Schedule from "effect/Schedule";
import * as HttpClient from "effect/http/HttpClient";
import PostgresApi, { Db, MpgIp, MpgSite } from "./fixtures/postgres-api.ts";
const { test } = Test.make({ providers: Fly.providers() });
const logLevel = Effect.provideService(
MinimumLogLevel,
process.env.DEBUG ? "Debug" : "Info",
);
const waitUntilClusterGone = (clusterId: string) =>
mpg.getClusterById({ id: clusterId }).pipe(
Effect.map((res) =>
res.data === undefined || res.data.status === "deleted"
? ("gone" as const)
: ("found" as const),
),
Effect.catchTag("NotFound", () => Effect.succeed("gone" as const)),
Effect.repeat({
schedule: Schedule.spaced("2 seconds"),
until: (status) => status === "gone",
times: 10,
}),
);
const waitUntilReady = (clusterId: string) =>
mpg.getClusterById({ id: clusterId }).pipe(
Effect.map((res) => res.data?.status),
Effect.catchTag("NotFound", () => Effect.succeed(undefined)),
Effect.repeat({
schedule: Schedule.spaced("5 seconds"),
until: (status) => status === "ready" || status === "error",
times: 24,
}),
);
const waitUntilAppGone = (appName: string) =>
machines.getApp({ app_name: appName }).pipe(
Effect.as("found" as const),
Effect.catchTag("NotFound", () => Effect.succeed("gone" as const)),
Effect.repeat({
schedule: Schedule.spaced("1 second"),
until: (status) => status === "gone",
times: 10,
}),
);
const secretNames = (appName: string) =>
machines
.listSecrets({
app_name: appName,
show_secrets: false,
})
.pipe(
Effect.map((res) => (res.secrets ?? []).map((secret) => secret.name)),
Effect.catchTag(["NotFound", "Forbidden"], () =>
Effect.succeed([] as Array<string | undefined>),
),
);
test.provider(
"createCluster with an invalid region is a typed BadRequest",
(stack) =>
Effect.gen(function* () {
yield* stack.destroy();
const orgSlug = yield* Fly.currentOrgSlug();
const error = yield* mpg
.createCluster({
org_slug: orgSlug,
name: "alchemy-mpg-invalid-region",
region: "not-a-region",
plan: "basic",
storage_in_gb: 10,
})
.pipe(Effect.flip);
expect(error._tag).toEqual("BadRequest");
expect(error.message).toEqual("region is invalid");
yield* stack.destroy();
}).pipe(logLevel),
{ tags: ["provider:fly", "provider:fly:postgres", "live"], timeout: 90_000 },
);
test.provider(
"create, attach, list, and destroy a managed postgres cluster",
(stack) =>
Effect.gen(function* () {
yield* stack.destroy();
const created = yield* stack.deploy(
Effect.gen(function* () {
const app = yield* Fly.App("MpgSite");
const db = yield* Fly.Postgres("Db", {
region: "iad",
plan: "basic",
volumeSizeGb: 10,
});
return { app, db };
}),
);
expect(created.db.clusterId).toEqual(expect.any(String));
expect(created.db.clusterId.length).toBeGreaterThan(0);
expect(created.db.name).toEqual(expect.any(String));
expect(created.db.region).toEqual("iad");
expect(created.db.plan).toEqual("basic");
const status = yield* waitUntilReady(created.db.clusterId);
expect(status).toEqual("ready");
const fetched = yield* mpg.getClusterById({
id: created.db.clusterId,
});
expect(fetched.data?.id).toEqual(created.db.clusterId);
expect(fetched.data?.name).toEqual(created.db.name);
expect(fetched.data?.region).toEqual("iad");
expect(fetched.data?.status).toEqual("ready");
const updated = yield* stack.deploy(
Effect.gen(function* () {
const app = yield* Fly.App("MpgSite");
const db = yield* Fly.Postgres("Db", {
region: "iad",
plan: "basic",
volumeSizeGb: 10,
});
return { app, db };
}),
);
expect(updated.db.clusterId).toEqual(created.db.clusterId);
expect(updated.db.name).toEqual(created.db.name);
expect(created.db.pooledConnectionUri.length).toBeGreaterThan(0);
const provider = yield* Provider.findProvider(Fly.Postgres);
const all = yield* provider.list();
const listed = all.find((row) => row.clusterId === created.db.clusterId);
expect(listed).toBeDefined();
expect(listed?.name).toEqual(created.db.name);
expect(listed?.region).toEqual("iad");
yield* stack.destroy();
const clusterGone = yield* waitUntilClusterGone(created.db.clusterId);
expect(clusterGone).toEqual("gone");
const appGone = yield* waitUntilAppGone(created.app.appName);
expect(appGone).toEqual("gone");
}).pipe(logLevel),
{
tags: [
"provider:fly",
"provider:fly:app",
"provider:fly:machine",
"provider:fly:postgres",
"live",
],
timeout: 180_000,
},
);
test.provider(
"a Service connects and SELECTs through ConnectPostgres",
(stack) =>
Effect.gen(function* () {
yield* stack.destroy();
const deployed = yield* stack.deploy(
Effect.gen(function* () {
const app = yield* MpgSite;
const db = yield* Db;
const ip = yield* MpgIp;
const api = yield* PostgresApi;
return { app, db, ip, api };
}),
);
expect(deployed.db.clusterId).toEqual(expect.any(String));
expect(deployed.api.url).toEqual(
`https://${deployed.app.appName}.fly.dev`,
);
const names = yield* secretNames(deployed.app.appName);
expect(names).toContain("DATABASE_URL");
yield* Effect.logInfo("ConnectPostgres deployed", {
url: deployed.api.url,
machineId: deployed.api.machineId,
state: deployed.api.state,
});
class NotReady extends Data.TaggedError("NotReady")<{
status: number;
body?: unknown;
}> {
override get message() {
return this.body === undefined
? `status ${this.status}`
: `status ${this.status}: ${JSON.stringify(this.body)}`;
}
}
const diagnose = Effect.gen(function* () {
const appName = deployed.api.appName;
const machineId = deployed.api.machineId;
const machine = yield* machines.getMachine({
app_name: appName,
machine_id: machineId,
});
const events = yield* machines.listMachineEvents({
app_name: appName,
machine_id: machineId,
limit: 20,
});
const listed = yield* machines.listMachineProcesses({
app_name: appName,
machine_id: machineId,
});
const processes = (Array.isArray(listed) ? listed : []).filter(
(proc) => typeof proc.command === "string" && proc.command.length > 0,
);
const ips = yield* machines.listAppIPAssignments({
app_name: appName,
});
const exec = yield* machines
.execMachine({
app_name: appName,
machine_id: machineId,
command: [
"bun",
"-e",
"const r = await fetch('http://127.0.0.1:3000/ping'); console.log(r.status, await r.text())",
],
timeout: 10,
})
.pipe(
Effect.catchCause((cause) =>
Effect.succeed({
stdout: undefined,
stderr: String(cause),
exit_code: -1,
}),
),
);
yield* Effect.logError("ConnectPostgres HTTP diagnose", {
state: machine.state,
checks: machine.checks,
events,
processes,
ips,
exec,
});
}).pipe(Effect.ignore);
const getJson = (path: string) =>
HttpClient.get(`${deployed.api.url}${path}`).pipe(
Effect.flatMap((res) =>
res.json.pipe(
Effect.flatMap((body) =>
res.status === 200
? Effect.succeed(body as { ok?: boolean; rows?: unknown })
: Effect.fail(new NotReady({ status: res.status, body })),
),
),
),
Effect.timeoutOrElse({
duration: "8 seconds",
orElse: () => Effect.fail(new NotReady({ status: 0 })),
}),
Effect.retry({
while: (e) =>
e._tag === "NotReady" &&
(e.status === 0 ||
e.status === 404 ||
e.status === 502 ||
e.status === 503),
schedule: Schedule.spaced("3 seconds"),
times: 8,
}),
Effect.timeout("60 seconds"),
Effect.tapError(() => diagnose),
);
const ping = yield* getJson("/ping");
expect(ping.ok).toEqual(true);
for (const path of ["/health", "/direct"]) {
const health = yield* getJson(path);
const executed = health.rows;
const list = Array.isArray(executed)
? executed
: executed !== null &&
typeof executed === "object" &&
Array.isArray((executed as { rows?: unknown }).rows)
? (executed as { rows: unknown[] }).rows
: [];
const first = list[0] as { ok?: unknown } | undefined;
expect(Number(first?.ok)).toEqual(1);
}
yield* stack.destroy();
const clusterGone = yield* waitUntilClusterGone(deployed.db.clusterId);
expect(clusterGone).toEqual("gone");
const appGone = yield* waitUntilAppGone(deployed.app.appName);
expect(appGone).toEqual("gone");
}).pipe(logLevel),
{
tags: [
"provider:fly",
"provider:fly:app",
"provider:fly:ipassignment",
"provider:fly:machine",
"provider:fly:postgres",
"provider:fly:service",
"live",
],
timeout: 300_000,
},
);