• Home
  • Features
  • Pricing
  • Docs
  • Announcements
  • Sign In

haraka / haraka-plugin-watch / 26803064356

02 Jun 2026 06:39AM UTC coverage: 91.147% (+32.7%) from 58.497%
26803064356

push

github

web-flow
release 2.1.0 (#75)

- security(client): escape payload fields before building HTML
  - use `.text()` for cell values
- security(client): resolve cell selectors via `.find()`
- fix(client): derive WebSocket scheme from page protocol (ws/wss)
- feat(client): Empty Table fades rows bottom-up, sparing live connections
- refactor(client): move all behavior into client.js
- fix(client): initialize the hint popover on DOM ready
- chore(html): remove the unused login dialog and stale reconnect hint
- refactor: drive result rendering from a plugin registry
- refactor: flatten get_class into small early-return helpers (cuts qlty smells)
- feat: color the asn cell by karma's asn_score (light green/red)
- fix: color access by ACL hit; soft fails (e.g. invalid domain) are yellow
- fix: color spamassassin by `score` (was `hits`)
- fix(p0f): show the OS color on the cell
- fix: keep the port lit on deny; only disconnect clears it
- fix: scale spamassassin/rspamd color by score
- doc: add Security section noting watch has no access control
- test: cover hooks, result handler and format branches (62% -> 97%)
- test: refactored against test-fixtures 1.7.0
- style: replace `wss.clients.forEach` with for..of in `hook_init_wss`

261 of 294 branches covered (88.78%)

Branch coverage included in aggregate %.

591 of 609 new or added lines in 3 files covered. (97.04%)

1 existing line in 1 file now uncovered.

748 of 813 relevant lines covered (92.0%)

7.83 hits per line

Source File
Press 'n' to go to next uncovered line, 'b' for previous

96.18
/plugins.js
1
'use strict'
2✔
2

2✔
3
// Per-plugin display registry. Each entry describes how a published result is
2✔
4
// rendered as a dashboard cell:
2✔
5
//   drop   - result keys whose presence means "noise, don't redraw"
2✔
6
//   quiet  - predicate for conditional noise suppression
2✔
7
//   format - (result) => cell | undefined; undefined falls back to the
2✔
8
//            generic get_title/get_class classifier in format_result()
2✔
9
//
2✔
10
// Connection-phase results (local/remote/helo/reset/disconnect/queue) are not
2✔
11
// here; they update connection-level cells and are handled in index.js.
2✔
12

2✔
13
// Color ramp, hammy (dark green) to spammy (dark red). Score-based plugins use
2✔
14
// it so intensity tracks the score instead of collapsing into a few buckets.
2✔
15
function ramp_class(value, bands) {
15✔
16
  for (const [below, cls] of bands) {
15✔
17
    if (value < below) return cls
57✔
18
  }
57!
19
}
15✔
20

2✔
21
const spamassassin_ramp = [
2✔
22
  [-5, 'bg_dgreen'],
2✔
23
  [0, 'bg_green'],
2✔
24
  [2, 'bg_lgreen'],
2✔
25
  [5, 'bg_yellow'],
2✔
26
  [10, 'bg_lred'],
2✔
27
  [20, 'bg_red'],
2✔
28
  [Infinity, 'bg_dred'],
2✔
29
]
2✔
30

2✔
31
const rspamd_ramp = [
2✔
32
  [-5, 'bg_dgreen'],
2✔
33
  [0, 'bg_green'],
2✔
34
  [3, 'bg_lgreen'],
2✔
35
  [6, 'bg_yellow'],
2✔
36
  [10, 'bg_lred'],
2✔
37
  [15, 'bg_red'],
2✔
38
  [Infinity, 'bg_dred'],
2✔
39
]
2✔
40

2✔
41
function shorten(value) {
7✔
42
  if (typeof value !== 'string' || value.length <= 22) return value
7✔
43
  return `..${value.slice(-22)}`
1✔
44
}
7✔
45

2✔
46
exports.get_plugin_name = function (pi_name) {
2✔
47
  // coalesce auth/* and queue/* plugins to 'auth' and 'queue'
108✔
48
  if (/^(queue|auth)\//.test(pi_name)) return pi_name.split('/').shift()
108✔
49

106✔
50
  switch (pi_name) {
106✔
51
    case 'connect.fcrdns':
108✔
52
      return 'fcrdns'
1✔
53
    case 'connect.asn':
108!
NEW
54
      return 'asn'
×
55
    case 'connect.geoip':
108✔
56
      return 'geoip'
1✔
57
    case 'connect.p0f':
108✔
58
      return 'p0f'
1✔
59
    case 'data.uribl':
108!
NEW
60
      return 'uribl'
×
61
    case 'dkim_verify':
108✔
62
    case 'dkim_sign':
108✔
63
      return 'dkim'
3✔
64
    case 'dmarc-perl':
108✔
65
    case 'data.dmarc':
108✔
66
      return 'dmarc'
2✔
67
    case 'data.headers':
108✔
68
      return 'headers'
1✔
69
    case 'outbound':
108✔
70
      return 'queue'
1✔
71
  }
108✔
72

96✔
73
  return pi_name
96✔
74
}
108✔
75

2✔
76
exports.format_recipient = function (r = {}) {
2✔
77
  const address = typeof r.address === 'string' ? r.address : ''
6✔
78
  const action = typeof r.action === 'string' ? r.action.toLowerCase() : ''
6✔
79
  const action_class = {
6✔
80
    reject: 'bg_red',
6✔
81
    accept: 'bg_green',
6✔
82
  }
6✔
83

6✔
84
  return {
6✔
85
    newval: shorten(address),
6✔
86
    classy: action_class[action] || 'black',
6✔
87
    title: address,
6✔
88
  }
6✔
89
}
6✔
90

2✔
91
exports.format_default = function (r) {
2✔
92
  if (r.pass) return { classy: 'bg_green', title: r.pass }
6✔
93
  if (r.fail) return { classy: 'bg_red', title: r.fail }
6✔
94
  if (r.err) return { classy: 'bg_yellow', title: r.err }
1✔
95
}
6✔
96

2✔
97
exports.format_fcrdns = function (r) {
2✔
98
  if (r.pass) return { classy: 'bg_green' }
5✔
99
  if (r.fail) return { title: r.fail, classy: 'bg_lred' }
5✔
100
  if (r.fcrdns) {
5✔
101
    if (typeof r.fcrdns === 'string') return { title: r.fcrdns }
2✔
102
    if (Array.isArray(r.fcrdns) && r.fcrdns.length) {
2✔
103
      return { title: r.fcrdns.join(' ') }
1✔
104
    }
1✔
105
  }
2✔
106
  return {}
1✔
107
}
5✔
108

2✔
109
exports.format_asn = function (r) {
2✔
110
  if (r.pass) return { classy: 'bg_green' }
3✔
111
  if (r.fail) return { title: r.fail, classy: 'bg_lred' }
3!
112
  if (r.asn) return { newval: r.asn, title: r.net }
3✔
113
  return {}
1✔
114
}
3✔
115

2✔
116
exports.format_p0f = function (r) {
2✔
117
  if (!r || !r.os_name) return {}
3✔
118
  const f = {
2✔
119
    title: `${r.os_name} ${r.os_flavor}, ${r.distance} hops`,
2✔
120
    newval: r.os_name,
2✔
121
  }
2✔
122
  if (/freebsd|mac|ios/i.test(r.os_name)) f.classy = 'bg_green'
3✔
123
  if (/windows/i.test(r.os_name)) f.classy = 'bg_red'
3✔
124
  return f
2✔
125
}
3✔
126

2✔
127
exports.format_bounce = function (r) {
2✔
128
  if (!r) return {}
3!
129
  if (r.isa === 'no') return { classy: 'bg_lgreen', title: 'not a bounce' }
3✔
130
  if (r.fail && r.fail.length) return { classy: 'bg_red', title: r.human }
3✔
131
  return { classy: 'bg_green' }
1✔
132
}
3✔
133

2✔
134
exports.format_helo = function (uuid, r) {
2✔
135
  const host = r.host || ''
4!
136
  return {
4✔
137
    uuid,
4✔
138
    helo: {
4✔
139
      newval: host.length > 22 ? `...${host.slice(-22)}` : host,
4✔
140
      title: r.host,
4✔
141
      classy: 'bg_white',
4✔
142
    },
4✔
143
  }
4✔
144
}
4✔
145

2✔
146
function pass_fail_class(r, none) {
6✔
147
  if (r.pass.length && r.fail.length === 0) return 'bg_green'
6✔
148
  if (r.pass.length) return 'bg_lgreen'
6✔
149
  if (r.fail.length) return 'bg_red'
6✔
150
  if (r.err.length) return 'bg_yellow'
6✔
151
  return none
1✔
152
}
6✔
153

2✔
154
function host_list_class(r) {
1✔
155
  if (r.pass.length && r.fail.length === 0) return 'bg_green'
1!
NEW
156
  if (r.pass.length) return 'bg_lgreen'
×
NEW
157
  return ''
×
158
}
1✔
159

2✔
160
function karma_class(r) {
9✔
161
  if (r.score === undefined) {
9✔
162
    const history = parseFloat(r.history) || 0
4✔
163
    if (history > 2) return 'bg_green'
4✔
164
    if (history < -1) return 'bg_red'
4✔
165
    return 'bg_yellow'
1✔
166
  }
1✔
167
  const score = parseFloat(r.score) || 0
9✔
168
  if (score > 3) return 'bg_green'
9✔
169
  if (score > 0) return 'bg_lgreen'
9✔
170
  if (score < -3) return 'bg_red'
9✔
171
  if (score < 0) return 'bg_lred'
9✔
172
  return 'bg_yellow'
1✔
173
}
9✔
174

2✔
175
function dmarc_class(r) {
4✔
176
  if (!r.result) return 'got'
4✔
177
  const comment = r.reason && r.reason.length ? r.reason[0].comment : ''
4✔
178
  if (r.result === 'pass') return 'bg_green'
4✔
179
  if (comment === 'no policy') return 'bg_yellow'
4✔
180
  return 'bg_red'
1✔
181
}
4✔
182

2✔
183
function spf_class(r) {
6✔
184
  if (r.result === 'Pass') return 'bg_green'
6✔
185
  if (r.result === 'Neutral') return 'bg_lgreen'
6✔
186
  if (/fail/i.test(r.result)) return 'bg_red'
6✔
187
  if (/error/i.test(r.result)) return 'bg_yellow'
6✔
188
  return ''
1✔
189
}
6✔
190

2✔
191
exports.get_class = function (pi_name, r) {
2✔
192
  if (!r.pass) r.pass = []
26✔
193
  if (!r.fail) r.fail = []
26✔
194
  if (!r.err) r.err = []
26✔
195

26✔
196
  switch (pi_name) {
26✔
197
    case 'dmarc':
26✔
198
    case 'dmarc-perl':
26✔
199
    case 'data.dmarc':
26✔
200
      return dmarc_class(r)
4✔
201
    case 'karma':
26✔
202
      return karma_class(r)
9✔
203
    case 'relay':
26✔
204
      return pass_fail_class(r, '')
3✔
205
    case 'rcpt_to.in_host_list':
26✔
206
      return host_list_class(r)
1✔
207
    case 'spf':
26✔
208
      return spf_class(r)
6✔
209
    default:
26✔
210
      return pass_fail_class(r, 'bg_lgrey')
3✔
211
  }
26✔
212
}
26✔
213

2✔
214
exports.get_title = function (pi_name, r) {
2✔
215
  // title: the value shown in the HTML tooltip
6✔
216
  switch (pi_name) {
6✔
217
    case 'dmarc':
6✔
218
    case 'dmarc-perl':
6✔
219
    case 'data.dmarc': {
6✔
220
      const comment = r.reason && r.reason.length ? r.reason[0].comment : ''
2✔
221
      return r.result === 'pass'
2✔
222
        ? r.result
2✔
223
        : [r.result, r.disposition, comment].join(', ')
2✔
224
    }
2✔
225
    case 'queue':
6✔
226
      return r.human
1✔
227
    default:
6✔
228
      return r.human_html
3✔
229
  }
6✔
230
}
6✔
231

2✔
232
exports.format_results = function (pi_name, r) {
2✔
233
  const s = {
1✔
234
    title: exports.get_title(pi_name, r),
1✔
235
    classy: exports.get_class(pi_name, r),
1✔
236
  }
1✔
237

1✔
238
  if (pi_name === 'spf') {
1✔
239
    s.scope = r.scope
1✔
240
  }
1✔
241
  return s
1✔
242
}
1✔
243

2✔
244
exports.format_remote_host = function (uuid, r) {
2✔
245
  const host = r.host || ''
4!
246
  const ip = r.ip || ''
4!
247
  const hostShort = normalize_host(host)
4✔
248

4✔
249
  return {
4✔
250
    uuid,
4✔
251
    remote_host: {
4✔
252
      newval: hostShort ? `${hostShort} / ${ip}` : ip,
4✔
253
      title: hostShort ? `${hostShort} / ${ip}` : ip,
4✔
254
    },
4✔
255
  }
4✔
256
}
4✔
257

2✔
258
function normalize_host(host) {
6✔
259
  if (host === 'DNSERROR' || host === 'Unknown') return ''
6✔
260
  if (host.length > 22) return `...${host.substring(host.length - 20)}`
6✔
261
  return host
3✔
262
}
6✔
263

2✔
264
exports.get_remote_host = function (connection) {
2✔
265
  let host = ''
2✔
266
  let ip = ''
2✔
267
  if (connection.remote) {
2✔
268
    if (connection.remote.host) host = connection.remote.host
2✔
269
    if (connection.remote.ip) ip = connection.remote.ip
2✔
270
  }
2✔
271

2✔
272
  const hostShort = normalize_host(host)
2✔
273

2✔
274
  return {
2✔
275
    newval: hostShort ? `${hostShort} / ${ip}` : ip,
2!
276
    title: hostShort ? `${hostShort} / ${ip}` : ip,
2!
277
  }
2✔
278
}
2✔
279

2✔
280
function pass_fail(r) {
2✔
281
  if (r.pass || r.fail) return exports.format_default(r)
2✔
282
  if (r.skip) return {}
1✔
283
}
2✔
284

2✔
285
function format_access(r) {
5✔
286
  // an ACL hit drives the color: whitelist allows (green), blacklist blocks
5✔
287
  // (red). a bare fail with no ACL hit (e.g. invalid HELO domain) is a soft
5✔
288
  // warning, not a block. blacklist denials also get dark red from w_deny.
5✔
289
  if (r.whitelist) return { classy: 'bg_green', title: r.pass }
5✔
290
  if (r.blacklist) return { classy: 'bg_red', title: r.fail }
5✔
291
  if (r.fail) return { classy: 'bg_yellow', title: r.fail }
5✔
292
  if (r.pass) return { classy: 'bg_green', title: r.pass }
1!
NEW
293
  if (r.skip) return {}
×
294
}
5✔
295

2✔
296
function format_tls(r) {
1✔
297
  if (!r.enabled) return
1!
298
  return {
1✔
299
    classy: r.verified ? 'bg_green' : 'bg_lgreen',
1!
300
    title: JSON.stringify(r),
1✔
301
  }
1✔
302
}
1✔
303

2✔
304
function format_list(r) {
1✔
305
  if (r.fail) return { title: r.fail, classy: 'bg_lred' }
1✔
306
}
1✔
307

2✔
308
function format_geoip(r) {
3✔
309
  const f = {}
3✔
310
  if (r.human) {
3✔
311
    f.title = r.human
3✔
312
    f.newval = r.human.substring(0, 6)
3✔
313
  }
3✔
314
  if (r.distance) {
3✔
315
    f.classy = parseInt(r.distance, 10) > 4000 ? 'bg_red' : 'bg_green'
2✔
316
  }
2✔
317
  return f
3✔
318
}
3✔
319

2✔
320
function format_karma(r) {
7✔
321
  if (r.score !== undefined) {
7✔
322
    if (r.score < -8) return { classy: 'bg_red', title: r.score }
6✔
323
    if (r.score < -3) return { classy: 'bg_lred', title: r.score }
6✔
324
    if (r.score < 0) return { classy: 'bg_yellow', title: r.score }
6✔
325
    if (r.score > 3) return { classy: 'bg_green', title: r.score }
6✔
326
    if (r.score >= 0) return { classy: 'bg_lgreen', title: r.score }
1✔
327
  }
6✔
328
  if (r.fail) return { title: r.fail }
7!
329
  if (r.err) return { title: r.err, classy: 'bg_yellow' }
7!
330
  if (r.emit) return {}
7!
331
  if (r.pass) return { classy: 'bg_green' }
1✔
332
}
7✔
333

2✔
334
// karma reports the remote's ASN reputation as asn_score; light the asn cell
2✔
335
// green/red by its sign (the karma cell itself ignores this message)
2✔
336
function karma_asn_cell(r) {
5✔
337
  const score = parseFloat(r.asn_score)
5✔
338
  if (!score) return null
5✔
339
  return { asn: { classy: score > 0 ? 'bg_lgreen' : 'bg_lred' } }
5✔
340
}
5✔
341

2✔
342
function format_mail_from(r) {
1✔
343
  if (!r.address) return
1!
344
  return { newval: shorten(r.address), classy: 'black', title: r.address }
1✔
345
}
1✔
346

2✔
347
function format_spf(r) {
5✔
348
  if (r.scope) {
5✔
349
    const res = { title: r.result, scope: r.scope }
4✔
350
    switch (r.result) {
4✔
351
      case 'None':
4✔
352
        res.classy = 'bg_lgrey'
1✔
353
        break
1✔
354
      case 'Pass':
4✔
355
        res.classy = 'bg_green'
1✔
356
        break
1✔
357
      case 'Fail':
4✔
358
        res.classy = 'bg_red'
1✔
359
        break
1✔
360
      case 'SoftFail':
4✔
361
        res.classy = 'bg_yellow'
1✔
362
        break
1✔
363
    }
4✔
364
    return res
4✔
365
  }
4✔
366
  if (r.skip) return { classy: 'bg_yellow' }
1✔
367
}
5✔
368

2✔
369
function format_recipient_result(r) {
1✔
370
  if (r.recipient) return exports.format_recipient(r.recipient)
1✔
371
}
1✔
372

2✔
373
function format_headers(r) {
2✔
374
  if (r.fail) {
2✔
375
    if (/^direct/.test(r.fail)) return { classy: 'bg_lred' }
2✔
376
    if (/^from_match/.test(r.fail)) return { classy: 'bg_yellow' }
1✔
377
  }
2✔
378
}
2✔
379

2✔
380
function format_dkim(r) {
3✔
381
  if (r.pass || r.fail) return exports.format_default(r)
3✔
382
  if (r.err) return { classy: 'bg_yellow', title: r.err }
1✔
383
}
3✔
384

2✔
385
function format_rspamd(r) {
12✔
386
  if (r.score !== undefined) {
12✔
387
    let classy
10✔
388
    if (r.is_skipped === true) classy = ''
10✔
389
    else if (r.action === 'greylist') classy = 'bg_grey'
9✔
390
    else if (r.action === 'reject') classy = 'bg_dred'
8✔
391
    else classy = ramp_class(parseFloat(r.score), rspamd_ramp)
7✔
392
    return { classy, title: JSON.stringify(r) }
10✔
393
  }
10✔
394
  if (r.err) return { classy: 'bg_yellow', title: r.err }
12✔
395
  // supplementary results (symbols, etc.) must not repaint the scored cell
1✔
396
  return {}
1✔
397
}
12✔
398

2✔
399
function format_spamassassin(r) {
10✔
400
  const score = r.score ?? r.hits
10✔
401
  if (score !== undefined) {
10✔
402
    return {
8✔
403
      classy: ramp_class(parseFloat(score), spamassassin_ramp),
8✔
404
      title: JSON.stringify(r),
8✔
405
    }
8✔
406
  }
8✔
407
  if (r.err) return { classy: 'bg_yellow', title: r.err }
10✔
408
  // skip/emit and other supplementary results leave the cell unchanged
1✔
409
  return {}
1✔
410
}
10✔
411

2✔
412
function format_dmarc(r) {
3✔
413
  if (r.pass) return { classy: 'bg_green', title: r.pass }
3✔
414
  if (r.fail) return { classy: 'bg_red', title: r.fail }
3✔
415
  if (r.dmarc === 'none') return { classy: 'bg_grey', title: r.dmarc }
1!
NEW
416
  if (r.dmarc === 'other') return {}
×
417
}
3✔
418

2✔
419
function format_queue(r) {
2✔
420
  if (r.pass) return { classy: 'bg_green', title: r.pass }
2✔
421
  if (r.fail) return { classy: 'bg_red', title: r.fail }
1!
NEW
422
  if (r.msg === '') return {}
×
423
}
2✔
424

2✔
425
const noise_text = ['pass', 'skip', 'ips', 'multi', 'helo_host']
2✔
426

2✔
427
const registry = {
2✔
428
  access: { drop: ['msg'], format: format_access },
2✔
429
  tls: { drop: ['msg'], format: format_tls },
2✔
430
  dnsbl: { drop: ['emit', 'pass'], format: format_list },
2✔
431
  'dns-list': { drop: ['emit', 'pass'], format: format_list },
2✔
432
  uribl: { drop: ['pass', 'skip'], format: format_list },
2✔
433
  early_talker: { drop: noise_text, format: pass_fail },
2✔
434
  'helo.checks': { drop: noise_text, format: pass_fail },
2✔
435
  karma: {
2✔
436
    drop: ['awards', 'msg', 'todo', 'asn_score'],
2✔
437
    format: format_karma,
2✔
438
    extra: karma_asn_cell,
2✔
439
  },
2✔
440
  'mail_from.is_resolvable': { drop: ['msg'], format: pass_fail },
2✔
441
  'known-senders': { drop: ['rcpt_ods', 'sender'], format: pass_fail },
2✔
442
  'rcpt_to.in_host_list': { drop: ['msg', 'skip'], format: pass_fail },
2✔
443
  'rcpt_to.qmail_deliverable': { drop: ['msg', 'skip'], format: pass_fail },
2✔
444
  'qmail-deliverable': { drop: ['msg', 'skip'], format: pass_fail },
2✔
445
  limit: {
2✔
446
    drop: [
2✔
447
      'concurrent_count',
2✔
448
      'rate_rcpt',
2✔
449
      'rate_rcpt_sender',
2✔
450
      'concurrent',
2✔
451
      'rate_conn',
2✔
452
      'msg',
2✔
453
    ],
2✔
454
    format: pass_fail,
2✔
455
  },
2✔
456
  relay: { drop: ['skip'], format: pass_fail },
2✔
457
  headers: {
2✔
458
    drop: ['pass', 'msg', 'skip'],
2✔
459
    quiet: (r) => r.fail === 'UA',
2✔
460
    format: format_headers,
2✔
461
  },
2✔
462

2✔
463
  auth: { format: pass_fail },
2✔
464
  avg: { format: pass_fail },
2✔
465
  clamd: { format: pass_fail },
2✔
466
  bounce: { format: exports.format_bounce },
2✔
467
  fcrdns: { format: exports.format_fcrdns },
2✔
468
  asn: { format: exports.format_asn },
2✔
469
  geoip: { format: format_geoip },
2✔
470
  p0f: { format: exports.format_p0f },
2✔
471
  mail_from: { format: format_mail_from },
2✔
472
  spf: { format: format_spf },
2✔
473
  recipient: { format: format_recipient_result },
2✔
474
  rcpt_to: { format: format_recipient_result },
2✔
475
  dkim: { format: format_dkim },
2✔
476
  rspamd: { format: format_rspamd },
2✔
477
  spamassassin: { format: format_spamassassin },
2✔
478
  dmarc: { format: format_dmarc },
2✔
479
  queue: { format: format_queue },
2✔
480
}
2✔
481

2✔
482
exports.registry = registry
2✔
483

2✔
484
exports.should_drop = function (pi_name, r) {
2✔
485
  const entry = registry[exports.get_plugin_name(pi_name)]
17✔
486
  if (!entry) return false
17!
487
  if (entry.quiet && entry.quiet(r)) return true
17!
488
  if (entry.drop) {
17✔
489
    for (const key of entry.drop) {
16✔
490
      if (r[key] !== undefined) return true
34✔
491
    }
34✔
492
  }
1✔
493
  return false
2✔
494
}
17✔
495

2✔
496
exports.format_result = function (pi_name, r) {
2✔
497
  const name = exports.get_plugin_name(pi_name)
60✔
498
  const entry = registry[name]
60✔
499
  const cell = entry?.format ? entry.format(r) : undefined
60✔
500
  return (
60✔
501
    cell ?? {
60✔
502
      title: exports.get_title(name, r),
1✔
503
      classy: exports.get_class(name, r),
1✔
504
    }
1✔
505
  )
60✔
506
}
60✔
507

2✔
508
// cross-plugin cells a result drives in addition to its own (e.g. karma's
2✔
509
// asn_score lighting the asn cell). Computed independently of should_drop.
2✔
510
exports.extra_cells = function (pi_name, r) {
2✔
511
  const entry = registry[exports.get_plugin_name(pi_name)]
17✔
512
  return entry?.extra ? entry.extra(r) || {} : {}
17✔
513
}
17✔
STATUS · Troubleshooting · Open an Issue · Sales · Support · CAREERS · ENTERPRISE · START FREE · SCHEDULE DEMO
ANNOUNCEMENTS · TWITTER · TOS & SLA · Supported CI Services · What's a CI service? · Automated Testing

© 2026 Coveralls, Inc