Skip to content

Commit 2bc84b7

Browse files
committed
test(coverage): cover six previously zero-coverage files (35 tests)
Phase 1 of the coverage push. Six small zero-coverage files now at 100%: - env/opengrep-version.mts (3 tests): version getter happy path, missing env var, empty-string env var - env/trivy-version.mts (3 tests): same shape - env/trufflehog-version.mts (3 tests): same shape - utils/spawn/apply-machine-mode.mts (6 tests): pass-through when ambient mode off; defers to raw applier when on; inferSubcommand finds first non-flag, returns undefined for empty/all-flag input - commands/uninstall/teardown-tab-completion.mts (8 tests): getBashrcDetails error pass-through; .bashrc absent → "not found"; bashrc without our block → "missing"; full block removal; fallback individual-line removal when block edited; left[] reports remaining completion-prefix lines; homePath empty short-circuits the check - commands/manifest/detect-manifest-actions.mts (12 tests): empty dir; build.sbt → sbt; gradlew → gradle; environment.yml → conda; environment.yaml fallback; both .yml + .yaml → counts once; all-three; sockJson disabling each generator; cdxgen always false Tests use real tmpdirs for filesystem checks (detect-manifest-actions) and surgical vi.mock for fs / completion / homePath / paths.mjs where mocking is cleaner than driving the real implementation.
1 parent 3b0ae7b commit 2bc84b7

6 files changed

Lines changed: 561 additions & 0 deletions

File tree

