@sweny-ai/core 0.1.43 → 0.1.45
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/__tests__/version-consistency.test.js +1 -5
- package/dist/cli/config.js +6 -3
- package/dist/cli/config.test.d.ts +1 -0
- package/dist/cli/config.test.js +111 -0
- package/dist/cli/main.js +22 -1
- package/dist/cli/output.d.ts +10 -0
- package/dist/cli/output.js +120 -0
- package/dist/cli/output.test.d.ts +1 -0
- package/dist/cli/output.test.js +107 -0
- package/package.json +1 -1
|
@@ -189,11 +189,7 @@ describe("docs action code blocks", () => {
|
|
|
189
189
|
"packages/web/src/content/docs/advanced/mcp-servers.md",
|
|
190
190
|
"packages/web/src/content/docs/advanced/troubleshooting.md",
|
|
191
191
|
];
|
|
192
|
-
|
|
193
|
-
// still show triage-specific inputs under swenyai/sweny@v5.
|
|
194
|
-
// This is tracked as a known issue from the multi-repo split.
|
|
195
|
-
// Remove .todo once the docs are migrated to use swenyai/triage@v1.
|
|
196
|
-
it.todo("no docs code block uses swenyai/sweny@v5 with triage-only inputs", () => {
|
|
192
|
+
it("no docs code block uses swenyai/sweny@v5 with triage-only inputs", () => {
|
|
197
193
|
const allIssues = ACTION_DOC_FILES.flatMap((f) => findMisusedBlocks(resolve(ROOT, f)));
|
|
198
194
|
if (allIssues.length > 0) {
|
|
199
195
|
expect.fail(`Found ${allIssues.length} code block(s) mixing swenyai/sweny@${CURRENT_ACTION_VERSION} with triage-only inputs:\n` +
|
package/dist/cli/config.js
CHANGED
|
@@ -11,7 +11,7 @@ export function registerTriageCommand(program) {
|
|
|
11
11
|
.option("--observability-provider <provider>", "Observability provider (default: datadog)")
|
|
12
12
|
.option("--issue-tracker-provider <provider>", "Issue tracker provider (default: github-issues)")
|
|
13
13
|
.option("--source-control-provider <provider>", "Source control provider (default: github)")
|
|
14
|
-
.option("--notification-provider <provider>", "Notification provider (default: console)")
|
|
14
|
+
.option("--notification-provider <provider>", "Notification provider: console, github-summary, slack, teams, discord, webhook, email, file (default: console)")
|
|
15
15
|
.option("--time-range <range>", "Time range to analyze (default: 24h)")
|
|
16
16
|
.option("--severity-focus <focus>", "Severity level focus (default: errors)")
|
|
17
17
|
.option("--service-filter <filter>", "Service filter pattern (default: *)")
|
|
@@ -355,7 +355,10 @@ export function validateInputs(config) {
|
|
|
355
355
|
// Notification credentials by provider
|
|
356
356
|
switch (config.notificationProvider) {
|
|
357
357
|
case "console":
|
|
358
|
-
|
|
358
|
+
case "github-summary":
|
|
359
|
+
// No credentials needed. github-summary writes a markdown summary to the
|
|
360
|
+
// file at $GITHUB_STEP_SUMMARY when running inside GitHub Actions; outside
|
|
361
|
+
// Actions it behaves like console.
|
|
359
362
|
break;
|
|
360
363
|
case "slack":
|
|
361
364
|
case "teams":
|
|
@@ -376,7 +379,7 @@ export function validateInputs(config) {
|
|
|
376
379
|
// No external credentials needed
|
|
377
380
|
break;
|
|
378
381
|
default:
|
|
379
|
-
errors.push(`Unknown --notification-provider "${config.notificationProvider}". Valid values: console, slack, teams, discord, webhook, email, file`);
|
|
382
|
+
errors.push(`Unknown --notification-provider "${config.notificationProvider}". Valid values: console, github-summary, slack, teams, discord, webhook, email, file`);
|
|
380
383
|
}
|
|
381
384
|
// Workspace tools: reject unknown names early
|
|
382
385
|
for (const tool of config.workspaceTools) {
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
export {};
|
|
@@ -0,0 +1,111 @@
|
|
|
1
|
+
import { describe, it, expect } from "vitest";
|
|
2
|
+
import { validateInputs } from "./config.js";
|
|
3
|
+
/**
|
|
4
|
+
* Focused tests for validateInputs() — specifically the notification-provider
|
|
5
|
+
* switch. Regressions here break every `swenyai/triage` action run because
|
|
6
|
+
* that action passes --notification-provider github-summary by default.
|
|
7
|
+
*/
|
|
8
|
+
function baseConfig(overrides = {}) {
|
|
9
|
+
return {
|
|
10
|
+
codingAgentProvider: "claude",
|
|
11
|
+
anthropicApiKey: "sk-ant-test",
|
|
12
|
+
claudeOauthToken: "",
|
|
13
|
+
openaiApiKey: "",
|
|
14
|
+
geminiApiKey: "",
|
|
15
|
+
observabilityProvider: "file",
|
|
16
|
+
observabilityCredentials: {},
|
|
17
|
+
issueTrackerProvider: "file",
|
|
18
|
+
linearApiKey: "",
|
|
19
|
+
linearTeamId: "",
|
|
20
|
+
linearBugLabelId: "",
|
|
21
|
+
linearTriageLabelId: "",
|
|
22
|
+
linearStateBacklog: "",
|
|
23
|
+
linearStateInProgress: "",
|
|
24
|
+
linearStatePeerReview: "",
|
|
25
|
+
timeRange: "24h",
|
|
26
|
+
severityFocus: "errors",
|
|
27
|
+
serviceFilter: "*",
|
|
28
|
+
investigationDepth: "standard",
|
|
29
|
+
maxInvestigateTurns: 50,
|
|
30
|
+
maxImplementTurns: 30,
|
|
31
|
+
baseBranch: "main",
|
|
32
|
+
prLabels: ["agent"],
|
|
33
|
+
issueLabels: [],
|
|
34
|
+
dryRun: false,
|
|
35
|
+
reviewMode: "review",
|
|
36
|
+
noveltyMode: true,
|
|
37
|
+
issueOverride: "",
|
|
38
|
+
additionalInstructions: "",
|
|
39
|
+
serviceMapPath: ".github/service-map.yml",
|
|
40
|
+
githubToken: "ghp_test",
|
|
41
|
+
botToken: "",
|
|
42
|
+
sourceControlProvider: "github",
|
|
43
|
+
jiraBaseUrl: "",
|
|
44
|
+
jiraEmail: "",
|
|
45
|
+
jiraApiToken: "",
|
|
46
|
+
gitlabToken: "",
|
|
47
|
+
gitlabProjectId: "",
|
|
48
|
+
gitlabBaseUrl: "https://gitlab.com",
|
|
49
|
+
notificationProvider: "console",
|
|
50
|
+
notificationWebhookUrl: "",
|
|
51
|
+
sendgridApiKey: "",
|
|
52
|
+
emailFrom: "",
|
|
53
|
+
emailTo: "",
|
|
54
|
+
webhookSigningSecret: "",
|
|
55
|
+
repository: "acme/app",
|
|
56
|
+
repositoryOwner: "",
|
|
57
|
+
json: false,
|
|
58
|
+
stream: false,
|
|
59
|
+
bell: false,
|
|
60
|
+
cacheDir: ".sweny/cache",
|
|
61
|
+
cacheTtl: 86400,
|
|
62
|
+
noCache: false,
|
|
63
|
+
outputDir: ".sweny/output",
|
|
64
|
+
mcpServers: {},
|
|
65
|
+
workspaceTools: [],
|
|
66
|
+
rules: [],
|
|
67
|
+
context: [],
|
|
68
|
+
...overrides,
|
|
69
|
+
};
|
|
70
|
+
}
|
|
71
|
+
describe("validateInputs — notification provider", () => {
|
|
72
|
+
it("accepts console (no credentials required)", () => {
|
|
73
|
+
const errors = validateInputs(baseConfig({ notificationProvider: "console" }));
|
|
74
|
+
expect(errors.filter((e) => e.includes("notification-provider"))).toEqual([]);
|
|
75
|
+
});
|
|
76
|
+
it("accepts github-summary (no credentials required)", () => {
|
|
77
|
+
// Regression guard: the `swenyai/triage` composite action passes this as
|
|
78
|
+
// its default. If the validator stops accepting it every scheduled run
|
|
79
|
+
// across every consumer immediately breaks.
|
|
80
|
+
const errors = validateInputs(baseConfig({ notificationProvider: "github-summary" }));
|
|
81
|
+
expect(errors.filter((e) => e.includes("notification-provider"))).toEqual([]);
|
|
82
|
+
});
|
|
83
|
+
it("accepts file (no credentials required)", () => {
|
|
84
|
+
const errors = validateInputs(baseConfig({ notificationProvider: "file" }));
|
|
85
|
+
expect(errors.filter((e) => e.includes("notification-provider"))).toEqual([]);
|
|
86
|
+
});
|
|
87
|
+
it("requires NOTIFICATION_WEBHOOK_URL for slack", () => {
|
|
88
|
+
const errors = validateInputs(baseConfig({ notificationProvider: "slack", notificationWebhookUrl: "" }));
|
|
89
|
+
expect(errors.some((e) => e.includes("NOTIFICATION_WEBHOOK_URL"))).toBe(true);
|
|
90
|
+
});
|
|
91
|
+
it("accepts slack when NOTIFICATION_WEBHOOK_URL is set", () => {
|
|
92
|
+
const errors = validateInputs(baseConfig({
|
|
93
|
+
notificationProvider: "slack",
|
|
94
|
+
notificationWebhookUrl: "https://hooks.slack.com/services/T/B/X",
|
|
95
|
+
}));
|
|
96
|
+
expect(errors.filter((e) => e.includes("notification"))).toEqual([]);
|
|
97
|
+
});
|
|
98
|
+
it("requires sendgrid credentials for email", () => {
|
|
99
|
+
const errors = validateInputs(baseConfig({ notificationProvider: "email" }));
|
|
100
|
+
expect(errors.some((e) => e.includes("SENDGRID_API_KEY"))).toBe(true);
|
|
101
|
+
expect(errors.some((e) => e.includes("EMAIL_FROM"))).toBe(true);
|
|
102
|
+
expect(errors.some((e) => e.includes("EMAIL_TO"))).toBe(true);
|
|
103
|
+
});
|
|
104
|
+
it("rejects unknown providers and lists github-summary in the error", () => {
|
|
105
|
+
const errors = validateInputs(baseConfig({ notificationProvider: "bogus" }));
|
|
106
|
+
const err = errors.find((e) => e.includes("Unknown --notification-provider"));
|
|
107
|
+
expect(err).toBeDefined();
|
|
108
|
+
expect(err).toContain("github-summary");
|
|
109
|
+
expect(err).toContain("console");
|
|
110
|
+
});
|
|
111
|
+
});
|
package/dist/cli/main.js
CHANGED
|
@@ -25,7 +25,7 @@ import { buildCredentialMap } from "./credentials.js";
|
|
|
25
25
|
import { runInit } from "./init.js";
|
|
26
26
|
import { runE2eInit, runE2eRun } from "./e2e.js";
|
|
27
27
|
import { registerTriageCommand, registerImplementCommand, parseCliInputs, validateInputs, validateWarnings, } from "./config.js";
|
|
28
|
-
import { c, formatBanner, getStepDetails, formatStepLine, formatDagResultHuman, formatResultJson, formatValidationErrors, formatCrashError, formatCheckResults, } from "./output.js";
|
|
28
|
+
import { c, formatBanner, getStepDetails, formatStepLine, formatDagResultHuman, formatDagResultMarkdown, formatResultJson, formatValidationErrors, formatCrashError, formatCheckResults, } from "./output.js";
|
|
29
29
|
import { checkProviderConnectivity } from "./check.js";
|
|
30
30
|
import { registerSetupCommand } from "./setup.js";
|
|
31
31
|
// ── Stream observer (NDJSON) ────────────────────────────────────────
|
|
@@ -350,6 +350,17 @@ triageCmd.action(async (options) => {
|
|
|
350
350
|
else {
|
|
351
351
|
console.log(formatDagResultHuman(results, durationMs, config));
|
|
352
352
|
}
|
|
353
|
+
// GitHub Actions step summary
|
|
354
|
+
if (config.notificationProvider === "github-summary" && process.env.GITHUB_STEP_SUMMARY) {
|
|
355
|
+
try {
|
|
356
|
+
const md = formatDagResultMarkdown(results, durationMs, config);
|
|
357
|
+
fs.appendFileSync(process.env.GITHUB_STEP_SUMMARY, md);
|
|
358
|
+
}
|
|
359
|
+
catch (err) {
|
|
360
|
+
// Don't fail the run if the summary file can't be written
|
|
361
|
+
console.error(c.subtle(` ⚠ could not write GITHUB_STEP_SUMMARY: ${err instanceof Error ? err.message : err}`));
|
|
362
|
+
}
|
|
363
|
+
}
|
|
353
364
|
// Terminal bell
|
|
354
365
|
if (config.bell)
|
|
355
366
|
process.stderr.write("\x07");
|
|
@@ -364,6 +375,16 @@ triageCmd.action(async (options) => {
|
|
|
364
375
|
else {
|
|
365
376
|
console.error(formatCrashError(error));
|
|
366
377
|
}
|
|
378
|
+
// Best-effort GitHub Actions step summary on crash
|
|
379
|
+
if (config.notificationProvider === "github-summary" && process.env.GITHUB_STEP_SUMMARY) {
|
|
380
|
+
try {
|
|
381
|
+
const msg = error instanceof Error ? error.message : "Unknown error";
|
|
382
|
+
fs.appendFileSync(process.env.GITHUB_STEP_SUMMARY, `## ❌ SWEny Triage Crashed\n\n\`\`\`\n${msg}\n\`\`\`\n`);
|
|
383
|
+
}
|
|
384
|
+
catch {
|
|
385
|
+
// ignore
|
|
386
|
+
}
|
|
387
|
+
}
|
|
367
388
|
// Terminal bell even on crash
|
|
368
389
|
if (config.bell)
|
|
369
390
|
process.stderr.write("\x07");
|
package/dist/cli/output.d.ts
CHANGED
|
@@ -24,3 +24,13 @@ export declare function extractCredentialHint(err: unknown): string | null;
|
|
|
24
24
|
export declare function formatCrashError(error: unknown): string;
|
|
25
25
|
export declare function formatCheckResults(results: CheckResult[]): string;
|
|
26
26
|
export declare function formatResultJson(results: Map<string, NodeResult>): string;
|
|
27
|
+
/**
|
|
28
|
+
* Format triage results as GitHub-flavored markdown for `$GITHUB_STEP_SUMMARY`.
|
|
29
|
+
*
|
|
30
|
+
* Designed to render nicely in the GitHub Actions job summary panel:
|
|
31
|
+
* - Status line with an emoji for quick visual scanning
|
|
32
|
+
* - Links to any created issues or PRs
|
|
33
|
+
* - Findings table (for dry runs or no-action results)
|
|
34
|
+
* - Error detail when a node failed
|
|
35
|
+
*/
|
|
36
|
+
export declare function formatDagResultMarkdown(results: Map<string, NodeResult>, durationMs: number, config?: CliConfig): string;
|
package/dist/cli/output.js
CHANGED
|
@@ -404,6 +404,126 @@ export function formatCheckResults(results) {
|
|
|
404
404
|
export function formatResultJson(results) {
|
|
405
405
|
return JSON.stringify(Object.fromEntries(results), null, 2);
|
|
406
406
|
}
|
|
407
|
+
// ── Markdown output (for GitHub Actions step summary) ───────────
|
|
408
|
+
/**
|
|
409
|
+
* Format triage results as GitHub-flavored markdown for `$GITHUB_STEP_SUMMARY`.
|
|
410
|
+
*
|
|
411
|
+
* Designed to render nicely in the GitHub Actions job summary panel:
|
|
412
|
+
* - Status line with an emoji for quick visual scanning
|
|
413
|
+
* - Links to any created issues or PRs
|
|
414
|
+
* - Findings table (for dry runs or no-action results)
|
|
415
|
+
* - Error detail when a node failed
|
|
416
|
+
*/
|
|
417
|
+
export function formatDagResultMarkdown(results, durationMs, config) {
|
|
418
|
+
const duration = formatDuration(durationMs);
|
|
419
|
+
const lines = [];
|
|
420
|
+
// Detect failure anywhere in the DAG
|
|
421
|
+
let failedNodeId;
|
|
422
|
+
let failedResult;
|
|
423
|
+
for (const [nodeId, result] of results) {
|
|
424
|
+
if (result.status === "failed") {
|
|
425
|
+
failedNodeId = nodeId;
|
|
426
|
+
failedResult = result;
|
|
427
|
+
break;
|
|
428
|
+
}
|
|
429
|
+
}
|
|
430
|
+
if (failedResult) {
|
|
431
|
+
lines.push(`## ❌ SWEny Triage Failed`);
|
|
432
|
+
lines.push("");
|
|
433
|
+
lines.push(`**Failed at:** \`${failedNodeId}\` `);
|
|
434
|
+
lines.push(`**Duration:** ${duration}`);
|
|
435
|
+
if (failedResult.data?.error) {
|
|
436
|
+
lines.push("");
|
|
437
|
+
lines.push("```");
|
|
438
|
+
lines.push(String(failedResult.data.error));
|
|
439
|
+
lines.push("```");
|
|
440
|
+
}
|
|
441
|
+
return lines.join("\n") + "\n";
|
|
442
|
+
}
|
|
443
|
+
const investigateData = results.get("investigate")?.data;
|
|
444
|
+
const issueData = results.get("create_issue")?.data;
|
|
445
|
+
const prData = results.get("create_pr")?.data;
|
|
446
|
+
const novelCount = investigateData?.novel_count ?? 0;
|
|
447
|
+
const highestSeverity = investigateData?.highest_severity;
|
|
448
|
+
const findings = investigateData?.findings;
|
|
449
|
+
// Header — pick the most informative title
|
|
450
|
+
if (prData?.prUrl) {
|
|
451
|
+
lines.push(`## ✅ SWEny Triage — PR opened`);
|
|
452
|
+
}
|
|
453
|
+
else if (issueData?.issueIdentifier || issueData?.issueUrl) {
|
|
454
|
+
lines.push(`## ✅ SWEny Triage — Issue created`);
|
|
455
|
+
}
|
|
456
|
+
else if (config?.dryRun) {
|
|
457
|
+
lines.push(`## 🔍 SWEny Triage — Dry run complete`);
|
|
458
|
+
}
|
|
459
|
+
else if (novelCount === 0) {
|
|
460
|
+
lines.push(`## ✅ SWEny Triage — No new incidents`);
|
|
461
|
+
}
|
|
462
|
+
else {
|
|
463
|
+
lines.push(`## ℹ️ SWEny Triage — No action taken`);
|
|
464
|
+
}
|
|
465
|
+
lines.push("");
|
|
466
|
+
lines.push(`**Duration:** ${duration} `);
|
|
467
|
+
if (config?.serviceFilter && config.serviceFilter !== "*") {
|
|
468
|
+
lines.push(`**Service filter:** \`${config.serviceFilter}\` `);
|
|
469
|
+
}
|
|
470
|
+
if (config?.timeRange) {
|
|
471
|
+
lines.push(`**Time range:** ${config.timeRange} `);
|
|
472
|
+
}
|
|
473
|
+
if (highestSeverity) {
|
|
474
|
+
lines.push(`**Highest severity:** ${highestSeverity} `);
|
|
475
|
+
}
|
|
476
|
+
lines.push(`**Novel findings:** ${novelCount}`);
|
|
477
|
+
// Issue and PR links
|
|
478
|
+
if (issueData?.issueIdentifier || issueData?.issueUrl) {
|
|
479
|
+
lines.push("");
|
|
480
|
+
lines.push("### 📋 Issue");
|
|
481
|
+
const id = issueData.issueIdentifier ? `**${String(issueData.issueIdentifier)}** — ` : "";
|
|
482
|
+
const title = issueData.issueTitle ? String(issueData.issueTitle) : "";
|
|
483
|
+
const url = issueData.issueUrl ? String(issueData.issueUrl) : "";
|
|
484
|
+
if (url) {
|
|
485
|
+
lines.push(`${id}[${title || url}](${url})`);
|
|
486
|
+
}
|
|
487
|
+
else {
|
|
488
|
+
lines.push(`${id}${title}`);
|
|
489
|
+
}
|
|
490
|
+
}
|
|
491
|
+
if (prData?.prUrl) {
|
|
492
|
+
lines.push("");
|
|
493
|
+
lines.push("### 🔧 Pull request");
|
|
494
|
+
const num = prData.prNumber ? `#${String(prData.prNumber)}` : "";
|
|
495
|
+
lines.push(`[${num || String(prData.prUrl)}](${String(prData.prUrl)})`);
|
|
496
|
+
}
|
|
497
|
+
// Findings table — mostly useful for dry runs and no-action results
|
|
498
|
+
if (findings && findings.length > 0 && !prData?.prUrl) {
|
|
499
|
+
lines.push("");
|
|
500
|
+
lines.push("### 🔎 Findings");
|
|
501
|
+
lines.push("");
|
|
502
|
+
lines.push("| Severity | Title | Status |");
|
|
503
|
+
lines.push("| --- | --- | --- |");
|
|
504
|
+
for (const f of findings.slice(0, 10)) {
|
|
505
|
+
const sev = String(f.severity ?? "—");
|
|
506
|
+
const title = String(f.title ?? "").replace(/\|/g, "\\|");
|
|
507
|
+
const status = f.is_duplicate ? "duplicate" : "novel";
|
|
508
|
+
lines.push(`| ${sev} | ${title} | ${status} |`);
|
|
509
|
+
}
|
|
510
|
+
if (findings.length > 10) {
|
|
511
|
+
lines.push("");
|
|
512
|
+
lines.push(`_…and ${findings.length - 10} more_`);
|
|
513
|
+
}
|
|
514
|
+
}
|
|
515
|
+
// Recommendation / next step
|
|
516
|
+
const rec = investigateData?.recommendation;
|
|
517
|
+
if (rec) {
|
|
518
|
+
lines.push("");
|
|
519
|
+
lines.push(`**Next:** ${String(rec)}`);
|
|
520
|
+
}
|
|
521
|
+
if (config?.dryRun) {
|
|
522
|
+
lines.push("");
|
|
523
|
+
lines.push("_Dry run mode — no side effects were taken._");
|
|
524
|
+
}
|
|
525
|
+
return lines.join("\n") + "\n";
|
|
526
|
+
}
|
|
407
527
|
// ── Helpers ─────────────────────────────────────────────────────
|
|
408
528
|
function formatDuration(ms) {
|
|
409
529
|
const s = Math.round(ms / 1000);
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
export {};
|
|
@@ -0,0 +1,107 @@
|
|
|
1
|
+
import { describe, it, expect } from "vitest";
|
|
2
|
+
import { formatDagResultMarkdown } from "./output.js";
|
|
3
|
+
function success(data = {}) {
|
|
4
|
+
return { status: "success", data, toolCalls: [] };
|
|
5
|
+
}
|
|
6
|
+
function failed(data = {}) {
|
|
7
|
+
return { status: "failed", data, toolCalls: [] };
|
|
8
|
+
}
|
|
9
|
+
function minimalConfig(overrides = {}) {
|
|
10
|
+
return {
|
|
11
|
+
timeRange: "6h",
|
|
12
|
+
serviceFilter: "permit-service",
|
|
13
|
+
dryRun: false,
|
|
14
|
+
...overrides,
|
|
15
|
+
};
|
|
16
|
+
}
|
|
17
|
+
describe("formatDagResultMarkdown", () => {
|
|
18
|
+
it("renders a success header with issue + PR when both exist", () => {
|
|
19
|
+
const results = new Map([
|
|
20
|
+
[
|
|
21
|
+
"investigate",
|
|
22
|
+
success({ novel_count: 1, highest_severity: "high", findings: [{ severity: "high", title: "500 on /parse" }] }),
|
|
23
|
+
],
|
|
24
|
+
[
|
|
25
|
+
"create_issue",
|
|
26
|
+
success({
|
|
27
|
+
issueIdentifier: "OFF-1234",
|
|
28
|
+
issueTitle: "Parse failure",
|
|
29
|
+
issueUrl: "https://linear.app/acme/issue/OFF-1234",
|
|
30
|
+
}),
|
|
31
|
+
],
|
|
32
|
+
["create_pr", success({ prNumber: 42, prUrl: "https://github.com/acme/app/pull/42" })],
|
|
33
|
+
]);
|
|
34
|
+
const md = formatDagResultMarkdown(results, 15_000, minimalConfig());
|
|
35
|
+
expect(md).toContain("## ✅ SWEny Triage — PR opened");
|
|
36
|
+
expect(md).toContain("**Duration:** 15s");
|
|
37
|
+
expect(md).toContain("**Service filter:** `permit-service`");
|
|
38
|
+
expect(md).toContain("**Highest severity:** high");
|
|
39
|
+
expect(md).toContain("OFF-1234");
|
|
40
|
+
expect(md).toContain("https://linear.app/acme/issue/OFF-1234");
|
|
41
|
+
expect(md).toContain("#42");
|
|
42
|
+
expect(md).toContain("https://github.com/acme/app/pull/42");
|
|
43
|
+
});
|
|
44
|
+
it("renders an issue-only result when no PR was opened", () => {
|
|
45
|
+
const results = new Map([
|
|
46
|
+
["investigate", success({ novel_count: 1, highest_severity: "medium" })],
|
|
47
|
+
[
|
|
48
|
+
"create_issue",
|
|
49
|
+
success({ issueIdentifier: "OFF-5", issueTitle: "Investigate warning", issueUrl: "https://linear.app/x/5" }),
|
|
50
|
+
],
|
|
51
|
+
]);
|
|
52
|
+
const md = formatDagResultMarkdown(results, 5_000, minimalConfig());
|
|
53
|
+
expect(md).toContain("## ✅ SWEny Triage — Issue created");
|
|
54
|
+
expect(md).toContain("OFF-5");
|
|
55
|
+
expect(md).not.toContain("Pull request");
|
|
56
|
+
});
|
|
57
|
+
it("renders a clean no-incident result when novel_count is 0", () => {
|
|
58
|
+
const results = new Map([["investigate", success({ novel_count: 0 })]]);
|
|
59
|
+
const md = formatDagResultMarkdown(results, 3_000, minimalConfig());
|
|
60
|
+
expect(md).toContain("## ✅ SWEny Triage — No new incidents");
|
|
61
|
+
expect(md).toContain("**Novel findings:** 0");
|
|
62
|
+
});
|
|
63
|
+
it("renders a findings table in dry-run mode", () => {
|
|
64
|
+
const results = new Map([
|
|
65
|
+
[
|
|
66
|
+
"investigate",
|
|
67
|
+
success({
|
|
68
|
+
novel_count: 2,
|
|
69
|
+
highest_severity: "high",
|
|
70
|
+
findings: [
|
|
71
|
+
{ severity: "high", title: "OOM in worker", is_duplicate: false },
|
|
72
|
+
{ severity: "low", title: "Stale metric", is_duplicate: true },
|
|
73
|
+
],
|
|
74
|
+
}),
|
|
75
|
+
],
|
|
76
|
+
]);
|
|
77
|
+
const md = formatDagResultMarkdown(results, 9_000, minimalConfig({ dryRun: true }));
|
|
78
|
+
expect(md).toContain("## 🔍 SWEny Triage — Dry run complete");
|
|
79
|
+
expect(md).toContain("| Severity | Title | Status |");
|
|
80
|
+
expect(md).toContain("| high | OOM in worker | novel |");
|
|
81
|
+
expect(md).toContain("| low | Stale metric | duplicate |");
|
|
82
|
+
expect(md).toContain("_Dry run mode — no side effects were taken._");
|
|
83
|
+
});
|
|
84
|
+
it("renders a failure summary when a node failed", () => {
|
|
85
|
+
const results = new Map([
|
|
86
|
+
["gather", success()],
|
|
87
|
+
["investigate", failed({ error: "Rate limited by Anthropic API" })],
|
|
88
|
+
]);
|
|
89
|
+
const md = formatDagResultMarkdown(results, 2_000, minimalConfig());
|
|
90
|
+
expect(md).toContain("## ❌ SWEny Triage Failed");
|
|
91
|
+
expect(md).toContain("**Failed at:** `investigate`");
|
|
92
|
+
expect(md).toContain("Rate limited by Anthropic API");
|
|
93
|
+
});
|
|
94
|
+
it("escapes pipe characters in finding titles so the table stays valid", () => {
|
|
95
|
+
const results = new Map([
|
|
96
|
+
[
|
|
97
|
+
"investigate",
|
|
98
|
+
success({
|
|
99
|
+
novel_count: 1,
|
|
100
|
+
findings: [{ severity: "high", title: "broken | pipe | title" }],
|
|
101
|
+
}),
|
|
102
|
+
],
|
|
103
|
+
]);
|
|
104
|
+
const md = formatDagResultMarkdown(results, 1_000, minimalConfig({ dryRun: true }));
|
|
105
|
+
expect(md).toContain("broken \\| pipe \\| title");
|
|
106
|
+
});
|
|
107
|
+
});
|