Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
38 changes: 33 additions & 5 deletions bun.lock

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

2 changes: 1 addition & 1 deletion package.json
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
{
"name": "effect-bun-tooling",
"version": "0.4.1",
"version": "0.5.0",
"private": true,
"description": "General-purpose Effect + Bun test tooling. Public, standalone, source-TypeScript packages consumed with no build step.",
"license": "MIT",
Expand Down
2 changes: 1 addition & 1 deletion packages/bun-svelte-test/package.json
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
{
"name": "@packages/bun-svelte-test",
"version": "0.4.1",
"version": "0.5.0",
"description": "Bun.plugin Svelte 5 compiler loader plus a happy-dom preload for bun test. Lets `bun test` compile and mount .svelte components and .svelte.ts runes modules, retiring the vitest carve-out that existed solely because bun has no native .svelte loader.",
"license": "MIT",
"repository": {
Expand Down
2 changes: 1 addition & 1 deletion packages/ddd-meta/__tests__/host-validity.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -109,7 +109,7 @@ const announceAbsentLane = (): void => {
'',
' ############################################################################',
' # ddd-meta host-validity: the rustc ARM DID NOT RUN. rustc is not on PATH. #',
` # waived in host-lanes.toml: ${String(HOST_LANES.rustc.waived)}`.padEnd(77) + '#',
`${` # waived in host-lanes.toml: ${String(HOST_LANES.rustc.waived)}`.padEnd(77)}#`,
' # The nesting-host property was NOT asserted on this machine. #',
' ############################################################################',
'',
Expand Down
8 changes: 4 additions & 4 deletions packages/ddd-meta/__tests__/write-totality.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -27,7 +27,7 @@ const DECODE_ARM_CASES: readonly (readonly [string, string, FrontMatterFieldValu
['a link key that is not an ident', 'links', { ADR: 'https://x.test/a' }],
];

const valueOf = (key: string, field: FrontMatterFieldValue): FrontMatter =>
const frontMatterOf = (key: string, field: FrontMatterFieldValue): FrontMatter =>
({ [key]: field }) as unknown as FrontMatter;

const rawPayloadOf = (key: string, field: FrontMatterFieldValue): string =>
Expand Down Expand Up @@ -69,7 +69,7 @@ describe('the write path refuses a value it cannot render, rather than emitting
describe('a value that renders as valid TOML but fails field validation is refused by the decode arm', () => {
for (const [label, key, field] of DECODE_ARM_CASES) {
it(`refuses ${label}`, () => {
expect(`${label}: ${refusalTagOf(valueOf(key, field))}`).toBe(`${label}: PayloadNotRenderable`);
expect(`${label}: ${refusalTagOf(frontMatterOf(key, field))}`).toBe(`${label}: PayloadNotRenderable`);
});
}

Expand All @@ -83,8 +83,8 @@ describe('a value that renders as valid TOML but fails field validation is refus
it('separates the two arms: a hostile KEY fails the parse arm, a bad VALUE fails the decode arm', () => {
const hostileKey = parseToml(rawPayloadOf('links', { 'a*/b': 'https://x.test/ok' }));
const badValue = parseToml(rawPayloadOf('l', 'Domain'));
expect(Result.isFailure(hostileKey)).toBe(true);
expect(Result.isSuccess(badValue)).toBe(true);
expect(hostileKey.pipe(Result.isFailure)).toBe(true);
expect(badValue.pipe(Result.isSuccess)).toBe(true);
});
});

Expand Down
2 changes: 1 addition & 1 deletion packages/ddd-meta/package.json
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
{
"name": "@packages/ddd-meta",
"version": "0.4.1",
"version": "0.5.0",
"description": "The front-matter FORMAT: a schema-validated TOML metadata block carried in a comment at the top of an owned, comment-capable file, plus the four per-language comment carriers (block, hash, apostrophe, xml) that read and write one identical payload. Closed 11-field registry whose governing rule is that a field is legitimate only when no AST can PRODUCE its value; refutable-but-not-derivable is the point. Ships the format and ZERO vocabulary: layer/pattern/subdomain/tier members arrive as injected data, never as a second copy of someone else's truth.",
"license": "MIT",
"repository": {
Expand Down
2 changes: 1 addition & 1 deletion packages/ddd-path/package.json
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
{
"name": "@packages/ddd-path",
"version": "0.4.1",
"version": "0.5.0",
"description": "The DDD path grammar: bounded context, language root and test trees derived from file paths alone, across six languages, with partial adoption and no flag day",
"license": "MIT",
"repository": {
Expand Down
30 changes: 29 additions & 1 deletion packages/effect-bun-test/README.md
Original file line number Diff line number Diff line change
Expand Up @@ -96,11 +96,39 @@ nothing pulls in a subsystem you are not using.
| `describeWrapped(name, f)` | `describe` with harness methods bound. |
| `makeMethods(it)` | Bind the harness onto a custom registrar. |
| `addEqualityTesters()` | **No-op.** Kept for `@effect/vitest` API parity — see caveats. |
| `describe` `test` `expect` `beforeAll` `beforeEach` `afterAll` `afterEach` `mock` `spyOn` `jest` `setSystemTime` | Re-exported `bun:test` primitives, so one import serves the whole file. |
| `it.each(cases)(name, fn, opts?)` | bun's own table-driven registrar, for a NON-Effect body. Delegated to `bun:test` outright, so `%s`/`%p` name formatting, array-case spreading and the `.skip` / `.only` / `.todo` / `.failing` chain behave exactly as bun's do. |
| `it.for(cases)(name, fn, opts?)` | Like `it.each` but hands the case as ONE value plus a `TestContext`, never spread. |
| `it.skip` `it.only` `it.skipIf(c)` `it.runIf(c)` `it.fails` | bun's registrar modifiers, carried on the harness `it`. |
| `describe` `test` `expect` `beforeAll` `beforeEach` `afterAll` `afterEach` `mock` `spyOn` `jest` `setSystemTime` `type Mock` | Re-exported `bun:test` primitives, so one import serves the whole file. |

`TestContext` (the object passed to your test fn) carries `signal`, `onTestFinished`,
`onTestFailed`. **`ctx.signal` is inert** — see [Known limitations](#known-limitations).

### The bare `it` refuses an Effect, and that is load-bearing

`it(name, fn)` types `fn`'s return as bun's own `void | Promise<unknown>`. An `Effect` is not
thenable, so bun never runs one handed to the bare registrar: the case reports **PASS with every
assertion skipped**, and the test count does not move. Typing it this way makes that a `tsc` error
instead:

```ts
it('x', () => Effect.gen(function* () { ... })); // tsc: Effect<...> is not assignable to
// void | Promise<unknown>
it.effect('x', () => Effect.gen(function* () { ... })); // correct
```

Two implementation facts, both measured, both easy to undo by accident:

- **The union is the guard.** Splitting `void | Promise<unknown>` into separate `void` and
`Promise<unknown>` overloads RE-OPENS the hole: a bare `void` return type triggers TypeScript's
void-assignability rule, which accepts a callback returning anything at all. Only inside a union
does that rule not apply.
- Biome's `noConfusingVoidType` fires on exactly this union, so it carries a suppression. That
suppression is the reason the guard exists — it is not tidy-up-able.

`__tests__/bare-it-rejects-effect.types.test.ts` pins this with a `@ts-expect-error` fixture, so a
regression fails `bun run tsc` rather than going quiet.

### Assertions

For tagged errors, **`@packages/effect-test-kit/tagged` is canonical** — import it directly:
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,22 @@
import { Effect } from 'effect';
import { describe, expect, it } from '../src/index.ts';

describe('the bare it callable rejects an Effect at compile time', () => {
it('accepts a synchronous body', () => {
expect(1).toBe(1);
});

it('accepts a promise-returning body', () =>
Promise.resolve().then(() => {
expect(1).toBe(1);
}));

// An Effect returned from the bare `it` is never run by bun, so the case would pass with every
// assertion skipped. If this stops erroring, the guard has regressed.
it('refuses an Effect body', () =>
// @ts-expect-error the bare callable accepts only void | Promise<unknown>
Effect.gen(function* () {
yield* Effect.void;
expect(1).toBe(2);
}));
});
43 changes: 43 additions & 0 deletions packages/effect-bun-test/__tests__/bun-registrars.test.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,43 @@
import { describe, expect, it } from '../src/index.ts';

const SPREAD_CASES = [
['a', 1],
['b', 2],
] as const;

const seenSpread: Array<string> = [];
const seenScalar: Array<number> = [];

describe('it.each — bun parity on the harness registrar', () => {
it.each([...SPREAD_CASES])('spreads an array case into positional args: %s', (label, value) => {
seenSpread.push(`${label}${value}`);
expect(typeof label).toBe('string');
expect(typeof value).toBe('number');
});

it.each([10, 20, 30])('passes a scalar case as one arg: %p', (value) => {
seenScalar.push(value);
expect(typeof value).toBe('number');
});

it('registered one case per entry, and the bodies actually ran', () => {
expect(seenSpread).toEqual(['a1', 'b2']);
expect(seenScalar).toEqual([10, 20, 30]);
});

it.each([1])(
'honours a trailing TestOptions argument',
(value) => {
expect(value).toBe(1);
},
{ timeout: 2_000 },
);
});

describe('the harness it carries bun own registrar surface', () => {
it('exposes each, for, skip, only, skipIf, runIf and fails', () => {
for (const member of ['each', 'for', 'skip', 'only', 'skipIf', 'runIf', 'fails'] as const) {
expect(typeof (it as unknown as Record<string, unknown>)[member]).not.toBe('undefined');
}
});
});
2 changes: 1 addition & 1 deletion packages/effect-bun-test/package.json
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
{
"name": "@packages/effect-bun-test",
"version": "0.4.1",
"version": "0.5.0",
"description": "Effect-native test harness on top of bun:test. it.effect / it.scoped / it.live / layer() replace inline Effect.runPromise, and it.effect runs on Effect's TestContext so virtual time is free. Vendored from Effect-TS/effect PR #6236 (@effect/bun-test, head 3f8d6e8) and extended with paved paths for env/config, scripted subprocesses, in-repo fixture roots, and tagged-error assertions.",
"license": "MIT",
"repository": {
Expand Down
1 change: 1 addition & 0 deletions packages/effect-bun-test/src/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -6,6 +6,7 @@ import type * as Scope from 'effect/Scope';
import * as internal from './internal/internal.ts';
import type { API, Methods, MethodsNonLive, Tester, TestServices } from './types.ts';

export type { Mock } from 'bun:test';
export type {
API,
Arbitraries,
Expand Down
48 changes: 18 additions & 30 deletions packages/effect-bun-test/src/internal/internal.ts
Original file line number Diff line number Diff line change
Expand Up @@ -25,14 +25,6 @@

type BunRegistrar = (name: string, fn: BunTestFn, options?: number | BunOptions) => void;

type BunEachRegistrar<T> = {
(name: string, fn: (value: T) => unknown, options?: number | BunOptions): void;
skip: BunEachRegistrar<T>;
only: BunEachRegistrar<T>;
todo: BunEachRegistrar<T>;
failing: BunEachRegistrar<T>;
};

interface BunTestApi extends BunRegistrar {
skip: BunRegistrar;
only: BunRegistrar;
Expand All @@ -41,9 +33,19 @@
if: (condition: unknown) => BunRegistrar;
skipIf: (condition: unknown) => BunRegistrar;
todoIf: (condition: unknown) => BunRegistrar;
each: <T>(cases: ReadonlyArray<T>) => BunEachRegistrar<T>;
each: typeof bunEach;
}

type BunRegistrarFamily = ((name: string, fn: unknown, options?: unknown) => void) & {
skip: BunRegistrarFamily;
only: BunRegistrarFamily;
todo: BunRegistrarFamily;
failing: BunRegistrarFamily;
};

const bunEach: typeof test.each = (...args: Parameters<typeof test.each>) =>
(test.each as typeof test.each).apply(test, args);

const bunTest = test as unknown as BunTestApi;

type TestCallback = () => void | Promise<void>;
Expand Down Expand Up @@ -152,17 +154,15 @@
resolve()(name, fn, toBunOptions(opts));
}) as unknown as BunTest.API;

type ForFn<T> = (arg: T, ctx: BunTest.TestContext) => unknown | Promise<unknown>;

const makeForRegistrar =
<T>(cases: ReadonlyArray<T>) =>
(
name: string,
optsOrFn: number | BunTest.TestOptions | ForFn<T>,
maybeFnOrOpts?: ForFn<T> | number | BunTest.TestOptions,
optsOrFn: number | BunTest.TestOptions | BunTest.ForFn<T>,
maybeFnOrOpts?: BunTest.ForFn<T> | number | BunTest.TestOptions,
): void => {
const fnFirst = typeof optsOrFn === 'function';
const fn = fnFirst ? optsOrFn : (maybeFnOrOpts as ForFn<T> | undefined);
const fn = fnFirst ? optsOrFn : (maybeFnOrOpts as BunTest.ForFn<T> | undefined);
const opts = fnFirst
? (maybeFnOrOpts as number | BunTest.TestOptions | undefined)
: (optsOrFn as number | BunTest.TestOptions);
Expand All @@ -171,7 +171,7 @@
}

const o = isObject(opts) ? (opts as BunTest.TestOptions) : undefined;
const cased = bunTest.each(cases);
const cased = (bunEach as unknown as <U>(c: ReadonlyArray<U>) => BunRegistrarFamily)(cases);
const register =
o?.todo === true
? cased.todo
Expand All @@ -183,23 +183,10 @@
? cased.skip
: cased;

register(name, (value) => fn(value, makeContext()), toBunOptions(opts));
register(name, (value: T) => fn(value, makeContext()), toBunOptions(opts));
};

export type DefaultApi = BunTest.API & {
skip: BunTest.API;
only: BunTest.API;
skipIf: (condition: unknown) => BunTest.API;
runIf: (condition: unknown) => BunTest.API;
fails: BunTest.API;
for: <T>(
cases: ReadonlyArray<T>,
) => (
name: string,
optsOrFn: number | BunTest.TestOptions | ForFn<T>,
maybeFnOrOpts?: ForFn<T> | number | BunTest.TestOptions,
) => void;
};
export type DefaultApi = BunTest.API & BunTest.BunRegistrars;

const makeDefaultApi = (): DefaultApi =>
Object.assign(
Expand All @@ -214,6 +201,7 @@
skipIf: (condition: unknown) => makeRegistrar(bunTest.skipIf(condition)),
runIf: (condition: unknown) => makeRegistrar(bunTest.if(condition)),
fails: makeRegistrar(bunTest.failing),
each: bunEach,
for: makeForRegistrar,
},
);
Expand Down Expand Up @@ -305,7 +293,7 @@
};

const fastCheckParams = <Ts>(timeout: unknown): fc.Parameters<Ts> =>
isObject(timeout) && 'fastCheck' in timeout ? (timeout['fastCheck'] as fc.Parameters<Ts>) : {};

Check notice on line 296 in packages/effect-bun-test/src/internal/internal.ts

View workflow job for this annotation

GitHub Actions / dod

lint/complexity/useLiteralKeys

The computed expression can be simplified without the use of a string literal.

const toTestOptions = (
timeout: number | (BunTest.TestOptions & { fastCheck?: unknown }) | undefined,
Expand Down
Loading
Loading