Skip to content

Commit 6687e27

Browse files
committed
test(cli): add additional unit tests for coverage improvement
Add tests for: - flags.mts: Invalid NODE_OPTIONS handling, semi-space size tiers - link.mts: githubRepoLink function (4 tests, achieves 100% coverage) - manager.mts: Invalid timestamp handling, zero timestampFetch - notifier.mts: Formatting error edge cases Increases coverage from 71.76% toward 75% target.
1 parent ebded16 commit 6687e27

File tree

9 files changed

+1019
-0
lines changed

9 files changed

+1019
-0
lines changed

packages/cli/test/unit/flags.test.mts

Lines changed: 39 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -126,6 +126,17 @@ describe('flags', () => {
126126
// Should be 75% of 4GB in MiB = 3072.
127127
expect(result).toBe(3072)
128128
})
129+
130+
it('handles invalid NODE_OPTIONS value gracefully', () => {
131+
// Set NODE_OPTIONS with an invalid pattern (non-numeric after equals).
132+
// Since the regex only matches digits, this will fall through to default.
133+
mockValues.nodeOptions = '--max-old-space-size=abc'
134+
resetFlagCache()
135+
136+
const result = getMaxOldSpaceSizeFlag()
137+
// Should fall back to default (75% of 8GB).
138+
expect(result).toBe(6144)
139+
})
129140
})
130141

131142
describe('getMaxSemiSpaceSizeFlag', () => {
@@ -177,6 +188,34 @@ describe('flags', () => {
177188
// 2048 MiB heap should use 16 MiB semi-space.
178189
expect(result).toBe(16)
179190
})
191+
192+
it('scales for 1024 MiB heap', () => {
193+
mockValues.maxOldSpaceSize = 1024
194+
resetFlagCache()
195+
196+
const result = getMaxSemiSpaceSizeFlag()
197+
// 1024 MiB heap should use 8 MiB semi-space.
198+
expect(result).toBe(8)
199+
})
200+
201+
it('scales for 4096 MiB heap', () => {
202+
mockValues.maxOldSpaceSize = 4096
203+
resetFlagCache()
204+
205+
const result = getMaxSemiSpaceSizeFlag()
206+
// 4096 MiB heap should use 32 MiB semi-space.
207+
expect(result).toBe(32)
208+
})
209+
210+
it('handles invalid NODE_OPTIONS for semi-space gracefully', () => {
211+
// Set NODE_OPTIONS with a non-matching pattern.
212+
mockValues.nodeOptions = '--max-semi-space-size=xyz'
213+
resetFlagCache()
214+
215+
const result = getMaxSemiSpaceSizeFlag()
216+
// Should fall back to calculated default based on old space.
217+
expect(result).toBe(64) // Default for 6144 MiB old space.
218+
})
180219
})
181220

182221
describe('commonFlags', () => {

packages/cli/test/unit/utils/command/registry-core.test.mts

Lines changed: 173 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -90,6 +90,52 @@ describe('CommandRegistry', () => {
9090
})
9191
})
9292

