@taiga-ai/mcp-server 0.20.0-dev.454 → 0.21.0-dev.455
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 +435 -183
- package/package.json +1 -1
- package/src/server.integration.test.ts +355 -68
- package/src/tools/autonomy.ts +9 -11
- package/src/tools/backlogs.ts +84 -81
- package/src/tools/builds.ts +96 -28
- package/src/tools/catalogue.test.ts +5 -8
- package/src/tools/catalogue.ts +5 -8
- package/src/tools/context.ts +21 -20
- package/src/tools/documents.ts +34 -43
- package/src/tools/errors.ts +2 -1
- package/src/tools/inbox.ts +19 -33
- package/src/tools/index.ts +42 -7
- package/src/tools/inputs.ts +1 -1
- package/src/tools/instructions.ts +17 -19
- package/src/tools/jobs.ts +5 -6
- package/src/tools/knowledge.ts +17 -23
- package/src/tools/maintenance.ts +129 -82
- package/src/tools/observe.ts +30 -39
- package/src/tools/product-documents.ts +20 -26
- package/src/tools/products.ts +23 -73
- package/src/tools/prompts.ts +1 -1
- package/src/tools/queue.ts +10 -12
- package/src/tools/repositories.ts +20 -25
- package/src/tools/sections.ts +41 -6
- package/src/tools/setup.ts +100 -54
- package/src/tools/summarize.ts +4 -1
- package/src/tools/wait.ts +13 -22
- package/taiga-ai-mcp-server-0.21.0-dev.455.tgz +0 -0
- package/taiga-ai-mcp-server-0.20.0-dev.454.tgz +0 -0
package/package.json
CHANGED
|
@@ -5,6 +5,7 @@ import { TaigaClient } from './client.js';
|
|
|
5
5
|
import { createServer } from './server.js';
|
|
6
6
|
import type { WaitOptions } from './tools/wait.js';
|
|
7
7
|
import { inProductWords } from './tools/errors.js';
|
|
8
|
+
import { PAID_WORK } from './tools/index.js';
|
|
8
9
|
|
|
9
10
|
/** What a tool returns for an API value: its keys in the product's words (PRO-2800). */
|
|
10
11
|
const asSeen = <T>(value: T): T => inProductWords(value) as T;
|
|
@@ -38,9 +39,9 @@ const ALL_CAPABILITIES = [
|
|
|
38
39
|
const ALL_TOOL_NAMES = [
|
|
39
40
|
'taiga_add_knowledge',
|
|
40
41
|
'taiga_approve_plan',
|
|
42
|
+
'taiga_create_environment',
|
|
41
43
|
'taiga_create_factory',
|
|
42
44
|
'taiga_create_product',
|
|
43
|
-
'taiga_define_environment',
|
|
44
45
|
'taiga_delete_knowledge',
|
|
45
46
|
'taiga_finish_discovery',
|
|
46
47
|
'taiga_fix_finding',
|
|
@@ -55,7 +56,6 @@ const ALL_TOOL_NAMES = [
|
|
|
55
56
|
'taiga_get_job',
|
|
56
57
|
'taiga_get_knowledge',
|
|
57
58
|
'taiga_get_line',
|
|
58
|
-
'taiga_get_maintenance',
|
|
59
59
|
'taiga_get_monitoring',
|
|
60
60
|
'taiga_get_plan',
|
|
61
61
|
'taiga_get_policy_standing',
|
|
@@ -97,7 +97,6 @@ const ALL_TOOL_NAMES = [
|
|
|
97
97
|
'taiga_reopen_discovery',
|
|
98
98
|
'taiga_request_initiatives',
|
|
99
99
|
'taiga_resolve_action',
|
|
100
|
-
'taiga_resolve_product',
|
|
101
100
|
'taiga_resume_initiative',
|
|
102
101
|
'taiga_retry_knowledge',
|
|
103
102
|
'taiga_set_auto_build',
|
|
@@ -439,7 +438,6 @@ describe('MCP server integration', () => {
|
|
|
439
438
|
'taiga_list_products',
|
|
440
439
|
'taiga_get_product',
|
|
441
440
|
'taiga_get_product_context',
|
|
442
|
-
'taiga_resolve_product',
|
|
443
441
|
'taiga_list_product_documents',
|
|
444
442
|
'taiga_get_product_document',
|
|
445
443
|
'taiga_list_runs',
|
|
@@ -448,7 +446,6 @@ describe('MCP server integration', () => {
|
|
|
448
446
|
'taiga_wait_for_run',
|
|
449
447
|
'taiga_get_job',
|
|
450
448
|
'taiga_wait_for_job',
|
|
451
|
-
'taiga_get_maintenance',
|
|
452
449
|
'taiga_list_findings',
|
|
453
450
|
'taiga_get_finding',
|
|
454
451
|
'taiga_get_policy_standing',
|
|
@@ -504,7 +501,7 @@ describe('MCP server integration', () => {
|
|
|
504
501
|
'taiga_fix_finding',
|
|
505
502
|
'taiga_fix_policy_violation',
|
|
506
503
|
'taiga_resolve_action',
|
|
507
|
-
'
|
|
504
|
+
'taiga_create_environment',
|
|
508
505
|
'taiga_update_environment',
|
|
509
506
|
'taiga_update_product_document',
|
|
510
507
|
'taiga_regenerate_product_document',
|
|
@@ -978,24 +975,19 @@ describe('MCP server integration', () => {
|
|
|
978
975
|
};
|
|
979
976
|
const step = { id: 'step-1', runId: BUILD_ID, stepOrder: 1, title: 'Add the route', status: 'completed' };
|
|
980
977
|
|
|
981
|
-
it('should
|
|
978
|
+
it('should read the product a git remote belongs to through the repository filter', async () => {
|
|
982
979
|
const { client, fetchSpy } = await bootMcpPair({
|
|
983
980
|
capabilities: ['products:read'],
|
|
984
981
|
routes: { '/v1/products': { data: [product], nextCursor: null } },
|
|
985
982
|
});
|
|
986
983
|
|
|
987
984
|
const result = await client.callTool({
|
|
988
|
-
name: '
|
|
985
|
+
name: 'taiga_get_product',
|
|
989
986
|
arguments: { remoteUrl: 'git@github.com:Acme/Web.git' },
|
|
990
987
|
});
|
|
991
988
|
|
|
992
989
|
expect(result.isError).toBeFalsy();
|
|
993
|
-
expect(parseToolResult(result)).
|
|
994
|
-
repository: 'Acme/Web',
|
|
995
|
-
host: 'https://github.com',
|
|
996
|
-
products: [productSummary],
|
|
997
|
-
truncated: false,
|
|
998
|
-
});
|
|
990
|
+
expect(parseToolResult(result)).toMatchObject({ id: PRODUCT_ID, name: product.name });
|
|
999
991
|
const url = sentUrl(fetchSpy, '/v1/products');
|
|
1000
992
|
expect(url.searchParams.get('repository')).toBe('Acme/Web');
|
|
1001
993
|
expect(url.searchParams.get('repositoryHost')).toBe('https://github.com');
|
|
@@ -1016,13 +1008,7 @@ describe('MCP server integration', () => {
|
|
|
1016
1008
|
});
|
|
1017
1009
|
}
|
|
1018
1010
|
|
|
1019
|
-
|
|
1020
|
-
fetchSpy.mock.calls
|
|
1021
|
-
.map((c: unknown[]) => new URL(c[0] as string))
|
|
1022
|
-
.filter((u: URL) => u.pathname === '/v1/products')
|
|
1023
|
-
.map((u: URL) => u.searchParams.get('cursor'));
|
|
1024
|
-
|
|
1025
|
-
it('should follow the pages of a shared repository rather than stop at the first', async () => {
|
|
1011
|
+
it('should refuse a remote shared by several products with their IDs, across pages', async () => {
|
|
1026
1012
|
const second = { ...product, id: PRODUCT_ID_2, name: 'Web Admin' };
|
|
1027
1013
|
const { client, fetchSpy } = await bootMcpPair({ capabilities: ['products:read'] });
|
|
1028
1014
|
serveProductPages(fetchSpy, (cursor) =>
|
|
@@ -1030,32 +1016,18 @@ describe('MCP server integration', () => {
|
|
|
1030
1016
|
);
|
|
1031
1017
|
|
|
1032
1018
|
const result = await client.callTool({
|
|
1033
|
-
name: '
|
|
1034
|
-
arguments: { remoteUrl: 'git@github.com:acme/web.git' },
|
|
1035
|
-
});
|
|
1036
|
-
|
|
1037
|
-
const parsed = parseToolResult(result);
|
|
1038
|
-
expect(parsed.products.map((p: { id: string }) => p.id)).toEqual([PRODUCT_ID, PRODUCT_ID_2]);
|
|
1039
|
-
expect(parsed.truncated).toBe(false);
|
|
1040
|
-
expect(productPageCursors(fetchSpy)).toEqual([null, 'page-2']);
|
|
1041
|
-
});
|
|
1042
|
-
|
|
1043
|
-
it('should stop after ten pages and say the list is truncated', async () => {
|
|
1044
|
-
const { client, fetchSpy } = await bootMcpPair({ capabilities: ['products:read'] });
|
|
1045
|
-
serveProductPages(fetchSpy, () => ({ data: [product], nextCursor: 'more' }));
|
|
1046
|
-
|
|
1047
|
-
const result = await client.callTool({
|
|
1048
|
-
name: 'taiga_resolve_product',
|
|
1019
|
+
name: 'taiga_get_product',
|
|
1049
1020
|
arguments: { remoteUrl: 'git@github.com:acme/web.git' },
|
|
1050
1021
|
});
|
|
1051
1022
|
|
|
1052
|
-
|
|
1053
|
-
|
|
1054
|
-
expect(
|
|
1055
|
-
expect(
|
|
1023
|
+
expect(result.isError).toBe(true);
|
|
1024
|
+
const said = parseToolResult(result);
|
|
1025
|
+
expect(said.error).toBe('AMBIGUOUS');
|
|
1026
|
+
expect(said.message).toContain(PRODUCT_ID);
|
|
1027
|
+
expect(said.message).toContain(PRODUCT_ID_2);
|
|
1056
1028
|
});
|
|
1057
1029
|
|
|
1058
|
-
it('should fail the whole resolution when a later page fails, not
|
|
1030
|
+
it('should fail the whole resolution when a later page fails, not answer from a partial list', async () => {
|
|
1059
1031
|
const { client, fetchSpy } = await bootMcpPair({ capabilities: ['products:read'] });
|
|
1060
1032
|
const router = fetchSpy.getMockImplementation() as (url: string, init: RequestInit) => Promise<unknown>;
|
|
1061
1033
|
fetchSpy.mockImplementation(async (url: string, init: RequestInit) => {
|
|
@@ -1070,7 +1042,7 @@ describe('MCP server integration', () => {
|
|
|
1070
1042
|
});
|
|
1071
1043
|
|
|
1072
1044
|
const result = await client.callTool({
|
|
1073
|
-
name: '
|
|
1045
|
+
name: 'taiga_get_product',
|
|
1074
1046
|
arguments: { remoteUrl: 'git@github.com:acme/web.git' },
|
|
1075
1047
|
});
|
|
1076
1048
|
|
|
@@ -1085,7 +1057,7 @@ describe('MCP server integration', () => {
|
|
|
1085
1057
|
});
|
|
1086
1058
|
|
|
1087
1059
|
const result = await client.callTool({
|
|
1088
|
-
name: '
|
|
1060
|
+
name: 'taiga_get_product',
|
|
1089
1061
|
arguments: { remoteUrl: 'https://github.com/acme/web.git' },
|
|
1090
1062
|
});
|
|
1091
1063
|
|
|
@@ -1101,7 +1073,7 @@ describe('MCP server integration', () => {
|
|
|
1101
1073
|
const before = fetchSpy.mock.calls.length;
|
|
1102
1074
|
|
|
1103
1075
|
const result = await client.callTool({
|
|
1104
|
-
name: '
|
|
1076
|
+
name: 'taiga_get_product',
|
|
1105
1077
|
arguments: { remoteUrl: '/Users/dev/web' },
|
|
1106
1078
|
});
|
|
1107
1079
|
|
|
@@ -1205,9 +1177,11 @@ describe('MCP server integration', () => {
|
|
|
1205
1177
|
expect(parseToolResult(result)).toEqual({
|
|
1206
1178
|
...rest,
|
|
1207
1179
|
initiativeId: ITEM_ID,
|
|
1208
|
-
// Completed with no merge recorded: an older run that settled on an open pull request.
|
|
1180
|
+
// Completed with no merge recorded: an older run that settled on an open pull request. Without
|
|
1181
|
+
// backlog:read the key cannot say whether its initiative still wants it.
|
|
1209
1182
|
disposition: 'unmerged',
|
|
1210
|
-
outcome:
|
|
1183
|
+
outcome:
|
|
1184
|
+
'Settled without merging: its pull request was left unmerged. Whether its initiative still wants it was not checked.',
|
|
1211
1185
|
steps: [step],
|
|
1212
1186
|
});
|
|
1213
1187
|
});
|
|
@@ -2031,9 +2005,7 @@ describe('MCP server integration', () => {
|
|
|
2031
2005
|
expect(bundle.documents.notes).toEqual([
|
|
2032
2006
|
'No published plan architecture; taiga_list_product_documents shows what the product has.',
|
|
2033
2007
|
]);
|
|
2034
|
-
expect(bundle.policies).toEqual([
|
|
2035
|
-
{ type: 'policy_security', name: 'Security Policy', version: 3, sections: ['summary', 'scope'] },
|
|
2036
|
-
]);
|
|
2008
|
+
expect(bundle.policies).toEqual([{ type: 'policy_security', name: 'Security Policy', version: 3, sections: 2 }]);
|
|
2037
2009
|
expect(bundle.initiatives.items.map((row: { id: string }) => row.id)).toEqual([ITEM_ID]);
|
|
2038
2010
|
expect(bundle.initiatives.items[0]).not.toHaveProperty('description');
|
|
2039
2011
|
expect(bundle.initiatives.truncated).toBe(false);
|
|
@@ -2966,7 +2938,7 @@ describe('MCP server integration', () => {
|
|
|
2966
2938
|
'taiga_stop_run',
|
|
2967
2939
|
'taiga_request_initiatives',
|
|
2968
2940
|
'taiga_plan_again',
|
|
2969
|
-
'
|
|
2941
|
+
'taiga_create_environment',
|
|
2970
2942
|
'taiga_add_knowledge',
|
|
2971
2943
|
'taiga_retry_knowledge',
|
|
2972
2944
|
]) {
|
|
@@ -4978,7 +4950,7 @@ describe('MCP server integration', () => {
|
|
|
4978
4950
|
.filter((c: unknown[]) => ((c[1] as RequestInit | undefined)?.method ?? 'GET') === method)
|
|
4979
4951
|
.map((c: unknown[]) => new URL(c[0] as string));
|
|
4980
4952
|
|
|
4981
|
-
it(
|
|
4953
|
+
it('should open the findings with the sweep overview, and say when the product was never swept', async () => {
|
|
4982
4954
|
const summary = {
|
|
4983
4955
|
productId: PRODUCT_ID,
|
|
4984
4956
|
openFindings: 2,
|
|
@@ -4990,6 +4962,7 @@ describe('MCP server integration', () => {
|
|
|
4990
4962
|
capabilities: ['products:read'],
|
|
4991
4963
|
routes: {
|
|
4992
4964
|
[`/v1/products/${PRODUCT_ID}/maintenance/summary`]: { data: summary },
|
|
4965
|
+
[`/v1/products/${PRODUCT_ID}/maintenance/findings`]: { data: [], nextCursor: null },
|
|
4993
4966
|
[`/v1/products/${PRODUCT_ID}/maintenance/runs`]: {
|
|
4994
4967
|
data: [
|
|
4995
4968
|
{ id: 'run-1', trigger: 'event', status: 'completed', scanners: { deps: 'passed' }, agentJobId: 'x' },
|
|
@@ -4999,8 +4972,8 @@ describe('MCP server integration', () => {
|
|
|
4999
4972
|
},
|
|
5000
4973
|
});
|
|
5001
4974
|
const standing = parseToolResult(
|
|
5002
|
-
await swept.client.callTool({ name: '
|
|
5003
|
-
);
|
|
4975
|
+
await swept.client.callTool({ name: 'taiga_list_findings', arguments: { productId: PRODUCT_ID } })
|
|
4976
|
+
).sweep;
|
|
5004
4977
|
expect(standing).toMatchObject({ openFindings: 2, latestSweeps: [{ id: 'run-1', status: 'completed' }] });
|
|
5005
4978
|
expect(standing.latestSweeps[0]).not.toHaveProperty('agentJobId');
|
|
5006
4979
|
expect(standing).not.toHaveProperty('note');
|
|
@@ -5010,12 +4983,13 @@ describe('MCP server integration', () => {
|
|
|
5010
4983
|
capabilities: ['products:read'],
|
|
5011
4984
|
routes: {
|
|
5012
4985
|
[`/v1/products/${PRODUCT_ID}/maintenance/summary`]: { data: { ...summary, openFindings: 0 } },
|
|
4986
|
+
[`/v1/products/${PRODUCT_ID}/maintenance/findings`]: { data: [], nextCursor: null },
|
|
5013
4987
|
[`/v1/products/${PRODUCT_ID}/maintenance/runs`]: { data: [], nextCursor: null },
|
|
5014
4988
|
},
|
|
5015
4989
|
});
|
|
5016
4990
|
const quiet = parseToolResult(
|
|
5017
|
-
await never.client.callTool({ name: '
|
|
5018
|
-
);
|
|
4991
|
+
await never.client.callTool({ name: 'taiga_list_findings', arguments: { productId: PRODUCT_ID } })
|
|
4992
|
+
).sweep;
|
|
5019
4993
|
expect(quiet.note).toMatch(/never been swept/);
|
|
5020
4994
|
});
|
|
5021
4995
|
|
|
@@ -5437,7 +5411,7 @@ describe('MCP server integration', () => {
|
|
|
5437
5411
|
});
|
|
5438
5412
|
|
|
5439
5413
|
await client.callTool({
|
|
5440
|
-
name: '
|
|
5414
|
+
name: 'taiga_create_environment',
|
|
5441
5415
|
arguments: { productId: PRODUCT_ID, name: 'staging', type: 'staging' },
|
|
5442
5416
|
});
|
|
5443
5417
|
expect(sentBody(fetchSpy, 'POST', '/environments')).toEqual({ name: 'staging', type: 'staging' });
|
|
@@ -7073,11 +7047,15 @@ describe('what the second agent review found (PRO-2778, PRO-2780)', () => {
|
|
|
7073
7047
|
for (const tool of tools) {
|
|
7074
7048
|
const notes = tool.annotations ?? {};
|
|
7075
7049
|
const description = tool.description ?? '';
|
|
7076
|
-
if (notes.readOnlyHint) expect(description, tool.name).not.toMatch(/^(Writes\.|Ask first
|
|
7077
|
-
else if (notes.destructiveHint)
|
|
7050
|
+
if (notes.readOnlyHint) expect(description, tool.name).not.toMatch(/^(Writes\.|Ask first)/);
|
|
7051
|
+
else if (notes.destructiveHint)
|
|
7052
|
+
expect(description, tool.name).toMatch(/^Ask first \((paid agent work|others see it)\)\. /);
|
|
7078
7053
|
else expect(description, tool.name).toMatch(/^Writes\. /);
|
|
7079
7054
|
}
|
|
7080
|
-
expect(tools.find((t) => t.name === 'taiga_queue_initiative')!.description).toMatch(
|
|
7055
|
+
expect(tools.find((t) => t.name === 'taiga_queue_initiative')!.description).toMatch(
|
|
7056
|
+
/^Ask first \(paid agent work\)/
|
|
7057
|
+
);
|
|
7058
|
+
expect(tools.find((t) => t.name === 'taiga_rename_factory')!.description).toMatch(/^Ask first \(others see it\)/);
|
|
7081
7059
|
expect(tools.find((t) => t.name === 'taiga_create_factory')!.description).toMatch(/^Writes\. /);
|
|
7082
7060
|
});
|
|
7083
7061
|
|
|
@@ -7132,7 +7110,7 @@ describe('what the third agent review found (PRO-2800)', () => {
|
|
|
7132
7110
|
expect(me.role).toBeTypeOf('string');
|
|
7133
7111
|
expect(me.capabilities).toEqual(expect.arrayContaining(['products:read', 'backlog:write']));
|
|
7134
7112
|
expect(me.tools.reads).toContain('taiga_get_product_context');
|
|
7135
|
-
expect(me.tools.
|
|
7113
|
+
expect(me.tools.askFirstPaid).toContain('taiga_queue_initiative');
|
|
7136
7114
|
expect(me.tools.reads).not.toContain('taiga_whoami');
|
|
7137
7115
|
});
|
|
7138
7116
|
|
|
@@ -7205,7 +7183,9 @@ describe('what the third agent review found (PRO-2800)', () => {
|
|
|
7205
7183
|
await client.callTool({ name: 'taiga_list_runs', arguments: { productId: PRODUCT_ID } })
|
|
7206
7184
|
).data as Record<string, unknown>[];
|
|
7207
7185
|
|
|
7208
|
-
expect(runs[0]!['outcome']).toMatch(
|
|
7186
|
+
expect(runs[0]!['outcome']).toMatch(
|
|
7187
|
+
/look at what is still running or queued in CI on GitHub, then review the pull request/
|
|
7188
|
+
);
|
|
7209
7189
|
expect(runs[1]!['outcome']).toMatch(/CI is still failing after 1 automatic round\./);
|
|
7210
7190
|
expect(runs[2]!['outcome']).toBe('Failed: TypeError: boom (2 of 3 steps done).');
|
|
7211
7191
|
// A row says the handback in its outcome; the full read keeps the record.
|
|
@@ -7297,6 +7277,38 @@ describe('what the third agent review found (PRO-2800)', () => {
|
|
|
7297
7277
|
expect(got.lifecycle.latestRun.id).toBe(BUILD_ID);
|
|
7298
7278
|
});
|
|
7299
7279
|
|
|
7280
|
+
it('points a control id asked of a product document at the policy', async () => {
|
|
7281
|
+
const { client } = await bootMcpPair({
|
|
7282
|
+
capabilities: ['products:read'],
|
|
7283
|
+
routes: {
|
|
7284
|
+
[`GET /v1/products/${PRODUCT_ID}`]: {
|
|
7285
|
+
data: { id: PRODUCT_ID, name: 'Web', slug: 'web', status: 'active', factoryId: FACTORY_ID, settings: {} },
|
|
7286
|
+
},
|
|
7287
|
+
[`GET /v1/products/${PRODUCT_ID}/documents/threat_model`]: {
|
|
7288
|
+
data: {
|
|
7289
|
+
id: 'd',
|
|
7290
|
+
type: 'threat_model',
|
|
7291
|
+
version: 1,
|
|
7292
|
+
isDraft: false,
|
|
7293
|
+
data: { sections: { risks: { content: 'SEC-5.1 applies.' } } },
|
|
7294
|
+
},
|
|
7295
|
+
},
|
|
7296
|
+
},
|
|
7297
|
+
});
|
|
7298
|
+
|
|
7299
|
+
const doc = parseToolResult(
|
|
7300
|
+
await client.callTool({
|
|
7301
|
+
name: 'taiga_get_product_document',
|
|
7302
|
+
arguments: { productId: PRODUCT_ID, type: 'threat_model', sections: ['SEC-5.1'] },
|
|
7303
|
+
})
|
|
7304
|
+
);
|
|
7305
|
+
|
|
7306
|
+
expect(doc.unknownSections).toEqual(['SEC-5.1']);
|
|
7307
|
+
expect(doc.note).toBe(
|
|
7308
|
+
'SEC-5.1 is a control of policy_security: read it with taiga_get_document (type: policy_security, sections: ["SEC-5.1"]).'
|
|
7309
|
+
);
|
|
7310
|
+
});
|
|
7311
|
+
|
|
7300
7312
|
it('reads the sections that state a policy control id', async () => {
|
|
7301
7313
|
const { client } = await bootMcpPair({
|
|
7302
7314
|
capabilities: ALL_CAPABILITIES,
|
|
@@ -7315,6 +7327,7 @@ describe('what the third agent review found (PRO-2800)', () => {
|
|
|
7315
7327
|
logging: { content: '| SEC-5.10 | Log access |' },
|
|
7316
7328
|
appendix: { content: 'See SEC-5.1A and SEC-5.1-appendix, and SEC-5.1.2.' },
|
|
7317
7329
|
summary: { content: 'Secrets follow SEC-5.1.' },
|
|
7330
|
+
telecom: { content: '| TEL-TIC-1.1 | Lawful intercept |' },
|
|
7318
7331
|
},
|
|
7319
7332
|
},
|
|
7320
7333
|
},
|
|
@@ -7325,14 +7338,15 @@ describe('what the third agent review found (PRO-2800)', () => {
|
|
|
7325
7338
|
const doc = parseToolResult(
|
|
7326
7339
|
await client.callTool({
|
|
7327
7340
|
name: 'taiga_get_document',
|
|
7328
|
-
arguments: { type: 'policy_security', sections: ['SEC-5.1', 'SEC-9.9'] },
|
|
7341
|
+
arguments: { type: 'policy_security', sections: ['SEC-5.1', 'SEC-9.9', 'TEL-TIC-1.1', 'TIC-1.1'] },
|
|
7329
7342
|
})
|
|
7330
7343
|
);
|
|
7331
7344
|
|
|
7332
|
-
// A whole id only, and a sentence's full stop after it is still the id
|
|
7333
|
-
|
|
7334
|
-
expect(doc.
|
|
7335
|
-
expect(doc.
|
|
7345
|
+
// A whole id only, and a sentence's full stop after it is still the id; an overlay family's id resolves,
|
|
7346
|
+
// and its tail (TIC-1.1) is not the same control.
|
|
7347
|
+
expect(Object.keys(doc.data.sections).sort()).toEqual(['secretsManagement', 'summary', 'telecom']);
|
|
7348
|
+
expect(doc.controls).toEqual({ 'SEC-5.1': ['secretsManagement', 'summary'], 'TEL-TIC-1.1': ['telecom'] });
|
|
7349
|
+
expect(doc.unknownSections).toEqual(['SEC-9.9', 'TIC-1.1']);
|
|
7336
7350
|
});
|
|
7337
7351
|
|
|
7338
7352
|
it('loads the three entry tools upfront in Claude Code, and only those', async () => {
|
|
@@ -7609,7 +7623,7 @@ describe('what the third agent review found (PRO-2800)', () => {
|
|
|
7609
7623
|
[
|
|
7610
7624
|
'gated',
|
|
7611
7625
|
'Handed back to a person: CI is still failing on secret-scan, and the last fix round changed nothing. ' +
|
|
7612
|
-
'PR #29, still a draft, waits for them to fix it by hand or review it back to the agent.',
|
|
7626
|
+
'PR #29, still a draft, waits for them to fix it by hand, or review the pull request on GitHub, which hands it back to the agent.',
|
|
7613
7627
|
],
|
|
7614
7628
|
[
|
|
7615
7629
|
'failed',
|
|
@@ -7751,3 +7765,276 @@ describe('what the third agent review found (PRO-2800)', () => {
|
|
|
7751
7765
|
expect(instructions).toContain('taiga_get_product_context');
|
|
7752
7766
|
});
|
|
7753
7767
|
});
|
|
7768
|
+
|
|
7769
|
+
describe('what the fourth agent review found (PRO-2800)', () => {
|
|
7770
|
+
beforeEach(() => {
|
|
7771
|
+
vi.restoreAllMocks();
|
|
7772
|
+
});
|
|
7773
|
+
|
|
7774
|
+
it('says a run whose initiative was canceled or deleted is abandoned, not waiting on anyone', async () => {
|
|
7775
|
+
const GONE = '0199a1b2-c3d4-7e5f-8a9b-0c1d2e3f4f01';
|
|
7776
|
+
const base = { productId: PRODUCT_ID, status: 'needs_review', prState: 'open', completedSteps: 3, totalSteps: 3 };
|
|
7777
|
+
const { client } = await bootMcpPair({
|
|
7778
|
+
capabilities: ['products:read', 'backlog:read'],
|
|
7779
|
+
routes: {
|
|
7780
|
+
[`/v1/products/${PRODUCT_ID}/builds`]: {
|
|
7781
|
+
data: [
|
|
7782
|
+
{
|
|
7783
|
+
...base,
|
|
7784
|
+
id: 'r1',
|
|
7785
|
+
backlogItemId: ITEM_ID,
|
|
7786
|
+
prNumber: 10,
|
|
7787
|
+
prUrl: 'https://x/10',
|
|
7788
|
+
actions: ['retry_from_failed_step'],
|
|
7789
|
+
},
|
|
7790
|
+
{ ...base, id: 'r2', backlogItemId: GONE, prNumber: 11, prUrl: 'https://x/11' },
|
|
7791
|
+
],
|
|
7792
|
+
nextCursor: null,
|
|
7793
|
+
},
|
|
7794
|
+
[`GET /v1/backlog/${ITEM_ID}`]: { data: { id: ITEM_ID, status: 'canceled', state: 'canceled' } },
|
|
7795
|
+
[`GET /v1/backlog/${GONE}`]: refusal(404, 'NOT_FOUND', 'Backlog item not found'),
|
|
7796
|
+
},
|
|
7797
|
+
});
|
|
7798
|
+
|
|
7799
|
+
const runs = parseToolResult(
|
|
7800
|
+
await client.callTool({ name: 'taiga_list_runs', arguments: { productId: PRODUCT_ID } })
|
|
7801
|
+
).data as Record<string, unknown>[];
|
|
7802
|
+
|
|
7803
|
+
expect(runs.map((run) => [run['disposition'], run['outcome']])).toEqual([
|
|
7804
|
+
[
|
|
7805
|
+
'abandoned',
|
|
7806
|
+
'Its initiative was canceled, so nobody is waiting on this run. PR #10 is still open on the forge.',
|
|
7807
|
+
],
|
|
7808
|
+
['abandoned', 'Its initiative was deleted, so nobody is waiting on this run. PR #11 is still open on the forge.'],
|
|
7809
|
+
]);
|
|
7810
|
+
expect(runs[0]!['actions']).toEqual([]);
|
|
7811
|
+
});
|
|
7812
|
+
|
|
7813
|
+
it('refuses a factory or product name already taken, unless the person wants a second', async () => {
|
|
7814
|
+
const { client, fetchSpy } = await bootMcpPair({
|
|
7815
|
+
capabilities: ALL_CAPABILITIES,
|
|
7816
|
+
routes: {
|
|
7817
|
+
'GET /v1/factories': { data: [{ id: FACTORY_ID, name: 'Platform', slug: 'platform' }], nextCursor: null },
|
|
7818
|
+
'POST /v1/factories': { data: { id: 'new', name: 'platform', slug: 'platform-2', isDefault: false } },
|
|
7819
|
+
},
|
|
7820
|
+
});
|
|
7821
|
+
|
|
7822
|
+
const refused = await client.callTool({ name: 'taiga_create_factory', arguments: { name: ' platform ' } });
|
|
7823
|
+
const made = await client.callTool({
|
|
7824
|
+
name: 'taiga_create_factory',
|
|
7825
|
+
arguments: { name: 'platform', allowDuplicateName: true },
|
|
7826
|
+
});
|
|
7827
|
+
|
|
7828
|
+
expect(parseToolResult(refused)).toMatchObject({ error: 'DUPLICATE_NAME', details: { existingId: FACTORY_ID } });
|
|
7829
|
+
expect(made.isError).toBeFalsy();
|
|
7830
|
+
expect(
|
|
7831
|
+
fetchSpy.mock.calls.filter((call: unknown[]) => (call[1] as RequestInit | undefined)?.method === 'POST')
|
|
7832
|
+
).toHaveLength(1);
|
|
7833
|
+
});
|
|
7834
|
+
|
|
7835
|
+
it('refuses a product name already taken, unless the person wants a second', async () => {
|
|
7836
|
+
const { client, fetchSpy } = await bootMcpPair({
|
|
7837
|
+
capabilities: ALL_CAPABILITIES,
|
|
7838
|
+
routes: {
|
|
7839
|
+
'GET /v1/products': {
|
|
7840
|
+
data: [{ id: PRODUCT_ID, name: 'Web', slug: 'web', factoryId: FACTORY_ID }],
|
|
7841
|
+
nextCursor: null,
|
|
7842
|
+
},
|
|
7843
|
+
'POST /v1/products': {
|
|
7844
|
+
data: { id: PRODUCT_ID_2, name: 'web', slug: 'web-2', status: 'active', factoryId: FACTORY_ID, settings: {} },
|
|
7845
|
+
},
|
|
7846
|
+
},
|
|
7847
|
+
});
|
|
7848
|
+
|
|
7849
|
+
const refused = await client.callTool({
|
|
7850
|
+
name: 'taiga_create_product',
|
|
7851
|
+
arguments: { factoryId: FACTORY_ID, name: 'WEB', start: 'scratch' },
|
|
7852
|
+
});
|
|
7853
|
+
const made = await client.callTool({
|
|
7854
|
+
name: 'taiga_create_product',
|
|
7855
|
+
arguments: { factoryId: FACTORY_ID, name: 'web', start: 'scratch', allowDuplicateName: true },
|
|
7856
|
+
});
|
|
7857
|
+
|
|
7858
|
+
expect(parseToolResult(refused)).toMatchObject({ error: 'DUPLICATE_NAME', details: { existingId: PRODUCT_ID } });
|
|
7859
|
+
expect(made.isError).toBeFalsy();
|
|
7860
|
+
const posts = fetchSpy.mock.calls.filter(
|
|
7861
|
+
(call: unknown[]) =>
|
|
7862
|
+
(call[1] as RequestInit | undefined)?.method === 'POST' && String(call[0]).endsWith('/v1/products')
|
|
7863
|
+
);
|
|
7864
|
+
expect(posts).toHaveLength(1);
|
|
7865
|
+
});
|
|
7866
|
+
|
|
7867
|
+
it('bounds the full policy standing and reads one violation whole', async () => {
|
|
7868
|
+
const long = 'x'.repeat(5_000);
|
|
7869
|
+
const V1 = '0199a1b2-c3d4-7e5f-8a9b-0c1d2e3f4f02';
|
|
7870
|
+
const { client } = await bootMcpPair({
|
|
7871
|
+
capabilities: ['products:read'],
|
|
7872
|
+
routes: {
|
|
7873
|
+
[`GET /v1/products/${PRODUCT_ID}/maintenance/policies`]: {
|
|
7874
|
+
data: {
|
|
7875
|
+
productId: PRODUCT_ID,
|
|
7876
|
+
controls: [{ controlId: 'SEC-1', divergenceCount: 1 }],
|
|
7877
|
+
divergences: [{ id: V1, controlId: 'SEC-1', severity: 'high', evidence: long }],
|
|
7878
|
+
resolvedDivergences: Array.from({ length: 25 }, (_, n) => ({ id: `r${n}` })),
|
|
7879
|
+
},
|
|
7880
|
+
},
|
|
7881
|
+
},
|
|
7882
|
+
});
|
|
7883
|
+
|
|
7884
|
+
const full = parseToolResult(
|
|
7885
|
+
await client.callTool({ name: 'taiga_get_policy_standing', arguments: { productId: PRODUCT_ID, full: true } })
|
|
7886
|
+
);
|
|
7887
|
+
const one = parseToolResult(
|
|
7888
|
+
await client.callTool({
|
|
7889
|
+
name: 'taiga_get_policy_standing',
|
|
7890
|
+
arguments: { productId: PRODUCT_ID, violationId: V1 },
|
|
7891
|
+
})
|
|
7892
|
+
);
|
|
7893
|
+
|
|
7894
|
+
expect(full.divergences[0].evidence.length).toBeLessThan(1_700);
|
|
7895
|
+
expect(full.divergences[0].evidence).toMatch(/cut at 1500 of 5000 characters/);
|
|
7896
|
+
expect(full.resolvedDivergences).toHaveLength(20);
|
|
7897
|
+
expect(full.resolvedNote).toBe('The first 20 of 25 resolved violations.');
|
|
7898
|
+
expect(one.evidence).toHaveLength(5_000);
|
|
7899
|
+
});
|
|
7900
|
+
|
|
7901
|
+
it('lists the initiatives in one board column, across pages', async () => {
|
|
7902
|
+
const row = (id: string, state: string) => ({ id, title: id, state, status: 'in_progress' });
|
|
7903
|
+
const { client } = await bootMcpPair({
|
|
7904
|
+
capabilities: ['backlog:read'],
|
|
7905
|
+
routes: {
|
|
7906
|
+
[`/v1/products/${PRODUCT_ID}/backlog`]: (url: string) => {
|
|
7907
|
+
const second = new URL(url).searchParams.get('cursor') === 'p2';
|
|
7908
|
+
const body = second
|
|
7909
|
+
? { data: [row('c', 'queue')], nextCursor: null }
|
|
7910
|
+
: { data: [row('a', 'queue'), row('b', 'build')], nextCursor: 'p2' };
|
|
7911
|
+
return { ok: true, status: 200, json: async () => body, text: async () => JSON.stringify(body) };
|
|
7912
|
+
},
|
|
7913
|
+
},
|
|
7914
|
+
});
|
|
7915
|
+
|
|
7916
|
+
const queued = parseToolResult(
|
|
7917
|
+
await client.callTool({ name: 'taiga_list_initiatives', arguments: { productId: PRODUCT_ID, state: 'queue' } })
|
|
7918
|
+
);
|
|
7919
|
+
|
|
7920
|
+
expect(queued.data.map((item: { id: string }) => item.id)).toEqual(['a', 'c']);
|
|
7921
|
+
});
|
|
7922
|
+
|
|
7923
|
+
it('lists only registered ask-first tools as paid agent work', async () => {
|
|
7924
|
+
const { client } = await bootMcpPair({ capabilities: ALL_CAPABILITIES });
|
|
7925
|
+
const { tools } = await client.listTools();
|
|
7926
|
+
const byName = new Map(tools.map((tool) => [tool.name, tool]));
|
|
7927
|
+
|
|
7928
|
+
for (const name of PAID_WORK) {
|
|
7929
|
+
expect(byName.get(name)?.annotations?.destructiveHint, name).toBe(true);
|
|
7930
|
+
expect(byName.get(name)?.description, name).toMatch(/^Ask first \(paid agent work\)/);
|
|
7931
|
+
}
|
|
7932
|
+
});
|
|
7933
|
+
|
|
7934
|
+
it('says when the sweep overview could not be read, so an empty findings list is no all-clear', async () => {
|
|
7935
|
+
const { client } = await bootMcpPair({
|
|
7936
|
+
capabilities: ['products:read'],
|
|
7937
|
+
routes: { [`/v1/products/${PRODUCT_ID}/maintenance/findings`]: { data: [], nextCursor: null } },
|
|
7938
|
+
});
|
|
7939
|
+
|
|
7940
|
+
const findings = parseToolResult(
|
|
7941
|
+
await client.callTool({ name: 'taiga_list_findings', arguments: { productId: PRODUCT_ID } })
|
|
7942
|
+
);
|
|
7943
|
+
|
|
7944
|
+
expect(findings.sweep).toBeNull();
|
|
7945
|
+
expect(findings.sweepNote).toMatch(/not an all-clear/);
|
|
7946
|
+
});
|
|
7947
|
+
|
|
7948
|
+
it("says a waiting run's initiative was not checked when the key cannot read it", async () => {
|
|
7949
|
+
const { client } = await bootMcpPair({
|
|
7950
|
+
capabilities: ['products:read'],
|
|
7951
|
+
routes: {
|
|
7952
|
+
[`/v1/products/${PRODUCT_ID}/builds`]: {
|
|
7953
|
+
data: [
|
|
7954
|
+
{
|
|
7955
|
+
id: 'r1',
|
|
7956
|
+
productId: PRODUCT_ID,
|
|
7957
|
+
backlogItemId: ITEM_ID,
|
|
7958
|
+
status: 'needs_review',
|
|
7959
|
+
prNumber: 4,
|
|
7960
|
+
prUrl: 'https://x/4',
|
|
7961
|
+
},
|
|
7962
|
+
],
|
|
7963
|
+
nextCursor: null,
|
|
7964
|
+
},
|
|
7965
|
+
},
|
|
7966
|
+
});
|
|
7967
|
+
|
|
7968
|
+
const runs = parseToolResult(
|
|
7969
|
+
await client.callTool({ name: 'taiga_list_runs', arguments: { productId: PRODUCT_ID } })
|
|
7970
|
+
).data as Record<string, unknown>[];
|
|
7971
|
+
|
|
7972
|
+
expect(runs[0]!['disposition']).toBe('gated');
|
|
7973
|
+
expect(runs[0]!['outcome']).toMatch(/Whether its initiative still wants it was not checked\.$/);
|
|
7974
|
+
});
|
|
7975
|
+
|
|
7976
|
+
it('reads a control id only in a policy, not in other company documents', async () => {
|
|
7977
|
+
const { client } = await bootMcpPair({
|
|
7978
|
+
capabilities: ALL_CAPABILITIES,
|
|
7979
|
+
routes: {
|
|
7980
|
+
'GET /v1/tenants/current/documents/design_standards': {
|
|
7981
|
+
data: {
|
|
7982
|
+
id: 'd',
|
|
7983
|
+
type: 'design_standards',
|
|
7984
|
+
version: 1,
|
|
7985
|
+
isDraft: false,
|
|
7986
|
+
data: { sections: { forms: { content: 'Store secrets per SEC-5.1.' } } },
|
|
7987
|
+
},
|
|
7988
|
+
},
|
|
7989
|
+
},
|
|
7990
|
+
});
|
|
7991
|
+
|
|
7992
|
+
const doc = parseToolResult(
|
|
7993
|
+
await client.callTool({
|
|
7994
|
+
name: 'taiga_get_document',
|
|
7995
|
+
arguments: { type: 'design_standards', sections: ['SEC-5.1'] },
|
|
7996
|
+
})
|
|
7997
|
+
);
|
|
7998
|
+
const overlay = parseToolResult(
|
|
7999
|
+
await client.callTool({
|
|
8000
|
+
name: 'taiga_get_document',
|
|
8001
|
+
arguments: { type: 'design_standards', sections: ['HC-FI-DATA-1.1'] },
|
|
8002
|
+
})
|
|
8003
|
+
);
|
|
8004
|
+
|
|
8005
|
+
expect(doc).not.toHaveProperty('controls');
|
|
8006
|
+
expect(doc.unknownSections).toEqual(['SEC-5.1']);
|
|
8007
|
+
expect(doc.note).toMatch(/^SEC-5\.1 is a control of policy_security: /);
|
|
8008
|
+
// An overlay family's control belongs to the policy its rightmost prefix names.
|
|
8009
|
+
expect(overlay.note).toMatch(/^HC-FI-DATA-1\.1 is a control of policy_data: /);
|
|
8010
|
+
});
|
|
8011
|
+
|
|
8012
|
+
it('leaves a run still executing as it is, whatever its initiative', async () => {
|
|
8013
|
+
const { client } = await bootMcpPair({
|
|
8014
|
+
capabilities: ['products:read', 'backlog:read'],
|
|
8015
|
+
routes: {
|
|
8016
|
+
[`/v1/products/${PRODUCT_ID}/builds`]: {
|
|
8017
|
+
data: [{ id: 'r1', productId: PRODUCT_ID, backlogItemId: ITEM_ID, status: 'implementing', prUrl: null }],
|
|
8018
|
+
nextCursor: null,
|
|
8019
|
+
},
|
|
8020
|
+
[`GET /v1/backlog/${ITEM_ID}`]: { data: { id: ITEM_ID, status: 'canceled', state: 'canceled' } },
|
|
8021
|
+
},
|
|
8022
|
+
});
|
|
8023
|
+
|
|
8024
|
+
const runs = parseToolResult(
|
|
8025
|
+
await client.callTool({ name: 'taiga_list_runs', arguments: { productId: PRODUCT_ID } })
|
|
8026
|
+
).data as Record<string, unknown>[];
|
|
8027
|
+
|
|
8028
|
+
expect(runs[0]!['disposition']).toBe('in_flight');
|
|
8029
|
+
});
|
|
8030
|
+
|
|
8031
|
+
it('splits the ask-first notice into paid agent work and what others see, in whoami too', async () => {
|
|
8032
|
+
const { client } = await bootMcpPair({ capabilities: ALL_CAPABILITIES });
|
|
8033
|
+
|
|
8034
|
+
const me = parseToolResult(await client.callTool({ name: 'taiga_whoami', arguments: {} }));
|
|
8035
|
+
|
|
8036
|
+
expect(me.tools.askFirstPaid).toContain('taiga_start_import');
|
|
8037
|
+
expect(me.tools.askFirstShared).toContain('taiga_rename_factory');
|
|
8038
|
+
expect(me.capabilities.some((capability: string) => /^(projects|teams):/.test(capability))).toBe(false);
|
|
8039
|
+
});
|
|
8040
|
+
});
|