@@ -6,6 +6,96 @@ import * as sinon from 'sinon';
66import * as persistentState from '../../../common/persistentState' ;
77import { getVenvForWorkspace , VENV_WORKSPACE_KEY } from '../../../managers/builtin/venvUtils' ;
88
9+ /**
10+ * Helper that replicates the VIRTUAL_ENV subpath check from getVenvForWorkspace
11+ * using a specific path module, allowing cross-platform verification.
12+ */
13+ function isVenvInsideWorkspace (
14+ fsPath : string ,
15+ virtualEnv : string ,
16+ pathModule : typeof path . posix | typeof path . win32 ,
17+ ) : boolean {
18+ const rel = pathModule . relative ( fsPath , virtualEnv ) ;
19+ return rel === '' || ( ! rel . startsWith ( '..' ) && ! pathModule . isAbsolute ( rel ) ) ;
20+ }
21+
22+ suite ( 'VIRTUAL_ENV subpath check - cross-platform' , ( ) => {
23+ suite ( 'POSIX paths' , ( ) => {
24+ const p = path . posix ;
25+
26+ test ( 'venv inside workspace should match' , ( ) => {
27+ assert . strictEqual ( isVenvInsideWorkspace ( '/proj/app' , '/proj/app/.venv' , p ) , true ) ;
28+ } ) ;
29+
30+ test ( 'venv deeply nested inside workspace should match' , ( ) => {
31+ assert . strictEqual ( isVenvInsideWorkspace ( '/proj/app' , '/proj/app/sub/dir/.venv' , p ) , true ) ;
32+ } ) ;
33+
34+ test ( 'venv equal to workspace should match' , ( ) => {
35+ assert . strictEqual ( isVenvInsideWorkspace ( '/proj/app' , '/proj/app' , p ) , true ) ;
36+ } ) ;
37+
38+ test ( 'sibling with shared prefix should NOT match' , ( ) => {
39+ assert . strictEqual ( isVenvInsideWorkspace ( '/proj/app' , '/proj/app2/.venv' , p ) , false ) ;
40+ } ) ;
41+
42+ test ( 'venv in completely different location should NOT match' , ( ) => {
43+ assert . strictEqual ( isVenvInsideWorkspace ( '/proj/app' , '/other/place/.venv' , p ) , false ) ;
44+ } ) ;
45+
46+ test ( 'parent directory should NOT match' , ( ) => {
47+ assert . strictEqual ( isVenvInsideWorkspace ( '/proj/app' , '/proj/.venv' , p ) , false ) ;
48+ } ) ;
49+ } ) ;
50+
51+ suite ( 'Windows paths' , ( ) => {
52+ const p = path . win32 ;
53+
54+ test ( 'venv inside workspace should match' , ( ) => {
55+ assert . strictEqual ( isVenvInsideWorkspace ( 'C:\\proj\\app' , 'C:\\proj\\app\\.venv' , p ) , true ) ;
56+ } ) ;
57+
58+ test ( 'venv deeply nested inside workspace should match' , ( ) => {
59+ assert . strictEqual ( isVenvInsideWorkspace ( 'C:\\proj\\app' , 'C:\\proj\\app\\sub\\dir\\.venv' , p ) , true ) ;
60+ } ) ;
61+
62+ test ( 'venv equal to workspace should match' , ( ) => {
63+ assert . strictEqual ( isVenvInsideWorkspace ( 'C:\\proj\\app' , 'C:\\proj\\app' , p ) , true ) ;
64+ } ) ;
65+
66+ test ( 'sibling with shared prefix should NOT match' , ( ) => {
67+ assert . strictEqual ( isVenvInsideWorkspace ( 'C:\\proj\\app' , 'C:\\proj\\app2\\.venv' , p ) , false ) ;
68+ } ) ;
69+
70+ test ( 'venv on a different drive should NOT match' , ( ) => {
71+ assert . strictEqual ( isVenvInsideWorkspace ( 'C:\\proj\\app' , 'D:\\proj\\app\\.venv' , p ) , false ) ;
72+ } ) ;
73+
74+ test ( 'parent directory should NOT match' , ( ) => {
75+ assert . strictEqual ( isVenvInsideWorkspace ( 'C:\\proj\\app' , 'C:\\proj\\.venv' , p ) , false ) ;
76+ } ) ;
77+
78+ test ( 'case-insensitive drive letters should match' , ( ) => {
79+ // path.win32.relative handles case-insensitive drive letters
80+ assert . strictEqual ( isVenvInsideWorkspace ( 'c:\\proj\\app' , 'C:\\proj\\app\\.venv' , p ) , true ) ;
81+ } ) ;
82+
83+ test ( 'UNC path inside workspace should match' , ( ) => {
84+ assert . strictEqual (
85+ isVenvInsideWorkspace ( '\\\\server\\share\\proj' , '\\\\server\\share\\proj\\.venv' , p ) ,
86+ true ,
87+ ) ;
88+ } ) ;
89+
90+ test ( 'UNC path sibling should NOT match' , ( ) => {
91+ assert . strictEqual (
92+ isVenvInsideWorkspace ( '\\\\server\\share\\proj' , '\\\\server\\share\\proj2\\.venv' , p ) ,
93+ false ,
94+ ) ;
95+ } ) ;
96+ } ) ;
97+ } ) ;
98+
999suite ( 'getVenvForWorkspace' , ( ) => {
10100 let mockState : {
11101 get : sinon . SinonStub ;
@@ -163,4 +253,41 @@ suite('getVenvForWorkspace', () => {
163253
164254 assert . ok ( ! mockState . set . called , 'Should not call set when there is no stale entry to clear' ) ;
165255 } ) ;
256+
257+ test ( 'should fall back to VIRTUAL_ENV when state.get rejects' , async ( ) => {
258+ const workspacePath = path . join ( tmpDir , 'projectA' ) ;
259+ const virtualEnvPath = path . join ( workspacePath , '.venv' ) ;
260+ await fse . ensureDir ( virtualEnvPath ) ;
261+ process . env . VIRTUAL_ENV = virtualEnvPath ;
262+
263+ mockState . get . withArgs ( VENV_WORKSPACE_KEY ) . rejects ( new Error ( 'storage corrupted' ) ) ;
264+
265+ const result = await getVenvForWorkspace ( workspacePath ) ;
266+ assert . strictEqual ( result , virtualEnvPath , 'Should fall back to VIRTUAL_ENV when state.get rejects' ) ;
267+ } ) ;
268+
269+ test ( 'should fall back to VIRTUAL_ENV when getWorkspacePersistentState rejects' , async ( ) => {
270+ const workspacePath = path . join ( tmpDir , 'projectA' ) ;
271+ const virtualEnvPath = path . join ( workspacePath , '.venv' ) ;
272+ await fse . ensureDir ( virtualEnvPath ) ;
273+ process . env . VIRTUAL_ENV = virtualEnvPath ;
274+
275+ getWorkspacePersistentStateStub . rejects ( new Error ( 'persistent state unavailable' ) ) ;
276+
277+ const result = await getVenvForWorkspace ( workspacePath ) ;
278+ assert . strictEqual ( result , virtualEnvPath , 'Should fall back to VIRTUAL_ENV when persistent state fails' ) ;
279+ } ) ;
280+
281+ test ( 'should NOT use VIRTUAL_ENV for sibling path with shared prefix' , async ( ) => {
282+ const projectA = path . join ( tmpDir , 'app' ) ;
283+ const siblingVenv = path . join ( tmpDir , 'app2' , '.venv' ) ;
284+ await fse . ensureDir ( projectA ) ;
285+ await fse . ensureDir ( siblingVenv ) ;
286+ process . env . VIRTUAL_ENV = siblingVenv ;
287+
288+ mockState . get . withArgs ( VENV_WORKSPACE_KEY ) . resolves ( undefined ) ;
289+
290+ const result = await getVenvForWorkspace ( projectA ) ;
291+ assert . strictEqual ( result , undefined , 'Should NOT match sibling path with shared prefix' ) ;
292+ } ) ;
166293} ) ;
0 commit comments