@taiga-ai/mcp-server 0.25.0-rc.472 → 0.26.0-dev.479

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 (55) hide show
  1. package/README.md +111 -0
  2. package/dist/index.js +322 -122
  3. package/package.json +15 -2
  4. package/scripts/build.mjs +0 -37
  5. package/scripts/prepare-publish.cjs +0 -49
  6. package/src/client.test.ts +0 -424
  7. package/src/client.ts +0 -976
  8. package/src/config.test.ts +0 -24
  9. package/src/config.ts +0 -28
  10. package/src/index.ts +0 -45
  11. package/src/modern.integration.test.ts +0 -153
  12. package/src/server.integration.test.ts +0 -9075
  13. package/src/server.ts +0 -18
  14. package/src/tools/annotations.ts +0 -38
  15. package/src/tools/autonomy.ts +0 -86
  16. package/src/tools/backlogs.ts +0 -766
  17. package/src/tools/builds.ts +0 -668
  18. package/src/tools/catalogue.test.ts +0 -170
  19. package/src/tools/catalogue.ts +0 -223
  20. package/src/tools/context.ts +0 -513
  21. package/src/tools/documents.ts +0 -375
  22. package/src/tools/errors.ts +0 -172
  23. package/src/tools/hints.ts +0 -99
  24. package/src/tools/inbox.ts +0 -546
  25. package/src/tools/index.ts +0 -239
  26. package/src/tools/inputs.ts +0 -47
  27. package/src/tools/instructions.ts +0 -41
  28. package/src/tools/jobs.ts +0 -90
  29. package/src/tools/knowledge.ts +0 -249
  30. package/src/tools/maintenance.ts +0 -540
  31. package/src/tools/markdown.test.ts +0 -249
  32. package/src/tools/markdown.ts +0 -151
  33. package/src/tools/observe.ts +0 -311
  34. package/src/tools/product-documents.ts +0 -184
  35. package/src/tools/products.ts +0 -529
  36. package/src/tools/prompts.ts +0 -265
  37. package/src/tools/queue.ts +0 -250
  38. package/src/tools/readiness.ts +0 -177
  39. package/src/tools/remote.test.ts +0 -44
  40. package/src/tools/remote.ts +0 -107
  41. package/src/tools/repositories.ts +0 -360
  42. package/src/tools/request-prompt.test.ts +0 -32
  43. package/src/tools/request-prompt.ts +0 -28
  44. package/src/tools/resources.ts +0 -217
  45. package/src/tools/sections.test.ts +0 -143
  46. package/src/tools/sections.ts +0 -287
  47. package/src/tools/setup.ts +0 -455
  48. package/src/tools/summarize.test.ts +0 -20
  49. package/src/tools/summarize.ts +0 -96
  50. package/src/tools/wait.ts +0 -490
  51. package/src/version.test.ts +0 -10
  52. package/src/version.ts +0 -14
  53. package/taiga-ai-mcp-server-0.25.0-rc.472.tgz +0 -0
  54. package/tsconfig.json +0 -21
  55. package/vitest.config.ts +0 -9
