diff --git a/.github/workflows/test-readlink-node-versions.yml b/.github/workflows/test-readlink-node-versions.yml new file mode 100644 index 00000000..2b091bf0 --- /dev/null +++ b/.github/workflows/test-readlink-node-versions.yml @@ -0,0 +1,112 @@ +name: Test readlink behavior across Node versions + +on: + workflow_dispatch: + pull_request: + paths: + - 'packages/io/src/io-util.ts' + - 'packages/io/__tests__/io.test.ts' + +jobs: + test-readlink-windows: + name: Test readlink on Windows - Node ${{ matrix.node-version }} + runs-on: windows-latest + strategy: + matrix: + node-version: [20, 24] + + steps: + - name: Checkout code + uses: actions/checkout@v4 + + - name: Setup Node.js ${{ matrix.node-version }} + uses: actions/setup-node@v4 + with: + node-version: ${{ matrix.node-version }} + + - name: Show Node version + run: node --version + + - name: Create test script + shell: pwsh + run: | + $script = @' + const fs = require('fs'); + const path = require('path'); + const os = require('os'); + + async function testReadlink() { + const testDir = path.join(os.tmpdir(), 'readlink-test-' + Date.now()); + const actualDir = path.join(testDir, 'actual_directory'); + const directSymlink = path.join(testDir, 'direct_symlink'); + const intermediateSymlink = path.join(testDir, 'intermediate_symlink'); + const chainSymlink = path.join(testDir, 'chain_symlink'); + + try { + // Create test structure + await fs.promises.mkdir(actualDir, { recursive: true }); + await fs.promises.symlink(actualDir, directSymlink, 'junction'); + await fs.promises.symlink(actualDir, intermediateSymlink, 'junction'); + await fs.promises.symlink(intermediateSymlink, chainSymlink, 'junction'); + + console.log('='.repeat(60)); + console.log('Node.js version:', process.version); + console.log('Platform:', os.platform()); + console.log('='.repeat(60)); + + const directTarget = await fs.promises.readlink(directSymlink); + console.log('\n[Test 1] Direct symlink → directory:'); + console.log(' Path:', JSON.stringify(directTarget)); + console.log(' Length:', directTarget.length); + console.log(' Last char:', JSON.stringify(directTarget.slice(-1))); + console.log(' Ends with backslash:', directTarget.endsWith('\\')); + + const chainTarget = await fs.promises.readlink(chainSymlink); + console.log('\n[Test 2] Chain symlink → intermediate symlink:'); + console.log(' Path:', JSON.stringify(chainTarget)); + console.log(' Length:', chainTarget.length); + console.log(' Last char:', JSON.stringify(chainTarget.slice(-1))); + console.log(' Ends with backslash:', chainTarget.endsWith('\\')); + + const intermediateTarget = await fs.promises.readlink(intermediateSymlink); + console.log('\n[Test 3] Intermediate symlink → directory:'); + console.log(' Path:', JSON.stringify(intermediateTarget)); + console.log(' Length:', intermediateTarget.length); + console.log(' Last char:', JSON.stringify(intermediateTarget.slice(-1))); + console.log(' Ends with backslash:', intermediateTarget.endsWith('\\')); + + console.log('\n' + '='.repeat(60)); + console.log('SUMMARY:'); + console.log(' Test 1 (dir): trailing backslash =', directTarget.endsWith('\\')); + console.log(' Test 2 (symlink): trailing backslash =', chainTarget.endsWith('\\')); + console.log(' Test 3 (dir): trailing backslash =', intermediateTarget.endsWith('\\')); + console.log('='.repeat(60)); + + // Cleanup + await fs.promises.rm(testDir, { recursive: true, force: true }); + } catch (err) { + console.error('Error:', err); + process.exit(1); + } + } + + testReadlink(); + '@ + $script | Out-File -FilePath test-native-readlink.js -Encoding UTF8 + + - name: Test native fs.promises.readlink() behavior + run: node test-native-readlink.js + + - name: Install dependencies + run: npm ci + working-directory: packages/io + + - name: Build package + run: npm run tsc + working-directory: packages/io + + - name: Run readlink tests + run: npm test -- --testNamePattern="readlink" + working-directory: . + env: + CI: true