Skip to content

Commit 01575b1

Browse files
edvilmeCopilot
andcommitted
test: use flask (universal + transitive deps) for package-manager roundtrip
Switch the roundtrip integration test package from cowsay to flask. cowsay is not on conda's default channels, so conda's managePackages silently swallowed the failed install and the post-install assertion tripped on CI. flask is available across all managers (incl. conda's default main channel) and pulls in transitive deps (jinja2, werkzeug, click, ...), letting the test verify direct-vs-transitive classification. Install is a hard assertion again (no best-effort skip). The enriched package list is read from refreshPackages' return value, since getPackages({skipCache}) re-lists without isTransitive enrichment. The transitive-dependency assertion is conditional on the manager classifying transitivity at all (conda leaves isTransitive undefined). Bump per-test timeout to 5m to accommodate conda solving flask + deps. Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com> Copilot-Session: 3f16397e-0917-4efb-8d75-566c71ebf9ba
1 parent fb1bd18 commit 01575b1

1 file changed

Lines changed: 53 additions & 51 deletions

File tree

src/test/integration/packageManagerRoundtrip.integration.test.ts

Lines changed: 53 additions & 51 deletions
Original file line numberDiff line numberDiff line change
@@ -13,28 +13,28 @@
1313
* ROUNDTRIP (per manager, all via the public API):
1414
* 1. list packages -> getPackages (baseline; test package absent)
1515
* 2. install test package -> managePackages({ install })
16-
* 3. list again -> getPackages (test package now present)
17-
* 4. list direct packages -> getPackages filtered by !isTransitive
16+
* 3. refresh + list -> refreshPackages (enriched, test package now present)
17+
* 4. direct vs transitive -> installed package is direct; its deps are transitive
1818
* 4b. available versions -> getPackageAvailableVersions (when supported)
1919
* 5. uninstall test package -> managePackages({ uninstall })
2020
* 6. list again -> getPackages (test package absent again)
2121
*
2222
* NOTES:
23+
* - The test package (flask) is chosen because it is available from every common
24+
* manager's default sources (PyPI for pip/uv/poetry/pipenv, and conda's default
25+
* `main` channel for conda) and pulls in real transitive dependencies (jinja2,
26+
* werkzeug, click, ...). That lets the roundtrip exercise a genuine install on
27+
* every manager AND verify the direct-vs-transitive classification.
2328
* - "Available versions" is exercised via getPackageAvailableVersions when the
2429
* running PythonEnvironmentApi surfaces it. That getter was added to the public
2530
* API; the call is capability-guarded so this test compiles and runs regardless
2631
* of whether the active API build includes it, and gracefully skips the check
2732
* for managers that resolve to `undefined` (no version listing support).
28-
* - The install step is best-effort: a manager-agnostic test cannot guarantee the
29-
* test package is installable from every manager's configured sources (e.g. conda
30-
* channels may not carry a PyPI-only package, and some managers surface install
31-
* failures as notifications rather than throwing). When the package does not appear
32-
* after install, that manager is skipped rather than failed; managers that can
33-
* install it still run the full lifecycle with assertions.
34-
* - Direct (non-transitive) packages are derived from Package.isTransitive,
35-
* which IS part of the public API.
36-
* - The test package (cowsay) is small and dependency-free so a successful
37-
* install shows up as a direct package with no transitive fan-out.
33+
* - Transitive classification comes from Package.isTransitive, which is populated by
34+
* the enriched refreshPackages result (getPackages({skipCache}) re-lists WITHOUT
35+
* enrichment). Managers that implement direct-name detection (pip/uv/poetry) mark
36+
* dependencies isTransitive=true; managers that don't (conda) leave it undefined,
37+
* so the transitive-dependency assertion is conditional on the manager classifying it.
3838
*/
3939

4040
import * as assert from 'assert';
@@ -44,8 +44,13 @@ import { normalizePackageName } from '../../managers/builtin/utils';
4444
import { ENVS_EXTENSION_ID } from '../constants';
4545
import { waitForCondition } from '../testUtils';
4646

47-
/** Small, dependency-free package used for the install/uninstall roundtrip. */
48-
const TEST_PACKAGE = 'cowsay';
47+
/**
48+
* Package used for the install/uninstall roundtrip. flask is available from every
49+
* common manager's default source (PyPI and conda's default `main` channel) and pulls
50+
* in transitive dependencies, so the roundtrip can verify both installation and the
51+
* direct-vs-transitive classification on every manager.
52+
*/
53+
const TEST_PACKAGE = 'flask';
4954

