@tlog/mcp 0.1.3
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +281 -0
- package/dist/constants.d.ts +4 -0
- package/dist/constants.d.ts.map +1 -0
- package/dist/constants.js +4 -0
- package/dist/constants.js.map +1 -0
- package/dist/core-tools.d.ts +146 -0
- package/dist/core-tools.d.ts.map +1 -0
- package/dist/core-tools.js +884 -0
- package/dist/core-tools.js.map +1 -0
- package/dist/index.d.ts +22 -0
- package/dist/index.d.ts.map +1 -0
- package/dist/index.js +30 -0
- package/dist/index.js.map +1 -0
- package/dist/normalization.d.ts +5 -0
- package/dist/normalization.d.ts.map +1 -0
- package/dist/normalization.js +186 -0
- package/dist/normalization.js.map +1 -0
- package/dist/runtime.d.ts +4 -0
- package/dist/runtime.d.ts.map +1 -0
- package/dist/runtime.js +19 -0
- package/dist/runtime.js.map +1 -0
- package/dist/schema-contract.d.ts +15 -0
- package/dist/schema-contract.d.ts.map +1 -0
- package/dist/schema-contract.js +439 -0
- package/dist/schema-contract.js.map +1 -0
- package/dist/schemas.d.ts +172 -0
- package/dist/schemas.d.ts.map +1 -0
- package/dist/schemas.js +154 -0
- package/dist/schemas.js.map +1 -0
- package/dist/server.d.ts +3 -0
- package/dist/server.d.ts.map +1 -0
- package/dist/server.js +594 -0
- package/dist/server.js.map +1 -0
- package/dist/types.d.ts +23 -0
- package/dist/types.d.ts.map +1 -0
- package/dist/types.js +2 -0
- package/dist/types.js.map +1 -0
- package/dist/utils.d.ts +14 -0
- package/dist/utils.d.ts.map +1 -0
- package/dist/utils.js +146 -0
- package/dist/utils.js.map +1 -0
- package/package.json +39 -0
|
@@ -0,0 +1,884 @@
|
|
|
1
|
+
import { mkdir, readdir, unlink } from "node:fs/promises";
|
|
2
|
+
import { basename, join } from "node:path";
|
|
3
|
+
import { calculateBurndown, buildCaseFileName, buildDefaultCase, buildDefaultSuite, buildIdIndex, detectEntityType, extractTemplateFromDirectory, readYamlFile, resolveById, stringifyYaml, suiteSchema, testCaseSchema, validateCase, validateSuite, writeYamlFileAtomic } from "@tlog/shared";
|
|
4
|
+
import { normalizeCaseCandidate, normalizeSuiteCandidate } from "./normalization.js";
|
|
5
|
+
import { asObject, assertNoOverwrite, extractPromptMetadata, listYamlFiles, resolvePathInsideWorkspace, selectAppliedMode, summarizeDiff, toRelativePath } from "./utils.js";
|
|
6
|
+
function inferDescriptionFromText(value, fallbackTitle) {
|
|
7
|
+
if (typeof value === "string" && value.trim().length > 0) {
|
|
8
|
+
return value.trim();
|
|
9
|
+
}
|
|
10
|
+
return fallbackTitle;
|
|
11
|
+
}
|
|
12
|
+
function planPathForSuite(workspaceRoot, targetDir, _suite) {
|
|
13
|
+
const resolvedDir = resolvePathInsideWorkspace(workspaceRoot, targetDir);
|
|
14
|
+
return join(resolvedDir, "index.yaml");
|
|
15
|
+
}
|
|
16
|
+
const ID_PATTERN = /^[A-Za-z0-9_-]+$/;
|
|
17
|
+
function assertValidIdFormat(id) {
|
|
18
|
+
if (!ID_PATTERN.test(id)) {
|
|
19
|
+
throw new Error(`validation: invalid id format (${id}) expected ^[A-Za-z0-9_-]+$`);
|
|
20
|
+
}
|
|
21
|
+
}
|
|
22
|
+
async function assertNoDuplicateId(workspaceRoot, dir, id, currentPath) {
|
|
23
|
+
const resolvedDir = resolvePathInsideWorkspace(workspaceRoot, dir);
|
|
24
|
+
let index;
|
|
25
|
+
try {
|
|
26
|
+
index = await buildIdIndex(resolvedDir);
|
|
27
|
+
}
|
|
28
|
+
catch (error) {
|
|
29
|
+
if (String(error).includes("ENOENT")) {
|
|
30
|
+
return;
|
|
31
|
+
}
|
|
32
|
+
throw error;
|
|
33
|
+
}
|
|
34
|
+
const duplicate = index.duplicates.find((item) => item.id === id);
|
|
35
|
+
if (duplicate) {
|
|
36
|
+
const paths = duplicate.paths.map((path) => toRelativePath(workspaceRoot, path));
|
|
37
|
+
throw new Error(`conflict: duplicate id detected (${id}) paths=${paths.join(",")}`);
|
|
38
|
+
}
|
|
39
|
+
const existing = index.byId.get(id);
|
|
40
|
+
if (existing && existing.path !== currentPath) {
|
|
41
|
+
throw new Error(`conflict: id already exists (${id}) path=${toRelativePath(workspaceRoot, existing.path)}`);
|
|
42
|
+
}
|
|
43
|
+
}
|
|
44
|
+
function planPathForCase(workspaceRoot, targetDir, testCase) {
|
|
45
|
+
const resolvedDir = resolvePathInsideWorkspace(workspaceRoot, targetDir);
|
|
46
|
+
return join(resolvedDir, `${testCase.id}.testcase.yaml`);
|
|
47
|
+
}
|
|
48
|
+
async function writeEntityIfRequested(workspaceRoot, filePath, payload, write) {
|
|
49
|
+
const yamlText = stringifyYaml(payload);
|
|
50
|
+
if (!write) {
|
|
51
|
+
return {
|
|
52
|
+
yamlText,
|
|
53
|
+
writtenFile: null
|
|
54
|
+
};
|
|
55
|
+
}
|
|
56
|
+
const resolvedPath = resolvePathInsideWorkspace(workspaceRoot, filePath);
|
|
57
|
+
assertNoOverwrite(resolvedPath);
|
|
58
|
+
await writeYamlFileAtomic(resolvedPath, payload);
|
|
59
|
+
return {
|
|
60
|
+
yamlText,
|
|
61
|
+
writtenFile: toRelativePath(workspaceRoot, resolvedPath)
|
|
62
|
+
};
|
|
63
|
+
}
|
|
64
|
+
export async function createSuiteFromPromptCore(input) {
|
|
65
|
+
const extracted = extractPromptMetadata(input.instruction, "suite");
|
|
66
|
+
assertValidIdFormat(extracted.id);
|
|
67
|
+
await assertNoDuplicateId(input.workspaceRoot, input.targetDir, extracted.id);
|
|
68
|
+
const defaults = input.defaults ?? {};
|
|
69
|
+
const description = inferDescriptionFromText(defaults.description, extracted.title);
|
|
70
|
+
const normalized = normalizeSuiteCandidate({
|
|
71
|
+
id: extracted.id,
|
|
72
|
+
title: extracted.title,
|
|
73
|
+
description,
|
|
74
|
+
...defaults
|
|
75
|
+
}, {
|
|
76
|
+
id: extracted.id,
|
|
77
|
+
title: extracted.title,
|
|
78
|
+
description,
|
|
79
|
+
...defaults
|
|
80
|
+
});
|
|
81
|
+
const outputPath = planPathForSuite(input.workspaceRoot, input.targetDir, normalized.entity);
|
|
82
|
+
const writeResult = await writeEntityIfRequested(input.workspaceRoot, outputPath, normalized.entity, input.write ?? false);
|
|
83
|
+
return {
|
|
84
|
+
suite: normalized.entity,
|
|
85
|
+
yamlText: writeResult.yamlText,
|
|
86
|
+
writtenFile: writeResult.writtenFile,
|
|
87
|
+
warnings: [...extracted.warnings, ...normalized.warnings],
|
|
88
|
+
diffSummary: ["created suite"]
|
|
89
|
+
};
|
|
90
|
+
}
|
|
91
|
+
export async function createTestcaseFromPromptCore(input) {
|
|
92
|
+
function asStringArray(value) {
|
|
93
|
+
if (!Array.isArray(value)) {
|
|
94
|
+
return [];
|
|
95
|
+
}
|
|
96
|
+
return value.filter((item) => typeof item === "string" && item.trim().length > 0);
|
|
97
|
+
}
|
|
98
|
+
function buildTestsFromContext(context) {
|
|
99
|
+
if (!context) {
|
|
100
|
+
return [];
|
|
101
|
+
}
|
|
102
|
+
const testsRaw = Array.isArray(context.tests) ? context.tests : [];
|
|
103
|
+
const fromTests = [];
|
|
104
|
+
for (let index = 0; index < testsRaw.length; index += 1) {
|
|
105
|
+
const obj = asObject(testsRaw[index]);
|
|
106
|
+
const expected = typeof obj.expected === "string" ? obj.expected.trim() : "";
|
|
107
|
+
if (expected.length === 0) {
|
|
108
|
+
continue;
|
|
109
|
+
}
|
|
110
|
+
fromTests.push({
|
|
111
|
+
name: typeof obj.name === "string" && obj.name.trim().length > 0 ? obj.name : `generated-${index + 1}`,
|
|
112
|
+
expected,
|
|
113
|
+
actual: typeof obj.actual === "string" ? obj.actual : "",
|
|
114
|
+
trails: Array.isArray(obj.trails) ? obj.trails.filter((v) => typeof v === "string") : [],
|
|
115
|
+
status: obj.status ?? null
|
|
116
|
+
});
|
|
117
|
+
}
|
|
118
|
+
if (fromTests.length > 0) {
|
|
119
|
+
return fromTests;
|
|
120
|
+
}
|
|
121
|
+
const expectedRaw = context.expected;
|
|
122
|
+
const expectedList = typeof expectedRaw === "string" && expectedRaw.trim().length > 0
|
|
123
|
+
? [expectedRaw.trim()]
|
|
124
|
+
: asStringArray(expectedRaw);
|
|
125
|
+
return expectedList.map((expected, index) => ({
|
|
126
|
+
name: `generated-${index + 1}`,
|
|
127
|
+
expected,
|
|
128
|
+
actual: "",
|
|
129
|
+
trails: [],
|
|
130
|
+
status: null
|
|
131
|
+
}));
|
|
132
|
+
}
|
|
133
|
+
function buildCaseHintsFromInstruction(instruction) {
|
|
134
|
+
return {
|
|
135
|
+
operations: [`review instruction and derive executable steps: ${instruction}`],
|
|
136
|
+
tests: [
|
|
137
|
+
{
|
|
138
|
+
name: "generated-1",
|
|
139
|
+
expected: "期待結果を context.tests[].expected で指定してください",
|
|
140
|
+
actual: "",
|
|
141
|
+
trails: [],
|
|
142
|
+
status: null
|
|
143
|
+
}
|
|
144
|
+
],
|
|
145
|
+
warnings: []
|
|
146
|
+
};
|
|
147
|
+
}
|
|
148
|
+
const extracted = extractPromptMetadata(input.instruction, "case");
|
|
149
|
+
assertValidIdFormat(extracted.id);
|
|
150
|
+
await assertNoDuplicateId(input.workspaceRoot, input.suiteDir, extracted.id);
|
|
151
|
+
const contextOperations = asStringArray(input.context?.operations);
|
|
152
|
+
const contextTests = buildTestsFromContext(input.context);
|
|
153
|
+
const inferred = buildCaseHintsFromInstruction(input.instruction);
|
|
154
|
+
const operations = contextOperations.length > 0 ? contextOperations : inferred.operations;
|
|
155
|
+
const tests = contextTests.length > 0 ? contextTests : inferred.tests;
|
|
156
|
+
const description = inferDescriptionFromText(typeof input.context?.description === "string" ? input.context.description : input.instruction, extracted.title);
|
|
157
|
+
const merged = {
|
|
158
|
+
...input.context,
|
|
159
|
+
id: extracted.id,
|
|
160
|
+
title: extracted.title,
|
|
161
|
+
description,
|
|
162
|
+
operations,
|
|
163
|
+
tests
|
|
164
|
+
};
|
|
165
|
+
const normalized = normalizeCaseCandidate(merged, { id: extracted.id, title: extracted.title });
|
|
166
|
+
const outputPath = planPathForCase(input.workspaceRoot, input.suiteDir, normalized.entity);
|
|
167
|
+
const writeResult = await writeEntityIfRequested(input.workspaceRoot, outputPath, normalized.entity, input.write ?? false);
|
|
168
|
+
return {
|
|
169
|
+
testcase: normalized.entity,
|
|
170
|
+
yamlText: writeResult.yamlText,
|
|
171
|
+
writtenFile: writeResult.writtenFile,
|
|
172
|
+
warnings: [...extracted.warnings, ...inferred.warnings, ...normalized.warnings],
|
|
173
|
+
diffSummary: ["created testcase"]
|
|
174
|
+
};
|
|
175
|
+
}
|
|
176
|
+
export async function expandTestcaseCore(input) {
|
|
177
|
+
const path = resolvePathInsideWorkspace(input.workspaceRoot, input.testcasePath);
|
|
178
|
+
const before = await readYamlFile(path);
|
|
179
|
+
const beforeObj = asObject(before);
|
|
180
|
+
const afterDraft = { ...beforeObj };
|
|
181
|
+
const preserveFields = new Set(input.preserveFields ?? []);
|
|
182
|
+
if (!preserveFields.has("description")) {
|
|
183
|
+
afterDraft.description = `${String(beforeObj.description ?? "").trim()}\n${input.instruction}`.trim();
|
|
184
|
+
}
|
|
185
|
+
if (!preserveFields.has("operations")) {
|
|
186
|
+
const operations = Array.isArray(beforeObj.operations)
|
|
187
|
+
? beforeObj.operations.filter((v) => typeof v === "string")
|
|
188
|
+
: [];
|
|
189
|
+
operations.push(`review: ${input.instruction}`);
|
|
190
|
+
afterDraft.operations = Array.from(new Set(operations));
|
|
191
|
+
}
|
|
192
|
+
if (!preserveFields.has("tests")) {
|
|
193
|
+
const existing = Array.isArray(beforeObj.tests) ? beforeObj.tests.map((v) => asObject(v)) : [];
|
|
194
|
+
existing.push({
|
|
195
|
+
name: `generated-${existing.length + 1}`,
|
|
196
|
+
expected: "to be confirmed",
|
|
197
|
+
actual: "",
|
|
198
|
+
trails: [],
|
|
199
|
+
status: null
|
|
200
|
+
});
|
|
201
|
+
afterDraft.tests = existing;
|
|
202
|
+
}
|
|
203
|
+
const normalized = normalizeCaseCandidate(afterDraft, {
|
|
204
|
+
id: typeof beforeObj.id === "string" ? beforeObj.id : "case-expanded",
|
|
205
|
+
title: typeof beforeObj.title === "string" ? beforeObj.title : "Expanded Case"
|
|
206
|
+
});
|
|
207
|
+
const changes = summarizeDiff(beforeObj, normalized.entity);
|
|
208
|
+
if (input.write) {
|
|
209
|
+
await writeYamlFileAtomic(path, normalized.entity);
|
|
210
|
+
}
|
|
211
|
+
return {
|
|
212
|
+
before: beforeObj,
|
|
213
|
+
after: normalized.entity,
|
|
214
|
+
diffSummary: changes,
|
|
215
|
+
warnings: normalized.warnings,
|
|
216
|
+
writtenFile: input.write ? toRelativePath(input.workspaceRoot, path) : null
|
|
217
|
+
};
|
|
218
|
+
}
|
|
219
|
+
export async function organizeExecutionTargetsCore(input) {
|
|
220
|
+
const loaded = input.testcasePath
|
|
221
|
+
? await readYamlFile(resolvePathInsideWorkspace(input.workspaceRoot, input.testcasePath))
|
|
222
|
+
: input.testcase;
|
|
223
|
+
if (!loaded) {
|
|
224
|
+
throw new Error("validation: testcasePath or testcase is required");
|
|
225
|
+
}
|
|
226
|
+
const normalized = normalizeCaseCandidate(loaded, {
|
|
227
|
+
id: "organized-case",
|
|
228
|
+
title: "Organized Case"
|
|
229
|
+
});
|
|
230
|
+
const generatedTests = [
|
|
231
|
+
{
|
|
232
|
+
name: `${input.strategy}-critical-path`,
|
|
233
|
+
expected: "critical behaviors are covered",
|
|
234
|
+
actual: "",
|
|
235
|
+
trails: [],
|
|
236
|
+
status: null
|
|
237
|
+
},
|
|
238
|
+
{
|
|
239
|
+
name: `${input.strategy}-edge-cases`,
|
|
240
|
+
expected: "edge cases are covered",
|
|
241
|
+
actual: "",
|
|
242
|
+
trails: [],
|
|
243
|
+
status: null
|
|
244
|
+
}
|
|
245
|
+
];
|
|
246
|
+
const appliedMode = selectAppliedMode(input.mode, JSON.stringify(loaded));
|
|
247
|
+
const existingNames = new Set(normalized.entity.tests.map((test) => test.name));
|
|
248
|
+
const appended = generatedTests.filter((test) => !existingNames.has(test.name));
|
|
249
|
+
const proposedTests = appliedMode === "replace" ? generatedTests : [...normalized.entity.tests, ...appended];
|
|
250
|
+
const afterCase = {
|
|
251
|
+
...normalized.entity,
|
|
252
|
+
tests: proposedTests
|
|
253
|
+
};
|
|
254
|
+
const final = normalizeCaseCandidate(afterCase, { id: afterCase.id, title: afterCase.title });
|
|
255
|
+
if (input.write && input.testcasePath) {
|
|
256
|
+
const filePath = resolvePathInsideWorkspace(input.workspaceRoot, input.testcasePath);
|
|
257
|
+
await writeYamlFileAtomic(filePath, final.entity);
|
|
258
|
+
}
|
|
259
|
+
return {
|
|
260
|
+
proposedTests: final.entity.tests,
|
|
261
|
+
rationale: [`strategy=${input.strategy}`],
|
|
262
|
+
coverageGaps: ["non-functional test scope not inferred"],
|
|
263
|
+
appliedMode,
|
|
264
|
+
warnings: final.warnings,
|
|
265
|
+
diffSummary: summarizeDiff(normalized.entity, final.entity),
|
|
266
|
+
writtenFile: input.write && input.testcasePath ? input.testcasePath : null
|
|
267
|
+
};
|
|
268
|
+
}
|
|
269
|
+
export async function initTestsDirectoryCore(input) {
|
|
270
|
+
const outputDir = input.outputDir ?? "./tests";
|
|
271
|
+
const targetDir = resolvePathInsideWorkspace(input.workspaceRoot, outputDir);
|
|
272
|
+
const plannedFiles = [];
|
|
273
|
+
const writtenFiles = [];
|
|
274
|
+
const baseSuite = buildDefaultSuite({ id: "default", title: "Default Suite" });
|
|
275
|
+
let suite = baseSuite;
|
|
276
|
+
let sampleCase = buildDefaultCase({ id: "default-case", title: "Default Case" });
|
|
277
|
+
if (input.templateDir) {
|
|
278
|
+
const templatePath = resolvePathInsideWorkspace(input.workspaceRoot, input.templateDir);
|
|
279
|
+
const extracted = await extractTemplateFromDirectory(templatePath);
|
|
280
|
+
suite = normalizeSuiteCandidate({ ...baseSuite, ...extracted.suite }, baseSuite).entity;
|
|
281
|
+
sampleCase = normalizeCaseCandidate({ ...sampleCase, ...extracted.testCase }, sampleCase).entity;
|
|
282
|
+
}
|
|
283
|
+
const suitePath = join(targetDir, "index.yaml");
|
|
284
|
+
const casePath = join(targetDir, buildCaseFileName(sampleCase.id, sampleCase.title));
|
|
285
|
+
plannedFiles.push(toRelativePath(input.workspaceRoot, suitePath), toRelativePath(input.workspaceRoot, casePath));
|
|
286
|
+
if (input.write) {
|
|
287
|
+
await mkdir(targetDir, { recursive: true });
|
|
288
|
+
assertNoOverwrite(suitePath);
|
|
289
|
+
assertNoOverwrite(casePath);
|
|
290
|
+
await writeYamlFileAtomic(suitePath, suite);
|
|
291
|
+
await writeYamlFileAtomic(casePath, sampleCase);
|
|
292
|
+
writtenFiles.push(...plannedFiles);
|
|
293
|
+
}
|
|
294
|
+
return {
|
|
295
|
+
plannedFiles,
|
|
296
|
+
writtenFiles,
|
|
297
|
+
warnings: []
|
|
298
|
+
};
|
|
299
|
+
}
|
|
300
|
+
export async function createTemplateDirectoryCore(input) {
|
|
301
|
+
const outputDir = resolvePathInsideWorkspace(input.workspaceRoot, input.outputDir);
|
|
302
|
+
const suiteTemplatePath = join(outputDir, "index.yaml");
|
|
303
|
+
const caseTemplatePath = join(outputDir, "template.testcase.yaml");
|
|
304
|
+
let suite = buildDefaultSuite({ id: "template-suite", title: "Template Suite" });
|
|
305
|
+
let testCase = buildDefaultCase({ id: "template-case", title: "Template Case" });
|
|
306
|
+
if (input.fromDir) {
|
|
307
|
+
const from = resolvePathInsideWorkspace(input.workspaceRoot, input.fromDir);
|
|
308
|
+
const extracted = await extractTemplateFromDirectory(from);
|
|
309
|
+
suite = normalizeSuiteCandidate(extracted.suite, suite).entity;
|
|
310
|
+
testCase = normalizeCaseCandidate(extracted.testCase, testCase).entity;
|
|
311
|
+
}
|
|
312
|
+
const plannedFiles = [
|
|
313
|
+
toRelativePath(input.workspaceRoot, suiteTemplatePath),
|
|
314
|
+
toRelativePath(input.workspaceRoot, caseTemplatePath)
|
|
315
|
+
];
|
|
316
|
+
const writtenFiles = [];
|
|
317
|
+
if (input.write) {
|
|
318
|
+
await mkdir(outputDir, { recursive: true });
|
|
319
|
+
assertNoOverwrite(suiteTemplatePath);
|
|
320
|
+
assertNoOverwrite(caseTemplatePath);
|
|
321
|
+
await writeYamlFileAtomic(suiteTemplatePath, suite);
|
|
322
|
+
await writeYamlFileAtomic(caseTemplatePath, testCase);
|
|
323
|
+
writtenFiles.push(...plannedFiles);
|
|
324
|
+
}
|
|
325
|
+
return {
|
|
326
|
+
templatePath: toRelativePath(input.workspaceRoot, outputDir),
|
|
327
|
+
plannedFiles,
|
|
328
|
+
writtenFiles
|
|
329
|
+
};
|
|
330
|
+
}
|
|
331
|
+
export async function createSuiteFileCore(input) {
|
|
332
|
+
assertValidIdFormat(input.id);
|
|
333
|
+
await assertNoDuplicateId(input.workspaceRoot, input.dir, input.id);
|
|
334
|
+
const description = inferDescriptionFromText(typeof input.fields?.description === "string" ? input.fields.description : undefined, input.title);
|
|
335
|
+
const normalized = normalizeSuiteCandidate({ id: input.id, title: input.title, description, ...input.fields }, { id: input.id, title: input.title, description });
|
|
336
|
+
const filePath = planPathForSuite(input.workspaceRoot, input.dir, normalized.entity);
|
|
337
|
+
const writeResult = await writeEntityIfRequested(input.workspaceRoot, filePath, normalized.entity, input.write ?? false);
|
|
338
|
+
return {
|
|
339
|
+
entity: normalized.entity,
|
|
340
|
+
yamlText: writeResult.yamlText,
|
|
341
|
+
writtenFile: writeResult.writtenFile,
|
|
342
|
+
warnings: normalized.warnings,
|
|
343
|
+
diffSummary: ["created suite file"]
|
|
344
|
+
};
|
|
345
|
+
}
|
|
346
|
+
export async function createCaseFileCore(input) {
|
|
347
|
+
assertValidIdFormat(input.id);
|
|
348
|
+
await assertNoDuplicateId(input.workspaceRoot, input.dir, input.id);
|
|
349
|
+
const description = inferDescriptionFromText(typeof input.fields?.description === "string" ? input.fields.description : undefined, input.title);
|
|
350
|
+
const normalized = normalizeCaseCandidate({ id: input.id, title: input.title, description, ...input.fields }, { id: input.id, title: input.title, description });
|
|
351
|
+
const filePath = planPathForCase(input.workspaceRoot, input.dir, normalized.entity);
|
|
352
|
+
const writeResult = await writeEntityIfRequested(input.workspaceRoot, filePath, normalized.entity, input.write ?? false);
|
|
353
|
+
return {
|
|
354
|
+
entity: normalized.entity,
|
|
355
|
+
yamlText: writeResult.yamlText,
|
|
356
|
+
writtenFile: writeResult.writtenFile,
|
|
357
|
+
warnings: normalized.warnings,
|
|
358
|
+
diffSummary: ["created case file"]
|
|
359
|
+
};
|
|
360
|
+
}
|
|
361
|
+
export async function updateSuiteCore(input) {
|
|
362
|
+
const dir = resolvePathInsideWorkspace(input.workspaceRoot, input.dir);
|
|
363
|
+
assertValidIdFormat(input.id);
|
|
364
|
+
const index = await buildIdIndex(dir);
|
|
365
|
+
const found = resolveById(index, input.id);
|
|
366
|
+
if (!found || found.type !== "suite") {
|
|
367
|
+
throw new Error(`validation: suite id not found (${input.id})`);
|
|
368
|
+
}
|
|
369
|
+
await assertNoDuplicateId(input.workspaceRoot, input.dir, input.id, found.path);
|
|
370
|
+
const before = await readYamlFile(found.path);
|
|
371
|
+
const beforeObj = asObject(before);
|
|
372
|
+
if (typeof input.patch.id === "string" && input.patch.id !== input.id) {
|
|
373
|
+
throw new Error("validation: id cannot be changed by update_suite");
|
|
374
|
+
}
|
|
375
|
+
const merged = {
|
|
376
|
+
...beforeObj,
|
|
377
|
+
...input.patch,
|
|
378
|
+
id: input.id
|
|
379
|
+
};
|
|
380
|
+
const normalized = normalizeSuiteCandidate(merged, {
|
|
381
|
+
id: input.id,
|
|
382
|
+
title: typeof beforeObj.title === "string" ? beforeObj.title : "Updated Suite"
|
|
383
|
+
});
|
|
384
|
+
const validation = validateSuite(normalized.entity);
|
|
385
|
+
if (!validation.ok || !validation.data) {
|
|
386
|
+
return {
|
|
387
|
+
updated: false,
|
|
388
|
+
id: input.id,
|
|
389
|
+
path: toRelativePath(input.workspaceRoot, found.path),
|
|
390
|
+
errors: validation.errors,
|
|
391
|
+
warnings: [...normalized.warnings, ...validation.warnings.map((warning) => `${warning.path}: ${warning.message}`)]
|
|
392
|
+
};
|
|
393
|
+
}
|
|
394
|
+
if (input.write) {
|
|
395
|
+
await writeYamlFileAtomic(found.path, validation.data);
|
|
396
|
+
}
|
|
397
|
+
return {
|
|
398
|
+
updated: true,
|
|
399
|
+
id: input.id,
|
|
400
|
+
path: toRelativePath(input.workspaceRoot, found.path),
|
|
401
|
+
before: beforeObj,
|
|
402
|
+
after: validation.data,
|
|
403
|
+
warnings: [...normalized.warnings, ...validation.warnings.map((warning) => `${warning.path}: ${warning.message}`)],
|
|
404
|
+
diffSummary: summarizeDiff(beforeObj, validation.data),
|
|
405
|
+
writtenFile: input.write ? toRelativePath(input.workspaceRoot, found.path) : null
|
|
406
|
+
};
|
|
407
|
+
}
|
|
408
|
+
export async function updateCaseCore(input) {
|
|
409
|
+
const dir = resolvePathInsideWorkspace(input.workspaceRoot, input.dir);
|
|
410
|
+
assertValidIdFormat(input.id);
|
|
411
|
+
const index = await buildIdIndex(dir);
|
|
412
|
+
const found = resolveById(index, input.id);
|
|
413
|
+
if (!found || found.type !== "case") {
|
|
414
|
+
throw new Error(`validation: case id not found (${input.id})`);
|
|
415
|
+
}
|
|
416
|
+
await assertNoDuplicateId(input.workspaceRoot, input.dir, input.id, found.path);
|
|
417
|
+
const before = await readYamlFile(found.path);
|
|
418
|
+
const beforeObj = asObject(before);
|
|
419
|
+
if (typeof input.patch.id === "string" && input.patch.id !== input.id) {
|
|
420
|
+
throw new Error("validation: id cannot be changed by update_case");
|
|
421
|
+
}
|
|
422
|
+
const merged = {
|
|
423
|
+
...beforeObj,
|
|
424
|
+
...input.patch,
|
|
425
|
+
id: input.id
|
|
426
|
+
};
|
|
427
|
+
const normalized = normalizeCaseCandidate(merged, {
|
|
428
|
+
id: input.id,
|
|
429
|
+
title: typeof beforeObj.title === "string" ? beforeObj.title : "Updated Case"
|
|
430
|
+
});
|
|
431
|
+
const validation = validateCase(normalized.entity);
|
|
432
|
+
if (!validation.ok || !validation.data) {
|
|
433
|
+
return {
|
|
434
|
+
updated: false,
|
|
435
|
+
id: input.id,
|
|
436
|
+
path: toRelativePath(input.workspaceRoot, found.path),
|
|
437
|
+
errors: validation.errors,
|
|
438
|
+
warnings: [...normalized.warnings, ...validation.warnings.map((warning) => `${warning.path}: ${warning.message}`)]
|
|
439
|
+
};
|
|
440
|
+
}
|
|
441
|
+
if (input.write) {
|
|
442
|
+
await writeYamlFileAtomic(found.path, validation.data);
|
|
443
|
+
}
|
|
444
|
+
return {
|
|
445
|
+
updated: true,
|
|
446
|
+
id: input.id,
|
|
447
|
+
path: toRelativePath(input.workspaceRoot, found.path),
|
|
448
|
+
before: beforeObj,
|
|
449
|
+
after: validation.data,
|
|
450
|
+
warnings: [...normalized.warnings, ...validation.warnings.map((warning) => `${warning.path}: ${warning.message}`)],
|
|
451
|
+
diffSummary: summarizeDiff(beforeObj, validation.data),
|
|
452
|
+
writtenFile: input.write ? toRelativePath(input.workspaceRoot, found.path) : null
|
|
453
|
+
};
|
|
454
|
+
}
|
|
455
|
+
export async function validateTestsDirectoryCore(input) {
|
|
456
|
+
const dir = resolvePathInsideWorkspace(input.workspaceRoot, input.dir);
|
|
457
|
+
const files = await listYamlFiles(dir);
|
|
458
|
+
const errors = [];
|
|
459
|
+
const warnings = [];
|
|
460
|
+
for (const file of files) {
|
|
461
|
+
const loaded = await readYamlFile(file);
|
|
462
|
+
const type = detectEntityType(file);
|
|
463
|
+
if (type === "suite") {
|
|
464
|
+
const result = validateSuite(loaded);
|
|
465
|
+
if (!result.ok) {
|
|
466
|
+
errors.push({ path: toRelativePath(input.workspaceRoot, file), diagnostics: result.errors });
|
|
467
|
+
}
|
|
468
|
+
if (result.warnings.length > 0) {
|
|
469
|
+
warnings.push({ path: toRelativePath(input.workspaceRoot, file), diagnostics: result.warnings });
|
|
470
|
+
}
|
|
471
|
+
continue;
|
|
472
|
+
}
|
|
473
|
+
const result = validateCase(loaded);
|
|
474
|
+
if (!result.ok) {
|
|
475
|
+
errors.push({ path: toRelativePath(input.workspaceRoot, file), diagnostics: result.errors });
|
|
476
|
+
}
|
|
477
|
+
if (result.warnings.length > 0) {
|
|
478
|
+
warnings.push({ path: toRelativePath(input.workspaceRoot, file), diagnostics: result.warnings });
|
|
479
|
+
}
|
|
480
|
+
}
|
|
481
|
+
return {
|
|
482
|
+
errors,
|
|
483
|
+
warnings,
|
|
484
|
+
summary: {
|
|
485
|
+
totalFiles: files.length,
|
|
486
|
+
errorFiles: errors.length,
|
|
487
|
+
warningFiles: warnings.length
|
|
488
|
+
}
|
|
489
|
+
};
|
|
490
|
+
}
|
|
491
|
+
export async function listTemplatesCore(input) {
|
|
492
|
+
const root = resolvePathInsideWorkspace(input.workspaceRoot, input.dir);
|
|
493
|
+
const entries = await readdir(root, { withFileTypes: true });
|
|
494
|
+
const templates = [];
|
|
495
|
+
for (const entry of entries) {
|
|
496
|
+
if (!entry.isDirectory()) {
|
|
497
|
+
continue;
|
|
498
|
+
}
|
|
499
|
+
const path = join(root, entry.name);
|
|
500
|
+
const files = await readdir(path, { withFileTypes: true });
|
|
501
|
+
const hasIndex = files.some((file) => file.isFile() && file.name === "index.yaml");
|
|
502
|
+
if (!hasIndex) {
|
|
503
|
+
continue;
|
|
504
|
+
}
|
|
505
|
+
templates.push({
|
|
506
|
+
name: entry.name,
|
|
507
|
+
path: toRelativePath(input.workspaceRoot, path)
|
|
508
|
+
});
|
|
509
|
+
}
|
|
510
|
+
return { templates };
|
|
511
|
+
}
|
|
512
|
+
export async function listSuitesCore(input) {
|
|
513
|
+
const root = resolvePathInsideWorkspace(input.workspaceRoot, input.dir);
|
|
514
|
+
const files = await listYamlFiles(root);
|
|
515
|
+
const suites = [];
|
|
516
|
+
for (const file of files) {
|
|
517
|
+
if (basename(file) !== "index.yaml" && !file.endsWith(".suite.yaml")) {
|
|
518
|
+
continue;
|
|
519
|
+
}
|
|
520
|
+
const loaded = await readYamlFile(file);
|
|
521
|
+
const parsed = suiteSchema.safeParse(loaded);
|
|
522
|
+
if (!parsed.success) {
|
|
523
|
+
continue;
|
|
524
|
+
}
|
|
525
|
+
if (input.filters?.id && parsed.data.id !== input.filters.id) {
|
|
526
|
+
continue;
|
|
527
|
+
}
|
|
528
|
+
if (input.filters?.tag && !parsed.data.tags.includes(input.filters.tag)) {
|
|
529
|
+
continue;
|
|
530
|
+
}
|
|
531
|
+
suites.push({
|
|
532
|
+
id: parsed.data.id,
|
|
533
|
+
path: toRelativePath(input.workspaceRoot, file)
|
|
534
|
+
});
|
|
535
|
+
}
|
|
536
|
+
return { suites };
|
|
537
|
+
}
|
|
538
|
+
export async function listCasesCore(input) {
|
|
539
|
+
const root = resolvePathInsideWorkspace(input.workspaceRoot, input.dir);
|
|
540
|
+
const files = await listYamlFiles(root);
|
|
541
|
+
const cases = [];
|
|
542
|
+
for (const file of files) {
|
|
543
|
+
if (!file.endsWith(".testcase.yaml") && basename(file) === "index.yaml") {
|
|
544
|
+
continue;
|
|
545
|
+
}
|
|
546
|
+
const loaded = await readYamlFile(file);
|
|
547
|
+
const parsed = testCaseSchema.safeParse(loaded);
|
|
548
|
+
if (!parsed.success) {
|
|
549
|
+
continue;
|
|
550
|
+
}
|
|
551
|
+
if (input.filters?.id && parsed.data.id !== input.filters.id) {
|
|
552
|
+
continue;
|
|
553
|
+
}
|
|
554
|
+
if (input.filters?.status && parsed.data.status !== input.filters.status) {
|
|
555
|
+
continue;
|
|
556
|
+
}
|
|
557
|
+
if (input.filters?.tags && input.filters.tags.length > 0) {
|
|
558
|
+
const matchedTags = input.filters.tags.some((tag) => parsed.data.tags.includes(tag));
|
|
559
|
+
if (!matchedTags) {
|
|
560
|
+
continue;
|
|
561
|
+
}
|
|
562
|
+
}
|
|
563
|
+
if (input.filters?.scopedOnly === true && parsed.data.scoped !== true) {
|
|
564
|
+
continue;
|
|
565
|
+
}
|
|
566
|
+
if (input.filters?.owners && input.filters.owners.length > 0) {
|
|
567
|
+
const issueOwners = parsed.data.issues.flatMap((issue) => issue.owners);
|
|
568
|
+
const matchedOwners = input.filters.owners.some((owner) => issueOwners.includes(owner));
|
|
569
|
+
if (!matchedOwners) {
|
|
570
|
+
continue;
|
|
571
|
+
}
|
|
572
|
+
}
|
|
573
|
+
if (input.filters?.issueStatus) {
|
|
574
|
+
const hasIssueStatus = parsed.data.issues.some((issue) => issue.status === input.filters?.issueStatus);
|
|
575
|
+
if (!hasIssueStatus) {
|
|
576
|
+
continue;
|
|
577
|
+
}
|
|
578
|
+
}
|
|
579
|
+
if (input.filters?.issueHas && input.filters.issueHas.trim().length > 0) {
|
|
580
|
+
const keyword = input.filters.issueHas.trim().toLowerCase();
|
|
581
|
+
const hasKeyword = parsed.data.issues.some((issue) => {
|
|
582
|
+
const haystack = [
|
|
583
|
+
issue.incident,
|
|
584
|
+
...issue.owners,
|
|
585
|
+
...issue.causes,
|
|
586
|
+
...issue.solutions,
|
|
587
|
+
...issue.remarks
|
|
588
|
+
]
|
|
589
|
+
.join("\n")
|
|
590
|
+
.toLowerCase();
|
|
591
|
+
return haystack.includes(keyword);
|
|
592
|
+
});
|
|
593
|
+
if (!hasKeyword) {
|
|
594
|
+
continue;
|
|
595
|
+
}
|
|
596
|
+
}
|
|
597
|
+
cases.push({
|
|
598
|
+
id: parsed.data.id,
|
|
599
|
+
status: parsed.data.status,
|
|
600
|
+
path: toRelativePath(input.workspaceRoot, file)
|
|
601
|
+
});
|
|
602
|
+
}
|
|
603
|
+
return { cases };
|
|
604
|
+
}
|
|
605
|
+
export async function resolveEntityPathByIdCore(input) {
|
|
606
|
+
const dir = resolvePathInsideWorkspace(input.workspaceRoot, input.dir);
|
|
607
|
+
const index = await buildIdIndex(dir);
|
|
608
|
+
const found = resolveById(index, input.id);
|
|
609
|
+
return {
|
|
610
|
+
id: input.id,
|
|
611
|
+
found: found
|
|
612
|
+
? {
|
|
613
|
+
path: toRelativePath(input.workspaceRoot, found.path),
|
|
614
|
+
type: found.type,
|
|
615
|
+
title: found.title
|
|
616
|
+
}
|
|
617
|
+
: null,
|
|
618
|
+
duplicates: index.duplicates.map((dup) => ({
|
|
619
|
+
id: dup.id,
|
|
620
|
+
paths: dup.paths.map((path) => toRelativePath(input.workspaceRoot, path))
|
|
621
|
+
}))
|
|
622
|
+
};
|
|
623
|
+
}
|
|
624
|
+
export async function resolveRelatedTargetsCore(input) {
|
|
625
|
+
const dir = resolvePathInsideWorkspace(input.workspaceRoot, input.dir);
|
|
626
|
+
const index = await buildIdIndex(dir);
|
|
627
|
+
const source = resolveById(index, input.sourceId);
|
|
628
|
+
if (!source) {
|
|
629
|
+
throw new Error(`validation: source id not found (${input.sourceId})`);
|
|
630
|
+
}
|
|
631
|
+
const targetIds = input.relatedIds ?? source.related;
|
|
632
|
+
const resolved = [];
|
|
633
|
+
const missing = [];
|
|
634
|
+
for (const id of targetIds) {
|
|
635
|
+
const entity = resolveById(index, id);
|
|
636
|
+
if (!entity) {
|
|
637
|
+
missing.push(id);
|
|
638
|
+
continue;
|
|
639
|
+
}
|
|
640
|
+
resolved.push({
|
|
641
|
+
id: entity.id,
|
|
642
|
+
type: entity.type,
|
|
643
|
+
title: entity.title,
|
|
644
|
+
path: toRelativePath(input.workspaceRoot, entity.path)
|
|
645
|
+
});
|
|
646
|
+
}
|
|
647
|
+
return {
|
|
648
|
+
source: {
|
|
649
|
+
id: source.id,
|
|
650
|
+
type: source.type,
|
|
651
|
+
path: toRelativePath(input.workspaceRoot, source.path)
|
|
652
|
+
},
|
|
653
|
+
resolved,
|
|
654
|
+
missing,
|
|
655
|
+
warnings: missing.map((id) => `related id not found: ${id}`)
|
|
656
|
+
};
|
|
657
|
+
}
|
|
658
|
+
export async function syncRelatedCore(input) {
|
|
659
|
+
const dir = resolvePathInsideWorkspace(input.workspaceRoot, input.dir);
|
|
660
|
+
const index = await buildIdIndex(dir);
|
|
661
|
+
const source = resolveById(index, input.sourceId);
|
|
662
|
+
if (!source) {
|
|
663
|
+
throw new Error(`validation: source id not found (${input.sourceId})`);
|
|
664
|
+
}
|
|
665
|
+
const targetIds = Array.from(new Set(input.relatedIds ?? source.related));
|
|
666
|
+
const resolvedTargets = targetIds
|
|
667
|
+
.map((id) => resolveById(index, id))
|
|
668
|
+
.filter((entity) => Boolean(entity));
|
|
669
|
+
const missing = targetIds.filter((id) => !resolvedTargets.some((entity) => entity.id === id));
|
|
670
|
+
const sourceBefore = asObject(await readYamlFile(source.path));
|
|
671
|
+
const sourceNext = {
|
|
672
|
+
...sourceBefore,
|
|
673
|
+
related: targetIds
|
|
674
|
+
};
|
|
675
|
+
const normalizedSource = source.type === "suite"
|
|
676
|
+
? normalizeSuiteCandidate(sourceNext, {
|
|
677
|
+
id: source.id,
|
|
678
|
+
title: typeof sourceBefore.title === "string" ? sourceBefore.title : "Synced Suite"
|
|
679
|
+
})
|
|
680
|
+
: normalizeCaseCandidate(sourceNext, {
|
|
681
|
+
id: source.id,
|
|
682
|
+
title: typeof sourceBefore.title === "string" ? sourceBefore.title : "Synced Case"
|
|
683
|
+
});
|
|
684
|
+
const sourceValidation = source.type === "suite" ? validateSuite(normalizedSource.entity) : validateCase(normalizedSource.entity);
|
|
685
|
+
if (!sourceValidation.ok || !sourceValidation.data) {
|
|
686
|
+
return {
|
|
687
|
+
synced: false,
|
|
688
|
+
mode: input.mode,
|
|
689
|
+
sourceId: input.sourceId,
|
|
690
|
+
missing,
|
|
691
|
+
warnings: [...normalizedSource.warnings],
|
|
692
|
+
errors: sourceValidation.errors
|
|
693
|
+
};
|
|
694
|
+
}
|
|
695
|
+
const writePlans = [
|
|
696
|
+
{
|
|
697
|
+
path: source.path,
|
|
698
|
+
before: sourceBefore,
|
|
699
|
+
after: sourceValidation.data
|
|
700
|
+
}
|
|
701
|
+
];
|
|
702
|
+
if (input.mode === "two-way") {
|
|
703
|
+
for (const target of resolvedTargets) {
|
|
704
|
+
const targetBefore = asObject(await readYamlFile(target.path));
|
|
705
|
+
const targetRelated = Array.isArray(targetBefore.related)
|
|
706
|
+
? targetBefore.related.filter((item) => typeof item === "string")
|
|
707
|
+
: [];
|
|
708
|
+
const targetNext = {
|
|
709
|
+
...targetBefore,
|
|
710
|
+
related: Array.from(new Set([...targetRelated, source.id]))
|
|
711
|
+
};
|
|
712
|
+
const normalizedTarget = target.type === "suite"
|
|
713
|
+
? normalizeSuiteCandidate(targetNext, {
|
|
714
|
+
id: target.id,
|
|
715
|
+
title: typeof targetBefore.title === "string" ? targetBefore.title : "Synced Suite"
|
|
716
|
+
})
|
|
717
|
+
: normalizeCaseCandidate(targetNext, {
|
|
718
|
+
id: target.id,
|
|
719
|
+
title: typeof targetBefore.title === "string" ? targetBefore.title : "Synced Case"
|
|
720
|
+
});
|
|
721
|
+
const targetValidation = target.type === "suite" ? validateSuite(normalizedTarget.entity) : validateCase(normalizedTarget.entity);
|
|
722
|
+
if (!targetValidation.ok || !targetValidation.data) {
|
|
723
|
+
return {
|
|
724
|
+
synced: false,
|
|
725
|
+
mode: input.mode,
|
|
726
|
+
sourceId: input.sourceId,
|
|
727
|
+
targetId: target.id,
|
|
728
|
+
missing,
|
|
729
|
+
warnings: [...normalizedTarget.warnings],
|
|
730
|
+
errors: targetValidation.errors
|
|
731
|
+
};
|
|
732
|
+
}
|
|
733
|
+
writePlans.push({
|
|
734
|
+
path: target.path,
|
|
735
|
+
before: targetBefore,
|
|
736
|
+
after: targetValidation.data
|
|
737
|
+
});
|
|
738
|
+
}
|
|
739
|
+
}
|
|
740
|
+
if (input.write) {
|
|
741
|
+
for (const plan of writePlans) {
|
|
742
|
+
await writeYamlFileAtomic(plan.path, plan.after);
|
|
743
|
+
}
|
|
744
|
+
}
|
|
745
|
+
return {
|
|
746
|
+
synced: true,
|
|
747
|
+
mode: input.mode,
|
|
748
|
+
sourceId: source.id,
|
|
749
|
+
targetIds: resolvedTargets.map((target) => target.id),
|
|
750
|
+
missing,
|
|
751
|
+
warnings: missing.map((id) => `related id not found: ${id}`),
|
|
752
|
+
changed: writePlans.map((plan) => ({
|
|
753
|
+
path: toRelativePath(input.workspaceRoot, plan.path),
|
|
754
|
+
diffSummary: summarizeDiff(plan.before, plan.after)
|
|
755
|
+
})),
|
|
756
|
+
writtenFiles: input.write ? writePlans.map((plan) => toRelativePath(input.workspaceRoot, plan.path)) : []
|
|
757
|
+
};
|
|
758
|
+
}
|
|
759
|
+
export async function getWorkspaceSnapshotCore(input) {
|
|
760
|
+
const suitesResult = await listSuitesCore({ workspaceRoot: input.workspaceRoot, dir: input.dir });
|
|
761
|
+
const casesResult = await listCasesCore({ workspaceRoot: input.workspaceRoot, dir: input.dir });
|
|
762
|
+
const suites = suitesResult.suites;
|
|
763
|
+
const casesRaw = casesResult.cases;
|
|
764
|
+
let cases = casesRaw;
|
|
765
|
+
if (input.excludeUnscoped) {
|
|
766
|
+
const root = resolvePathInsideWorkspace(input.workspaceRoot, input.dir);
|
|
767
|
+
const filtered = [];
|
|
768
|
+
for (const item of casesRaw) {
|
|
769
|
+
const abs = resolvePathInsideWorkspace(input.workspaceRoot, item.path);
|
|
770
|
+
const loaded = await readYamlFile(abs);
|
|
771
|
+
const parsed = testCaseSchema.safeParse(loaded);
|
|
772
|
+
if (!parsed.success || parsed.data.scoped !== false) {
|
|
773
|
+
filtered.push(item);
|
|
774
|
+
}
|
|
775
|
+
}
|
|
776
|
+
cases = filtered;
|
|
777
|
+
void root;
|
|
778
|
+
}
|
|
779
|
+
return {
|
|
780
|
+
snapshot: {
|
|
781
|
+
suites,
|
|
782
|
+
cases
|
|
783
|
+
},
|
|
784
|
+
summary: {
|
|
785
|
+
suiteCount: suites.length,
|
|
786
|
+
caseCount: cases.length
|
|
787
|
+
}
|
|
788
|
+
};
|
|
789
|
+
}
|
|
790
|
+
export async function suiteStatsCore(input) {
|
|
791
|
+
const dir = resolvePathInsideWorkspace(input.workspaceRoot, input.dir);
|
|
792
|
+
const index = await buildIdIndex(dir);
|
|
793
|
+
const suiteEntity = resolveById(index, input.suiteId);
|
|
794
|
+
if (!suiteEntity || suiteEntity.type !== "suite") {
|
|
795
|
+
throw new Error(`validation: suite id not found (${input.suiteId})`);
|
|
796
|
+
}
|
|
797
|
+
const suiteLoaded = await readYamlFile(suiteEntity.path);
|
|
798
|
+
const suiteParsed = suiteSchema.safeParse(suiteLoaded);
|
|
799
|
+
if (!suiteParsed.success) {
|
|
800
|
+
throw new Error(`validation: invalid suite schema (${input.suiteId})`);
|
|
801
|
+
}
|
|
802
|
+
const files = await listYamlFiles(dir);
|
|
803
|
+
const suiteDir = suiteEntity.path.slice(0, Math.max(0, suiteEntity.path.lastIndexOf("/")));
|
|
804
|
+
const allowScopedAggregation = suiteParsed.data.scoped === true;
|
|
805
|
+
const cases = [];
|
|
806
|
+
for (const file of files) {
|
|
807
|
+
if (!file.endsWith(".testcase.yaml")) {
|
|
808
|
+
continue;
|
|
809
|
+
}
|
|
810
|
+
const rel = toRelativePath(input.workspaceRoot, file);
|
|
811
|
+
if (suiteDir.length > 0 && !rel.startsWith(suiteDir)) {
|
|
812
|
+
continue;
|
|
813
|
+
}
|
|
814
|
+
const loaded = await readYamlFile(file);
|
|
815
|
+
const parsed = testCaseSchema.safeParse(loaded);
|
|
816
|
+
if (!parsed.success) {
|
|
817
|
+
continue;
|
|
818
|
+
}
|
|
819
|
+
if (!allowScopedAggregation) {
|
|
820
|
+
continue;
|
|
821
|
+
}
|
|
822
|
+
if (parsed.data.scoped !== true) {
|
|
823
|
+
continue;
|
|
824
|
+
}
|
|
825
|
+
cases.push(parsed.data);
|
|
826
|
+
}
|
|
827
|
+
const burndown = calculateBurndown(cases, suiteParsed.data.duration.scheduled.start, suiteParsed.data.duration.scheduled.end);
|
|
828
|
+
return {
|
|
829
|
+
suite: {
|
|
830
|
+
id: suiteParsed.data.id,
|
|
831
|
+
title: suiteParsed.data.title,
|
|
832
|
+
path: toRelativePath(input.workspaceRoot, suiteEntity.path)
|
|
833
|
+
},
|
|
834
|
+
summary: burndown.summary,
|
|
835
|
+
burndown: burndown.buckets,
|
|
836
|
+
anomalies: burndown.anomalies
|
|
837
|
+
};
|
|
838
|
+
}
|
|
839
|
+
async function deleteEntityById(input) {
|
|
840
|
+
const dir = resolvePathInsideWorkspace(input.workspaceRoot, input.dir);
|
|
841
|
+
const index = await buildIdIndex(dir);
|
|
842
|
+
const found = resolveById(index, input.id);
|
|
843
|
+
if (!found || found.type !== input.expectedType) {
|
|
844
|
+
throw new Error(`validation: ${input.expectedType} id not found (${input.id})`);
|
|
845
|
+
}
|
|
846
|
+
const impacted = index.entities
|
|
847
|
+
.filter((entity) => entity.related.includes(input.id) && entity.id !== input.id)
|
|
848
|
+
.map((entity) => ({
|
|
849
|
+
id: entity.id,
|
|
850
|
+
type: entity.type,
|
|
851
|
+
path: toRelativePath(input.workspaceRoot, entity.path)
|
|
852
|
+
}));
|
|
853
|
+
const plan = {
|
|
854
|
+
id: found.id,
|
|
855
|
+
type: found.type,
|
|
856
|
+
path: toRelativePath(input.workspaceRoot, found.path)
|
|
857
|
+
};
|
|
858
|
+
if (input.dryRun) {
|
|
859
|
+
return {
|
|
860
|
+
deleted: false,
|
|
861
|
+
dryRun: true,
|
|
862
|
+
plan,
|
|
863
|
+
impacted
|
|
864
|
+
};
|
|
865
|
+
}
|
|
866
|
+
if (input.confirm !== true) {
|
|
867
|
+
throw new Error("validation: confirm=true is required when dryRun=false");
|
|
868
|
+
}
|
|
869
|
+
await unlink(found.path);
|
|
870
|
+
return {
|
|
871
|
+
deleted: true,
|
|
872
|
+
dryRun: false,
|
|
873
|
+
plan,
|
|
874
|
+
impacted,
|
|
875
|
+
writtenFiles: [toRelativePath(input.workspaceRoot, found.path)]
|
|
876
|
+
};
|
|
877
|
+
}
|
|
878
|
+
export async function deleteSuiteCore(input) {
|
|
879
|
+
return deleteEntityById({ ...input, expectedType: "suite" });
|
|
880
|
+
}
|
|
881
|
+
export async function deleteCaseCore(input) {
|
|
882
|
+
return deleteEntityById({ ...input, expectedType: "case" });
|
|
883
|
+
}
|
|
884
|
+
//# sourceMappingURL=core-tools.js.map
|