@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
+ /**
2
+ * Demo API tests — showcases Glubean SDK features against DummyJSON.
3
+ *
4
+ * This file is designed to produce rich, visually impressive results:
5
+ * - Multiple test types (simple + multi-step builder)
6
+ * - Auto-traced HTTP calls via ctx.http (method, URL, status, duration)
7
+ * - Fluent assertions, structured logs
8
+ * - Auth flows, data integrity checks, pagination
9
+ *
10
+ * Run: glubean run demo.test.ts --result-json
11
+ */
12
+ import { test } from "@glubean/sdk";
13
+
14
+ // ---------------------------------------------------------------------------
15
+ // 1. Simple test — List products
16
+ // ---------------------------------------------------------------------------
17
+
18
+ export const listProducts = test(
19
+ { id: "list-products", name: "List Products", tags: ["smoke"] },
20
+ async (ctx) => {
21
+ const baseUrl = ctx.vars.require("BASE_URL");
22
+
23
+ const data = await ctx.http.get(`${baseUrl}/products?limit=5`).json<{
24
+ products: unknown[];
25
+ total: number;
26
+ }>();
27
+
28
+ ctx.expect(data.products.length).toBe(5);
29
+ ctx.expect(data.total).toBeGreaterThan(0);
30
+
31
+ ctx.log(`Found ${data.total} products total`);
32
+ },
33
+ );
34
+
35
+ // ---------------------------------------------------------------------------
36
+ // 2. Simple test — Search products
37
+ // ---------------------------------------------------------------------------
38
+
39
+ export const searchProducts = test(
40
+ { id: "search-products", name: "Search Products", tags: ["smoke"] },
41
+ async (ctx) => {
42
+ const baseUrl = ctx.vars.require("BASE_URL");
43
+
44
+ const data = await ctx.http
45
+ .get(`${baseUrl}/products/search?q=phone`)
46
+ .json<{ products: { title: string }[] }>();
47
+
48
+ ctx.expect(data.products.length).toBeGreaterThan(0);
49
+
50
+ const names = data.products.map((p) => p.title);
51
+ ctx.log(`Found ${data.products.length} products matching 'phone'`, names);
52
+ },
53
+ );
54
+
55
+ // ---------------------------------------------------------------------------
56
+ // 3. Multi-step builder — Authentication flow
57
+ // ---------------------------------------------------------------------------
58
+
59
+ export const authFlow = test("auth-flow")
60
+ .meta({ name: "Authentication Flow", tags: ["auth"] })
61
+ .step("login", async (ctx) => {
62
+ const baseUrl = ctx.vars.require("BASE_URL");
63
+ const username = ctx.secrets.require("USERNAME");
64
+ const password = ctx.secrets.require("PASSWORD");
65
+
66
+ const data = await ctx.http
67
+ .post(`${baseUrl}/auth/login`, {
68
+ json: { username, password, expiresInMins: 1 },
69
+ })
70
+ .json<{
71
+ accessToken: string;
72
+ refreshToken: string;
73
+ username: string;
74
+ }>();
75
+
76
+ ctx.expect(data.accessToken).toBeDefined();
77
+ ctx.expect(data.username).toBe(username);
78
+
79
+ ctx.log(`Logged in as ${data.username}`);
80
+
81
+ return { token: data.accessToken, refreshToken: data.refreshToken };
82
+ })
83
+ .step("get profile", async (ctx, state) => {
84
+ const baseUrl = ctx.vars.require("BASE_URL");
85
+
86
+ const data = await ctx.http
87
+ .get(`${baseUrl}/auth/me`, {
88
+ headers: { Authorization: `Bearer ${state.token}` },
89
+ })
90
+ .json<{
91
+ email: string;
92
+ firstName: string;
93
+ lastName: string;
94
+ }>();
95
+
96
+ ctx.expect(data.email).toBeDefined();
97
+ ctx.expect(data.firstName).toBeDefined();
98
+
99
+ ctx.log(`Profile: ${data.firstName} ${data.lastName} (${data.email})`);
100
+
101
+ return state;
102
+ })
103
+ .step("refresh token", async (ctx, state) => {
104
+ const baseUrl = ctx.vars.require("BASE_URL");
105
+
106
+ const data = await ctx.http
107
+ .post(`${baseUrl}/auth/refresh`, {
108
+ json: { refreshToken: state.refreshToken, expiresInMins: 1 },
109
+ })
110
+ .json<{ accessToken: string }>();
111
+
112
+ ctx.expect(data.accessToken).toBeDefined();
113
+
114
+ ctx.log("Token refreshed successfully");
115
+ });
116
+
117
+ // ---------------------------------------------------------------------------
118
+ // 4. Simple test — Cart data integrity
119
+ // ---------------------------------------------------------------------------
120
+
121
+ export const cartIntegrity = test(
122
+ {
123
+ id: "cart-integrity",
124
+ name: "Cart Data Integrity",
125
+ tags: ["data-integrity"],
126
+ },
127
+ async (ctx) => {
128
+ const baseUrl = ctx.vars.require("BASE_URL");
129
+
130
+ const cart = await ctx.http.get(`${baseUrl}/carts/1`).json<{
131
+ products: { quantity: number; price: number }[];
132
+ total: number;
133
+ discountedTotal: number;
134
+ }>();
135
+
136
+ ctx.expect(cart.products.length).toBeGreaterThan(0);
137
+
138
+ // Verify each product has valid data
139
+ for (const p of cart.products.slice(0, 3)) {
140
+ ctx.expect(p.quantity).toBeGreaterThan(0);
141
+ ctx.expect(p.price).toBeGreaterThan(0);
142
+ }
143
+
144
+ // Verify discount math
145
+ ctx.assert(
146
+ cart.discountedTotal <= cart.total,
147
+ "Discounted total should be <= total",
148
+ { actual: cart.discountedTotal, expected: `<= ${cart.total}` },
149
+ );
150
+
151
+ ctx.log(`Cart has ${cart.products.length} items`);
152
+ ctx.log(`Total: $${cart.total}, After discount: $${cart.discountedTotal}`);
153
+ },
154
+ );
155
+
156
+ // ---------------------------------------------------------------------------
157
+ // 5. Simple test — Pagination consistency
158
+ // ---------------------------------------------------------------------------
159
+
160
+ export const paginationCheck = test(
161
+ { id: "pagination-check", name: "Pagination Consistency", tags: ["data"] },
162
+ async (ctx) => {
163
+ const baseUrl = ctx.vars.require("BASE_URL");
164
+
165
+ const d1 = await ctx.http
166
+ .get(`${baseUrl}/products?limit=10&skip=0`)
167
+ .json<{ products: unknown[]; total: number; skip: number }>();
168
+
169
+ const d2 = await ctx.http
170
+ .get(`${baseUrl}/products?limit=10&skip=10`)
171
+ .json<{ products: unknown[]; total: number; skip: number }>();
172
+
173
+ ctx.expect(d1.products.length).toBe(10);
174
+ ctx.expect(d2.skip).toBe(10);
175
+ ctx.assert(
176
+ d2.skip + d2.products.length <= d2.total,
177
+ "skip + length should be <= total",
178
+ { actual: d2.skip + d2.products.length, expected: `<= ${d2.total}` },
179
+ );
180
+
181
+ ctx.log(`Page 1: ${d1.products.length} items (skip=0)`);
182
+ ctx.log(`Page 2: ${d2.products.length} items (skip=10)`);
183
+ ctx.log(`Total: ${d2.total}, verified skip + length <= total`);
184
+ },
185
+ );
186
+
187
+ // ---------------------------------------------------------------------------
188
+ // 6. Multi-step builder — User-Todos cross-resource integrity
189
+ // ---------------------------------------------------------------------------
190
+
191
+ export const userTodosIntegrity = test("user-todos-integrity")
192
+ .meta({ name: "User Todos Cross-Resource", tags: ["data-integrity"] })
193
+ .step("fetch user", async (ctx) => {
194
+ const baseUrl = ctx.vars.require("BASE_URL");
195
+
196
+ const user = await ctx.http
197
+ .get(`${baseUrl}/users/1`)
198
+ .json<{ id: number; firstName: string; lastName: string }>();
199
+
200
+ ctx.expect(user.id).toBe(1);
201
+ ctx.log(`User: ${user.firstName} ${user.lastName} (id=${user.id})`);
202
+
203
+ return { userId: user.id };
204
+ })
205
+ .step("verify todos", async (ctx, state) => {
206
+ const baseUrl = ctx.vars.require("BASE_URL");
207
+
208
+ const data = await ctx.http
209
+ .get(`${baseUrl}/todos?limit=5&skip=0`)
210
+ .json<{ todos: { id: number; todo: string; userId: number }[] }>();
211
+
212
+ ctx.expect(data.todos.length).toBeGreaterThan(0);
213
+
214
+ // Check that we can find todos for this user
215
+ const userTodos = data.todos.filter((t) => t.userId === state.userId);
216
+
217
+ ctx.log(
218
+ `Found ${data.todos.length} todos, ${userTodos.length} belong to user ${state.userId}`,
219
+ );
220
+
221
+ // Each todo should have required fields
222
+ for (const todo of data.todos.slice(0, 3)) {
223
+ ctx.expect(todo.id).toBeDefined();
224
+ ctx.expect(todo.todo).toBeDefined();
225
+ }
226
+ });
@@ -0,0 +1,79 @@
1
+ {
2
+ "target": "/Users/peisong/glubean/oss/packages/cli/templates/explore-api.test.ts",
3
+ "files": [
4
+ "packages/cli/templates/explore-api.test.ts"
5
+ ],
6
+ "runAt": "2026-02-16 13:46:19",
7
+ "summary": {
8
+ "total": 1,
9
+ "passed": 0,
10
+ "failed": 1,
11
+ "skipped": 0,
12
+ "durationMs": 34,
13
+ "stats": {
14
+ "httpRequestTotal": 0,
15
+ "httpErrorTotal": 0,
16
+ "assertionTotal": 0,
17
+ "assertionFailed": 0,
18
+ "warningTotal": 0,
19
+ "warningTriggered": 0,
20
+ "stepTotal": 0,
21
+ "stepPassed": 0,
22
+ "stepFailed": 0
23
+ }
24
+ },
25
+ "tests": [
26
+ {
27
+ "testId": "quick-check",
28
+ "testName": "Quick Endpoint Check",
29
+ "tags": [
30
+ "explore"
31
+ ],
32
+ "success": false,
33
+ "durationMs": 34,
34
+ "events": [
35
+ {
36
+ "type": "log",
37
+ "message": "Loading test module: file:///Users/peisong/glubean/oss/packages/cli/templates/explore-api.test.ts"
38
+ },
39
+ {
40
+ "type": "start",
41
+ "id": "quick-check",
42
+ "name": "Quick Endpoint Check",
43
+ "tags": [
44
+ "explore"
45
+ ]
46
+ },
47
+ {
48
+ "type": "summary",
49
+ "data": {
50
+ "httpRequestTotal": 0,
51
+ "httpErrorTotal": 0,
52
+ "httpErrorRate": 0,
53
+ "assertionTotal": 0,
54
+ "assertionFailed": 0,
55
+ "warningTotal": 0,
56
+ "warningTriggered": 0,
57
+ "schemaValidationTotal": 0,
58
+ "schemaValidationFailed": 0,
59
+ "schemaValidationWarnings": 0,
60
+ "stepTotal": 0,
61
+ "stepPassed": 0,
62
+ "stepFailed": 0,
63
+ "stepSkipped": 0
64
+ }
65
+ },
66
+ {
67
+ "type": "status",
68
+ "status": "failed",
69
+ "error": "Missing required var: BASE_URL",
70
+ "stack": "Error: Missing required var: BASE_URL\n at Object.require (file:///Users/peisong/glubean/oss/packages/runner/harness.ts:360:15)\n at Object.fn (file:///Users/peisong/glubean/oss/packages/cli/templates/explore-api.test.ts:18:30)\n at executeNewTest (file:///Users/peisong/glubean/oss/packages/runner/harness.ts:1192:18)\n at file:///Users/peisong/glubean/oss/packages/runner/harness.ts:971:11"
71
+ },
72
+ {
73
+ "type": "error",
74
+ "message": "Process exited with code 1"
75
+ }
76
+ ]
77
+ }
78
+ ]
79
+ }
@@ -0,0 +1,42 @@
1
+ # Glubean — Playground
2
+
3
+ This is a quick playground for exploring APIs with [Glubean](https://glubean.com). Write TypeScript, run it, see every
4
+ request traced.
5
+
6
+ ```bash
7
+ deno task explore
8
+ ```
9
+
10
+ ## What's here
11
+
12
+ | Path | Purpose |
13
+ | --------------------------- | ----------------------------------------------- |
14
+ | `explore/api.test.ts` | GET and POST examples — edit and run |
15
+ | `explore/search.test.ts` | `test.pick` — one test, multiple variations |
16
+ | `explore/auth.test.ts` | Multi-step auth — login, use token, get profile |
17
+ | `data/search-examples.json` | Search parameters for pick examples |
18
+
19
+ Edit the files, change the URLs, hit play. That's it.
20
+
21
+ ---
22
+
23
+ ## Ready for real work?
24
+
25
+ This playground is for trying things out. When you're ready to build a real test suite — with AI writing and running
26
+ tests for you — create a full project:
27
+
28
+ ```bash
29
+ mkdir my-api-tests && cd my-api-tests
30
+ glubean init
31
+ ```
32
+
33
+ Choose **Best Practice** to unlock:
34
+
35
+ - **AI closed-loop** — your AI reads your API spec, writes tests, runs them via MCP, reads failures, fixes, and reruns
36
+ until green. You review the result, not the process.
37
+ - **OpenAPI-driven** — drop your spec in `context/`, and the AI knows every endpoint, method, and schema. No guessing.
38
+ - **Multi-environment** — same tests against dev, staging, and production. Switch with one flag.
39
+ - **Data-driven tests** — generate dozens of cases from JSON, CSV, or YAML with `test.each`.
40
+ - **CI + Cloud** — Git hooks, GitHub Actions, scheduled runs, Slack alerts when something breaks.
41
+
42
+ The difference: here you write tests manually. There, the AI writes them and proves they work — before you even look.
@@ -0,0 +1,42 @@
1
+ /**
2
+ * Quick API exploration — GET and POST examples ready to run.
3
+ *
4
+ * Edit the URLs, change the payload, hit play. Every request is
5
+ * auto-traced so you can inspect headers, timing, and response
6
+ * bodies in the trace viewer.
7
+ *
8
+ * Run: deno task explore
9
+ */
10
+ import { test } from "@glubean/sdk";
11
+
12
+ export const getProduct = test(
13
+ { id: "get-product", name: "GET Product", tags: ["explore"] },
14
+ async (ctx) => {
15
+ const baseUrl = ctx.vars.require("BASE_URL");
16
+
17
+ const res = await ctx.http.get(`${baseUrl}/products/1`);
18
+ const data = await res.json();
19
+
20
+ ctx.expect(res.status).toBe(200);
21
+ ctx.expect(data.title).toBeDefined();
22
+
23
+ ctx.log("Product", data);
24
+ },
25
+ );
26
+
27
+ export const createProduct = test(
28
+ { id: "create-product", name: "POST Create Product", tags: ["explore"] },
29
+ async (ctx) => {
30
+ const baseUrl = ctx.vars.require("BASE_URL");
31
+
32
+ const res = await ctx.http.post(`${baseUrl}/products/add`, {
33
+ json: { title: "Test Product", price: 9.99, category: "test" },
34
+ });
35
+ const data = await res.json();
36
+
37
+ ctx.expect(res.status).toBe(200);
38
+ ctx.expect(data.id).toBeDefined();
39
+
40
+ ctx.log("Created", data);
41
+ },
42
+ );
@@ -0,0 +1,45 @@
1
+ /**
2
+ * Multi-step auth flow — login, use token, get profile.
3
+ *
4
+ * This demonstrates the builder API: each step passes state to the next.
5
+ * The trace viewer shows all three requests as a connected flow, not
6
+ * isolated calls. That's the difference between Glubean and a REST client.
7
+ *
8
+ * Run: deno task explore
9
+ */
10
+ import { test } from "@glubean/sdk";
11
+
12
+ export const authFlow = test("auth-flow")
13
+ .meta({ name: "Auth Flow", tags: ["explore", "auth"] })
14
+ .step("login", async (ctx) => {
15
+ const baseUrl = ctx.vars.require("BASE_URL");
16
+ const username = ctx.secrets.require("USERNAME");
17
+ const password = ctx.secrets.require("PASSWORD");
18
+
19
+ const data = await ctx.http
20
+ .post(`${baseUrl}/auth/login`, {
21
+ json: { username, password, expiresInMins: 1 },
22
+ })
23
+ .json<{ accessToken: string; refreshToken: string; username: string }>();
24
+
25
+ ctx.expect(data.accessToken).toBeDefined().orFail();
26
+ ctx.expect(data.username).toBe(username);
27
+
28
+ ctx.log(`Logged in as ${data.username}`);
29
+
30
+ return { token: data.accessToken };
31
+ })
32
+ .step("get profile", async (ctx, state) => {
33
+ const baseUrl = ctx.vars.require("BASE_URL");
34
+
35
+ const data = await ctx.http
36
+ .get(`${baseUrl}/auth/me`, {
37
+ headers: { Authorization: `Bearer ${state.token}` },
38
+ })
39
+ .json<{ email: string; firstName: string; lastName: string }>();
40
+
41
+ ctx.expect(data.email).toBeDefined();
42
+ ctx.expect(data.firstName).toBeDefined();
43
+
44
+ ctx.log(`Profile: ${data.firstName} ${data.lastName} (${data.email})`);
45
+ });
@@ -0,0 +1,34 @@
1
+ /**
2
+ * Parameterized search — run different query variations with test.pick.
3
+ *
4
+ * Each example in search-examples.json defines a search term and expected
5
+ * outcome. In VS Code, CodeLens buttons appear above the test so you can
6
+ * click a specific example to run.
7
+ *
8
+ * Run all: deno task explore
9
+ * Pick one: glubean run explore/search.test.ts --pick by-name
10
+ * Pick another: glubean run explore/search.test.ts --pick no-results
11
+ */
12
+ import { test } from "@glubean/sdk";
13
+ import examples from "../data/search-examples.json" with { type: "json" };
14
+
15
+ export const searchProducts = test.pick(examples)(
16
+ "search-$_pick",
17
+ async ({ http, vars, expect, log }, { q, expected }) => {
18
+ const baseUrl = vars.require("BASE_URL");
19
+
20
+ const res = await http
21
+ .get(`${baseUrl}/products/search`, { searchParams: { q } })
22
+ .json<{ products: { title: string }[]; total: number }>();
23
+
24
+ expect(res.total).toBeGreaterThan(expected.minResults - 1);
25
+
26
+ if (expected.titleContains && res.products.length > 0) {
27
+ expect(res.products[0].title.toLowerCase()).toContain(
28
+ expected.titleContains,
29
+ );
30
+ }
31
+
32
+ log(`"${q}" → ${res.total} results`);
33
+ },
34
+ );
@@ -0,0 +1,97 @@
1
+ {
2
+ "openapi": "3.0.0",
3
+ "info": {
4
+ "title": "Glubean Sample API (mock)",
5
+ "version": "0.0.0",
6
+ "description": "A mock OpenAPI spec shipped with `glubean init` for AI-assisted test generation. The real sample API service is not ready yet."
7
+ },
8
+ "servers": [
9
+ {
10
+ "url": "https://sample-api.glubean.com",
11
+ "description": "Mock URL"
12
+ }
13
+ ],
14
+ "components": {
15
+ "securitySchemes": {
16
+ "ApiKeyAuth": {
17
+ "type": "http",
18
+ "scheme": "bearer",
19
+ "bearerFormat": "API key"
20
+ }
21
+ },
22
+ "schemas": {
23
+ "ErrorResponse": {
24
+ "type": "object",
25
+ "required": [
26
+ "error"
27
+ ],
28
+ "properties": {
29
+ "error": {
30
+ "type": "object",
31
+ "required": [
32
+ "code",
33
+ "message"
34
+ ],
35
+ "properties": {
36
+ "code": {
37
+ "type": "string",
38
+ "example": "VALIDATION_ERROR"
39
+ },
40
+ "message": {
41
+ "type": "string",
42
+ "example": "Invalid payload"
43
+ },
44
+ "fields": {
45
+ "type": "array",
46
+ "items": {
47
+ "type": "object",
48
+ "required": [
49
+ "path",
50
+ "reason"
51
+ ],
52
+ "properties": {
53
+ "path": {
54
+ "type": "string",
55
+ "example": "email"
56
+ },
57
+ "reason": {
58
+ "type": "string",
59
+ "example": "must be a valid email"
60
+ }
61
+ }
62
+ }
63
+ }
64
+ }
65
+ }
66
+ }
67
+ }
68
+ }
69
+ },
70
+ "security": [
71
+ {
72
+ "ApiKeyAuth": []
73
+ }
74
+ ],
75
+ "paths": {
76
+ "/health": {
77
+ "get": {
78
+ "operationId": "healthCheck",
79
+ "tags": [
80
+ "System"
81
+ ],
82
+ "responses": {
83
+ "200": {
84
+ "description": "Service health",
85
+ "content": {
86
+ "application/json": {
87
+ "schema": {
88
+ "type": "object"
89
+ }
90
+ }
91
+ }
92
+ }
93
+ }
94
+ }
95
+ }
96
+ }
97
+ }