Lines changed: 173 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,173 @@
1+
/**
2+
* Unit tests for detectManifestActions.
3+
*
4+
* Walks a directory looking for files that indicate which manifest
5+
* generators (sbt, gradle, conda) should run. Per-generator
6+
* `socket.json` `disabled` flags can suppress detection.
7+
*
8+
* Test Coverage:
9+
* - Empty directory → no detections, count 0
10+
* - build.sbt present → sbt=true, count 1
11+
* - gradlew present → gradle=true, count 1
12+
* - environment.yml present → conda=true, count 1
13+
* - environment.yaml present (when no .yml) → conda=true
14+
* - Both .yml and .yaml present → only counts once (yml wins)
15+
* - All three present → all true, count 3
16+
* - sockJson disables sbt → sbt=false even with build.sbt
17+
* - sockJson disables gradle → gradle=false even with gradlew
18+
* - sockJson disables conda → conda=false even with environment.yml
19+
* - cdxgen field is always false (not auto-detected)
20+
*
21+
* Related Files:
22+
* - src/commands/manifest/detect-manifest-actions.mts - Implementation
23+
*/
24+
25+
import { mkdirSync, mkdtempSync, rmSync, writeFileSync } from 'node:fs'
26+
import { tmpdir } from 'node:os'
27+
import path from 'node:path'
28+
29+
import { afterEach, beforeEach, describe, expect, it, vi } from 'vitest'
30+
31+
import type { SocketJson } from '../../../../src/utils/socket/json.mts'
32+
33+
// Source-of-truth constants/paths.mts evaluates the bundle-tools.json
34+
// version table at import time, which fails outside of a build (where
35+
// INLINED_COANA_VERSION is missing). We only need ENVIRONMENT_YML /
36+
// ENVIRONMENT_YAML, so stub them.
37+
vi.mock('../../../../src/constants/paths.mjs', () => ({
38+
ENVIRONMENT_YAML: 'environment.yaml',
39+
ENVIRONMENT_YML: 'environment.yml',
40+
}))
41+
42+
const { detectManifestActions } = await import(
43+
'../../../../src/commands/manifest/detect-manifest-actions.mts'
44+
)
45+
46+
let cwd = ''
47+
48+
beforeEach(() => {
49+
cwd = mkdtempSync(path.join(tmpdir(), 'detect-manifest-'))
50+
})
51+
52+
afterEach(() => {
53+
rmSync(cwd, { force: true, recursive: true })
54+
})
55+
56+
function touch(rel: string) {
57+
const full = path.join(cwd, rel)
58+
mkdirSync(path.dirname(full), { recursive: true })
59+
writeFileSync(full, '')
60+
}
61+
62+
describe('detectManifestActions', () => {
63+
it('returns all-false counts on an empty directory', async () => {
64+
const result = await detectManifestActions(null, cwd)
65+
expect(result).toEqual({
66+
cdxgen: false,
67+
count: 0,
68+
conda: false,
69+
gradle: false,
70+
sbt: false,
71+
})
72+
})
73+
74+
it('detects build.sbt as Scala sbt project', async () => {
75+
touch('build.sbt')
76+
const result = await detectManifestActions(null, cwd)
77+
expect(result.sbt).toBe(true)
78+
expect(result.count).toBe(1)
79+
})
80+
81+
it('detects gradlew as Gradle project', async () => {
82+
touch('gradlew')
83+
const result = await detectManifestActions(null, cwd)
84+
expect(result.gradle).toBe(true)
85+
expect(result.count).toBe(1)
86+
})
87+
88+
it('detects environment.yml as Conda project', async () => {
89+
touch('environment.yml')
90+
const result = await detectManifestActions(null, cwd)
91+
expect(result.conda).toBe(true)
92+
expect(result.count).toBe(1)
93+
})
94+
95+
it('detects environment.yaml as Conda project when .yml is absent', async () => {
96+
touch('environment.yaml')
97+
const result = await detectManifestActions(null, cwd)
98+
expect(result.conda).toBe(true)
99+
expect(result.count).toBe(1)
100+
})
101+
102+
it('counts conda only once when both .yml and .yaml are present', async () => {
103+
touch('environment.yml')
104+
touch('environment.yaml')
105+
const result = await detectManifestActions(null, cwd)
106+
expect(result.conda).toBe(true)
107+
expect(result.count).toBe(1)
108+
})
109+
110+
it('detects all three when all marker files are present', async () => {
111+
touch('build.sbt')
112+
touch('gradlew')
113+
touch('environment.yml')
114+
const result = await detectManifestActions(null, cwd)
115+
expect(result.sbt).toBe(true)
116+
expect(result.gradle).toBe(true)
117+
expect(result.conda).toBe(true)
118+
expect(result.count).toBe(3)
119+
})
120+
121+
it('respects socket.json disabling sbt detection', async () => {
122+
touch('build.sbt')
123+
const sockJson = {
124+
defaults: { manifest: { sbt: { disabled: true } } },
125+
} as unknown as SocketJson
126+
const result = await detectManifestActions(sockJson, cwd)
127+
expect(result.sbt).toBe(false)
128+
expect(result.count).toBe(0)
129+
})
130+
131+
it('respects socket.json disabling gradle detection', async () => {
132+
touch('gradlew')
133+
const sockJson = {
134+
defaults: { manifest: { gradle: { disabled: true } } },
135+
} as unknown as SocketJson
136+
const result = await detectManifestActions(sockJson, cwd)
137+
expect(result.gradle).toBe(false)
138+
expect(result.count).toBe(0)
139+
})
140+
141+
it('respects socket.json disabling conda detection', async () => {
142+
touch('environment.yml')
143+
const sockJson = {
144+
defaults: { manifest: { conda: { disabled: true } } },
145+
} as unknown as SocketJson
146+
const result = await detectManifestActions(sockJson, cwd)
147+
expect(result.conda).toBe(false)
148+
expect(result.count).toBe(0)
149+
})
150+
151+
it('always reports cdxgen as false (not auto-detected)', async () => {
152+
touch('build.sbt')
153+
touch('gradlew')
154+
touch('environment.yml')
155+
const result = await detectManifestActions(null, cwd)
156+
expect(result.cdxgen).toBe(false)
157+
})
158+
159+
it('ignores other socket.json keys when checking specific generators', async () => {
160+
// Only sbt is disabled; gradle and conda remain enabled.
161+
touch('build.sbt')
162+
touch('gradlew')
163+
touch('environment.yml')
164+
const sockJson = {
165+
defaults: { manifest: { sbt: { disabled: true } } },
166+
} as unknown as SocketJson
167+
const result = await detectManifestActions(sockJson, cwd)
168+
expect(result.sbt).toBe(false)
169+
expect(result.gradle).toBe(true)
170+
expect(result.conda).toBe(true)
171+
expect(result.count).toBe(2)
172+
})
173+
})
Lines changed: 177 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,177 @@
1+
/**
2+
* Unit tests for teardownTabCompletion.
3+
*
4+
* Removes Socket CLI tab-completion lines from the user's ~/.bashrc.
5+
* Mocks node:fs so tests don't touch the real filesystem.
6+
*
7+
* Test Coverage:
8+
* - getBashrcDetails error pass-through
9+
* - .bashrc absent → "not found" action
10+
* - .bashrc present without our block → "missing" action
11+
* - .bashrc present with our block → removes the full block
12+
* - .bashrc present where the block was edited → falls back to
13+
* removing sourcingCommand / completionCommand individually
14+
* - findRemainingCompletionSetups discovers other targets
15+
* - homePath unset edge case (skip the bashrc lookup entirely)
16+
*
17+
* Related Files:
18+
* - src/commands/uninstall/teardown-tab-completion.mts - Implementation
19+
* - src/utils/cli/completion.mts - getBashrcDetails / COMPLETION_CMD_PREFIX
20+
* - src/constants/paths.mts - homePath
21+
*/
22+
23+
import { beforeEach, describe, expect, it, vi } from 'vitest'
24+
25+
const { mockExistsSync, mockReadFileSync, mockWriteFileSync } = vi.hoisted(
26+
() => ({
27+
mockExistsSync: vi.fn(),
28+
mockReadFileSync: vi.fn(),
29+
mockWriteFileSync: vi.fn(),
30+
}),
31+
)
32+
33+
vi.mock('node:fs', () => ({
34+
default: {
35+
existsSync: mockExistsSync,
36+
readFileSync: mockReadFileSync,
37+
writeFileSync: mockWriteFileSync,
38+
},
39+
existsSync: mockExistsSync,
40+
readFileSync: mockReadFileSync,
41+
writeFileSync: mockWriteFileSync,
42+
}))
43+
44+
const { mockGetBashrcDetails } = vi.hoisted(() => ({
45+
mockGetBashrcDetails: vi.fn(),
46+
}))
47+
48+
vi.mock('../../../../src/utils/cli/completion.mts', () => ({
49+
COMPLETION_CMD_PREFIX: 'source <(socket install completion ',
50+
getBashrcDetails: mockGetBashrcDetails,
51+
}))
52+
53+
const { mockHomePath } = vi.hoisted(() => ({
54+
mockHomePath: { value: '/home/test' as string },
55+
}))
56+
57+
vi.mock('../../../../src/constants/paths.mts', () => ({
58+
get homePath() {
59+
return mockHomePath.value
60+
},
61+
}))
62+
63+
const { teardownTabCompletion } = await import(
64+
'../../../../src/commands/uninstall/teardown-tab-completion.mts'
65+
)
66+
67+
const validDetails = {
68+
ok: true as const,
69+
data: {
70+
completionCommand: 'source <(socket install completion socket)',
71+
sourcingCommand: 'eval "$(socket install completion socket)"',
72+
toAddToBashrc:
73+
'# Socket CLI tab completion\nsource <(socket install completion socket)\n',
74+
},
75+
}
76+
77+
beforeEach(() => {
78+
vi.clearAllMocks()
79+
mockHomePath.value = '/home/test'
80+
mockGetBashrcDetails.mockReturnValue(validDetails)
81+
})
82+
83+
describe('teardownTabCompletion', () => {
84+
it('passes through getBashrcDetails errors', async () => {
85+
mockGetBashrcDetails.mockReturnValue({
86+
ok: false,
87+
message: 'unknown shell',
88+
cause: 'unsupported',
89+
})
90+
const result = await teardownTabCompletion('socket')
91+
expect(result.ok).toBe(false)
92+
expect(mockExistsSync).not.toHaveBeenCalled()
93+
})
94+
95+
it('returns "not found" action when ~/.bashrc does not exist', async () => {
96+
mockExistsSync.mockReturnValue(false)
97+
const result = await teardownTabCompletion('socket')
98+
expect(result.ok).toBe(true)
99+
if (result.ok) {
100+
expect(result.data.action).toBe('not found')
101+
expect(result.data.left).toEqual([])
102+
}
103+
expect(mockReadFileSync).not.toHaveBeenCalled()
104+
})
105+
106+
it('returns "missing" when ~/.bashrc has no completion block', async () => {
107+
mockExistsSync.mockReturnValue(true)
108+
mockReadFileSync.mockReturnValue('export PATH=$PATH:/usr/local/bin\n')
109+
const result = await teardownTabCompletion('socket')
110+
expect(result.ok).toBe(true)
111+
if (result.ok) {
112+
expect(result.data.action).toBe('missing')
113+
expect(result.data.left).toEqual([])
114+
}
115+
expect(mockWriteFileSync).not.toHaveBeenCalled()
116+
})
117+
118+
it('removes the completion block when present', async () => {
119+
mockExistsSync.mockReturnValue(true)
120+
const before = `export PATH=$PATH:/usr/local/bin\n${validDetails.data.toAddToBashrc}\nalias ll='ls -la'\n`
121+
mockReadFileSync.mockReturnValue(before)
122+
const result = await teardownTabCompletion('socket')
123+
expect(result.ok).toBe(true)
124+
if (result.ok) {
125+
expect(result.data.action).toBe('removed')
126+
}
127+
expect(mockWriteFileSync).toHaveBeenCalledTimes(1)
128+
const written = mockWriteFileSync.mock.calls[0]![1]
129+
expect(written).not.toContain(validDetails.data.toAddToBashrc)
130+
})
131+
132+
it('falls back to removing sourcing/completion lines individually when the block was edited', async () => {
133+
mockExistsSync.mockReturnValue(true)
134+
// Manually-edited bashrc: someone removed the comment and reorganized.
135+
const partial = `${validDetails.data.toAddToBashrc}\n${validDetails.data.sourcingCommand}\n${validDetails.data.completionCommand}\n`
136+
mockReadFileSync.mockReturnValue(partial)
137+
const result = await teardownTabCompletion('socket')
138+
expect(result.ok).toBe(true)
139+
const written = mockWriteFileSync.mock.calls[0]![1] as string
140+
expect(written).not.toContain(validDetails.data.sourcingCommand)
141+
expect(written).not.toContain(validDetails.data.completionCommand)
142+
})
143+
144+
it('reports remaining completion-prefix lines in the "left" array', async () => {
145+
mockExistsSync.mockReturnValue(true)
146+
const otherTarget = 'source <(socket install completion otherCli)'
147+
const before = `${validDetails.data.toAddToBashrc}\n${otherTarget}\n`
148+
mockReadFileSync.mockReturnValue(before)
149+
const result = await teardownTabCompletion('socket')
150+
expect(result.ok).toBe(true)
151+
if (result.ok) {
152+
expect(result.data.left).toEqual(['otherCli)'])
153+
}
154+
})
155+
156+
it('reports remaining setups in the "missing" branch too', async () => {
157+
mockExistsSync.mockReturnValue(true)
158+
const otherTarget = 'source <(socket install completion otherCli)'
159+
mockReadFileSync.mockReturnValue(otherTarget + '\n')
160+
const result = await teardownTabCompletion('socket')
161+
expect(result.ok).toBe(true)
162+
if (result.ok) {
163+
expect(result.data.action).toBe('missing')
164+
expect(result.data.left).toEqual(['otherCli)'])
165+
}
166+
})
167+
168+
it('skips bashrc handling when homePath is empty', async () => {
169+
mockHomePath.value = ''
170+
const result = await teardownTabCompletion('socket')
171+
expect(result.ok).toBe(true)
172+
if (result.ok) {
173+
expect(result.data.action).toBe('not found')
174+
}
175+
expect(mockExistsSync).not.toHaveBeenCalled()
176+
})
177+
})
Lines changed: 45 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,45 @@
1+
/**
2+
* Unit tests for OpenGrep version getter.
3+
*
4+
* The getter reads INLINED_OPENGREP_VERSION from process.env directly
5+
* so esbuild's define plugin can inline the value at build time. Tests
6+
* verify the runtime success and the missing-env throw.
7+
*
8+
* Related Files:
9+
* - src/env/opengrep-version.mts
10+
*/
11+
12+
import { afterEach, beforeEach, describe, expect, it } from 'vitest'
13+
14+
import { getOpengrepVersion } from '../../../src/env/opengrep-version.mts'
15+
16+
describe('env/opengrep-version', () => {
17+
let original: string | undefined
18+
19+
beforeEach(() => {
20+
original = process.env['INLINED_OPENGREP_VERSION']
21+
})
22+
23+
afterEach(() => {
24+
if (original !== undefined) {
25+
process.env['INLINED_OPENGREP_VERSION'] = original
26+
} else {
27+
delete process.env['INLINED_OPENGREP_VERSION']
28+
}
29+
})
30+
31+
it('returns the version string when the env var is set', () => {
32+
process.env['INLINED_OPENGREP_VERSION'] = '1.2.3'
33+
expect(getOpengrepVersion()).toBe('1.2.3')
34+
})
35+
36+
it('throws a build-time-inlined message when the env var is missing', () => {
37+
delete process.env['INLINED_OPENGREP_VERSION']
38+
expect(() => getOpengrepVersion()).toThrow(/INLINED_OPENGREP_VERSION/)
39+
})
40+
41+
it('throws when the env var is the empty string', () => {
42+
process.env['INLINED_OPENGREP_VERSION'] = ''
43+
expect(() => getOpengrepVersion()).toThrow(/INLINED_OPENGREP_VERSION/)
44+
})
45+
})

0 commit comments

Comments
 (0)