Part 3: Testing
Write an integration test that deploys the stack from Part 2, calls the program, and checks the disk directly.
Install the test harness
Section titled “Install the test harness”pnpm add -D https://pkg.alchemy.run/@alchemy.run/test/pr:2930:0cabab6bun add -d https://pkg.alchemy.run/@alchemy.run/test/pr:2930:0cabab6npm install -D https://pkg.alchemy.run/@alchemy.run/test/pr:2930:0cabab6yarn add -D https://pkg.alchemy.run/@alchemy.run/test/pr:2930:0cabab6Create the test file
Section titled “Create the test file”Test.make takes the same providers and state as your Stack:
import { expect } from "bun:test";import * as Archil from "@alchemy.run/archil";import * as Test from "@alchemy.run/test/Bun";import * as Alchemy from "alchemy";import * as Effect from "effect/Effect";import * as HttpClient from "effect/http/HttpClient";import * as HttpClientRequest from "effect/http/HttpClientRequest";import * as Schedule from "effect/Schedule";import Stack from "../alchemy.run.ts";
const { test, beforeAll, afterAll, deploy, destroy } = Test.make({ providers: Archil.providers(), state: Alchemy.localState(), profile: process.env.ALCHEMY_PROFILE,});Deploy before the tests
Section titled “Deploy before the tests”beforeAll(deploy(Stack)) deploys once for the whole file and hands back
the stack’s outputs. afterAll destroys it unless NO_DESTROY is set:
const stack = beforeAll(deploy(Stack), { timeout: 300_000 });afterAll.skipIf(!!process.env.NO_DESTROY)(destroy(Stack), { timeout: 180_000 });Retry while the program starts
Section titled “Retry while the program starts”A fresh hostname, or a sandbox waking up, can take a few seconds to answer:
const send = (request: HttpClientRequest.HttpClientRequest) => Effect.gen(function* () { const client = yield* HttpClient.HttpClient; return yield* client.execute(request).pipe( Effect.flatMap((res) => res.status >= 500 ? Effect.fail(new Error(`HTTP ${res.status}`)) : Effect.succeed(res), ), Effect.retry({ schedule: Schedule.spaced("2 seconds"), times: 20 }), ); });Test the routes
Section titled “Test the routes”test( "GET of a missing file is a 404", Effect.gen(function* () { const { url } = yield* stack; const res = yield* send(HttpClientRequest.get(`${url}/missing.txt`)); expect(res.status).toBe(404); }), { timeout: 180_000 },);Check the disk directly
Section titled “Check the disk directly”test.provider runs with your Archil credentials, so the test can read
the disk out of band and confirm the file is really there:
import * as Objects from "@distilled.cloud/archil/Objects";
test.provider( "PUT writes a file to the disk and GET reads it back", () => Effect.gen(function* () { const { url, diskId } = yield* stack; const key = `notes/${crypto.randomUUID()}.txt`; yield* send( HttpClientRequest.put(`${url}/${key}`).pipe(HttpClientRequest.bodyText("on the disk")), ); const text = yield* Objects.getObjectText({ disk: String(diskId) }, key).pipe( Effect.provide(Objects.ObjectCredentialsFromApiKey), ); expect(text).toBe("on the disk"); }), { timeout: 180_000 },);Run the tests
Section titled “Run the tests”ALCHEMY_PROFILE=default bun testSet NO_DESTROY=1 to keep the stack between runs while you iterate.
The test deploys the real stack, calls it over HTTP, and checks Archil directly. In Part 4 you’ll deploy separate copies with stages.