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

1261 lines
41 KiB
TypeScript

import * as Output from "@/Output";
import { ref as makeRef } from "@/Ref";
import type { ResourceLike } from "@/Resource";
import { Stack } from "@/Stack";
import { Stage } from "@/Stage";
import { inMemoryState } from "@/State/InMemoryState";
import type { ResourceState } from "@/State/ResourceState";
import { describe, expect, it } from "alchemy-test";
import * as Cause from "effect/Cause";
import * as Config from "effect/Config";
import * as ConfigProvider from "effect/ConfigProvider";
import * as Context from "effect/Context";
import * as Effect from "effect/Effect";
import * as Exit from "effect/Exit";
import * as Layer from "effect/Layer";
import * as Redacted from "effect/Redacted";
const provideState = <A, E, R>(effect: Effect.Effect<A, E, R>) =>
effect.pipe(Effect.provide(inMemoryState()));
const fakeResource = <T extends string, A extends object>(
type: T,
fqn: string,
logicalId: string = fqn,
): ResourceLike<T, any, A> =>
({
Type: type,
FQN: fqn,
LogicalId: logicalId,
Namespace: undefined,
}) as any;
describe("Output.evaluate", { tags: ["unit", "local"] }, () => {
describe("primitives and plain values", () => {
it.effect("returns primitive values as-is", () =>
provideState(
Effect.gen(function* () {
expect(yield* Output.evaluate(42, {})).toBe(42);
expect(yield* Output.evaluate("hello", {})).toBe("hello");
expect(yield* Output.evaluate(true, {})).toBe(true);
expect(yield* Output.evaluate(null, {})).toBe(null);
expect(yield* Output.evaluate(undefined, {})).toBe(undefined);
}),
),
);
it.effect("recursively evaluates plain objects", () =>
provideState(
Effect.gen(function* () {
const result = yield* Output.evaluate({ a: 1, b: { c: "x" } }, {});
expect(result).toEqual({ a: 1, b: { c: "x" } });
}),
),
);
it.effect("recursively evaluates arrays", () =>
provideState(
Effect.gen(function* () {
const result = yield* Output.evaluate([1, "two", { k: 3 }], {});
expect(result).toEqual([1, "two", { k: 3 }]);
}),
),
);
});
describe("Redacted", () => {
it.effect("preserves Redacted values at the top level", () =>
provideState(
Effect.gen(function* () {
const secret = Redacted.make("hunter2");
const result = yield* Output.evaluate(secret, {});
expect(Redacted.isRedacted(result)).toBe(true);
expect(Redacted.value(result as Redacted.Redacted<string>)).toBe(
"hunter2",
);
}),
),
);
it.effect("preserves Redacted values nested inside an object", () =>
provideState(
Effect.gen(function* () {
const secret = Redacted.make("hunter2");
const result = yield* Output.evaluate(
{ value: secret, name: "x" },
{},
);
expect(result.name).toBe("x");
expect(Redacted.isRedacted(result.value)).toBe(true);
expect(Redacted.value(result.value)).toBe("hunter2");
}),
),
);
it.effect("preserves Redacted values nested inside an array", () =>
provideState(
Effect.gen(function* () {
const secret = Redacted.make("hunter2");
const [result] = yield* Output.evaluate([secret], {});
expect(Redacted.isRedacted(result)).toBe(true);
expect(Redacted.value(result)).toBe("hunter2");
}),
),
);
});
describe("Config", () => {
it.effect("resolves a Config value at the top level", () =>
provideState(
Effect.gen(function* () {
const result = yield* Output.evaluate(Config.succeed(1337), {});
expect(result).toBe(1337);
}),
),
);
it.effect("resolves a Config value nested inside an object", () =>
provideState(
Effect.gen(function* () {
const result = yield* Output.evaluate(
{ port: Config.succeed(8080), host: "localhost" },
{},
);
expect(result).toEqual({ port: 8080, host: "localhost" });
}),
),
);
it.effect("resolves a Config value nested inside an array", () =>
provideState(
Effect.gen(function* () {
const [result] = yield* Output.evaluate([Config.succeed(42)], {});
expect(result).toBe(42);
}),
),
);
it.effect("resolves a Config against the ConfigProvider environment", () =>
provideState(
Effect.gen(function* () {
const result = yield* Output.evaluate(
{ port: Config.Number("PORT").pipe(Config.withDefault(1337)) },
{},
).pipe(
Effect.provide(
ConfigProvider.layer(
ConfigProvider.fromEnv({ env: { PORT: "8080" } }),
),
),
);
expect(result).toEqual({ port: 8080 });
}),
),
);
it.effect("a Config resolving to a Redacted keeps it wrapped", () =>
provideState(
Effect.gen(function* () {
const result = yield* Output.evaluate(
Config.succeed(Redacted.make("hunter2")),
{},
);
expect(Redacted.isRedacted(result)).toBe(true);
expect(
Redacted.value(result as unknown as Redacted.Redacted<string>),
).toBe("hunter2");
}),
),
);
});
describe("LiteralExpr", () => {
it.effect("evaluates Output.literal(value)", () =>
provideState(
Effect.gen(function* () {
const expr = Output.literal("foo");
expect(yield* Output.evaluate(expr, {})).toBe("foo");
}),
),
);
it.effect("evaluates a literal nested within an object", () =>
provideState(
Effect.gen(function* () {
const result = yield* Output.evaluate(
{ greeting: Output.literal("hi") },
{},
);
expect(result).toEqual({ greeting: "hi" });
}),
),
);
});
describe("ResourceExpr", () => {
it.effect("resolves to the upstream value keyed by FQN", () =>
provideState(
Effect.gen(function* () {
const src = fakeResource("Test.Bucket", "MyBucket");
const expr = Output.of(src);
const result = yield* Output.evaluate(expr, {
MyBucket: { name: "my-bucket" },
});
expect(result).toEqual({ name: "my-bucket" });
}),
),
);
it.effect("fails with MissingSourceError when upstream is absent", () =>
provideState(
Effect.gen(function* () {
const src = fakeResource("Test.Bucket", "Missing");
const expr = Output.of(src);
const exit = yield* Effect.exit(Output.evaluate(expr, {}));
expect(Exit.isFailure(exit)).toBe(true);
if (Exit.isFailure(exit)) {
const failure = exit.cause.toJSON() as any;
expect(JSON.stringify(failure)).toContain("MissingSourceError");
}
}),
),
);
it.effect("evaluates a raw resource (isResource branch)", () =>
provideState(
Effect.gen(function* () {
const src = fakeResource("Test.Bucket", "RawBucket");
const result = yield* Output.evaluate(src as any, {
RawBucket: { ok: true },
});
expect(result).toEqual({ ok: true });
}),
),
);
it.effect("raw resource with missing upstream fails", () =>
provideState(
Effect.gen(function* () {
const src = fakeResource("Test.Bucket", "Gone");
const exit = yield* Effect.exit(Output.evaluate(src as any, {}));
expect(Exit.isFailure(exit)).toBe(true);
}),
),
);
it("classifies resource expressions when a stable kind shadows the discriminator", () => {
const src = fakeResource("Test.Database", "Database");
const expr = new Output.ResourceExpr(src, { kind: "postgresql" });
expect((expr as any).kind).toBe("postgresql");
expect(Output.isResourceExpr(expr)).toBe(true);
});
});
describe("PropExpr", () => {
it.effect("accesses a property on a resource expression", () =>
provideState(
Effect.gen(function* () {
const src = fakeResource<"Test.Bucket", { name: string }>(
"Test.Bucket",
"B",
);
const expr = Output.of(src) as any;
const result = yield* Output.evaluate(expr.name, {
B: { name: "the-name" },
});
expect(result).toBe("the-name");
}),
),
);
it.effect("returns undefined when accessing missing property", () =>
provideState(
Effect.gen(function* () {
const src = fakeResource("Test.Bucket", "B2");
const expr = Output.of(src) as any;
const result = yield* Output.evaluate(expr.missing, {
B2: { other: 1 },
});
expect(result).toBeUndefined();
}),
),
);
it.effect("supports nested property access", () =>
provideState(
Effect.gen(function* () {
const src = fakeResource("Test.Bucket", "B3");
const expr = Output.of(src) as any;
const result = yield* Output.evaluate(expr.nested.deep, {
B3: { nested: { deep: "value" } },
});
expect(result).toBe("value");
}),
),
);
});
describe("ApplyExpr (map)", () => {
it.effect("applies a synchronous function over a literal", () =>
provideState(
Effect.gen(function* () {
const expr = Output.map(Output.literal(2), (n) => n * 3);
expect(yield* Output.evaluate(expr, {})).toBe(6);
}),
),
);
it.effect("composes multiple maps", () =>
provideState(
Effect.gen(function* () {
const expr = Output.literal(2).pipe(
Output.map((n: number) => n + 1),
Output.map((n: number) => n * 10),
);
expect(yield* Output.evaluate(expr, {})).toBe(30);
}),
),
);
it.effect("maps over a resource attribute", () =>
provideState(
Effect.gen(function* () {
const src = fakeResource("Test.Bucket", "B4");
const expr = (Output.of(src) as any).name.pipe(
Output.map((s: string) => s.toUpperCase()),
);
const result = yield* Output.evaluate(expr, {
B4: { name: "abc" },
});
expect(result).toBe("ABC");
}),
),
);
});
describe("EffectExpr (mapEffect)", () => {
it.effect("evaluates an effectful transformation", () =>
provideState(
Effect.gen(function* () {
const expr = Output.literal(5).pipe(
Output.mapEffect((n: number) => Effect.succeed(n * 2)),
);
expect(yield* Output.evaluate(expr, {})).toBe(10);
}),
),
);
it.effect("chains multiple effectful transformations", () =>
provideState(
Effect.gen(function* () {
const expr = Output.literal("a").pipe(
Output.mapEffect((s: string) => Effect.succeed(s + "b")),
Output.mapEffect((s) => Effect.succeed(s + "c")),
);
expect(yield* Output.evaluate(expr, {})).toBe("abc");
}),
),
);
});
describe("FlatMapExpr (flatMap)", () => {
it.effect("flattens an Output returned from the function", () =>
provideState(
Effect.gen(function* () {
const expr = Output.literal(5).pipe(
Output.flatMap((n: number) => Output.literal(n * 2)),
);
expect(yield* Output.evaluate(expr, {})).toBe(10);
}),
),
);
it.effect("supports the data-first form", () =>
provideState(
Effect.gen(function* () {
const expr = Output.flatMap(Output.literal("a"), (s: string) =>
Output.literal(s + "b"),
);
expect(yield* Output.evaluate(expr, {})).toBe("ab");
}),
),
);
it.effect("chains multiple flatMaps", () =>
provideState(
Effect.gen(function* () {
const expr = Output.literal(1).pipe(
Output.flatMap((n: number) => Output.literal(n + 1)),
Output.flatMap((n: number) => Output.literal(n * 10)),
);
expect(yield* Output.evaluate(expr, {})).toBe(20);
}),
),
);
it.effect("flatMaps into another resource's output", () =>
provideState(
Effect.gen(function* () {
const a = fakeResource("Test.A", "FA");
const b = fakeResource("Test.B", "FB");
const expr = (Output.of(a) as any).name.pipe(
Output.flatMap(() => (Output.of(b) as any).name),
);
const result = yield* Output.evaluate(expr, {
FA: { name: "a-name" },
FB: { name: "b-name" },
});
expect(result).toBe("b-name");
}),
),
);
it.effect("can flatMap into a non-Output literal value", () =>
provideState(
Effect.gen(function* () {
const expr = Output.literal(3).pipe(
Output.flatMap((n: number) => Output.asOutput(n + 1)),
);
expect(yield* Output.evaluate(expr, {})).toBe(4);
}),
),
);
it("tracks only the source expression as upstream", () => {
const a = fakeResource("Test.A", "UA");
const expr = (Output.of(a) as any).name.pipe(
Output.flatMap((s: string) => Output.literal(s)),
);
expect(Object.keys(Output.upstream(expr))).toEqual(["UA"]);
});
});
describe("method-style combinators on a proxied Output", () => {
// Resource outputs are Proxies, so `.map(fn)` / `.mapEffect(fn)` /
// `.flatMap(fn)` / `.apply(fn)` / `.effect(fn)` must be callable as
// methods (not just via `Output.map(output, fn)` / `.pipe(...)`).
const resourceOutput = () => {
const src = fakeResource<"Test.Bucket", { name: string }>(
"Test.Bucket",
"M",
);
return (Output.of(src) as any).name as Output.Output<string>;
};
it.effect(".map(fn) builds an ApplyExpr", () =>
provideState(
Effect.gen(function* () {
const expr = (resourceOutput() as any).map((s: string) =>
s.toUpperCase(),
);
expect(Output.isApplyExpr(expr)).toBe(true);
expect(yield* Output.evaluate(expr, { M: { name: "abc" } })).toBe(
"ABC",
);
}),
),
);
it.effect(".apply(fn) builds an ApplyExpr", () =>
provideState(
Effect.gen(function* () {
const expr = (resourceOutput() as any).apply((s: string) =>
s.toUpperCase(),
);
expect(Output.isApplyExpr(expr)).toBe(true);
expect(yield* Output.evaluate(expr, { M: { name: "abc" } })).toBe(
"ABC",
);
}),
),
);
it.effect(".mapEffect(fn) builds an EffectExpr", () =>
provideState(
Effect.gen(function* () {
const expr = (resourceOutput() as any).mapEffect((s: string) =>
Effect.succeed(s + "!"),
);
expect(Output.isEffectExpr(expr)).toBe(true);
expect(yield* Output.evaluate(expr, { M: { name: "abc" } })).toBe(
"abc!",
);
}),
),
);
it.effect(".effect(fn) builds an EffectExpr", () =>
provideState(
Effect.gen(function* () {
const expr = (resourceOutput() as any).effect((s: string) =>
Effect.succeed(s + "!"),
);
expect(Output.isEffectExpr(expr)).toBe(true);
expect(yield* Output.evaluate(expr, { M: { name: "abc" } })).toBe(
"abc!",
);
}),
),
);
it.effect(".flatMap(fn) builds a FlatMapExpr", () =>
provideState(
Effect.gen(function* () {
const expr = (resourceOutput() as any).flatMap((s: string) =>
Output.literal(s + "?"),
);
expect(Output.isFlatMapExpr(expr)).toBe(true);
expect(yield* Output.evaluate(expr, { M: { name: "abc" } })).toBe(
"abc?",
);
}),
),
);
});
describe("AllExpr", () => {
it.effect("evaluates all wrapped outputs in parallel", () =>
provideState(
Effect.gen(function* () {
const expr = Output.all(
Output.literal(1),
Output.literal("two"),
Output.literal(true),
);
const result = yield* Output.evaluate(expr, {});
expect(result).toEqual([1, "two", true]);
}),
),
);
it.effect("evaluates all with resource expressions", () =>
provideState(
Effect.gen(function* () {
const a = fakeResource("Test.A", "A");
const b = fakeResource("Test.B", "B");
const expr = Output.all(Output.of(a), Output.of(b));
const result = yield* Output.evaluate(expr, {
A: { x: 1 },
B: { y: 2 },
});
expect(result).toEqual([{ x: 1 }, { y: 2 }]);
}),
),
);
});
describe("RefExpr", () => {
const provideStackStage = <A, E, R>(
effect: Effect.Effect<A, E, R>,
stack = "myStack",
stage = "myStage",
) =>
effect.pipe(
Effect.provide(
Layer.mergeAll(
Layer.succeed(Stack, { name: stack } as any),
Layer.succeed(Stage, stage),
),
),
);
it.effect("resolves a Ref against in-memory state", () =>
Effect.gen(function* () {
const initial = {
myStack: {
myStage: {
myResource: {
fqn: "myResource",
attr: { hello: "world" },
} as unknown as ResourceState,
},
},
};
const r = makeRef<ResourceLike>("myResource");
const expr = Output.of(r);
const result = yield* provideStackStage(
Output.evaluate(expr, {}).pipe(
Effect.provide(inMemoryState(initial)),
),
);
expect(result).toEqual({ hello: "world" });
}),
);
it.effect("uses explicit stack/stage when provided on the ref", () =>
Effect.gen(function* () {
const initial = {
otherStack: {
otherStage: {
someResource: {
fqn: "someResource",
attr: { v: 1 },
} as unknown as ResourceState,
},
},
};
const r = makeRef<ResourceLike>("someResource", {
stack: "otherStack",
stage: "otherStage",
});
const expr = Output.of(r);
// No Stack/Stage layers needed since the ref carries them.
const result = yield* Output.evaluate(expr, {}).pipe(
Effect.provide(inMemoryState(initial)),
);
expect(result).toEqual({ v: 1 });
}),
);
it.effect(
"fails with InvalidReferenceError when ref target is missing",
() =>
Effect.gen(function* () {
const r = makeRef<ResourceLike>("ghost", {
stack: "s",
stage: "t",
});
const expr = Output.of(r);
const exit = yield* Effect.exit(
Output.evaluate(expr, {}).pipe(Effect.provide(inMemoryState())),
);
expect(Exit.isFailure(exit)).toBe(true);
if (Exit.isFailure(exit)) {
expect(JSON.stringify(exit.cause.toJSON())).toContain(
"InvalidReferenceError",
);
}
}),
);
it.effect(
"PropExpr on a Ref reads the attribute from persisted state",
() =>
Effect.gen(function* () {
const initial = {
myStack: {
myStage: {
shared: {
fqn: "shared",
attr: { url: "https://example.com", name: "shared" },
} as unknown as ResourceState,
},
},
};
const r = makeRef<ResourceLike>("shared");
const expr = (Output.of(r) as any).url as Output.Output<string>;
const result = yield* provideStackStage(
Output.evaluate(expr, {}).pipe(
Effect.provide(inMemoryState(initial)),
),
);
expect(result).toBe("https://example.com");
}),
);
});
describe("StackRefExpr", () => {
const provideStage = <A, E, R>(
effect: Effect.Effect<A, E, R>,
stage = "myStage",
) => effect.pipe(Effect.provide(Layer.succeed(Stage, stage)));
it.effect("Output.stackRef resolves to the persisted stack output", () =>
Effect.gen(function* () {
const expr = yield* Output.stackRef<{ url: string }>("Backend");
const result = yield* provideStage(
Output.evaluate(expr, {}).pipe(
Effect.provide(
inMemoryState(
{},
{ Backend: { myStage: { url: "https://api.example.com" } } },
),
),
),
);
expect(result).toEqual({ url: "https://api.example.com" });
}),
);
it.effect("explicit stage overrides the ambient Stage", () =>
Effect.gen(function* () {
const expr = yield* Output.stackRef<{ url: string }>("Backend", {
stage: "prod",
});
// No Stage layer is provided — the ref carries it explicitly.
const result = yield* Output.evaluate(expr, {}).pipe(
Effect.provide(
inMemoryState(
{},
{ Backend: { prod: { url: "https://prod.example.com" } } },
),
),
);
expect(result).toEqual({ url: "https://prod.example.com" });
}),
);
it.effect("PropExpr off a stackRef reads a single attribute", () =>
Effect.gen(function* () {
const backend = yield* Output.stackRef<{ url: string }>("Backend");
const expr = (backend as any).url as Output.Output<string>;
const result = yield* provideStage(
Output.evaluate(expr, {}).pipe(
Effect.provide(
inMemoryState(
{},
{ Backend: { myStage: { url: "https://api.example.com" } } },
),
),
),
);
expect(result).toBe("https://api.example.com");
}),
);
it.effect(
"fails with InvalidReferenceError when the target stack/stage has no persisted output",
() =>
Effect.gen(function* () {
const expr = yield* Output.stackRef<{ url: string }>("Backend", {
stage: "ghost",
});
const exit = yield* Effect.exit(
Output.evaluate(expr, {}).pipe(Effect.provide(inMemoryState())),
);
expect(Exit.isFailure(exit)).toBe(true);
if (Exit.isFailure(exit)) {
const err = Cause.squash(
exit.cause,
) as Output.InvalidReferenceError;
expect(err._tag).toBe("InvalidReferenceError");
expect(err.stack).toBe("Backend");
expect(err.stage).toBe("ghost");
}
}),
);
});
describe("composition", () => {
it.effect("evaluates outputs nested inside arrays and objects", () =>
provideState(
Effect.gen(function* () {
const a = fakeResource("Test.A", "RA");
const b = fakeResource("Test.B", "RB");
const value = {
list: [Output.of(a), Output.literal("lit")],
nested: {
prop: (Output.of(b) as any).name.pipe(
Output.map((s: string) => `name=${s}`),
),
},
scalar: 42,
};
const result = yield* Output.evaluate(value, {
RA: { foo: "f" },
RB: { name: "bee" },
});
expect(result).toEqual({
list: [{ foo: "f" }, "lit"],
nested: { prop: "name=bee" },
scalar: 42,
});
}),
),
);
});
});
describe("Output.interpolate", { tags: ["unit", "local"] }, () => {
it.effect("interpolates literal values into a template", () =>
provideState(
Effect.gen(function* () {
const expr = Output.interpolate`hello ${Output.literal("world")}!`;
expect(yield* Output.evaluate(expr, {})).toBe("hello world!");
}),
),
);
it.effect("interpolates resource attributes", () =>
provideState(
Effect.gen(function* () {
const src = fakeResource("Test.Bucket", "Buck");
// @ts-expect-error
const name = Output.of(src).name;
const expr = Output.interpolate`s3://${name}/key`;
const result = yield* Output.evaluate(expr, {
Buck: { name: "my-bucket" },
});
expect(result).toBe("s3://my-bucket/key");
}),
),
);
it.effect("renders nullish args as empty strings", () =>
provideState(
Effect.gen(function* () {
const expr = Output.interpolate`a${Output.literal(null)}b${Output.literal(
undefined,
)}c`;
expect(yield* Output.evaluate(expr, {})).toBe("abc");
}),
),
);
it("derives its binding id from the template and arguments only", () => {
// The runtime bundler reprints function source, so a binding id that
// embeds the mapper's source differs between deploy and runtime.
const src = fakeResource("Test.Service", "Web");
// @ts-expect-error
const service = Output.of(src).serviceName;
const expr = Output.interpolate`http://${service}:${Output.literal(80)}`;
expect(String(expr[Symbol.for("nodejs.util.inspect.custom")]())).toBe(
"interpolate(http://${Web.serviceName}:${80})",
);
});
});
describe("Output coercion guard", { tags: ["unit", "local"] }, () => {
// These guard against the long-standing footgun where coercing an
// unresolved Output silently produced a placeholder — the inspect
// string for string-hints, or NaN for number-hints. Both let bogus
// values flow into resource props and ultimately into the cloud.
it("throws on template-literal interpolation of a raw Output", () => {
const src = fakeResource("Test.Bucket", "Buck");
// @ts-expect-error — synthetic prop access
const name = Output.of(src).name;
expect(() => `${name}`).toThrow(/Output\.interpolate/);
});
it("throws on string concatenation with a raw Output", () => {
const src = fakeResource("Test.Bucket", "Buck");
// @ts-expect-error — synthetic prop access
const name = Output.of(src).name;
expect(() => "s3://" + name).toThrow(/Output\.interpolate/);
});
it("throws on number coercion of a raw Output", () => {
const src = fakeResource("Test.Bucket", "Buck");
// @ts-expect-error — synthetic prop access
const name = Output.of(src).name;
expect(() => +name).toThrow(/Output\.(interpolate|map)/);
});
it("throws on arithmetic with a raw Output", () => {
const src = fakeResource("Test.Bucket", "Buck");
// @ts-expect-error — synthetic prop access
const name = Output.of(src).name;
expect(() => (name as unknown as number) * 2).toThrow(
/Output\.(interpolate|map)/,
);
});
});
describe(
"Redacted stack-output serialization (regression #598)",
{ tags: ["unit", "local"] },
() => {
// A resource whose attribute is a `Redacted<string>` — e.g. `Random.text`.
// When such an attribute flows
// into a Stack output it is JSON-serialized for persistence to state /
// Doppler / GH secrets. `JSON.stringify(Redacted)` returns the literal
// string "<redacted>", so a publisher reading the output would send
// `Bearer <redacted>` instead of the real token. The fix is to unwrap with
// `Output.map(Redacted.value)` before returning it from the stack.
const SECRET =
"1486c434bd35732a185d1712c587ddfafd9e1c8d7a94fb15cf6ece51128";
const redactedResource = () => {
const src = fakeResource<
"Alchemy.Random",
{ text: Redacted.Redacted<string> }
>("Alchemy.Random", "AuthTokenValue");
return (Output.of(src) as any).text as Output.Output<
Redacted.Redacted<string>
>;
};
const env = { AuthTokenValue: { text: Redacted.make(SECRET) } };
it.effect(
'a raw Redacted output serializes to the literal "<redacted>" (the bug)',
() =>
provideState(
Effect.gen(function* () {
const result = yield* Output.evaluate(redactedResource(), env);
// The evaluated value is still a Redacted, and persisting it as a
// stack output (JSON) loses the real token.
expect(Redacted.isRedacted(result)).toBe(true);
expect(JSON.stringify({ authToken: result })).toBe(
'{"authToken":"<redacted>"}',
);
}),
),
);
it.effect("Output.map(Redacted.value) emits the real token (the fix)", () =>
provideState(
Effect.gen(function* () {
const expr = redactedResource().pipe(Output.map(Redacted.value));
const result = yield* Output.evaluate(expr, env);
expect(result).toBe(SECRET);
expect(JSON.stringify({ authToken: result })).toBe(
`{"authToken":"${SECRET}"}`,
);
}),
),
);
},
);
describe("Output.isOutput / isExpr", { tags: ["unit", "local"] }, () => {
it("identifies Output expressions", () => {
expect(Output.isOutput(Output.literal(1))).toBe(true);
expect(Output.isOutput(Output.all(Output.literal(1)))).toBe(true);
expect(Output.isExpr(Output.literal(1))).toBe(true);
});
it("rejects non-Output values", () => {
expect(Output.isOutput(1)).toBeFalsy();
expect(Output.isOutput("x")).toBeFalsy();
expect(Output.isOutput(null)).toBeFalsy();
expect(Output.isOutput(undefined)).toBeFalsy();
expect(Output.isOutput({})).toBeFalsy();
expect(Output.isOutput([])).toBeFalsy();
});
});
describe("Output.asOutput", { tags: ["unit", "local"] }, () => {
it.effect("wraps a plain value as a literal Output", () =>
provideState(
Effect.gen(function* () {
const o = Output.asOutput("foo");
expect(Output.isOutput(o)).toBe(true);
expect(yield* Output.evaluate(o, {})).toBe("foo");
}),
),
);
it.effect("wraps an Effect as an EffectExpr", () =>
provideState(
Effect.gen(function* () {
const o = Output.asOutput(Effect.succeed(123));
expect(Output.isOutput(o)).toBe(true);
expect(yield* Output.evaluate(o, {})).toBe(123);
}),
),
);
it("returns the same Output if already an Output", () => {
const o = Output.literal("x");
expect(Output.asOutput(o)).toBe(o);
});
});
describe(
"Output.upstream / hasOutputs / resolveUpstream",
{ tags: ["unit", "local"] },
() => {
it("returns upstream resources from a ResourceExpr", () => {
const src = fakeResource("Test.A", "FQN-A");
const expr = Output.of(src);
const up = Output.upstream(expr);
expect(Object.keys(up)).toEqual(["FQN-A"]);
});
it("returns upstream resources from a PropExpr", () => {
const src = fakeResource("Test.A", "FQN-A");
const expr = (Output.of(src) as any).foo;
expect(Object.keys(Output.upstream(expr))).toEqual(["FQN-A"]);
});
it("merges upstream resources from AllExpr", () => {
const a = fakeResource("Test.A", "A");
const b = fakeResource("Test.B", "B");
const expr = Output.all(Output.of(a), Output.of(b));
expect(Object.keys(Output.upstream(expr)).sort()).toEqual(["A", "B"]);
});
it("returns empty upstream for literals", () => {
expect(Output.upstream(Output.literal(1))).toEqual({});
});
it("treats a raw Resource passed directly as an upstream dependency", () => {
const src = fakeResource("Test.A", "RawA");
expect(Object.keys(Output.upstream(src as any))).toEqual(["RawA"]);
});
it("upstreamAny detects a raw Resource passed directly as a prop value", () => {
const a = fakeResource("Test.A", "FQN-A");
const b = fakeResource("Test.B", "FQN-B");
const props = { image: a, network: b };
expect(Object.keys(Output.upstreamAny(props)).sort()).toEqual([
"FQN-A",
"FQN-B",
]);
});
it("upstreamAny detects raw Resources nested in arrays/objects", () => {
const a = fakeResource("Test.A", "FQN-A");
const b = fakeResource("Test.B", "FQN-B");
const props = {
volumes: [{ source: a }],
env: { ref: b },
};
expect(Object.keys(Output.upstreamAny(props)).sort()).toEqual([
"FQN-A",
"FQN-B",
]);
});
it("upstreamAny detects raw Resource at the top level", () => {
const a = fakeResource("Test.A", "Top");
expect(Object.keys(Output.upstreamAny(a))).toEqual(["Top"]);
});
it("resolveUpstream picks up raw Resources alongside Output expressions", () => {
const a = fakeResource("Test.A", "A1");
const b = fakeResource("Test.B", "B1");
const result = Output.resolveUpstream({
raw: a,
via: Output.of(b),
});
expect(Object.keys(result).sort()).toEqual(["A1", "B1"]);
});
it("hasOutputs is true when an object contains an Output referencing a resource", () => {
const src = fakeResource("Test.A", "X");
expect(Output.hasOutputs({ k: Output.of(src) })).toBe(true);
});
it("hasOutputs is false for plain values", () => {
expect(Output.hasOutputs({ k: 1, b: "x" })).toBe(false);
expect(Output.hasOutputs([1, 2, 3])).toBe(false);
});
it("resolveUpstream walks arrays and objects to gather resources", () => {
const a = fakeResource("Test.A", "RA");
const b = fakeResource("Test.B", "RB");
const result = Output.resolveUpstream({
arr: [Output.of(a)],
nested: { prop: Output.of(b) },
scalar: 1,
});
expect(Object.keys(result).sort()).toEqual(["RA", "RB"]);
});
},
);
// effect ≥4.0.0-beta.103's Context is self-referential (cacheRoot points back
// at itself), which sent the prop walkers into unbounded recursion during
// `Plan.make` of the Cloudflare state-store bootstrap stack (#1082). The rule
// pinned here: Resources/Outputs are dependencies, plain data (arrays, plain
// objects) is traversed to find them, and every other value — ANY class
// instance, effect's or otherwise — is a leaf. Plus WeakSet cycle guards so
// cyclic plain data terminates.
describe(
"upstream walkers: cycles and non-plain leaves (#1082)",
{ tags: ["unit", "local"] },
() => {
it("upstreamAny terminates on a cyclic plain object and still finds resources", () => {
const a = fakeResource("Test.A", "CY-A");
const cyclic: any = { name: "cycle" };
cyclic.self = cyclic;
const props = { config: cyclic, dep: Output.of(a) };
expect(Object.keys(Output.upstreamAny(props))).toEqual(["CY-A"]);
});
it("resolveUpstream terminates on mutually-cyclic objects and arrays", () => {
const x: any = { tag: "x" };
const y: any = { tag: "y", x };
x.y = y;
const arr: any[] = [x];
x.arr = arr;
expect(Output.resolveUpstream({ x, y, arr })).toEqual({});
});
it("a shared (diamond) sub-object still contributes its resources", () => {
const a = fakeResource("Test.A", "DI-A");
const shared = { dep: Output.of(a) };
const result = Output.upstreamAny({ left: shared, right: shared });
expect(Object.keys(result)).toEqual(["DI-A"]);
});
it("treats Effect, Layer, and Context values as leaves", () => {
const effect = Effect.succeed(1);
const layer = Layer.succeed(Stage, "test");
const context = Context.empty();
expect(Output.upstreamAny({ effect, layer, context })).toEqual({});
expect(Output.resolveUpstream({ effect, layer, context })).toEqual({});
expect(Output.hasOutputs({ effect, layer, context })).toBe(false);
});
it("traverses null-prototype objects and treats functions as leaves", () => {
const a = fakeResource("Test.A", "NP-A");
const nullProto = Object.assign(Object.create(null), {
dep: Output.of(a),
});
expect(Object.keys(Output.upstreamAny({ nullProto }))).toEqual(["NP-A"]);
// A function in props is a leaf — a closure capturing a resource is not
// a declared dependency.
expect(Output.upstreamAny({ fn: () => a })).toEqual({});
});
it("treats any class instance as a leaf (never walks its internals)", () => {
const a = fakeResource("Test.A", "CL-A");
class SdkConfig {
// A dependency smuggled inside a foreign class instance is invisible
// by design — the engine can't evaluate or persist through it.
dep = Output.of(a);
date = new Date(0);
}
expect(Output.upstreamAny({ config: new SdkConfig() })).toEqual({});
// ...but the same dependency in plain data right next to it is found.
expect(
Object.keys(
Output.upstreamAny({ config: new SdkConfig(), dep: Output.of(a) }),
),
).toEqual(["CL-A"]);
});
it("mimics a Worker `exports` entry (constructor Effect + services Context)", () => {
const a = fakeResource("Test.A", "EX-A");
const props = {
name: "worker",
exports: {
Store: {
kind: "durableObject",
constructor: Effect.void,
services: Context.empty(),
},
},
dep: Output.of(a),
};
expect(Object.keys(Output.upstreamAny(props))).toEqual(["EX-A"]);
});
it.effect("evaluate passes Effect/Layer/Context through by identity", () =>
provideState(
Effect.gen(function* () {
const effect = Effect.succeed(1);
const layer = Layer.succeed(Stage, "test");
const context = Context.empty();
const result = yield* Output.evaluate({ effect, layer, context }, {});
expect(result.effect).toBe(effect);
expect(result.layer).toBe(layer);
expect(result.context).toBe(context);
}),
),
);
it.effect(
"evaluate passes Date/Redacted through by identity (prototype intact)",
() =>
provideState(
Effect.gen(function* () {
const date = new Date(0);
const secret = Redacted.make("hunter2");
const result = yield* Output.evaluate({ date, secret }, {});
expect(result.date).toBe(date);
expect(result.secret).toBe(secret);
}),
),
);
it.effect(
"evaluate still resolves Config values (Configs are Effects)",
() =>
provideState(
Effect.gen(function* () {
const result = yield* Output.evaluate(
{ value: Config.succeed("resolved") },
{},
);
expect(result.value).toBe("resolved");
}),
),
);
it.effect(
"evaluate resolves outputs at every nesting depth of plain data",
() =>
provideState(
Effect.gen(function* () {
const src = fakeResource("Test.A", "EV-A");
const out = (Output.of(src) as any).name;
const result = yield* Output.evaluate(
{
layers: [
{ config: { hosts: [{ url: out }, { url: "static" }] } },
],
matrix: [[out]],
mixed: [1, "x", { deep: [out] }, null],
nullProto: Object.assign(Object.create(null), { ref: out }),
},
{ "EV-A": { name: "resolved-name" } },
);
expect(result.layers[0].config.hosts[0].url).toBe("resolved-name");
expect(result.layers[0].config.hosts[1].url).toBe("static");
expect(result.matrix[0][0]).toBe("resolved-name");
expect(result.mixed).toEqual([
1,
"x",
{ deep: ["resolved-name"] },
null,
]);
expect(result.nullProto.ref).toBe("resolved-name");
}),
),
);
it.effect("evaluate cuts cyclic plain data but resolves siblings", () =>
provideState(
Effect.gen(function* () {
const src = fakeResource("Test.A", "CY-EV");
const out = (Output.of(src) as any).name;
const cyclic: any = { tag: "c" };
cyclic.self = cyclic;
const arr: any[] = [cyclic];
cyclic.arr = arr;
const result = yield* Output.evaluate(
{ cyc: cyclic, url: out },
{ "CY-EV": { name: "sibling" } },
);
expect(result.url).toBe("sibling");
expect(result.cyc.tag).toBe("c");
expect(result.cyc.self).toBeUndefined();
expect(result.cyc.arr[0]).toBeUndefined();
}),
),
);
it.effect(
"evaluate preserves diamonds — shared objects evaluate in both positions",
() =>
provideState(
Effect.gen(function* () {
const src = fakeResource("Test.A", "DI-EV");
const shared = { url: (Output.of(src) as any).name };
const result = yield* Output.evaluate(
{ left: shared, right: shared, list: [shared] },
{ "DI-EV": { name: "diamond" } },
);
expect(result.left).toEqual({ url: "diamond" });
expect(result.right).toEqual({ url: "diamond" });
expect(result.list[0]).toEqual({ url: "diamond" });
}),
),
);
},
);
describe("Output.toEnvKey / toUpper", { tags: ["unit", "local"] }, () => {
it("uppercases strings", () => {
expect(Output.toUpper("hello")).toBe("HELLO");
});
it("joins id + suffix and replaces dashes with underscores", () => {
expect(Output.toEnvKey("my-bucket", "name")).toBe("MY_BUCKET_NAME");
expect(Output.toEnvKey("svc", "api-key")).toBe("SVC_API_KEY");
});
});