5055
/** True when a package with the given (normalized) name is in the list. */
5156
function hasPackage(packages: Package[] | undefined, name: string): boolean {
@@ -66,16 +71,14 @@ type AvailableVersionsCapable = {
6671
};
6772

6873
/**
69-
* Runs the full lifecycle roundtrip for a single environment/manager.
70-
* Returns `true` when the full lifecycle was exercised (install succeeded and all
71-
* assertions ran), or `false` when the manager could not install the test package
72-
* from its configured sources and the roundtrip was skipped.
74+
* Runs the full lifecycle roundtrip for a single environment/manager. Throws (via
75+
* assertion) on any failure; returns normally when the full lifecycle succeeded.
7376
*/
74-
async function runRoundtrip(api: PythonEnvironmentApi, env: PythonEnvironment, managerId: string): Promise<boolean> {
77+
async function runRoundtrip(api: PythonEnvironmentApi, env: PythonEnvironment, managerId: string): Promise<void> {
7578
const baseline = await api.getPackages(env, { skipCache: true });
7679
if (baseline === undefined) {
7780
// No usable package manager for this environment.
78-
return false;
81+
return;
7982
}
8083

8184
// Avoid clobbering a pre-existing install of the test package.
@@ -86,34 +89,38 @@ async function runRoundtrip(api: PythonEnvironmentApi, env: PythonEnvironment, m
8689

8790
let installed = false;
8891
try {
89-
// 2. Install (best-effort). Some managers cannot install the test package from
90-
// their configured sources, and some surface that failure as a notification
91-
// rather than throwing, so success is verified by the next list rather than assumed.
92+
// 2. Install.
9293
await api.managePackages(env, { install: [TEST_PACKAGE] });
93-
94-
// 3. List again -> present?
95-
await api.refreshPackages(env);
96-
const afterInstall = await api.getPackages(env, { skipCache: true });
97-
98-
// If the package did not appear, this manager cannot install it from its
99-
// configured sources (e.g. conda channels lacking a PyPI-only package). Treat
100-
// that as a graceful skip rather than a hard failure so the test stays
101-
// manager-agnostic; managers that can install it still assert the full lifecycle.
102-
if (!hasPackage(afterInstall, TEST_PACKAGE)) {
103-
console.log(
104-
`[${managerId}] ${TEST_PACKAGE} could not be installed from this manager's configured sources; skipping roundtrip`,
105-
);
106-
return false;
107-
}
10894
installed = true;
10995

110-
// 4. Direct (non-transitive) packages should include the directly-installed package.
111-
const direct = (afterInstall ?? []).filter((p) => p.isTransitive !== true);
112-
assert.ok(
113-
hasPackage(direct, TEST_PACKAGE),
96+
// 3. Refresh and read the ENRICHED package list from the refresh result:
97+
// getPackages({skipCache}) re-lists without transitive enrichment, so the
98+
// refreshPackages return value is what carries Package.isTransitive.
99+
const afterInstall = (await api.refreshPackages(env)) ?? (await api.getPackages(env, { skipCache: true }));
100+
assert.ok(hasPackage(afterInstall, TEST_PACKAGE), `[${managerId}] ${TEST_PACKAGE} should be installed`);
101+
102+
// 4. The installed package is a direct (non-transitive) dependency.
103+
const installedPkg = (afterInstall ?? []).find(
104+
(p) => normalizePackageName(p.name) === normalizePackageName(TEST_PACKAGE),
105+
);
106+
assert.notStrictEqual(
107+
installedPkg?.isTransitive,
108+
true,
114109
`[${managerId}] ${TEST_PACKAGE} should be reported as a direct (non-transitive) package`,
115110
);
116111

112+
// 4a. Transitive-dependency detection. flask pulls in dependencies (jinja2,
113+
// werkzeug, ...). Managers that classify direct vs transitive (pip/uv/poetry)
114+
// mark those deps isTransitive=true; managers that don't (conda) leave it
115+
// undefined, so this assertion is conditional on the manager classifying at all.
116+
const classifiesTransitivity = (afterInstall ?? []).some((p) => p.isTransitive !== undefined);
117+
if (classifiesTransitivity) {
118+
assert.ok(
119+
(afterInstall ?? []).some((p) => p.isTransitive === true),
120+
`[${managerId}] expected at least one transitive dependency of ${TEST_PACKAGE} to be detected`,
121+
);
122+
}
123+
117124
// 4b. Available versions (optional API capability). Only asserted when the
118125
// running API surfaces the getter and the manager supports version listing.
119126
const versionsApi = api as unknown as AvailableVersionsCapable;
@@ -135,7 +142,6 @@ async function runRoundtrip(api: PythonEnvironmentApi, env: PythonEnvironment, m
135142
await api.refreshPackages(env);
136143
const afterUninstall = await api.getPackages(env, { skipCache: true });
137144
assert.ok(!hasPackage(afterUninstall, TEST_PACKAGE), `[${managerId}] ${TEST_PACKAGE} should be uninstalled`);
138-
return true;
139145
} finally {
140146
// Best-effort cleanup so a mid-roundtrip failure never leaves the env dirty.
141147
if (installed) {
@@ -149,7 +155,7 @@ async function runRoundtrip(api: PythonEnvironmentApi, env: PythonEnvironment, m
149155
}
150156

151157
suite('Integration: Package Manager Roundtrip', function () {
152-
this.timeout(180_000); // Install/uninstall across multiple managers can be slow.
158+
this.timeout(300_000); // Install/uninstall across multiple managers can be slow (conda solving flask + deps).
153159

154160
let api: PythonEnvironmentApi;
155161

@@ -222,13 +228,9 @@ suite('Integration: Package Manager Roundtrip', function () {
222228
continue;
223229
}
224230

225-
const exercisedManager = await runRoundtrip(api, env, managerId);
226-
if (exercisedManager) {
227-
exercised++;
228-
console.log(`[${managerId}] roundtrip passed (${env.displayName})`);
229-
} else {
230-
console.log(`[${managerId}] roundtrip skipped (${env.displayName})`);
231-
}
231+
await runRoundtrip(api, env, managerId);
232+
exercised++;
233+
console.log(`[${managerId}] roundtrip passed (${env.displayName})`);
232234
} catch (e) {
233235
failures.push(`[${managerId}] ${e instanceof Error ? e.message : String(e)}`);
234236
}

0 commit comments

Comments
 (0)