@glubean/cli 0.1.2

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 (113) hide show
  1. package/bin/gb.js +2 -0
  2. package/dist/commands/init.d.ts +19 -0
  3. package/dist/commands/init.d.ts.map +1 -0
  4. package/dist/commands/init.js +842 -0
  5. package/dist/commands/init.js.map +1 -0
  6. package/dist/commands/login.d.ts +10 -0
  7. package/dist/commands/login.d.ts.map +1 -0
  8. package/dist/commands/login.js +75 -0
  9. package/dist/commands/login.js.map +1 -0
  10. package/dist/commands/patch.d.ts +8 -0
  11. package/dist/commands/patch.d.ts.map +1 -0
  12. package/dist/commands/patch.js +73 -0
  13. package/dist/commands/patch.js.map +1 -0
  14. package/dist/commands/run.d.ts +26 -0
  15. package/dist/commands/run.d.ts.map +1 -0
  16. package/dist/commands/run.js +1093 -0
  17. package/dist/commands/run.js.map +1 -0
  18. package/dist/commands/scan.d.ts +6 -0
  19. package/dist/commands/scan.d.ts.map +1 -0
  20. package/dist/commands/scan.js +62 -0
  21. package/dist/commands/scan.js.map +1 -0
  22. package/dist/commands/spec_split.d.ts +5 -0
  23. package/dist/commands/spec_split.d.ts.map +1 -0
  24. package/dist/commands/spec_split.js +56 -0
  25. package/dist/commands/spec_split.js.map +1 -0
  26. package/dist/commands/sync.d.ts +13 -0
  27. package/dist/commands/sync.d.ts.map +1 -0
  28. package/dist/commands/sync.js +252 -0
  29. package/dist/commands/sync.js.map +1 -0
  30. package/dist/commands/trigger.d.ts +13 -0
  31. package/dist/commands/trigger.d.ts.map +1 -0
  32. package/dist/commands/trigger.js +213 -0
  33. package/dist/commands/trigger.js.map +1 -0
  34. package/dist/commands/validate_metadata.d.ts +6 -0
  35. package/dist/commands/validate_metadata.d.ts.map +1 -0
  36. package/dist/commands/validate_metadata.js +103 -0
  37. package/dist/commands/validate_metadata.js.map +1 -0
  38. package/dist/commands/worker.d.ts +14 -0
  39. package/dist/commands/worker.d.ts.map +1 -0
  40. package/dist/commands/worker.js +10 -0
  41. package/dist/commands/worker.js.map +1 -0
  42. package/dist/lib/auth.d.ts +39 -0
  43. package/dist/lib/auth.d.ts.map +1 -0
  44. package/dist/lib/auth.js +82 -0
  45. package/dist/lib/auth.js.map +1 -0
  46. package/dist/lib/ci.d.ts +12 -0
  47. package/dist/lib/ci.d.ts.map +1 -0
  48. package/dist/lib/ci.js +42 -0
  49. package/dist/lib/ci.js.map +1 -0
  50. package/dist/lib/config.d.ts +116 -0
  51. package/dist/lib/config.d.ts.map +1 -0
  52. package/dist/lib/config.js +264 -0
  53. package/dist/lib/config.js.map +1 -0
  54. package/dist/lib/constants.d.ts +6 -0
  55. package/dist/lib/constants.d.ts.map +1 -0
  56. package/dist/lib/constants.js +6 -0
  57. package/dist/lib/constants.js.map +1 -0
  58. package/dist/lib/env.d.ts +19 -0
  59. package/dist/lib/env.d.ts.map +1 -0
  60. package/dist/lib/env.js +40 -0
  61. package/dist/lib/env.js.map +1 -0
  62. package/dist/lib/git.d.ts +8 -0
  63. package/dist/lib/git.d.ts.map +1 -0
  64. package/dist/lib/git.js +68 -0
  65. package/dist/lib/git.js.map +1 -0
  66. package/dist/lib/openapi_patch.d.ts +23 -0
  67. package/dist/lib/openapi_patch.d.ts.map +1 -0
  68. package/dist/lib/openapi_patch.js +232 -0
  69. package/dist/lib/openapi_patch.js.map +1 -0
  70. package/dist/lib/openapi_split.d.ts +16 -0
  71. package/dist/lib/openapi_split.d.ts.map +1 -0
  72. package/dist/lib/openapi_split.js +188 -0
  73. package/dist/lib/openapi_split.js.map +1 -0
  74. package/dist/lib/upload.d.ts +44 -0
  75. package/dist/lib/upload.d.ts.map +1 -0
  76. package/dist/lib/upload.js +297 -0
  77. package/dist/lib/upload.js.map +1 -0
  78. package/dist/main.d.ts +8 -0
  79. package/dist/main.d.ts.map +1 -0
  80. package/dist/main.js +319 -0
  81. package/dist/main.js.map +1 -0
  82. package/dist/metadata.d.ts +17 -0
  83. package/dist/metadata.d.ts.map +1 -0
  84. package/dist/metadata.js +61 -0
  85. package/dist/metadata.js.map +1 -0
  86. package/dist/update_check.d.ts +14 -0
  87. package/dist/update_check.d.ts.map +1 -0
  88. package/dist/update_check.js +130 -0
  89. package/dist/update_check.js.map +1 -0
  90. package/dist/version.d.ts +5 -0
  91. package/dist/version.d.ts.map +1 -0
  92. package/dist/version.js +11 -0
  93. package/dist/version.js.map +1 -0
  94. package/package.json +34 -0
  95. package/templates/AI-INSTRUCTIONS.md +163 -0
  96. package/templates/README.md +226 -0
  97. package/templates/claude-skill-glubean-test.md +382 -0
  98. package/templates/data/create-user.json +14 -0
  99. package/templates/data/endpoints.csv +5 -0
  100. package/templates/data/scenarios.yaml +19 -0
  101. package/templates/data/search-examples.json +14 -0
  102. package/templates/data/users.json +17 -0
  103. package/templates/data-driven.test.ts.tpl +118 -0
  104. package/templates/demo.test.result.json +398 -0
  105. package/templates/demo.test.ts.tpl +226 -0
  106. package/templates/explore-api.test.result.json +79 -0
  107. package/templates/minimal/README.md +42 -0
  108. package/templates/minimal-api.test.ts.tpl +42 -0
  109. package/templates/minimal-auth.test.ts.tpl +45 -0
  110. package/templates/minimal-search.test.ts.tpl +34 -0
  111. package/templates/openapi.sample.json +97 -0
  112. package/templates/pick.test.result.json +165 -0
  113. package/templates/pick.test.ts.tpl +126 -0