package/package.json CHANGED
@@ -1,7 +1,8 @@
1
1
  {
2
2
  "name": "@taiga-ai/mcp-server",
3
- "version": "0.25.0-rc.472",
4
- "description": "MCP server for Taiga -- gives AI assistants access to documents and products",
3
+ "version": "0.26.0-dev.479",
4
+ "description": "Taiga for AI coding assistants: read products, policies, initiatives and runs, and operate the line, over MCP",
5
+ "mcpName": "ga.tai/mcp-server",
5
6
  "type": "module",
6
7
  "main": "./dist/index.js",
7
8
  "bin": {
@@ -16,6 +17,18 @@
16
17
  "engines": {
17
18
  "node": ">=20"
18
19
  },
20
+ "files": [
21
+ "dist",
22
+ "README.md"
23
+ ],
24
+ "keywords": [
25
+ "mcp",
26
+ "model-context-protocol",
27
+ "taiga",
28
+ "ai",
29
+ "claude"
30
+ ],
31
+ "homepage": "https://docs.tai.ga",
19
32
  "scripts": {
20
33
  "build": "tsc --noEmit && node scripts/build.mjs",
21
34
  "typecheck": "tsc --noEmit",
package/scripts/build.mjs DELETED
@@ -1,37 +0,0 @@
1
- import { readFileSync, writeFileSync } from 'node:fs';
2
- import { rolldown } from 'rolldown';
3
-
4
- const OUTFILE = 'dist/index.js';
5
- const SHEBANG = '#!/usr/bin/env node';
6
-
7
- /**
8
- * Bundle the MCP server into a single self-contained ESM file.
9
- *
10
- * The published npm artifact must not reference private workspace packages, so
11
- * `@taiga/*` (notably `@taiga/feature-flags`, a runtime value) is inlined. The
12
- * genuinely-published deps — the MCP server SDK and zod — stay external and are
13
- * installed normally by consumers. Node built-ins are external via platform.
14
- */
15
- const external = [/^@modelcontextprotocol\/server(\/|$)/, /^zod(\/|$)/, /^node:/];
16
-
17
- const bundle = await rolldown({
18
- input: 'src/index.ts',
19
- platform: 'node',
20
- external,
21
- });
22
-
23
- await bundle.write({
24
- file: OUTFILE,
25
- format: 'esm',
26
- });
27
-
28
- await bundle.close();
29
-
30
- // Guarantee the bin shebang survived bundling. Rolldown preserves the entry's
31
- // shebang today, but bundlers commonly strip them — without a `#!` line the
32
- // published bin fails to exec under `npx` even when chmod +x. This makes the
33
- // contract explicit and robust to future bundler changes (no-op if present).
34
- const code = readFileSync(OUTFILE, 'utf8');
35
- if (!code.startsWith('#!')) {
36
- writeFileSync(OUTFILE, `${SHEBANG}\n${code}`);
37
- }
@@ -1,49 +0,0 @@
1
- 'use strict';
2
-
3
- /**
4
- * Prepare package.json for npm publish.
5
- *
6
- * - Removes `private: true` so npm allows publishing
7
- * - Sets publishConfig to public access
8
- * - Strips workspace-only dependencies. `@taiga/types` and
9
- * `@taiga/feature-flags` carry runtime code, but the rolldown build bundles
10
- * both into dist/index.js, so neither is a real runtime dep. The `zod` that
11
- * `@taiga/types` imports stays external: it is a declared dependency.
12
- * - Drops the `types` entry points — the published artifact is a bundled CLI
13
- * and ships no `.d.ts` (the build runs `tsc --noEmit`).
14
- * - Marks the bin entry executable. The bundler emits dist/index.js with mode
15
- * 0644, so without this `npx -y @taiga-ai/mcp-server` fails with EACCES
16
- * because the kernel cannot exec a non-executable file despite the shebang.
17
- */
18
-
19
- const fs = require('fs');
20
- const path = require('path');
21
-
22
- const pkgPath = path.join(__dirname, '..', 'package.json');
23
- const binPath = path.join(__dirname, '..', 'dist', 'index.js');
24
-
25
- try {
26
- const raw = fs.readFileSync(pkgPath, 'utf8');
27
- const pkg = JSON.parse(raw);
28
-
29
- delete pkg.private;
30
- pkg.publishConfig = { access: 'public' };
31
-
32
- // Workspace deps are inlined by the bundle — drop them from the published deps.
33
- delete pkg.dependencies?.['@taiga/types'];
34
- delete pkg.dependencies?.['@taiga/feature-flags'];
35
-
36
- // No declarations are shipped for the bundled CLI.
37
- delete pkg.types;
38
- delete pkg.exports?.['.']?.types;
39
-
40
- fs.writeFileSync(pkgPath, JSON.stringify(pkg, null, 2) + '\n', 'utf8');
41
- console.log('prepare-publish: package.json updated');
42
-
43
- fs.chmodSync(binPath, 0o755);
44
- console.log('prepare-publish: dist/index.js marked executable');
45
- } catch (err) {
46
- console.error('prepare-publish: failed');
47
- console.error(err.stack || err);
48
- process.exit(1);
49
- }
@@ -1,424 +0,0 @@
1
- import { describe, it, expect, vi, beforeEach } from 'vitest';
2
- import { readFileSync } from 'node:fs';
3
- import { TaigaApiError, TaigaClient, withoutTenant } from './client.js';
4
-
5
- // Read independently of src/version.ts, so the test checks the real file.
6
- const pkg = JSON.parse(readFileSync(new URL('../package.json', import.meta.url), 'utf8')) as { version: string };
7
-
8
- // ---------------------------------------------------------------------------
9
- // Helpers -- thin wrappers to reduce boilerplate, not to obscure intent
10
- // ---------------------------------------------------------------------------
11
-
12
- function stubFetch(status: number, body: unknown) {
13
- const spy = vi.fn().mockResolvedValue({
14
- ok: status >= 200 && status < 300,
15
- status,
16
- json: () => Promise.resolve(body),
17
- text: () => Promise.resolve(JSON.stringify(body)),
18
- });
19
- vi.stubGlobal('fetch', spy);
20
- return spy;
21
- }
22
-
23
- function makeClient(opts?: { baseUrl?: string; apiKey?: string }) {
24
- return new TaigaClient({
25
- baseUrl: opts?.baseUrl ?? 'https://api.test.com',
26
- apiKey: opts?.apiKey ?? 'taiga_sk_live_testkey123',
27
- });
28
- }
29
-
30
- async function clientWithCapabilities(caps: string[]) {
31
- stubFetch(200, { data: { capabilities: caps } });
32
- const client = makeClient();
33
- await client.introspect();
34
- return client;
35
- }
36
-
37
- // ---------------------------------------------------------------------------
38
- // Tests
39
- // ---------------------------------------------------------------------------
40
-
41
- describe('TaigaClient', () => {
42
- beforeEach(() => {
43
- vi.restoreAllMocks();
44
- });
45
-
46
- // -------------------------------------------------------------------------
47
- // Authentication -- the client must always identify itself correctly
48
- // -------------------------------------------------------------------------
49
-
50
- describe('authentication', () => {
51
- it('should authenticate every request with the provided API key', async () => {
52
- // Arrange
53
- const spy = stubFetch(200, { data: [] });
54
- const client = makeClient({ apiKey: 'taiga_sk_live_secret42' });
55
-
56
- // Act
57
- await client.listProducts();
58
-
59
- // Assert
60
- const headers = spy.mock.calls[0]![1].headers;
61
- expect(headers.Authorization).toBe('Bearer taiga_sk_live_secret42');
62
- });
63
-
64
- it('should identify itself as taiga-ai-mcp-server in every request', async () => {
65
- const spy = stubFetch(200, { data: [] });
66
-
67
- await makeClient().listProducts();
68
-
69
- expect(spy.mock.calls[0]![1].headers['User-Agent']).toBe(`taiga-ai-mcp-server/${pkg.version}`);
70
- });
71
- });
72
-
73
- // -------------------------------------------------------------------------
74
- // Error handling -- failures must surface clearly, never silently swallow
75
- // -------------------------------------------------------------------------
76
-
77
- describe('error handling', () => {
78
- it('should reject with status code and body when API returns an error', async () => {
79
- stubFetch(403, { error: 'Forbidden' });
80
- const client = makeClient();
81
-
82
- await expect(client.listProducts()).rejects.toThrow(/403.*Forbidden/);
83
- });
84
-
85
- it('should reject with status code even when response body is unreadable', async () => {
86
- vi.stubGlobal(
87
- 'fetch',
88
- vi.fn().mockResolvedValue({
89
- ok: false,
90
- status: 500,
91
- text: () => Promise.reject(new Error('stream error')),
92
- })
93
- );
94
-
95
- await expect(makeClient().listProducts()).rejects.toThrow(/500/);
96
- });
97
-
98
- it('should reject when fetch itself throws (network failure)', async () => {
99
- vi.stubGlobal('fetch', vi.fn().mockRejectedValue(new Error('ECONNREFUSED')));
100
-
101
- await expect(makeClient().listProducts()).rejects.toThrow(/ECONNREFUSED/);
102
- });
103
- });
104
-
105
- // -------------------------------------------------------------------------
106
- // URL construction -- trailing slashes, optional params, nested paths
107
- // -------------------------------------------------------------------------
108
-
109
- describe('URL construction', () => {
110
- it('should normalize trailing slashes in base URL', async () => {
111
- const spy = stubFetch(200, { data: [] });
112
- const client = makeClient({ baseUrl: 'https://api.test.com///' });
113
-
114
- await client.listProducts();
115
-
116
- expect(spy.mock.calls[0]![0]).toBe('https://api.test.com/v1/products');
117
- });
118
-
119
- it('should omit query string when no optional params are provided', async () => {
120
- const spy = stubFetch(200, { data: [], nextCursor: null });
121
-
122
- await makeClient().listProducts();
123
-
124
- expect(spy.mock.calls[0]![0]).toBe('https://api.test.com/v1/products');
125
- });
126
-
127
- it('should include all optional filters as query params', async () => {
128
- const spy = stubFetch(200, { data: [], nextCursor: null });
129
-
130
- await makeClient().listProducts({ factoryId: 't1', status: 'active', limit: 10 });
131
-
132
- const url = spy.mock.calls[0]![0] as string;
133
- expect(url).toContain('factoryId=t1');
134
- expect(url).toContain('status=active');
135
- expect(url).toContain('limit=10');
136
- });
137
-
138
- it('should nest backlog items under the product path', async () => {
139
- const spy = stubFetch(200, { data: [], nextCursor: null });
140
-
141
- await makeClient().listBacklogItems('proj-1', { limit: 20 });
142
-
143
- const url = spy.mock.calls[0]![0] as string;
144
- expect(url).toContain('/v1/products/proj-1/backlog');
145
- expect(url).toContain('limit=20');
146
- });
147
- });
148
-
149
- // -------------------------------------------------------------------------
150
- // Introspection -- the foundation for capability-gated tool registration
151
- // -------------------------------------------------------------------------
152
-
153
- describe('withoutTenant', () => {
154
- it('drops tenantId at every depth and keeps an own __proto__ key as data', () => {
155
- const parsed = JSON.parse(
156
- '{"__proto__": {"x": 1}, "tenantId": "t", "a": {"tenantId": "t", "b": [{"tenantId": "t", "c": 1}, 2, null]}, "d": "s"}'
157
- ) as { tenantId: string; a: unknown; d: string };
158
-
159
- const out = withoutTenant(parsed);
160
-
161
- expect(JSON.stringify(out)).not.toContain('tenantId');
162
- expect(out.a).toEqual({ b: [{ c: 1 }, 2, null] });
163
- expect(out.d).toBe('s');
164
- expect(Object.prototype.hasOwnProperty.call(out, '__proto__')).toBe(true);
165
- expect(Object.getPrototypeOf(out)).toBe(Object.prototype);
166
- expect((out as { x?: unknown }).x).toBeUndefined();
167
- });
168
-
169
- it('walks a body nested deeper than a recursive copy could', () => {
170
- const depth = 20_000;
171
- const nested = JSON.parse(`${'{"a":['.repeat(depth)}{"tenantId":"t","v":1}${']}'.repeat(depth)}`) as unknown;
172
-
173
- const out = withoutTenant(nested);
174
-
175
- // Walked, not stringified: JSON.stringify recurses and would overflow where the copy did not.
176
- let cursor: unknown = out;
177
- for (let level = 0; level < depth; level += 1) {
178
- expect(Object.keys(cursor as object)).toEqual(['a']);
179
- cursor = (cursor as { a: unknown[] }).a[0];
180
- }
181
- expect(cursor).toEqual({ v: 1 });
182
- });
183
-
184
- it('keeps key order, which is the order sections render in', () => {
185
- const out = withoutTenant({ z: 1, tenantId: 't', a: { y: 2, x: [3, { tenantId: 't', w: 4 }] }, m: 5 });
186
-
187
- expect(Object.keys(out)).toEqual(['z', 'a', 'm']);
188
- expect(Object.keys(out.a)).toEqual(['y', 'x']);
189
- expect(out.a.x).toEqual([3, { w: 4 }]);
190
- });
191
- });
192
-
193
- describe('introspection', () => {
194
- it('should return and store the introspection result', async () => {
195
- const expected = {
196
- userId: 'u1',
197
- tenantId: 't1',
198
- email: 'test@example.com',
199
- role: 'member',
200
- capabilities: ['products:read', 'factories:read'],
201
- };
202
- stubFetch(200, { data: expected });
203
- const client = makeClient();
204
-
205
- // Act
206
- const result = await client.introspect();
207
-
208
- // Assert -- both return value and stored state
209
- expect(result).toEqual(expected);
210
- expect(client.getIntrospection()).toEqual(expected);
211
- });
212
-
213
- it('should report no capabilities before introspect is called', () => {
214
- const client = makeClient();
215
-
216
- expect(client.hasCapability('products:read')).toBe(false);
217
- });
218
-
219
- it('should correctly report granted vs missing capabilities', async () => {
220
- const client = await clientWithCapabilities(['products:read', 'factories:read']);
221
-
222
- expect(client.hasCapability('products:read')).toBe(true);
223
- expect(client.hasCapability('factories:read')).toBe(true);
224
- expect(client.hasCapability('backlog:read')).toBe(false);
225
- expect(client.hasCapability('tenant:documents:write')).toBe(false);
226
- });
227
-
228
- it('should report no capabilities when key has empty scope', async () => {
229
- const client = await clientWithCapabilities([]);
230
-
231
- expect(client.hasCapability('products:read')).toBe(false);
232
- });
233
- });
234
-
235
- // -------------------------------------------------------------------------
236
- // API methods -- each method hits the right endpoint with the right verb
237
- // These are the HTTP contracts the Taiga API expects.
238
- // -------------------------------------------------------------------------
239
-
240
- describe('API contracts', () => {
241
- it('should GET a single product by id', async () => {
242
- const spy = stubFetch(200, { data: { id: 'p1', name: 'Alpha' } });
243
-
244
- const result = await makeClient().getProduct('p1');
245
-
246
- expect(spy.mock.calls[0]![0]).toBe('https://api.test.com/v1/products/p1');
247
- expect(result.data).toEqual({ id: 'p1', name: 'Alpha' });
248
- });
249
-
250
- it('should GET a single document by type from tenant path', async () => {
251
- const spy = stubFetch(200, { data: { id: 'd1' } });
252
-
253
- await makeClient().getTenantDocument('security_policy');
254
-
255
- expect(spy.mock.calls[0]![0]).toBe('https://api.test.com/v1/tenants/current/documents/security_policy');
256
- });
257
-
258
- it('should PUT a document draft with the provided body', async () => {
259
- const spy = stubFetch(200, { data: { id: 'd1' } });
260
-
261
- await makeClient().saveTenantDocumentDraft('security_policy', { data: { title: 'New' } });
262
-
263
- expect(spy.mock.calls[0]![1].method).toBe('PUT');
264
- expect(spy.mock.calls[0]![0]).toContain('/tenants/current/documents/security_policy/draft');
265
- expect(JSON.parse(spy.mock.calls[0]![1].body)).toEqual({
266
- data: { title: 'New' },
267
- });
268
- expect(spy.mock.calls[0]![1].headers['Content-Type']).toBe('application/json');
269
- });
270
-
271
- it('should keep every path segment inside its route', async () => {
272
- const spy = stubFetch(200, { data: {} });
273
- const client = makeClient();
274
-
275
- await client.getTenantDocument('../products/x');
276
- await client.getFactoryDocument('f/1', 'a b');
277
- await client.getProduct('p/1');
278
- await client.getBacklogItem('b/1');
279
- await client.listBacklogItems('e/1');
280
- await client.getProductDocument('p/1', 'spec/x');
281
-
282
- const paths = spy.mock.calls.map((c) => new URL(c[0] as string).pathname);
283
- expect(paths).toEqual([
284
- '/v1/tenants/current/documents/..%2Fproducts%2Fx',
285
- '/v1/factories/f%2F1/documents/a%20b',
286
- '/v1/products/p%2F1',
287
- '/v1/backlog/b%2F1',
288
- '/v1/products/e%2F1/backlog',
289
- '/v1/products/p%2F1/documents/spec%2Fx',
290
- ]);
291
- });
292
-
293
- it('should refuse the dot segments encodeURIComponent cannot neutralise', async () => {
294
- // `/documents/../publish` would collapse to `/tenants/current/publish`.
295
- const spy = stubFetch(200, { data: {} });
296
- const client = makeClient();
297
-
298
- await expect(client.publishTenantDocument('..')).rejects.toThrow(/Invalid path segment/);
299
- await expect(client.getTenantDocumentDraft('.')).rejects.toThrow(/Invalid path segment/);
300
- await expect(client.getTenantDocument('')).rejects.toThrow(/Invalid path segment/);
301
- expect(spy).not.toHaveBeenCalled();
302
- });
303
-
304
- it('should POST a publish with no body and no JSON content type', async () => {
305
- // Fastify answers 400 "Body cannot be empty" to a JSON content type
306
- // without a body, which is what the publish routes would otherwise get.
307
- const spy = stubFetch(200, { data: { id: 'd1', version: 2 } });
308
-
309
- await makeClient().publishTenantDocument('policy_security');
310
-
311
- expect(spy.mock.calls[0]![1].method).toBe('POST');
312
- expect(spy.mock.calls[0]![0]).toContain('/tenants/current/documents/policy_security/publish');
313
- expect(spy.mock.calls[0]![1].body).toBeNull();
314
- expect(spy.mock.calls[0]![1].headers).not.toHaveProperty('Content-Type');
315
- });
316
-
317
- it('should expose the API status, code and message on a failed request', async () => {
318
- stubFetch(400, { error: 'Bad Request', code: 'VALIDATION_ERROR', message: 'Draft contains template residue' });
319
-
320
- const error = await makeClient()
321
- .publishTenantDocument('policy_security')
322
- .catch((e: unknown) => e as TaigaApiError);
323
-
324
- expect(error).toBeInstanceOf(TaigaApiError);
325
- expect(error).toMatchObject({
326
- status: 400,
327
- code: 'VALIDATION_ERROR',
328
- apiMessage: 'Draft contains template residue',
329
- });
330
- });
331
-
332
- it('should send the backlog filters the route reads, with includeArchived as the literal true', async () => {
333
- const spy = stubFetch(200, { data: [], nextCursor: null });
334
-
335
- await makeClient().listBacklogItems('p1', { status: 'done', planningStatus: 'planned', includeArchived: true });
336
-
337
- const url = new URL(spy.mock.calls[0]![0] as string);
338
- expect(url.pathname).toBe('/v1/products/p1/backlog');
339
- expect(url.searchParams.get('status')).toBe('done');
340
- expect(url.searchParams.get('planningStatus')).toBe('planned');
341
- expect(url.searchParams.get('includeArchived')).toBe('true');
342
- });
343
-
344
- it('should leave includeArchived out when false, since the route treats any value but true as false', async () => {
345
- const spy = stubFetch(200, { data: [], nextCursor: null });
346
-
347
- await makeClient().listBacklogItems('p1', { includeArchived: false });
348
-
349
- expect(spy.mock.calls[0]![0]).toBe('https://api.test.com/v1/products/p1/backlog');
350
- });
351
-
352
- it('should GET runs under the product and a run with its steps by id', async () => {
353
- const spy = stubFetch(200, { data: [] });
354
- const client = makeClient();
355
-
356
- await client.listBuildRuns('p1', { backlogItemId: 'i1', limit: 5 });
357
- await client.getBuildRun('b/1');
358
- await client.listBuildRunSteps('b/1');
359
-
360
- const paths = spy.mock.calls.map((c) => {
361
- const url = new URL(c[0] as string);
362
- return url.pathname + url.search;
363
- });
364
- expect(paths).toEqual([
365
- '/v1/products/p1/builds?backlogItemId=i1&limit=5',
366
- '/v1/builds/b%2F1',
367
- '/v1/builds/b%2F1/steps',
368
- ]);
369
- });
370
-
371
- it('should send the repository filter and host when resolving a product', async () => {
372
- const spy = stubFetch(200, { data: [], nextCursor: null });
373
-
374
- await makeClient().listProducts({ repository: 'acme/web', repositoryHost: 'https://github.com' });
375
-
376
- const url = new URL(spy.mock.calls[0]![0] as string);
377
- expect(url.searchParams.get('repository')).toBe('acme/web');
378
- expect(url.searchParams.get('repositoryHost')).toBe('https://github.com');
379
- });
380
-
381
- it('should read a product document from the asked timeline and list one timeline at a time', async () => {
382
- const spy = stubFetch(200, { data: [] });
383
- const client = makeClient();
384
-
385
- await client.getProductDocument('p1', 'architecture', 'reality');
386
- await client.getProductDocument('p1', 'architecture');
387
- await client.listProductDocuments('p1', { origin: 'reality', limit: 100 });
388
-
389
- const paths = spy.mock.calls.map((c) => {
390
- const url = new URL(c[0] as string);
391
- return url.pathname + url.search;
392
- });
393
- expect(paths).toEqual([
394
- '/v1/products/p1/documents/architecture?origin=reality',
395
- '/v1/products/p1/documents/architecture',
396
- '/v1/products/p1/documents?origin=reality&limit=100',
397
- ]);
398
- });
399
-
400
- it('should GET a single backlog item by id (flat path)', async () => {
401
- const spy = stubFetch(200, { data: { id: 'item-1' } });
402
-
403
- await makeClient().getBacklogItem('item-1');
404
-
405
- expect(spy.mock.calls[0]![0]).toBe('https://api.test.com/v1/backlog/item-1');
406
- });
407
-
408
- it('should GET factories with optional limit', async () => {
409
- const spy = stubFetch(200, { data: [], nextCursor: null });
410
-
411
- await makeClient().listFactories({ limit: 50 });
412
-
413
- expect(spy.mock.calls[0]![0]).toContain('/v1/factories?limit=50');
414
- });
415
-
416
- it('should GET documents from tenant path with optional limit', async () => {
417
- const spy = stubFetch(200, { data: [], nextCursor: null });
418
-
419
- await makeClient().listTenantDocuments({ limit: 5 });
420
-
421
- expect(spy.mock.calls[0]![0]).toContain('/v1/tenants/current/documents?limit=5');
422
- });
423
- });
424
- });