cast/test/manifest.test.ts
claude-hdb a10349d835 feat: cast — the Coolify executor, extracted from the infra state repo
Public tool, private state. cast holds no hostnames, no bindings, no
secrets: it joins a product repo's .infra/ manifest with a state directory
you point it at, and makes Coolify match.

Extracted from heavy-duty/infra, which was half tool and half state — the
inconsistency that made it impossible to say whether "infra" named a CLI
or a runbook. rig builds the boxes; cast fills them; infra is what they
are filled with.

Two changes were required to make it genuinely stateless and publishable:

- The implicit cwd contract (environments.yaml / secrets/ / .coolify.env
  resolved against the working directory, silently reading the wrong file
  from the wrong place) is now an explicit --state <dir> / $CAST_STATE.
- BANNED_IN_PROD — a hardcoded list of one product's ALLOW_* flags, the
  only product knowledge in the executor — becomes the generic, operator-
  owned environments.<env>.forbidden_var_patterns. The guard now lives in
  private state, so a product-side change cannot lower its own guard, and
  it is a pattern rather than a list, so it catches unforeseen siblings.

Age identities resolve as $CAST_AGE_KEY_FILE_<ENV> then
~/.config/cast/age-<env>.key — which is the entire attended-vs-unattended
apply mechanism, with no environment names known to the tool.

Instance identity (org names, the GitHub App name, founder domains) is out
of the fixtures and out of register-github-app.sh, which took APP_NAME and
ORG as arguments rather than baking them in.

69 tests green; bin/cast + curl installer mirror rig's shape.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-07-11 12:25:44 +00:00

138 lines
4.1 KiB
TypeScript

import { describe, expect, it } from "vitest";
import { loadBindings } from "../src/bindings.js";
import { loadManifest } from "../src/manifest.js";
const FIX = new URL("./fixtures/", import.meta.url).pathname;
describe("loadManifest", () => {
it("parses a valid manifest", () => {
const m = loadManifest(`${FIX}manifest.yaml`);
expect(m.project).toBe("widget");
expect(m.environments.prod.applications["core-api"].build.pack).toBe(
"nixpacks",
);
expect(m.environments.prod.databases?.postgres.backup?.retention).toBe(7);
});
it("rejects unknown build packs", () => {
expect(() =>
loadManifest(`${FIX}manifest.yaml`, {
overrideText: `
project: x
environments:
prod:
applications:
a:
source: { repo: o/r, branch: main }
build: { pack: docker-compose, base_directory: / }
domains: []
`,
}),
).toThrow(/pack/);
});
it("rejects instance identity in manifests (no uuid-like fields)", () => {
expect(() =>
loadManifest(`${FIX}manifest.yaml`, {
overrideText: `
project: x
environments:
prod:
applications:
a:
source: { repo: o/r, branch: main }
build: { pack: static, base_directory: / }
domains: []
server_uuid: abc123
`,
}),
).toThrow(/unrecognized|server_uuid/i);
});
it("accepts a dockercompose app with compose_file + service_domains and no port/healthcheck/domains", () => {
const m = loadManifest(`${FIX}manifest.yaml`, {
overrideText: `
project: widget
environments:
prod:
applications:
core:
source: { repo: acme/widget, branch: main }
build: { pack: dockercompose, base_directory: /, compose_file: docker-compose.yaml }
service_domains:
api: ["https://api.example.com"]
env_template: core.prod.env.template
`,
});
const app = m.environments.prod.applications.core;
expect(app.build.pack).toBe("dockercompose");
expect(app.build.compose_file).toBe("docker-compose.yaml");
expect(app.service_domains).toEqual({ api: ["https://api.example.com"] });
expect(app.port).toBeUndefined();
expect(app.healthcheck).toBeUndefined();
expect(app.domains).toBeUndefined();
});
it("rejects a dockercompose app without compose_file", () => {
expect(() =>
loadManifest(`${FIX}manifest.yaml`, {
overrideText: `
project: widget
environments:
prod:
applications:
core:
source: { repo: acme/widget, branch: main }
build: { pack: dockercompose, base_directory: / }
service_domains:
api: ["https://api.example.com"]
`,
}),
).toThrow(/compose_file/);
});
it("rejects a dockercompose app with top-level domains", () => {
expect(() =>
loadManifest(`${FIX}manifest.yaml`, {
overrideText: `
project: widget
environments:
prod:
applications:
core:
source: { repo: acme/widget, branch: main }
build: { pack: dockercompose, base_directory: /, compose_file: docker-compose.yaml }
service_domains:
api: ["https://api.example.com"]
domains: ["https://api.example.com"]
`,
}),
).toThrow(/domains/);
});
it("rejects service_domains on a nixpacks app", () => {
expect(() =>
loadManifest(`${FIX}manifest.yaml`, {
overrideText: `
project: widget
environments:
prod:
applications:
core:
source: { repo: acme/widget, branch: main }
build: { pack: nixpacks, base_directory: / }
domains: ["https://api.example.com"]
service_domains:
api: ["https://api.example.com"]
`,
}),
).toThrow(/service_domains/);
});
});
describe("loadBindings", () => {
it("parses bindings", () => {
const b = loadBindings(`${FIX}environments.yaml`);
expect(b.environments.prod.server).toBe("prod-box");
expect(b.github_apps.widget).toBe("my-github-app");
});
it("carries an environment's forbidden_var_patterns through", () => {
const b = loadBindings(`${FIX}environments.yaml`);
expect(b.environments.prod.forbidden_var_patterns).toEqual(["^ALLOW_"]);
expect(b.environments.staging.forbidden_var_patterns).toBeUndefined();
});
});