@questi0nm4rk/feats 1.0.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (66) hide show
  1. package/README.md +101 -0
  2. package/dist/feats.js +27862 -0
  3. package/dist/plugin/bun-plugin.js +12918 -0
  4. package/dist/src/assertions/config-assertions.d.ts +5 -0
  5. package/dist/src/assertions/output-assertions.d.ts +9 -0
  6. package/dist/src/cli/cli-result.d.ts +6 -0
  7. package/dist/src/cli/cli-runner.d.ts +8 -0
  8. package/dist/src/feats.d.ts +16 -0
  9. package/dist/src/fixtures/fixture-manager.d.ts +6 -0
  10. package/dist/src/fixtures/fixture-project.d.ts +12 -0
  11. package/dist/src/parser/adapter.d.ts +5 -0
  12. package/dist/src/parser/models.d.ts +35 -0
  13. package/dist/src/plugin/bun-plugin.d.ts +3 -0
  14. package/dist/src/random/seeded-rng.d.ts +8 -0
  15. package/dist/src/registry/expression-adapter.d.ts +6 -0
  16. package/dist/src/registry/parameter-types.d.ts +8 -0
  17. package/dist/src/registry/step-definition.d.ts +7 -0
  18. package/dist/src/registry/step-registry.d.ts +14 -0
  19. package/dist/src/reporting/error-formatter.d.ts +2 -0
  20. package/dist/src/reporting/pending-steps.d.ts +2 -0
  21. package/dist/src/runner/feature-runner.d.ts +9 -0
  22. package/dist/src/runner/hook-runner.d.ts +12 -0
  23. package/dist/src/runner/scenario-runner.d.ts +3 -0
  24. package/dist/src/runner/tag-filter.d.ts +2 -0
  25. package/dist/src/state/world.d.ts +4 -0
  26. package/dist/tests/assertions/config-assertions.test.d.ts +1 -0
  27. package/dist/tests/assertions/output-assertions.test.d.ts +1 -0
  28. package/dist/tests/cli/cli-runner.test.d.ts +1 -0
  29. package/dist/tests/features/self-test.steps.d.ts +1 -0
  30. package/dist/tests/features/self-test.test.d.ts +1 -0
  31. package/dist/tests/fixtures/sample-project/src/main.d.ts +1 -0
  32. package/dist/tests/fixtures-manager/fixture-manager.test.d.ts +1 -0
  33. package/dist/tests/parser/adapter.test.d.ts +1 -0
  34. package/dist/tests/parser/models.test.d.ts +1 -0
  35. package/dist/tests/random/seeded-rng.test.d.ts +1 -0
  36. package/dist/tests/registry/expression-adapter.test.d.ts +1 -0
  37. package/dist/tests/registry/parameter-types.test.d.ts +1 -0
  38. package/dist/tests/registry/step-registry.test.d.ts +1 -0
  39. package/dist/tests/reporting/error-formatter.test.d.ts +1 -0
  40. package/dist/tests/reporting/pending-steps.test.d.ts +1 -0
  41. package/dist/tests/runner/feature-runner.test.d.ts +1 -0
  42. package/dist/tests/runner/hook-runner.test.d.ts +1 -0
  43. package/dist/tests/runner/scenario-runner.test.d.ts +1 -0
  44. package/package.json +43 -0
  45. package/src/assertions/config-assertions.ts +123 -0
  46. package/src/assertions/output-assertions.ts +67 -0
  47. package/src/cli/cli-result.ts +6 -0
  48. package/src/cli/cli-runner.ts +66 -0
  49. package/src/feats.ts +30 -0
  50. package/src/fixtures/fixture-manager.ts +34 -0
  51. package/src/fixtures/fixture-project.ts +69 -0
  52. package/src/parser/adapter.ts +214 -0
  53. package/src/parser/models.ts +70 -0
  54. package/src/plugin/bun-plugin.ts +18 -0
  55. package/src/random/seeded-rng.ts +94 -0
  56. package/src/registry/expression-adapter.ts +34 -0
  57. package/src/registry/parameter-types.ts +24 -0
  58. package/src/registry/step-definition.ts +12 -0
  59. package/src/registry/step-registry.ts +63 -0
  60. package/src/reporting/error-formatter.ts +17 -0
  61. package/src/reporting/pending-steps.ts +16 -0
  62. package/src/runner/feature-runner.ts +86 -0
  63. package/src/runner/hook-runner.ts +54 -0
  64. package/src/runner/scenario-runner.ts +13 -0
  65. package/src/runner/tag-filter.ts +109 -0
  66. package/src/state/world.ts +5 -0