93+
describe('unregister()', () => {
94+
it('should unregister a command', () => {
95+
const command: CommandDefinition = {
96+
name: 'test',
97+
description: 'Test command',
98+
async handler() {
99+
return { ok: true, data: undefined }
100+
},
101+
}
102+
103+
registry.register(command)
104+
expect(registry.has('test')).toBe(true)
105+
106+
const result = registry.unregister('test')
107+
108+
expect(result).toBe(true)
109+
expect(registry.has('test')).toBe(false)
110+
})
111+
112+
it('should unregister command aliases', () => {
113+
const command: CommandDefinition = {
114+
name: 'test',
115+
description: 'Test command',
116+
aliases: ['t', 'tst'],
117+
async handler() {
118+
return { ok: true, data: undefined }
119+
},
120+
}
121+
122+
registry.register(command)
123+
expect(registry.has('t')).toBe(true)
124+
expect(registry.has('tst')).toBe(true)
125+
126+
registry.unregister('test')
127+
128+
expect(registry.has('test')).toBe(false)
129+
expect(registry.has('t')).toBe(false)
130+
expect(registry.has('tst')).toBe(false)
131+
})
132+
133+
it('should return false when unregistering unknown command', () => {
134+
const result = registry.unregister('nonexistent')
135+
expect(result).toBe(false)
136+
})
137+
})
138+
93139
describe('list()', () => {
94140
it('should list all commands', () => {
95141
const cmd1: CommandDefinition = {
@@ -337,6 +383,133 @@ describe('CommandRegistry', () => {
337383
expect(result.ok).toBe(false)
338384
expect(result.cause).toContain('Count must be non-negative')
339385
})
386+
387+
it('should error when string flag is missing value', async () => {
388+
const command: CommandDefinition = {
389+
name: 'test',
390+
description: 'Test command',
391+
flags: {
392+
name: {
393+
type: 'string',
394+
description: 'Name',
395+
},
396+
},
397+
async handler() {
398+
return { ok: true, data: undefined }
399+
},
400+
}
401+
402+
registry.register(command)
403+
404+
const result = await registry.execute('test', ['--name'])
405+
406+
expect(result.ok).toBe(false)
407+
expect(result.message).toContain('Missing value for flag --name')
408+
})
409+
410+
it('should error when number flag has invalid value', async () => {
411+
const command: CommandDefinition = {
412+
name: 'test',
413+
description: 'Test command',
414+
flags: {
415+
count: {
416+
type: 'number',
417+
description: 'Count',
418+
},
419+
},
420+
async handler() {
421+
return { ok: true, data: undefined }
422+
},
423+
}
424+
425+
registry.register(command)
426+
427+
const result = await registry.execute('test', ['--count', 'notanumber'])
428+
429+
expect(result.ok).toBe(false)
430+
expect(result.message).toContain('Invalid number value for --count')
431+
})
432+
433+
it('should parse array flags', async () => {
434+
const command: CommandDefinition = {
435+
name: 'test',
436+
description: 'Test command',
437+
flags: {
438+
tags: {
439+
type: 'array',
440+
description: 'Tags',
441+
},
442+
},
443+
async handler({ flags }) {
444+
expect(flags.tags).toEqual(['tag1', 'tag2', 'tag3'])
445+
return { ok: true, data: undefined }
446+
},
447+
}
448+
449+
registry.register(command)
450+
451+
const result = await registry.execute('test', [
452+
'--tags',
453+
'tag1',
454+
'--tags',
455+
'tag2',
456+
'--tags',
457+
'tag3',
458+
])
459+
460+
expect(result.ok).toBe(true)
461+
})
462+
463+
it('should parse array flags with = syntax', async () => {
464+
const command: CommandDefinition = {
465+
name: 'test',
466+
description: 'Test command',
467+
flags: {
468+
tags: {
469+
type: 'array',
470+
description: 'Tags',
471+
},
472+
},
473+
async handler({ flags }) {
474+
expect(flags.tags).toEqual(['tag1', 'tag2'])
475+
return { ok: true, data: undefined }
476+
},
477+
}
478+
479+
registry.register(command)
480+
481+
const result = await registry.execute('test', [
482+
'--tags=tag1',
483+
'--tags=tag2',
484+
])
485+
486+
expect(result.ok).toBe(true)
487+
})
488+
})
489+
490+
describe('plugins', () => {
491+
it('should install plugin and call its install method', () => {
492+
let installed = false
493+
const plugin = {
494+
name: 'test-plugin',
495+
install(reg: CommandRegistry) {
496+
installed = true
497+
// Plugin can register commands.
498+
reg.register({
499+
name: 'plugin-cmd',
500+
description: 'Plugin command',
501+
async handler() {
502+
return { ok: true, data: undefined }
503+
},
504+
})
505+
},
506+
}
507+
508+
registry.use(plugin)
509+
510+
expect(installed).toBe(true)
511+
expect(registry.has('plugin-cmd')).toBe(true)
512+
})
340513
})
341514

342515
describe('middleware', () => {

packages/cli/test/unit/utils/ecosystem/environment.test.mts

Lines changed: 98 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -535,5 +535,103 @@ describe('package-environment', () => {
535535
expect(mockLogger.warn).toHaveBeenCalled()
536536
}
537537
})
538+
539+
it('returns error when node version is not supported', async () => {
540+
mockFindUp.mockImplementation(async files => {
541+
if (Array.isArray(files) && files.includes('package-lock.json')) {
542+
return '/project/package-lock.json'
543+
}
544+
if (files === 'package.json') {
545+
return '/project/package.json'
546+
}
547+
return undefined
548+
})
549+
mockExistsSync.mockReturnValue(true)
550+
mockWhichBin.mockResolvedValue('/usr/local/bin/npm')
551+
mockReadPackageJson.mockResolvedValue({
552+
name: 'test-project',
553+
version: '1.0.0',
554+
})
555+
mockReadFileUtf8.mockResolvedValue('lock content')
556+
// First return true for agent, then false for node.
557+
let callCount = 0
558+
mockSatisfies.mockImplementation(() => {
559+
callCount++
560+
return callCount === 1 // true for agent, false for node.
561+
})
562+
563+
const result = await detectAndValidatePackageEnvironment('/project')
564+
565+
expect(result.ok).toBe(false)
566+
if (!result.ok) {
567+
expect(result.message).toBe('Version mismatch')
568+
}
569+
})
570+
571+
it('returns error when package node engine requirements are not met', async () => {
572+
mockFindUp.mockImplementation(async files => {
573+
if (Array.isArray(files) && files.includes('package-lock.json')) {
574+
return '/project/package-lock.json'
575+
}
576+
if (files === 'package.json') {
577+
return '/project/package.json'
578+
}
579+
return undefined
580+
})
581+
mockExistsSync.mockReturnValue(true)
582+
mockWhichBin.mockResolvedValue('/usr/local/bin/npm')
583+
mockReadPackageJson.mockResolvedValue({
584+
name: 'test-project',
585+
version: '1.0.0',
586+
engines: {
587+
node: '>=22.0.0',
588+
},
589+
})
590+
mockReadFileUtf8.mockResolvedValue('lock content')
591+
// Return true for agent and node supported, but false for pkgSupports.
592+
let callCount = 0
593+
mockSatisfies.mockImplementation(() => {
594+
callCount++
595+
// First two calls return true (agent supported, node supported).
596+
// Third call returns false (pkgSupports.agent).
597+
// Fourth call returns false (pkgSupports.node).
598+
return callCount <= 2
599+
})
600+
601+
const result = await detectAndValidatePackageEnvironment('/project')
602+
603+
expect(result.ok).toBe(false)
604+
if (!result.ok) {
605+
expect(result.message).toBe('Engine mismatch')
606+
}
607+
})
608+
609+
it('returns error when package.json is missing', async () => {
610+
mockFindUp.mockImplementation(async files => {
611+
if (Array.isArray(files) && files.includes('package-lock.json')) {
612+
return '/project/package-lock.json'
613+
}
614+
if (files === 'package.json') {
615+
return '/project/package.json'
616+
}
617+
return undefined
618+
})
619+
// Return true for path existence, but make pkgPath undefined by not having editablePkgJson.
620+
mockExistsSync.mockReturnValue(true)
621+
mockWhichBin.mockResolvedValue('/usr/local/bin/npm')
622+
// Return undefined to simulate missing package.json.
623+
mockReadPackageJson.mockResolvedValue(undefined)
624+
mockToEditablePackageJson.mockResolvedValue(undefined)
625+
mockReadFileUtf8.mockResolvedValue('lock content')
626+
627+
const result = await detectAndValidatePackageEnvironment('/project')
628+
629+
expect(result.ok).toBe(false)
630+
if (!result.ok) {
631+
// The validation checks for lockfile presence first, and
632+
// editablePkgJson being undefined makes lockName undefined.
633+
expect(result.message).toBe('Missing lockfile')
634+
}
635+
})
538636
})
539637
})

0 commit comments

Comments
 (0)