@taiga-ai/mcp-server 0.25.0-rc.474 → 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.
- package/README.md +111 -0
- package/dist/index.js +292 -115
- package/package.json +15 -2
- package/scripts/build.mjs +0 -37
- package/scripts/prepare-publish.cjs +0 -49
- package/src/client.test.ts +0 -424
- package/src/client.ts +0 -977
- package/src/config.test.ts +0 -24
- package/src/config.ts +0 -28
- package/src/index.ts +0 -45
- package/src/modern.integration.test.ts +0 -153
- package/src/server.integration.test.ts +0 -9214
- package/src/server.ts +0 -18
- package/src/tools/annotations.ts +0 -38
- package/src/tools/autonomy.ts +0 -86
- package/src/tools/backlogs.ts +0 -802
- package/src/tools/builds.ts +0 -668
- package/src/tools/catalogue.test.ts +0 -170
- package/src/tools/catalogue.ts +0 -223
- package/src/tools/context.ts +0 -513
- package/src/tools/documents.ts +0 -375
- package/src/tools/errors.ts +0 -172
- package/src/tools/hints.ts +0 -99
- package/src/tools/inbox.ts +0 -546
- package/src/tools/index.ts +0 -239
- package/src/tools/inputs.ts +0 -47
- package/src/tools/instructions.ts +0 -41
- package/src/tools/jobs.ts +0 -90
- package/src/tools/knowledge.ts +0 -249
- package/src/tools/maintenance.ts +0 -547
- package/src/tools/markdown.test.ts +0 -249
- package/src/tools/markdown.ts +0 -151
- package/src/tools/observe.ts +0 -311
- package/src/tools/product-documents.ts +0 -184
- package/src/tools/products.ts +0 -529
- package/src/tools/prompts.ts +0 -265
- package/src/tools/queue.ts +0 -250
- package/src/tools/readiness.ts +0 -177
- package/src/tools/remote.test.ts +0 -44
- package/src/tools/remote.ts +0 -107
- package/src/tools/repositories.ts +0 -360
- package/src/tools/request-prompt.test.ts +0 -32
- package/src/tools/request-prompt.ts +0 -28
- package/src/tools/resources.ts +0 -217
- package/src/tools/sections.test.ts +0 -143
- package/src/tools/sections.ts +0 -287
- package/src/tools/setup.ts +0 -455
- package/src/tools/summarize.test.ts +0 -20
- package/src/tools/summarize.ts +0 -96
- package/src/tools/wait.ts +0 -490
- package/src/version.test.ts +0 -10
- package/src/version.ts +0 -14
- package/taiga-ai-mcp-server-0.25.0-rc.474.tgz +0 -0
- package/tsconfig.json +0 -21
- package/vitest.config.ts +0 -9
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
|
-
}
|
package/src/client.test.ts
DELETED
|
@@ -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
|
-
});
|