@@ -3,16 +3,20 @@ import path from 'node:path';
33import * as sinon from 'sinon' ;
44import { EnvironmentManager , PythonEnvironment , PythonEnvironmentApi , PythonEnvironmentInfo } from '../../../api' ;
55import * as childProcessApis from '../../../common/childProcess.apis' ;
6+ import * as persistentState from '../../../common/persistentState' ;
67import * as pathUtils from '../../../common/utils/pathUtils' ;
78import * as platformUtils from '../../../common/utils/platformUtils' ;
89import { NativeEnvInfo } from '../../../managers/common/nativePythonFinder' ;
910import * as utils from '../../../managers/common/utils' ;
1011import {
12+ clearPoetryCache ,
1113 getDefaultPoetryCacheDir ,
1214 getDefaultPoetryVirtualenvsPath ,
1315 getPoetryVersion ,
16+ getPoetryVirtualenvsPath ,
1417 isPoetryVirtualenvsInProject ,
1518 nativeToPythonEnv ,
19+ POETRY_VIRTUALENVS_PATH_KEY ,
1620} from '../../../managers/poetry/poetryUtils' ;
1721
1822suite ( 'isPoetryVirtualenvsInProject' , ( ) => {
@@ -365,3 +369,181 @@ suite('getDefaultPoetryVirtualenvsPath', () => {
365369 assert . strictEqual ( result , undefined ) ;
366370 } ) ;
367371} ) ;
372+
373+ suite ( 'getPoetryVirtualenvsPath - {cache-dir} placeholder resolution' , ( ) => {
374+ let execProcessStub : sinon . SinonStub ;
375+ let isWindowsStub : sinon . SinonStub ;
376+ let isMacStub : sinon . SinonStub ;
377+ let getUserHomeDirStub : sinon . SinonStub ;
378+ let getWorkspacePersistentStateStub : sinon . SinonStub ;
379+ let mockState : { get : sinon . SinonStub ; set : sinon . SinonStub } ;
380+
381+ setup ( async ( ) => {
382+ execProcessStub = sinon . stub ( childProcessApis , 'execProcess' ) ;
383+ isWindowsStub = sinon . stub ( platformUtils , 'isWindows' ) ;
384+ isMacStub = sinon . stub ( platformUtils , 'isMac' ) ;
385+ getUserHomeDirStub = sinon . stub ( pathUtils , 'getUserHomeDir' ) ;
386+
387+ // Create a mock state object to track persistence
388+ mockState = {
389+ get : sinon . stub ( ) ,
390+ set : sinon . stub ( ) . resolves ( ) ,
391+ } ;
392+ getWorkspacePersistentStateStub = sinon . stub ( persistentState , 'getWorkspacePersistentState' ) ;
393+ getWorkspacePersistentStateStub . resolves ( mockState ) ;
394+
395+ // Clear Poetry cache before each test
396+ await clearPoetryCache ( ) ;
397+ } ) ;
398+
399+ teardown ( ( ) => {
400+ sinon . restore ( ) ;
401+ } ) ;
402+
403+ test ( 'resolves {cache-dir} placeholder when poetry config cache-dir succeeds' , async ( ) => {
404+ // First call: virtualenvs.path returns a path with {cache-dir}
405+ execProcessStub . onFirstCall ( ) . resolves ( { stdout : '{cache-dir}/virtualenvs\n' , stderr : '' } ) ;
406+ // Second call: cache-dir config returns the actual path
407+ execProcessStub . onSecondCall ( ) . resolves ( { stdout : '/home/test/.cache/pypoetry\n' , stderr : '' } ) ;
408+
409+ const result = await getPoetryVirtualenvsPath ( '/usr/bin/poetry' ) ;
410+
411+ assert . strictEqual ( result , path . join ( '/home/test/.cache/pypoetry' , 'virtualenvs' ) ) ;
412+ // Verify the resolved path was persisted
413+ assert . ok (
414+ mockState . set . calledWith (
415+ POETRY_VIRTUALENVS_PATH_KEY ,
416+ path . join ( '/home/test/.cache/pypoetry' , 'virtualenvs' ) ,
417+ ) ,
418+ ) ;
419+ } ) ;
420+
421+ test ( 'falls back to platform default when poetry config cache-dir fails' , async ( ) => {
422+ isWindowsStub . returns ( false ) ;
423+ isMacStub . returns ( false ) ;
424+ getUserHomeDirStub . returns ( '/home/test' ) ;
425+
426+ // First call: virtualenvs.path returns a path with {cache-dir}
427+ execProcessStub . onFirstCall ( ) . resolves ( { stdout : '{cache-dir}/virtualenvs\n' , stderr : '' } ) ;
428+ // Second call: cache-dir config fails
429+ execProcessStub . onSecondCall ( ) . rejects ( new Error ( 'Command failed' ) ) ;
430+
431+ const result = await getPoetryVirtualenvsPath ( '/usr/bin/poetry' ) ;
432+
433+ // Should fall back to platform default cache dir
434+ const expectedPath = path . join ( '/home/test' , '.cache' , 'pypoetry' , 'virtualenvs' ) ;
435+ assert . strictEqual ( result , expectedPath ) ;
436+ // The resolved path should still be persisted
437+ assert . ok ( mockState . set . calledWith ( POETRY_VIRTUALENVS_PATH_KEY , expectedPath ) ) ;
438+ } ) ;
439+
440+ test ( 'falls back to platform default when poetry config cache-dir returns non-absolute path' , async ( ) => {
441+ isWindowsStub . returns ( false ) ;
442+ isMacStub . returns ( false ) ;
443+ getUserHomeDirStub . returns ( '/home/test' ) ;
444+
445+ // First call: virtualenvs.path returns a path with {cache-dir}
446+ execProcessStub . onFirstCall ( ) . resolves ( { stdout : '{cache-dir}/virtualenvs\n' , stderr : '' } ) ;
447+ // Second call: cache-dir returns a relative path (invalid)
448+ execProcessStub . onSecondCall ( ) . resolves ( { stdout : 'relative/path\n' , stderr : '' } ) ;
449+
450+ const result = await getPoetryVirtualenvsPath ( '/usr/bin/poetry' ) ;
451+
452+ // Should fall back to platform default cache dir
453+ const expectedPath = path . join ( '/home/test' , '.cache' , 'pypoetry' , 'virtualenvs' ) ;
454+ assert . strictEqual ( result , expectedPath ) ;
455+ } ) ;
456+
457+ test ( 'does not persist path when placeholder cannot be resolved and no platform default' , async ( ) => {
458+ isWindowsStub . returns ( false ) ;
459+ isMacStub . returns ( false ) ;
460+ getUserHomeDirStub . returns ( undefined ) ; // No home dir available
461+
462+ // First call: virtualenvs.path returns a path with {cache-dir}
463+ execProcessStub . onFirstCall ( ) . resolves ( { stdout : '{cache-dir}/virtualenvs\n' , stderr : '' } ) ;
464+ // Second call: cache-dir config fails
465+ execProcessStub . onSecondCall ( ) . rejects ( new Error ( 'Command failed' ) ) ;
466+
467+ const result = await getPoetryVirtualenvsPath ( '/usr/bin/poetry' ) ;
468+
469+ // Should fall back to platform default (which returns undefined when home is not available)
470+ assert . strictEqual ( result , undefined ) ;
471+ // Path should NOT be persisted when unresolved
472+ assert . ok ( ! mockState . set . calledWith ( POETRY_VIRTUALENVS_PATH_KEY , sinon . match . any ) ) ;
473+ } ) ;
474+
475+ test ( 'handles virtualenvs.path without {cache-dir} placeholder (absolute path)' , async ( ) => {
476+ // virtualenvs.path returns an absolute path directly
477+ execProcessStub . onFirstCall ( ) . resolves ( { stdout : '/custom/virtualenvs/path\n' , stderr : '' } ) ;
478+
479+ const result = await getPoetryVirtualenvsPath ( '/usr/bin/poetry' ) ;
480+
481+ assert . strictEqual ( result , '/custom/virtualenvs/path' ) ;
482+ // Should be persisted
483+ assert . ok ( mockState . set . calledWith ( POETRY_VIRTUALENVS_PATH_KEY , '/custom/virtualenvs/path' ) ) ;
484+ } ) ;
485+
486+ test ( 'falls back to platform default when virtualenvs.path returns non-absolute path without placeholder' , async ( ) => {
487+ isWindowsStub . returns ( false ) ;
488+ isMacStub . returns ( false ) ;
489+ getUserHomeDirStub . returns ( '/home/test' ) ;
490+
491+ // virtualenvs.path returns a relative path (not valid)
492+ execProcessStub . onFirstCall ( ) . resolves ( { stdout : 'relative/path\n' , stderr : '' } ) ;
493+
494+ const result = await getPoetryVirtualenvsPath ( '/usr/bin/poetry' ) ;
495+
496+ // Should fall back to platform default
497+ const expectedPath = path . join ( '/home/test' , '.cache' , 'pypoetry' , 'virtualenvs' ) ;
498+ assert . strictEqual ( result , expectedPath ) ;
499+ } ) ;
500+
501+ test ( 'uses cached value from persistent state' , async ( ) => {
502+ mockState . get . resolves ( '/cached/virtualenvs/path' ) ;
503+
504+ const result = await getPoetryVirtualenvsPath ( '/usr/bin/poetry' ) ;
505+
506+ assert . strictEqual ( result , '/cached/virtualenvs/path' ) ;
507+ // Should not call exec since we have a cached value
508+ assert . ok ( ! execProcessStub . called ) ;
509+ } ) ;
510+
511+ test ( 'handles virtualenvs.path config command failure' , async ( ) => {
512+ isWindowsStub . returns ( false ) ;
513+ isMacStub . returns ( false ) ;
514+ getUserHomeDirStub . returns ( '/home/test' ) ;
515+
516+ // virtualenvs.path config fails
517+ execProcessStub . onFirstCall ( ) . rejects ( new Error ( 'Config command failed' ) ) ;
518+
519+ const result = await getPoetryVirtualenvsPath ( '/usr/bin/poetry' ) ;
520+
521+ // Should fall back to platform default
522+ const expectedPath = path . join ( '/home/test' , '.cache' , 'pypoetry' , 'virtualenvs' ) ;
523+ assert . strictEqual ( result , expectedPath ) ;
524+ } ) ;
525+
526+ test ( 'Windows: resolves {cache-dir} with platform default when cache-dir query fails' , async ( ) => {
527+ const originalLocalAppData = process . env . LOCALAPPDATA ;
528+ try {
529+ isWindowsStub . returns ( true ) ;
530+ process . env . LOCALAPPDATA = 'C:\\Users\\test\\AppData\\Local' ;
531+
532+ // First call: virtualenvs.path returns a path with {cache-dir}
533+ execProcessStub . onFirstCall ( ) . resolves ( { stdout : '{cache-dir}/virtualenvs\n' , stderr : '' } ) ;
534+ // Second call: cache-dir config fails
535+ execProcessStub . onSecondCall ( ) . rejects ( new Error ( 'Command failed' ) ) ;
536+
537+ const result = await getPoetryVirtualenvsPath ( 'C:\\poetry\\poetry.exe' ) ;
538+
539+ const expectedPath = path . join ( 'C:\\Users\\test\\AppData\\Local' , 'pypoetry' , 'Cache' , 'virtualenvs' ) ;
540+ assert . strictEqual ( result , expectedPath ) ;
541+ } finally {
542+ if ( originalLocalAppData === undefined ) {
543+ delete process . env . LOCALAPPDATA ;
544+ } else {
545+ process . env . LOCALAPPDATA = originalLocalAppData ;
546+ }
547+ }
548+ } ) ;
549+ } ) ;
0 commit comments