ai-dev-requirements 0.1.7 → 0.1.9
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.cjs +371 -14
- package/dist/index.mjs +371 -14
- package/dist/index.mjs.map +1 -1
- package/package.json +1 -1
package/dist/index.cjs
CHANGED
|
@@ -123,7 +123,7 @@ const SEARCH_TASKS_QUERY = `
|
|
|
123
123
|
key
|
|
124
124
|
tasks(filterGroup: $filterGroup, orderBy: $orderBy, limit: $limit, includeAncestors: { pathField: "path" }) {
|
|
125
125
|
key uuid number name
|
|
126
|
-
issueType { uuid name }
|
|
126
|
+
issueType { uuid name detailType }
|
|
127
127
|
status { uuid name category }
|
|
128
128
|
priority { value }
|
|
129
129
|
assign { uuid name }
|
|
@@ -132,6 +132,15 @@ const SEARCH_TASKS_QUERY = `
|
|
|
132
132
|
}
|
|
133
133
|
}
|
|
134
134
|
`;
|
|
135
|
+
const ISSUE_TYPES_QUERY = `
|
|
136
|
+
query IssueTypes($orderBy: OrderBy) {
|
|
137
|
+
issueTypes(orderBy: $orderBy) {
|
|
138
|
+
uuid
|
|
139
|
+
name
|
|
140
|
+
detailType
|
|
141
|
+
}
|
|
142
|
+
}
|
|
143
|
+
`;
|
|
135
144
|
const TASK_BY_NUMBER_QUERY = SEARCH_TASKS_QUERY;
|
|
136
145
|
const RELATED_TASKS_QUERY = `
|
|
137
146
|
query Task($key: Key) {
|
|
@@ -193,6 +202,117 @@ const DEFAULT_STATUS_NOT_IN = [
|
|
|
193
202
|
"Dn3k8ffK",
|
|
194
203
|
"TbmY2So5"
|
|
195
204
|
];
|
|
205
|
+
const TESTCASE_LIBRARY_LIST_QUERY = `
|
|
206
|
+
query Q {
|
|
207
|
+
testcaseLibraries {
|
|
208
|
+
uuid name key
|
|
209
|
+
testcaseCaseCount
|
|
210
|
+
}
|
|
211
|
+
}
|
|
212
|
+
`;
|
|
213
|
+
const TESTCASE_MODULE_SEARCH_QUERY = `
|
|
214
|
+
query Q($filter: Filter) {
|
|
215
|
+
testcaseModules(filter: $filter) {
|
|
216
|
+
uuid name key
|
|
217
|
+
parent { uuid name }
|
|
218
|
+
}
|
|
219
|
+
}
|
|
220
|
+
`;
|
|
221
|
+
const TESTCASE_LIST_PAGED_QUERY = `
|
|
222
|
+
query PAGED_LIBRARY_TESTCASE_LIST($testCaseFilter: Filter, $pagination: Pagination) {
|
|
223
|
+
buckets(groupBy: {testcaseCases: {}}, pagination: $pagination) {
|
|
224
|
+
testcaseCases(filterGroup: $testCaseFilter, limit: 10000) {
|
|
225
|
+
uuid name key id
|
|
226
|
+
priority { uuid value }
|
|
227
|
+
type { uuid value }
|
|
228
|
+
assign { uuid name }
|
|
229
|
+
testcaseModule { uuid }
|
|
230
|
+
}
|
|
231
|
+
key
|
|
232
|
+
pageInfo { count totalCount hasNextPage endCursor }
|
|
233
|
+
}
|
|
234
|
+
}
|
|
235
|
+
`;
|
|
236
|
+
const TESTCASE_DETAIL_QUERY = `
|
|
237
|
+
query QUERY_TESTCASES_DETAIL($testCaseFilter: Filter, $stepFilter: Filter) {
|
|
238
|
+
testcaseCases(filter: $testCaseFilter) {
|
|
239
|
+
uuid name key id condition desc path
|
|
240
|
+
assign { uuid name }
|
|
241
|
+
priority { uuid value }
|
|
242
|
+
type { uuid value }
|
|
243
|
+
testcaseLibrary { uuid }
|
|
244
|
+
testcaseModule { uuid }
|
|
245
|
+
relatedTasks { uuid name number }
|
|
246
|
+
}
|
|
247
|
+
testcaseCaseSteps(filter: $stepFilter, orderBy: { index: ASC }) {
|
|
248
|
+
key uuid
|
|
249
|
+
testcaseCase { uuid }
|
|
250
|
+
desc result index
|
|
251
|
+
}
|
|
252
|
+
}
|
|
253
|
+
`;
|
|
254
|
+
function parseOnesSearchIntent(query) {
|
|
255
|
+
if (!query) return "keyword";
|
|
256
|
+
const normalized = query.toLowerCase();
|
|
257
|
+
if (query.includes("缺陷") || normalized.includes("bug")) return "all_bugs";
|
|
258
|
+
if (query.includes("任务")) return "all_tasks";
|
|
259
|
+
return "keyword";
|
|
260
|
+
}
|
|
261
|
+
function extractAssigneeName(query, intent) {
|
|
262
|
+
if (intent === "keyword") return null;
|
|
263
|
+
const trimmed = query.trim();
|
|
264
|
+
if (!trimmed) return null;
|
|
265
|
+
const ownerStyleMatch = trimmed.match(/\u8D1F\u8D23\u4EBA\u4E3A(.+?)\u7684?(?:\u7F3A\u9677|bug)$/i);
|
|
266
|
+
if (ownerStyleMatch?.[1]) return ownerStyleMatch[1].trim();
|
|
267
|
+
const candidate = trimmed.match(/^(查询)?(.+?)的(?:缺陷|bug|任务)$/i)?.[2]?.trim();
|
|
268
|
+
if (!candidate || candidate.includes("我")) return null;
|
|
269
|
+
return candidate;
|
|
270
|
+
}
|
|
271
|
+
function extractNamedAssignee(query, intent) {
|
|
272
|
+
if (intent === "keyword") return null;
|
|
273
|
+
const compact = query.replace(/\s+/g, "").trim();
|
|
274
|
+
if (!compact) return null;
|
|
275
|
+
const ownerStyleMatch = compact.match(/(?:\u8D1F\u8D23\u4EBA\u4E3A|\u8D1F\u8D23\u4EBA\u662F|\u6307\u6D3E\u7ED9|\u5206\u914D\u7ED9)(.+?)\u7684?(?:\u7F3A\u9677|bug|\u4EFB\u52A1)$/i);
|
|
276
|
+
if (ownerStyleMatch?.[1]) return ownerStyleMatch[1].trim();
|
|
277
|
+
const candidate = compact.match(/^(?:\u67E5\u8BE2|\u67E5\u627E|\u641C\u7D22)?(.+?)\u7684?(?:\u7F3A\u9677|bug|\u4EFB\u52A1)$/i)?.[1]?.trim();
|
|
278
|
+
if (!candidate || candidate.startsWith("我") || /^(?:\u6211|\u6211\u7684|\u6211\u6240\u6709|\u6211\u5168\u90E8|\u672C\u4EBA|\u5F53\u524D\u7528\u6237)$/.test(candidate)) return null;
|
|
279
|
+
return candidate;
|
|
280
|
+
}
|
|
281
|
+
function getBugStatusPriority(task) {
|
|
282
|
+
if (task.status?.category === "to_do") return 0;
|
|
283
|
+
if (task.status?.category === "in_progress") return 1;
|
|
284
|
+
return Number.POSITIVE_INFINITY;
|
|
285
|
+
}
|
|
286
|
+
function isOpenOrInProgressBug(task) {
|
|
287
|
+
const category = task.status?.category;
|
|
288
|
+
return category === "to_do" || category === "in_progress";
|
|
289
|
+
}
|
|
290
|
+
function extractTeamUsers(payload) {
|
|
291
|
+
const record = payload && typeof payload === "object" ? payload : null;
|
|
292
|
+
if (!record) return [];
|
|
293
|
+
const rawUsers = [
|
|
294
|
+
record.users,
|
|
295
|
+
record.items,
|
|
296
|
+
record.list,
|
|
297
|
+
record.results,
|
|
298
|
+
record.data?.users,
|
|
299
|
+
record.data?.items,
|
|
300
|
+
record.data?.list,
|
|
301
|
+
record.data?.results
|
|
302
|
+
].find(Array.isArray);
|
|
303
|
+
if (!rawUsers) return [];
|
|
304
|
+
return rawUsers.map((item) => {
|
|
305
|
+
const user = item && typeof item === "object" ? item : null;
|
|
306
|
+
if (!user) return null;
|
|
307
|
+
const uuid = user.uuid ?? user.user?.uuid ?? user.orgUser?.uuid ?? user.orgUserUuid ?? user.org_user_uuid ?? user.org_user?.org_user_uuid;
|
|
308
|
+
const name = user.name ?? user.user?.name ?? user.orgUser?.name ?? user.org_user?.name;
|
|
309
|
+
if (!uuid || !name) return null;
|
|
310
|
+
return {
|
|
311
|
+
uuid,
|
|
312
|
+
name
|
|
313
|
+
};
|
|
314
|
+
}).filter((item) => item !== null);
|
|
315
|
+
}
|
|
196
316
|
function base64Url(buffer) {
|
|
197
317
|
return buffer.toString("base64").replace(/\+/g, "-").replace(/\//g, "_").replace(/=+$/g, "");
|
|
198
318
|
}
|
|
@@ -223,6 +343,7 @@ function toRequirement(task, description = "") {
|
|
|
223
343
|
}
|
|
224
344
|
var OnesAdapter = class extends BaseAdapter {
|
|
225
345
|
session = null;
|
|
346
|
+
issueTypesCache = null;
|
|
226
347
|
constructor(sourceType, config, resolvedAuth) {
|
|
227
348
|
super(sourceType, config, resolvedAuth);
|
|
228
349
|
}
|
|
@@ -383,6 +504,42 @@ var OnesAdapter = class extends BaseAdapter {
|
|
|
383
504
|
}
|
|
384
505
|
return response.json();
|
|
385
506
|
}
|
|
507
|
+
async fetchIssueTypes() {
|
|
508
|
+
if (this.issueTypesCache) return this.issueTypesCache;
|
|
509
|
+
this.issueTypesCache = (await this.graphql(ISSUE_TYPES_QUERY, { orderBy: { namePinyin: "ASC" } }, "issueTypes")).data?.issueTypes ?? [];
|
|
510
|
+
return this.issueTypesCache;
|
|
511
|
+
}
|
|
512
|
+
async searchTeamUsers(keyword) {
|
|
513
|
+
const session = await this.login();
|
|
514
|
+
const url = `${this.config.apiBase}/project/api/project/team/${session.teamUuid}/users/search`;
|
|
515
|
+
const response = await fetch(url, {
|
|
516
|
+
method: "POST",
|
|
517
|
+
headers: {
|
|
518
|
+
"Authorization": `Bearer ${session.accessToken}`,
|
|
519
|
+
"Content-Type": "application/json"
|
|
520
|
+
},
|
|
521
|
+
body: JSON.stringify({
|
|
522
|
+
keyword,
|
|
523
|
+
status: [1],
|
|
524
|
+
team_member_status: [1, 4],
|
|
525
|
+
types: [1, 10]
|
|
526
|
+
})
|
|
527
|
+
});
|
|
528
|
+
if (!response.ok) {
|
|
529
|
+
const text = await response.text().catch(() => "");
|
|
530
|
+
throw new Error(`ONES user search error: ${response.status} ${text}`);
|
|
531
|
+
}
|
|
532
|
+
return extractTeamUsers(await response.json());
|
|
533
|
+
}
|
|
534
|
+
async resolveAssigneeUuid(name) {
|
|
535
|
+
const trimmed = name.trim();
|
|
536
|
+
if (!trimmed) return null;
|
|
537
|
+
const users = await this.searchTeamUsers(trimmed);
|
|
538
|
+
const exactMatch = users.find((user) => user.name === trimmed);
|
|
539
|
+
if (exactMatch) return exactMatch.uuid;
|
|
540
|
+
const normalizedTarget = trimmed.toLowerCase();
|
|
541
|
+
return users.find((user) => user.name.toLowerCase().includes(normalizedTarget))?.uuid ?? null;
|
|
542
|
+
}
|
|
386
543
|
/**
|
|
387
544
|
* Fetch task info via REST API (includes description/rich fields not available in GraphQL).
|
|
388
545
|
* Reference: ones/packages/core/src/tasks.ts → fetchTaskInfo
|
|
@@ -547,6 +704,27 @@ var OnesAdapter = class extends BaseAdapter {
|
|
|
547
704
|
async searchRequirements(params) {
|
|
548
705
|
const page = params.page ?? 1;
|
|
549
706
|
const pageSize = params.pageSize ?? 50;
|
|
707
|
+
const intent = parseOnesSearchIntent(params.query);
|
|
708
|
+
const assigneeName = extractNamedAssignee(params.query, intent) ?? extractAssigneeName(params.query, intent);
|
|
709
|
+
const assigneeUuid = assigneeName ? await this.resolveAssigneeUuid(assigneeName) : null;
|
|
710
|
+
if (assigneeName && !assigneeUuid) return {
|
|
711
|
+
items: [],
|
|
712
|
+
total: 0,
|
|
713
|
+
page,
|
|
714
|
+
pageSize
|
|
715
|
+
};
|
|
716
|
+
let bugTypeUuids = [];
|
|
717
|
+
let taskTypeUuids = [];
|
|
718
|
+
if (intent === "all_bugs" || intent === "all_tasks") {
|
|
719
|
+
const issueTypes = await this.fetchIssueTypes();
|
|
720
|
+
bugTypeUuids = issueTypes.filter((item) => item.detailType === 3).map((item) => item.uuid);
|
|
721
|
+
taskTypeUuids = issueTypes.filter((item) => item.detailType === 2).map((item) => item.uuid);
|
|
722
|
+
}
|
|
723
|
+
const filter = { status_notIn: DEFAULT_STATUS_NOT_IN };
|
|
724
|
+
if (assigneeName) filter.assign_in = [assigneeUuid];
|
|
725
|
+
else filter.assign_in = ["${currentUser}"];
|
|
726
|
+
if (intent === "all_bugs") filter.issueType_in = bugTypeUuids;
|
|
727
|
+
if (intent === "all_tasks") filter.issueType_in = taskTypeUuids;
|
|
550
728
|
let tasks = (await this.graphql(SEARCH_TASKS_QUERY, {
|
|
551
729
|
groupBy: { tasks: {} },
|
|
552
730
|
groupOrderBy: null,
|
|
@@ -554,10 +732,7 @@ var OnesAdapter = class extends BaseAdapter {
|
|
|
554
732
|
position: "ASC",
|
|
555
733
|
createTime: "DESC"
|
|
556
734
|
},
|
|
557
|
-
filterGroup: [
|
|
558
|
-
assign_in: ["${currentUser}"],
|
|
559
|
-
status_notIn: DEFAULT_STATUS_NOT_IN
|
|
560
|
-
}],
|
|
735
|
+
filterGroup: [filter],
|
|
561
736
|
search: null,
|
|
562
737
|
pagination: {
|
|
563
738
|
limit: pageSize * page,
|
|
@@ -565,8 +740,11 @@ var OnesAdapter = class extends BaseAdapter {
|
|
|
565
740
|
},
|
|
566
741
|
limit: 1e3
|
|
567
742
|
}, "group-task-data")).data?.buckets?.flatMap((b) => b.tasks ?? []) ?? [];
|
|
568
|
-
if (
|
|
569
|
-
|
|
743
|
+
if (intent === "all_bugs") tasks = tasks.filter((task) => task.issueType?.uuid ? bugTypeUuids.includes(task.issueType.uuid) : false).filter((task) => isOpenOrInProgressBug(task)).sort((a, b) => getBugStatusPriority(a) - getBugStatusPriority(b));
|
|
744
|
+
if (intent === "all_tasks") tasks = tasks.filter((task) => task.issueType?.uuid ? taskTypeUuids.includes(task.issueType.uuid) : false);
|
|
745
|
+
if (assigneeUuid) tasks = tasks.filter((task) => task.assign?.uuid === assigneeUuid);
|
|
746
|
+
if (intent === "keyword" && params.query) {
|
|
747
|
+
const keyword = params.query.trim();
|
|
570
748
|
const lower = keyword.toLowerCase();
|
|
571
749
|
const numMatch = keyword.match(/^#?(\d+)$/);
|
|
572
750
|
if (numMatch) tasks = tasks.filter((t) => t.number === Number.parseInt(numMatch[1], 10));
|
|
@@ -654,6 +832,108 @@ var OnesAdapter = class extends BaseAdapter {
|
|
|
654
832
|
raw: task
|
|
655
833
|
};
|
|
656
834
|
}
|
|
835
|
+
async getTestcases(params) {
|
|
836
|
+
let libraryUuid = params.libraryUuid ?? this.config.options?.testcaseLibraryUuid;
|
|
837
|
+
if (!libraryUuid) {
|
|
838
|
+
const libs = (await this.graphql(TESTCASE_LIBRARY_LIST_QUERY, {}, "library-select")).data?.testcaseLibraries ?? [];
|
|
839
|
+
if (libs.length === 0) throw new Error("ONES: No testcase libraries found for this team");
|
|
840
|
+
libs.sort((a, b) => b.testcaseCaseCount - a.testcaseCaseCount);
|
|
841
|
+
libraryUuid = libs[0].uuid;
|
|
842
|
+
}
|
|
843
|
+
const task = ((await this.graphql(SEARCH_TASKS_QUERY, {
|
|
844
|
+
groupBy: { tasks: {} },
|
|
845
|
+
groupOrderBy: null,
|
|
846
|
+
orderBy: { createTime: "DESC" },
|
|
847
|
+
filterGroup: [{ number_in: [params.taskNumber] }],
|
|
848
|
+
search: null,
|
|
849
|
+
pagination: {
|
|
850
|
+
limit: 10,
|
|
851
|
+
preciseCount: false
|
|
852
|
+
},
|
|
853
|
+
limit: 10
|
|
854
|
+
}, "group-task-data")).data?.buckets?.flatMap((b) => b.tasks ?? []) ?? []).find((t) => t.number === params.taskNumber);
|
|
855
|
+
if (!task) throw new Error(`ONES: Task #${params.taskNumber} not found`);
|
|
856
|
+
const modules = (await this.graphql(TESTCASE_MODULE_SEARCH_QUERY, { filter: {
|
|
857
|
+
testcaseLibrary_in: [libraryUuid],
|
|
858
|
+
name_match: `#${params.taskNumber}`
|
|
859
|
+
} }, "find-testcase-module")).data?.testcaseModules ?? [];
|
|
860
|
+
if (modules.length === 0) throw new Error(`ONES: No testcase module matching "#${params.taskNumber}" in library ${libraryUuid}`);
|
|
861
|
+
const mod = modules[0];
|
|
862
|
+
const caseList = [];
|
|
863
|
+
let cursor = "";
|
|
864
|
+
let totalCount = 0;
|
|
865
|
+
while (true) {
|
|
866
|
+
const bucket = (await this.graphql(TESTCASE_LIST_PAGED_QUERY, {
|
|
867
|
+
testCaseFilter: [{
|
|
868
|
+
testcaseLibrary_in: [libraryUuid],
|
|
869
|
+
path_match: mod.uuid
|
|
870
|
+
}],
|
|
871
|
+
pagination: {
|
|
872
|
+
limit: 50,
|
|
873
|
+
after: cursor,
|
|
874
|
+
preciseCount: true
|
|
875
|
+
}
|
|
876
|
+
}, "testcase-list-paged")).data?.buckets?.[0];
|
|
877
|
+
if (!bucket) break;
|
|
878
|
+
caseList.push(...bucket.testcaseCases ?? []);
|
|
879
|
+
totalCount = bucket.pageInfo.totalCount;
|
|
880
|
+
if (!bucket.pageInfo.hasNextPage) break;
|
|
881
|
+
cursor = bucket.pageInfo.endCursor;
|
|
882
|
+
}
|
|
883
|
+
if (caseList.length === 0) return {
|
|
884
|
+
taskNumber: params.taskNumber,
|
|
885
|
+
taskName: task.name,
|
|
886
|
+
moduleName: mod.name,
|
|
887
|
+
moduleUuid: mod.uuid,
|
|
888
|
+
totalCount: 0,
|
|
889
|
+
cases: []
|
|
890
|
+
};
|
|
891
|
+
const allCases = [];
|
|
892
|
+
const BATCH_SIZE = 20;
|
|
893
|
+
for (let i = 0; i < caseList.length; i += BATCH_SIZE) {
|
|
894
|
+
const uuids = caseList.slice(i, i + BATCH_SIZE).map((c) => c.uuid);
|
|
895
|
+
const detailData = await this.graphql(TESTCASE_DETAIL_QUERY, {
|
|
896
|
+
testCaseFilter: { uuid_in: [...uuids, null] },
|
|
897
|
+
stepFilter: { testcaseCase_in: uuids }
|
|
898
|
+
}, "library-testcase-detail");
|
|
899
|
+
const cases = detailData.data?.testcaseCases ?? [];
|
|
900
|
+
const steps = detailData.data?.testcaseCaseSteps ?? [];
|
|
901
|
+
const stepsByCase = /* @__PURE__ */ new Map();
|
|
902
|
+
for (const step of steps) {
|
|
903
|
+
const caseUuid = step.testcaseCase.uuid;
|
|
904
|
+
if (!stepsByCase.has(caseUuid)) stepsByCase.set(caseUuid, []);
|
|
905
|
+
stepsByCase.get(caseUuid).push({
|
|
906
|
+
uuid: step.uuid,
|
|
907
|
+
index: step.index,
|
|
908
|
+
desc: step.desc ?? "",
|
|
909
|
+
result: step.result ?? ""
|
|
910
|
+
});
|
|
911
|
+
}
|
|
912
|
+
for (const c of cases) {
|
|
913
|
+
const freshDesc = c.desc ? await this.refreshImageUrls(c.desc) : "";
|
|
914
|
+
allCases.push({
|
|
915
|
+
uuid: c.uuid,
|
|
916
|
+
id: c.id,
|
|
917
|
+
name: c.name,
|
|
918
|
+
priority: c.priority?.value ?? "N/A",
|
|
919
|
+
type: c.type?.value ?? "Unknown",
|
|
920
|
+
assignName: c.assign?.name ?? null,
|
|
921
|
+
condition: c.condition ?? "",
|
|
922
|
+
desc: freshDesc,
|
|
923
|
+
steps: (stepsByCase.get(c.uuid) ?? []).sort((a, b) => a.index - b.index),
|
|
924
|
+
modulePath: c.path ?? ""
|
|
925
|
+
});
|
|
926
|
+
}
|
|
927
|
+
}
|
|
928
|
+
return {
|
|
929
|
+
taskNumber: params.taskNumber,
|
|
930
|
+
taskName: task.name,
|
|
931
|
+
moduleName: mod.name,
|
|
932
|
+
moduleUuid: mod.uuid,
|
|
933
|
+
totalCount,
|
|
934
|
+
cases: allCases
|
|
935
|
+
};
|
|
936
|
+
}
|
|
657
937
|
};
|
|
658
938
|
|
|
659
939
|
//#endregion
|
|
@@ -755,6 +1035,11 @@ function loadConfigFromEnv() {
|
|
|
755
1035
|
const account = process.env.ONES_ACCOUNT;
|
|
756
1036
|
const password = process.env.ONES_PASSWORD;
|
|
757
1037
|
if (!apiBase || !account || !password) return null;
|
|
1038
|
+
let options;
|
|
1039
|
+
const configPath = findConfigFile(process.cwd());
|
|
1040
|
+
if (configPath) try {
|
|
1041
|
+
options = JSON.parse((0, node_fs.readFileSync)(configPath, "utf-8"))?.sources?.ones?.options;
|
|
1042
|
+
} catch {}
|
|
758
1043
|
return {
|
|
759
1044
|
sources: { ones: {
|
|
760
1045
|
enabled: true,
|
|
@@ -763,7 +1048,8 @@ function loadConfigFromEnv() {
|
|
|
763
1048
|
type: "ones-pkce",
|
|
764
1049
|
emailEnv: "ONES_ACCOUNT",
|
|
765
1050
|
passwordEnv: "ONES_PASSWORD"
|
|
766
|
-
}
|
|
1051
|
+
},
|
|
1052
|
+
options
|
|
767
1053
|
} },
|
|
768
1054
|
defaultSource: "ones"
|
|
769
1055
|
};
|
|
@@ -976,6 +1262,58 @@ function formatRequirement(req) {
|
|
|
976
1262
|
return lines.join("\n");
|
|
977
1263
|
}
|
|
978
1264
|
|
|
1265
|
+
//#endregion
|
|
1266
|
+
//#region src/tools/get-testcases.ts
|
|
1267
|
+
const GetTestcasesSchema = zod_v4.z.object({
|
|
1268
|
+
taskNumber: zod_v4.z.string().describe("Task number (e.g. \"302\" or \"#302\"). Finds all testcases in the matching module."),
|
|
1269
|
+
libraryUuid: zod_v4.z.string().optional().describe("Testcase library UUID. If omitted, uses configured default."),
|
|
1270
|
+
source: zod_v4.z.string().optional().describe("Source to fetch from. If omitted, uses the default source.")
|
|
1271
|
+
});
|
|
1272
|
+
async function handleGetTestcases(input, adapters, defaultSource) {
|
|
1273
|
+
const sourceType = input.source ?? defaultSource;
|
|
1274
|
+
if (!sourceType) throw new Error("No source specified and no default source configured");
|
|
1275
|
+
const adapter = adapters.get(sourceType);
|
|
1276
|
+
if (!adapter) throw new Error(`Source "${sourceType}" is not configured. Available: ${[...adapters.keys()].join(", ")}`);
|
|
1277
|
+
const numMatch = input.taskNumber.match(/^#?(\d+)$/);
|
|
1278
|
+
if (!numMatch) throw new Error(`Invalid task number: "${input.taskNumber}". Expected a number like "302" or "#302".`);
|
|
1279
|
+
return { content: [{
|
|
1280
|
+
type: "text",
|
|
1281
|
+
text: formatTestcases(await adapter.getTestcases({
|
|
1282
|
+
taskNumber: Number.parseInt(numMatch[1], 10),
|
|
1283
|
+
libraryUuid: input.libraryUuid
|
|
1284
|
+
}))
|
|
1285
|
+
}] };
|
|
1286
|
+
}
|
|
1287
|
+
function formatTestcases(result) {
|
|
1288
|
+
const lines = [
|
|
1289
|
+
`# ${result.taskName} — 测试用例`,
|
|
1290
|
+
"",
|
|
1291
|
+
`- **模块**: ${result.moduleName}`,
|
|
1292
|
+
`- **共 ${result.totalCount} 个用例**(已加载 ${result.cases.length} 个)`,
|
|
1293
|
+
""
|
|
1294
|
+
];
|
|
1295
|
+
for (const tc of result.cases) {
|
|
1296
|
+
lines.push(`## ${tc.id} ${tc.name}`);
|
|
1297
|
+
lines.push("");
|
|
1298
|
+
lines.push(`- 优先级: ${tc.priority} | 类型: ${tc.type}`);
|
|
1299
|
+
if (tc.assignName) lines.push(`- 维护人: ${tc.assignName}`);
|
|
1300
|
+
if (tc.condition) lines.push(`- 前置条件: ${tc.condition}`);
|
|
1301
|
+
if (tc.desc) lines.push(`- 备注: ${tc.desc}`);
|
|
1302
|
+
if (tc.steps.length > 0) {
|
|
1303
|
+
lines.push("");
|
|
1304
|
+
lines.push("| 步骤 | 操作描述 | 预期结果 |");
|
|
1305
|
+
lines.push("|------|----------|----------|");
|
|
1306
|
+
for (const step of tc.steps) {
|
|
1307
|
+
const desc = step.desc.replace(/\n/g, "<br>");
|
|
1308
|
+
const res = step.result.replace(/\n/g, "<br>");
|
|
1309
|
+
lines.push(`| ${step.index + 1} | ${desc} | ${res} |`);
|
|
1310
|
+
}
|
|
1311
|
+
}
|
|
1312
|
+
lines.push("");
|
|
1313
|
+
}
|
|
1314
|
+
return lines.join("\n");
|
|
1315
|
+
}
|
|
1316
|
+
|
|
979
1317
|
//#endregion
|
|
980
1318
|
//#region src/tools/list-sources.ts
|
|
981
1319
|
async function handleListSources(adapters, config) {
|
|
@@ -1010,6 +1348,9 @@ const SearchRequirementsSchema = zod_v4.z.object({
|
|
|
1010
1348
|
page: zod_v4.z.number().int().min(1).optional().describe("Page number (default: 1)"),
|
|
1011
1349
|
pageSize: zod_v4.z.number().int().min(1).max(50).optional().describe("Results per page (default: 20, max: 50)")
|
|
1012
1350
|
});
|
|
1351
|
+
function formatStatusMarker(status) {
|
|
1352
|
+
return `[${status.toUpperCase()}]`;
|
|
1353
|
+
}
|
|
1013
1354
|
async function handleSearchRequirements(input, adapters, defaultSource) {
|
|
1014
1355
|
const sourceType = input.source ?? defaultSource;
|
|
1015
1356
|
if (!sourceType) throw new Error("No source specified and no default source configured");
|
|
@@ -1020,15 +1361,18 @@ async function handleSearchRequirements(input, adapters, defaultSource) {
|
|
|
1020
1361
|
page: input.page,
|
|
1021
1362
|
pageSize: input.pageSize
|
|
1022
1363
|
});
|
|
1023
|
-
const lines = [`Found **${result.total}**
|
|
1364
|
+
const lines = [`Found **${result.total}** items (page ${result.page}/${Math.ceil(result.total / result.pageSize) || 1}):`, ""];
|
|
1365
|
+
if (/\u6211.*\u7F3A\u9677|bug|\u6211.*\u4EFB\u52A1/i.test(input.query)) {
|
|
1366
|
+
lines.push(`Query: ${input.query}`);
|
|
1367
|
+
lines.push("Use an item ID or number in the next step to fetch detail.");
|
|
1368
|
+
lines.push("");
|
|
1369
|
+
}
|
|
1024
1370
|
for (const item of result.items) {
|
|
1025
|
-
lines.push(`### ${item.id}: ${item.title}`);
|
|
1371
|
+
lines.push(`### ${formatStatusMarker(item.status)} ${item.id}: ${item.title}`);
|
|
1026
1372
|
lines.push(`- Status: ${item.status} | Priority: ${item.priority} | Type: ${item.type}`);
|
|
1027
1373
|
lines.push(`- Assignee: ${item.assignee ?? "Unassigned"}`);
|
|
1028
|
-
|
|
1029
|
-
|
|
1030
|
-
lines.push(`- ${desc}`);
|
|
1031
|
-
}
|
|
1374
|
+
const desc = item.description ? item.description.length > 200 ? `${item.description.slice(0, 200)}...` : item.description : "(empty)";
|
|
1375
|
+
lines.push(`- Content: ${desc}`);
|
|
1032
1376
|
lines.push("");
|
|
1033
1377
|
}
|
|
1034
1378
|
return { content: [{
|
|
@@ -1149,6 +1493,19 @@ async function main() {
|
|
|
1149
1493
|
};
|
|
1150
1494
|
}
|
|
1151
1495
|
});
|
|
1496
|
+
server.tool("get_testcases", "Get all test cases for a task by its number (e.g. 302). Searches the testcase library for a matching module and returns all cases with steps.", GetTestcasesSchema.shape, async (params) => {
|
|
1497
|
+
try {
|
|
1498
|
+
return await handleGetTestcases(params, adapters, config.config.defaultSource);
|
|
1499
|
+
} catch (err) {
|
|
1500
|
+
return {
|
|
1501
|
+
content: [{
|
|
1502
|
+
type: "text",
|
|
1503
|
+
text: `Error: ${err.message}`
|
|
1504
|
+
}],
|
|
1505
|
+
isError: true
|
|
1506
|
+
};
|
|
1507
|
+
}
|
|
1508
|
+
});
|
|
1152
1509
|
const transport = new _modelcontextprotocol_sdk_server_stdio_js.StdioServerTransport();
|
|
1153
1510
|
await server.connect(transport);
|
|
1154
1511
|
}
|