Skip to content

Commit 2f904be

Browse files
committed
test: assert package manager command output parsing; fix uv uninstall doc
Address PR review: strengthen command unit tests to verify output parsing behavior (package lists, direct names with normalization, version strings, available-version filtering) using representative runner output, instead of asserting the full argument vector (which merely restates buildCommand). Mutation commands remain smoke tests; their command strings are covered by integration tests. Also correct UvUninstallCommand doc comment: 'uv pip uninstall' has no -y/--yes flag and is non-interactive by default. Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com> Copilot-Session: 3f16397e-0917-4efb-8d75-566c71ebf9ba
1 parent 4184a12 commit 2f904be

4 files changed

Lines changed: 140 additions & 91 deletions

File tree

src/managers/builtin/commands/uninstall.ts

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -25,7 +25,8 @@ export class PipUninstallCommand extends UninstallCommand {
2525

2626
/**
2727
* UV uninstall command.
28-
* Parsed command: `uv pip uninstall -y --python <path> <package>`
28+
* Parsed command: `uv pip uninstall --python <path> <package>`
29+
* Note: `uv pip uninstall` is non-interactive by default and has no `-y`/`--yes` option.
2930
* Official documentation: https://docs.astral.sh/uv/pip/
3031
*/
3132
export class UvUninstallCommand extends UninstallCommand {

src/test/managers/builtin/commands.unit.test.ts

Lines changed: 72 additions & 46 deletions
Original file line numberDiff line numberDiff line change
@@ -29,83 +29,109 @@ suite('Pip and UV commands', () => {
2929
sinon.restore();
3030
});
3131

32-
test('PipAvailableVersionsCommand executes without error', async () => {
33-
runPythonStub.resolves(JSON.stringify({ versions: ['1.0.0'] }));
34-
const command = new PipAvailableVersionsCommand({ pythonExecutable: 'python', log: mockLog });
35-
36-
await assert.doesNotReject(() => command.execute({ packageName: 'package', pythonVersion: '3.13.1' }));
32+
// Mutation commands have no output to parse; unit coverage is a smoke test that
33+
// execute() resolves. Their concrete command strings are exercised by integration tests.
34+
suite('mutation commands execute without error', () => {
35+
test('PipInstallCommand', async () => {
36+
const command = new PipInstallCommand({ pythonExecutable: 'python', log: mockLog });
37+
await assert.doesNotReject(() => command.execute({ packages: [{ packageName: 'package' }] }));
38+
});
39+
40+
test('UvInstallCommand', async () => {
41+
const command = new UvInstallCommand({ pythonExecutable: 'python', log: mockLog });
42+
await assert.doesNotReject(() => command.execute({ packages: [{ packageName: 'package' }] }));
43+
});
44+
45+
test('PipUninstallCommand', async () => {
46+
const command = new PipUninstallCommand({ pythonExecutable: 'python', log: mockLog });
47+
await assert.doesNotReject(() => command.execute({ packages: [{ packageName: 'package' }] }));
48+
});
49+
50+
test('UvUninstallCommand', async () => {
51+
const command = new UvUninstallCommand({ pythonExecutable: 'python', log: mockLog });
52+
await assert.doesNotReject(() => command.execute({ packages: [{ packageName: 'package' }] }));
53+
});
3754
});
3855

39-
test('UvAvailableVersionsCommand executes without error', async () => {
40-
runUvStub.resolves(JSON.stringify({ versions: ['1.0.0'] }));
41-
const command = new UvAvailableVersionsCommand({ pythonExecutable: 'python', log: mockLog });
42-
43-
await assert.doesNotReject(() => command.execute({ packageName: 'package', pythonVersion: '3.13.1' }));
44-
});
45-
46-
test('PipInstallCommand executes without error', async () => {
47-
const command = new PipInstallCommand({ pythonExecutable: 'python', log: mockLog });
48-
49-
await assert.doesNotReject(() => command.execute({ packages: [{ packageName: 'package' }] }));
50-
});
56+
test('PipAvailableVersionsCommand parses versions and filters prereleases', async () => {
57+
runPythonStub.resolves(JSON.stringify({ versions: ['2.0.0rc1', '1.0.0'] }));
58+
const command = new PipAvailableVersionsCommand({ pythonExecutable: 'python', log: mockLog });
5159

52-
test('UvInstallCommand executes without error', async () => {
53-
const command = new UvInstallCommand({ pythonExecutable: 'python', log: mockLog });
60+
const result = await command.execute({
61+
packageName: 'package',
62+
pythonVersion: '3.13.1',
63+
includePrerelease: false,
64+
});
5465

55-
await assert.doesNotReject(() => command.execute({ packages: [{ packageName: 'package' }] }));
66+
assert.deepStrictEqual(result.map((v) => v.public), ['1.0.0']);
5667
});
5768

58-
test('PipUninstallCommand executes without error', async () => {
59-
const command = new PipUninstallCommand({ pythonExecutable: 'python', log: mockLog });
60-
61-
await assert.doesNotReject(() => command.execute({ packages: [{ packageName: 'package' }] }));
62-
});
69+
test('UvAvailableVersionsCommand parses versions from embedded JSON', async () => {
70+
runUvStub.resolves(`Some preamble\n${JSON.stringify({ versions: ['1.0.0'] })}`);
71+
const command = new UvAvailableVersionsCommand({ pythonExecutable: 'python', log: mockLog });
6372

64-
test('UvUninstallCommand executes without error', async () => {
65-
const command = new UvUninstallCommand({ pythonExecutable: 'python', log: mockLog });
73+
const result = await command.execute({ packageName: 'package', pythonVersion: '3.13.1' });
6674

67-
await assert.doesNotReject(() => command.execute({ packages: [{ packageName: 'package' }] }));
75+
assert.deepStrictEqual(result.map((v) => v.public), ['1.0.0']);
6876
});
6977

70-
test('PipListCommand executes without error', async () => {
71-
runPythonStub.resolves(JSON.stringify([{ name: 'package', version: '1.0.0' }]));
78+
test('PipListCommand parses packages and drops entries missing a version', async () => {
79+
runPythonStub.resolves(JSON.stringify([{ name: 'package', version: '1.0.0' }, { name: 'broken' }]));
7280
const command = new PipListCommand({ pythonExecutable: 'python', log: mockLog });
7381

74-
await assert.doesNotReject(() => command.execute());
82+
const result = await command.execute();
83+
84+
assert.deepStrictEqual(
85+
result.map((p) => ({ name: p.name, version: p.version })),
86+
[{ name: 'package', version: '1.0.0' }],
87+
);
7588
});
7689

77-
test('UvListCommand executes without error', async () => {
90+
test('UvListCommand parses packages from JSON', async () => {
7891
runUvStub.resolves(JSON.stringify([{ name: 'package', version: '1.0.0' }]));
7992
const command = new UvListCommand({ pythonExecutable: 'python', log: mockLog });
8093

81-
await assert.doesNotReject(() => command.execute());
94+
const result = await command.execute();
95+
96+
assert.deepStrictEqual(
97+
result.map((p) => ({ name: p.name, version: p.version })),
98+
[{ name: 'package', version: '1.0.0' }],
99+
);
82100
});
83101

84-
test('PipListDirectNamesCommand executes without error', async () => {
85-
runPythonStub.resolves(JSON.stringify([{ name: 'package', version: '1.0.0' }]));
102+
test('PipListDirectNamesCommand returns normalized names', async () => {
103+
runPythonStub.resolves(JSON.stringify([{ name: 'Flask_Thing', version: '1.0.0' }]));
86104
const command = new PipListDirectNamesCommand({ pythonExecutable: 'python', log: mockLog });
87105

88-
await assert.doesNotReject(() => command.execute());
106+
const result = await command.execute();
107+
108+
assert.deepStrictEqual([...result], ['flask-thing']);
89109
});
90110

91-
test('UvListDirectNamesCommand executes without error', async () => {
92-
runUvStub.resolves('package 1.0.0');
111+
test('UvListDirectNamesCommand keeps top-level packages and skips indented dependencies', async () => {
112+
runUvStub.resolves(['Flask_Thing 1.0.0', '├── dependency 2.0.0', '└── another-dep 3.0.0'].join('\n'));
93113
const command = new UvListDirectNamesCommand({ pythonExecutable: 'python', log: mockLog });
94114

95-
await assert.doesNotReject(() => command.execute());
115+
const result = await command.execute();
116+
117+
assert.deepStrictEqual([...result], ['flask-thing']);
96118
});
97119

98-
test('PipVersionCommand executes without error', async () => {
99-
runPythonStub.resolves('pip 24.0 from site-packages');
120+
test('PipVersionCommand parses the version from pip --version output', async () => {
121+
runPythonStub.resolves('pip 24.0 from /site-packages/pip (python 3.13)');
100122
const command = new PipVersionCommand({ pythonExecutable: 'python', log: mockLog });
101123

102-
await assert.doesNotReject(() => command.execute());
124+
const result = await command.execute();
125+
126+
assert.strictEqual(result?.public, '24.0');
103127
});
104128

105-
test('UvVersionCommand executes without error', async () => {
106-
runUvStub.resolves('uv 0.4.20');
129+
test('UvVersionCommand parses the version from uv --version output', async () => {
130+
runUvStub.resolves('uv 0.4.20 (abcdef 2024-01-01)');
107131
const command = new UvVersionCommand({ pythonExecutable: 'python', log: mockLog });
108132

109-
await assert.doesNotReject(() => command.execute());
133+
const result = await command.execute();
134+
135+
assert.strictEqual(result?.public, '0.4.20');
110136
});
111-
});
137+
});

src/test/managers/conda/commands.unit.test.ts

Lines changed: 37 additions & 26 deletions
Original file line numberDiff line numberDiff line change
@@ -26,48 +26,59 @@ suite('Conda commands', () => {
2626
sinon.restore();
2727
});
2828

29-
test('CondaAvailableVersionsCommand executes without error', async () => {
30-
runCondaStub.resolves(JSON.stringify({ package: [{ version: '1.0.0' }] }));
31-
const command = new CondaAvailableVersionsCommand({ pythonExecutable: 'conda', log: mockLog });
32-
33-
await assert.doesNotReject(() => command.execute({ packageName: 'package', pythonVersion: '' }));
34-
});
35-
36-
test('CondaInstallCommand executes without error', async () => {
37-
const command = new CondaInstallCommand({
38-
pythonExecutable: 'conda',
39-
condaEnvironmentPath: 'environment',
40-
log: mockLog,
29+
// Mutation commands have no output to parse; unit coverage is a smoke test that
30+
// execute() resolves. Their concrete command strings are exercised by integration tests.
31+
suite('mutation commands execute without error', () => {
32+
test('CondaInstallCommand', async () => {
33+
const command = new CondaInstallCommand({
34+
pythonExecutable: 'conda',
35+
condaEnvironmentPath: 'environment',
36+
log: mockLog,
37+
});
38+
await assert.doesNotReject(() => command.execute({ packages: [{ packageName: 'package' }] }));
4139
});
4240

43-
await assert.doesNotReject(() => command.execute({ packages: [{ packageName: 'package' }] }));
41+
test('CondaUninstallCommand', async () => {
42+
const command = new CondaUninstallCommand({
43+
pythonExecutable: 'conda',
44+
condaEnvironmentPath: 'environment',
45+
log: mockLog,
46+
});
47+
await assert.doesNotReject(() => command.execute({ packages: [{ packageName: 'package' }] }));
48+
});
4449
});
4550

46-
test('CondaUninstallCommand executes without error', async () => {
47-
const command = new CondaUninstallCommand({
48-
pythonExecutable: 'conda',
49-
condaEnvironmentPath: 'environment',
50-
log: mockLog,
51-
});
51+
test('CondaAvailableVersionsCommand parses versions keyed by package name', async () => {
52+
runCondaStub.resolves(JSON.stringify({ package: [{ version: '1.0.0' }, { version: '2.0.0' }] }));
53+
const command = new CondaAvailableVersionsCommand({ pythonExecutable: 'conda', log: mockLog });
54+
55+
const result = await command.execute({ packageName: 'package', pythonVersion: '' });
5256

53-
await assert.doesNotReject(() => command.execute({ packages: [{ packageName: 'package' }] }));
57+
assert.deepStrictEqual(result.map((v) => v.public), ['1.0.0', '2.0.0']);
5458
});
5559

56-
test('CondaListCommand executes without error', async () => {
57-
runCondaStub.resolves(JSON.stringify([{ name: 'package', version: '1.0.0' }]));
60+
test('CondaListCommand parses packages and drops entries missing a version', async () => {
61+
runCondaStub.resolves(JSON.stringify([{ name: 'package', version: '1.0.0' }, { name: 'broken' }]));
5862
const command = new CondaListCommand({
5963
pythonExecutable: 'conda',
6064
condaEnvironmentPath: 'environment',
6165
log: mockLog,
6266
});
6367

64-
await assert.doesNotReject(() => command.execute());
68+
const result = await command.execute();
69+
70+
assert.deepStrictEqual(
71+
result.map((p) => ({ name: p.name, version: p.version })),
72+
[{ name: 'package', version: '1.0.0' }],
73+
);
6574
});
6675

67-
test('CondaVersionCommand executes without error', async () => {
76+
test('CondaVersionCommand parses the version from conda --version output', async () => {
6877
runCondaStub.resolves('conda 24.1.2');
6978
const command = new CondaVersionCommand({ pythonExecutable: 'conda', log: mockLog });
7079

71-
await assert.doesNotReject(() => command.execute());
80+
const result = await command.execute();
81+
82+
assert.strictEqual(result?.public, '24.1.2');
7283
});
73-
});
84+
});

src/test/managers/poetry/commands.unit.test.ts

Lines changed: 29 additions & 18 deletions
Original file line numberDiff line numberDiff line change
@@ -28,35 +28,46 @@ suite('Poetry commands', () => {
2828
sinon.restore();
2929
});
3030

31-
test('PoetryAddCommand executes without error', async () => {
32-
const command = new PoetryAddCommand({ pythonExecutable: 'poetry', cwd: 'project', log: mockLog });
31+
// Mutation commands have no output to parse; unit coverage is a smoke test that
32+
// execute() resolves. Their concrete command strings are exercised by integration tests.
33+
suite('mutation commands execute without error', () => {
34+
test('PoetryAddCommand', async () => {
35+
const command = new PoetryAddCommand({ pythonExecutable: 'poetry', cwd: 'project', log: mockLog });
36+
await assert.doesNotReject(() => command.execute({ packages: [{ packageName: 'package' }] }));
37+
});
3338

34-
await assert.doesNotReject(() => command.execute({ packages: [{ packageName: 'package' }] }));
39+
test('PoetryRemoveCommand', async () => {
40+
const command = new PoetryRemoveCommand({ pythonExecutable: 'poetry', cwd: 'project', log: mockLog });
41+
await assert.doesNotReject(() => command.execute({ packages: [{ packageName: 'package' }] }));
42+
});
3543
});
3644

37-
test('PoetryRemoveCommand executes without error', async () => {
38-
const command = new PoetryRemoveCommand({ pythonExecutable: 'poetry', cwd: 'project', log: mockLog });
39-
40-
await assert.doesNotReject(() => command.execute({ packages: [{ packageName: 'package' }] }));
41-
});
42-
43-
test('PoetryShowCommand executes without error', async () => {
44-
runPoetryStub.resolves('package 1.0.0 description');
45+
test('PoetryShowCommand parses name, version and description', async () => {
46+
runPoetryStub.resolves(['requests 2.31.0 Python HTTP for Humans.', ''].join('\n'));
4547
const command = new PoetryShowCommand({ pythonExecutable: 'poetry', cwd: 'project', log: mockLog });
4648

47-
await assert.doesNotReject(() => command.execute());
49+
const result = await command.execute();
50+
51+
assert.deepStrictEqual(
52+
result.map((p) => ({ name: p.name, version: p.version, description: p.description })),
53+
[{ name: 'requests', version: '2.31.0', description: '2.31.0 - Python HTTP for Humans.' }],
54+
);
4855
});
4956

50-
test('PoetryShowTopLevelCommand executes without error', async () => {
51-
runPoetryStub.resolves('package');
57+
test('PoetryShowTopLevelCommand returns normalized names', async () => {
58+
runPoetryStub.resolves(['Flask_Thing', 'requests'].join('\n'));
5259
const command = new PoetryShowTopLevelCommand({ pythonExecutable: 'poetry', cwd: 'project', log: mockLog });
5360

54-
await assert.doesNotReject(() => command.execute());
61+
const result = await command.execute();
62+
63+
assert.deepStrictEqual([...result], ['flask-thing', 'requests']);
5564
});
5665

57-
test('PoetryVersionCommand executes without error', async () => {
66+
test('PoetryVersionCommand parses the poetry version', async () => {
5867
const command = new PoetryVersionCommand({ pythonExecutable: 'poetry', log: mockLog });
5968

60-
await assert.doesNotReject(() => command.execute());
69+
const result = await command.execute();
70+
71+
assert.strictEqual(result?.public, '1.8.2');
6172
});
62-
});
73+
});

0 commit comments

Comments
 (0)