hanoman 0.9.14 → 0.9.16
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/build-info.json +3 -3
- package/dist/cli.js +11 -3
- package/dist/server.js +155 -54
- package/package.json +1 -1
- package/prisma/migrations/20261005120000_qa_free_code/migration.sql +3 -0
- package/prisma/schema.prisma +2 -0
- package/web/assets/{ChangelogScreen-CS_GSORI.js → ChangelogScreen-C-Rr4PIZ.js} +1 -1
- package/web/assets/{DalangHanomanScreen-maIEXuvi.js → DalangHanomanScreen-BR83CtJF.js} +1 -1
- package/web/assets/{DocPreviewModal-DFqCURCs.js → DocPreviewModal-BFuCcScy.js} +1 -1
- package/web/assets/{DocsWorkspace-hxJ037xu.js → DocsWorkspace-8P9q1gwJ.js} +1 -1
- package/web/assets/{IdeScreen-CHXLJ0Az.js → IdeScreen-m986qjh-.js} +1 -1
- package/web/assets/{LeadScreen-DI8TW6QX.js → LeadScreen-cjs5j8ok.js} +1 -1
- package/web/assets/{ReviewScreen-CAi2WDfW.js → ReviewScreen-EdyUvvWF.js} +1 -1
- package/web/assets/{SchedulerScreen-D1vCNuRr.js → SchedulerScreen-q87P2S6p.js} +1 -1
- package/web/assets/{SettingsScreen-1GM6-O69.js → SettingsScreen-BgEGI7Na.js} +1 -1
- package/web/assets/{TeamScreen-eYlsFpzN.js → TeamScreen-8a2JJaiD.js} +1 -1
- package/web/assets/{TerminalScreen-u6e-6CB7.js → TerminalScreen-DtkWCziM.js} +1 -1
- package/web/assets/{TriageScreen-B6czWJOz.js → TriageScreen-DXTQuLxN.js} +1 -1
- package/web/assets/{VpsScreen-CaYPux2B.js → VpsScreen-Sd40e0YW.js} +1 -1
- package/web/assets/{index--wY0Cvay.js → index-CFo8nW-6.js} +212 -212
- package/web/assets/{live-WWHlTNwv.js → live-DT9TuqQb.js} +1 -1
- package/web/index.html +1 -1
package/dist/build-info.json
CHANGED
package/dist/cli.js
CHANGED
|
@@ -7948,7 +7948,7 @@ var init_semver = __esm({
|
|
|
7948
7948
|
});
|
|
7949
7949
|
|
|
7950
7950
|
// ../shared/src/qa.ts
|
|
7951
|
-
var QA_REPORT_STATUSES, QA_VERDICTS, QA_CASE_STATUSES, QA_SEVERITIES, QA_PRIORITIES, zQaEnvironment, zSteps, zCreateQaReport, zPatchQaReport, zCreateQaCase, zPatchQaCase, zCreateQaFinding, zPatchQaFinding;
|
|
7951
|
+
var QA_REPORT_STATUSES, QA_VERDICTS, QA_CASE_STATUSES, QA_SEVERITIES, QA_PRIORITIES, zQaEnvironment, zSteps, zCreateQaReport, zPatchQaReport, zQaCode, zCreateQaCase, zPatchQaCase, zCreateQaFinding, zPatchQaFinding;
|
|
7952
7952
|
var init_qa = __esm({
|
|
7953
7953
|
"../shared/src/qa.ts"() {
|
|
7954
7954
|
"use strict";
|
|
@@ -7970,7 +7970,9 @@ var init_qa = __esm({
|
|
|
7970
7970
|
verdict: external_exports.enum(QA_VERDICTS).nullable().default(null)
|
|
7971
7971
|
});
|
|
7972
7972
|
zPatchQaReport = zCreateQaReport.partial().extend({ status: external_exports.enum(QA_REPORT_STATUSES).optional() });
|
|
7973
|
+
zQaCode = external_exports.string().trim().max(40).refine((c) => !/[\u0000-\u001f·]/.test(c), "kode tak boleh berisi baris baru atau karakter \xB7").refine((c) => c.toLowerCase() !== "laporan", '"Laporan" dicadangkan untuk lampiran laporan').transform((c) => c || null).nullable();
|
|
7973
7974
|
zCreateQaCase = external_exports.object({
|
|
7975
|
+
code: zQaCode.optional(),
|
|
7974
7976
|
title: external_exports.string().trim().min(1).max(300),
|
|
7975
7977
|
steps: external_exports.string().max(1e4).default(""),
|
|
7976
7978
|
expected: external_exports.string().max(1e4).default(""),
|
|
@@ -7980,6 +7982,7 @@ var init_qa = __esm({
|
|
|
7980
7982
|
});
|
|
7981
7983
|
zPatchQaCase = zCreateQaCase.partial();
|
|
7982
7984
|
zCreateQaFinding = external_exports.object({
|
|
7985
|
+
code: zQaCode.optional(),
|
|
7983
7986
|
title: external_exports.string().trim().min(1).max(300),
|
|
7984
7987
|
caseId: external_exports.string().max(120).nullable().default(null),
|
|
7985
7988
|
severity: external_exports.enum(QA_SEVERITIES).default("major"),
|
|
@@ -12134,7 +12137,7 @@ var init_team2 = __esm({
|
|
|
12134
12137
|
});
|
|
12135
12138
|
|
|
12136
12139
|
// ../shared/src/mcp-catalog/qa.ts
|
|
12137
|
-
var PROJECT3, REPORT, CASE, FINDING, base, one, parseEnv, parseSteps, nullable2, put, REPORT_FIELDS, reportBody, caseBody, findingBody, CASE_FIELDS, FINDING_FIELDS, QA_TOOLS;
|
|
12140
|
+
var PROJECT3, REPORT, CASE, FINDING, base, one, parseEnv, parseSteps, nullable2, put, REPORT_FIELDS, reportBody, caseBody, findingBody, CODE_FIELD, CASE_FIELDS, FINDING_FIELDS, QA_TOOLS;
|
|
12138
12141
|
var init_qa2 = __esm({
|
|
12139
12142
|
"../shared/src/mcp-catalog/qa.ts"() {
|
|
12140
12143
|
"use strict";
|
|
@@ -12182,6 +12185,7 @@ var init_qa2 = __esm({
|
|
|
12182
12185
|
caseBody = (a) => {
|
|
12183
12186
|
const b = {};
|
|
12184
12187
|
for (const k of ["title", "steps", "expected", "actual", "status"]) put(b, k, s(a[k]));
|
|
12188
|
+
put(b, "code", nullable2(a.code));
|
|
12185
12189
|
return b;
|
|
12186
12190
|
};
|
|
12187
12191
|
findingBody = (a) => {
|
|
@@ -12189,9 +12193,12 @@ var init_qa2 = __esm({
|
|
|
12189
12193
|
for (const k of ["title", "severity", "priority", "area", "expected", "actual", "status"]) put(b, k, s(a[k]));
|
|
12190
12194
|
put(b, "steps", parseSteps(a.steps));
|
|
12191
12195
|
put(b, "caseId", nullable2(a.testCase));
|
|
12196
|
+
put(b, "code", nullable2(a.code));
|
|
12192
12197
|
return b;
|
|
12193
12198
|
};
|
|
12199
|
+
CODE_FIELD = str("Kode bebas (mis. `LOGIN-01`), unik di satu laporan (test case + temuan, tak peka huruf). String KOSONG kembali ke nomor otomatis; tak menyebutnya membiarkan apa adanya.");
|
|
12194
12200
|
CASE_FIELDS = {
|
|
12201
|
+
code: CODE_FIELD,
|
|
12195
12202
|
title: str("Judul test case."),
|
|
12196
12203
|
steps: str("Langkah uji (teks bebas)."),
|
|
12197
12204
|
expected: str("Hasil yang diharapkan."),
|
|
@@ -12199,6 +12206,7 @@ var init_qa2 = __esm({
|
|
|
12199
12206
|
status: enumStr(QA_CASE_STATUSES, "Status eksekusi.")
|
|
12200
12207
|
};
|
|
12201
12208
|
FINDING_FIELDS = {
|
|
12209
|
+
code: CODE_FIELD,
|
|
12202
12210
|
title: str("Judul temuan \u2014 satu masalah, satu baris."),
|
|
12203
12211
|
severity: enumStr(QA_SEVERITIES, "Dampak TEKNIS. Bukan prioritas."),
|
|
12204
12212
|
priority: enumStr(QA_PRIORITIES, "Urutan perbaikan P0 (paling mendesak)\u2013P3. Terpisah dari severity: bug minor di halaman checkout bisa P0."),
|
|
@@ -12225,7 +12233,7 @@ var init_qa2 = __esm({
|
|
|
12225
12233
|
{
|
|
12226
12234
|
name: "hanoman_qa_report_get",
|
|
12227
12235
|
title: "Detail laporan QA",
|
|
12228
|
-
description: "Satu laporan QA lengkap: header, test case (`TC-01\u2026`), temuan (`F-01\u2026
|
|
12236
|
+
description: "Satu laporan QA lengkap: header, test case (kode otomatis `TC-01\u2026` atau kode bebas QA), temuan (`F-01\u2026` atau kode bebas, dengan repro bernomor, severity, prioritas, `caseCode`), dan metadata lampiran (nama, tipe, ukuran \u2014 bukan byte).",
|
|
12229
12237
|
inputSchema: obj({ properties: { project: PROJECT3, report: REPORT }, required: ["project", "report"] }),
|
|
12230
12238
|
mode: "read",
|
|
12231
12239
|
capability: "qa:read",
|
package/dist/server.js
CHANGED
|
@@ -8713,12 +8713,23 @@ var init_semver = __esm({
|
|
|
8713
8713
|
});
|
|
8714
8714
|
|
|
8715
8715
|
// ../shared/src/qa.ts
|
|
8716
|
-
function assignCodes(rows, prefix, pad) {
|
|
8716
|
+
function assignCodes(rows, prefix, pad, taken = /* @__PURE__ */ new Set()) {
|
|
8717
8717
|
const ts = (r) => new Date(r.createdAt).getTime();
|
|
8718
|
-
const
|
|
8719
|
-
|
|
8720
|
-
|
|
8721
|
-
|
|
8718
|
+
const ordered = [...rows].sort((a, b) => ts(a) - ts(b) || (a.id < b.id ? -1 : a.id > b.id ? 1 : 0));
|
|
8719
|
+
const out4 = /* @__PURE__ */ new Map();
|
|
8720
|
+
let n2 = 0;
|
|
8721
|
+
for (const r of ordered) {
|
|
8722
|
+
if (r.code) {
|
|
8723
|
+
out4.set(r.id, r.code);
|
|
8724
|
+
continue;
|
|
8725
|
+
}
|
|
8726
|
+
let code;
|
|
8727
|
+
do {
|
|
8728
|
+
code = `${prefix}${String(++n2).padStart(pad, "0")}`;
|
|
8729
|
+
} while (taken.has(qaCodeKey(code)));
|
|
8730
|
+
out4.set(r.id, code);
|
|
8731
|
+
}
|
|
8732
|
+
return rows.map((r) => ({ ...r, code: out4.get(r.id) }));
|
|
8722
8733
|
}
|
|
8723
8734
|
function qaStats(cases, findings) {
|
|
8724
8735
|
const c = { total: cases.length, todo: 0, pass: 0, fail: 0, blocked: 0, skipped: 0 };
|
|
@@ -8737,7 +8748,7 @@ function qaSeverityToSpec(s2) {
|
|
|
8737
8748
|
function qaPriorityToSpec(p3) {
|
|
8738
8749
|
return p3 === "P0" || p3 === "P1" ? "tinggi" : p3 === "P2" ? "sedang" : "rendah";
|
|
8739
8750
|
}
|
|
8740
|
-
var QA_REPORT_STATUSES, QA_VERDICTS, QA_CASE_STATUSES, QA_SEVERITIES, QA_PRIORITIES, QA_FINDING_STATUSES, QA_OWNER_TYPES, zQaEnvironment, zSteps, zCreateQaReport, zPatchQaReport, zCreateQaCase, zPatchQaCase, zCreateQaFinding, zPatchQaFinding, QA_EXPORT_FORMATS;
|
|
8751
|
+
var QA_REPORT_STATUSES, QA_VERDICTS, QA_CASE_STATUSES, QA_SEVERITIES, QA_PRIORITIES, QA_FINDING_STATUSES, QA_OWNER_TYPES, zQaEnvironment, zSteps, zCreateQaReport, zPatchQaReport, zQaCode, zCreateQaCase, zPatchQaCase, zCreateQaFinding, zPatchQaFinding, qaCodeKey, QA_EXPORT_FORMATS;
|
|
8741
8752
|
var init_qa = __esm({
|
|
8742
8753
|
"../shared/src/qa.ts"() {
|
|
8743
8754
|
"use strict";
|
|
@@ -8761,7 +8772,9 @@ var init_qa = __esm({
|
|
|
8761
8772
|
verdict: external_exports.enum(QA_VERDICTS).nullable().default(null)
|
|
8762
8773
|
});
|
|
8763
8774
|
zPatchQaReport = zCreateQaReport.partial().extend({ status: external_exports.enum(QA_REPORT_STATUSES).optional() });
|
|
8775
|
+
zQaCode = external_exports.string().trim().max(40).refine((c) => !/[\u0000-\u001f·]/.test(c), "kode tak boleh berisi baris baru atau karakter \xB7").refine((c) => c.toLowerCase() !== "laporan", '"Laporan" dicadangkan untuk lampiran laporan').transform((c) => c || null).nullable();
|
|
8764
8776
|
zCreateQaCase = external_exports.object({
|
|
8777
|
+
code: zQaCode.optional(),
|
|
8765
8778
|
title: external_exports.string().trim().min(1).max(300),
|
|
8766
8779
|
steps: external_exports.string().max(1e4).default(""),
|
|
8767
8780
|
expected: external_exports.string().max(1e4).default(""),
|
|
@@ -8771,6 +8784,7 @@ var init_qa = __esm({
|
|
|
8771
8784
|
});
|
|
8772
8785
|
zPatchQaCase = zCreateQaCase.partial();
|
|
8773
8786
|
zCreateQaFinding = external_exports.object({
|
|
8787
|
+
code: zQaCode.optional(),
|
|
8774
8788
|
title: external_exports.string().trim().min(1).max(300),
|
|
8775
8789
|
caseId: external_exports.string().max(120).nullable().default(null),
|
|
8776
8790
|
severity: external_exports.enum(QA_SEVERITIES).default("major"),
|
|
@@ -8782,6 +8796,7 @@ var init_qa = __esm({
|
|
|
8782
8796
|
status: external_exports.enum(["open", "wontfix"]).default("open")
|
|
8783
8797
|
});
|
|
8784
8798
|
zPatchQaFinding = zCreateQaFinding.partial();
|
|
8799
|
+
qaCodeKey = (c) => c.trim().toLowerCase();
|
|
8785
8800
|
QA_EXPORT_FORMATS = ["zip", "md", "docx", "pdf", "xlsx", "csv"];
|
|
8786
8801
|
}
|
|
8787
8802
|
});
|
|
@@ -8915,7 +8930,8 @@ function parseQaMarkdown(text2) {
|
|
|
8915
8930
|
if (!c[1]) throw new QaMarkdownError("judul test case kosong", abs(cs, idx));
|
|
8916
8931
|
cases.push({
|
|
8917
8932
|
id: c[6] || null,
|
|
8918
|
-
code: c[0],
|
|
8933
|
+
code: c[0].trim(),
|
|
8934
|
+
customCode: c[0].trim() || null,
|
|
8919
8935
|
title: c[1],
|
|
8920
8936
|
steps: c[2],
|
|
8921
8937
|
expected: c[3],
|
|
@@ -8930,8 +8946,8 @@ function parseQaMarkdown(text2) {
|
|
|
8930
8946
|
if (fs2) {
|
|
8931
8947
|
const heads = fs2.lines.map((l, idx) => ({ l, idx })).filter((x) => x.l.startsWith("### "));
|
|
8932
8948
|
heads.forEach((h2, n2) => {
|
|
8933
|
-
const hm = /^### (
|
|
8934
|
-
if (!hm) throw new QaMarkdownError("judul temuan tak valid; format: `### F-01 \xB7 [major/P1] Judul`", abs(fs2, h2.idx));
|
|
8949
|
+
const hm = /^### (.+?) · \[(\w+)\/(P\d)\] (.+)$/.exec(h2.l);
|
|
8950
|
+
if (!hm) throw new QaMarkdownError("judul temuan tak valid; format: `### F-01 \xB7 [major/P1] Judul` (kode bebas, tanpa \xB7)", abs(fs2, h2.idx));
|
|
8935
8951
|
const severity = oneOf(hm[2], QA_SEVERITIES, "severity", abs(fs2, h2.idx));
|
|
8936
8952
|
const priority = oneOf(hm[3], QA_PRIORITIES, "prioritas", abs(fs2, h2.idx));
|
|
8937
8953
|
const end2 = n2 + 1 < heads.length ? heads[n2 + 1].idx : fs2.lines.length;
|
|
@@ -8956,7 +8972,7 @@ function parseQaMarkdown(text2) {
|
|
|
8956
8972
|
area = am[1].trim();
|
|
8957
8973
|
continue;
|
|
8958
8974
|
}
|
|
8959
|
-
const tm = /^\*\*Test case:\*\* ?(
|
|
8975
|
+
const tm = /^\*\*Test case:\*\* ?(.+?)\s*$/.exec(l);
|
|
8960
8976
|
if (tm) {
|
|
8961
8977
|
caseCode = tm[1];
|
|
8962
8978
|
continue;
|
|
@@ -8991,7 +9007,8 @@ function parseQaMarkdown(text2) {
|
|
|
8991
9007
|
id: typeof meta.id === "string" && meta.id ? meta.id : null,
|
|
8992
9008
|
caseId: typeof meta.caseId === "string" && meta.caseId ? meta.caseId : null,
|
|
8993
9009
|
caseCode,
|
|
8994
|
-
code: hm[1],
|
|
9010
|
+
code: hm[1].trim(),
|
|
9011
|
+
customCode: hm[1].trim(),
|
|
8995
9012
|
title: hm[4].trim(),
|
|
8996
9013
|
severity,
|
|
8997
9014
|
priority,
|
|
@@ -9008,7 +9025,7 @@ function parseQaMarkdown(text2) {
|
|
|
9008
9025
|
if (ls) {
|
|
9009
9026
|
let code = null;
|
|
9010
9027
|
for (const l of ls.lines) {
|
|
9011
|
-
const hm = /^### (
|
|
9028
|
+
const hm = /^### (.+?)\s*$/.exec(l);
|
|
9012
9029
|
if (hm) {
|
|
9013
9030
|
code = hm[1];
|
|
9014
9031
|
continue;
|
|
@@ -9017,6 +9034,28 @@ function parseQaMarkdown(text2) {
|
|
|
9017
9034
|
if (c) c.attachments.push(...pathsIn([l]));
|
|
9018
9035
|
}
|
|
9019
9036
|
}
|
|
9037
|
+
const seen2 = /* @__PURE__ */ new Set();
|
|
9038
|
+
for (const x of [...cases, ...findings]) {
|
|
9039
|
+
if (!x.code) continue;
|
|
9040
|
+
const k = x.code.toLowerCase();
|
|
9041
|
+
if (k === "laporan") throw new QaMarkdownError(`kode "${x.code}" dicadangkan untuk lampiran laporan`, 1);
|
|
9042
|
+
if (seen2.has(k)) throw new QaMarkdownError(`kode ${x.code} dipakai lebih dari sekali; kode harus unik`, 1);
|
|
9043
|
+
seen2.add(k);
|
|
9044
|
+
}
|
|
9045
|
+
let tcN = 0;
|
|
9046
|
+
for (const c of cases) if (!c.code) {
|
|
9047
|
+
do {
|
|
9048
|
+
c.code = `TC-${String(++tcN).padStart(2, "0")}`;
|
|
9049
|
+
} while (seen2.has(c.code.toLowerCase()));
|
|
9050
|
+
seen2.add(c.code.toLowerCase());
|
|
9051
|
+
}
|
|
9052
|
+
for (const f of findings) {
|
|
9053
|
+
if (f.caseCode) {
|
|
9054
|
+
const c = cases.find((x) => x.code.toLowerCase() === f.caseCode.toLowerCase());
|
|
9055
|
+
if (!c) throw new QaMarkdownError(`temuan ${f.code}: test case ${f.caseCode} tidak ditemukan`, 1);
|
|
9056
|
+
f.caseCode = c.code;
|
|
9057
|
+
}
|
|
9058
|
+
}
|
|
9020
9059
|
return {
|
|
9021
9060
|
reportId: fs("reportId") || null,
|
|
9022
9061
|
title: fs("title").trim(),
|
|
@@ -13460,7 +13499,7 @@ var init_team2 = __esm({
|
|
|
13460
13499
|
});
|
|
13461
13500
|
|
|
13462
13501
|
// ../shared/src/mcp-catalog/qa.ts
|
|
13463
|
-
var PROJECT3, REPORT, CASE, FINDING, base, one, parseEnv, parseSteps, nullable2, put, REPORT_FIELDS, reportBody, caseBody, findingBody, CASE_FIELDS, FINDING_FIELDS, QA_TOOLS;
|
|
13502
|
+
var PROJECT3, REPORT, CASE, FINDING, base, one, parseEnv, parseSteps, nullable2, put, REPORT_FIELDS, reportBody, caseBody, findingBody, CODE_FIELD, CASE_FIELDS, FINDING_FIELDS, QA_TOOLS;
|
|
13464
13503
|
var init_qa2 = __esm({
|
|
13465
13504
|
"../shared/src/mcp-catalog/qa.ts"() {
|
|
13466
13505
|
"use strict";
|
|
@@ -13508,6 +13547,7 @@ var init_qa2 = __esm({
|
|
|
13508
13547
|
caseBody = (a) => {
|
|
13509
13548
|
const b = {};
|
|
13510
13549
|
for (const k of ["title", "steps", "expected", "actual", "status"]) put(b, k, s(a[k]));
|
|
13550
|
+
put(b, "code", nullable2(a.code));
|
|
13511
13551
|
return b;
|
|
13512
13552
|
};
|
|
13513
13553
|
findingBody = (a) => {
|
|
@@ -13515,9 +13555,12 @@ var init_qa2 = __esm({
|
|
|
13515
13555
|
for (const k of ["title", "severity", "priority", "area", "expected", "actual", "status"]) put(b, k, s(a[k]));
|
|
13516
13556
|
put(b, "steps", parseSteps(a.steps));
|
|
13517
13557
|
put(b, "caseId", nullable2(a.testCase));
|
|
13558
|
+
put(b, "code", nullable2(a.code));
|
|
13518
13559
|
return b;
|
|
13519
13560
|
};
|
|
13561
|
+
CODE_FIELD = str("Kode bebas (mis. `LOGIN-01`), unik di satu laporan (test case + temuan, tak peka huruf). String KOSONG kembali ke nomor otomatis; tak menyebutnya membiarkan apa adanya.");
|
|
13520
13562
|
CASE_FIELDS = {
|
|
13563
|
+
code: CODE_FIELD,
|
|
13521
13564
|
title: str("Judul test case."),
|
|
13522
13565
|
steps: str("Langkah uji (teks bebas)."),
|
|
13523
13566
|
expected: str("Hasil yang diharapkan."),
|
|
@@ -13525,6 +13568,7 @@ var init_qa2 = __esm({
|
|
|
13525
13568
|
status: enumStr(QA_CASE_STATUSES, "Status eksekusi.")
|
|
13526
13569
|
};
|
|
13527
13570
|
FINDING_FIELDS = {
|
|
13571
|
+
code: CODE_FIELD,
|
|
13528
13572
|
title: str("Judul temuan \u2014 satu masalah, satu baris."),
|
|
13529
13573
|
severity: enumStr(QA_SEVERITIES, "Dampak TEKNIS. Bukan prioritas."),
|
|
13530
13574
|
priority: enumStr(QA_PRIORITIES, "Urutan perbaikan P0 (paling mendesak)\u2013P3. Terpisah dari severity: bug minor di halaman checkout bisa P0."),
|
|
@@ -13551,7 +13595,7 @@ var init_qa2 = __esm({
|
|
|
13551
13595
|
{
|
|
13552
13596
|
name: "hanoman_qa_report_get",
|
|
13553
13597
|
title: "Detail laporan QA",
|
|
13554
|
-
description: "Satu laporan QA lengkap: header, test case (`TC-01\u2026`), temuan (`F-01\u2026
|
|
13598
|
+
description: "Satu laporan QA lengkap: header, test case (kode otomatis `TC-01\u2026` atau kode bebas QA), temuan (`F-01\u2026` atau kode bebas, dengan repro bernomor, severity, prioritas, `caseCode`), dan metadata lampiran (nama, tipe, ukuran \u2014 bukan byte).",
|
|
13555
13599
|
inputSchema: obj({ properties: { project: PROJECT3, report: REPORT }, required: ["project", "report"] }),
|
|
13556
13600
|
mode: "read",
|
|
13557
13601
|
capability: "qa:read",
|
|
@@ -52197,8 +52241,8 @@ var init_sync = __esm({
|
|
|
52197
52241
|
// `qaFinding.backlogId` ikut (cermin task.specId): tanpa itu mesin lain bisa mengirim ulang temuan yang
|
|
52198
52242
|
// di mesin ini sudah jadi backlog.
|
|
52199
52243
|
qaReport: ["projectId", "title", "buildVersion", "environment", "scope", "tester", "summary", "status", "verdict", "createdAt", "updatedAt"],
|
|
52200
|
-
qaCase: ["reportId", "title", "steps", "expected", "actual", "status", "order", "createdAt", "updatedAt"],
|
|
52201
|
-
qaFinding: ["reportId", "caseId", "title", "severity", "priority", "area", "steps", "expected", "actual", "status", "backlogId", "createdAt", "updatedAt"],
|
|
52244
|
+
qaCase: ["reportId", "code", "title", "steps", "expected", "actual", "status", "order", "createdAt", "updatedAt"],
|
|
52245
|
+
qaFinding: ["reportId", "caseId", "code", "title", "severity", "priority", "area", "steps", "expected", "actual", "status", "backlogId", "createdAt", "updatedAt"],
|
|
52202
52246
|
qaAttachment: ["reportId", "projectId", "ownerType", "ownerId", "filename", "mimeType", "size", "sha256", "storageKey", "createdAt", "updatedAt"]
|
|
52203
52247
|
};
|
|
52204
52248
|
DATE_FIELDS = {
|
|
@@ -64014,8 +64058,9 @@ var attachmentView = (a) => ({
|
|
|
64014
64058
|
syncState: a.syncState === "local-only" && !effectiveStr("SYNC_SERVER_URL") ? "available" : a.syncState,
|
|
64015
64059
|
createdAt: iso3(a.createdAt)
|
|
64016
64060
|
});
|
|
64017
|
-
|
|
64018
|
-
|
|
64061
|
+
var takenCodes = (...lists) => new Set(lists.flat().filter((r) => r.code).map((r) => qaCodeKey(r.code)));
|
|
64062
|
+
function caseViews(rows, taken = /* @__PURE__ */ new Set()) {
|
|
64063
|
+
return assignCodes(rows, "TC-", 2, taken).map((r) => ({
|
|
64019
64064
|
id: r.id,
|
|
64020
64065
|
reportId: r.reportId,
|
|
64021
64066
|
code: r.code,
|
|
@@ -64029,9 +64074,9 @@ function caseViews(rows) {
|
|
|
64029
64074
|
updatedAt: iso3(r.updatedAt)
|
|
64030
64075
|
})).sort((a, b) => a.order - b.order || (a.id < b.id ? -1 : 1));
|
|
64031
64076
|
}
|
|
64032
|
-
function findingViews(rows, cases, specs = /* @__PURE__ */ new Map()) {
|
|
64077
|
+
function findingViews(rows, cases, specs = /* @__PURE__ */ new Map(), taken = /* @__PURE__ */ new Set()) {
|
|
64033
64078
|
const caseCode = new Map(cases.map((c) => [c.id, c.code]));
|
|
64034
|
-
return assignCodes(rows, "F-", 2).map((r) => ({
|
|
64079
|
+
return assignCodes(rows, "F-", 2, taken).map((r) => ({
|
|
64035
64080
|
id: r.id,
|
|
64036
64081
|
reportId: r.reportId,
|
|
64037
64082
|
code: r.code,
|
|
@@ -64071,7 +64116,8 @@ async function reportDetail(projectId, reportId) {
|
|
|
64071
64116
|
});
|
|
64072
64117
|
if (!row) return null;
|
|
64073
64118
|
const code = (await reportCodes(projectId)).get(row.id);
|
|
64074
|
-
const
|
|
64119
|
+
const taken = takenCodes(row.cases, row.findings);
|
|
64120
|
+
const cases = caseViews(row.cases, taken);
|
|
64075
64121
|
const specIds = [...new Set(row.findings.map((f) => f.backlogId).filter((x) => !!x))];
|
|
64076
64122
|
const specs = new Map(
|
|
64077
64123
|
(await prisma.spec.findMany({ where: { id: { in: specIds } }, select: { id: true, stage: true, priority: true } })).map((x) => [x.id, x])
|
|
@@ -64079,10 +64125,19 @@ async function reportDetail(projectId, reportId) {
|
|
|
64079
64125
|
return {
|
|
64080
64126
|
...reportView(row, code, row.cases, row.findings),
|
|
64081
64127
|
cases,
|
|
64082
|
-
findings: findingViews(row.findings, cases, specs),
|
|
64128
|
+
findings: findingViews(row.findings, cases, specs, taken),
|
|
64083
64129
|
attachments: row.attachments.map(attachmentView)
|
|
64084
64130
|
};
|
|
64085
64131
|
}
|
|
64132
|
+
async function qaCodeProblem(reportId, code, exceptId) {
|
|
64133
|
+
if (!code) return null;
|
|
64134
|
+
const [cases, findings] = await Promise.all([
|
|
64135
|
+
prisma.qaCase.findMany({ where: { reportId, code: { not: null } }, select: { id: true, code: true } }),
|
|
64136
|
+
prisma.qaFinding.findMany({ where: { reportId, code: { not: null } }, select: { id: true, code: true } })
|
|
64137
|
+
]);
|
|
64138
|
+
const key = qaCodeKey(code);
|
|
64139
|
+
return [...cases, ...findings].some((r) => r.id !== exceptId && qaCodeKey(r.code) === key) ? { error: "kode sudah dipakai di laporan ini; kode test case dan temuan harus unik", code } : null;
|
|
64140
|
+
}
|
|
64086
64141
|
|
|
64087
64142
|
// src/services/sync-delete.ts
|
|
64088
64143
|
init_sync();
|
|
@@ -79444,8 +79499,11 @@ async function qa(app3) {
|
|
|
79444
79499
|
if (!parsed.success) return bad(reply, parsed.error);
|
|
79445
79500
|
const p3 = parsed.data;
|
|
79446
79501
|
const last2 = await prisma.qaCase.findFirst({ where: { reportId: rid }, orderBy: { order: "desc" }, select: { order: true } });
|
|
79502
|
+
const clash = await qaCodeProblem(rid, p3.code);
|
|
79503
|
+
if (clash) return reply.code(409).send(clash);
|
|
79447
79504
|
const row = await prisma.qaCase.create({ data: {
|
|
79448
79505
|
reportId: rid,
|
|
79506
|
+
code: p3.code ?? null,
|
|
79449
79507
|
title: p3.title,
|
|
79450
79508
|
steps: p3.steps,
|
|
79451
79509
|
expected: p3.expected,
|
|
@@ -79465,6 +79523,8 @@ async function qa(app3) {
|
|
|
79465
79523
|
if (r.status === "closed") return locked(reply);
|
|
79466
79524
|
const parsed = zPatchQaCase.safeParse(req.body ?? {});
|
|
79467
79525
|
if (!parsed.success) return bad(reply, parsed.error);
|
|
79526
|
+
const clash = await qaCodeProblem(rid, parsed.data.code, cid);
|
|
79527
|
+
if (clash) return reply.code(409).send(clash);
|
|
79468
79528
|
await prisma.qaCase.update({ where: { id: cid }, data: parsed.data });
|
|
79469
79529
|
await notifySynced("qaCase", cid);
|
|
79470
79530
|
await touch(rid);
|
|
@@ -79495,8 +79555,11 @@ async function qa(app3) {
|
|
|
79495
79555
|
const p3 = parsed.data;
|
|
79496
79556
|
const problem = await caseProblem(rid, p3.caseId);
|
|
79497
79557
|
if (problem) return reply.code(400).send(problem);
|
|
79558
|
+
const clash = await qaCodeProblem(rid, p3.code);
|
|
79559
|
+
if (clash) return reply.code(409).send(clash);
|
|
79498
79560
|
const row = await prisma.qaFinding.create({ data: {
|
|
79499
79561
|
reportId: rid,
|
|
79562
|
+
code: p3.code ?? null,
|
|
79500
79563
|
caseId: p3.caseId,
|
|
79501
79564
|
title: p3.title,
|
|
79502
79565
|
severity: p3.severity,
|
|
@@ -79522,7 +79585,10 @@ async function qa(app3) {
|
|
|
79522
79585
|
const p3 = parsed.data;
|
|
79523
79586
|
const problem = await caseProblem(rid, p3.caseId);
|
|
79524
79587
|
if (problem) return reply.code(400).send(problem);
|
|
79588
|
+
const clash = await qaCodeProblem(rid, p3.code, fid);
|
|
79589
|
+
if (clash) return reply.code(409).send(clash);
|
|
79525
79590
|
await prisma.qaFinding.update({ where: { id: fid }, data: {
|
|
79591
|
+
code: p3.code,
|
|
79526
79592
|
caseId: p3.caseId,
|
|
79527
79593
|
title: p3.title,
|
|
79528
79594
|
severity: p3.severity,
|
|
@@ -79885,9 +79951,10 @@ var GUIDE2 = [
|
|
|
79885
79951
|
["Ringkasan", "Judul wajib. Kolom lain boleh kosong. Status: draft, submitted, closed. Keputusan: go (siap), no-go (belum siap), conditional (siap dengan catatan); wajib untuk submitted/closed."],
|
|
79886
79952
|
["Test case", "Satu baris = satu pengujian. Status: belum, lulus, gagal, terblokir, dilewati (atau todo, pass, fail, blocked, skipped). Langkah: satu per baris di dalam sel (Alt+Enter)."],
|
|
79887
79953
|
["Temuan", "Satu baris = satu masalah. Severity: blocker (tidak bisa dipakai), critical (kritis), major (fitur terganggu), minor (gangguan kecil), trivial (tampilan). Prioritas: P0 segera, P1 tinggi, P2 normal, P3 rendah."],
|
|
79888
|
-
["Lampiran", "Satu baris = satu berkas. Pemilik: Laporan, TC-01
|
|
79954
|
+
["Lampiran", "Satu baris = satu berkas. Pemilik: Laporan, atau Kode test case/temuan (apa pun yang Anda tulis, mis. TC-01 atau LOGIN-3). Berkas: nama screenshot/log yang dipilih bersama Excel, atau attachments/nama.png dalam ZIP. Maks 10 MB/berkas, 30 berkas dan 100 MB/laporan."],
|
|
79889
79955
|
["Impor dengan lampiran", "Pilih Excel dan berkas lampiran bersama-sama di dashboard, atau ZIP berisi report.xlsx dan folder attachments/. Gambar yang hanya ditempel di sel Excel tidak diimpor; sertakan berkas gambarnya."],
|
|
79890
|
-
["Ref", "Biarkan Ref laporan/test case/temuan hasil ekspor untuk memperbarui laporan asal. Kosongkan untuk entri baru.
|
|
79956
|
+
["Ref", "Biarkan Ref laporan/test case/temuan hasil ekspor untuk memperbarui laporan asal. Kosongkan untuk entri baru."],
|
|
79957
|
+
["Kode", "Bebas Anda tentukan (mis. LOGIN-01, AUTH.3), asal unik di seluruh laporan \u2014 test case dan temuan tidak boleh sama, huruf besar/kecil dianggap sama. Kosongkan untuk nomor otomatis (TC-01 / F-01). Test case pada Temuan merujuk Kode test case."],
|
|
79891
79958
|
["Lingkungan", "Satu per baris: os=Windows 11, browser=Chrome, device=Laptop, url=https://\u2026 ."]
|
|
79892
79959
|
];
|
|
79893
79960
|
function writeQaWorkbook(d, paths2 = {}) {
|
|
@@ -79953,7 +80020,7 @@ function parseQaWorkbook(buf) {
|
|
|
79953
80020
|
const rowOf = (k) => Math.max(1, summary.findIndex((r) => norm3(r[0] ?? "") === norm3(k)) + 1);
|
|
79954
80021
|
if (!val("Judul").trim()) fail3("Ringkasan", rowOf("Judul"), "Judul wajib diisi");
|
|
79955
80022
|
const environment = /* @__PURE__ */ Object.create(null);
|
|
79956
|
-
for (const line of val("Lingkungan").split(
|
|
80023
|
+
for (const line of val("Lingkungan").split(/\r\n|\r|\n|;(?=\s*[^=:;\s]+\s*[=:])/).filter((l) => l.trim())) {
|
|
79957
80024
|
const match = /^\s*([^=:]+)[=:]\s*(.*)$/.exec(line);
|
|
79958
80025
|
if (!match) fail3("Ringkasan", rowOf("Lingkungan"), "Lingkungan: gunakan kunci=nilai, satu per baris");
|
|
79959
80026
|
environment[match[1].trim()] = match[2];
|
|
@@ -79983,48 +80050,64 @@ function parseQaWorkbook(buf) {
|
|
|
79983
80050
|
if (e instanceof QaTableError) throw new XlsxError(`Test case, ${e.message}`);
|
|
79984
80051
|
throw e;
|
|
79985
80052
|
}
|
|
79986
|
-
const
|
|
80053
|
+
const rows = read2("Temuan");
|
|
80054
|
+
const headers = rows[0]?.map(norm3) ?? [];
|
|
80055
|
+
for (const h2 of ["judul", "severity", "prioritas", "langkah", "expected", "actual"]) if (!headers.includes(h2)) fail3("Temuan", 1, `kolom ${h2} wajib ada`);
|
|
80056
|
+
const getF = (r, k) => r[headers.indexOf(norm3(k))] ?? "";
|
|
80057
|
+
const findingRows = rows.slice(1).map((r, i) => ({ r, row: i + 2 })).filter(({ r }) => !r.every((v) => !v.trim()));
|
|
80058
|
+
const used = /* @__PURE__ */ new Map();
|
|
80059
|
+
const claim = (sheet, row, code) => {
|
|
80060
|
+
if (norm3(code) === "laporan") fail3(sheet, row, 'Kode "Laporan" dicadangkan untuk lampiran laporan');
|
|
80061
|
+
if (used.has(qaCodeKey(code))) fail3(sheet, row, `Kode ${code} dipakai ${used.get(qaCodeKey(code))}; kode harus unik`);
|
|
80062
|
+
used.set(qaCodeKey(code), `${sheet}`);
|
|
80063
|
+
};
|
|
80064
|
+
for (const c of cases) if (c.code.trim()) claim("Test case", c.row, c.code.trim());
|
|
80065
|
+
for (const { r, row } of findingRows) if (getF(r, "Kode").trim()) claim("Temuan", row, getF(r, "Kode").trim());
|
|
80066
|
+
const autoCode = (prefix, counter2) => {
|
|
80067
|
+
let code;
|
|
80068
|
+
do {
|
|
80069
|
+
code = `${prefix}${String(++counter2.n).padStart(2, "0")}`;
|
|
80070
|
+
} while (used.has(qaCodeKey(code)));
|
|
80071
|
+
return code;
|
|
80072
|
+
};
|
|
80073
|
+
const caseCounter = { n: 0 };
|
|
80074
|
+
const caseByKey = /* @__PURE__ */ new Map();
|
|
79987
80075
|
const refs2 = /* @__PURE__ */ new Set();
|
|
79988
|
-
for (const
|
|
79989
|
-
const
|
|
79990
|
-
|
|
80076
|
+
for (const c of cases) {
|
|
80077
|
+
const customCode = c.code.trim() || null;
|
|
80078
|
+
const code = customCode ?? autoCode("TC-", caseCounter);
|
|
79991
80079
|
if (c.ref && refs2.has(c.ref)) fail3("Test case", c.row, "Ref berulang");
|
|
79992
|
-
codes.add(code);
|
|
79993
80080
|
if (c.ref) refs2.add(c.ref);
|
|
79994
80081
|
const result = zCreateQaCase.safeParse({ title: c.title, steps: c.steps ?? "", expected: c.expected ?? "", actual: c.actual ?? "", status: c.status ?? "todo" });
|
|
79995
80082
|
if (!result.success) fail3("Test case", c.row, result.error.issues.map((e) => `${e.path}: ${e.message}`).join("; "));
|
|
79996
|
-
|
|
80083
|
+
caseByKey.set(qaCodeKey(code), code);
|
|
80084
|
+
parsed.cases.push({ ...result.data, id: c.ref, code, customCode, attachments: [] });
|
|
79997
80085
|
}
|
|
79998
|
-
const
|
|
79999
|
-
const headers = rows[0]?.map(norm3) ?? [];
|
|
80000
|
-
for (const h2 of ["judul", "severity", "prioritas", "langkah", "expected", "actual"]) if (!headers.includes(h2)) fail3("Temuan", 1, `kolom ${h2} wajib ada`);
|
|
80001
|
-
const findingCodes = /* @__PURE__ */ new Set();
|
|
80086
|
+
const findingCounter = { n: 0 };
|
|
80002
80087
|
const findingRefs = /* @__PURE__ */ new Set();
|
|
80003
|
-
for (
|
|
80004
|
-
const
|
|
80005
|
-
|
|
80006
|
-
const
|
|
80007
|
-
const code = get("Kode") || `F-${String(parsed.findings.length + 1).padStart(2, "0")}`;
|
|
80008
|
-
if (!/^F-\d+$/.test(code) || findingCodes.has(code)) fail3("Temuan", i + 1, "Kode harus unik dan berformat F-01");
|
|
80009
|
-
findingCodes.add(code);
|
|
80088
|
+
for (const { r, row } of findingRows) {
|
|
80089
|
+
const get = (k) => getF(r, k);
|
|
80090
|
+
const customCode = get("Kode").trim() || null;
|
|
80091
|
+
const code = customCode ?? autoCode("F-", findingCounter);
|
|
80010
80092
|
const id2 = get("Ref") || null;
|
|
80011
|
-
if (id2 && findingRefs.has(id2)) fail3("Temuan",
|
|
80093
|
+
if (id2 && findingRefs.has(id2)) fail3("Temuan", row, "Ref berulang");
|
|
80012
80094
|
if (id2) findingRefs.add(id2);
|
|
80013
|
-
const
|
|
80014
|
-
|
|
80015
|
-
|
|
80095
|
+
const caseRef = get("Test case").trim();
|
|
80096
|
+
const caseCode = caseRef ? caseByKey.get(qaCodeKey(caseRef)) ?? null : null;
|
|
80097
|
+
if (caseRef && !caseCode) fail3("Temuan", row, `Test case ${caseRef} tidak ditemukan`);
|
|
80098
|
+
const status2 = choice(get("Status") || "open", QA_FINDING_STATUSES, "Temuan", row);
|
|
80016
80099
|
const result = zCreateQaFinding.safeParse({
|
|
80017
80100
|
title: get("Judul"),
|
|
80018
|
-
severity: choice(get("Severity") || "major", QA_SEVERITIES, "Temuan",
|
|
80019
|
-
priority: choice(get("Prioritas") || "P2", QA_PRIORITIES, "Temuan",
|
|
80101
|
+
severity: choice(get("Severity") || "major", QA_SEVERITIES, "Temuan", row),
|
|
80102
|
+
priority: choice(get("Prioritas") || "P2", QA_PRIORITIES, "Temuan", row),
|
|
80020
80103
|
area: get("Area"),
|
|
80021
80104
|
steps: get("Langkah").split("\n").map((s2) => s2.trim().replace(/^\d+[.)]\s+/, "")).filter(Boolean),
|
|
80022
80105
|
expected: get("Expected"),
|
|
80023
80106
|
actual: get("Actual"),
|
|
80024
80107
|
status: status2 === "sent" ? "open" : status2
|
|
80025
80108
|
});
|
|
80026
|
-
if (!result.success) fail3("Temuan",
|
|
80027
|
-
parsed.findings.push({ ...result.data, status: status2, id: id2, code, caseId: null, caseCode, attachments: [] });
|
|
80109
|
+
if (!result.success) fail3("Temuan", row, result.error.issues.map((e) => `${e.path}: ${e.message}`).join("; "));
|
|
80110
|
+
parsed.findings.push({ ...result.data, status: status2, id: id2, code, customCode, caseId: null, caseCode, attachments: [] });
|
|
80028
80111
|
}
|
|
80029
80112
|
const attachments = read2("Lampiran");
|
|
80030
80113
|
if (norm3(attachments[0]?.[0] ?? "") !== "pemilik" || norm3(attachments[0]?.[1] ?? "") !== "berkas") fail3("Lampiran", 1, "butuh kolom Pemilik dan Berkas");
|
|
@@ -80032,7 +80115,7 @@ function parseQaWorkbook(buf) {
|
|
|
80032
80115
|
const [owner = "", path = ""] = attachments[i];
|
|
80033
80116
|
if (!owner.trim() && !path.trim()) continue;
|
|
80034
80117
|
if (!path.trim() || path.includes("\\") || path.startsWith("/") || path.split("/").some((p3) => p3 === ".." || p3 === "." || !p3)) fail3("Lampiran", i + 1, "Berkas harus nama berkas atau path relatif yang aman");
|
|
80035
|
-
const target3 = norm3(owner) === "laporan" ? parsed.attachments : parsed.cases.find((c) => c.code === owner
|
|
80118
|
+
const target3 = norm3(owner) === "laporan" ? parsed.attachments : parsed.cases.find((c) => qaCodeKey(c.code) === qaCodeKey(owner))?.attachments ?? parsed.findings.find((f) => qaCodeKey(f.code) === qaCodeKey(owner))?.attachments;
|
|
80036
80119
|
if (!target3) fail3("Lampiran", i + 1, `Pemilik "${owner}" tidak ditemukan; gunakan Laporan atau Kode TC/F yang ada`);
|
|
80037
80120
|
target3.push(path.trim());
|
|
80038
80121
|
}
|
|
@@ -80156,7 +80239,7 @@ async function importReport(projectId, file, companions = []) {
|
|
|
80156
80239
|
const caseByFileId = /* @__PURE__ */ new Map();
|
|
80157
80240
|
const caseByCode = /* @__PURE__ */ new Map();
|
|
80158
80241
|
for (const [idx, c] of parsed.cases.entries()) {
|
|
80159
|
-
const fields = { title: c.title, steps: c.steps, expected: c.expected, actual: c.actual, status: c.status, order: idx + 1 };
|
|
80242
|
+
const fields = { code: c.customCode, title: c.title, steps: c.steps, expected: c.expected, actual: c.actual, status: c.status, order: idx + 1 };
|
|
80160
80243
|
const row = c.id && knownCases.has(c.id) ? await tx.qaCase.update({ where: { id: c.id }, data: fields }) : await tx.qaCase.create({ data: { reportId: report.id, ...fields, createdAt: new Date(now + 1 + idx) } });
|
|
80161
80244
|
if (c.id) caseByFileId.set(c.id, row.id);
|
|
80162
80245
|
caseByCode.set(c.code, row.id);
|
|
@@ -80167,6 +80250,7 @@ async function importReport(projectId, file, companions = []) {
|
|
|
80167
80250
|
for (const [idx, f] of parsed.findings.entries()) {
|
|
80168
80251
|
const caseId = f.caseId && caseByFileId.get(f.caseId) || f.caseCode && caseByCode.get(f.caseCode) || null;
|
|
80169
80252
|
const fields = {
|
|
80253
|
+
code: f.customCode,
|
|
80170
80254
|
caseId,
|
|
80171
80255
|
title: f.title,
|
|
80172
80256
|
severity: f.severity,
|
|
@@ -80249,6 +80333,20 @@ async function importCases(projectId, reportId, file) {
|
|
|
80249
80333
|
const result = await prisma.$transaction(async (tx) => {
|
|
80250
80334
|
const existing = new Map((await tx.qaCase.findMany({ where: { reportId } })).map((c) => [c.id, c]));
|
|
80251
80335
|
let maxOrder = Math.max(0, ...[...existing.values()].map((c) => c.order));
|
|
80336
|
+
const findings = await tx.qaFinding.findMany({ where: { reportId }, select: { code: true } });
|
|
80337
|
+
const shown = new Map(caseViews([...existing.values()], takenCodes([...existing.values()], findings)).map((c) => [c.id, c.code]));
|
|
80338
|
+
const owner = /* @__PURE__ */ new Map();
|
|
80339
|
+
for (const c of existing.values()) if (c.code) owner.set(qaCodeKey(c.code), c.id);
|
|
80340
|
+
for (const f of findings) if (f.code) owner.set(qaCodeKey(f.code), "finding");
|
|
80341
|
+
const wanted = (r, id2, current) => {
|
|
80342
|
+
const code = r.code.trim();
|
|
80343
|
+
if (!code || code === current) return void 0;
|
|
80344
|
+
if (qaCodeKey(code) === "laporan") throw new QaImportError(400, `baris ${r.row}: kode "Laporan" dicadangkan`);
|
|
80345
|
+
const holder = owner.get(qaCodeKey(code));
|
|
80346
|
+
if (holder && holder !== id2) throw new QaImportError(400, `baris ${r.row}: kode ${code} sudah dipakai; kode harus unik`);
|
|
80347
|
+
owner.set(qaCodeKey(code), id2);
|
|
80348
|
+
return code;
|
|
80349
|
+
};
|
|
80252
80350
|
const out4 = { updated: 0, created: 0, unchanged: 0 };
|
|
80253
80351
|
for (const [i, r] of parsed.entries()) {
|
|
80254
80352
|
const cur = r.ref ? existing.get(r.ref) : void 0;
|
|
@@ -80260,18 +80358,21 @@ async function importCases(projectId, reportId, file) {
|
|
|
80260
80358
|
actual: r.actual ?? cur.actual,
|
|
80261
80359
|
status: r.status ?? cur.status
|
|
80262
80360
|
};
|
|
80263
|
-
|
|
80361
|
+
const code2 = wanted(r, cur.id, shown.get(cur.id));
|
|
80362
|
+
if (!code2 && next.title === cur.title && next.steps === cur.steps && next.expected === cur.expected && next.actual === cur.actual && next.status === cur.status) {
|
|
80264
80363
|
out4.unchanged++;
|
|
80265
80364
|
continue;
|
|
80266
80365
|
}
|
|
80267
|
-
await tx.qaCase.update({ where: { id: cur.id }, data: next });
|
|
80366
|
+
await tx.qaCase.update({ where: { id: cur.id }, data: code2 ? { ...next, code: code2 } : next });
|
|
80268
80367
|
changed.push(cur.id);
|
|
80269
80368
|
out4.updated++;
|
|
80270
80369
|
continue;
|
|
80271
80370
|
}
|
|
80272
80371
|
if (!r.title) throw new QaImportError(400, `baris ${r.row}: judul wajib diisi (Ref "${r.ref}" tak dikenal di laporan ini)`);
|
|
80372
|
+
const code = wanted(r, `new-${i}`, void 0);
|
|
80273
80373
|
const made = await tx.qaCase.create({ data: {
|
|
80274
80374
|
reportId,
|
|
80375
|
+
code: code ?? null,
|
|
80275
80376
|
title: r.title,
|
|
80276
80377
|
steps: r.steps ?? "",
|
|
80277
80378
|
expected: r.expected ?? "",
|
package/package.json
CHANGED
package/prisma/schema.prisma
CHANGED
|
@@ -1088,6 +1088,7 @@ model QaReport {
|
|
|
1088
1088
|
model QaCase {
|
|
1089
1089
|
id String @id @default(cuid())
|
|
1090
1090
|
reportId String
|
|
1091
|
+
code String? // kode bebas ketikan QA (ADR-0176); null = nomor otomatis TC-nn. Unik per laporan (digerbang service)
|
|
1091
1092
|
title String
|
|
1092
1093
|
steps String @default("")
|
|
1093
1094
|
expected String @default("")
|
|
@@ -1108,6 +1109,7 @@ model QaFinding {
|
|
|
1108
1109
|
id String @id @default(cuid())
|
|
1109
1110
|
reportId String
|
|
1110
1111
|
caseId String?
|
|
1112
|
+
code String? // kode bebas ketikan QA (ADR-0176); null = nomor otomatis F-nn. Unik per laporan (digerbang service)
|
|
1111
1113
|
title String
|
|
1112
1114
|
severity String @default("major") // blocker | critical | major | minor | trivial
|
|
1113
1115
|
priority String @default("P2") // P0..P3 — terpisah dari severity
|