argusqa-os 9.2.0
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/.mcp.json +8 -0
- package/LICENSE +21 -0
- package/README.md +879 -0
- package/package.json +69 -0
- package/src/adapters/browser.js +82 -0
- package/src/argus.js +8 -0
- package/src/batch-runner.js +8 -0
- package/src/cli/init.js +314 -0
- package/src/config/schema.js +108 -0
- package/src/config/targets.js +309 -0
- package/src/domain/finding.js +25 -0
- package/src/mcp-server.js +156 -0
- package/src/orchestration/crawl-and-report.js +16 -0
- package/src/orchestration/dispatcher.js +263 -0
- package/src/orchestration/env-comparison.js +498 -0
- package/src/orchestration/orchestrator.js +1128 -0
- package/src/orchestration/report-processor.js +134 -0
- package/src/orchestration/slack-notifier.js +337 -0
- package/src/orchestration/watch-mode.js +316 -0
- package/src/registry.js +18 -0
- package/src/server/index.js +94 -0
- package/src/server/interaction-handler.js +126 -0
- package/src/server/slash-command-handler.js +185 -0
- package/src/utils/api-frequency.js +128 -0
- package/src/utils/baseline-manager.js +255 -0
- package/src/utils/codebase-analyzer.js +299 -0
- package/src/utils/content-analyzer.js +155 -0
- package/src/utils/contract-validator.js +178 -0
- package/src/utils/css-analyzer.js +407 -0
- package/src/utils/diff.js +189 -0
- package/src/utils/flakiness-detector.js +82 -0
- package/src/utils/flow-runner.js +572 -0
- package/src/utils/github-reporter.js +310 -0
- package/src/utils/hover-analyzer.js +214 -0
- package/src/utils/html-reporter.js +301 -0
- package/src/utils/issues-analyzer.js +171 -0
- package/src/utils/keyboard-analyzer.js +141 -0
- package/src/utils/lighthouse-checker.js +120 -0
- package/src/utils/logger.js +39 -0
- package/src/utils/login-orchestrator.js +99 -0
- package/src/utils/mcp-client.js +264 -0
- package/src/utils/mcp-parsers.js +57 -0
- package/src/utils/memory-analyzer.js +270 -0
- package/src/utils/network-timing-analyzer.js +76 -0
- package/src/utils/parallel-crawler.js +28 -0
- package/src/utils/responsive-analyzer.js +253 -0
- package/src/utils/retry.js +36 -0
- package/src/utils/route-discoverer.js +306 -0
- package/src/utils/security-analyzer.js +302 -0
- package/src/utils/seo-analyzer.js +164 -0
- package/src/utils/session-manager.js +12 -0
- package/src/utils/session-persistence.js +214 -0
- package/src/utils/severity-overrides.js +91 -0
- package/src/utils/slack-guard.js +18 -0
- package/src/utils/slug.js +8 -0
- package/src/utils/snapshot-analyzer.js +330 -0
- package/src/utils/telemetry.js +190 -0
|
@@ -0,0 +1,134 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Argus Report Processor (v9.1.3)
|
|
3
|
+
*
|
|
4
|
+
* Post-crawl pipeline: dedup → severity overrides → summary rebuild →
|
|
5
|
+
* baseline load/apply/save → trend append → JSON write.
|
|
6
|
+
*
|
|
7
|
+
* Extracted from crawl-and-report.js god object.
|
|
8
|
+
*/
|
|
9
|
+
|
|
10
|
+
import fs from 'fs';
|
|
11
|
+
import path from 'path';
|
|
12
|
+
|
|
13
|
+
import { childLogger } from '../utils/logger.js';
|
|
14
|
+
import { applyOverrides } from '../utils/severity-overrides.js';
|
|
15
|
+
import { loadBaseline, saveBaseline, applyBaseline, appendTrend, getCurrentBranch } from '../utils/baseline-manager.js';
|
|
16
|
+
|
|
17
|
+
const logger = childLogger('report-processor');
|
|
18
|
+
|
|
19
|
+
// ── Deduplication ─────────────────────────────────────────────────────────────
|
|
20
|
+
|
|
21
|
+
/**
|
|
22
|
+
* Deduplicate findings: same type + message (first 200 chars) + url = one entry.
|
|
23
|
+
* @param {object[]} findings
|
|
24
|
+
* @returns {object[]}
|
|
25
|
+
*/
|
|
26
|
+
export function deduplicateFindings(findings) {
|
|
27
|
+
const seen = new Set();
|
|
28
|
+
return findings.filter(e => {
|
|
29
|
+
if (!e || typeof e !== 'object') return false;
|
|
30
|
+
const key = `${e.type ?? 'unknown'}::${(e.message ?? '').slice(0, 200)}::${e.url ?? ''}`;
|
|
31
|
+
if (seen.has(key)) return false;
|
|
32
|
+
seen.add(key);
|
|
33
|
+
return true;
|
|
34
|
+
});
|
|
35
|
+
}
|
|
36
|
+
|
|
37
|
+
// ── Summary Rebuild ───────────────────────────────────────────────────────────
|
|
38
|
+
|
|
39
|
+
/**
|
|
40
|
+
* Recount report.summary from all findings in routes, flows, and codebase.
|
|
41
|
+
* Called after applyOverrides() which may suppress or reclassify findings.
|
|
42
|
+
* @param {object} report - Mutable report object
|
|
43
|
+
*/
|
|
44
|
+
export function rebuildSummary(report) {
|
|
45
|
+
report.summary = { total: 0, critical: 0, warning: 0, info: 0 };
|
|
46
|
+
|
|
47
|
+
function countFinding(finding) {
|
|
48
|
+
report.summary.total++;
|
|
49
|
+
if (finding.severity === 'critical' || finding.severity === 'warning' || finding.severity === 'info') {
|
|
50
|
+
report.summary[finding.severity]++;
|
|
51
|
+
} else if (finding.severity) {
|
|
52
|
+
logger.warn(`[ARGUS] Unknown severity "${finding.severity}" on finding type "${finding.type ?? 'unknown'}"`);
|
|
53
|
+
}
|
|
54
|
+
}
|
|
55
|
+
|
|
56
|
+
for (const routeResult of report.routes) {
|
|
57
|
+
for (const err of routeResult.errors) countFinding(err);
|
|
58
|
+
}
|
|
59
|
+
for (const flowResult of (report.flows ?? [])) {
|
|
60
|
+
for (const finding of (flowResult.findings ?? [])) countFinding(finding);
|
|
61
|
+
}
|
|
62
|
+
for (const finding of (report.codebase ?? [])) {
|
|
63
|
+
countFinding(finding);
|
|
64
|
+
}
|
|
65
|
+
}
|
|
66
|
+
|
|
67
|
+
// ── Main Post-Crawl Processor ─────────────────────────────────────────────────
|
|
68
|
+
|
|
69
|
+
/**
|
|
70
|
+
* Apply overrides → rebuild summary → baseline load/apply → write JSON → save baseline + trend.
|
|
71
|
+
*
|
|
72
|
+
* @param {object} report - Mutable report object (modified in place)
|
|
73
|
+
* @param {object} options
|
|
74
|
+
* @param {string} options.outputDir - Directory to write error-report-*.json
|
|
75
|
+
* @param {Array} options.severityOverrides - From targets.js
|
|
76
|
+
* @returns {{ reportPath: string, diff: object }}
|
|
77
|
+
*/
|
|
78
|
+
export async function processReport(report, { outputDir, severityOverrides }) {
|
|
79
|
+
// 1. Apply severity overrides (suppress or reclassify findings)
|
|
80
|
+
const { overriddenCount, suppressedCount } = applyOverrides(report, severityOverrides);
|
|
81
|
+
if (overriddenCount > 0 || suppressedCount > 0) {
|
|
82
|
+
logger.info(`[ARGUS] Severity overrides: ${overriddenCount} remapped, ${suppressedCount} suppressed`);
|
|
83
|
+
}
|
|
84
|
+
|
|
85
|
+
// 2. Rebuild summary after overrides
|
|
86
|
+
rebuildSummary(report);
|
|
87
|
+
|
|
88
|
+
// 3. Load baseline + compute diff
|
|
89
|
+
const branch = getCurrentBranch();
|
|
90
|
+
const safeBranch = branch.replace(/[/\\]/g, '__').replace(/[^a-zA-Z0-9._-]/g, '_');
|
|
91
|
+
const baselinePath = path.join(outputDir, 'baselines', `${safeBranch}.json`);
|
|
92
|
+
const trendsPath = path.join(outputDir, 'baselines', `${safeBranch}-trends.json`);
|
|
93
|
+
logger.info(`[ARGUS] Branch: "${branch}" → baseline: ${baselinePath}`);
|
|
94
|
+
|
|
95
|
+
const baseline = loadBaseline(baselinePath);
|
|
96
|
+
const diff = applyBaseline(report, baseline);
|
|
97
|
+
|
|
98
|
+
if (!diff.isFirstRun) {
|
|
99
|
+
logger.info(`[ARGUS] Baseline diff: ${diff.newCount} new finding(s), ${diff.resolvedCount} resolved`);
|
|
100
|
+
if ((diff.flowNewCount ?? 0) > 0 || (diff.flowResolvedCount ?? 0) > 0) {
|
|
101
|
+
logger.info(`[ARGUS] Flow diff: ${diff.flowNewCount} new flow finding(s), ${diff.flowResolvedCount} resolved`);
|
|
102
|
+
}
|
|
103
|
+
} else {
|
|
104
|
+
logger.info('[ARGUS] First run — no baseline to compare; all findings treated as new');
|
|
105
|
+
}
|
|
106
|
+
|
|
107
|
+
// 4. Write JSON report
|
|
108
|
+
const timestamp = new Date().toISOString().replace(/[:.]/g, '-');
|
|
109
|
+
const reportPath = path.join(outputDir, `error-report-${timestamp}.json`);
|
|
110
|
+
try {
|
|
111
|
+
fs.writeFileSync(reportPath, JSON.stringify(report, null, 2));
|
|
112
|
+
} catch (err) {
|
|
113
|
+
logger.error(`[ARGUS] Failed to write report JSON: ${err.message}`);
|
|
114
|
+
throw err;
|
|
115
|
+
}
|
|
116
|
+
logger.info(`[ARGUS] Report written: ${reportPath}`);
|
|
117
|
+
|
|
118
|
+
// 5. Persist baseline + append trend entry
|
|
119
|
+
saveBaseline(baselinePath, report);
|
|
120
|
+
appendTrend(trendsPath, {
|
|
121
|
+
runAt: report.generatedAt,
|
|
122
|
+
baseUrl: report.baseUrl,
|
|
123
|
+
summary: report.summary,
|
|
124
|
+
newFindings: diff.newCount,
|
|
125
|
+
resolvedFindings: diff.resolvedCount,
|
|
126
|
+
routeCount: report.routes.length,
|
|
127
|
+
flowCount: report.flows?.length ?? 0,
|
|
128
|
+
flowNewFindings: diff.flowNewCount ?? 0,
|
|
129
|
+
flowResolvedFindings: diff.flowResolvedCount ?? 0,
|
|
130
|
+
});
|
|
131
|
+
logger.info(`[ARGUS] Baseline saved → ${baselinePath} (branch: "${branch}")`);
|
|
132
|
+
|
|
133
|
+
return { reportPath, diff };
|
|
134
|
+
}
|
|
@@ -0,0 +1,337 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* ARGUS Phase 4: Slack Notification Dispatcher
|
|
3
|
+
*
|
|
4
|
+
* Posts rich Block Kit bug reports to Slack with:
|
|
5
|
+
* - Severity-based channel routing
|
|
6
|
+
* - Screenshot uploads via files.getUploadURLExternal + files.completeUploadExternal
|
|
7
|
+
* - Interactive action buttons (View Page, Acknowledge, Retest)
|
|
8
|
+
* - Threaded follow-up support
|
|
9
|
+
*
|
|
10
|
+
* Requires environment variables:
|
|
11
|
+
* SLACK_BOT_TOKEN — xoxb-... token
|
|
12
|
+
* SLACK_CHANNEL_CRITICAL — channel ID for critical bugs
|
|
13
|
+
* SLACK_CHANNEL_WARNINGS — channel ID for warnings
|
|
14
|
+
* SLACK_CHANNEL_DIGEST — channel ID for daily digest
|
|
15
|
+
*/
|
|
16
|
+
|
|
17
|
+
import { WebClient } from '@slack/web-api';
|
|
18
|
+
import fs from 'fs';
|
|
19
|
+
import path from 'path';
|
|
20
|
+
import 'dotenv/config';
|
|
21
|
+
import { childLogger } from '../utils/logger.js';
|
|
22
|
+
|
|
23
|
+
const logger = childLogger('slack-notifier');
|
|
24
|
+
|
|
25
|
+
const slack = new WebClient(process.env.SLACK_BOT_TOKEN);
|
|
26
|
+
|
|
27
|
+
const CHANNELS = {
|
|
28
|
+
critical: process.env.SLACK_CHANNEL_CRITICAL,
|
|
29
|
+
warning: process.env.SLACK_CHANNEL_WARNINGS,
|
|
30
|
+
info: process.env.SLACK_CHANNEL_DIGEST,
|
|
31
|
+
};
|
|
32
|
+
|
|
33
|
+
const SEVERITY_EMOJI = {
|
|
34
|
+
critical: '🔴',
|
|
35
|
+
warning: '🟡',
|
|
36
|
+
info: '🔵',
|
|
37
|
+
};
|
|
38
|
+
|
|
39
|
+
// ── Rate-limit-aware postMessage wrapper ─────────────────────────────────────
|
|
40
|
+
|
|
41
|
+
const SLACK_RATE_LIMIT_RETRIES = 3;
|
|
42
|
+
|
|
43
|
+
async function slackPostWithBackoff(args) {
|
|
44
|
+
for (let attempt = 0; attempt < SLACK_RATE_LIMIT_RETRIES; attempt++) {
|
|
45
|
+
try {
|
|
46
|
+
return await slack.chat.postMessage(args);
|
|
47
|
+
} catch (err) {
|
|
48
|
+
const isRateLimit = err.code === 'slack_webapi_rate_limited'
|
|
49
|
+
|| err.message?.toLowerCase().includes('ratelimited');
|
|
50
|
+
if (!isRateLimit || attempt === SLACK_RATE_LIMIT_RETRIES - 1) throw err;
|
|
51
|
+
const retryAfterMs = (err.retryAfter ?? 1) * 1000;
|
|
52
|
+
logger.warn(`[ARGUS] Slack rate limited — retrying in ${retryAfterMs}ms (attempt ${attempt + 1})`);
|
|
53
|
+
await new Promise(r => setTimeout(r, retryAfterMs));
|
|
54
|
+
}
|
|
55
|
+
}
|
|
56
|
+
}
|
|
57
|
+
|
|
58
|
+
// ── File Upload (Current Slack API) ───────────────────────────────────────────
|
|
59
|
+
|
|
60
|
+
/**
|
|
61
|
+
* Upload a file to Slack using the current (non-deprecated) upload API.
|
|
62
|
+
* Steps: getUploadURLExternal → POST binary → completeUploadExternal
|
|
63
|
+
*
|
|
64
|
+
* @param {string} filePath - Absolute path to the file
|
|
65
|
+
* @param {string} channelId - Channel to share the file into
|
|
66
|
+
* @param {string} filename - Display filename in Slack
|
|
67
|
+
* @returns {string|null} Slack file ID if successful, null on failure
|
|
68
|
+
*/
|
|
69
|
+
async function uploadFileToSlack(filePath, channelId, filename) {
|
|
70
|
+
if (!filePath || !fs.existsSync(filePath)) return null;
|
|
71
|
+
|
|
72
|
+
const fileBuffer = fs.readFileSync(filePath);
|
|
73
|
+
const fileSize = fileBuffer.length;
|
|
74
|
+
|
|
75
|
+
// Step 1: Get a pre-signed upload URL from Slack
|
|
76
|
+
let uploadUrl, fileId;
|
|
77
|
+
try {
|
|
78
|
+
const urlResponse = await slack.files.getUploadURLExternal({
|
|
79
|
+
filename,
|
|
80
|
+
length: fileSize,
|
|
81
|
+
});
|
|
82
|
+
uploadUrl = urlResponse.upload_url;
|
|
83
|
+
fileId = urlResponse.file_id;
|
|
84
|
+
} catch (err) {
|
|
85
|
+
logger.error('[ARGUS] Failed to get Slack upload URL:', err.message);
|
|
86
|
+
return null;
|
|
87
|
+
}
|
|
88
|
+
|
|
89
|
+
// Step 2: PUT the binary data to the pre-signed URL
|
|
90
|
+
// Slack requires PUT here — POST silently fails and produces a broken/missing file
|
|
91
|
+
try {
|
|
92
|
+
const response = await fetch(uploadUrl, {
|
|
93
|
+
method: 'PUT',
|
|
94
|
+
headers: { 'Content-Type': 'application/octet-stream' },
|
|
95
|
+
body: fileBuffer,
|
|
96
|
+
signal: AbortSignal.timeout(30000),
|
|
97
|
+
});
|
|
98
|
+
if (!response.ok) {
|
|
99
|
+
logger.error('[ARGUS] Slack upload PUT failed:', response.status, response.statusText);
|
|
100
|
+
return null;
|
|
101
|
+
}
|
|
102
|
+
} catch (err) {
|
|
103
|
+
logger.error('[ARGUS] Slack upload fetch error:', err.message);
|
|
104
|
+
return null;
|
|
105
|
+
}
|
|
106
|
+
|
|
107
|
+
// Step 3: Complete the upload and share to channel
|
|
108
|
+
try {
|
|
109
|
+
await slack.files.completeUploadExternal({
|
|
110
|
+
files: [{ id: fileId, title: filename }],
|
|
111
|
+
channel_id: channelId,
|
|
112
|
+
});
|
|
113
|
+
} catch (err) {
|
|
114
|
+
logger.error('[ARGUS] Failed to complete Slack upload:', err.message);
|
|
115
|
+
return null;
|
|
116
|
+
}
|
|
117
|
+
|
|
118
|
+
return fileId;
|
|
119
|
+
}
|
|
120
|
+
|
|
121
|
+
// ── Block Kit Message Builder ─────────────────────────────────────────────────
|
|
122
|
+
|
|
123
|
+
/**
|
|
124
|
+
* Build a Slack Block Kit message payload for a bug report.
|
|
125
|
+
*
|
|
126
|
+
* @param {object} opts
|
|
127
|
+
* @param {string} opts.severity - 'critical' | 'warning' | 'info'
|
|
128
|
+
* @param {string} opts.title - Short title
|
|
129
|
+
* @param {string} opts.description - Longer description / AI-generated summary
|
|
130
|
+
* @param {string} opts.url - Affected URL
|
|
131
|
+
* @param {string|null} opts.fileId - Slack file ID of uploaded screenshot (or null)
|
|
132
|
+
* @param {object} opts.details - Raw detail object (shown as JSON in fallback)
|
|
133
|
+
* @returns {object[]} Slack blocks array
|
|
134
|
+
*/
|
|
135
|
+
function buildBugReportBlocks({ severity, title, description, url, fileId, details }) {
|
|
136
|
+
const emoji = SEVERITY_EMOJI[severity] ?? '⚪';
|
|
137
|
+
const severityLabel = severity.charAt(0).toUpperCase() + severity.slice(1);
|
|
138
|
+
|
|
139
|
+
const blocks = [
|
|
140
|
+
// Header
|
|
141
|
+
{
|
|
142
|
+
type: 'header',
|
|
143
|
+
text: {
|
|
144
|
+
type: 'plain_text',
|
|
145
|
+
text: `${emoji} [${severityLabel}] ${title}`,
|
|
146
|
+
emoji: true,
|
|
147
|
+
},
|
|
148
|
+
},
|
|
149
|
+
// Description
|
|
150
|
+
{
|
|
151
|
+
type: 'section',
|
|
152
|
+
text: {
|
|
153
|
+
type: 'mrkdwn',
|
|
154
|
+
text: description.length > 3000 ? description.slice(0, 2997) + '...' : description,
|
|
155
|
+
},
|
|
156
|
+
},
|
|
157
|
+
// URL + timestamp
|
|
158
|
+
{
|
|
159
|
+
type: 'context',
|
|
160
|
+
elements: [
|
|
161
|
+
{
|
|
162
|
+
type: 'mrkdwn',
|
|
163
|
+
text: `*URL:* ${url} | *Detected:* <!date^${Math.floor(Date.now() / 1000)}^{date_short_pretty} at {time}|${new Date().toISOString()}>`,
|
|
164
|
+
},
|
|
165
|
+
],
|
|
166
|
+
},
|
|
167
|
+
// Divider
|
|
168
|
+
{ type: 'divider' },
|
|
169
|
+
];
|
|
170
|
+
|
|
171
|
+
// Screenshot block — uses slack_file reference so no external hosting needed
|
|
172
|
+
if (fileId) {
|
|
173
|
+
blocks.push({
|
|
174
|
+
type: 'image',
|
|
175
|
+
slack_file: { id: fileId },
|
|
176
|
+
alt_text: `Screenshot for: ${title}`,
|
|
177
|
+
});
|
|
178
|
+
blocks.push({ type: 'divider' });
|
|
179
|
+
}
|
|
180
|
+
|
|
181
|
+
// Action buttons
|
|
182
|
+
blocks.push({
|
|
183
|
+
type: 'actions',
|
|
184
|
+
elements: [
|
|
185
|
+
{
|
|
186
|
+
type: 'button',
|
|
187
|
+
text: { type: 'plain_text', text: 'View Page', emoji: true },
|
|
188
|
+
url,
|
|
189
|
+
action_id: 'view_page',
|
|
190
|
+
style: severity === 'critical' ? 'danger' : 'primary',
|
|
191
|
+
},
|
|
192
|
+
{
|
|
193
|
+
type: 'button',
|
|
194
|
+
text: { type: 'plain_text', text: 'Acknowledge', emoji: true },
|
|
195
|
+
action_id: 'acknowledge',
|
|
196
|
+
value: JSON.stringify({ title: title.slice(0, 100), url: url.slice(0, 200), severity }),
|
|
197
|
+
},
|
|
198
|
+
{
|
|
199
|
+
type: 'button',
|
|
200
|
+
text: { type: 'plain_text', text: 'Retest', emoji: true },
|
|
201
|
+
action_id: 'retest',
|
|
202
|
+
value: JSON.stringify({ url: url.slice(0, 200), severity }),
|
|
203
|
+
},
|
|
204
|
+
],
|
|
205
|
+
});
|
|
206
|
+
|
|
207
|
+
return blocks;
|
|
208
|
+
}
|
|
209
|
+
|
|
210
|
+
// ── Main Dispatcher ───────────────────────────────────────────────────────────
|
|
211
|
+
|
|
212
|
+
/**
|
|
213
|
+
* Post a bug report to the appropriate Slack channel.
|
|
214
|
+
*
|
|
215
|
+
* @param {object} opts
|
|
216
|
+
* @param {'critical'|'warning'|'info'} opts.severity
|
|
217
|
+
* @param {string} opts.title
|
|
218
|
+
* @param {string} opts.description
|
|
219
|
+
* @param {string} opts.url - Affected URL
|
|
220
|
+
* @param {string|null} opts.screenshotPath - Local path to screenshot file
|
|
221
|
+
* @param {object} opts.details - Additional raw detail data
|
|
222
|
+
* @param {string|null} opts.threadTs - If set, post as thread reply (from follow-up retest)
|
|
223
|
+
* @returns {{ ts: string, channel: string }|null} Message timestamp + channel, or null on failure
|
|
224
|
+
*/
|
|
225
|
+
export async function postBugReport({ severity, title, description, url, screenshotPath, details, threadTs = null }) {
|
|
226
|
+
const channelId = CHANNELS[severity];
|
|
227
|
+
|
|
228
|
+
if (!channelId) {
|
|
229
|
+
logger.warn(`[ARGUS] No Slack channel configured for severity: ${severity}. Set SLACK_CHANNEL_${severity.toUpperCase()} in .env`);
|
|
230
|
+
return null;
|
|
231
|
+
}
|
|
232
|
+
|
|
233
|
+
if (!process.env.SLACK_BOT_TOKEN) {
|
|
234
|
+
logger.warn('[ARGUS] SLACK_BOT_TOKEN not set — skipping Slack notification');
|
|
235
|
+
logger.info(`[ARGUS] Would post: [${severity}] ${title} → ${url}`);
|
|
236
|
+
return null;
|
|
237
|
+
}
|
|
238
|
+
|
|
239
|
+
// Upload screenshot if provided
|
|
240
|
+
const filename = screenshotPath ? path.basename(screenshotPath) : null;
|
|
241
|
+
const fileId = screenshotPath
|
|
242
|
+
? await uploadFileToSlack(screenshotPath, channelId, filename)
|
|
243
|
+
: null;
|
|
244
|
+
|
|
245
|
+
// Build Block Kit blocks
|
|
246
|
+
const blocks = buildBugReportBlocks({ severity, title, description, url, fileId, details });
|
|
247
|
+
|
|
248
|
+
// Post message
|
|
249
|
+
try {
|
|
250
|
+
const result = await slackPostWithBackoff({
|
|
251
|
+
channel: channelId,
|
|
252
|
+
text: `[${severity.toUpperCase()}] ${title} — ${url}`, // fallback text
|
|
253
|
+
blocks,
|
|
254
|
+
thread_ts: threadTs ?? undefined,
|
|
255
|
+
});
|
|
256
|
+
|
|
257
|
+
logger.info(`[ARGUS] Slack message posted: ${result.ts} → channel ${channelId}`);
|
|
258
|
+
return { ts: result.ts, channel: channelId };
|
|
259
|
+
} catch (err) {
|
|
260
|
+
logger.error('[ARGUS] Failed to post Slack message:', err.message);
|
|
261
|
+
return null;
|
|
262
|
+
}
|
|
263
|
+
}
|
|
264
|
+
|
|
265
|
+
/**
|
|
266
|
+
* Post a retest follow-up as a thread reply to the original bug message.
|
|
267
|
+
*
|
|
268
|
+
* @param {string} originalTs - Timestamp of the original bug message
|
|
269
|
+
* @param {string} channelId - Channel of the original message
|
|
270
|
+
* @param {'pass'|'fail'} outcome
|
|
271
|
+
* @param {string} details - Human-readable retest result summary
|
|
272
|
+
*/
|
|
273
|
+
export async function postRetestResult(originalTs, channelId, outcome, details) {
|
|
274
|
+
if (!process.env.SLACK_BOT_TOKEN) return;
|
|
275
|
+
|
|
276
|
+
const emoji = outcome === 'pass' ? '✅' : '❌';
|
|
277
|
+
const text = `${emoji} *Retest ${outcome.toUpperCase()}*\n${details}`;
|
|
278
|
+
|
|
279
|
+
try {
|
|
280
|
+
await slackPostWithBackoff({
|
|
281
|
+
channel: channelId,
|
|
282
|
+
text,
|
|
283
|
+
thread_ts: originalTs,
|
|
284
|
+
});
|
|
285
|
+
} catch (err) {
|
|
286
|
+
logger.error('[ARGUS] Failed to post retest reply:', err.message);
|
|
287
|
+
}
|
|
288
|
+
}
|
|
289
|
+
|
|
290
|
+
/**
|
|
291
|
+
* Update an existing Slack message (e.g., to mark a bug as acknowledged).
|
|
292
|
+
*
|
|
293
|
+
* @param {string} ts - Message timestamp
|
|
294
|
+
* @param {string} channelId - Channel ID
|
|
295
|
+
* @param {string} acknowledgingUser - Display name of acknowledging user
|
|
296
|
+
*/
|
|
297
|
+
export async function acknowledgeMessage(ts, channelId, acknowledgingUser) {
|
|
298
|
+
if (!process.env.SLACK_BOT_TOKEN) return;
|
|
299
|
+
|
|
300
|
+
try {
|
|
301
|
+
// Append an acknowledged context block by updating the message
|
|
302
|
+
const existing = await slack.conversations.history({
|
|
303
|
+
channel: channelId,
|
|
304
|
+
latest: ts,
|
|
305
|
+
inclusive: true,
|
|
306
|
+
limit: 1,
|
|
307
|
+
});
|
|
308
|
+
|
|
309
|
+
const msg = existing.messages?.[0];
|
|
310
|
+
if (!msg) {
|
|
311
|
+
logger.warn('[ARGUS] acknowledgeMessage: original message not found for ts:', ts);
|
|
312
|
+
return;
|
|
313
|
+
}
|
|
314
|
+
|
|
315
|
+
const updatedBlocks = [
|
|
316
|
+
...(msg.blocks ?? []),
|
|
317
|
+
{
|
|
318
|
+
type: 'context',
|
|
319
|
+
elements: [
|
|
320
|
+
{
|
|
321
|
+
type: 'mrkdwn',
|
|
322
|
+
text: `✅ Acknowledged by *${acknowledgingUser}* at <!date^${Math.floor(Date.now() / 1000)}^{time}|now>`,
|
|
323
|
+
},
|
|
324
|
+
],
|
|
325
|
+
},
|
|
326
|
+
];
|
|
327
|
+
|
|
328
|
+
await slack.chat.update({
|
|
329
|
+
channel: channelId,
|
|
330
|
+
ts,
|
|
331
|
+
blocks: updatedBlocks,
|
|
332
|
+
text: msg.text + ' [ACKNOWLEDGED]',
|
|
333
|
+
});
|
|
334
|
+
} catch (err) {
|
|
335
|
+
logger.error('[ARGUS] Failed to acknowledge message:', err.message);
|
|
336
|
+
}
|
|
337
|
+
}
|