@@ -0,0 +1,226 @@
1
+ # Glubean API Tests
2
+
3
+ This project was generated by `glubean init`.
4
+
5
+ ## Project Structure
6
+
7
+ ```
8
+ ├── tests/ # Permanent tests — run in CI, Cloud, and locally
9
+ ├── explore/ # Exploratory tests — quick iteration in your editor
10
+ ├── data/ # Shared test data (JSON, CSV, YAML) used by both
11
+ ├── context/ # API specs and reference docs for AI and tooling
12
+ ├── deno.json # Config: tasks, SDK import, testDir/exploreDir
13
+ ├── .env # Default environment variables (BASE_URL, etc.)
14
+ ├── .env.secrets # Default secrets (API keys — gitignored)
15
+ ├── .env.staging # Staging environment variables
16
+ ├── .env.staging.secrets # Staging secrets (gitignored)
17
+ ├── CLAUDE.md # AI instructions (Claude Code, Cursor)
18
+ └── AGENTS.md # AI instructions (Codex, other agents)
19
+ ```
20
+
21
+ **Required:** `tests/` and `deno.json`. Everything else is recommended.
22
+
23
+ > **AI instructions:** Both `CLAUDE.md` and `AGENTS.md` are generated with the same content. Keep whichever your AI tool
24
+ > reads — **Claude Code** reads `CLAUDE.md`, **Codex** reads `AGENTS.md`, **Cursor** reads both. Delete the one you
25
+ > don't need.
26
+
27
+ - **`tests/`** — Your permanent test files (`*.test.ts`). These are what `deno task test` and CI run. All test files
28
+ must end in `.test.ts`.
29
+ - **`explore/`** — Same `*.test.ts` files, but for interactive exploration. Think of it as a scratchpad: quick API
30
+ calls, debugging, prototyping. Move files to `tests/` when they're ready to be permanent. Run with
31
+ `deno task explore`.
32
+ - **`data/`** — Shared test data files (JSON, CSV, YAML). Both `tests/` and `explore/` can import from here. Keeps test
33
+ logic clean and data reviewable.
34
+ - **`context/`** — API specs (OpenAPI), reference docs, and anything that helps you (or your AI) understand the API
35
+ under test. Drop your OpenAPI spec here and point your AI at it.
36
+
37
+ The `data/` and `context/` directories are optional conventions — you can organize data and specs however you prefer.
38
+ These defaults exist because they work well with the CLI tooling and AI workflows.
39
+
40
+ ## Prerequisites
41
+
42
+ 1. Install [Deno](https://deno.land) (v2+):
43
+
44
+ ```bash
45
+ # macOS
46
+ brew install deno
47
+
48
+ # or see https://docs.deno.com/runtime/getting_started/installation/
49
+ ```
50
+
51
+ 2. Install the Glubean CLI:
52
+
53
+ ```bash
54
+ deno install -Ag -n glubean jsr:@glubean/cli
55
+ ```
56
+
57
+ ## Quick start
58
+
59
+ 1. Edit `.env` with your API base URL
60
+ 2. Edit `.env.secrets` with any API keys
61
+ 3. Run tests:
62
+
63
+ ```bash
64
+ deno task test # run all tests in tests/
65
+ deno task test:verbose # with detailed output
66
+ deno task test:staging # run against staging environment
67
+ deno task explore # run explore/ tests
68
+ ```
69
+
70
+ Or run a specific file:
71
+
72
+ ```bash
73
+ glubean run tests/demo.test.ts --verbose
74
+ ```
75
+
76
+ ## Environment switching
77
+
78
+ The same tests can run against different environments by switching env files. Each env file pairs with a secrets file
79
+ automatically:
80
+
81
+ | Env file | Secrets file | Task shortcut |
82
+ | -------------- | ---------------------- | ------------------------ |
83
+ | `.env` | `.env.secrets` | `deno task test` |
84
+ | `.env.staging` | `.env.staging.secrets` | `deno task test:staging` |
85
+
86
+ To add more environments (e.g. production), create `.env.production` and `.env.production.secrets`, then run:
87
+
88
+ ```bash
89
+ glubean run --env-file .env.production
90
+ ```
91
+
92
+ Or add a task in `deno.json`:
93
+
94
+ ```json
95
+ "test:production": "glubean run --env-file .env.production"
96
+ ```
97
+
98
+ All `*.secrets` files are gitignored by default. The non-secret `.env.*` files can be committed safely — they typically
99
+ only contain `BASE_URL` and other non-sensitive config.
100
+
101
+ ## Use your AI to generate tests (from OpenAPI)
102
+
103
+ This project includes a sample OpenAPI spec at `context/openapi.sample.json`. Most AI coding tools can use this spec to
104
+ generate accurate tests quickly.
105
+
106
+ Copy/paste prompts you can try (edit as needed):
107
+
108
+ 1. Generate a smoke suite:
109
+
110
+ > Read `context/openapi.sample.json` and `CLAUDE.md` (or `AGENTS.md`). Generate Glubean tests in `tests/smoke.test.ts`
111
+ > that cover all GET endpoints with 200 responses. Use the builder API with `.step()` for multi-step flows. Use
112
+ > `ctx.http` for requests (auto-traces every call). Add assertions for status code and basic response schema. Group
113
+ > tests by tag.
114
+
115
+ 2. Generate auth + negative tests:
116
+
117
+ > Using `context/openapi.sample.json`, generate tests in `tests/auth.test.ts` that verify authentication behavior:
118
+ > missing API key returns 401, invalid API key returns 401, insufficient scope returns 403. Use the builder API with
119
+ > `.setup()` for shared auth headers. Use `ctx.secrets.require("API_KEY")` and also test missing/empty API key scenarios
120
+ > safely.
121
+
122
+ 3. Generate CRUD flow tests:
123
+
124
+ > From `context/openapi.sample.json`, identify resources with create/read/update/delete endpoints. Generate builder API
125
+ > tests in `tests/crud.test.ts` with steps: create → verify → update → verify → delete → verify. Pass state (IDs)
126
+ > between steps.
127
+
128
+ 4. Generate validation tests:
129
+
130
+ > From `context/openapi.sample.json`, identify POST/PUT endpoints with request bodies. Generate tests in
131
+ > `tests/validation.test.ts` for validation edge cases (missing required fields, invalid enums, min/max constraints).
132
+ > Assert 422 responses and validate the error schema shape.
133
+
134
+ ## Try MCP in Cursor (AI closed loop)
135
+
136
+ Glubean also ships an MCP (Model Context Protocol) server so your AI tool can **discover tests**, **run them**, read
137
+ **structured failures**, and iterate quickly:
138
+
139
+ > AI writes tests → runs locally → reads assertions/logs/traces → fixes → reruns → ✓ pass
140
+
141
+ ### Setup
142
+
143
+ 1. Configure the MCP server in Cursor (via UI or `~/.cursor/mcp.json`):
144
+
145
+ ```json
146
+ {
147
+ "mcpServers": {
148
+ "glubean": {
149
+ "command": "deno",
150
+ "args": ["run", "-A", "jsr:@glubean/mcp"],
151
+ "description": "Glubean test runner and cloud integration"
152
+ }
153
+ }
154
+ }
155
+ ```
156
+
157
+ 2. Restart Cursor.
158
+
159
+ ### Prompts to try
160
+
161
+ - Generate + run + fix (local):
162
+
163
+ > Use the Glubean MCP tools to discover tests in `tests/`, run them locally, and if any fail, fix the test code and
164
+ > rerun until everything passes. Keep changes minimal and explain each fix.
165
+
166
+ - Generate tests from spec + run (local):
167
+
168
+ > Read `context/openapi.sample.json` and generate Glubean tests in `tests/smoke.test.ts`. Then use MCP to run them
169
+ > locally. If validation or auth failures happen, adjust payloads/headers and rerun until green.
170
+
171
+ ### Optional: enable cloud tools
172
+
173
+ Local MCP tools do not require cloud auth. If you also want the AI to trigger remote runs via the Open Platform, set:
174
+
175
+ ```bash
176
+ export GLUBEAN_TOKEN=gpt_your_project_token
177
+ export GLUBEAN_API_URL=https://api.glubean.com
178
+ ```
179
+
180
+ ## What you get from the cloud (and why it matters)
181
+
182
+ Running locally is great for authoring and debugging. The cloud is where the same verification code becomes a
183
+ **continuous operational workflow** — no rewrites, no extra config files, no separate CI pipeline.
184
+
185
+ ### The problem Glubean Cloud solves
186
+
187
+ | Without Glubean Cloud | With Glubean Cloud |
188
+ | ------------------------------------------------ | ----------------------------------------------------------------------- |
189
+ | Tests run in CI, pass, and are forgotten | Tests run on a schedule — every 5 min, hourly, daily |
190
+ | Staging tested, production assumed OK | Same tests, different environments, zero code changes |
191
+ | API breaks at 3am, discovered next morning | Slack/email alert within minutes with exact failure details |
192
+ | Test results are ephemeral terminal output | Structured history: assertions, traces, logs — searchable and shareable |
193
+ | Secrets hardcoded or scattered across CI configs | Encrypted environment groups, injected at runtime, never in git |
194
+
195
+ ### What the cloud provides
196
+
197
+ - **Automatic bundle builds**: `git push` triggers a build — your test files are packaged into an immutable bundle (like
198
+ a Docker image for verification).
199
+ - **Environment groups**: configure `staging` and `production` with different `BASE_URL`, API keys, and secrets. Run the
200
+ same tests against both.
201
+ - **Scheduling**: every N minutes, daily at a specific time, weekly, or cron. Workers pick up runs from a queue —
202
+ nothing to maintain.
203
+ - **Alerts**: Slack, email, or webhooks. Failures include the exact assertion that broke (`expected 200, got 502`), not
204
+ a generic "tests failed" message.
205
+ - **Dashboard**: full run history with assertion timelines, API traces, HTTP metrics, and step-level breakdowns.
206
+ - **Open Platform**: project-scoped tokens and webhooks for integrating results into your internal tools and automation.
207
+
208
+ ### Get started with the cloud
209
+
210
+ 1. Create an account at `https://app.glubean.com`
211
+ 2. Create a Project and connect your GitHub repository
212
+ 3. Push your tests:
213
+
214
+ ```bash
215
+ git add .
216
+ git commit -m "add api tests"
217
+ git push
218
+ ```
219
+
220
+ 4. Glubean builds your bundle automatically. Click **Run Now** in the dashboard, or set up a schedule to run
221
+ continuously.
222
+
223
+ ## Notes
224
+
225
+ - `.env.secrets` should stay local and must not be committed.
226
+ - Edit test files in `tests/` to match your real API behavior and contracts.
@@ -0,0 +1,382 @@
1
+ ---
2
+ name: gb
3
+ description: Generate Glubean API tests from OpenAPI specs or user instructions. Reads context/, writes tests, runs them via MCP, and fixes failures automatically.
4
+ allowed-tools:
5
+ - Read
6
+ - Write
7
+ - Edit
8
+ - Glob
9
+ - Grep
10
+ - Bash
11
+ - mcp__glubean__glubean_run_local_file
12
+ - mcp__glubean__glubean_discover_tests
13
+ - mcp__glubean__glubean_list_test_files
14
+ - mcp__glubean__glubean_diagnose_config
15
+ - mcp__glubean__glubean_get_last_run_summary
16
+ - mcp__glubean__glubean_get_local_events
17
+ ---
18
+
19
+ # Glubean Test Generator
20
+
21
+ You are a Glubean test expert. Generate, run, and fix API tests using `@glubean/sdk`.
22
+
23
+ ## Your workflow
24
+
25
+ 1. **Read the API spec** — first check for `context/*-endpoints/_index.md` (pre-split specs). If found, read the index
26
+ and only open the specific endpoint file you need. If no split endpoints exist, search `context/` for OpenAPI specs
27
+ (`.json`, `.yaml`). If a spec is larger than 50K, suggest the user run `glubean spec split context/<file>` first. If
28
+ no spec found, ask the user for endpoint details.
29
+ 2. **Read existing tests** — check `tests/` and `explore/` for patterns, configure files, and naming conventions already
30
+ in use.
31
+ 3. **Write tests** — generate test files following the SDK API and conventions below.
32
+ 4. **Run tests** — use MCP tool `glubean_run_local_file` to execute. If MCP is unavailable, use `deno task test` via
33
+ Bash.
34
+ 5. **Fix failures** — read the structured failure output, fix the test code, and rerun. Repeat until green.
35
+
36
+ If $ARGUMENTS is provided, treat it as the target: an endpoint path, a tag, a file to test, or a natural language
37
+ description.
38
+
39
+ ## Project structure
40
+
41
+ ```
42
+ tests/ # Permanent test files (*.test.ts)
43
+ explore/ # Exploratory tests (same format, for iteration)
44
+ data/ # Test data files (JSON, CSV, YAML)
45
+ context/ # OpenAPI specs and reference docs
46
+ .env # Public variables (BASE_URL)
47
+ .env.secrets # Credentials — gitignored
48
+ deno.json # Runtime config, imports, glubean settings
49
+ ```
50
+
51
+ ## Import convention
52
+
53
+ Always use the import map alias from `deno.json`:
54
+
55
+ ```typescript
56
+ import { configure, fromCsv, fromDir, fromYaml, test } from "@glubean/sdk";
57
+ ```
58
+
59
+ Never use `jsr:` URLs directly.
60
+
61
+ ---
62
+
63
+ ## SDK API Reference
64
+
65
+ ### test()
66
+
67
+ ```typescript
68
+ // Quick mode — single function
69
+ export const myTest = test(
70
+ { id: "kebab-case-id", name: "Human name", tags: ["api"] },
71
+ async (ctx) => { ... }
72
+ );
73
+
74
+ // Builder mode — multi-step
75
+ export const myFlow = test("flow-id")
76
+ .meta({ name: "Flow name", tags: ["api"] })
77
+ .setup(async (ctx) => { return { token: "..." }; })
78
+ .step("step-name", async (ctx, state) => { return { ...state, new: "data" }; })
79
+ .teardown(async (ctx, state) => { /* cleanup, always runs */ });
80
+
81
+ // Reusable steps with .use() — extract common sequences into plain functions
82
+ const withAuth = (b: TestBuilder<unknown>) => b
83
+ .step("login", async (ctx) => {
84
+ const { token } = await ctx.http.post("/login", { json: { ... } }).json<{ token: string }>();
85
+ return { token };
86
+ });
87
+
88
+ export const testA = test("test-a").use(withAuth).step("act", async (ctx, { token }) => { ... });
89
+ export const testB = test("test-b").use(withAuth).step("verify", async (ctx, { token }) => { ... });
90
+
91
+ // .group(id, fn) — same as .use() but tags steps for visual grouping in reports
92
+ export const checkout = test("checkout")
93
+ .group("auth", withAuth)
94
+ .step("pay", async (ctx, { token }) => { ... });
95
+ // Report: checkout → [auth] login → pay
96
+ ```
97
+
98
+ ### TestContext (ctx)
99
+
100
+ ```typescript
101
+ ctx.http // HTTP client (auto-traces)
102
+ ctx.expect(value) // Soft assertion
103
+ ctx.assert(condition, message?) // Hard assertion
104
+ ctx.log(message, data?) // Structured log
105
+ ctx.vars.require("KEY") // Read env var (throws if missing)
106
+ ctx.secrets.require("KEY") // Read secret (auto-redacted)
107
+ ctx.metric("name", value, { unit? }) // Record metric
108
+ ctx.validate(data, zodSchema) // Schema validation
109
+ ctx.pollUntil({ timeoutMs }, fn) // Poll until truthy
110
+ ctx.skip(reason?) // Skip test
111
+ ```
112
+
113
+ ### HTTP Client
114
+
115
+ ```typescript
116
+ const res = await ctx.http.get(url, options?);
117
+ const data = await ctx.http.post(url, { json: { ... } }).json<T>();
118
+ // Also: .put(), .patch(), .delete()
119
+ // Response: .json<T>(), .text(), .blob()
120
+ ```
121
+
122
+ Options:
123
+
124
+ ```typescript
125
+ {
126
+ json: { ... }, // JSON body
127
+ searchParams: { key: "value" }, // Query params
128
+ headers: { "X-Custom": "val" }, // Headers
129
+ timeout: 5000, // ms
130
+ retry: 3, // Retry count
131
+ }
132
+ ```
133
+
134
+ Extend with defaults:
135
+
136
+ ```typescript
137
+ const authed = ctx.http.extend({
138
+ headers: { Authorization: `Bearer ${token}` },
139
+ });
140
+ ```
141
+
142
+ ### Assertions — ctx.expect()
143
+
144
+ ```typescript
145
+ expect(x).toBe(y); // Strict equal
146
+ expect(x).toEqual(y); // Deep equal
147
+ expect(x).toBeTruthy();
148
+ expect(x).toBeDefined();
149
+ expect(n).toBeGreaterThan(5);
150
+ expect(s).toContain("sub");
151
+ expect(s).toMatch(/regex/);
152
+ expect(arr).toHaveLength(3);
153
+ expect(obj).toMatchObject({ key: "val" });
154
+ expect(obj).toHaveProperty("path.to.key");
155
+ expect(res).toHaveStatus(200); // HTTP response
156
+ expect(x).not.toBe(y); // Negate
157
+ expect(x).toBe(y).orFail(); // Hard fail (stop test)
158
+ ```
159
+
160
+ ### configure() — shared HTTP client
161
+
162
+ ```typescript
163
+ // config/api.ts or tests/configure.ts
164
+ import { configure } from "@glubean/sdk";
165
+
166
+ export const { http, vars, secrets } = configure({
167
+ vars: { baseUrl: "BASE_URL" },
168
+ secrets: { apiKey: "API_KEY" },
169
+ http: {
170
+ prefixUrl: "BASE_URL", // Env var name
171
+ headers: {
172
+ Authorization: "Bearer {{API_KEY}}", // {{var}} interpolation
173
+ },
174
+ },
175
+ });
176
+ ```
177
+
178
+ ### Data loading
179
+
180
+ ```typescript
181
+ const rows = await fromDir<T>("./data/users/"); // One JSON file = one row
182
+ const cases = await fromDir.merge<T>("./data/search/"); // Merged for test.pick
183
+ const csv = await fromCsv<T>("./data/file.csv");
184
+ const yaml = await fromYaml<T>("./data/file.yaml");
185
+ ```
186
+
187
+ **Data file formats:**
188
+
189
+ `fromDir` — each file is one row. File content is a flat object:
190
+
191
+ ```json
192
+ // data/users/alice.json — becomes one row with _name="alice"
193
+ { "username": "alice", "role": "admin", "expected": 200 }
194
+ ```
195
+
196
+ `fromDir.merge` — each file contains named examples as top-level keys. Keys = pick names, values = scenario objects. All
197
+ files are shallow-merged into one map:
198
+
199
+ ```json
200
+ // data/search/queries.json
201
+ {
202
+ "by-name": { "q": "phone", "expected": "phone" },
203
+ "by-category": { "q": "laptop", "expected": "laptop" }
204
+ }
205
+ // data/search/edge-cases.json
206
+ {
207
+ "empty-query": { "q": "", "expected": "" }
208
+ }
209
+ // → merged result: { "by-name": {...}, "by-category": {...}, "empty-query": {...} }
210
+ ```
211
+
212
+ Inline examples (no data file needed):
213
+
214
+ ```typescript
215
+ // test.each — array of objects
216
+ test.each([
217
+ { id: 1, expected: 200 },
218
+ { id: 999, expected: 404 },
219
+ ])("get-user-$id", async (ctx, { id, expected }) => { ... });
220
+
221
+ // test.pick — object with named keys
222
+ test.pick({
223
+ "normal": { name: "Alice", age: 25 },
224
+ "edge-case": { name: "", age: -1 },
225
+ })("create-user-$_pick", async (ctx, data) => { ... });
226
+ ```
227
+
228
+ ### test.each — data-driven
229
+
230
+ ```typescript
231
+ // Quick mode — single function per row
232
+ const users = await fromDir<{ username: string }>("./data/users/");
233
+ export const tests = test.each(users)(
234
+ "user-lookup-$username", // $field interpolates
235
+ async (ctx, { username }) => { ... },
236
+ );
237
+
238
+ // Builder mode — multi-step per row (omit callback to get builder)
239
+ export const flows = test.each(users)("user-flow-$username")
240
+ .step("fetch", async (ctx, _state, row) => {
241
+ const res = await ctx.http.get(`/users/${row.username}`).json<{ id: string }>();
242
+ return { id: res.id };
243
+ })
244
+ .step("verify", async (ctx, state, row) => {
245
+ ctx.expect(state.id).toBeDefined();
246
+ });
247
+ ```
248
+
249
+ ### test.pick — named examples
250
+
251
+ ```typescript
252
+ // Quick mode — single function
253
+ const cases = await fromDir.merge<{ q: string }>("./data/search/");
254
+ export const tests = test.pick(cases)(
255
+ "search-$_pick", // $_pick = case name
256
+ async (ctx, { q }) => { ... },
257
+ );
258
+
259
+ // Builder mode — multi-step per picked example (omit callback to get builder)
260
+ export const flows = test.pick(cases)("search-flow-$_pick")
261
+ .setup(async (ctx, row) => ({ query: row.q }))
262
+ .step("search", async (ctx, state, row) => {
263
+ const res = await ctx.http.get("/search", { searchParams: { q: state.query } }).json<{ total: number }>();
264
+ return { ...state, total: res.total };
265
+ })
266
+ .step("verify", async (ctx, state) => {
267
+ ctx.expect(state.total).toBeGreaterThan(0);
268
+ })
269
+ .teardown(async (ctx, state) => { /* cleanup */ });
270
+ ```
271
+
272
+ Both `test.each` and `test.pick` support the same builder API as `test()`. Omit the callback to enter builder mode;
273
+ chain `.step()`, `.setup()`, `.teardown()`, `.meta()`, etc.
274
+
275
+ ---
276
+
277
+ ## Patterns
278
+
279
+ ### Simple API test
280
+
281
+ ```typescript
282
+ import { test } from "@glubean/sdk";
283
+ import { http } from "./configure.ts";
284
+
285
+ export const listUsers = test(
286
+ { id: "list-users", tags: ["api", "smoke"] },
287
+ async ({ expect }) => {
288
+ const users = await http.get("users").json<{ users: unknown[] }>();
289
+ expect(users.users.length).toBeGreaterThan(0);
290
+ },
291
+ );
292
+ ```
293
+
294
+ ### CRUD with cleanup
295
+
296
+ ```typescript
297
+ export const crud = test("resource-crud")
298
+ .meta({ tags: ["api"] })
299
+ .setup(async () => {
300
+ const item = await http.post("items", { json: { name: "test" } }).json<{ id: string }>();
301
+ return { id: item.id };
302
+ })
303
+ .step("read", async ({ expect }, state) => {
304
+ const item = await http.get(`items/${state.id}`).json<{ name: string }>();
305
+ expect(item.name).toBe("test");
306
+ return state;
307
+ })
308
+ .step("update", async ({ expect }, state) => {
309
+ const item = await http.put(`items/${state.id}`, { json: { name: "updated" } }).json<{ name: string }>();
310
+ expect(item.name).toBe("updated");
311
+ return state;
312
+ })
313
+ .teardown(async (_ctx, state) => {
314
+ if (state?.id) await http.delete(`items/${state.id}`);
315
+ });
316
+ ```
317
+
318
+ ### Auth flow
319
+
320
+ ```typescript
321
+ export const auth = test("auth-flow")
322
+ .meta({ tags: ["api", "auth"] })
323
+ .step("login", async ({ http, expect, secrets }) => {
324
+ const res = await http.post("https://api.example.com/auth/login", {
325
+ json: { username: secrets.require("USERNAME"), password: secrets.require("PASSWORD") },
326
+ }).json<{ token: string }>();
327
+ expect(res.token).toBeDefined();
328
+ return { token: res.token };
329
+ })
330
+ .step("access protected", async ({ http, expect }, state) => {
331
+ const authed = http.extend({ headers: { Authorization: `Bearer ${state.token}` } });
332
+ const profile = await authed.get("https://api.example.com/me").json<{ id: string }>();
333
+ expect(profile.id).toBeDefined();
334
+ });
335
+ ```
336
+
337
+ ### Error / negative test
338
+
339
+ ```typescript
340
+ export const unauthorized = test(
341
+ { id: "unauthorized-access", tags: ["api", "auth"] },
342
+ async ({ http, expect }) => {
343
+ const res = await http.get("https://api.example.com/protected");
344
+ expect(res).toHaveStatus(401);
345
+ },
346
+ );
347
+ ```
348
+
349
+ ---
350
+
351
+ ## Rules
352
+
353
+ - **IDs**: kebab-case, unique across the project
354
+ - **Tags**: always set — `["smoke"]`, `["api"]`, `["e2e"]`, `["auth"]`
355
+ - **Secrets**: use `secrets.require("KEY")` or `{{KEY}}` in configure. Never hardcode.
356
+ - **Cleanup**: any test that creates data MUST have `.teardown()`
357
+ - **Data**: keep test data in `data/`, not inline in test files
358
+ - **Imports**: use `.ts` extensions for local files
359
+ - **One export per behavior**: each `export const` is one test case
360
+
361
+ ## Coverage expectations
362
+
363
+ For each endpoint, consider:
364
+
365
+ - Success path (200/201)
366
+ - Auth boundary (401/403) — missing or invalid credentials
367
+ - Validation boundary (400/422) — invalid input
368
+ - Not-found boundary (404) — nonexistent resource
369
+
370
+ Cover all applicable boundaries unless the user asks for a narrower scope. If the spec lacks detail for a negative case,
371
+ say so explicitly.
372
+
373
+ ## Anti-patterns to avoid
374
+
375
+ - Hardcoded secrets or base URLs
376
+ - Using raw `fetch()` instead of `ctx.http` or configured client
377
+ - No tags on tests
378
+ - Creating resources without teardown cleanup
379
+ - Guessing endpoint paths — always check the spec first
380
+ - Using `jsr:` URLs instead of `@glubean/sdk` alias
381
+ - Using `any` or `unknown` for HTTP response types — always provide a type parameter: `.json<{ id: string }>()`, not
382
+ `.json<any>()`. The SDK is fully typed; if you know the shape from the spec, type it.
@@ -0,0 +1,14 @@
1
+ {
2
+ "normal": {
3
+ "body": { "firstName": "Alice", "lastName": "Smith", "age": 28 },
4
+ "query": { "org": "acme" }
5
+ },
6
+ "young": {
7
+ "body": { "firstName": "Bob", "lastName": "Junior", "age": 18 },
8
+ "query": { "org": "acme" }
9
+ },
10
+ "edge-case": {
11
+ "body": { "firstName": "", "lastName": "", "age": -1 },
12
+ "query": { "org": "test" }
13
+ }
14
+ }
@@ -0,0 +1,5 @@
1
+ method,path,expected
2
+ GET,/products/1,200
3
+ GET,/products/99999,404
4
+ GET,/users/1,200
5
+ GET,/carts/1,200
@@ -0,0 +1,19 @@
1
+ # Scenarios for data-driven API tests
2
+ # Each entry describes one test case
3
+ - id: get-first-product
4
+ method: GET
5
+ path: /products/1
6
+ expected: 200
7
+ description: Fetch first product
8
+
9
+ - id: get-missing-product
10
+ method: GET
11
+ path: /products/99999
12
+ expected: 404
13
+ description: Non-existent product returns 404
14
+
15
+ - id: get-product-categories
16
+ method: GET
17
+ path: /products/categories
18
+ expected: 200
19
+ description: Product categories should be accessible
@@ -0,0 +1,14 @@
1
+ {
2
+ "by-name": {
3
+ "q": "phone",
4
+ "expected": { "minResults": 1, "titleContains": "phone" }
5
+ },
6
+ "by-category": {
7
+ "q": "laptop",
8
+ "expected": { "minResults": 1, "titleContains": "laptop" }
9
+ },
10
+ "no-results": {
11
+ "q": "zzzznotreal",
12
+ "expected": { "minResults": 0, "titleContains": "" }
13
+ }
14
+ }
@@ -0,0 +1,17 @@
1
+ [
2
+ {
3
+ "id": 1,
4
+ "expected": 200,
5
+ "role": "admin"
6
+ },
7
+ {
8
+ "id": 2,
9
+ "expected": 200,
10
+ "role": "user"
11
+ },
12
+ {
13
+ "id": 99999,
14
+ "expected": 404,
15
+ "role": "unknown"
16
+ }
17
+ ]