Skip to content

Commit af03cea

Browse files
committed
Organize output scrubbers into dedicated test/utils/scrubbers/ directory
- Create test/utils/scrubbers/ with separate files by category: - text.mts: Text normalization (log symbols, newlines, ASCII safety) - security.mts: Token sanitization and error message scrubbing - package-managers.mts: Timing and version scrubbing for npm/pnpm/firewall - clean-output.mts: Main entry point that composes all scrubbers - Update pnpm timing scrubber to normalize both seconds and milliseconds to 'Xs' - Update pnpm test snapshots to use normalized timing (Xs instead of Xms) - Update test/utils.mts to import cleanOutput from new location All pnpm tests now pass with stable snapshots.
1 parent 7be5197 commit af03cea

File tree

6 files changed

+187
-114
lines changed

6 files changed

+187
-114
lines changed

src/commands/pnpm/cmd-pnpm.test.mts

Lines changed: 6 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -168,7 +168,7 @@ describe('socket pnpm', async () => {
168168
Already up to date
169169
Progress: resolved 1043, reused 932, downloaded 0, added 0, done
170170
171-
Done in 1s using pnpm v10.17.0"
171+
Done in Xs using pnpm v10.17.0"
172172
`)
173173
expect(code, 'dry-run add should exit with code 0').toBe(0)
174174
},
@@ -207,7 +207,7 @@ describe('socket pnpm', async () => {
207207
Already up to date
208208
Progress: resolved 1043, reused 932, downloaded 0, added 0, done
209209
210-
Done in 1.2s using pnpm v10.17.0"
210+
Done in Xs using pnpm v10.17.0"
211211
`)
212212
expect(code, 'dry-run add scoped package should exit with code 0').toBe(0)
213213
},
@@ -233,7 +233,7 @@ describe('socket pnpm', async () => {
233233
234234
. prepare$ husky
235235
. prepare: Done
236-
Done in 824ms using pnpm v10.17.0"
236+
Done in Xs using pnpm v10.17.0"
237237
`)
238238
expect(code, 'dry-run install should exit with code 0').toBe(0)
239239
},
@@ -259,7 +259,7 @@ describe('socket pnpm', async () => {
259259
260260
. prepare$ husky
261261
. prepare: Done
262-
Done in 837ms using pnpm v10.17.0"
262+
Done in Xs using pnpm v10.17.0"
263263
`)
264264
expect(
265265
code,
@@ -288,7 +288,7 @@ describe('socket pnpm', async () => {
288288
289289
. prepare$ husky
290290
. prepare: Done
291-
Done in 820ms using pnpm v10.17.0"
291+
Done in Xs using pnpm v10.17.0"
292292
`)
293293
expect(
294294
code,
@@ -317,7 +317,7 @@ describe('socket pnpm', async () => {
317317
318318
. prepare$ husky
319319
. prepare: Done
320-
Done in 925ms using pnpm v10.17.0"
320+
Done in Xs using pnpm v10.17.0"
321321
`)
322322
expect(
323323
code,

test/utils.mts

Lines changed: 3 additions & 108 deletions
Original file line numberDiff line numberDiff line change
@@ -4,9 +4,9 @@ import { fileURLToPath } from 'node:url'
44
import { it, vi } from 'vitest'
55

66
import { spawn } from '@socketsecurity/registry/lib/spawn'
7-
import { stripAnsi } from '@socketsecurity/registry/lib/strings'
87

98
import constants, { FLAG_HELP, FLAG_VERSION } from '../src/constants.mts'
9+
import { cleanOutput } from './utils/scrubbers/clean-output.mts'
1010

1111
import type { CResult } from '../src/types.mts'
1212
import type {
@@ -19,16 +19,6 @@ import type { MockedFunction } from 'vitest'
1919
const __filename = fileURLToPath(import.meta.url)
2020
const __dirname = path.dirname(__filename)
2121

22-
// The asciiUnsafeRegexp match characters that are:
23-
// * Control characters in the Unicode range:
24-
// - \u0000 to \u0007 (e.g., NUL, BEL)
25-
// - \u0009 (Tab, but note: not \u0008 Backspace or \u000A Newline)
26-
// - \u000B to \u001F (other non-printable control characters)
27-
// * All non-ASCII characters:
28-
// - \u0080 to \uFFFF (extended Unicode)
29-
// eslint-disable-next-line no-control-regex
30-
const asciiUnsafeRegexp = /[\u0000-\u0007\u0009\u000b-\u001f\u0080-\uffff]/g
31-
3222
// Note: The fixture directory is in the same directory as this utils file.
3323
export const testPath = __dirname
3424

@@ -61,103 +51,8 @@ export const YARN_BERRY_AGENT_FIXTURE = path.join(
6151
export const BUN_AGENT_FIXTURE = path.join(AGENT_FIXTURE_PATH, 'bun')
6252
export const VLT_AGENT_FIXTURE = path.join(AGENT_FIXTURE_PATH, 'vlt')
6353

64-
function normalizeLogSymbols(str: string): string {
65-
return str
66-
.replaceAll('✖', '×')
67-
.replaceAll('ℹ', 'i')
68-
.replaceAll('✔', '√')
69-
.replaceAll('⚠', '‼')
70-
}
71-
72-
function normalizeNewlines(str: string): string {
73-
return (
74-
str
75-
// Replace all literal \r\n.
76-
.replaceAll('\r\n', '\n')
77-
// Replace all escaped \\r\\n.
78-
.replaceAll('\\r\\n', '\\n')
79-
)
80-
}
81-
82-
function stripZeroWidthSpace(str: string): string {
83-
return str.replaceAll('\u200b', '')
84-
}
85-
86-
function toAsciiSafeString(str: string): string {
87-
return str.replace(asciiUnsafeRegexp, m => {
88-
const code = m.charCodeAt(0)
89-
return code < 255
90-
? `\\x${code.toString(16).padStart(2, '0')}`
91-
: `\\u${code.toString(16).padStart(4, '0')}`
92-
})
93-
}
94-
95-
function stripTokenErrorMessages(str: string): string {
96-
// Remove API token error messages to avoid snapshot inconsistencies
97-
// when local environment has/doesn't have tokens set.
98-
return str.replace(
99-
/^\s*[×]\s+This command requires a Socket API token for access.*$/gm,
100-
'',
101-
)
102-
}
103-
104-
function scrubFirewallOutput(str: string): string {
105-
// Scrub dynamic content from Socket Firewall output to prevent snapshot inconsistencies
106-
// from version numbers, timing, package counts, and other variable data.
107-
108-
let result = str
109-
110-
// Normalize Yarn version numbers (e.g., "Yarn 4.10.3" -> "Yarn X.X.X")
111-
result = result.replace(/Yarn \d+\.\d+\.\d+/g, 'Yarn X.X.X')
112-
113-
// Normalize timing information (e.g., "3s 335ms" -> "Xs XXXms", "0s 357ms" -> "Xs XXXms")
114-
result = result.replace(/\d+s \d+ms/g, 'Xs XXXms')
115-
116-
// Normalize package count information (e.g., "1137 more" -> "XXXX more")
117-
result = result.replace(/and \d+ more\./g, 'and XXXX more.')
118-
119-
return result
120-
}
121-
122-
function sanitizeTokens(str: string): string {
123-
// Sanitize Socket API tokens to prevent leaking credentials into snapshots.
124-
// Socket tokens follow the format: sktsec_[alphanumeric+underscore characters]
125-
126-
// Match Socket API tokens: sktsec_ followed by word characters
127-
const tokenPattern = /sktsec_\w+/g
128-
let result = str.replace(tokenPattern, 'sktsec_REDACTED_TOKEN')
129-
130-
// Sanitize token values in JSON-like structures
131-
result = result.replace(
132-
/"apiToken"\s*:\s*"sktsec_[^"]+"/g,
133-
'"apiToken":"sktsec_REDACTED_TOKEN"',
134-
)
135-
136-
// Sanitize token prefixes that might be displayed (e.g., "zP416" -> "REDAC")
137-
// Match 5-character alphanumeric strings that appear after "token:" labels
138-
result = result.replace(
139-
/token:\s*\[?\d+m\]?([A-Za-z0-9]{5})\*{3}/gi,
140-
'token: REDAC***',
141-
)
142-
143-
return result
144-
}
145-
146-
export function cleanOutput(output: string | Buffer<ArrayBufferLike>): string {
147-
return toAsciiSafeString(
148-
normalizeLogSymbols(
149-
normalizeNewlines(
150-
stripZeroWidthSpace(
151-
scrubFirewallOutput(
152-
sanitizeTokens(
153-
stripTokenErrorMessages(stripAnsi(String(output).trim())),
154-
),
155-
),
156-
),
157-
),
158-
),
159-
).trim()
160-
}
54+
// Re-export cleanOutput from scrubbers for convenience
55+
export { cleanOutput }
16156

16257
/**
16358
* Check if output contains cdxgen help content.
Lines changed: 40 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,40 @@
1+
/** @fileoverview Main output cleaner that applies all scrubbers. */
2+
3+
import { stripAnsi } from '@socketsecurity/registry/lib/strings'
4+
5+
import {
6+
scrubFirewallOutput,
7+
scrubNpmOutput,
8+
scrubPnpmOutput,
9+
} from './package-managers.mts'
10+
import { sanitizeTokens, stripTokenErrorMessages } from './security.mts'
11+
import {
12+
normalizeLogSymbols,
13+
normalizeNewlines,
14+
stripZeroWidthSpace,
15+
toAsciiSafeString,
16+
} from './text.mts'
17+
18+
/**
19+
* Apply all standard scrubbers to output for consistent test snapshots.
20+
* This is the main function that should be used for cleaning test output.
21+
*/
22+
export function cleanOutput(output: string | Buffer<ArrayBufferLike>): string {
23+
return toAsciiSafeString(
24+
normalizeLogSymbols(
25+
normalizeNewlines(
26+
stripZeroWidthSpace(
27+
scrubPnpmOutput(
28+
scrubNpmOutput(
29+
scrubFirewallOutput(
30+
sanitizeTokens(
31+
stripTokenErrorMessages(stripAnsi(String(output).trim())),
32+
),
33+
),
34+
),
35+
),
36+
),
37+
),
38+
),
39+
).trim()
40+
}
Lines changed: 54 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,54 @@
1+
/** @fileoverview Package manager output scrubbers for test snapshots. */
2+
3+
/**
4+
* Scrub Socket Firewall output to prevent snapshot inconsistencies
5+
* from version numbers, timing, package counts, and other variable data.
6+
*/
7+
export function scrubFirewallOutput(str: string): string {
8+
let result = str
9+
10+
// Normalize Yarn version numbers (e.g., "Yarn 4.10.3" -> "Yarn X.X.X")
11+
result = result.replace(/Yarn \d+\.\d+\.\d+/g, 'Yarn X.X.X')
12+
13+
// Normalize timing information (e.g., "3s 335ms" -> "Xs XXXms", "0s 357ms" -> "Xs XXXms")
14+
result = result.replace(/\d+s \d+ms/g, 'Xs XXXms')
15+
16+
// Normalize package count information (e.g., "1137 more" -> "XXXX more")
17+
result = result.replace(/and \d+ more\./g, 'and XXXX more.')
18+
19+
return result
20+
}
21+
22+
/**
23+
* Scrub pnpm output to prevent snapshot inconsistencies from timing variations.
24+
* Normalizes execution time to prevent flaky tests.
25+
*/
26+
export function scrubPnpmOutput(str: string): string {
27+
let result = str
28+
29+
// Normalize pnpm timing: "Done in 1.2s" -> "Done in Xs", "Done in 825ms" -> "Done in Xs"
30+
result = result.replace(
31+
/Done in \d+(\.\d+)?s using pnpm/g,
32+
'Done in Xs using pnpm',
33+
)
34+
result = result.replace(/Done in \d+ms using pnpm/g, 'Done in Xs using pnpm')
35+
36+
return result
37+
}
38+
39+
/**
40+
* Scrub npm output to prevent snapshot inconsistencies from timing variations.
41+
* Normalizes execution time to prevent flaky tests.
42+
*/
43+
export function scrubNpmOutput(str: string): string {
44+
let result = str
45+
46+
// Normalize npm timing variations
47+
result = result.replace(
48+
/added \d+ packages in \d+(\.\d+)?s/g,
49+
'added X packages in Xs',
50+
)
51+
result = result.replace(/up to date in \d+(\.\d+)?s/g, 'up to date in Xs')
52+
53+
return result
54+
}

test/utils/scrubbers/security.mts

Lines changed: 37 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,37 @@
1+
/** @fileoverview Security-related scrubbers for test snapshots. */
2+
3+
/**
4+
* Strip API token error messages to avoid snapshot inconsistencies
5+
* when local environment has/doesn't have tokens set.
6+
*/
7+
export function stripTokenErrorMessages(str: string): string {
8+
return str.replace(
9+
/^\s*[×]\s+This command requires a Socket API token for access.*$/gm,
10+
'',
11+
)
12+
}
13+
14+
/**
15+
* Sanitize Socket API tokens to prevent leaking credentials into snapshots.
16+
* Socket tokens follow the format: sktsec_[alphanumeric+underscore characters]
17+
*/
18+
export function sanitizeTokens(str: string): string {
19+
// Match Socket API tokens: sktsec_ followed by word characters
20+
const tokenPattern = /sktsec_\w+/g
21+
let result = str.replace(tokenPattern, 'sktsec_REDACTED_TOKEN')
22+
23+
// Sanitize token values in JSON-like structures
24+
result = result.replace(
25+
/"apiToken"\s*:\s*"sktsec_[^"]+"/g,
26+
'"apiToken":"sktsec_REDACTED_TOKEN"',
27+
)
28+
29+
// Sanitize token prefixes that might be displayed (e.g., "zP416" -> "REDAC")
30+
// Match 5-character alphanumeric strings that appear after "token:" labels
31+
result = result.replace(
32+
/token:\s*\[?\d+m\]?([A-Za-z0-9]{5})\*{3}/gi,
33+
'token: REDAC***',
34+
)
35+
36+
return result
37+
}

test/utils/scrubbers/text.mts

Lines changed: 47 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,47 @@
1+
/** @fileoverview Text normalization scrubbers for test snapshots. */
2+
3+
/**
4+
* Normalize log symbols to ASCII equivalents for consistent snapshots.
5+
* Maps Unicode symbols to basic ASCII characters.
6+
*/
7+
export function normalizeLogSymbols(str: string): string {
8+
return str
9+
.replaceAll('✖', '×')
10+
.replaceAll('ℹ', 'i')
11+
.replaceAll('✔', '√')
12+
.replaceAll('⚠', '‼')
13+
}
14+
15+
/**
16+
* Normalize newlines to LF for consistent snapshots across platforms.
17+
*/
18+
export function normalizeNewlines(str: string): string {
19+
return (
20+
str
21+
// Replace all literal \r\n.
22+
.replaceAll('\r\n', '\n')
23+
// Replace all escaped \\r\\n.
24+
.replaceAll('\\r\\n', '\\n')
25+
)
26+
}
27+
28+
/**
29+
* Strip zero-width spaces that may appear in output.
30+
*/
31+
export function stripZeroWidthSpace(str: string): string {
32+
return str.replaceAll('\u200b', '')
33+
}
34+
35+
/**
36+
* Convert non-ASCII characters to escape sequences for safe snapshots.
37+
*/
38+
export function toAsciiSafeString(str: string): string {
39+
// Match characters outside printable ASCII range (32-126) excluding newlines/tabs.
40+
const asciiUnsafeRegexp = /[^\n\t\x20-\x7e]/g
41+
return str.replace(asciiUnsafeRegexp, m => {
42+
const code = m.charCodeAt(0)
43+
return code < 255
44+
? `\\x${code.toString(16).padStart(2, '0')}`
45+
: `\\u${code.toString(16).padStart(4, '0')}`
46+
})
47+
}

0 commit comments

Comments
 (0)