@sentinelqa/playwright-reporter 0.1.54 → 0.1.56
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 +142 -130
- package/dist/localReport.js +453 -3
- package/dist/mode.d.ts +2 -0
- package/dist/mode.js +16 -0
- package/dist/quickDiagnosis.js +92 -161
- package/dist/reporter.d.ts +1 -0
- package/dist/reporter.js +56 -14
- package/package.json +2 -2
package/dist/localReport.js
CHANGED
|
@@ -244,6 +244,23 @@ const readAttachmentJson = (attachments, name, baseDirs) => {
|
|
|
244
244
|
return null;
|
|
245
245
|
}
|
|
246
246
|
};
|
|
247
|
+
const stripInternalInstructionsFromErrorContextMarkdown = (value) => {
|
|
248
|
+
// Playwright generates `error-context.md`. Any internal guidance must not appear in user-visible reports.
|
|
249
|
+
const start = value.match(/^# Instructions[ \t]*\r?\n/m);
|
|
250
|
+
if (!start)
|
|
251
|
+
return value;
|
|
252
|
+
const startIndex = start.index ?? -1;
|
|
253
|
+
if (startIndex < 0)
|
|
254
|
+
return value;
|
|
255
|
+
const rest = value.slice(startIndex);
|
|
256
|
+
const nextHeading = rest.match(/^# (?!Instructions\b).*$/m);
|
|
257
|
+
const endIndex = nextHeading && typeof nextHeading.index === "number"
|
|
258
|
+
? startIndex + nextHeading.index
|
|
259
|
+
: value.length;
|
|
260
|
+
const before = value.slice(0, startIndex);
|
|
261
|
+
const after = value.slice(endIndex);
|
|
262
|
+
return `${before.trimEnd()}\n\n${after.trimStart()}`.trim() + "\n";
|
|
263
|
+
};
|
|
247
264
|
const copyArtifact = (sourcePath, kind, reportDir, usedRelativePaths, testId) => {
|
|
248
265
|
const hash = crypto_1.default
|
|
249
266
|
.createHash("sha1")
|
|
@@ -261,7 +278,20 @@ const copyArtifact = (sourcePath, kind, reportDir, usedRelativePaths, testId) =>
|
|
|
261
278
|
usedRelativePaths.add(relativePath);
|
|
262
279
|
const destination = path_1.default.join(reportDir, relativePath);
|
|
263
280
|
ensureDir(path_1.default.dirname(destination));
|
|
264
|
-
|
|
281
|
+
const baseName = path_1.default.basename(sourcePath).toLowerCase();
|
|
282
|
+
if (baseName === "error-context.md") {
|
|
283
|
+
try {
|
|
284
|
+
const raw = fs_1.default.readFileSync(sourcePath, "utf8");
|
|
285
|
+
const cleaned = stripInternalInstructionsFromErrorContextMarkdown(raw);
|
|
286
|
+
fs_1.default.writeFileSync(destination, cleaned, "utf8");
|
|
287
|
+
}
|
|
288
|
+
catch {
|
|
289
|
+
fs_1.default.copyFileSync(sourcePath, destination);
|
|
290
|
+
}
|
|
291
|
+
}
|
|
292
|
+
else {
|
|
293
|
+
fs_1.default.copyFileSync(sourcePath, destination);
|
|
294
|
+
}
|
|
265
295
|
return {
|
|
266
296
|
sourcePath,
|
|
267
297
|
fileName: path_1.default.basename(destination),
|
|
@@ -457,7 +487,10 @@ const buildRunDiff = (tests, snapshot) => {
|
|
|
457
487
|
const currentById = new Map(tests.map((test) => [test.id, test]));
|
|
458
488
|
const currentByMatchKey = new Map(tests.map((test) => [test.matchKey, test]));
|
|
459
489
|
const currentFailures = getFailureTests(tests);
|
|
460
|
-
const
|
|
490
|
+
const previousTests = Array.isArray(previous.tests) ? previous.tests : [];
|
|
491
|
+
if (previousTests.length === 0)
|
|
492
|
+
return null;
|
|
493
|
+
const previousFailures = previousTests.filter((test) => ["failed", "timedOut", "interrupted"].includes(test?.status));
|
|
461
494
|
const previousFailureIds = new Set(previousFailures.map((test) => test.id));
|
|
462
495
|
const previousFailureMatchKeys = new Set(previousFailures.map((test) => (typeof test.matchKey === "string" ? test.matchKey : test.id)));
|
|
463
496
|
return {
|
|
@@ -1091,7 +1124,7 @@ const tryMapRemainingArtifactsToTests = (tests, artifactPaths, reportDir, usedRe
|
|
|
1091
1124
|
}
|
|
1092
1125
|
}
|
|
1093
1126
|
};
|
|
1094
|
-
const
|
|
1127
|
+
const buildLegacyHtml = (tests, summary, extraArtifacts, runDiff, diagnosisSummary, failedRunHistory) => {
|
|
1095
1128
|
const failedTests = tests.filter((test) => ["failed", "timedOut", "interrupted"].includes(test.status));
|
|
1096
1129
|
const similarGroups = groupSimilarFailures(tests);
|
|
1097
1130
|
const generatedAt = new Date().toLocaleString();
|
|
@@ -1766,6 +1799,423 @@ const buildHtml = (tests, summary, extraArtifacts, runDiff, diagnosisSummary, fa
|
|
|
1766
1799
|
</body>
|
|
1767
1800
|
</html>`;
|
|
1768
1801
|
};
|
|
1802
|
+
const buildHtml = (tests, summary, extraArtifacts, _runDiff, diagnosisSummary, _failedRunHistory) => {
|
|
1803
|
+
// New default local report renderer: match the hosted public share report "Root Causes" UI.
|
|
1804
|
+
// Keep legacy renderer in the file as a fallback (and to avoid big diffs).
|
|
1805
|
+
const failedTests = tests.filter((test) => ["failed", "timedOut", "interrupted"].includes(test.status));
|
|
1806
|
+
// Hosted share reports prefer the reporter diagnosis summary (issues[]) for root-cause grouping.
|
|
1807
|
+
// Local reports must match the same canonical evidence and grouping, not re-cluster ad-hoc.
|
|
1808
|
+
const issueGroups = (() => {
|
|
1809
|
+
const issues = (diagnosisSummary?.issues || []).slice(0, 3);
|
|
1810
|
+
if (!issues.length)
|
|
1811
|
+
return [];
|
|
1812
|
+
const failures = getFailureTests(tests);
|
|
1813
|
+
return issues
|
|
1814
|
+
.map((issue) => {
|
|
1815
|
+
const affected = failures.filter((t) => {
|
|
1816
|
+
const matched = findIssueForTest(diagnosisSummary, t);
|
|
1817
|
+
return matched === issue;
|
|
1818
|
+
});
|
|
1819
|
+
return { issue, tests: affected };
|
|
1820
|
+
})
|
|
1821
|
+
.filter((group) => group.tests.length > 0);
|
|
1822
|
+
})();
|
|
1823
|
+
// Fallback: if diagnosis is missing, do a best-effort grouping based on quick per-test signals.
|
|
1824
|
+
const similarGroups = issueGroups.length > 0
|
|
1825
|
+
? []
|
|
1826
|
+
: groupSimilarFailures(tests)
|
|
1827
|
+
.filter((group) => group.tests.some((t) => ["failed", "timedOut", "interrupted"].includes(t.status)))
|
|
1828
|
+
.map((group) => ({
|
|
1829
|
+
...group,
|
|
1830
|
+
tests: group.tests.filter((t) => ["failed", "timedOut", "interrupted"].includes(t.status))
|
|
1831
|
+
}))
|
|
1832
|
+
.filter((group) => group.tests.length > 0);
|
|
1833
|
+
const formatDurationMs = (value) => {
|
|
1834
|
+
if (!Number.isFinite(value))
|
|
1835
|
+
return "-";
|
|
1836
|
+
if (value < 1000)
|
|
1837
|
+
return `${Math.round(value)} ms`;
|
|
1838
|
+
const seconds = Math.round(value / 1000);
|
|
1839
|
+
return `${seconds}s`;
|
|
1840
|
+
};
|
|
1841
|
+
const artifactCountByKind = (test) => {
|
|
1842
|
+
const by = {};
|
|
1843
|
+
for (const a of test.artifacts || [])
|
|
1844
|
+
by[a.kind] = (by[a.kind] || 0) + 1;
|
|
1845
|
+
return by;
|
|
1846
|
+
};
|
|
1847
|
+
const renderLocalArtifacts = (test) => {
|
|
1848
|
+
if (!test.artifacts?.length)
|
|
1849
|
+
return `<div class="muted">No artifacts captured.</div>`;
|
|
1850
|
+
return `<div class="artifact-grid">${test.artifacts.map((a) => renderArtifact(a)).join("\n")}</div>`;
|
|
1851
|
+
};
|
|
1852
|
+
const rootCauseTabs = issueGroups.length
|
|
1853
|
+
? issueGroups
|
|
1854
|
+
.map((group) => {
|
|
1855
|
+
const count = group.tests.length;
|
|
1856
|
+
const label = escapeHtml(group.issue.title || "Root cause");
|
|
1857
|
+
return `<span class="pill pill-muted">${label}<span class="pill-sub">${count} tests</span></span>`;
|
|
1858
|
+
})
|
|
1859
|
+
.join("\n")
|
|
1860
|
+
: similarGroups.length
|
|
1861
|
+
? similarGroups
|
|
1862
|
+
.map((group) => {
|
|
1863
|
+
const count = group.tests.length;
|
|
1864
|
+
const label = escapeHtml(group.signal || "Root cause");
|
|
1865
|
+
return `<span class="pill pill-muted">${label}<span class="pill-sub">${count} tests</span></span>`;
|
|
1866
|
+
})
|
|
1867
|
+
.join("\n")
|
|
1868
|
+
: `<span class="pill pill-muted">No failures</span>`;
|
|
1869
|
+
const renderIssueGroup = (group) => {
|
|
1870
|
+
const issue = group.issue;
|
|
1871
|
+
const title = escapeHtml(issue.title || "Root cause");
|
|
1872
|
+
const summaryText = escapeHtml(issue.cause || "Inspect failing artifacts and trace.");
|
|
1873
|
+
const evidenceFiles = issue.where ? escapeHtml(issue.where) : "";
|
|
1874
|
+
const evidenceBlocker = issue.blocker ? escapeHtml(issue.blocker) : null;
|
|
1875
|
+
const evidenceTargetState = issue.targetState ? escapeHtml(issue.targetState) : null;
|
|
1876
|
+
const evidenceExpected = issue.expected ? escapeHtml(issue.expected) : null;
|
|
1877
|
+
const evidenceReceived = issue.received ? escapeHtml(issue.received) : null;
|
|
1878
|
+
const evidenceFailingStep = issue.failingStep ? escapeHtml(issue.failingStep) : null;
|
|
1879
|
+
const rows = group.tests
|
|
1880
|
+
.map((test) => {
|
|
1881
|
+
const counts = artifactCountByKind(test);
|
|
1882
|
+
const videoCount = counts.video || 0;
|
|
1883
|
+
const screenshotCount = counts.screenshot || 0;
|
|
1884
|
+
const rowId = escapeHtml(test.id);
|
|
1885
|
+
const title = escapeHtml(test.title);
|
|
1886
|
+
const file = escapeHtml(test.file || "-");
|
|
1887
|
+
const duration = escapeHtml(formatDurationMs(test.duration));
|
|
1888
|
+
const desc = escapeHtml(test.errors?.[0] || "");
|
|
1889
|
+
return `
|
|
1890
|
+
<tr class="row" data-row="${rowId}">
|
|
1891
|
+
<td class="cell cell-test">
|
|
1892
|
+
<button class="chev" type="button" aria-label="Toggle details" data-toggle="${rowId}">›</button>
|
|
1893
|
+
<a class="test-link" href="#${rowId}" data-anchor="${rowId}">${title}</a>
|
|
1894
|
+
${desc ? `<div class="muted tiny mt-6">${desc}</div>` : ""}
|
|
1895
|
+
<div class="muted tiny">${escapeHtml(test.projectName || "")}</div>
|
|
1896
|
+
</td>
|
|
1897
|
+
<td class="cell cell-file mono muted">${file}</td>
|
|
1898
|
+
<td class="cell">${duration}</td>
|
|
1899
|
+
<td class="cell">
|
|
1900
|
+
<div class="pill-row">
|
|
1901
|
+
${videoCount > 0 ? `<span class="pill pill-muted">Video ${videoCount}</span>` : ""}
|
|
1902
|
+
${screenshotCount > 0 ? `<span class="pill pill-muted">Screenshot ${screenshotCount}</span>` : ""}
|
|
1903
|
+
</div>
|
|
1904
|
+
</td>
|
|
1905
|
+
<td class="cell">
|
|
1906
|
+
<button class="btn btn-outline" type="button" data-copy="${rowId}">Copy link</button>
|
|
1907
|
+
</td>
|
|
1908
|
+
</tr>
|
|
1909
|
+
<tr class="row-details" data-details="${rowId}" hidden>
|
|
1910
|
+
<td class="cell details" colspan="5">
|
|
1911
|
+
<div class="details-inner">
|
|
1912
|
+
${renderLocalArtifacts(test)}
|
|
1913
|
+
</div>
|
|
1914
|
+
</td>
|
|
1915
|
+
</tr>
|
|
1916
|
+
`;
|
|
1917
|
+
})
|
|
1918
|
+
.join("\n");
|
|
1919
|
+
return `
|
|
1920
|
+
<div class="group">
|
|
1921
|
+
<div class="group-head">
|
|
1922
|
+
<div class="kicker">${group.tests.length === 1 ? "1 test shares this root cause" : `${group.tests.length} tests share this root cause`}</div>
|
|
1923
|
+
<div class="group-title">${title}</div>
|
|
1924
|
+
<div class="group-summary">${summaryText}</div>
|
|
1925
|
+
<div class="group-meta muted tiny">
|
|
1926
|
+
<span>Impact: ${group.tests.length} tests affected</span>
|
|
1927
|
+
<span class="dot">·</span>
|
|
1928
|
+
<span>Inspect first: trace</span>
|
|
1929
|
+
</div>
|
|
1930
|
+
${issue.next
|
|
1931
|
+
? `<div class="group-meta muted tiny" style="margin-top:6px"><span>Likely fix: <span class="fg">${escapeHtml(issue.next)}</span></span></div>`
|
|
1932
|
+
: ""}
|
|
1933
|
+
<div class="evidence">
|
|
1934
|
+
<div class="ev-col">
|
|
1935
|
+
<div class="ev-title">Evidence</div>
|
|
1936
|
+
<div class="ev-lines muted tiny">
|
|
1937
|
+
${evidenceFiles ? `<div>Where: <span class="fg">${evidenceFiles}</span></div>` : ""}
|
|
1938
|
+
${evidenceFailingStep ? `<div>Failing step: <span class="fg">${evidenceFailingStep}</span></div>` : ""}
|
|
1939
|
+
${evidenceBlocker ? `<div>Blocker: <span class="fg">${evidenceBlocker}</span></div>` : ""}
|
|
1940
|
+
${evidenceTargetState ? `<div>Target state: <span class="fg">${evidenceTargetState}</span></div>` : ""}
|
|
1941
|
+
${evidenceExpected ? `<div>Expected: <span class="fg">${evidenceExpected}</span></div>` : ""}
|
|
1942
|
+
${evidenceReceived ? `<div>Received: <span class="fg">${evidenceReceived}</span></div>` : ""}
|
|
1943
|
+
</div>
|
|
1944
|
+
</div>
|
|
1945
|
+
<div class="ev-col">
|
|
1946
|
+
<div class="ev-title">Check first</div>
|
|
1947
|
+
<div class="ev-lines fg">
|
|
1948
|
+
<div>- Open trace at the failing step</div>
|
|
1949
|
+
</div>
|
|
1950
|
+
</div>
|
|
1951
|
+
</div>
|
|
1952
|
+
</div>
|
|
1953
|
+
<div class="table-wrap">
|
|
1954
|
+
<table class="table">
|
|
1955
|
+
<thead>
|
|
1956
|
+
<tr>
|
|
1957
|
+
<th>Test</th>
|
|
1958
|
+
<th>File</th>
|
|
1959
|
+
<th>Duration</th>
|
|
1960
|
+
<th>Artifacts</th>
|
|
1961
|
+
<th>Actions</th>
|
|
1962
|
+
</tr>
|
|
1963
|
+
</thead>
|
|
1964
|
+
<tbody>
|
|
1965
|
+
${rows}
|
|
1966
|
+
</tbody>
|
|
1967
|
+
</table>
|
|
1968
|
+
</div>
|
|
1969
|
+
</div>
|
|
1970
|
+
`;
|
|
1971
|
+
};
|
|
1972
|
+
const groupTitle = (group) => {
|
|
1973
|
+
const headline = group.signal || "Root cause";
|
|
1974
|
+
const count = group.tests.length;
|
|
1975
|
+
return `${headline} (${count} ${count === 1 ? "test" : "tests"})`;
|
|
1976
|
+
};
|
|
1977
|
+
const groupSummary = (group) => {
|
|
1978
|
+
if (group.summary)
|
|
1979
|
+
return group.summary;
|
|
1980
|
+
const top = group.tests[0];
|
|
1981
|
+
const err = top?.errors?.[0];
|
|
1982
|
+
return err ? err : "Inspect failing artifacts and trace.";
|
|
1983
|
+
};
|
|
1984
|
+
const renderGroup = (group) => {
|
|
1985
|
+
const title = escapeHtml(groupTitle(group));
|
|
1986
|
+
const summaryText = escapeHtml(groupSummary(group));
|
|
1987
|
+
const evidenceFiles = group.tests
|
|
1988
|
+
.map((t) => t.file)
|
|
1989
|
+
.filter(Boolean)
|
|
1990
|
+
.slice(0, 3)
|
|
1991
|
+
.join("; ");
|
|
1992
|
+
const evidenceBlocker = group.locator ? escapeHtml(group.locator) : null;
|
|
1993
|
+
const rows = group.tests
|
|
1994
|
+
.map((test) => {
|
|
1995
|
+
const counts = artifactCountByKind(test);
|
|
1996
|
+
const videoCount = counts.video || 0;
|
|
1997
|
+
const screenshotCount = counts.screenshot || 0;
|
|
1998
|
+
const rowId = escapeHtml(test.id);
|
|
1999
|
+
const title = escapeHtml(test.title);
|
|
2000
|
+
const file = escapeHtml(test.file || "-");
|
|
2001
|
+
const duration = escapeHtml(formatDurationMs(test.duration));
|
|
2002
|
+
const desc = escapeHtml(test.errors?.[0] || "");
|
|
2003
|
+
return `
|
|
2004
|
+
<tr class="row" data-row="${rowId}">
|
|
2005
|
+
<td class="cell cell-test">
|
|
2006
|
+
<button class="chev" type="button" aria-label="Toggle details" data-toggle="${rowId}">›</button>
|
|
2007
|
+
<a class="test-link" href="#${rowId}" data-anchor="${rowId}">${title}</a>
|
|
2008
|
+
${desc ? `<div class="muted tiny mt-6">${desc}</div>` : ""}
|
|
2009
|
+
<div class="muted tiny">${escapeHtml(test.projectName || "")}</div>
|
|
2010
|
+
</td>
|
|
2011
|
+
<td class="cell cell-file mono muted">${file}</td>
|
|
2012
|
+
<td class="cell">${duration}</td>
|
|
2013
|
+
<td class="cell">
|
|
2014
|
+
<div class="pill-row">
|
|
2015
|
+
${videoCount > 0 ? `<span class="pill pill-muted">Video ${videoCount}</span>` : ""}
|
|
2016
|
+
${screenshotCount > 0 ? `<span class="pill pill-muted">Screenshot ${screenshotCount}</span>` : ""}
|
|
2017
|
+
</div>
|
|
2018
|
+
</td>
|
|
2019
|
+
<td class="cell">
|
|
2020
|
+
<button class="btn btn-outline" type="button" data-copy="${rowId}">Copy link</button>
|
|
2021
|
+
</td>
|
|
2022
|
+
</tr>
|
|
2023
|
+
<tr class="row-details" data-details="${rowId}" hidden>
|
|
2024
|
+
<td class="cell details" colspan="5">
|
|
2025
|
+
<div class="details-inner">
|
|
2026
|
+
${renderLocalArtifacts(test)}
|
|
2027
|
+
</div>
|
|
2028
|
+
</td>
|
|
2029
|
+
</tr>
|
|
2030
|
+
`;
|
|
2031
|
+
})
|
|
2032
|
+
.join("\n");
|
|
2033
|
+
return `
|
|
2034
|
+
<div class="group">
|
|
2035
|
+
<div class="group-head">
|
|
2036
|
+
<div class="kicker">${group.tests.length === 1 ? "Root cause" : `${group.tests.length} tests share this root cause`}</div>
|
|
2037
|
+
<div class="group-title">${title}</div>
|
|
2038
|
+
<div class="group-summary">${summaryText}</div>
|
|
2039
|
+
<div class="group-meta muted tiny">
|
|
2040
|
+
<span>Impact: ${group.tests.length} tests affected</span>
|
|
2041
|
+
<span class="dot">·</span>
|
|
2042
|
+
<span>Inspect first: trace</span>
|
|
2043
|
+
</div>
|
|
2044
|
+
<div class="evidence">
|
|
2045
|
+
<div class="ev-col">
|
|
2046
|
+
<div class="ev-title">Evidence</div>
|
|
2047
|
+
<div class="ev-lines muted tiny">
|
|
2048
|
+
${evidenceFiles ? `<div>File: <span class="fg">${escapeHtml(evidenceFiles)}</span></div>` : ""}
|
|
2049
|
+
${evidenceBlocker ? `<div>Blocker: <span class="fg">${evidenceBlocker}</span></div>` : ""}
|
|
2050
|
+
</div>
|
|
2051
|
+
</div>
|
|
2052
|
+
<div class="ev-col">
|
|
2053
|
+
<div class="ev-title">Check first</div>
|
|
2054
|
+
<div class="ev-lines fg">
|
|
2055
|
+
<div>- Open trace at the failing step</div>
|
|
2056
|
+
</div>
|
|
2057
|
+
</div>
|
|
2058
|
+
</div>
|
|
2059
|
+
</div>
|
|
2060
|
+
<div class="table-wrap">
|
|
2061
|
+
<table class="table">
|
|
2062
|
+
<thead>
|
|
2063
|
+
<tr>
|
|
2064
|
+
<th>Test</th>
|
|
2065
|
+
<th>File</th>
|
|
2066
|
+
<th>Duration</th>
|
|
2067
|
+
<th>Artifacts</th>
|
|
2068
|
+
<th>Actions</th>
|
|
2069
|
+
</tr>
|
|
2070
|
+
</thead>
|
|
2071
|
+
<tbody>
|
|
2072
|
+
${rows}
|
|
2073
|
+
</tbody>
|
|
2074
|
+
</table>
|
|
2075
|
+
</div>
|
|
2076
|
+
</div>
|
|
2077
|
+
`;
|
|
2078
|
+
};
|
|
2079
|
+
const groupsHtml = issueGroups.length
|
|
2080
|
+
? issueGroups.map((g) => renderIssueGroup(g)).join("\n")
|
|
2081
|
+
: similarGroups.length
|
|
2082
|
+
? similarGroups.map((g) => renderGroup(g)).join("\n")
|
|
2083
|
+
: `<p class="muted">No failed tests.</p>`;
|
|
2084
|
+
const generatedAt = escapeHtml(new Date().toISOString());
|
|
2085
|
+
return `<!doctype html>
|
|
2086
|
+
<html lang="en">
|
|
2087
|
+
<head>
|
|
2088
|
+
<meta charset="utf-8" />
|
|
2089
|
+
<meta name="viewport" content="width=device-width, initial-scale=1" />
|
|
2090
|
+
<title>Sentinel Report (Local)</title>
|
|
2091
|
+
<style>
|
|
2092
|
+
:root{
|
|
2093
|
+
color-scheme: dark;
|
|
2094
|
+
--bg:#08090A;
|
|
2095
|
+
--fg:#F7F8F8;
|
|
2096
|
+
--muted:#8A8F98;
|
|
2097
|
+
--border:rgba(255,255,255,.10);
|
|
2098
|
+
--surface:rgba(255,255,255,.02);
|
|
2099
|
+
--surface-2:rgba(255,255,255,.03);
|
|
2100
|
+
--hover:#1a1a1a;
|
|
2101
|
+
--primary:#5E6AD2;
|
|
2102
|
+
}
|
|
2103
|
+
*{box-sizing:border-box}
|
|
2104
|
+
body{margin:0;background:var(--bg);color:var(--fg);font-family:ui-sans-serif,system-ui,-apple-system,BlinkMacSystemFont,"Segoe UI",sans-serif}
|
|
2105
|
+
a{color:inherit;text-decoration:none}
|
|
2106
|
+
a:hover{text-decoration:underline}
|
|
2107
|
+
.page{max-width:1100px;margin:0 auto;padding:28px 20px 80px}
|
|
2108
|
+
.top{display:flex;align-items:flex-end;justify-content:space-between;gap:16px;margin-bottom:18px}
|
|
2109
|
+
h1{font-size:22px;margin:0;font-weight:650;letter-spacing:-.02em}
|
|
2110
|
+
.sub{color:var(--muted);font-size:13px;margin-top:6px}
|
|
2111
|
+
.pill-row{display:flex;flex-wrap:wrap;gap:8px}
|
|
2112
|
+
.pill{display:inline-flex;align-items:center;gap:6px;border:1px solid var(--border);background:var(--surface);padding:4px 10px;border-radius:999px;font-size:10px;font-weight:650;letter-spacing:.02em;color:var(--muted);white-space:nowrap}
|
|
2113
|
+
.pill-sub{margin-left:8px;color:var(--muted);font-weight:600}
|
|
2114
|
+
.wrap{border-top:1px solid var(--border);padding-top:18px}
|
|
2115
|
+
.card{background:var(--surface);border-radius:16px;padding:18px}
|
|
2116
|
+
.title{font-size:14px;font-weight:650}
|
|
2117
|
+
.groups{margin-top:14px;display:flex;flex-direction:column;gap:18px}
|
|
2118
|
+
.group{border-top:1px solid var(--border);padding-top:18px}
|
|
2119
|
+
.group:first-child{border-top:none;padding-top:0}
|
|
2120
|
+
.group-head{padding:0 0 14px}
|
|
2121
|
+
.kicker{font-size:11px;font-weight:650;letter-spacing:.18em;text-transform:uppercase;color:var(--muted)}
|
|
2122
|
+
.group-title{margin-top:8px;font-size:18px;font-weight:650;color:var(--fg)}
|
|
2123
|
+
.group-summary{margin-top:6px;font-size:13px;color:var(--fg)}
|
|
2124
|
+
.group-meta{margin-top:6px;display:flex;flex-wrap:wrap;gap:10px}
|
|
2125
|
+
.dot{opacity:.6}
|
|
2126
|
+
.evidence{margin-top:14px;display:grid;grid-template-columns:1fr;gap:14px}
|
|
2127
|
+
@media(min-width:900px){.evidence{grid-template-columns:1fr 1fr}}
|
|
2128
|
+
.ev-col{border-left:1px solid var(--border);padding-left:14px}
|
|
2129
|
+
.ev-title{display:flex;align-items:center;gap:8px;font-size:11px;font-weight:650;letter-spacing:.16em;text-transform:uppercase;color:var(--primary)}
|
|
2130
|
+
.ev-lines{margin-top:8px}
|
|
2131
|
+
.fg{color:var(--fg)}
|
|
2132
|
+
.muted{color:var(--muted)}
|
|
2133
|
+
.tiny{font-size:12px}
|
|
2134
|
+
.mt-6{margin-top:6px}
|
|
2135
|
+
.mono{font-family:ui-monospace,SFMono-Regular,Menlo,monospace}
|
|
2136
|
+
.table-wrap{overflow:auto;border-top:1px solid var(--border)}
|
|
2137
|
+
.table{width:100%;border-collapse:collapse}
|
|
2138
|
+
th{padding:12px 14px;text-align:left;font-size:11px;font-weight:650;letter-spacing:.18em;text-transform:uppercase;color:var(--muted)}
|
|
2139
|
+
td{padding:12px 14px;vertical-align:top;font-size:13px;color:var(--fg)}
|
|
2140
|
+
tbody tr.row:hover{background:var(--hover)}
|
|
2141
|
+
tbody tr.row-details td{padding-top:0}
|
|
2142
|
+
tbody{border-top:1px solid rgba(255,255,255,.06)}
|
|
2143
|
+
tbody tr+tr{border-top:1px solid rgba(255,255,255,.06)}
|
|
2144
|
+
.cell-test{min-width:420px}
|
|
2145
|
+
.test-link{font-weight:650}
|
|
2146
|
+
.chev{display:inline-flex;align-items:center;justify-content:center;width:22px;height:22px;border:none;background:transparent;color:var(--muted);cursor:pointer;font-size:18px;line-height:1;margin-right:8px}
|
|
2147
|
+
.btn{display:inline-flex;align-items:center;justify-content:center;border-radius:10px;padding:8px 10px;font-size:12px;font-weight:650;cursor:pointer}
|
|
2148
|
+
.btn-outline{border:1px solid var(--border);background:transparent;color:var(--fg)}
|
|
2149
|
+
.btn-outline:hover{background:var(--surface-2)}
|
|
2150
|
+
.details{padding:0 14px 14px}
|
|
2151
|
+
.details-inner{border-left:1px solid var(--border);padding-left:14px}
|
|
2152
|
+
.artifact-grid{display:grid;gap:12px;grid-template-columns:repeat(auto-fit,minmax(240px,1fr));margin-top:12px}
|
|
2153
|
+
.artifact-card,.artifact-link{border:1px solid rgba(255,255,255,.10);border-radius:12px;background:rgba(255,255,255,.01);padding:12px}
|
|
2154
|
+
.artifact-meta{display:flex;align-items:center;justify-content:space-between;gap:12px}
|
|
2155
|
+
.artifact-kind{display:inline-flex;align-items:center;border:1px solid rgba(255,255,255,.10);background:rgba(255,255,255,.02);border-radius:999px;padding:3px 8px;font-size:10px;font-weight:650;letter-spacing:.08em;text-transform:uppercase;color:var(--muted)}
|
|
2156
|
+
.artifact-card img,.artifact-card video{width:100%;border-radius:10px;margin-top:10px;background:#05070b}
|
|
2157
|
+
</style>
|
|
2158
|
+
</head>
|
|
2159
|
+
<body>
|
|
2160
|
+
<div class="page">
|
|
2161
|
+
<div class="top">
|
|
2162
|
+
<div>
|
|
2163
|
+
<h1>Root Causes</h1>
|
|
2164
|
+
<div class="sub">Generated locally · ${generatedAt}</div>
|
|
2165
|
+
</div>
|
|
2166
|
+
<div class="pill-row">
|
|
2167
|
+
<span class="pill">Failed tests<span class="pill-sub">${failedTests.length}</span></span>
|
|
2168
|
+
</div>
|
|
2169
|
+
</div>
|
|
2170
|
+
|
|
2171
|
+
<div class="wrap">
|
|
2172
|
+
<div class="card">
|
|
2173
|
+
<div class="title">Root Causes</div>
|
|
2174
|
+
<div class="groups">
|
|
2175
|
+
<div class="pill-row">
|
|
2176
|
+
${rootCauseTabs}
|
|
2177
|
+
</div>
|
|
2178
|
+
${groupsHtml}
|
|
2179
|
+
</div>
|
|
2180
|
+
</div>
|
|
2181
|
+
</div>
|
|
2182
|
+
</div>
|
|
2183
|
+
<script>
|
|
2184
|
+
(function() {
|
|
2185
|
+
function baseUrl() {
|
|
2186
|
+
try { return window.location.href.split('#')[0]; } catch { return ''; }
|
|
2187
|
+
}
|
|
2188
|
+
document.querySelectorAll('[data-toggle]').forEach(function(btn) {
|
|
2189
|
+
btn.addEventListener('click', function(ev) {
|
|
2190
|
+
ev.preventDefault();
|
|
2191
|
+
ev.stopPropagation();
|
|
2192
|
+
var id = btn.getAttribute('data-toggle');
|
|
2193
|
+
if (!id) return;
|
|
2194
|
+
var details = document.querySelector('[data-details=\"' + id + '\"]');
|
|
2195
|
+
if (!details) return;
|
|
2196
|
+
var isHidden = details.hasAttribute('hidden');
|
|
2197
|
+
if (isHidden) details.removeAttribute('hidden');
|
|
2198
|
+
else details.setAttribute('hidden', '');
|
|
2199
|
+
btn.textContent = isHidden ? '⌄' : '›';
|
|
2200
|
+
});
|
|
2201
|
+
});
|
|
2202
|
+
document.querySelectorAll('[data-copy]').forEach(function(btn) {
|
|
2203
|
+
btn.addEventListener('click', function(ev) {
|
|
2204
|
+
ev.preventDefault();
|
|
2205
|
+
ev.stopPropagation();
|
|
2206
|
+
var id = btn.getAttribute('data-copy');
|
|
2207
|
+
if (!id) return;
|
|
2208
|
+
var link = baseUrl() + '#' + id;
|
|
2209
|
+
navigator.clipboard.writeText(link).catch(function(){});
|
|
2210
|
+
btn.textContent = 'Copied';
|
|
2211
|
+
setTimeout(function(){ btn.textContent = 'Copy link'; }, 1200);
|
|
2212
|
+
});
|
|
2213
|
+
});
|
|
2214
|
+
})();
|
|
2215
|
+
</script>
|
|
2216
|
+
</body>
|
|
2217
|
+
</html>`;
|
|
2218
|
+
};
|
|
1769
2219
|
function generateLocalDebugReport(options) {
|
|
1770
2220
|
const reportDir = path_1.default.resolve(process.cwd(), options.reportDir || DEFAULT_REPORT_DIR);
|
|
1771
2221
|
const reportFileName = options.reportFileName || DEFAULT_REPORT_FILE;
|
package/dist/mode.d.ts
ADDED
package/dist/mode.js
ADDED
|
@@ -0,0 +1,16 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
3
|
+
exports.readSentinelMode = void 0;
|
|
4
|
+
const readSentinelMode = (env = process.env) => {
|
|
5
|
+
const raw = env.SENTINEL_MODE?.trim();
|
|
6
|
+
if (!raw)
|
|
7
|
+
return "hosted";
|
|
8
|
+
if (raw === "hosted" || raw === "offline")
|
|
9
|
+
return raw;
|
|
10
|
+
throw new Error([
|
|
11
|
+
`Invalid SENTINEL_MODE value: "${raw}".`,
|
|
12
|
+
"Allowed values: hosted, offline.",
|
|
13
|
+
"Tip: remove SENTINEL_MODE to use the default hosted flow."
|
|
14
|
+
].join(" "));
|
|
15
|
+
};
|
|
16
|
+
exports.readSentinelMode = readSentinelMode;
|