@@ -0,0 +1,5 @@
1
+ export interface AssertConfigOpts {
2
+ readonly format?: "json" | "toml" | "yaml";
3
+ readonly subset?: boolean;
4
+ }
5
+ export declare function assertConfig(filePath: string, expected: Record<string, unknown>, opts?: AssertConfigOpts): void;
@@ -0,0 +1,9 @@
1
+ import type { CLIResult } from "@/cli/cli-result";
2
+ export interface OutputExpectations {
3
+ readonly stdout?: string | RegExp;
4
+ readonly stderr?: string | RegExp;
5
+ readonly exitCode?: number;
6
+ readonly contains?: string;
7
+ readonly notContains?: string;
8
+ }
9
+ export declare function assertOutput(result: CLIResult, expectations: OutputExpectations): void;
@@ -0,0 +1,6 @@
1
+ export interface CLIResult {
2
+ readonly stdout: string;
3
+ readonly stderr: string;
4
+ readonly exitCode: number;
5
+ readonly timedOut: boolean;
6
+ }
@@ -0,0 +1,8 @@
1
+ import type { CLIResult } from "@/cli/cli-result";
2
+ export interface RunCliOpts {
3
+ readonly cwd?: string;
4
+ readonly timeout?: number;
5
+ readonly env?: Record<string, string>;
6
+ readonly stdin?: string;
7
+ }
8
+ export declare function runCli(command: string, args?: string[], opts?: RunCliOpts): Promise<CLIResult>;
@@ -0,0 +1,16 @@
1
+ export { assertConfig } from "./assertions/config-assertions";
2
+ export { assertOutput } from "./assertions/output-assertions";
3
+ export type { CLIResult } from "./cli/cli-result";
4
+ export { runCli } from "./cli/cli-runner";
5
+ export { composeFixtures, setupFixture } from "./fixtures/fixture-manager";
6
+ export type { FixtureProject } from "./fixtures/fixture-project";
7
+ export { loadFeatures, parseFeature } from "./parser/adapter";
8
+ export type { DataTable, Feature, ParsedStep, Scenario, Tag } from "./parser/models";
9
+ export type { SeededRng } from "./random/seeded-rng";
10
+ export { createRng } from "./random/seeded-rng";
11
+ export { defineParameterType } from "./registry/parameter-types";
12
+ export { Given, Step, Then, When } from "./registry/step-registry";
13
+ export type { RunOptions } from "./runner/feature-runner";
14
+ export { runFeatures } from "./runner/feature-runner";
15
+ export { After, Before } from "./runner/hook-runner";
16
+ export type { World, WorldFactory } from "./state/world";
@@ -0,0 +1,6 @@
1
+ import type { FixtureProject } from "@/fixtures/fixture-project";
2
+ export interface FixtureOpts {
3
+ readonly fixtureDir: string;
4
+ }
5
+ export declare function setupFixture(name: string, opts: FixtureOpts): Promise<FixtureProject>;
6
+ export declare function composeFixtures(names: string[], opts: FixtureOpts): Promise<FixtureProject>;
@@ -0,0 +1,12 @@
1
+ import type { CLIResult } from "@/cli/cli-result";
2
+ export interface FixtureProject {
3
+ readonly dir: string;
4
+ run(command: string, args?: string[]): Promise<CLIResult>;
5
+ hasFile(path: string): boolean;
6
+ readFile(path: string): Promise<string>;
7
+ readJson(path: string): Promise<unknown>;
8
+ readToml(path: string): Promise<unknown>;
9
+ listFiles(glob?: string): Promise<string[]>;
10
+ cleanup(): Promise<void>;
11
+ }
12
+ export declare function createFixtureProject(dir: string): FixtureProject;
@@ -0,0 +1,5 @@
1
+ import type { Feature } from "@/parser/models";
2
+ export declare function parseFeature(source: string, uri: string): Feature;
3
+ export declare function loadFeatures(globPattern: string, opts?: {
4
+ cwd?: string;
5
+ }): Promise<Feature[]>;
@@ -0,0 +1,35 @@
1
+ export interface Tag {
2
+ readonly name: string;
3
+ }
4
+ export interface StepLocation {
5
+ readonly uri: string;
6
+ readonly line: number;
7
+ }
8
+ export interface DataTable {
9
+ readonly rows: readonly (readonly string[])[];
10
+ asObjects(): Record<string, string>[];
11
+ asLists(): string[][];
12
+ }
13
+ export interface ParsedStep {
14
+ readonly keyword: "Given" | "When" | "Then" | "And" | "But";
15
+ readonly text: string;
16
+ readonly dataTable: DataTable | undefined;
17
+ readonly docString: string | undefined;
18
+ readonly location: StepLocation;
19
+ }
20
+ export interface Scenario {
21
+ readonly name: string;
22
+ readonly tags: readonly Tag[];
23
+ readonly steps: readonly ParsedStep[];
24
+ }
25
+ export interface Feature {
26
+ readonly name: string;
27
+ readonly description: string;
28
+ readonly tags: readonly Tag[];
29
+ readonly background: {
30
+ readonly steps: readonly ParsedStep[];
31
+ } | undefined;
32
+ readonly scenarios: readonly Scenario[];
33
+ readonly uri: string;
34
+ }
35
+ export declare function createDataTable(rows: readonly (readonly string[])[]): DataTable;
@@ -0,0 +1,3 @@
1
+ import type { BunPlugin } from "bun";
2
+ declare const featsPlugin: BunPlugin;
3
+ export default featsPlugin;
@@ -0,0 +1,8 @@
1
+ export interface SeededRng {
2
+ readonly seed: number;
3
+ nextInt(min: number, max: number): number;
4
+ pick<T>(items: readonly T[]): T;
5
+ shuffle<T>(items: readonly T[]): T[];
6
+ sample<T>(items: readonly T[], count: number): T[];
7
+ }
8
+ export declare function createRng(seed?: number): SeededRng;
@@ -0,0 +1,6 @@
1
+ import type { StepDefinition } from "@/registry/step-definition";
2
+ export interface MatchResult {
3
+ readonly definition: StepDefinition;
4
+ readonly args: readonly unknown[];
5
+ }
6
+ export declare function matchStep(definitions: readonly StepDefinition[], stepText: string): MatchResult;
@@ -0,0 +1,8 @@
1
+ import { ParameterTypeRegistry } from "@cucumber/cucumber-expressions";
2
+ export declare function getParameterTypeRegistry(): ParameterTypeRegistry;
3
+ export declare function clearParameterTypeRegistry(): void;
4
+ export declare function defineParameterType(opts: {
5
+ name: string;
6
+ regexp: RegExp;
7
+ transformer: (value: string) => unknown;
8
+ }): void;
@@ -0,0 +1,7 @@
1
+ import type { World } from "@/state/world";
2
+ export type StepCallback<W extends World = World> = (world: W, ...args: unknown[]) => Promise<void> | void;
3
+ export interface StepDefinition<W extends World = World> {
4
+ readonly keyword: "Given" | "When" | "Then" | "Step";
5
+ readonly pattern: string;
6
+ readonly callback: StepCallback<W>;
7
+ }
@@ -0,0 +1,14 @@
1
+ import type { StepCallback, StepDefinition } from "@/registry/step-definition";
2
+ import type { World } from "@/state/world";
3
+ export declare class StepRegistry {
4
+ private readonly definitions;
5
+ add(definition: StepDefinition): void;
6
+ getAll(): readonly StepDefinition[];
7
+ clear(): void;
8
+ }
9
+ export declare function getRegistry(): StepRegistry;
10
+ export declare function clearRegistry(): void;
11
+ export declare function Given<W extends World = World>(pattern: string, callback: StepCallback<W>): void;
12
+ export declare function When<W extends World = World>(pattern: string, callback: StepCallback<W>): void;
13
+ export declare function Then<W extends World = World>(pattern: string, callback: StepCallback<W>): void;
14
+ export declare function Step<W extends World = World>(pattern: string, callback: StepCallback<W>): void;
@@ -0,0 +1,2 @@
1
+ import type { ParsedStep } from "@/parser/models";
2
+ export declare function formatStepError(step: ParsedStep, error: unknown): string;
@@ -0,0 +1,2 @@
1
+ import type { ParsedStep } from "@/parser/models";
2
+ export declare function generateStepSnippet(step: ParsedStep): string;
@@ -0,0 +1,9 @@
1
+ import type { Feature } from "@/parser/models";
2
+ import type { HookDefinition } from "@/runner/hook-runner";
3
+ import type { WorldFactory } from "@/state/world";
4
+ export interface RunOptions {
5
+ worldFactory?: WorldFactory;
6
+ tagFilter?: string;
7
+ }
8
+ export declare function runFeatures(features: readonly Feature[], opts?: RunOptions): void;
9
+ export type { HookDefinition };
@@ -0,0 +1,12 @@
1
+ import type { World } from "@/state/world";
2
+ export interface HookDefinition {
3
+ readonly tagFilter: string | undefined;
4
+ readonly callback: (world: World) => Promise<void> | void;
5
+ }
6
+ export declare function Before(callback: (world: World) => Promise<void> | void): void;
7
+ export declare function Before(tagFilter: string, callback: (world: World) => Promise<void> | void): void;
8
+ export declare function After(callback: (world: World) => Promise<void> | void): void;
9
+ export declare function After(tagFilter: string, callback: (world: World) => Promise<void> | void): void;
10
+ export declare function getBeforeHooks(): HookDefinition[];
11
+ export declare function getAfterHooks(): HookDefinition[];
12
+ export declare function clearHooks(): void;
@@ -0,0 +1,3 @@
1
+ import type { ParsedStep } from "@/parser/models";
2
+ import type { World } from "@/state/world";
3
+ export declare function executeStep(world: World, step: ParsedStep): Promise<void>;
@@ -0,0 +1,2 @@
1
+ import type { Tag } from "@/parser/models";
2
+ export declare function matchesTagFilter(tags: readonly Tag[], filterExpr: string): boolean;
@@ -0,0 +1,4 @@
1
+ export interface World {
2
+ [key: string]: unknown;
3
+ }
4
+ export type WorldFactory<W extends World = World> = () => W;
@@ -0,0 +1 @@
1
+ export {};
@@ -0,0 +1 @@
1
+ export {};
@@ -0,0 +1 @@
1
+ export {};
@@ -0,0 +1 @@
1
+ export {};
@@ -0,0 +1 @@
1
+ import "./self-test.steps";
@@ -0,0 +1 @@
1
+ export declare function greet(name: string): string;
@@ -0,0 +1 @@
1
+ export {};
@@ -0,0 +1 @@
1
+ export {};
@@ -0,0 +1 @@
1
+ export {};
@@ -0,0 +1 @@
1
+ export {};
@@ -0,0 +1 @@
1
+ export {};
@@ -0,0 +1 @@
1
+ export {};
@@ -0,0 +1 @@
1
+ export {};
@@ -0,0 +1 @@
1
+ export {};
@@ -0,0 +1 @@
1
+ export {};
@@ -0,0 +1 @@
1
+ export {};
@@ -0,0 +1 @@
1
+ export {};
package/package.json ADDED
@@ -0,0 +1,43 @@
1
+ {
2
+ "name": "@questi0nm4rk/feats",
3
+ "version": "1.0.0",
4
+ "description": "BDD/Gherkin test framework for Bun — feature testing with typed step definitions",
5
+ "type": "module",
6
+ "exports": {
7
+ ".": {
8
+ "bun": "./src/feats.ts",
9
+ "import": "./dist/feats.js",
10
+ "types": "./dist/src/feats.d.ts"
11
+ },
12
+ "./plugin": {
13
+ "bun": "./src/plugin/bun-plugin.ts",
14
+ "import": "./dist/plugin/bun-plugin.js",
15
+ "types": "./dist/src/plugin/bun-plugin.d.ts"
16
+ }
17
+ },
18
+ "files": [
19
+ "dist/",
20
+ "src/"
21
+ ],
22
+ "scripts": {
23
+ "test": "bun test",
24
+ "test:watch": "bun test --watch",
25
+ "typecheck": "tsc --noEmit",
26
+ "lint": "biome check src/ tests/",
27
+ "lint:fix": "biome check --write src/ tests/",
28
+ "build": "bun build src/feats.ts src/plugin/bun-plugin.ts --outdir dist --target bun",
29
+ "build:types": "tsc --emitDeclarationOnly --outDir dist"
30
+ },
31
+ "dependencies": {
32
+ "@cucumber/cucumber-expressions": "^19.0.0",
33
+ "@cucumber/gherkin": "^37.0.0",
34
+ "@cucumber/messages": "^32.0.0",
35
+ "smol-toml": "^1.5.0",
36
+ "yaml": "^2.7.0"
37
+ },
38
+ "devDependencies": {
39
+ "@biomejs/biome": "^2.4.0",
40
+ "@types/bun": "^1.3.0",
41
+ "typescript": "^5.8.0"
42
+ }
43
+ }
@@ -0,0 +1,123 @@
1
+ import { readFileSync } from "node:fs";
2
+ import { extname } from "node:path";
3
+ import { parse as parseToml } from "smol-toml";
4
+ import { parse as parseYaml } from "yaml";
5
+
6
+ export interface AssertConfigOpts {
7
+ readonly format?: "json" | "toml" | "yaml";
8
+ readonly subset?: boolean;
9
+ }
10
+
11
+ type ConfigFormat = "json" | "toml" | "yaml";
12
+
13
+ function detectFormat(filePath: string): ConfigFormat {
14
+ const ext = extname(filePath).toLowerCase();
15
+ if (ext === ".json") return "json";
16
+ if (ext === ".toml") return "toml";
17
+ if (ext === ".yaml" || ext === ".yml") return "yaml";
18
+ throw new Error(`Cannot auto-detect config format from extension: ${ext}`);
19
+ }
20
+
21
+ function parseConfig(content: string, format: ConfigFormat): unknown {
22
+ if (format === "json") return JSON.parse(content) as unknown;
23
+ if (format === "toml") return parseToml(content);
24
+ return parseYaml(content) as unknown;
25
+ }
26
+
27
+ function isRecord(value: unknown): value is Record<string, unknown> {
28
+ return typeof value === "object" && value !== null && !Array.isArray(value);
29
+ }
30
+
31
+ function diffValues(expected: unknown, actual: unknown, path: string, subset: boolean): string[] {
32
+ if (isRecord(expected) && isRecord(actual)) {
33
+ return diffObjects(expected, actual, path, subset);
34
+ }
35
+
36
+ if (Array.isArray(expected) && Array.isArray(actual)) {
37
+ return diffArrays(expected, actual, path, subset);
38
+ }
39
+
40
+ const expectedStr = JSON.stringify(expected);
41
+ const actualStr = JSON.stringify(actual);
42
+ if (actualStr !== expectedStr) {
43
+ return [`${path}: expected ${expectedStr}, got ${actualStr}`];
44
+ }
45
+ return [];
46
+ }
47
+
48
+ function diffArrays(
49
+ expected: unknown[],
50
+ actual: unknown[],
51
+ path: string,
52
+ subset: boolean,
53
+ ): string[] {
54
+ const errors: string[] = [];
55
+
56
+ if (!subset && expected.length !== actual.length) {
57
+ errors.push(`${path}: expected array length ${expected.length}, got ${actual.length}`);
58
+ return errors;
59
+ }
60
+
61
+ for (let i = 0; i < expected.length; i++) {
62
+ if (i >= actual.length) {
63
+ errors.push(`${path}[${i}]: expected ${JSON.stringify(expected[i])}, got undefined`);
64
+ continue;
65
+ }
66
+ errors.push(...diffValues(expected[i], actual[i], `${path}[${i}]`, subset));
67
+ }
68
+
69
+ return errors;
70
+ }
71
+
72
+ function diffObjects(
73
+ expected: Record<string, unknown>,
74
+ actual: unknown,
75
+ path: string,
76
+ subset: boolean,
77
+ ): string[] {
78
+ const errors: string[] = [];
79
+
80
+ if (!isRecord(actual)) {
81
+ errors.push(`${path}: expected object, got ${typeof actual}`);
82
+ return errors;
83
+ }
84
+
85
+ for (const [key, expectedVal] of Object.entries(expected)) {
86
+ const fullPath = path !== "" ? `${path}.${key}` : key;
87
+ const actualVal = actual[key];
88
+
89
+ if (actualVal === undefined) {
90
+ errors.push(`${fullPath}: expected ${JSON.stringify(expectedVal)}, got undefined`);
91
+ continue;
92
+ }
93
+
94
+ errors.push(...diffValues(expectedVal, actualVal, fullPath, subset));
95
+ }
96
+
97
+ if (!subset) {
98
+ for (const key of Object.keys(actual)) {
99
+ if (!(key in expected)) {
100
+ const fullPath = path !== "" ? `${path}.${key}` : key;
101
+ errors.push(`${fullPath}: unexpected key in actual`);
102
+ }
103
+ }
104
+ }
105
+
106
+ return errors;
107
+ }
108
+
109
+ export function assertConfig(
110
+ filePath: string,
111
+ expected: Record<string, unknown>,
112
+ opts?: AssertConfigOpts,
113
+ ): void {
114
+ const subset = opts?.subset ?? true;
115
+ const format = opts?.format ?? detectFormat(filePath);
116
+ const content = readFileSync(filePath, "utf-8");
117
+ const actual = parseConfig(content, format);
118
+
119
+ const errors = diffObjects(expected, actual, "", subset);
120
+ if (errors.length > 0) {
121
+ throw new Error(`Config assertion failed:\n ${errors.join("\n ")}`);
122
+ }
123
+ }
@@ -0,0 +1,67 @@
1
+ import type { CLIResult } from "@/cli/cli-result";
2
+
3
+ export interface OutputExpectations {
4
+ readonly stdout?: string | RegExp;
5
+ readonly stderr?: string | RegExp;
6
+ readonly exitCode?: number;
7
+ readonly contains?: string;
8
+ readonly notContains?: string;
9
+ }
10
+
11
+ function matchesExpectation(actual: string, expected: string | RegExp): boolean {
12
+ if (typeof expected === "string") {
13
+ return actual.includes(expected);
14
+ }
15
+ return expected.test(actual);
16
+ }
17
+
18
+ function serializeExpected(expected: string | RegExp): string {
19
+ if (typeof expected === "string") {
20
+ return JSON.stringify(expected);
21
+ }
22
+ return expected.toString();
23
+ }
24
+
25
+ export function assertOutput(result: CLIResult, expectations: OutputExpectations): void {
26
+ if (expectations.stdout !== undefined) {
27
+ if (!matchesExpectation(result.stdout, expectations.stdout)) {
28
+ throw new Error(
29
+ `stdout assertion failed: expected ${serializeExpected(expectations.stdout)}, got ${JSON.stringify(result.stdout)}`,
30
+ );
31
+ }
32
+ }
33
+
34
+ if (expectations.stderr !== undefined) {
35
+ if (!matchesExpectation(result.stderr, expectations.stderr)) {
36
+ throw new Error(
37
+ `stderr assertion failed: expected ${serializeExpected(expectations.stderr)}, got ${JSON.stringify(result.stderr)}`,
38
+ );
39
+ }
40
+ }
41
+
42
+ if (expectations.exitCode !== undefined) {
43
+ if (result.exitCode !== expectations.exitCode) {
44
+ throw new Error(
45
+ `exitCode assertion failed: expected ${expectations.exitCode}, got ${result.exitCode}`,
46
+ );
47
+ }
48
+ }
49
+
50
+ const combined = result.stdout + result.stderr;
51
+
52
+ if (expectations.contains !== undefined) {
53
+ if (!combined.includes(expectations.contains)) {
54
+ throw new Error(
55
+ `contains assertion failed: expected output to contain ${JSON.stringify(expectations.contains)}`,
56
+ );
57
+ }
58
+ }
59
+
60
+ if (expectations.notContains !== undefined) {
61
+ if (combined.includes(expectations.notContains)) {
62
+ throw new Error(
63
+ `notContains assertion failed: expected output not to contain ${JSON.stringify(expectations.notContains)}`,
64
+ );
65
+ }
66
+ }
67
+ }
@@ -0,0 +1,6 @@
1
+ export interface CLIResult {
2
+ readonly stdout: string;
3
+ readonly stderr: string;
4
+ readonly exitCode: number;
5
+ readonly timedOut: boolean;
6
+ }
@@ -0,0 +1,66 @@
1
+ import type { CLIResult } from "@/cli/cli-result";
2
+
3
+ export interface RunCliOpts {
4
+ readonly cwd?: string;
5
+ readonly timeout?: number;
6
+ readonly env?: Record<string, string>;
7
+ readonly stdin?: string;
8
+ }
9
+
10
+ export async function runCli(
11
+ command: string,
12
+ args?: string[],
13
+ opts?: RunCliOpts,
14
+ ): Promise<CLIResult> {
15
+ const timeout = opts?.timeout ?? 30000;
16
+
17
+ const stdinValue: "ignore" | Uint8Array =
18
+ opts?.stdin !== undefined ? new TextEncoder().encode(opts.stdin) : "ignore";
19
+
20
+ const envValue = opts?.env !== undefined ? { ...process.env, ...opts.env } : process.env;
21
+
22
+ const spawnOpts = {
23
+ env: envValue,
24
+ stdin: stdinValue,
25
+ stdout: "pipe" as const,
26
+ stderr: "pipe" as const,
27
+ ...(opts?.cwd !== undefined ? { cwd: opts.cwd } : {}),
28
+ };
29
+
30
+ const proc = Bun.spawn([command, ...(args ?? [])], spawnOpts);
31
+
32
+ let timedOut = false;
33
+
34
+ const waitWithTimeout = async (): Promise<number> => {
35
+ return new Promise((resolve) => {
36
+ const timer = setTimeout(() => {
37
+ timedOut = true;
38
+ proc.kill();
39
+ resolve(1);
40
+ }, timeout);
41
+
42
+ proc.exited
43
+ .then((code) => {
44
+ clearTimeout(timer);
45
+ resolve(code ?? 1);
46
+ })
47
+ .catch(() => {
48
+ clearTimeout(timer);
49
+ resolve(1);
50
+ });
51
+ });
52
+ };
53
+
54
+ const [exitCode, stdoutText, stderrText] = await Promise.all([
55
+ waitWithTimeout(),
56
+ new Response(proc.stdout).text(),
57
+ new Response(proc.stderr).text(),
58
+ ]);
59
+
60
+ return {
61
+ stdout: stdoutText,
62
+ stderr: stderrText,
63
+ exitCode,
64
+ timedOut,
65
+ };
66
+ }
package/src/feats.ts ADDED
@@ -0,0 +1,30 @@
1
+ // Assertions
2
+ export { assertConfig } from "./assertions/config-assertions";
3
+ export { assertOutput } from "./assertions/output-assertions";
4
+
5
+ // CLI execution
6
+ export type { CLIResult } from "./cli/cli-result";
7
+ export { runCli } from "./cli/cli-runner";
8
+
9
+ // Fixtures
10
+ export { composeFixtures, setupFixture } from "./fixtures/fixture-manager";
11
+ export type { FixtureProject } from "./fixtures/fixture-project";
12
+
13
+ // Feature loading + running
14
+ export { loadFeatures, parseFeature } from "./parser/adapter";
15
+ export type { DataTable, Feature, ParsedStep, Scenario, Tag } from "./parser/models";
16
+
17
+ // Random
18
+ export type { SeededRng } from "./random/seeded-rng";
19
+ export { createRng } from "./random/seeded-rng";
20
+
21
+ // Step definitions
22
+ export { defineParameterType } from "./registry/parameter-types";
23
+ export { Given, Step, Then, When } from "./registry/step-registry";
24
+ export type { RunOptions } from "./runner/feature-runner";
25
+ export { runFeatures } from "./runner/feature-runner";
26
+ // Lifecycle hooks
27
+ export { After, Before } from "./runner/hook-runner";
28
+
29
+ // Types
30
+ export type { World, WorldFactory } from "./state/world";