argusqa-os 9.4.5 → 9.5.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/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "argusqa-os",
3
- "version": "9.4.5",
3
+ "version": "9.5.0",
4
4
  "mcpName": "io.github.ironclawdevs27/argus",
5
5
  "description": "Argus — AI-powered automated dev-testing platform using Chrome DevTools MCP and Claude Code",
6
6
  "keywords": [
package/src/mcp-server.js CHANGED
@@ -1,6 +1,6 @@
1
1
  #!/usr/bin/env node
2
2
  /**
3
- * Argus MCP Server (v9.4.5)
3
+ * Argus MCP Server (v9.4.6)
4
4
  *
5
5
  * Exposes Argus as an MCP server so Claude (or any MCP client) can call
6
6
  * argus_audit, argus_audit_full, argus_compare, argus_last_report, and
@@ -125,6 +125,9 @@ async function withMcp(fn) {
125
125
  const mcp = await createMcpClient();
126
126
  try {
127
127
  return await fn(mcp);
128
+ } catch (err) {
129
+ logger.error('[ARGUS] MCP tool handler error:', err.message);
130
+ throw err;
128
131
  } finally {
129
132
  try { mcp.close(); } catch { /* ignore — process already gone */ }
130
133
  }
@@ -283,7 +286,7 @@ async function handleLastReport() {
283
286
  // ── Server bootstrap ──────────────────────────────────────────────────────────
284
287
 
285
288
  const server = new Server(
286
- { name: 'argus', version: '9.4.5' },
289
+ { name: 'argus', version: '9.4.6' },
287
290
  { capabilities: { tools: {} } },
288
291
  );
289
292
 
@@ -280,7 +280,7 @@ function classifyConsoleMessage(msg, routeIsCritical) {
280
280
  function classifyNetworkRequest(req, routeIsCritical) {
281
281
  const status = req.status ?? 0;
282
282
  if (status >= 500) return 'critical';
283
- if (status === 401 || status === 403) return 'critical';
283
+ if (status === 401 || status === 403) return routeIsCritical ? 'critical' : 'warning';
284
284
  if (status >= 400) return routeIsCritical ? 'warning' : 'info';
285
285
  return null;
286
286
  }
@@ -806,16 +806,22 @@ export async function crawlRouteExpensive(route, baseUrl, mcp) {
806
806
  const linksRaw = await browser.evaluate(INTERNAL_LINKS_SCRIPT);
807
807
  const rawLinks = unwrapEval(linksRaw);
808
808
  const links = [...new Set(Array.isArray(rawLinks) ? rawLinks.filter(Boolean) : [])];
809
- const headResults = await Promise.all(
810
- links.map(async href => {
811
- try {
812
- const res = await fetch(href, { method: 'HEAD', signal: AbortSignal.timeout(5000) });
813
- return { href, status: res.status };
814
- } catch (err) {
815
- return { href, status: 0, error: err.message };
816
- }
817
- })
818
- );
809
+ const BROKEN_LINK_BATCH_TIMEOUT_MS = 15000;
810
+ const headResults = await Promise.race([
811
+ Promise.all(
812
+ links.map(async href => {
813
+ try {
814
+ const res = await fetch(href, { method: 'HEAD', signal: AbortSignal.timeout(5000) });
815
+ return { href, status: res.status };
816
+ } catch (err) {
817
+ return { href, status: 0, error: err.message };
818
+ }
819
+ })
820
+ ),
821
+ new Promise((_, reject) =>
822
+ setTimeout(() => reject(new Error('broken-link batch timeout')), BROKEN_LINK_BATCH_TIMEOUT_MS)
823
+ ),
824
+ ]);
819
825
  for (const { href, status } of headResults) {
820
826
  if (status === 404) {
821
827
  errors.push({
@@ -22,7 +22,13 @@ import { childLogger } from '../utils/logger.js';
22
22
 
23
23
  const logger = childLogger('slack-notifier');
24
24
 
25
- const slack = new WebClient(process.env.SLACK_BOT_TOKEN);
25
+ // Lazy-init avoid constructing WebClient at module load so importing slack-notifier
26
+ // when SLACK_BOT_TOKEN is absent (HTML-report mode) doesn't create a useless client.
27
+ let _slack = null;
28
+ function getSlack() {
29
+ if (!_slack) _slack = new WebClient(process.env.SLACK_BOT_TOKEN);
30
+ return _slack;
31
+ }
26
32
 
27
33
  const CHANNELS = {
28
34
  critical: process.env.SLACK_CHANNEL_CRITICAL,
@@ -43,7 +49,7 @@ const SLACK_RATE_LIMIT_RETRIES = 3;
43
49
  async function slackPostWithBackoff(args) {
44
50
  for (let attempt = 0; attempt < SLACK_RATE_LIMIT_RETRIES; attempt++) {
45
51
  try {
46
- return await slack.chat.postMessage(args);
52
+ return await getSlack().chat.postMessage(args);
47
53
  } catch (err) {
48
54
  const isRateLimit = err.code === 'slack_webapi_rate_limited'
49
55
  || err.message?.toLowerCase().includes('ratelimited');
@@ -75,7 +81,7 @@ async function uploadFileToSlack(filePath, channelId, filename) {
75
81
  // Step 1: Get a pre-signed upload URL from Slack
76
82
  let uploadUrl, fileId;
77
83
  try {
78
- const urlResponse = await slack.files.getUploadURLExternal({
84
+ const urlResponse = await getSlack().files.getUploadURLExternal({
79
85
  filename,
80
86
  length: fileSize,
81
87
  });
@@ -106,7 +112,7 @@ async function uploadFileToSlack(filePath, channelId, filename) {
106
112
 
107
113
  // Step 3: Complete the upload and share to channel
108
114
  try {
109
- await slack.files.completeUploadExternal({
115
+ await getSlack().files.completeUploadExternal({
110
116
  files: [{ id: fileId, title: filename }],
111
117
  channel_id: channelId,
112
118
  });
@@ -299,7 +305,7 @@ export async function acknowledgeMessage(ts, channelId, acknowledgingUser) {
299
305
 
300
306
  try {
301
307
  // Append an acknowledged context block by updating the message
302
- const existing = await slack.conversations.history({
308
+ const existing = await getSlack().conversations.history({
303
309
  channel: channelId,
304
310
  latest: ts,
305
311
  inclusive: true,
@@ -325,7 +331,7 @@ export async function acknowledgeMessage(ts, channelId, acknowledgingUser) {
325
331
  },
326
332
  ];
327
333
 
328
- await slack.chat.update({
334
+ await getSlack().chat.update({
329
335
  channel: channelId,
330
336
  ts,
331
337
  blocks: updatedBlocks,
@@ -108,10 +108,11 @@ export function matchesContract(reqUrl, reqMethod, contract) {
108
108
  function loadSchema(contract) {
109
109
  if (contract.schema) return contract.schema;
110
110
  if (contract.schemaFile) {
111
- // Prevent path traversal — schemaFile must stay within the project directory
111
+ // Prevent path traversal — schemaFile must resolve inside the project directory.
112
+ // path.relative() + '..' check is robust across case differences and path separator variants.
112
113
  const resolved = path.resolve(contract.schemaFile);
113
- const cwd = process.cwd();
114
- if (!resolved.startsWith(cwd + path.sep) && resolved !== cwd) {
114
+ const rel = path.relative(process.cwd(), resolved);
115
+ if (rel.startsWith('..') || path.isAbsolute(rel)) {
115
116
  logger.warn('[ARGUS] contract-validator: schemaFile outside project directory — skipping:', contract.schemaFile);
116
117
  return null;
117
118
  }
@@ -119,6 +119,13 @@ export async function createMcpClient() {
119
119
  } else {
120
120
  resolve(msg.result);
121
121
  }
122
+ } else if (msg.id !== undefined && !pending.has(msg.id)) {
123
+ // Response arrived after timeout already fired — log for observability
124
+ logger.debug(`[ARGUS] MCP late response for id=${msg.id} (already timed out or unknown)`);
125
+ } else if (msg.method) {
126
+ // Server-initiated notification (e.g. progress) — not an error, no action needed
127
+ } else {
128
+ logger.debug('[ARGUS] MCP unexpected message shape:', JSON.stringify(msg).slice(0, 200));
122
129
  }
123
130
  } catch {
124
131
  // non-JSON line from process — ignore