@taiga-ai/mcp-server 0.18.0-rc.445 → 0.19.0-dev.451
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/dist/index.js +1128 -650
- package/package.json +1 -1
- package/src/client.test.ts +2 -2
- package/src/client.ts +201 -145
- package/src/modern.integration.test.ts +1 -1
- package/src/server.integration.test.ts +782 -243
- package/src/tools/autonomy.ts +3 -5
- package/src/tools/backlogs.ts +182 -72
- package/src/tools/builds.ts +142 -53
- package/src/tools/catalogue.test.ts +28 -22
- package/src/tools/context.ts +126 -56
- package/src/tools/documents.ts +12 -5
- package/src/tools/errors.ts +40 -2
- package/src/tools/hints.ts +37 -9
- package/src/tools/inbox.ts +149 -28
- package/src/tools/index.ts +15 -1
- package/src/tools/instructions.ts +32 -154
- package/src/tools/jobs.ts +6 -5
- package/src/tools/knowledge.ts +4 -6
- package/src/tools/maintenance.ts +53 -32
- package/src/tools/markdown.ts +13 -5
- package/src/tools/observe.ts +32 -14
- package/src/tools/product-documents.ts +4 -6
- package/src/tools/products.ts +32 -27
- package/src/tools/prompts.ts +14 -20
- package/src/tools/queue.ts +47 -26
- package/src/tools/readiness.ts +172 -0
- package/src/tools/repositories.ts +11 -9
- package/src/tools/resources.ts +16 -19
- package/src/tools/sections.test.ts +3 -3
- package/src/tools/sections.ts +57 -16
- package/src/tools/setup.ts +47 -51
- package/src/tools/summarize.test.ts +20 -0
- package/src/tools/summarize.ts +17 -13
- package/src/tools/wait.ts +39 -26
- package/taiga-ai-mcp-server-0.19.0-dev.451.tgz +0 -0
- package/tsconfig.json +1 -5
- package/taiga-ai-mcp-server-0.18.0-rc.445.tgz +0 -0
|
@@ -4,6 +4,10 @@ import { serveStdio } from '@modelcontextprotocol/server/stdio';
|
|
|
4
4
|
import { TaigaClient } from './client.js';
|
|
5
5
|
import { createServer } from './server.js';
|
|
6
6
|
import type { WaitOptions } from './tools/wait.js';
|
|
7
|
+
import { inProductWords } from './tools/errors.js';
|
|
8
|
+
|
|
9
|
+
/** What a tool returns for an API value: its keys in the product's words (PRO-2800). */
|
|
10
|
+
const asSeen = <T>(value: T): T => inProductWords(value) as T;
|
|
7
11
|
|
|
8
12
|
// ---------------------------------------------------------------------------
|
|
9
13
|
// Helpers -- thin wrappers, not abstraction layers
|
|
@@ -42,15 +46,15 @@ const ALL_TOOL_NAMES = [
|
|
|
42
46
|
'taiga_fix_finding',
|
|
43
47
|
'taiga_fix_policy_violation',
|
|
44
48
|
'taiga_generate_product_documents',
|
|
45
|
-
'taiga_get_backlog_item',
|
|
46
|
-
'taiga_get_build',
|
|
47
49
|
'taiga_get_delivery_metrics',
|
|
48
50
|
'taiga_get_deployments',
|
|
49
51
|
'taiga_get_document',
|
|
50
52
|
'taiga_get_factory_document',
|
|
51
53
|
'taiga_get_finding',
|
|
54
|
+
'taiga_get_initiative',
|
|
52
55
|
'taiga_get_job',
|
|
53
56
|
'taiga_get_knowledge',
|
|
57
|
+
'taiga_get_line',
|
|
54
58
|
'taiga_get_maintenance',
|
|
55
59
|
'taiga_get_monitoring',
|
|
56
60
|
'taiga_get_plan',
|
|
@@ -58,26 +62,26 @@ const ALL_TOOL_NAMES = [
|
|
|
58
62
|
'taiga_get_product',
|
|
59
63
|
'taiga_get_product_context',
|
|
60
64
|
'taiga_get_product_document',
|
|
61
|
-
'
|
|
65
|
+
'taiga_get_run',
|
|
62
66
|
'taiga_ignore_finding',
|
|
63
67
|
'taiga_link_repository',
|
|
64
68
|
'taiga_list_action_required',
|
|
65
|
-
'taiga_list_backlog_items',
|
|
66
|
-
'taiga_list_builds',
|
|
67
69
|
'taiga_list_documents',
|
|
68
70
|
'taiga_list_environments',
|
|
69
71
|
'taiga_list_factories',
|
|
70
72
|
'taiga_list_factory_documents',
|
|
71
73
|
'taiga_list_findings',
|
|
74
|
+
'taiga_list_initiatives',
|
|
72
75
|
'taiga_list_knowledge',
|
|
73
76
|
'taiga_list_linkable_repositories',
|
|
74
77
|
'taiga_list_product_documents',
|
|
75
78
|
'taiga_list_product_repositories',
|
|
76
79
|
'taiga_list_products',
|
|
77
80
|
'taiga_list_repository_branches',
|
|
81
|
+
'taiga_list_runs',
|
|
78
82
|
'taiga_list_setup_tasks',
|
|
79
83
|
'taiga_mark_product_document_reviewed',
|
|
80
|
-
'
|
|
84
|
+
'taiga_move_initiative',
|
|
81
85
|
'taiga_move_product',
|
|
82
86
|
'taiga_pause_line',
|
|
83
87
|
'taiga_plan_again',
|
|
@@ -85,7 +89,7 @@ const ALL_TOOL_NAMES = [
|
|
|
85
89
|
'taiga_publish_document',
|
|
86
90
|
'taiga_publish_factory_document',
|
|
87
91
|
'taiga_publish_product_document',
|
|
88
|
-
'
|
|
92
|
+
'taiga_queue_initiative',
|
|
89
93
|
'taiga_refresh_reality_documents',
|
|
90
94
|
'taiga_regenerate_product_document',
|
|
91
95
|
'taiga_reject_plan',
|
|
@@ -94,24 +98,24 @@ const ALL_TOOL_NAMES = [
|
|
|
94
98
|
'taiga_request_initiatives',
|
|
95
99
|
'taiga_resolve_action',
|
|
96
100
|
'taiga_resolve_product',
|
|
97
|
-
'
|
|
101
|
+
'taiga_resume_initiative',
|
|
98
102
|
'taiga_retry_knowledge',
|
|
99
103
|
'taiga_set_auto_build',
|
|
100
104
|
'taiga_set_auto_merge',
|
|
101
105
|
'taiga_set_product_autonomy',
|
|
102
106
|
'taiga_set_repository_branch',
|
|
103
107
|
'taiga_start_import',
|
|
104
|
-
'taiga_stop_build',
|
|
105
108
|
'taiga_stop_generating_documents',
|
|
109
|
+
'taiga_stop_run',
|
|
106
110
|
'taiga_unlink_repository',
|
|
107
111
|
'taiga_update_document',
|
|
108
112
|
'taiga_update_environment',
|
|
109
113
|
'taiga_update_factory_document',
|
|
110
114
|
'taiga_update_product',
|
|
111
115
|
'taiga_update_product_document',
|
|
112
|
-
'
|
|
113
|
-
'taiga_wait_for_build',
|
|
116
|
+
'taiga_wait_for_initiative',
|
|
114
117
|
'taiga_wait_for_job',
|
|
118
|
+
'taiga_wait_for_run',
|
|
115
119
|
];
|
|
116
120
|
|
|
117
121
|
const PRODUCT_ID = '0199a1b2-c3d4-7e5f-8a9b-0c1d2e3f4a5c';
|
|
@@ -267,7 +271,7 @@ function toolNames(tools: { name: string }[]) {
|
|
|
267
271
|
}
|
|
268
272
|
|
|
269
273
|
/** Extract parsed JSON from the first text content block of a tool call result. */
|
|
270
|
-
function parseToolResult(result:
|
|
274
|
+
function parseToolResult(result: { content?: unknown }) {
|
|
271
275
|
const content = result.content as { type: string; text: string }[];
|
|
272
276
|
return JSON.parse(content[0]!.text);
|
|
273
277
|
}
|
|
@@ -434,10 +438,10 @@ describe('MCP server integration', () => {
|
|
|
434
438
|
'taiga_resolve_product',
|
|
435
439
|
'taiga_list_product_documents',
|
|
436
440
|
'taiga_get_product_document',
|
|
437
|
-
'
|
|
438
|
-
'
|
|
439
|
-
'
|
|
440
|
-
'
|
|
441
|
+
'taiga_list_runs',
|
|
442
|
+
'taiga_get_run',
|
|
443
|
+
'taiga_get_line',
|
|
444
|
+
'taiga_wait_for_run',
|
|
441
445
|
'taiga_get_job',
|
|
442
446
|
'taiga_wait_for_job',
|
|
443
447
|
'taiga_get_maintenance',
|
|
@@ -461,12 +465,7 @@ describe('MCP server integration', () => {
|
|
|
461
465
|
'taiga_move_product',
|
|
462
466
|
],
|
|
463
467
|
'vcs:manage': ['taiga_link_repository', 'taiga_set_repository_branch', 'taiga_unlink_repository'],
|
|
464
|
-
'backlog:read': [
|
|
465
|
-
'taiga_list_backlog_items',
|
|
466
|
-
'taiga_get_backlog_item',
|
|
467
|
-
'taiga_get_plan',
|
|
468
|
-
'taiga_wait_for_backlog_item',
|
|
469
|
-
],
|
|
468
|
+
'backlog:read': ['taiga_list_initiatives', 'taiga_get_initiative', 'taiga_get_plan', 'taiga_wait_for_initiative'],
|
|
470
469
|
'factories:read': ['taiga_list_factories', 'taiga_list_factory_documents', 'taiga_get_factory_document'],
|
|
471
470
|
'tenant:documents:read': ['taiga_list_documents', 'taiga_get_document'],
|
|
472
471
|
'customDocuments:read': ['taiga_list_knowledge', 'taiga_get_knowledge'],
|
|
@@ -474,10 +473,10 @@ describe('MCP server integration', () => {
|
|
|
474
473
|
const PAIRED_CONTRACT: Record<string, string[]> = {
|
|
475
474
|
'tenant:documents:read tenant:documents:write': ['taiga_update_document', 'taiga_publish_document'],
|
|
476
475
|
'factories:read factories:documents:write': ['taiga_update_factory_document', 'taiga_publish_factory_document'],
|
|
477
|
-
'backlog:write': ['
|
|
478
|
-
'backlog:write products:write': ['
|
|
476
|
+
'backlog:write': ['taiga_move_initiative', 'taiga_set_auto_merge'],
|
|
477
|
+
'backlog:write products:write': ['taiga_queue_initiative'],
|
|
479
478
|
'products:write': [
|
|
480
|
-
'
|
|
479
|
+
'taiga_resume_initiative',
|
|
481
480
|
'taiga_set_auto_build',
|
|
482
481
|
'taiga_request_initiatives',
|
|
483
482
|
'taiga_set_product_autonomy',
|
|
@@ -495,7 +494,7 @@ describe('MCP server integration', () => {
|
|
|
495
494
|
'backlog:read backlog:write products:write': ['taiga_plan_again'],
|
|
496
495
|
'products:read products:write': [
|
|
497
496
|
'taiga_start_import',
|
|
498
|
-
'
|
|
497
|
+
'taiga_stop_run',
|
|
499
498
|
'taiga_pause_line',
|
|
500
499
|
'taiga_ignore_finding',
|
|
501
500
|
'taiga_fix_finding',
|
|
@@ -580,7 +579,7 @@ describe('MCP server integration', () => {
|
|
|
580
579
|
it('should replace one section of the existing draft and keep the rest', async () => {
|
|
581
580
|
const draft = policyDocument({ id: 'draft-1', version: null, isDraft: true });
|
|
582
581
|
const saved = policyDocument({ id: 'draft-1', version: null, isDraft: true });
|
|
583
|
-
|
|
582
|
+
saved.data.sections.summary.content = 'New summary';
|
|
584
583
|
|
|
585
584
|
const { client, fetchSpy } = await bootMcpPair({
|
|
586
585
|
capabilities: ALL_CAPABILITIES,
|
|
@@ -750,9 +749,9 @@ describe('MCP server integration', () => {
|
|
|
750
749
|
const { tools } = await client.listTools();
|
|
751
750
|
const byName = Object.fromEntries(tools.map((t) => [t.name, t.annotations]));
|
|
752
751
|
|
|
753
|
-
expect(byName
|
|
754
|
-
expect(byName
|
|
755
|
-
expect(byName
|
|
752
|
+
expect(byName['taiga_publish_document']).toMatchObject({ destructiveHint: true, idempotentHint: false });
|
|
753
|
+
expect(byName['taiga_publish_factory_document']).toMatchObject({ destructiveHint: true, idempotentHint: false });
|
|
754
|
+
expect(byName['taiga_update_document']).toMatchObject({ destructiveHint: false, idempotentHint: true });
|
|
756
755
|
});
|
|
757
756
|
|
|
758
757
|
it('should hand the model the API message when the publish gate refuses the draft', async () => {
|
|
@@ -1136,7 +1135,7 @@ describe('MCP server integration', () => {
|
|
|
1136
1135
|
});
|
|
1137
1136
|
|
|
1138
1137
|
const result = await client.callTool({
|
|
1139
|
-
name: '
|
|
1138
|
+
name: 'taiga_list_initiatives',
|
|
1140
1139
|
arguments: { productId: PRODUCT_ID, status: 'in_progress', category: 'feature', includeArchived: true },
|
|
1141
1140
|
});
|
|
1142
1141
|
|
|
@@ -1156,7 +1155,7 @@ describe('MCP server integration', () => {
|
|
|
1156
1155
|
routes: { [`/v1/backlog/${ITEM_ID}`]: { data: item } },
|
|
1157
1156
|
});
|
|
1158
1157
|
|
|
1159
|
-
const result = await client.callTool({ name: '
|
|
1158
|
+
const result = await client.callTool({ name: 'taiga_get_initiative', arguments: { initiativeId: ITEM_ID } });
|
|
1160
1159
|
|
|
1161
1160
|
expect(parseToolResult(result)).toEqual(item);
|
|
1162
1161
|
});
|
|
@@ -1168,8 +1167,8 @@ describe('MCP server integration', () => {
|
|
|
1168
1167
|
});
|
|
1169
1168
|
|
|
1170
1169
|
const result = await client.callTool({
|
|
1171
|
-
name: '
|
|
1172
|
-
arguments: { productId: PRODUCT_ID,
|
|
1170
|
+
name: 'taiga_list_runs',
|
|
1171
|
+
arguments: { productId: PRODUCT_ID, initiativeId: ITEM_ID },
|
|
1173
1172
|
});
|
|
1174
1173
|
|
|
1175
1174
|
const [row] = parseToolResult(result).data;
|
|
@@ -1195,10 +1194,18 @@ describe('MCP server integration', () => {
|
|
|
1195
1194
|
},
|
|
1196
1195
|
});
|
|
1197
1196
|
|
|
1198
|
-
const result = await client.callTool({ name: '
|
|
1197
|
+
const result = await client.callTool({ name: 'taiga_get_run', arguments: { runId: BUILD_ID, full: true } });
|
|
1199
1198
|
|
|
1200
1199
|
expect(result.isError).toBeFalsy();
|
|
1201
|
-
|
|
1200
|
+
const { backlogItemId: _item, ...rest } = run;
|
|
1201
|
+
expect(parseToolResult(result)).toEqual({
|
|
1202
|
+
...rest,
|
|
1203
|
+
initiativeId: ITEM_ID,
|
|
1204
|
+
// Completed with no merge recorded: an older run that settled on an open pull request.
|
|
1205
|
+
disposition: 'unmerged',
|
|
1206
|
+
outcome: 'Settled without merging: its pull request was left unmerged.',
|
|
1207
|
+
steps: [step],
|
|
1208
|
+
});
|
|
1202
1209
|
});
|
|
1203
1210
|
|
|
1204
1211
|
it('should drop tenantId from every response at the client, so a run, its steps, an item and a list row never carry it', async () => {
|
|
@@ -1214,14 +1221,22 @@ describe('MCP server integration', () => {
|
|
|
1214
1221
|
});
|
|
1215
1222
|
|
|
1216
1223
|
const build = parseToolResult(
|
|
1217
|
-
await client.callTool({ name: '
|
|
1224
|
+
await client.callTool({ name: 'taiga_get_run', arguments: { runId: BUILD_ID, full: true } })
|
|
1218
1225
|
);
|
|
1219
|
-
|
|
1226
|
+
const { backlogItemId: _item, ...rest } = run;
|
|
1227
|
+
expect(build).toEqual({
|
|
1228
|
+
...rest,
|
|
1229
|
+
initiativeId: ITEM_ID,
|
|
1230
|
+
disposition: 'unmerged',
|
|
1231
|
+
outcome: 'Settled without merging: its pull request was left unmerged.',
|
|
1232
|
+
steps: [{ ...step, verification: { ok: true } }],
|
|
1233
|
+
});
|
|
1220
1234
|
expect(JSON.stringify(build)).not.toContain('tenantId');
|
|
1221
1235
|
|
|
1222
1236
|
const got = parseToolResult(
|
|
1223
|
-
await client.callTool({ name: '
|
|
1237
|
+
await client.callTool({ name: 'taiga_get_initiative', arguments: { initiativeId: ITEM_ID } })
|
|
1224
1238
|
);
|
|
1239
|
+
// No lifecycle on this item, so no situation.
|
|
1225
1240
|
expect(got).toEqual(item);
|
|
1226
1241
|
});
|
|
1227
1242
|
|
|
@@ -1230,8 +1245,8 @@ describe('MCP server integration', () => {
|
|
|
1230
1245
|
const before = fetchSpy.mock.calls.length;
|
|
1231
1246
|
|
|
1232
1247
|
for (const [name, args] of [
|
|
1233
|
-
['
|
|
1234
|
-
['
|
|
1248
|
+
['taiga_list_initiatives', { productId: PRODUCT_ID, limit: 2.5 }],
|
|
1249
|
+
['taiga_list_runs', { productId: PRODUCT_ID, limit: 2.5 }],
|
|
1235
1250
|
['taiga_list_products', { limit: 2.5 }],
|
|
1236
1251
|
] as const) {
|
|
1237
1252
|
const result = await client.callTool({ name, arguments: { ...args } });
|
|
@@ -1245,8 +1260,8 @@ describe('MCP server integration', () => {
|
|
|
1245
1260
|
|
|
1246
1261
|
const names = toolNames((await client.listTools()).tools);
|
|
1247
1262
|
|
|
1248
|
-
expect(names).toContain('
|
|
1249
|
-
expect(names).not.toContain('
|
|
1263
|
+
expect(names).toContain('taiga_list_runs');
|
|
1264
|
+
expect(names).not.toContain('taiga_list_initiatives');
|
|
1250
1265
|
});
|
|
1251
1266
|
});
|
|
1252
1267
|
|
|
@@ -1949,6 +1964,34 @@ describe('MCP server integration', () => {
|
|
|
1949
1964
|
},
|
|
1950
1965
|
[`GET /v1/products/${PRODUCT_ID}/repositories`]: { data: [repo], nextCursor: null },
|
|
1951
1966
|
[`GET /v1/products/${PRODUCT_ID}/builds?limit=10`]: { data: [run], nextCursor: null },
|
|
1967
|
+
[`GET /v1/products/${PRODUCT_ID}/inbox`]: {
|
|
1968
|
+
data: [
|
|
1969
|
+
{ id: 'i1', kind: 'review_pr', status: 'open' },
|
|
1970
|
+
{ id: 'i2', kind: 'review_pr', status: 'open' },
|
|
1971
|
+
{ id: 'i3', kind: 'setup_secret', status: 'snoozed', snoozedUntil: '2999-01-01T00:00:00.000Z' },
|
|
1972
|
+
],
|
|
1973
|
+
nextCursor: null,
|
|
1974
|
+
totalCount: 3,
|
|
1975
|
+
},
|
|
1976
|
+
[`GET /v1/products/${PRODUCT_ID}/maintenance/summary`]: {
|
|
1977
|
+
data: {
|
|
1978
|
+
openFindings: 1,
|
|
1979
|
+
severityCounts: { critical: 0, high: 1 },
|
|
1980
|
+
lastRun: { id: 'sweep-1', status: 'completed', completedAt: '2026-10-01T00:00:00.000Z', findingsCount: 1 },
|
|
1981
|
+
},
|
|
1982
|
+
},
|
|
1983
|
+
[`GET /v1/products/${PRODUCT_ID}/maintenance/policies`]: {
|
|
1984
|
+
// The API's own shape: the lists, no summary.
|
|
1985
|
+
data: {
|
|
1986
|
+
controls: [
|
|
1987
|
+
{ controlId: 'SEC-1', divergenceCount: 2, findingCount: 0 },
|
|
1988
|
+
{ controlId: 'SEC-2', divergenceCount: 0, findingCount: 1 },
|
|
1989
|
+
{ controlId: 'SEC-3', divergenceCount: 0, findingCount: 0 },
|
|
1990
|
+
],
|
|
1991
|
+
divergences: [{ severity: 'critical' }, { severity: 'high' }, { severity: 'high' }],
|
|
1992
|
+
resolvedDivergences: [],
|
|
1993
|
+
},
|
|
1994
|
+
},
|
|
1952
1995
|
};
|
|
1953
1996
|
|
|
1954
1997
|
it('should assemble every part in one call and name the one document that is not published', async () => {
|
|
@@ -2003,7 +2046,24 @@ describe('MCP server integration', () => {
|
|
|
2003
2046
|
],
|
|
2004
2047
|
truncated: false,
|
|
2005
2048
|
});
|
|
2006
|
-
expect(bundle.builds).toEqual([
|
|
2049
|
+
expect(bundle.builds).toEqual([
|
|
2050
|
+
{
|
|
2051
|
+
id: BUILD_ID,
|
|
2052
|
+
runNumber: 1,
|
|
2053
|
+
status: 'completed',
|
|
2054
|
+
targetTitle: 'Add login',
|
|
2055
|
+
disposition: 'unmerged',
|
|
2056
|
+
outcome: 'Settled without a merged pull request.',
|
|
2057
|
+
},
|
|
2058
|
+
]);
|
|
2059
|
+
// PRO-2800: "how does it stand" in the same call, as counts.
|
|
2060
|
+
expect(bundle.attention).toEqual({ open: 3, byKind: { review_pr: 2, setup_secret: 1 }, snoozed: 1 });
|
|
2061
|
+
expect(bundle.maintenance).toEqual({
|
|
2062
|
+
openFindings: 1,
|
|
2063
|
+
bySeverity: { critical: 0, high: 1 },
|
|
2064
|
+
lastSweep: { status: 'completed', completedAt: '2026-10-01T00:00:00.000Z' },
|
|
2065
|
+
});
|
|
2066
|
+
expect(bundle.standing).toEqual({ openViolations: 3, bySeverity: { critical: 1, high: 2 }, controlsViolated: 2 });
|
|
2007
2067
|
expect(bundle.notGranted).toEqual([]);
|
|
2008
2068
|
expect(bundle.unavailable).toEqual({});
|
|
2009
2069
|
});
|
|
@@ -2328,8 +2388,8 @@ describe('MCP server integration', () => {
|
|
|
2328
2388
|
`/v1/factories/${FACTORY_ID}/documents`,
|
|
2329
2389
|
['factories:read'],
|
|
2330
2390
|
],
|
|
2331
|
-
['
|
|
2332
|
-
['
|
|
2391
|
+
['taiga_list_initiatives', { productId: PRODUCT_ID }, `/v1/products/${PRODUCT_ID}/backlog`, ['backlog:read']],
|
|
2392
|
+
['taiga_list_runs', { productId: PRODUCT_ID }, `/v1/products/${PRODUCT_ID}/builds`, ['products:read']],
|
|
2333
2393
|
] as const)('%s passes the cursor on and returns where the next page starts', async (name, args, path, caps) => {
|
|
2334
2394
|
const { client, fetchSpy } = await bootMcpPair({
|
|
2335
2395
|
capabilities: [...caps],
|
|
@@ -2385,9 +2445,9 @@ describe('MCP server integration', () => {
|
|
|
2385
2445
|
mimeType: 'text/markdown',
|
|
2386
2446
|
});
|
|
2387
2447
|
expect(resourceTemplates.map((t) => t.uriTemplate).sort()).toEqual([
|
|
2388
|
-
'taiga://backlog/{itemId}',
|
|
2389
2448
|
'taiga://documents/{type}',
|
|
2390
2449
|
'taiga://factories/{factoryId}/instructions',
|
|
2450
|
+
'taiga://initiatives/{initiativeId}',
|
|
2391
2451
|
'taiga://products/{productId}/documents/{type}',
|
|
2392
2452
|
'taiga://products/{productId}/documents/{type}{?origin}',
|
|
2393
2453
|
]);
|
|
@@ -2398,7 +2458,7 @@ describe('MCP server integration', () => {
|
|
|
2398
2458
|
|
|
2399
2459
|
const { resourceTemplates } = await client.listResourceTemplates();
|
|
2400
2460
|
|
|
2401
|
-
expect(resourceTemplates.map((t) => t.uriTemplate)).toEqual(['taiga://
|
|
2461
|
+
expect(resourceTemplates.map((t) => t.uriTemplate)).toEqual(['taiga://initiatives/{initiativeId}']);
|
|
2402
2462
|
});
|
|
2403
2463
|
|
|
2404
2464
|
it('should read a policy as Markdown with its sections as headings and no provenance', async () => {
|
|
@@ -2463,7 +2523,7 @@ describe('MCP server integration', () => {
|
|
|
2463
2523
|
});
|
|
2464
2524
|
|
|
2465
2525
|
const instructions = await client.readResource({ uri: `taiga://factories/${FACTORY_ID}/instructions` });
|
|
2466
|
-
const backlog = await client.readResource({ uri: `taiga://
|
|
2526
|
+
const backlog = await client.readResource({ uri: `taiga://initiatives/${ITEM_ID}` });
|
|
2467
2527
|
|
|
2468
2528
|
expect(textOf(instructions.contents)).toContain('## instructions\n\nUse pnpm.');
|
|
2469
2529
|
expect(backlog.contents[0]!.mimeType).toBe('application/json');
|
|
@@ -2475,7 +2535,7 @@ describe('MCP server integration', () => {
|
|
|
2475
2535
|
const before = fetchSpy.mock.calls.length;
|
|
2476
2536
|
|
|
2477
2537
|
await expect(client.readResource({ uri: 'taiga://documents/policy_ip' })).rejects.toThrow(/not one of/);
|
|
2478
|
-
await expect(client.readResource({ uri: 'taiga://
|
|
2538
|
+
await expect(client.readResource({ uri: 'taiga://initiatives/not-an-id' })).rejects.toThrow(/not an ID/);
|
|
2479
2539
|
await expect(client.readResource({ uri: 'taiga://nothing/here' })).rejects.toThrow(/not found/);
|
|
2480
2540
|
await expect(
|
|
2481
2541
|
client.readResource({ uri: `taiga://products/${PRODUCT_ID}/documents/architecture?origin=` })
|
|
@@ -2601,7 +2661,7 @@ describe('MCP server integration', () => {
|
|
|
2601
2661
|
const docsOnly = await bootMcpPair({ capabilities: ['tenant:documents:read'] });
|
|
2602
2662
|
|
|
2603
2663
|
expect((await full.client.listPrompts()).prompts.map((p) => p.name).sort()).toEqual([
|
|
2604
|
-
'
|
|
2664
|
+
'taiga_implement_initiative',
|
|
2605
2665
|
'taiga_review_against_policies',
|
|
2606
2666
|
'taiga_start_session',
|
|
2607
2667
|
]);
|
|
@@ -2643,7 +2703,10 @@ describe('MCP server integration', () => {
|
|
|
2643
2703
|
routes: { ...routes, [`GET /v1/products/${PRODUCT_ID}`]: { data: named } },
|
|
2644
2704
|
});
|
|
2645
2705
|
|
|
2646
|
-
const result = await client.getPrompt({
|
|
2706
|
+
const result = await client.getPrompt({
|
|
2707
|
+
name: 'taiga_implement_initiative',
|
|
2708
|
+
arguments: { initiativeId: ITEM_ID },
|
|
2709
|
+
});
|
|
2647
2710
|
|
|
2648
2711
|
const text = (result.messages[0]!.content as { text: string }).text;
|
|
2649
2712
|
expect(text.split('## Initiative')[0]).not.toContain('(none apply)');
|
|
@@ -2720,25 +2783,28 @@ describe('MCP server integration', () => {
|
|
|
2720
2783
|
const before = fetchSpy.mock.calls.length;
|
|
2721
2784
|
|
|
2722
2785
|
const notAnId = await client
|
|
2723
|
-
.getPrompt({ name: '
|
|
2786
|
+
.getPrompt({ name: 'taiga_implement_initiative', arguments: { initiativeId: 'PRO-123' } })
|
|
2724
2787
|
.catch((e: unknown) => e);
|
|
2725
2788
|
expect(notAnId).toMatchObject({ code: -32602 });
|
|
2726
2789
|
expect(fetchSpy.mock.calls.length).toBe(before);
|
|
2727
2790
|
|
|
2728
2791
|
const unseen = await client
|
|
2729
2792
|
.getPrompt({
|
|
2730
|
-
name: '
|
|
2731
|
-
arguments: {
|
|
2793
|
+
name: 'taiga_implement_initiative',
|
|
2794
|
+
arguments: { initiativeId: '0199a1b2-c3d4-7e5f-8a9b-0c1d2e3f4a99' },
|
|
2732
2795
|
})
|
|
2733
2796
|
.catch((e: unknown) => e);
|
|
2734
2797
|
expect(unseen).toMatchObject({ code: -32602 });
|
|
2735
|
-
expect(String((unseen as Error).message)).toContain('
|
|
2798
|
+
expect(String((unseen as Error).message)).toContain('taiga_list_initiatives shows what exists');
|
|
2736
2799
|
});
|
|
2737
2800
|
|
|
2738
2801
|
it('should assemble an initiative with its criteria, the architecture, the instructions and the policy sections', async () => {
|
|
2739
2802
|
const { client } = await bootMcpPair({ capabilities: ALL_CAPABILITIES, routes });
|
|
2740
2803
|
|
|
2741
|
-
const result = await client.getPrompt({
|
|
2804
|
+
const result = await client.getPrompt({
|
|
2805
|
+
name: 'taiga_implement_initiative',
|
|
2806
|
+
arguments: { initiativeId: ITEM_ID },
|
|
2807
|
+
});
|
|
2742
2808
|
|
|
2743
2809
|
const text = (result.messages[0]!.content as { text: string }).text;
|
|
2744
2810
|
expect(text).toContain('Users can log in');
|
|
@@ -2750,7 +2816,10 @@ describe('MCP server integration', () => {
|
|
|
2750
2816
|
it('should say which parts a narrower key could not read rather than drop them', async () => {
|
|
2751
2817
|
const { client } = await bootMcpPair({ capabilities: ['products:read', 'backlog:read'], routes });
|
|
2752
2818
|
|
|
2753
|
-
const result = await client.getPrompt({
|
|
2819
|
+
const result = await client.getPrompt({
|
|
2820
|
+
name: 'taiga_implement_initiative',
|
|
2821
|
+
arguments: { initiativeId: ITEM_ID },
|
|
2822
|
+
});
|
|
2754
2823
|
|
|
2755
2824
|
const text = (result.messages[0]!.content as { text: string }).text;
|
|
2756
2825
|
expect(text).toContain('(The factory instructions need factories:read, which this key lacks.)');
|
|
@@ -2766,7 +2835,10 @@ describe('MCP server integration', () => {
|
|
|
2766
2835
|
return { ok: false, status: 403, json: async () => body, text: async () => JSON.stringify(body) };
|
|
2767
2836
|
});
|
|
2768
2837
|
|
|
2769
|
-
const result = await client.getPrompt({
|
|
2838
|
+
const result = await client.getPrompt({
|
|
2839
|
+
name: 'taiga_implement_initiative',
|
|
2840
|
+
arguments: { initiativeId: ITEM_ID },
|
|
2841
|
+
});
|
|
2770
2842
|
|
|
2771
2843
|
const text = (result.messages[0]!.content as { text: string }).text;
|
|
2772
2844
|
expect(text).toContain(
|
|
@@ -2878,12 +2950,12 @@ describe('MCP server integration', () => {
|
|
|
2878
2950
|
const byName = Object.fromEntries(tools.map((t) => [t.name, t.annotations]));
|
|
2879
2951
|
|
|
2880
2952
|
for (const name of [
|
|
2881
|
-
'
|
|
2882
|
-
'
|
|
2883
|
-
'
|
|
2953
|
+
'taiga_queue_initiative',
|
|
2954
|
+
'taiga_move_initiative',
|
|
2955
|
+
'taiga_resume_initiative',
|
|
2884
2956
|
'taiga_approve_plan',
|
|
2885
2957
|
'taiga_reject_plan',
|
|
2886
|
-
'
|
|
2958
|
+
'taiga_stop_run',
|
|
2887
2959
|
'taiga_request_initiatives',
|
|
2888
2960
|
'taiga_plan_again',
|
|
2889
2961
|
'taiga_define_environment',
|
|
@@ -2904,8 +2976,9 @@ describe('MCP server integration', () => {
|
|
|
2904
2976
|
}
|
|
2905
2977
|
// A section id points at the read tool of its own scope, which lists the valid ids.
|
|
2906
2978
|
const sectionHelp = (name: string) =>
|
|
2907
|
-
(tools.find((t) => t.name === name)!.inputSchema.properties as Record<string, { description?: string }>)
|
|
2908
|
-
|
|
2979
|
+
(tools.find((t) => t.name === name)!.inputSchema.properties as Record<string, { description?: string }>)[
|
|
2980
|
+
'sectionId'
|
|
2981
|
+
]!.description;
|
|
2909
2982
|
expect(sectionHelp('taiga_update_document')).toContain('taiga_get_document,');
|
|
2910
2983
|
expect(sectionHelp('taiga_update_factory_document')).toContain('taiga_get_factory_document');
|
|
2911
2984
|
expect(sectionHelp('taiga_update_product_document')).toContain('taiga_get_product_document');
|
|
@@ -3079,7 +3152,7 @@ describe('MCP server integration', () => {
|
|
|
3079
3152
|
routes: { [`PUT /v1/backlog/${ITEM_ID}/state`]: { data: queued } },
|
|
3080
3153
|
});
|
|
3081
3154
|
|
|
3082
|
-
const result = await client.callTool({ name: '
|
|
3155
|
+
const result = await client.callTool({ name: 'taiga_queue_initiative', arguments: { initiativeId: ITEM_ID } });
|
|
3083
3156
|
|
|
3084
3157
|
expect(result.isError).toBeFalsy();
|
|
3085
3158
|
// One write to the item, no item read around it: the route answers with the item (the reads
|
|
@@ -3096,12 +3169,15 @@ describe('MCP server integration', () => {
|
|
|
3096
3169
|
routes: { [`PUT /v1/backlog/${ITEM_ID}/state`]: { data: queued } },
|
|
3097
3170
|
});
|
|
3098
3171
|
|
|
3099
|
-
await client.callTool({
|
|
3172
|
+
await client.callTool({
|
|
3173
|
+
name: 'taiga_queue_initiative',
|
|
3174
|
+
arguments: { initiativeId: ITEM_ID, afterId: OTHER_ITEM },
|
|
3175
|
+
});
|
|
3100
3176
|
expect(sentBody(fetchSpy, 'PUT', `/backlog/${ITEM_ID}/state`)).toEqual({ state: 'queue', afterId: OTHER_ITEM });
|
|
3101
3177
|
|
|
3102
3178
|
fetchSpy.mockClear();
|
|
3103
3179
|
vi.stubGlobal('fetch', fetchSpy);
|
|
3104
|
-
await client.callTool({ name: '
|
|
3180
|
+
await client.callTool({ name: 'taiga_queue_initiative', arguments: { initiativeId: ITEM_ID, afterId: null } });
|
|
3105
3181
|
expect(sentBody(fetchSpy, 'PUT', `/backlog/${ITEM_ID}/state`)).toEqual({ state: 'queue', afterId: null });
|
|
3106
3182
|
});
|
|
3107
3183
|
|
|
@@ -3113,7 +3189,7 @@ describe('MCP server integration', () => {
|
|
|
3113
3189
|
},
|
|
3114
3190
|
});
|
|
3115
3191
|
|
|
3116
|
-
const result = await client.callTool({ name: '
|
|
3192
|
+
const result = await client.callTool({ name: 'taiga_queue_initiative', arguments: { initiativeId: ITEM_ID } });
|
|
3117
3193
|
|
|
3118
3194
|
expect(result.isError).toBe(true);
|
|
3119
3195
|
expect(outcomeOf(result)).toEqual({ error: 'CONFLICT', message: 'Stop the running agent before re-planning' });
|
|
@@ -3122,7 +3198,7 @@ describe('MCP server integration', () => {
|
|
|
3122
3198
|
it('should report an item the key cannot see as a miss', async () => {
|
|
3123
3199
|
const { client } = await bootMcpPair({ capabilities: ALL_CAPABILITIES });
|
|
3124
3200
|
|
|
3125
|
-
const result = await client.callTool({ name: '
|
|
3201
|
+
const result = await client.callTool({ name: 'taiga_queue_initiative', arguments: { initiativeId: ITEM_ID } });
|
|
3126
3202
|
|
|
3127
3203
|
expect(result.isError).toBe(true);
|
|
3128
3204
|
expect(outcomeOf(result).error).toBe('NOT_FOUND');
|
|
@@ -3133,7 +3209,7 @@ describe('MCP server integration', () => {
|
|
|
3133
3209
|
const operatorOnly = await bootMcpPair({ capabilities: ['products:write'] });
|
|
3134
3210
|
|
|
3135
3211
|
expect(toolNames((await backlogOnly.client.listTools()).tools)).toEqual([
|
|
3136
|
-
'
|
|
3212
|
+
'taiga_move_initiative',
|
|
3137
3213
|
'taiga_set_auto_merge',
|
|
3138
3214
|
]);
|
|
3139
3215
|
// A request, the resume and the review gate take a product or an item ID a request or a queue hands
|
|
@@ -3147,7 +3223,7 @@ describe('MCP server integration', () => {
|
|
|
3147
3223
|
'taiga_refresh_reality_documents',
|
|
3148
3224
|
'taiga_reopen_discovery',
|
|
3149
3225
|
'taiga_request_initiatives',
|
|
3150
|
-
'
|
|
3226
|
+
'taiga_resume_initiative',
|
|
3151
3227
|
'taiga_set_auto_build',
|
|
3152
3228
|
'taiga_set_product_autonomy',
|
|
3153
3229
|
'taiga_stop_generating_documents',
|
|
@@ -3160,8 +3236,8 @@ describe('MCP server integration', () => {
|
|
|
3160
3236
|
const before = narrow.fetchSpy.mock.calls.length;
|
|
3161
3237
|
|
|
3162
3238
|
const result = await narrow.client.callTool({
|
|
3163
|
-
name: '
|
|
3164
|
-
arguments: {
|
|
3239
|
+
name: 'taiga_move_initiative',
|
|
3240
|
+
arguments: { initiativeId: ITEM_ID, state: 'queue' },
|
|
3165
3241
|
});
|
|
3166
3242
|
|
|
3167
3243
|
expect(result.isError).toBe(true);
|
|
@@ -3181,7 +3257,10 @@ describe('MCP server integration', () => {
|
|
|
3181
3257
|
[`PUT /v1/backlog/${ITEM_ID}/state`]: { data: { ...atStation, planningStatus: 'planning' } },
|
|
3182
3258
|
},
|
|
3183
3259
|
});
|
|
3184
|
-
const replanned = await station.client.callTool({
|
|
3260
|
+
const replanned = await station.client.callTool({
|
|
3261
|
+
name: 'taiga_plan_again',
|
|
3262
|
+
arguments: { initiativeId: ITEM_ID },
|
|
3263
|
+
});
|
|
3185
3264
|
expect(replanned.isError).toBeFalsy();
|
|
3186
3265
|
expect(sentBody(station.fetchSpy, 'PUT', `/backlog/${ITEM_ID}/state`)).toEqual({ state: 'plan' });
|
|
3187
3266
|
expect(outcomeOf(replanned)).toMatchObject({ state: 'plan', planningStatus: 'planning' });
|
|
@@ -3190,7 +3269,7 @@ describe('MCP server integration', () => {
|
|
|
3190
3269
|
capabilities: ALL_CAPABILITIES,
|
|
3191
3270
|
routes: { [`GET /v1/backlog/${ITEM_ID}`]: { data: item } },
|
|
3192
3271
|
});
|
|
3193
|
-
const refused = await idle.client.callTool({ name: 'taiga_plan_again', arguments: {
|
|
3272
|
+
const refused = await idle.client.callTool({ name: 'taiga_plan_again', arguments: { initiativeId: ITEM_ID } });
|
|
3194
3273
|
expect(refused.isError).toBe(true);
|
|
3195
3274
|
expect(outcomeOf(refused).message).toMatch(/not at the Build station\. The queue is the only way to start work/);
|
|
3196
3275
|
// Refused on the read: no move was sent.
|
|
@@ -3207,7 +3286,7 @@ describe('MCP server integration', () => {
|
|
|
3207
3286
|
},
|
|
3208
3287
|
});
|
|
3209
3288
|
|
|
3210
|
-
await client.callTool({ name: 'taiga_plan_again', arguments: {
|
|
3289
|
+
await client.callTool({ name: 'taiga_plan_again', arguments: { initiativeId: ITEM_ID, force: true } });
|
|
3211
3290
|
|
|
3212
3291
|
expect(sentBody(fetchSpy, 'PUT', `/backlog/${ITEM_ID}/state`)).toEqual({ state: 'plan', force: true });
|
|
3213
3292
|
});
|
|
@@ -3234,8 +3313,8 @@ describe('MCP server integration', () => {
|
|
|
3234
3313
|
'Initiatives are already being generated for this product',
|
|
3235
3314
|
],
|
|
3236
3315
|
[
|
|
3237
|
-
'
|
|
3238
|
-
{
|
|
3316
|
+
'taiga_resume_initiative',
|
|
3317
|
+
{ initiativeId: ITEM_ID },
|
|
3239
3318
|
'CONFLICT',
|
|
3240
3319
|
'This initiative is not stopped, so there is nothing to resume.',
|
|
3241
3320
|
],
|
|
@@ -3255,8 +3334,8 @@ describe('MCP server integration', () => {
|
|
|
3255
3334
|
});
|
|
3256
3335
|
|
|
3257
3336
|
const result = await client.callTool({
|
|
3258
|
-
name: '
|
|
3259
|
-
arguments: {
|
|
3337
|
+
name: 'taiga_move_initiative',
|
|
3338
|
+
arguments: { initiativeId: ITEM_ID, state: 'todo', afterId: null },
|
|
3260
3339
|
});
|
|
3261
3340
|
|
|
3262
3341
|
expect(result.isError).toBeFalsy();
|
|
@@ -3275,8 +3354,8 @@ describe('MCP server integration', () => {
|
|
|
3275
3354
|
});
|
|
3276
3355
|
|
|
3277
3356
|
const filed = await client.callTool({
|
|
3278
|
-
name: '
|
|
3279
|
-
arguments: {
|
|
3357
|
+
name: 'taiga_move_initiative',
|
|
3358
|
+
arguments: { initiativeId: ITEM_ID, state: 'canceled', archived: true },
|
|
3280
3359
|
});
|
|
3281
3360
|
|
|
3282
3361
|
expect(filed.isError).toBeFalsy();
|
|
@@ -3286,8 +3365,8 @@ describe('MCP server integration', () => {
|
|
|
3286
3365
|
fetchSpy.mockClear();
|
|
3287
3366
|
vi.stubGlobal('fetch', fetchSpy);
|
|
3288
3367
|
await client.callTool({
|
|
3289
|
-
name: '
|
|
3290
|
-
arguments: {
|
|
3368
|
+
name: 'taiga_move_initiative',
|
|
3369
|
+
arguments: { initiativeId: ITEM_ID, state: 'todo', archived: false },
|
|
3291
3370
|
});
|
|
3292
3371
|
expect(sentBody(fetchSpy, 'PUT', `/backlog/${ITEM_ID}/state`)).toEqual({ state: 'todo', archived: false });
|
|
3293
3372
|
});
|
|
@@ -3305,8 +3384,8 @@ describe('MCP server integration', () => {
|
|
|
3305
3384
|
});
|
|
3306
3385
|
|
|
3307
3386
|
const result = await client.callTool({
|
|
3308
|
-
name: '
|
|
3309
|
-
arguments: {
|
|
3387
|
+
name: 'taiga_move_initiative',
|
|
3388
|
+
arguments: { initiativeId: ITEM_ID, state: 'done', archived: true },
|
|
3310
3389
|
});
|
|
3311
3390
|
|
|
3312
3391
|
expect(result.isError).toBe(true);
|
|
@@ -3320,16 +3399,16 @@ describe('MCP server integration', () => {
|
|
|
3320
3399
|
const { client, fetchSpy } = await bootMcpPair({ capabilities: ALL_CAPABILITIES });
|
|
3321
3400
|
|
|
3322
3401
|
const { tools } = await client.listTools();
|
|
3323
|
-
const move = tools.find((t) => t.name === '
|
|
3402
|
+
const move = tools.find((t) => t.name === 'taiga_move_initiative')!;
|
|
3324
3403
|
const props = move.inputSchema.properties as Record<string, { enum?: string[] }>;
|
|
3325
3404
|
// The queue is the only way to start work: no "plan this now", nothing dropped into Build.
|
|
3326
|
-
expect(props
|
|
3405
|
+
expect(props['state']!.enum).toEqual(['backlog', 'todo', 'queue', 'done', 'canceled']);
|
|
3327
3406
|
|
|
3328
3407
|
const before = fetchSpy.mock.calls.length;
|
|
3329
3408
|
for (const state of ['build', 'plan']) {
|
|
3330
3409
|
const result = await client.callTool({
|
|
3331
|
-
name: '
|
|
3332
|
-
arguments: {
|
|
3410
|
+
name: 'taiga_move_initiative',
|
|
3411
|
+
arguments: { initiativeId: ITEM_ID, state },
|
|
3333
3412
|
});
|
|
3334
3413
|
expect(result.isError, state).toBe(true);
|
|
3335
3414
|
}
|
|
@@ -3345,8 +3424,8 @@ describe('MCP server integration', () => {
|
|
|
3345
3424
|
});
|
|
3346
3425
|
|
|
3347
3426
|
const result = await client.callTool({
|
|
3348
|
-
name: '
|
|
3349
|
-
arguments: {
|
|
3427
|
+
name: 'taiga_move_initiative',
|
|
3428
|
+
arguments: { initiativeId: ITEM_ID, state: 'todo' },
|
|
3350
3429
|
});
|
|
3351
3430
|
|
|
3352
3431
|
expect(result.isError).toBe(true);
|
|
@@ -3360,7 +3439,7 @@ describe('MCP server integration', () => {
|
|
|
3360
3439
|
routes: { [`POST /v1/backlog/${ITEM_ID}/resume`]: { data: resumed } },
|
|
3361
3440
|
});
|
|
3362
3441
|
|
|
3363
|
-
const result = await client.callTool({ name: '
|
|
3442
|
+
const result = await client.callTool({ name: 'taiga_resume_initiative', arguments: { initiativeId: ITEM_ID } });
|
|
3364
3443
|
|
|
3365
3444
|
expect(result.isError).toBeFalsy();
|
|
3366
3445
|
const call = fetchSpy.mock.calls.find((c: unknown[]) => (c[0] as string).endsWith('/resume'))!;
|
|
@@ -3378,11 +3457,11 @@ describe('MCP server integration', () => {
|
|
|
3378
3457
|
},
|
|
3379
3458
|
});
|
|
3380
3459
|
|
|
3381
|
-
const found = await client.callTool({ name: 'taiga_get_plan', arguments: {
|
|
3460
|
+
const found = await client.callTool({ name: 'taiga_get_plan', arguments: { initiativeId: ITEM_ID } });
|
|
3382
3461
|
// A summary by default: the step's prose, timestamps and planId stay behind.
|
|
3383
3462
|
expect(outcomeOf(found)).toEqual({
|
|
3384
3463
|
id: PLAN_ID,
|
|
3385
|
-
|
|
3464
|
+
initiativeId: ITEM_ID,
|
|
3386
3465
|
version: 1,
|
|
3387
3466
|
status: 'active',
|
|
3388
3467
|
steps: [
|
|
@@ -3399,19 +3478,21 @@ describe('MCP server integration', () => {
|
|
|
3399
3478
|
],
|
|
3400
3479
|
});
|
|
3401
3480
|
|
|
3402
|
-
const full = await client.callTool({ name: 'taiga_get_plan', arguments: {
|
|
3481
|
+
const full = await client.callTool({ name: 'taiga_get_plan', arguments: { initiativeId: ITEM_ID, full: true } });
|
|
3403
3482
|
const { tenantId: _t, ...planWithoutTenant } = plan;
|
|
3404
|
-
expect(outcomeOf(full)).toEqual(
|
|
3405
|
-
|
|
3406
|
-
|
|
3407
|
-
|
|
3483
|
+
expect(outcomeOf(full)).toEqual(
|
|
3484
|
+
asSeen({
|
|
3485
|
+
...planWithoutTenant,
|
|
3486
|
+
steps: plan.steps.map(({ tenantId: _s, ...step }) => step),
|
|
3487
|
+
})
|
|
3488
|
+
);
|
|
3408
3489
|
|
|
3409
|
-
const none = await client.callTool({ name: 'taiga_get_plan', arguments: {
|
|
3490
|
+
const none = await client.callTool({ name: 'taiga_get_plan', arguments: { initiativeId: OTHER_ITEM } });
|
|
3410
3491
|
expect(none.isError).toBe(true);
|
|
3411
3492
|
expect(outcomeOf(none)).toEqual({
|
|
3412
3493
|
error: 'NOT_FOUND',
|
|
3413
3494
|
message:
|
|
3414
|
-
'No active plan found.
|
|
3495
|
+
'No active plan found. taiga_get_initiative says whether one is being generated (planningStatus planning) or failed, and taiga_plan_again asks for a new one.',
|
|
3415
3496
|
});
|
|
3416
3497
|
});
|
|
3417
3498
|
|
|
@@ -3472,18 +3553,20 @@ describe('MCP server integration', () => {
|
|
|
3472
3553
|
},
|
|
3473
3554
|
});
|
|
3474
3555
|
|
|
3475
|
-
const stop = await client.callTool({ name: '
|
|
3556
|
+
const stop = await client.callTool({ name: 'taiga_stop_run', arguments: { runId: BUILD_ID } });
|
|
3476
3557
|
expect(stop.isError).toBeFalsy();
|
|
3477
3558
|
expect(outcomeOf(stop)).toEqual({
|
|
3478
3559
|
id: BUILD_ID,
|
|
3479
3560
|
runNumber: 1,
|
|
3480
|
-
|
|
3561
|
+
initiativeId: ITEM_ID,
|
|
3481
3562
|
status: 'stopped',
|
|
3482
3563
|
failedReason: 'user_stopped',
|
|
3483
3564
|
completedSteps: 1,
|
|
3484
3565
|
totalSteps: 3,
|
|
3485
3566
|
prUrl: null,
|
|
3486
3567
|
actions: ['resume', 'start_over'],
|
|
3568
|
+
disposition: 'stopped',
|
|
3569
|
+
outcome: 'Stopped before it finished (1 of 3 steps done).',
|
|
3487
3570
|
});
|
|
3488
3571
|
const call = fetchSpy.mock.calls.find((c: unknown[]) => (c[0] as string).endsWith('/stop'))!;
|
|
3489
3572
|
expect((call[1] as RequestInit).body).toBeNull();
|
|
@@ -3497,7 +3580,7 @@ describe('MCP server integration', () => {
|
|
|
3497
3580
|
},
|
|
3498
3581
|
});
|
|
3499
3582
|
|
|
3500
|
-
const result = await client.callTool({ name: '
|
|
3583
|
+
const result = await client.callTool({ name: 'taiga_stop_run', arguments: { runId: BUILD_ID } });
|
|
3501
3584
|
|
|
3502
3585
|
expect(result.isError).toBe(true);
|
|
3503
3586
|
expect(outcomeOf(result)).toEqual({ error: 'CONFLICT', message: 'Cannot stop a run that is completed' });
|
|
@@ -3519,7 +3602,7 @@ describe('MCP server integration', () => {
|
|
|
3519
3602
|
},
|
|
3520
3603
|
});
|
|
3521
3604
|
|
|
3522
|
-
const result = await client.callTool({ name: '
|
|
3605
|
+
const result = await client.callTool({ name: 'taiga_list_runs', arguments: { active: true, limit: 10 } });
|
|
3523
3606
|
|
|
3524
3607
|
expect(result.isError).toBeFalsy();
|
|
3525
3608
|
const { data, nextCursor } = outcomeOf(result);
|
|
@@ -3540,11 +3623,11 @@ describe('MCP server integration', () => {
|
|
|
3540
3623
|
expect(sentUrl(fetchSpy, '/products').searchParams.get('limit')).toBe('100');
|
|
3541
3624
|
});
|
|
3542
3625
|
|
|
3543
|
-
it('should refuse
|
|
3626
|
+
it('should refuse initiativeId without productId before any request rather than answer with every run', async () => {
|
|
3544
3627
|
const { client, fetchSpy } = await bootMcpPair({ capabilities: ['products:read'] });
|
|
3545
3628
|
const before = fetchSpy.mock.calls.length;
|
|
3546
3629
|
|
|
3547
|
-
const result = await client.callTool({ name: '
|
|
3630
|
+
const result = await client.callTool({ name: 'taiga_list_runs', arguments: { initiativeId: ITEM_ID } });
|
|
3548
3631
|
|
|
3549
3632
|
expect(result.isError).toBe(true);
|
|
3550
3633
|
expect(outcomeOf(result)).toMatchObject({ error: 'VALIDATION_ERROR' });
|
|
@@ -3560,7 +3643,7 @@ describe('MCP server integration', () => {
|
|
|
3560
3643
|
},
|
|
3561
3644
|
});
|
|
3562
3645
|
|
|
3563
|
-
const result = await client.callTool({ name: '
|
|
3646
|
+
const result = await client.callTool({ name: 'taiga_list_runs', arguments: {} });
|
|
3564
3647
|
|
|
3565
3648
|
const { data, note } = outcomeOf(result);
|
|
3566
3649
|
expect(data[0]).not.toHaveProperty('product');
|
|
@@ -3576,7 +3659,7 @@ describe('MCP server integration', () => {
|
|
|
3576
3659
|
},
|
|
3577
3660
|
});
|
|
3578
3661
|
|
|
3579
|
-
const { data, note } = outcomeOf(await client.callTool({ name: '
|
|
3662
|
+
const { data, note } = outcomeOf(await client.callTool({ name: 'taiga_list_runs', arguments: {} }));
|
|
3580
3663
|
|
|
3581
3664
|
expect(data[0]).not.toHaveProperty('product');
|
|
3582
3665
|
expect(note).toMatch(/cannot list \(deleted since, or not visible/);
|
|
@@ -3592,7 +3675,7 @@ describe('MCP server integration', () => {
|
|
|
3592
3675
|
},
|
|
3593
3676
|
});
|
|
3594
3677
|
|
|
3595
|
-
const result = outcomeOf(await client.callTool({ name: '
|
|
3678
|
+
const result = outcomeOf(await client.callTool({ name: 'taiga_list_runs', arguments: {} }));
|
|
3596
3679
|
|
|
3597
3680
|
expect(result.data[0].product).toEqual({ name: 'Web', slug: 'web' });
|
|
3598
3681
|
expect(result).not.toHaveProperty('note');
|
|
@@ -3608,7 +3691,7 @@ describe('MCP server integration', () => {
|
|
|
3608
3691
|
},
|
|
3609
3692
|
},
|
|
3610
3693
|
});
|
|
3611
|
-
const result = outcomeOf(await offline.client.callTool({ name: '
|
|
3694
|
+
const result = outcomeOf(await offline.client.callTool({ name: 'taiga_list_runs', arguments: {} }));
|
|
3612
3695
|
expect(result.data[0]).not.toHaveProperty('product');
|
|
3613
3696
|
expect(result.note).toMatch(/products could not be read/);
|
|
3614
3697
|
|
|
@@ -3621,7 +3704,7 @@ describe('MCP server integration', () => {
|
|
|
3621
3704
|
},
|
|
3622
3705
|
},
|
|
3623
3706
|
});
|
|
3624
|
-
const failure = await buggy.client.callTool({ name: '
|
|
3707
|
+
const failure = await buggy.client.callTool({ name: 'taiga_list_runs', arguments: {} });
|
|
3625
3708
|
expect(failure.isError).toBe(true);
|
|
3626
3709
|
expect(JSON.stringify(failure.content)).toContain('a bug in the products path');
|
|
3627
3710
|
expect(JSON.stringify(failure.content)).not.toContain('could not be read');
|
|
@@ -3636,7 +3719,7 @@ describe('MCP server integration', () => {
|
|
|
3636
3719
|
},
|
|
3637
3720
|
});
|
|
3638
3721
|
|
|
3639
|
-
const result = await client.callTool({ name: '
|
|
3722
|
+
const result = await client.callTool({ name: 'taiga_list_runs', arguments: {} });
|
|
3640
3723
|
|
|
3641
3724
|
expect(result.isError).toBeFalsy();
|
|
3642
3725
|
const { data, note } = outcomeOf(result);
|
|
@@ -3655,7 +3738,7 @@ describe('MCP server integration', () => {
|
|
|
3655
3738
|
},
|
|
3656
3739
|
});
|
|
3657
3740
|
|
|
3658
|
-
expect(outcomeOf(await client.callTool({ name: '
|
|
3741
|
+
expect(outcomeOf(await client.callTool({ name: 'taiga_list_runs', arguments: {} }))).not.toHaveProperty('note');
|
|
3659
3742
|
});
|
|
3660
3743
|
|
|
3661
3744
|
it('should refuse active with productId before any request rather than drop the filter', async () => {
|
|
@@ -3663,7 +3746,7 @@ describe('MCP server integration', () => {
|
|
|
3663
3746
|
const before = fetchSpy.mock.calls.length;
|
|
3664
3747
|
|
|
3665
3748
|
const result = await client.callTool({
|
|
3666
|
-
name: '
|
|
3749
|
+
name: 'taiga_list_runs',
|
|
3667
3750
|
arguments: { productId: PRODUCT_ID, active: true },
|
|
3668
3751
|
});
|
|
3669
3752
|
|
|
@@ -3678,7 +3761,7 @@ describe('MCP server integration', () => {
|
|
|
3678
3761
|
routes: { [`/v1/products/${PRODUCT_ID}/builds`]: { data: [run], nextCursor: null } },
|
|
3679
3762
|
});
|
|
3680
3763
|
|
|
3681
|
-
const result = await client.callTool({ name: '
|
|
3764
|
+
const result = await client.callTool({ name: 'taiga_list_runs', arguments: { productId: PRODUCT_ID } });
|
|
3682
3765
|
|
|
3683
3766
|
expect(result.isError).toBeFalsy();
|
|
3684
3767
|
expect(fetchSpy.mock.calls.some((c: unknown[]) => (c[0] as string).endsWith('/v1/products?limit=100'))).toBe(
|
|
@@ -3757,7 +3840,7 @@ describe('MCP server integration', () => {
|
|
|
3757
3840
|
});
|
|
3758
3841
|
|
|
3759
3842
|
const rows = parseToolResult(
|
|
3760
|
-
await client.callTool({ name: '
|
|
3843
|
+
await client.callTool({ name: 'taiga_list_initiatives', arguments: { productId: PRODUCT_ID } })
|
|
3761
3844
|
).data;
|
|
3762
3845
|
|
|
3763
3846
|
expect(rows[0].criteria).toEqual({ met: 1, total: 2 });
|
|
@@ -3790,9 +3873,7 @@ describe('MCP server integration', () => {
|
|
|
3790
3873
|
routes: { [`/v1/builds/${BUILD_ID}`]: { data: run }, [`/v1/builds/${BUILD_ID}/steps`]: { data: steps } },
|
|
3791
3874
|
});
|
|
3792
3875
|
|
|
3793
|
-
const result = parseToolResult(
|
|
3794
|
-
await client.callTool({ name: 'taiga_get_build', arguments: { buildId: BUILD_ID } })
|
|
3795
|
-
);
|
|
3876
|
+
const result = parseToolResult(await client.callTool({ name: 'taiga_get_run', arguments: { runId: BUILD_ID } }));
|
|
3796
3877
|
|
|
3797
3878
|
expect(result).toMatchObject(run);
|
|
3798
3879
|
expect(result.steps[0]).toEqual({
|
|
@@ -3827,7 +3908,7 @@ describe('MCP server integration', () => {
|
|
|
3827
3908
|
});
|
|
3828
3909
|
|
|
3829
3910
|
const queue = parseToolResult(
|
|
3830
|
-
await client.callTool({ name: '
|
|
3911
|
+
await client.callTool({ name: 'taiga_get_line', arguments: { productId: PRODUCT_ID } })
|
|
3831
3912
|
);
|
|
3832
3913
|
|
|
3833
3914
|
expect(queue.entries.map((row: { title: string }) => row.title)).toEqual(['ARC-3.3 OpenAPI', 'README']);
|
|
@@ -3859,10 +3940,10 @@ describe('MCP server integration', () => {
|
|
|
3859
3940
|
});
|
|
3860
3941
|
|
|
3861
3942
|
const queue = parseToolResult(
|
|
3862
|
-
await client.callTool({ name: '
|
|
3943
|
+
await client.callTool({ name: 'taiga_get_line', arguments: { productId: PRODUCT_ID } })
|
|
3863
3944
|
);
|
|
3864
3945
|
|
|
3865
|
-
expect(queue.entries[0]).toMatchObject({
|
|
3946
|
+
expect(queue.entries[0]).toMatchObject({ runId: BUILD_ID, prUrl: 'https://github.com/acme/web/pull/22' });
|
|
3866
3947
|
expect(queue.waitingOn).toBe(
|
|
3867
3948
|
'"ARC-3.3 OpenAPI" is waiting on a person (review_pr) (https://github.com/acme/web/pull/22); every other queued initiative waits until its pull request merges.'
|
|
3868
3949
|
);
|
|
@@ -3886,12 +3967,12 @@ describe('MCP server integration', () => {
|
|
|
3886
3967
|
});
|
|
3887
3968
|
|
|
3888
3969
|
const queue = parseToolResult(
|
|
3889
|
-
await client.callTool({ name: '
|
|
3970
|
+
await client.callTool({ name: 'taiga_get_line', arguments: { productId: PRODUCT_ID } })
|
|
3890
3971
|
);
|
|
3891
3972
|
expect(queue.waitingOn).toMatch(/^"ARC-3.3 OpenAPI" is waiting on a person/);
|
|
3892
3973
|
|
|
3893
3974
|
fetchSpy.mockClear();
|
|
3894
|
-
const queued = await client.callTool({ name: '
|
|
3975
|
+
const queued = await client.callTool({ name: 'taiga_queue_initiative', arguments: { initiativeId: ITEM_ID } });
|
|
3895
3976
|
expect(queued.isError).toBeFalsy();
|
|
3896
3977
|
// Placing an item names only the entry the line waits on: the active one.
|
|
3897
3978
|
const itemReads = fetchSpy.mock.calls
|
|
@@ -3914,7 +3995,7 @@ describe('MCP server integration', () => {
|
|
|
3914
3995
|
});
|
|
3915
3996
|
|
|
3916
3997
|
const queue = parseToolResult(
|
|
3917
|
-
await client.callTool({ name: '
|
|
3998
|
+
await client.callTool({ name: 'taiga_get_line', arguments: { productId: PRODUCT_ID } })
|
|
3918
3999
|
);
|
|
3919
4000
|
|
|
3920
4001
|
expect(queue.waitingOn).toContain('holding the entries behind it');
|
|
@@ -3936,7 +4017,7 @@ describe('MCP server integration', () => {
|
|
|
3936
4017
|
});
|
|
3937
4018
|
|
|
3938
4019
|
const queue = parseToolResult(
|
|
3939
|
-
await client.callTool({ name: '
|
|
4020
|
+
await client.callTool({ name: 'taiga_get_line', arguments: { productId: PRODUCT_ID } })
|
|
3940
4021
|
);
|
|
3941
4022
|
|
|
3942
4023
|
expect(queue.waitingOn).toBe(
|
|
@@ -3963,7 +4044,7 @@ describe('MCP server integration', () => {
|
|
|
3963
4044
|
expect(pause).toEqual({ productId: PRODUCT_ID, linePaused: true, lineStoppedAt: '2026-10-08T20:00:00.000Z' });
|
|
3964
4045
|
|
|
3965
4046
|
const queue = parseToolResult(
|
|
3966
|
-
await client.callTool({ name: '
|
|
4047
|
+
await client.callTool({ name: 'taiga_get_line', arguments: { productId: PRODUCT_ID } })
|
|
3967
4048
|
);
|
|
3968
4049
|
expect(queue.linePaused).toBe(true);
|
|
3969
4050
|
|
|
@@ -3988,7 +4069,7 @@ describe('MCP server integration', () => {
|
|
|
3988
4069
|
// One pair at a time: each boot stubs the global fetch the previous client reads through.
|
|
3989
4070
|
const noBacklog = await bootMcpPair({ capabilities: ['products:read'], routes: queueRoute });
|
|
3990
4071
|
const without = parseToolResult(
|
|
3991
|
-
await noBacklog.client.callTool({ name: '
|
|
4072
|
+
await noBacklog.client.callTool({ name: 'taiga_get_line', arguments: { productId: PRODUCT_ID } })
|
|
3992
4073
|
);
|
|
3993
4074
|
expect(noBacklog.fetchSpy.mock.calls.some((c: unknown[]) => (c[0] as string).includes('/backlog/'))).toBe(false);
|
|
3994
4075
|
const failing = await bootMcpPair({
|
|
@@ -3996,10 +4077,10 @@ describe('MCP server integration', () => {
|
|
|
3996
4077
|
routes: { ...queueRoute, [`/v1/backlog/${HEAD_ITEM}`]: refusal(500, 'INTERNAL', 'boom') },
|
|
3997
4078
|
});
|
|
3998
4079
|
const failed = parseToolResult(
|
|
3999
|
-
await failing.client.callTool({ name: '
|
|
4080
|
+
await failing.client.callTool({ name: 'taiga_get_line', arguments: { productId: PRODUCT_ID } })
|
|
4000
4081
|
);
|
|
4001
4082
|
|
|
4002
|
-
expect(without.entries[0]).toEqual({ position: 1,
|
|
4083
|
+
expect(without.entries[0]).toEqual({ position: 1, initiativeId: HEAD_ITEM, entryState: 'active' });
|
|
4003
4084
|
expect(without.note).toContain('backlog:read');
|
|
4004
4085
|
expect(failed.entries[0]).not.toHaveProperty('title');
|
|
4005
4086
|
expect(failed.note).toContain('could not be read');
|
|
@@ -4019,7 +4100,10 @@ describe('MCP server integration', () => {
|
|
|
4019
4100
|
});
|
|
4020
4101
|
|
|
4021
4102
|
const result = parseToolResult(
|
|
4022
|
-
await client.callTool({
|
|
4103
|
+
await client.callTool({
|
|
4104
|
+
name: 'taiga_queue_initiative',
|
|
4105
|
+
arguments: { initiativeId: ITEM_ID, autoBuild: false },
|
|
4106
|
+
})
|
|
4023
4107
|
);
|
|
4024
4108
|
|
|
4025
4109
|
// To place one item and say what the line waits on, only the blocking entry is read.
|
|
@@ -4055,14 +4139,14 @@ describe('MCP server integration', () => {
|
|
|
4055
4139
|
});
|
|
4056
4140
|
|
|
4057
4141
|
const result = parseToolResult(
|
|
4058
|
-
await client.callTool({ name: '
|
|
4142
|
+
await client.callTool({ name: 'taiga_queue_initiative', arguments: { initiativeId: ITEM_ID } })
|
|
4059
4143
|
);
|
|
4060
4144
|
|
|
4061
4145
|
expect(fetchSpy.mock.calls.some((c: unknown[]) => (c[0] as string).includes('/auto-build'))).toBe(false);
|
|
4062
4146
|
expect(result.reviewGate).toMatchObject({ buildsWithoutReview: true, decidedBy: 'product' });
|
|
4063
4147
|
// The queue read missed (no route); the outcome says so rather than failing the write it reports.
|
|
4064
4148
|
expect(result.queue).toBeNull();
|
|
4065
|
-
expect(result.note).toContain('
|
|
4149
|
+
expect(result.note).toContain('taiga_get_line');
|
|
4066
4150
|
});
|
|
4067
4151
|
|
|
4068
4152
|
it('should set an initiative back to the product default with null and report the gate that now applies', async () => {
|
|
@@ -4075,7 +4159,7 @@ describe('MCP server integration', () => {
|
|
|
4075
4159
|
});
|
|
4076
4160
|
|
|
4077
4161
|
const result = parseToolResult(
|
|
4078
|
-
await client.callTool({ name: 'taiga_set_auto_build', arguments: {
|
|
4162
|
+
await client.callTool({ name: 'taiga_set_auto_build', arguments: { initiativeId: ITEM_ID, autoBuild: null } })
|
|
4079
4163
|
);
|
|
4080
4164
|
|
|
4081
4165
|
expect(sentBody(fetchSpy, 'PUT', '/auto-build')).toEqual({ autoBuild: null });
|
|
@@ -4097,8 +4181,8 @@ describe('MCP server integration', () => {
|
|
|
4097
4181
|
|
|
4098
4182
|
const result = parseToolResult(
|
|
4099
4183
|
await client.callTool({
|
|
4100
|
-
name: '
|
|
4101
|
-
arguments: {
|
|
4184
|
+
name: 'taiga_wait_for_initiative',
|
|
4185
|
+
arguments: { initiativeId: ITEM_ID, until: 'planned', timeoutSeconds: 30 },
|
|
4102
4186
|
})
|
|
4103
4187
|
);
|
|
4104
4188
|
|
|
@@ -4122,7 +4206,10 @@ describe('MCP server integration', () => {
|
|
|
4122
4206
|
});
|
|
4123
4207
|
|
|
4124
4208
|
const result = parseToolResult(
|
|
4125
|
-
await client.callTool({
|
|
4209
|
+
await client.callTool({
|
|
4210
|
+
name: 'taiga_wait_for_initiative',
|
|
4211
|
+
arguments: { initiativeId: ITEM_ID, until: 'pr_open' },
|
|
4212
|
+
})
|
|
4126
4213
|
);
|
|
4127
4214
|
|
|
4128
4215
|
expect(result).toMatchObject({ reached: false, stoppedBecause: 'needs a person: planning_failed' });
|
|
@@ -4142,8 +4229,8 @@ describe('MCP server integration', () => {
|
|
|
4142
4229
|
});
|
|
4143
4230
|
const plan = parseToolResult(
|
|
4144
4231
|
await verdict.client.callTool({
|
|
4145
|
-
name: '
|
|
4146
|
-
arguments: {
|
|
4232
|
+
name: 'taiga_wait_for_initiative',
|
|
4233
|
+
arguments: { initiativeId: ITEM_ID, until: 'pr_open' },
|
|
4147
4234
|
})
|
|
4148
4235
|
);
|
|
4149
4236
|
expect(plan).toMatchObject({ reached: false, stoppedBecause: 'waits on a person: start_build' });
|
|
@@ -4155,8 +4242,8 @@ describe('MCP server integration', () => {
|
|
|
4155
4242
|
});
|
|
4156
4243
|
const unsure = parseToolResult(
|
|
4157
4244
|
await unknown.client.callTool({
|
|
4158
|
-
name: '
|
|
4159
|
-
arguments: {
|
|
4245
|
+
name: 'taiga_wait_for_initiative',
|
|
4246
|
+
arguments: { initiativeId: ITEM_ID, until: 'your_turn' },
|
|
4160
4247
|
})
|
|
4161
4248
|
);
|
|
4162
4249
|
expect(unsure.reached).toBe(false);
|
|
@@ -4168,8 +4255,8 @@ describe('MCP server integration', () => {
|
|
|
4168
4255
|
});
|
|
4169
4256
|
const pr = parseToolResult(
|
|
4170
4257
|
await review.client.callTool({
|
|
4171
|
-
name: '
|
|
4172
|
-
arguments: {
|
|
4258
|
+
name: 'taiga_wait_for_initiative',
|
|
4259
|
+
arguments: { initiativeId: ITEM_ID, until: 'done' },
|
|
4173
4260
|
})
|
|
4174
4261
|
);
|
|
4175
4262
|
expect(pr).toMatchObject({ reached: false, stoppedBecause: 'waits on a person: review_pr' });
|
|
@@ -4195,8 +4282,8 @@ describe('MCP server integration', () => {
|
|
|
4195
4282
|
const started = Date.now();
|
|
4196
4283
|
const result = parseToolResult(
|
|
4197
4284
|
await client.callTool({
|
|
4198
|
-
name: '
|
|
4199
|
-
arguments: {
|
|
4285
|
+
name: 'taiga_wait_for_initiative',
|
|
4286
|
+
arguments: { initiativeId: ITEM_ID, until: 'planned', timeoutSeconds: 1 },
|
|
4200
4287
|
})
|
|
4201
4288
|
);
|
|
4202
4289
|
|
|
@@ -4217,8 +4304,8 @@ describe('MCP server integration', () => {
|
|
|
4217
4304
|
});
|
|
4218
4305
|
|
|
4219
4306
|
const result = await client.callTool({
|
|
4220
|
-
name: '
|
|
4221
|
-
arguments: {
|
|
4307
|
+
name: 'taiga_wait_for_initiative',
|
|
4308
|
+
arguments: { initiativeId: ITEM_ID, until: 'planned', timeoutSeconds: 1 },
|
|
4222
4309
|
});
|
|
4223
4310
|
|
|
4224
4311
|
expect(result.isError).toBeFalsy();
|
|
@@ -4243,7 +4330,10 @@ describe('MCP server integration', () => {
|
|
|
4243
4330
|
});
|
|
4244
4331
|
|
|
4245
4332
|
const result = parseToolResult(
|
|
4246
|
-
await client.callTool({
|
|
4333
|
+
await client.callTool({
|
|
4334
|
+
name: 'taiga_wait_for_initiative',
|
|
4335
|
+
arguments: { initiativeId: ITEM_ID, until: 'changed' },
|
|
4336
|
+
})
|
|
4247
4337
|
);
|
|
4248
4338
|
|
|
4249
4339
|
expect(result).toMatchObject({ reached: true, item: { latestBuild: { prState: 'closed' } } });
|
|
@@ -4267,7 +4357,10 @@ describe('MCP server integration', () => {
|
|
|
4267
4357
|
const controller = new AbortController();
|
|
4268
4358
|
|
|
4269
4359
|
const pending = client.callTool(
|
|
4270
|
-
{
|
|
4360
|
+
{
|
|
4361
|
+
name: 'taiga_wait_for_initiative',
|
|
4362
|
+
arguments: { initiativeId: ITEM_ID, until: 'planned', timeoutSeconds: 60 },
|
|
4363
|
+
},
|
|
4271
4364
|
{ signal: controller.signal }
|
|
4272
4365
|
);
|
|
4273
4366
|
await vi.waitFor(() => expect(hung).toBeDefined());
|
|
@@ -4300,8 +4393,8 @@ describe('MCP server integration', () => {
|
|
|
4300
4393
|
});
|
|
4301
4394
|
const pr = parseToolResult(
|
|
4302
4395
|
await shipped.client.callTool({
|
|
4303
|
-
name: '
|
|
4304
|
-
arguments: {
|
|
4396
|
+
name: 'taiga_wait_for_initiative',
|
|
4397
|
+
arguments: { initiativeId: ITEM_ID, until: 'pr_open' },
|
|
4305
4398
|
})
|
|
4306
4399
|
);
|
|
4307
4400
|
expect(pr).toMatchObject({ reached: false, stoppedBecause: 'the initiative is done' });
|
|
@@ -4317,8 +4410,8 @@ describe('MCP server integration', () => {
|
|
|
4317
4410
|
});
|
|
4318
4411
|
const done = parseToolResult(
|
|
4319
4412
|
await dropped.client.callTool({
|
|
4320
|
-
name: '
|
|
4321
|
-
arguments: {
|
|
4413
|
+
name: 'taiga_wait_for_initiative',
|
|
4414
|
+
arguments: { initiativeId: ITEM_ID, until: 'done' },
|
|
4322
4415
|
})
|
|
4323
4416
|
);
|
|
4324
4417
|
expect(done).toMatchObject({ reached: false, stoppedBecause: 'the initiative is canceled' });
|
|
@@ -4337,8 +4430,8 @@ describe('MCP server integration', () => {
|
|
|
4337
4430
|
});
|
|
4338
4431
|
const runPr = parseToolResult(
|
|
4339
4432
|
await finished.client.callTool({
|
|
4340
|
-
name: '
|
|
4341
|
-
arguments: {
|
|
4433
|
+
name: 'taiga_wait_for_run',
|
|
4434
|
+
arguments: { runId: BUILD_ID, until: 'pr_open' },
|
|
4342
4435
|
})
|
|
4343
4436
|
);
|
|
4344
4437
|
expect(runPr).toMatchObject({ reached: false, stoppedBecause: 'the run is completed' });
|
|
@@ -4373,7 +4466,10 @@ describe('MCP server integration', () => {
|
|
|
4373
4466
|
});
|
|
4374
4467
|
|
|
4375
4468
|
const result = parseToolResult(
|
|
4376
|
-
await client.callTool({
|
|
4469
|
+
await client.callTool({
|
|
4470
|
+
name: 'taiga_wait_for_initiative',
|
|
4471
|
+
arguments: { initiativeId: ITEM_ID, until: 'pr_open' },
|
|
4472
|
+
})
|
|
4377
4473
|
);
|
|
4378
4474
|
|
|
4379
4475
|
// Not stopped at the landed plan: the open gate builds it without anyone.
|
|
@@ -4401,8 +4497,8 @@ describe('MCP server integration', () => {
|
|
|
4401
4497
|
|
|
4402
4498
|
const result = parseToolResult(
|
|
4403
4499
|
await client.callTool({
|
|
4404
|
-
name: '
|
|
4405
|
-
arguments: {
|
|
4500
|
+
name: 'taiga_wait_for_initiative',
|
|
4501
|
+
arguments: { initiativeId: ITEM_ID, until: 'your_turn' },
|
|
4406
4502
|
})
|
|
4407
4503
|
);
|
|
4408
4504
|
|
|
@@ -4435,8 +4531,8 @@ describe('MCP server integration', () => {
|
|
|
4435
4531
|
|
|
4436
4532
|
const result = parseToolResult(
|
|
4437
4533
|
await client.callTool({
|
|
4438
|
-
name: '
|
|
4439
|
-
arguments: {
|
|
4534
|
+
name: 'taiga_wait_for_initiative',
|
|
4535
|
+
arguments: { initiativeId: ITEM_ID, until: 'your_turn' },
|
|
4440
4536
|
})
|
|
4441
4537
|
);
|
|
4442
4538
|
|
|
@@ -4459,7 +4555,10 @@ describe('MCP server integration', () => {
|
|
|
4459
4555
|
});
|
|
4460
4556
|
|
|
4461
4557
|
const result = parseToolResult(
|
|
4462
|
-
await client.callTool({
|
|
4558
|
+
await client.callTool({
|
|
4559
|
+
name: 'taiga_wait_for_initiative',
|
|
4560
|
+
arguments: { initiativeId: ITEM_ID, until: 'changed' },
|
|
4561
|
+
})
|
|
4463
4562
|
);
|
|
4464
4563
|
|
|
4465
4564
|
expect(result).toMatchObject({ reached: true, item: { autoBuild: false } });
|
|
@@ -4473,8 +4572,8 @@ describe('MCP server integration', () => {
|
|
|
4473
4572
|
|
|
4474
4573
|
const result = parseToolResult(
|
|
4475
4574
|
await client.callTool({
|
|
4476
|
-
name: '
|
|
4477
|
-
arguments: {
|
|
4575
|
+
name: 'taiga_wait_for_initiative',
|
|
4576
|
+
arguments: { initiativeId: ITEM_ID, until: 'changed', timeoutSeconds: 1 },
|
|
4478
4577
|
})
|
|
4479
4578
|
);
|
|
4480
4579
|
|
|
@@ -4491,7 +4590,7 @@ describe('MCP server integration', () => {
|
|
|
4491
4590
|
const onprogress = vi.fn();
|
|
4492
4591
|
|
|
4493
4592
|
const result = await client.callTool(
|
|
4494
|
-
{ name: '
|
|
4593
|
+
{ name: 'taiga_wait_for_initiative', arguments: { initiativeId: ITEM_ID, until: 'changed' } },
|
|
4495
4594
|
{ onprogress }
|
|
4496
4595
|
);
|
|
4497
4596
|
|
|
@@ -4508,8 +4607,8 @@ describe('MCP server integration', () => {
|
|
|
4508
4607
|
});
|
|
4509
4608
|
const step = parseToolResult(
|
|
4510
4609
|
await stepped.client.callTool({
|
|
4511
|
-
name: '
|
|
4512
|
-
arguments: {
|
|
4610
|
+
name: 'taiga_wait_for_run',
|
|
4611
|
+
arguments: { runId: BUILD_ID, until: 'step_done' },
|
|
4513
4612
|
})
|
|
4514
4613
|
);
|
|
4515
4614
|
const failed = await bootMcpPair({
|
|
@@ -4518,8 +4617,8 @@ describe('MCP server integration', () => {
|
|
|
4518
4617
|
});
|
|
4519
4618
|
const settled = parseToolResult(
|
|
4520
4619
|
await failed.client.callTool({
|
|
4521
|
-
name: '
|
|
4522
|
-
arguments: {
|
|
4620
|
+
name: 'taiga_wait_for_run',
|
|
4621
|
+
arguments: { runId: BUILD_ID, until: 'pr_open' },
|
|
4523
4622
|
})
|
|
4524
4623
|
);
|
|
4525
4624
|
|
|
@@ -4538,8 +4637,8 @@ describe('MCP server integration', () => {
|
|
|
4538
4637
|
});
|
|
4539
4638
|
const result = parseToolResult(
|
|
4540
4639
|
await recovered.client.callTool({
|
|
4541
|
-
name: '
|
|
4542
|
-
arguments: {
|
|
4640
|
+
name: 'taiga_wait_for_initiative',
|
|
4641
|
+
arguments: { initiativeId: ITEM_ID, until: 'planned' },
|
|
4543
4642
|
})
|
|
4544
4643
|
);
|
|
4545
4644
|
expect(result.reached).toBe(true);
|
|
@@ -4550,8 +4649,8 @@ describe('MCP server integration', () => {
|
|
|
4550
4649
|
routes: { [`/v1/backlog/${ITEM_ID}`]: () => (calls++ === 0 ? answer({ data: item }) : failing) },
|
|
4551
4650
|
});
|
|
4552
4651
|
const failed = await broken.client.callTool({
|
|
4553
|
-
name: '
|
|
4554
|
-
arguments: {
|
|
4652
|
+
name: 'taiga_wait_for_initiative',
|
|
4653
|
+
arguments: { initiativeId: ITEM_ID, until: 'planned' },
|
|
4555
4654
|
});
|
|
4556
4655
|
expect(failed.isError).toBe(true);
|
|
4557
4656
|
expect(calls).toBe(4);
|
|
@@ -4565,8 +4664,8 @@ describe('MCP server integration', () => {
|
|
|
4565
4664
|
});
|
|
4566
4665
|
const stopped = parseToolResult(
|
|
4567
4666
|
await planning.client.callTool({
|
|
4568
|
-
name: '
|
|
4569
|
-
arguments: {
|
|
4667
|
+
name: 'taiga_wait_for_initiative',
|
|
4668
|
+
arguments: { initiativeId: ITEM_ID, until: 'planned' },
|
|
4570
4669
|
})
|
|
4571
4670
|
);
|
|
4572
4671
|
expect(stopped).toMatchObject({ reached: false, stoppedBecause: 'planning failed' });
|
|
@@ -4578,8 +4677,8 @@ describe('MCP server integration', () => {
|
|
|
4578
4677
|
});
|
|
4579
4678
|
const changed = parseToolResult(
|
|
4580
4679
|
await waiting.client.callTool({
|
|
4581
|
-
name: '
|
|
4582
|
-
arguments: {
|
|
4680
|
+
name: 'taiga_wait_for_initiative',
|
|
4681
|
+
arguments: { initiativeId: ITEM_ID, until: 'changed' },
|
|
4583
4682
|
})
|
|
4584
4683
|
);
|
|
4585
4684
|
expect(changed).toMatchObject({ reached: true, item: { state: 'plan', needsHumanCause: null } });
|
|
@@ -4595,8 +4694,8 @@ describe('MCP server integration', () => {
|
|
|
4595
4694
|
});
|
|
4596
4695
|
|
|
4597
4696
|
const result = await client.callTool({
|
|
4598
|
-
name: '
|
|
4599
|
-
arguments: {
|
|
4697
|
+
name: 'taiga_queue_initiative',
|
|
4698
|
+
arguments: { initiativeId: ITEM_ID, autoBuild: false },
|
|
4600
4699
|
});
|
|
4601
4700
|
|
|
4602
4701
|
expect(result.isError).toBe(true);
|
|
@@ -4614,7 +4713,7 @@ describe('MCP server integration', () => {
|
|
|
4614
4713
|
});
|
|
4615
4714
|
|
|
4616
4715
|
const result = parseToolResult(
|
|
4617
|
-
await client.callTool({ name: 'taiga_set_auto_build', arguments: {
|
|
4716
|
+
await client.callTool({ name: 'taiga_set_auto_build', arguments: { initiativeId: ITEM_ID, autoBuild: null } })
|
|
4618
4717
|
);
|
|
4619
4718
|
|
|
4620
4719
|
expect(result.reviewGate).toBeNull();
|
|
@@ -4628,7 +4727,7 @@ describe('MCP server integration', () => {
|
|
|
4628
4727
|
});
|
|
4629
4728
|
|
|
4630
4729
|
const result = parseToolResult(
|
|
4631
|
-
await client.callTool({ name: 'taiga_set_auto_build', arguments: {
|
|
4730
|
+
await client.callTool({ name: 'taiga_set_auto_build', arguments: { initiativeId: ITEM_ID, autoBuild: false } })
|
|
4632
4731
|
);
|
|
4633
4732
|
|
|
4634
4733
|
expect(result.reviewGate).toMatchObject({ buildsWithoutReview: false, decidedBy: 'initiative' });
|
|
@@ -4653,17 +4752,17 @@ describe('MCP server integration', () => {
|
|
|
4653
4752
|
});
|
|
4654
4753
|
|
|
4655
4754
|
const [failed, refused, unknown] = parseToolResult(
|
|
4656
|
-
await client.callTool({ name: '
|
|
4755
|
+
await client.callTool({ name: 'taiga_get_line', arguments: { productId: PRODUCT_ID } })
|
|
4657
4756
|
).entries;
|
|
4658
4757
|
|
|
4659
4758
|
expect(failed.halt.nextStep).toMatch(/^It clears when the run is retried/);
|
|
4660
4759
|
expect(refused.halt).toMatchObject({
|
|
4661
4760
|
reason: 'No seats',
|
|
4662
|
-
nextStep: expect.stringMatching(/^
|
|
4761
|
+
nextStep: expect.stringMatching(/^taiga_resume_initiative lifts it/),
|
|
4663
4762
|
});
|
|
4664
4763
|
expect(unknown.halt).toMatchObject({
|
|
4665
4764
|
reason: 'Stored prose',
|
|
4666
|
-
nextStep: expect.stringContaining('
|
|
4765
|
+
nextStep: expect.stringContaining('taiga_get_initiative'),
|
|
4667
4766
|
});
|
|
4668
4767
|
});
|
|
4669
4768
|
|
|
@@ -4678,7 +4777,7 @@ describe('MCP server integration', () => {
|
|
|
4678
4777
|
},
|
|
4679
4778
|
});
|
|
4680
4779
|
|
|
4681
|
-
const result = await client.callTool({ name: '
|
|
4780
|
+
const result = await client.callTool({ name: 'taiga_get_line', arguments: { productId: PRODUCT_ID } });
|
|
4682
4781
|
|
|
4683
4782
|
expect(result.isError).toBe(true);
|
|
4684
4783
|
expect((result.content as { text: string }[])[0]!.text).toContain('a bug, not the API');
|
|
@@ -4815,14 +4914,10 @@ describe('MCP server integration', () => {
|
|
|
4815
4914
|
// One shape whatever matched (PRO-2778): none right still answers, listing every real id with its size.
|
|
4816
4915
|
expect(miss.isError).toBeFalsy();
|
|
4817
4916
|
const missed = parseToolResult(miss);
|
|
4818
|
-
|
|
4917
|
+
// No text matched, so no empty data.sections beside the ids (PRO-2800).
|
|
4918
|
+
expect(missed).not.toHaveProperty('data');
|
|
4819
4919
|
expect(missed.unknownSections).toEqual(['nope']);
|
|
4820
|
-
expect(missed.
|
|
4821
|
-
'overview',
|
|
4822
|
-
'features',
|
|
4823
|
-
'flows',
|
|
4824
|
-
'risks',
|
|
4825
|
-
]);
|
|
4920
|
+
expect(missed.outline.map((s: { id: string }) => s.id)).toEqual(['overview', 'features', 'flows', 'risks']);
|
|
4826
4921
|
});
|
|
4827
4922
|
|
|
4828
4923
|
it('should read a product document as its outline with sections: []', async () => {
|
|
@@ -4835,8 +4930,8 @@ describe('MCP server integration', () => {
|
|
|
4835
4930
|
})
|
|
4836
4931
|
);
|
|
4837
4932
|
|
|
4838
|
-
expect(outline.
|
|
4839
|
-
expect(outline.
|
|
4933
|
+
expect(outline).not.toHaveProperty('data');
|
|
4934
|
+
expect(outline.outline).toEqual([
|
|
4840
4935
|
{ id: 'overview', characters: 5_000 },
|
|
4841
4936
|
{ id: 'features', characters: expect.any(Number) },
|
|
4842
4937
|
{ id: 'flows', characters: 5_000 },
|
|
@@ -4999,7 +5094,7 @@ describe('MCP server integration', () => {
|
|
|
4999
5094
|
arguments: { productId: PRODUCT_ID, findingId: FINDING_ID },
|
|
5000
5095
|
})
|
|
5001
5096
|
);
|
|
5002
|
-
expect(one).toMatchObject({
|
|
5097
|
+
expect(one).toMatchObject({ initiativeId: ITEM_ID, finding: { id: FINDING_ID, status: 'remediating' } });
|
|
5003
5098
|
expect(one.finding).not.toHaveProperty('fingerprint');
|
|
5004
5099
|
|
|
5005
5100
|
const group = parseToolResult(
|
|
@@ -5015,7 +5110,7 @@ describe('MCP server integration', () => {
|
|
|
5015
5110
|
currentVersion: '4.17.20',
|
|
5016
5111
|
productRepositoryId: repoId,
|
|
5017
5112
|
});
|
|
5018
|
-
expect(group).toEqual({
|
|
5113
|
+
expect(group).toEqual({ initiativeId: ITEM_ID, findingIds: [FINDING_ID] });
|
|
5019
5114
|
|
|
5020
5115
|
// A finding of another product is not fixed as a group of this one.
|
|
5021
5116
|
const before = callsTo(fetchSpy, 'POST').length;
|
|
@@ -5121,7 +5216,7 @@ describe('MCP server integration', () => {
|
|
|
5121
5216
|
arguments: { productId: PRODUCT_ID, controlId: 'SEC-3.4', violationId: VIOLATION_ID },
|
|
5122
5217
|
});
|
|
5123
5218
|
|
|
5124
|
-
expect(control).toEqual({
|
|
5219
|
+
expect(control).toEqual({ initiativeId: ITEM_ID, divergenceIds: [VIOLATION_ID], created: true });
|
|
5125
5220
|
expect(callsTo(fetchSpy, 'POST').map((u) => u.pathname)).toEqual([
|
|
5126
5221
|
`/v1/products/${PRODUCT_ID}/maintenance/policies/controls/SEC-3.4/remediate`,
|
|
5127
5222
|
`/v1/products/${PRODUCT_ID}/maintenance/policies/divergences/${VIOLATION_ID}/remediate`,
|
|
@@ -5179,17 +5274,17 @@ describe('MCP server integration', () => {
|
|
|
5179
5274
|
expect(product.searchParams.has('resolved')).toBe(false);
|
|
5180
5275
|
|
|
5181
5276
|
const done = parseToolResult(
|
|
5182
|
-
await client.callTool({ name: 'taiga_resolve_action', arguments: {
|
|
5277
|
+
await client.callTool({ name: 'taiga_resolve_action', arguments: { actionId: INBOX_ID, status: 'done' } })
|
|
5183
5278
|
);
|
|
5184
5279
|
expect(done.status).toBe('done');
|
|
5185
5280
|
expect(sentBody(fetchSpy, 'PATCH', '/inbox/')).toEqual({ status: 'done' });
|
|
5186
5281
|
|
|
5187
5282
|
fetchSpy.mockClear();
|
|
5188
5283
|
vi.stubGlobal('fetch', fetchSpy);
|
|
5189
|
-
await client.callTool({ name: 'taiga_resolve_action', arguments: {
|
|
5284
|
+
await client.callTool({ name: 'taiga_resolve_action', arguments: { actionId: INBOX_ID, snoozedUntil: null } });
|
|
5190
5285
|
expect(sentBody(fetchSpy, 'PATCH', '/inbox/')).toEqual({ snoozedUntil: null });
|
|
5191
5286
|
|
|
5192
|
-
const nothing = await client.callTool({ name: 'taiga_resolve_action', arguments: {
|
|
5287
|
+
const nothing = await client.callTool({ name: 'taiga_resolve_action', arguments: { actionId: INBOX_ID } });
|
|
5193
5288
|
expect(nothing.isError).toBe(true);
|
|
5194
5289
|
});
|
|
5195
5290
|
|
|
@@ -5201,7 +5296,7 @@ describe('MCP server integration', () => {
|
|
|
5201
5296
|
commitSha: 'abc',
|
|
5202
5297
|
commitSubject: 'Merge #1',
|
|
5203
5298
|
artifacts: {},
|
|
5204
|
-
metadata: { big: true },
|
|
5299
|
+
metadata: { big: true, repositoryFullName: 'acme/api' },
|
|
5205
5300
|
};
|
|
5206
5301
|
const { client, fetchSpy } = await bootMcpPair({
|
|
5207
5302
|
capabilities: ['products:read'],
|
|
@@ -5246,8 +5341,11 @@ describe('MCP server integration', () => {
|
|
|
5246
5341
|
});
|
|
5247
5342
|
expect(result.environments.map((e: { id: string }) => e.id)).toEqual([ENV_ID, ENV_ID_2]);
|
|
5248
5343
|
expect(result).not.toHaveProperty('moreEnvironments');
|
|
5249
|
-
|
|
5344
|
+
// Named by the repository that deployed it: each runs its own in an
|
|
5345
|
+
// environment (PRO-2528). The rest of the metadata stays behind.
|
|
5346
|
+
expect(result.running[0]).toMatchObject({ id: 'd1', commitSubject: 'Merge #1', repository: 'acme/api' });
|
|
5250
5347
|
expect(result.running[0]).not.toHaveProperty('metadata');
|
|
5348
|
+
expect(result.history.data[0]).toMatchObject({ id: 'd1', repository: 'acme/api' });
|
|
5251
5349
|
expect(result.history).toMatchObject({ nextCursor: 'more' });
|
|
5252
5350
|
expect(result.syncStatus).toBeNull();
|
|
5253
5351
|
expect(result.syncUnavailable).toContain('500');
|
|
@@ -5507,7 +5605,7 @@ describe('MCP server integration', () => {
|
|
|
5507
5605
|
expect(product).not.toHaveProperty('settings');
|
|
5508
5606
|
|
|
5509
5607
|
const item = parseToolResult(
|
|
5510
|
-
await client.callTool({ name: 'taiga_set_auto_merge', arguments: {
|
|
5608
|
+
await client.callTool({ name: 'taiga_set_auto_merge', arguments: { initiativeId: ITEM_ID, autoMerge: false } })
|
|
5511
5609
|
);
|
|
5512
5610
|
expect(sentBody(fetchSpy, 'PATCH', `/backlog/${ITEM_ID}`)).toEqual({ autoMerge: false });
|
|
5513
5611
|
expect(item).not.toHaveProperty('description');
|
|
@@ -6328,11 +6426,11 @@ describe('what the agent review found (PRO-2759)', () => {
|
|
|
6328
6426
|
},
|
|
6329
6427
|
});
|
|
6330
6428
|
|
|
6331
|
-
const item = await client.callTool({ name: '
|
|
6429
|
+
const item = await client.callTool({ name: 'taiga_get_initiative', arguments: { initiativeId: ITEM_ID } });
|
|
6332
6430
|
expect(parseToolResult(item)).toEqual({
|
|
6333
6431
|
error: 'NOT_FOUND',
|
|
6334
6432
|
message:
|
|
6335
|
-
"Backlog item not found.
|
|
6433
|
+
"Backlog item not found. taiga_list_initiatives lists a product's initiatives with their IDs; a run or " +
|
|
6336
6434
|
'plan ID is not one.',
|
|
6337
6435
|
});
|
|
6338
6436
|
|
|
@@ -6340,10 +6438,34 @@ describe('what the agent review found (PRO-2759)', () => {
|
|
|
6340
6438
|
name: 'taiga_list_repository_branches',
|
|
6341
6439
|
arguments: { productId: PRODUCT_ID, repositoryId: LINK_ID },
|
|
6342
6440
|
});
|
|
6441
|
+
// The API names the link, so only the link is hinted: the product the call
|
|
6442
|
+
// also named was right, and its hint read as blaming it (PRO-2800).
|
|
6343
6443
|
expect(parseToolResult(branches).message).toBe(
|
|
6344
6444
|
"Repository link not found. taiga_list_product_repositories lists the product's repository links; a GitHub " +
|
|
6345
|
-
'repository id is not a link ID.
|
|
6346
|
-
|
|
6445
|
+
'repository id is not a link ID.'
|
|
6446
|
+
);
|
|
6447
|
+
});
|
|
6448
|
+
|
|
6449
|
+
it('hints every ID the call named when the 404 does not say which one is missing (PRO-2800)', async () => {
|
|
6450
|
+
const { client } = await bootMcpPair({
|
|
6451
|
+
capabilities: ALL_CAPABILITIES,
|
|
6452
|
+
routes: {
|
|
6453
|
+
[`GET /v1/products/${PRODUCT_ID}/repositories/${LINK_ID}/branches`]: refusal(
|
|
6454
|
+
404,
|
|
6455
|
+
'NOT_FOUND',
|
|
6456
|
+
'The requested resource does not exist'
|
|
6457
|
+
),
|
|
6458
|
+
},
|
|
6459
|
+
});
|
|
6460
|
+
|
|
6461
|
+
const branches = await client.callTool({
|
|
6462
|
+
name: 'taiga_list_repository_branches',
|
|
6463
|
+
arguments: { productId: PRODUCT_ID, repositoryId: LINK_ID },
|
|
6464
|
+
});
|
|
6465
|
+
expect(parseToolResult(branches).message).toBe(
|
|
6466
|
+
"The requested resource does not exist. taiga_list_product_repositories lists the product's repository " +
|
|
6467
|
+
'links; a GitHub repository id is not a link ID. taiga_list_products lists the products the key can see; ' +
|
|
6468
|
+
'a name is not an ID (taiga_get_product takes a slug).'
|
|
6347
6469
|
);
|
|
6348
6470
|
});
|
|
6349
6471
|
|
|
@@ -6357,12 +6479,16 @@ describe('what the agent review found (PRO-2759)', () => {
|
|
|
6357
6479
|
},
|
|
6358
6480
|
});
|
|
6359
6481
|
|
|
6360
|
-
const noPlan = parseToolResult(
|
|
6361
|
-
|
|
6482
|
+
const noPlan = parseToolResult(
|
|
6483
|
+
await client.callTool({ name: 'taiga_get_plan', arguments: { initiativeId: ITEM_ID } })
|
|
6484
|
+
);
|
|
6485
|
+
const noItem = parseToolResult(
|
|
6486
|
+
await client.callTool({ name: 'taiga_get_plan', arguments: { initiativeId: OTHER } })
|
|
6487
|
+
);
|
|
6362
6488
|
|
|
6363
6489
|
expect(noPlan.message).toContain('taiga_plan_again asks for a new one');
|
|
6364
6490
|
expect(noItem.message).toBe(
|
|
6365
|
-
"Backlog item not found.
|
|
6491
|
+
"Backlog item not found. taiga_list_initiatives lists a product's initiatives with their IDs; a run or " +
|
|
6366
6492
|
'plan ID is not one.'
|
|
6367
6493
|
);
|
|
6368
6494
|
});
|
|
@@ -6376,9 +6502,7 @@ describe('what the agent review found (PRO-2759)', () => {
|
|
|
6376
6502
|
},
|
|
6377
6503
|
});
|
|
6378
6504
|
|
|
6379
|
-
const result = parseToolResult(
|
|
6380
|
-
await client.callTool({ name: 'taiga_get_build', arguments: { buildId: BUILD_ID } })
|
|
6381
|
-
);
|
|
6505
|
+
const result = parseToolResult(await client.callTool({ name: 'taiga_get_run', arguments: { runId: BUILD_ID } }));
|
|
6382
6506
|
|
|
6383
6507
|
expect(result.stoppedIn).toMatch(/^before the first step started/);
|
|
6384
6508
|
});
|
|
@@ -6386,7 +6510,7 @@ describe('what the agent review found (PRO-2759)', () => {
|
|
|
6386
6510
|
it('says where a product ID comes from when a name is passed for one', async () => {
|
|
6387
6511
|
const { client } = await bootMcpPair({ capabilities: ALL_CAPABILITIES });
|
|
6388
6512
|
|
|
6389
|
-
const result = await client.callTool({ name: '
|
|
6513
|
+
const result = await client.callTool({ name: 'taiga_list_initiatives', arguments: { productId: 'Web shop' } });
|
|
6390
6514
|
|
|
6391
6515
|
expect(result.isError).toBe(true);
|
|
6392
6516
|
expect((result.content as { text: string }[])[0]!.text).toContain(
|
|
@@ -6452,12 +6576,12 @@ describe('what the agent review found (PRO-2759)', () => {
|
|
|
6452
6576
|
// One shape whatever matched (PRO-2778): every real id with its size, the miss named.
|
|
6453
6577
|
expect(none.isError).toBeFalsy();
|
|
6454
6578
|
const missed = parseToolResult(none);
|
|
6455
|
-
expect(missed.
|
|
6579
|
+
expect(missed).not.toHaveProperty('data');
|
|
6456
6580
|
expect(missed.unknownSections).toEqual(['secrets']);
|
|
6457
|
-
expect(missed.
|
|
6581
|
+
expect(missed.outline.map((s: { id: string }) => s.id)).toEqual(['summary', 'scope']);
|
|
6458
6582
|
});
|
|
6459
6583
|
|
|
6460
|
-
it('says what a product in setup
|
|
6584
|
+
it('says what a product in setup still needs, from what is linked and what the API would refuse on (PRO-2800)', async () => {
|
|
6461
6585
|
const draft = {
|
|
6462
6586
|
id: PRODUCT_ID,
|
|
6463
6587
|
name: 'Web',
|
|
@@ -6469,29 +6593,46 @@ describe('what the agent review found (PRO-2759)', () => {
|
|
|
6469
6593
|
ownsInfrastructure: false,
|
|
6470
6594
|
deploymentNotes: null,
|
|
6471
6595
|
};
|
|
6596
|
+
const blockers = { data: { inSetup: true, missingDocuments: ['specification', 'architecture'], openChecks: [] } };
|
|
6597
|
+
let links: unknown[] = [];
|
|
6472
6598
|
const { client } = await bootMcpPair({
|
|
6473
6599
|
capabilities: ALL_CAPABILITIES,
|
|
6474
6600
|
routes: {
|
|
6475
6601
|
[`GET /v1/products/${PRODUCT_ID}`]: { data: draft },
|
|
6476
6602
|
[`PATCH /v1/products/${PRODUCT_ID}`]: { data: draft },
|
|
6603
|
+
[`GET /v1/products/${PRODUCT_ID}/setup`]: blockers,
|
|
6604
|
+
[`GET /v1/products/${PRODUCT_ID}/repositories`]: () => ({
|
|
6605
|
+
ok: true,
|
|
6606
|
+
status: 200,
|
|
6607
|
+
json: async () => ({ data: links, nextCursor: null }),
|
|
6608
|
+
text: async () => JSON.stringify({ data: links, nextCursor: null }),
|
|
6609
|
+
}),
|
|
6477
6610
|
},
|
|
6478
6611
|
});
|
|
6479
6612
|
|
|
6480
6613
|
const product = parseToolResult(
|
|
6481
6614
|
await client.callTool({ name: 'taiga_get_product', arguments: { productId: PRODUCT_ID } })
|
|
6482
6615
|
);
|
|
6483
|
-
expect(product.
|
|
6484
|
-
|
|
6485
|
-
|
|
6486
|
-
|
|
6487
|
-
|
|
6616
|
+
expect(product.readiness).toEqual({
|
|
6617
|
+
ready: false,
|
|
6618
|
+
repository: { linked: false },
|
|
6619
|
+
import: 'not started',
|
|
6620
|
+
missingDocuments: ['Specification', 'Architecture'],
|
|
6621
|
+
openChecks: [],
|
|
6622
|
+
nextStep:
|
|
6623
|
+
'Link its repository (taiga_list_linkable_repositories, then taiga_link_repository), then taiga_start_import.',
|
|
6624
|
+
});
|
|
6625
|
+
|
|
6626
|
+
// Linked: the next move follows, which the step alone never said.
|
|
6627
|
+
links = [{ id: LINK_ID, repositoryName: 'acme/web', effectiveBranch: 'main' }];
|
|
6488
6628
|
const updated = parseToolResult(
|
|
6489
6629
|
await client.callTool({
|
|
6490
6630
|
name: 'taiga_update_product',
|
|
6491
6631
|
arguments: { productId: PRODUCT_ID, ownsInfrastructure: false },
|
|
6492
6632
|
})
|
|
6493
6633
|
);
|
|
6494
|
-
expect(updated.
|
|
6634
|
+
expect(updated.readiness.repository).toEqual({ linked: true, name: 'acme/web', branch: 'main' });
|
|
6635
|
+
expect(updated.readiness.nextStep).toMatch(/^taiga_start_import derives the documents from the code/);
|
|
6495
6636
|
expect(updated.warnings).toEqual([expect.stringContaining('has no deploymentNotes')]);
|
|
6496
6637
|
});
|
|
6497
6638
|
|
|
@@ -6506,16 +6647,56 @@ describe('what the agent review found (PRO-2759)', () => {
|
|
|
6506
6647
|
},
|
|
6507
6648
|
});
|
|
6508
6649
|
|
|
6650
|
+
// An API without the setup read (404 here): the step is what there is to go on.
|
|
6509
6651
|
const read = parseToolResult(
|
|
6510
6652
|
await client.callTool({ name: 'taiga_update_product', arguments: { productId: PRODUCT_ID, name: 'Web' } })
|
|
6511
6653
|
);
|
|
6512
|
-
// The product's read is unreachable
|
|
6654
|
+
// The product's read is unreachable too: no next step, not the conversation's.
|
|
6513
6655
|
const unread = parseToolResult(
|
|
6514
6656
|
await client.callTool({ name: 'taiga_update_product', arguments: { productId: PRODUCT_ID_2, name: 'Web' } })
|
|
6515
6657
|
);
|
|
6516
6658
|
|
|
6517
|
-
expect(read.
|
|
6518
|
-
expect(
|
|
6659
|
+
expect(read.readiness.ready).toBeNull();
|
|
6660
|
+
expect(read.readiness.nextStep).toMatch(/^The specification is done/);
|
|
6661
|
+
expect(unread.readiness.nextStep).toBeNull();
|
|
6662
|
+
expect(unread.readiness.repository).toBeNull();
|
|
6663
|
+
});
|
|
6664
|
+
|
|
6665
|
+
it('says what a scratch product still needs, from the Discovery conversation to the last document (PRO-2800)', async () => {
|
|
6666
|
+
const draft = {
|
|
6667
|
+
id: PRODUCT_ID,
|
|
6668
|
+
name: 'Web',
|
|
6669
|
+
slug: 'web',
|
|
6670
|
+
status: 'draft',
|
|
6671
|
+
factoryId: FACTORY_ID,
|
|
6672
|
+
discoveryStep: 'documents',
|
|
6673
|
+
settings: {},
|
|
6674
|
+
};
|
|
6675
|
+
let missing = ['specification', 'architecture'];
|
|
6676
|
+
const { client } = await bootMcpPair({
|
|
6677
|
+
capabilities: ALL_CAPABILITIES,
|
|
6678
|
+
routes: {
|
|
6679
|
+
[`GET /v1/products/${PRODUCT_ID}`]: { data: draft },
|
|
6680
|
+
[`GET /v1/products/${PRODUCT_ID}/repositories`]: { data: [], nextCursor: null },
|
|
6681
|
+
[`GET /v1/products/${PRODUCT_ID}/setup`]: () => {
|
|
6682
|
+
const body = { data: { inSetup: true, missingDocuments: missing, openChecks: [] } };
|
|
6683
|
+
return { ok: true, status: 200, json: async () => body, text: async () => JSON.stringify(body) };
|
|
6684
|
+
},
|
|
6685
|
+
},
|
|
6686
|
+
});
|
|
6687
|
+
const read = async () =>
|
|
6688
|
+
parseToolResult(await client.callTool({ name: 'taiga_get_product', arguments: { productId: PRODUCT_ID } }))
|
|
6689
|
+
.readiness;
|
|
6690
|
+
|
|
6691
|
+
expect((await read()).nextStep).toMatch(
|
|
6692
|
+
/^The specification comes from the Discovery conversation, held in the web/
|
|
6693
|
+
);
|
|
6694
|
+
missing = ['architecture'];
|
|
6695
|
+
expect((await read()).nextStep).toMatch(/^Still to publish: Architecture\. taiga_generate_product_documents/);
|
|
6696
|
+
missing = [];
|
|
6697
|
+
const done = await read();
|
|
6698
|
+
expect(done.ready).toBe(true);
|
|
6699
|
+
expect(done.nextStep).toMatch(/^Every required document is published: taiga_finish_discovery/);
|
|
6519
6700
|
});
|
|
6520
6701
|
|
|
6521
6702
|
it('says nothing of where a merged run stopped, whatever its stale status says', async () => {
|
|
@@ -6529,9 +6710,7 @@ describe('what the agent review found (PRO-2759)', () => {
|
|
|
6529
6710
|
},
|
|
6530
6711
|
});
|
|
6531
6712
|
|
|
6532
|
-
const result = parseToolResult(
|
|
6533
|
-
await client.callTool({ name: 'taiga_get_build', arguments: { buildId: BUILD_ID } })
|
|
6534
|
-
);
|
|
6713
|
+
const result = parseToolResult(await client.callTool({ name: 'taiga_get_run', arguments: { runId: BUILD_ID } }));
|
|
6535
6714
|
|
|
6536
6715
|
expect(result).not.toHaveProperty('stoppedIn');
|
|
6537
6716
|
});
|
|
@@ -6568,9 +6747,7 @@ describe('what the agent review found (PRO-2759)', () => {
|
|
|
6568
6747
|
},
|
|
6569
6748
|
});
|
|
6570
6749
|
|
|
6571
|
-
const result = parseToolResult(
|
|
6572
|
-
await client.callTool({ name: 'taiga_get_build', arguments: { buildId: BUILD_ID } })
|
|
6573
|
-
);
|
|
6750
|
+
const result = parseToolResult(await client.callTool({ name: 'taiga_get_run', arguments: { runId: BUILD_ID } }));
|
|
6574
6751
|
|
|
6575
6752
|
expect(result.stoppedIn).toMatch(/^after the last step, in hardening/);
|
|
6576
6753
|
});
|
|
@@ -6704,7 +6881,7 @@ describe('what the second agent review found (PRO-2778, PRO-2780)', () => {
|
|
|
6704
6881
|
});
|
|
6705
6882
|
|
|
6706
6883
|
const item = parseToolResult(
|
|
6707
|
-
await client.callTool({ name: '
|
|
6884
|
+
await client.callTool({ name: 'taiga_get_initiative', arguments: { initiativeId: ITEM_ID } })
|
|
6708
6885
|
);
|
|
6709
6886
|
expect(item.planFailure).toEqual({
|
|
6710
6887
|
cause: 'cancelled',
|
|
@@ -6721,7 +6898,7 @@ describe('what the second agent review found (PRO-2778, PRO-2780)', () => {
|
|
|
6721
6898
|
},
|
|
6722
6899
|
});
|
|
6723
6900
|
const bare = parseToolResult(
|
|
6724
|
-
await refused.client.callTool({ name: '
|
|
6901
|
+
await refused.client.callTool({ name: 'taiga_get_initiative', arguments: { initiativeId: ITEM_ID } })
|
|
6725
6902
|
);
|
|
6726
6903
|
expect(bare).toEqual(failed);
|
|
6727
6904
|
|
|
@@ -6734,7 +6911,7 @@ describe('what the second agent review found (PRO-2778, PRO-2780)', () => {
|
|
|
6734
6911
|
},
|
|
6735
6912
|
});
|
|
6736
6913
|
const staleItem = parseToolResult(
|
|
6737
|
-
await stale.client.callTool({ name: '
|
|
6914
|
+
await stale.client.callTool({ name: 'taiga_get_initiative', arguments: { initiativeId: ITEM_ID } })
|
|
6738
6915
|
);
|
|
6739
6916
|
expect(staleItem.planFailure).toEqual({
|
|
6740
6917
|
cause: 'worker_lost',
|
|
@@ -6763,7 +6940,9 @@ describe('what the second agent review found (PRO-2778, PRO-2780)', () => {
|
|
|
6763
6940
|
},
|
|
6764
6941
|
});
|
|
6765
6942
|
expect(
|
|
6766
|
-
parseToolResult(
|
|
6943
|
+
parseToolResult(
|
|
6944
|
+
await retried.client.callTool({ name: 'taiga_get_initiative', arguments: { initiativeId: ITEM_ID } })
|
|
6945
|
+
)
|
|
6767
6946
|
).toEqual(failed);
|
|
6768
6947
|
|
|
6769
6948
|
// No failure in the history, or no history reachable: the item as it is.
|
|
@@ -6775,7 +6954,9 @@ describe('what the second agent review found (PRO-2778, PRO-2780)', () => {
|
|
|
6775
6954
|
},
|
|
6776
6955
|
});
|
|
6777
6956
|
expect(
|
|
6778
|
-
parseToolResult(
|
|
6957
|
+
parseToolResult(
|
|
6958
|
+
await silent.client.callTool({ name: 'taiga_get_initiative', arguments: { initiativeId: ITEM_ID } })
|
|
6959
|
+
)
|
|
6779
6960
|
).toEqual(failed);
|
|
6780
6961
|
const offline = await bootMcpPair({
|
|
6781
6962
|
capabilities: ALL_CAPABILITIES,
|
|
@@ -6786,7 +6967,9 @@ describe('what the second agent review found (PRO-2778, PRO-2780)', () => {
|
|
|
6786
6967
|
},
|
|
6787
6968
|
});
|
|
6788
6969
|
expect(
|
|
6789
|
-
parseToolResult(
|
|
6970
|
+
parseToolResult(
|
|
6971
|
+
await offline.client.callTool({ name: 'taiga_get_initiative', arguments: { initiativeId: ITEM_ID } })
|
|
6972
|
+
)
|
|
6790
6973
|
).toEqual(failed);
|
|
6791
6974
|
|
|
6792
6975
|
// A plan that did not fail reads no history.
|
|
@@ -6794,7 +6977,7 @@ describe('what the second agent review found (PRO-2778, PRO-2780)', () => {
|
|
|
6794
6977
|
capabilities: ALL_CAPABILITIES,
|
|
6795
6978
|
routes: { [`GET /v1/backlog/${ITEM_ID}`]: { data: { ...failed, planningStatus: 'planned' } } },
|
|
6796
6979
|
});
|
|
6797
|
-
await planned.client.callTool({ name: '
|
|
6980
|
+
await planned.client.callTool({ name: 'taiga_get_initiative', arguments: { initiativeId: ITEM_ID } });
|
|
6798
6981
|
expect(planned.fetchSpy.mock.calls.some((c: unknown[]) => String(c[0]).includes('/activity'))).toBe(false);
|
|
6799
6982
|
});
|
|
6800
6983
|
|
|
@@ -6846,7 +7029,7 @@ describe('what the second agent review found (PRO-2778, PRO-2780)', () => {
|
|
|
6846
7029
|
const { tools } = await client.listTools();
|
|
6847
7030
|
const metrics = tools.find((t) => t.name === 'taiga_get_delivery_metrics')!;
|
|
6848
7031
|
const properties = metrics.inputSchema.properties as Record<string, { enum?: unknown }>;
|
|
6849
|
-
expect(properties
|
|
7032
|
+
expect(properties['windowDays']!.enum).toEqual([30, 60, 90]);
|
|
6850
7033
|
});
|
|
6851
7034
|
|
|
6852
7035
|
it('opens every writing tool with what it does, and leaves the reads as they were', async () => {
|
|
@@ -6861,7 +7044,7 @@ describe('what the second agent review found (PRO-2778, PRO-2780)', () => {
|
|
|
6861
7044
|
expect(description, tool.name).toMatch(/^Acts on paid or shared work: ask the person first\. /);
|
|
6862
7045
|
else expect(description, tool.name).toMatch(/^Writes\. /);
|
|
6863
7046
|
}
|
|
6864
|
-
expect(tools.find((t) => t.name === '
|
|
7047
|
+
expect(tools.find((t) => t.name === 'taiga_queue_initiative')!.description).toMatch(/^Acts on paid/);
|
|
6865
7048
|
expect(tools.find((t) => t.name === 'taiga_create_factory')!.description).toMatch(/^Writes\. /);
|
|
6866
7049
|
});
|
|
6867
7050
|
|
|
@@ -6900,3 +7083,359 @@ describe('what the second agent review found (PRO-2778, PRO-2780)', () => {
|
|
|
6900
7083
|
expect(standing.controls[0].findingsByTier).toEqual(tiers);
|
|
6901
7084
|
});
|
|
6902
7085
|
});
|
|
7086
|
+
|
|
7087
|
+
describe('what the third agent review found (PRO-2800)', () => {
|
|
7088
|
+
beforeEach(() => {
|
|
7089
|
+
vi.restoreAllMocks();
|
|
7090
|
+
});
|
|
7091
|
+
|
|
7092
|
+
it('loads the three entry tools upfront in Claude Code, and only those', async () => {
|
|
7093
|
+
const { client } = await bootMcpPair({ capabilities: ALL_CAPABILITIES });
|
|
7094
|
+
|
|
7095
|
+
const { tools } = await client.listTools();
|
|
7096
|
+
const upfront = tools.filter((tool) => tool._meta?.['anthropic/alwaysLoad'] === true).map((tool) => tool.name);
|
|
7097
|
+
|
|
7098
|
+
expect(upfront.sort()).toEqual(['taiga_get_product_context', 'taiga_list_action_required', 'taiga_list_products']);
|
|
7099
|
+
});
|
|
7100
|
+
|
|
7101
|
+
it('opens Action Required with what is open by product and kind, rows without their metadata unless asked', async () => {
|
|
7102
|
+
const created = new Date(Date.now() - 3 * 24 * 60 * 60 * 1000).toISOString();
|
|
7103
|
+
const rows = [
|
|
7104
|
+
{
|
|
7105
|
+
id: 'i1',
|
|
7106
|
+
productId: PRODUCT_ID,
|
|
7107
|
+
productName: 'Web',
|
|
7108
|
+
kind: 'review_pr',
|
|
7109
|
+
status: 'open',
|
|
7110
|
+
title: 'Review & merge PR #9',
|
|
7111
|
+
createdAt: created,
|
|
7112
|
+
metadata: { prUrl: 'https://github.com/acme/web/pull/9', prNumber: 9 },
|
|
7113
|
+
},
|
|
7114
|
+
{
|
|
7115
|
+
id: 'i2',
|
|
7116
|
+
productId: PRODUCT_ID,
|
|
7117
|
+
productName: 'Web',
|
|
7118
|
+
kind: 'review_doc',
|
|
7119
|
+
status: 'open',
|
|
7120
|
+
createdAt: created,
|
|
7121
|
+
metadata: { decision: { detail: 'x'.repeat(2_000) } },
|
|
7122
|
+
},
|
|
7123
|
+
{ id: 'i3', productId: PRODUCT_ID_2, productName: 'Api', kind: 'review_pr', status: 'open', createdAt: created },
|
|
7124
|
+
];
|
|
7125
|
+
// The API pages: a short page says there is more, so the summary reads up to 100 itself.
|
|
7126
|
+
const inbox = (url: string) => {
|
|
7127
|
+
const limit = Number(new URL(url).searchParams.get('limit') ?? 25);
|
|
7128
|
+
const body = { data: rows.slice(0, limit), nextCursor: limit < rows.length ? 'more' : null, totalCount: 3 };
|
|
7129
|
+
return { ok: true, status: 200, json: async () => body, text: async () => JSON.stringify(body) };
|
|
7130
|
+
};
|
|
7131
|
+
const { client, fetchSpy } = await bootMcpPair({
|
|
7132
|
+
capabilities: ['products:read'],
|
|
7133
|
+
routes: { 'GET /v1/inbox': inbox },
|
|
7134
|
+
});
|
|
7135
|
+
|
|
7136
|
+
const list = parseToolResult(
|
|
7137
|
+
await client.callTool({ name: 'taiga_list_action_required', arguments: { limit: 2 } })
|
|
7138
|
+
);
|
|
7139
|
+
const full = parseToolResult(
|
|
7140
|
+
await client.callTool({ name: 'taiga_list_action_required', arguments: { full: true } })
|
|
7141
|
+
);
|
|
7142
|
+
|
|
7143
|
+
expect(list.summary).toEqual([
|
|
7144
|
+
{ productId: PRODUCT_ID, product: 'Web', open: 2, byKind: { review_pr: 1, review_doc: 1 } },
|
|
7145
|
+
{ productId: PRODUCT_ID_2, product: 'Api', open: 1, byKind: { review_pr: 1 } },
|
|
7146
|
+
]);
|
|
7147
|
+
expect(list.totalCount).toBe(3);
|
|
7148
|
+
expect(list.data[0]).toMatchObject({ id: 'i1', prUrl: 'https://github.com/acme/web/pull/9', waitingDays: 3 });
|
|
7149
|
+
expect(JSON.stringify(list.data)).not.toContain('metadata');
|
|
7150
|
+
expect(full.data[1].metadata).toEqual(rows[1]!.metadata);
|
|
7151
|
+
// The page asked for and the summary's own read, nothing on a later page.
|
|
7152
|
+
const limits = fetchSpy.mock.calls
|
|
7153
|
+
.map((call: unknown[]) => new URL(call[0] as string))
|
|
7154
|
+
.filter((url: URL) => url.pathname === '/v1/inbox')
|
|
7155
|
+
.map((url: URL) => url.searchParams.get('limit'));
|
|
7156
|
+
// The short page and the summary's read; the full page held everything, so it read once.
|
|
7157
|
+
expect(limits).toEqual(['2', '100', null]);
|
|
7158
|
+
});
|
|
7159
|
+
|
|
7160
|
+
it("counts a resolved item's wait up to its resolution", async () => {
|
|
7161
|
+
const day = 24 * 60 * 60 * 1000;
|
|
7162
|
+
const { client } = await bootMcpPair({
|
|
7163
|
+
capabilities: ['products:read'],
|
|
7164
|
+
routes: {
|
|
7165
|
+
'GET /v1/inbox': {
|
|
7166
|
+
data: [
|
|
7167
|
+
{
|
|
7168
|
+
id: 'r1',
|
|
7169
|
+
productId: PRODUCT_ID,
|
|
7170
|
+
kind: 'review_pr',
|
|
7171
|
+
status: 'done',
|
|
7172
|
+
createdAt: new Date(Date.now() - 40 * day).toISOString(),
|
|
7173
|
+
resolvedAt: new Date(Date.now() - 38 * day).toISOString(),
|
|
7174
|
+
},
|
|
7175
|
+
],
|
|
7176
|
+
nextCursor: null,
|
|
7177
|
+
},
|
|
7178
|
+
},
|
|
7179
|
+
});
|
|
7180
|
+
|
|
7181
|
+
const archive = parseToolResult(
|
|
7182
|
+
await client.callTool({ name: 'taiga_list_action_required', arguments: { resolved: true } })
|
|
7183
|
+
);
|
|
7184
|
+
|
|
7185
|
+
expect(archive.data[0].waitingDays).toBe(2);
|
|
7186
|
+
});
|
|
7187
|
+
|
|
7188
|
+
it('lists a variable filed under two categories once, naming the other record', async () => {
|
|
7189
|
+
const task = (id: string, category: string, key: string | null) => ({
|
|
7190
|
+
id,
|
|
7191
|
+
productId: PRODUCT_ID,
|
|
7192
|
+
category,
|
|
7193
|
+
key,
|
|
7194
|
+
title: `Set ${key ?? 'something'}`,
|
|
7195
|
+
});
|
|
7196
|
+
const { client } = await bootMcpPair({
|
|
7197
|
+
capabilities: ['products:read'],
|
|
7198
|
+
routes: {
|
|
7199
|
+
'GET /v1/setup-tasks': {
|
|
7200
|
+
data: [
|
|
7201
|
+
task('t1', 'secret', 'JWT_SECRET'),
|
|
7202
|
+
task('t2', 'manual_config', null),
|
|
7203
|
+
task('t3', 'env_var', 'JWT_SECRET'),
|
|
7204
|
+
],
|
|
7205
|
+
nextCursor: null,
|
|
7206
|
+
},
|
|
7207
|
+
},
|
|
7208
|
+
});
|
|
7209
|
+
|
|
7210
|
+
const tasks = parseToolResult(await client.callTool({ name: 'taiga_list_setup_tasks', arguments: {} }));
|
|
7211
|
+
|
|
7212
|
+
expect(tasks.data.map((row: { id: string }) => row.id)).toEqual(['t1', 't2']);
|
|
7213
|
+
expect(tasks.data[0].alsoFiledAs).toEqual([{ id: 't3', category: 'env_var', title: 'Set JWT_SECRET' }]);
|
|
7214
|
+
});
|
|
7215
|
+
|
|
7216
|
+
it('leads a variable with its open record when an older one is done', async () => {
|
|
7217
|
+
const { client } = await bootMcpPair({
|
|
7218
|
+
capabilities: ['products:read'],
|
|
7219
|
+
routes: {
|
|
7220
|
+
'GET /v1/setup-tasks': {
|
|
7221
|
+
data: [
|
|
7222
|
+
{ id: 't1', productId: PRODUCT_ID, category: 'secret', key: 'K', title: 'Old', inboxItemStatus: 'done' },
|
|
7223
|
+
{ id: 't2', productId: PRODUCT_ID, category: 'env_var', key: 'K', title: 'New', inboxItemStatus: 'open' },
|
|
7224
|
+
],
|
|
7225
|
+
nextCursor: null,
|
|
7226
|
+
},
|
|
7227
|
+
},
|
|
7228
|
+
});
|
|
7229
|
+
|
|
7230
|
+
const tasks = parseToolResult(
|
|
7231
|
+
await client.callTool({ name: 'taiga_list_setup_tasks', arguments: { includeDone: true } })
|
|
7232
|
+
);
|
|
7233
|
+
|
|
7234
|
+
expect(tasks.data).toHaveLength(1);
|
|
7235
|
+
expect(tasks.data[0]).toMatchObject({ id: 't2', inboxItemStatus: 'open' });
|
|
7236
|
+
expect(tasks.data[0].alsoFiledAs).toEqual([
|
|
7237
|
+
{ id: 't1', category: 'secret', title: 'Old', inboxItemStatus: 'done' },
|
|
7238
|
+
]);
|
|
7239
|
+
});
|
|
7240
|
+
|
|
7241
|
+
it("names a finding's sweep sweepId, so runId stays what taiga_get_run takes", async () => {
|
|
7242
|
+
const FINDING = '0199a1b2-c3d4-7e5f-8a9b-0c1d2e3f4d01';
|
|
7243
|
+
const { client } = await bootMcpPair({
|
|
7244
|
+
capabilities: ['products:read'],
|
|
7245
|
+
routes: {
|
|
7246
|
+
[`GET /v1/maintenance/findings/${FINDING}`]: {
|
|
7247
|
+
data: { id: FINDING, runId: 'sweep-1', remediation: { buildRunId: BUILD_ID } },
|
|
7248
|
+
},
|
|
7249
|
+
},
|
|
7250
|
+
});
|
|
7251
|
+
|
|
7252
|
+
const finding = parseToolResult(
|
|
7253
|
+
await client.callTool({ name: 'taiga_get_finding', arguments: { findingId: FINDING } })
|
|
7254
|
+
);
|
|
7255
|
+
|
|
7256
|
+
expect(finding).toEqual({ sweepId: 'sweep-1', id: FINDING, remediation: { runId: BUILD_ID } });
|
|
7257
|
+
});
|
|
7258
|
+
|
|
7259
|
+
it('says how each run stands in a sentence, and says nothing of a draft where there is no pull request', async () => {
|
|
7260
|
+
const base = { productId: PRODUCT_ID, completedSteps: 6, totalSteps: 6, prDraft: true };
|
|
7261
|
+
const { client } = await bootMcpPair({
|
|
7262
|
+
capabilities: ['products:read'],
|
|
7263
|
+
routes: {
|
|
7264
|
+
[`/v1/products/${PRODUCT_ID}/builds`]: {
|
|
7265
|
+
data: [
|
|
7266
|
+
{ ...base, id: 'r1', status: 'closed', prNumber: 28, prUrl: 'https://x/pull/28', prDraft: false },
|
|
7267
|
+
{
|
|
7268
|
+
...base,
|
|
7269
|
+
id: 'r2',
|
|
7270
|
+
status: 'completed',
|
|
7271
|
+
prMerged: true,
|
|
7272
|
+
prNumber: 7,
|
|
7273
|
+
prUrl: 'https://x/7',
|
|
7274
|
+
baseBranch: 'main',
|
|
7275
|
+
},
|
|
7276
|
+
{ ...base, id: 'r3', status: 'failed', failedReason: 'agent_error', completedSteps: 2, prUrl: null },
|
|
7277
|
+
{ ...base, id: 'r4', status: 'needs_review', prNumber: 9, prUrl: null },
|
|
7278
|
+
{ ...base, id: 'r5', status: 'hardening', prUrl: null },
|
|
7279
|
+
// Merged, but the completion write after the merge failed: the latch decides.
|
|
7280
|
+
{
|
|
7281
|
+
...base,
|
|
7282
|
+
id: 'r6',
|
|
7283
|
+
status: 'failed',
|
|
7284
|
+
prMerged: true,
|
|
7285
|
+
prNumber: 8,
|
|
7286
|
+
prUrl: 'https://x/8',
|
|
7287
|
+
baseBranch: 'main',
|
|
7288
|
+
},
|
|
7289
|
+
],
|
|
7290
|
+
nextCursor: null,
|
|
7291
|
+
},
|
|
7292
|
+
},
|
|
7293
|
+
});
|
|
7294
|
+
|
|
7295
|
+
const runs = parseToolResult(
|
|
7296
|
+
await client.callTool({ name: 'taiga_list_runs', arguments: { productId: PRODUCT_ID } })
|
|
7297
|
+
).data as Record<string, unknown>[];
|
|
7298
|
+
|
|
7299
|
+
expect(runs.map((run) => [run['disposition'], run['outcome']])).toEqual([
|
|
7300
|
+
['rejected', 'PR #28 was closed without merging, so the work did not land.'],
|
|
7301
|
+
['succeeded', 'Merged: PR #7 is in main.'],
|
|
7302
|
+
['failed', 'Failed: agent_error (2 of 6 steps done).'],
|
|
7303
|
+
['gated', 'Waiting for a person to review and merge PR #9.'],
|
|
7304
|
+
['in_flight', 'Running: hardening: running the security scanners and fixing what they find (6 of 6 steps done).'],
|
|
7305
|
+
['succeeded', 'Merged: PR #8 is in main.'],
|
|
7306
|
+
]);
|
|
7307
|
+
expect(runs[0]).toHaveProperty('prDraft', false);
|
|
7308
|
+
expect(runs[2]).not.toHaveProperty('prDraft');
|
|
7309
|
+
// A recorded PR number with no URL still has a pull request, and its draft state.
|
|
7310
|
+
expect(runs[3]).toHaveProperty('prDraft', true);
|
|
7311
|
+
});
|
|
7312
|
+
|
|
7313
|
+
it('says where an initiative stands and the call that moves it on, in one sentence', async () => {
|
|
7314
|
+
const item = {
|
|
7315
|
+
id: ITEM_ID,
|
|
7316
|
+
title: 'Add login',
|
|
7317
|
+
status: 'in_progress',
|
|
7318
|
+
state: 'build',
|
|
7319
|
+
needsHumanCause: null,
|
|
7320
|
+
lifecycle: {
|
|
7321
|
+
turn: 'you',
|
|
7322
|
+
stage: 'building',
|
|
7323
|
+
cta: 'retry_build',
|
|
7324
|
+
planFailureCause: null,
|
|
7325
|
+
latestBuild: { status: 'closed', prNumber: 28 },
|
|
7326
|
+
},
|
|
7327
|
+
};
|
|
7328
|
+
const { client } = await bootMcpPair({
|
|
7329
|
+
capabilities: ['backlog:read'],
|
|
7330
|
+
routes: {
|
|
7331
|
+
[`GET /v1/backlog/${ITEM_ID}`]: { data: item },
|
|
7332
|
+
[`/v1/products/${PRODUCT_ID}/backlog`]: { data: [item], nextCursor: null },
|
|
7333
|
+
},
|
|
7334
|
+
});
|
|
7335
|
+
|
|
7336
|
+
const got = parseToolResult(
|
|
7337
|
+
await client.callTool({ name: 'taiga_get_initiative', arguments: { initiativeId: ITEM_ID } })
|
|
7338
|
+
);
|
|
7339
|
+
const listed = parseToolResult(
|
|
7340
|
+
await client.callTool({ name: 'taiga_list_initiatives', arguments: { productId: PRODUCT_ID } })
|
|
7341
|
+
);
|
|
7342
|
+
|
|
7343
|
+
const said =
|
|
7344
|
+
"Being built; its latest run had its pull request closed without merging (PR #28). A person's turn: a new " +
|
|
7345
|
+
'run starts over from the web; taiga_move_initiative takes it off the line.';
|
|
7346
|
+
expect(got.situation).toBe(said);
|
|
7347
|
+
expect(listed.data[0].situation).toBe(said);
|
|
7348
|
+
});
|
|
7349
|
+
|
|
7350
|
+
it("keeps the run's own reason beside the halt that stopped the line", async () => {
|
|
7351
|
+
const item = {
|
|
7352
|
+
id: ITEM_ID,
|
|
7353
|
+
title: 'Add login',
|
|
7354
|
+
needsHumanCause: 'pr_closed',
|
|
7355
|
+
needsHumanDetail: 'The pull request was closed by alice.',
|
|
7356
|
+
lifecycle: { turn: 'you', stage: 'building', cta: 'retry_build', planFailureCause: null, latestBuild: null },
|
|
7357
|
+
};
|
|
7358
|
+
const { client } = await bootMcpPair({
|
|
7359
|
+
capabilities: ['backlog:read'],
|
|
7360
|
+
routes: { [`GET /v1/backlog/${ITEM_ID}`]: { data: item } },
|
|
7361
|
+
});
|
|
7362
|
+
|
|
7363
|
+
const got = parseToolResult(
|
|
7364
|
+
await client.callTool({ name: 'taiga_get_initiative', arguments: { initiativeId: ITEM_ID } })
|
|
7365
|
+
);
|
|
7366
|
+
|
|
7367
|
+
expect(got.situation).toContain('The pull request was closed by alice.');
|
|
7368
|
+
expect(got.situation).toMatch(/^Being built\. The line stopped on it: .+\. The pull request was closed by alice\./);
|
|
7369
|
+
});
|
|
7370
|
+
|
|
7371
|
+
it("leaves the keys of a document's sections as their writers named them", async () => {
|
|
7372
|
+
const { client } = await bootMcpPair({
|
|
7373
|
+
capabilities: ALL_CAPABILITIES,
|
|
7374
|
+
routes: {
|
|
7375
|
+
'GET /v1/tenants/current/documents/policy_security': {
|
|
7376
|
+
data: {
|
|
7377
|
+
id: 'doc-1',
|
|
7378
|
+
type: 'policy_security',
|
|
7379
|
+
version: 1,
|
|
7380
|
+
isDraft: false,
|
|
7381
|
+
data: { version: '1.0.0', sections: { backlogItemId: { content: 'a section by that name' } } },
|
|
7382
|
+
},
|
|
7383
|
+
},
|
|
7384
|
+
},
|
|
7385
|
+
});
|
|
7386
|
+
|
|
7387
|
+
const doc = parseToolResult(
|
|
7388
|
+
await client.callTool({ name: 'taiga_get_document', arguments: { type: 'policy_security' } })
|
|
7389
|
+
);
|
|
7390
|
+
|
|
7391
|
+
expect(Object.keys(doc.data.sections)).toEqual(['backlogItemId']);
|
|
7392
|
+
});
|
|
7393
|
+
|
|
7394
|
+
it("names IDs in refusal details in the product's words", async () => {
|
|
7395
|
+
const { client } = await bootMcpPair({
|
|
7396
|
+
capabilities: ALL_CAPABILITIES,
|
|
7397
|
+
routes: {
|
|
7398
|
+
[`POST /v1/builds/${BUILD_ID}/stop`]: refusal(409, 'CONFLICT', 'Not running', { buildRunId: BUILD_ID }),
|
|
7399
|
+
},
|
|
7400
|
+
});
|
|
7401
|
+
|
|
7402
|
+
const result = await client.callTool({ name: 'taiga_stop_run', arguments: { runId: BUILD_ID } });
|
|
7403
|
+
|
|
7404
|
+
expect(parseToolResult(result).details).toEqual({ runId: BUILD_ID });
|
|
7405
|
+
});
|
|
7406
|
+
|
|
7407
|
+
it('leaves stopping the line to the person when a move meets work in flight', async () => {
|
|
7408
|
+
const { client } = await bootMcpPair({
|
|
7409
|
+
capabilities: ALL_CAPABILITIES,
|
|
7410
|
+
routes: {
|
|
7411
|
+
[`PUT /v1/products/${PRODUCT_ID}/factory`]: refusal(
|
|
7412
|
+
409,
|
|
7413
|
+
'WORK_IN_FLIGHT',
|
|
7414
|
+
'This product is planning right now. Stop the line, then move the product.'
|
|
7415
|
+
),
|
|
7416
|
+
},
|
|
7417
|
+
});
|
|
7418
|
+
|
|
7419
|
+
const result = await client.callTool({
|
|
7420
|
+
name: 'taiga_move_product',
|
|
7421
|
+
arguments: { productId: PRODUCT_ID, factoryId: FACTORY_ID },
|
|
7422
|
+
});
|
|
7423
|
+
|
|
7424
|
+
expect(result.isError).toBe(true);
|
|
7425
|
+
const said = parseToolResult(result);
|
|
7426
|
+
expect(said.error).toBe('WORK_IN_FLIGHT');
|
|
7427
|
+
expect(said.message).toMatch(/^This product is planning right now\. Moving it now means stopping the line/);
|
|
7428
|
+
expect(said.message).toMatch(/ask the person, and pass stopLine: true only if they say so\.$/);
|
|
7429
|
+
expect(said.message).not.toContain('Stop the line, then');
|
|
7430
|
+
});
|
|
7431
|
+
|
|
7432
|
+
it('fits the server instructions inside what Claude Code keeps', async () => {
|
|
7433
|
+
const { client } = await bootMcpPair({ capabilities: ALL_CAPABILITIES });
|
|
7434
|
+
|
|
7435
|
+
const instructions = client.getInstructions() ?? '';
|
|
7436
|
+
|
|
7437
|
+
expect(instructions.length).toBeGreaterThan(500);
|
|
7438
|
+
expect(instructions.length).toBeLessThanOrEqual(2048);
|
|
7439
|
+
expect(instructions).toContain('taiga_get_product_context');
|
|
7440
|
+
});
|
|
7441
|
+
});
|