diff --git a/packages/fs/fs-local/src/fsio.ts b/packages/fs/fs-local/src/fsio.ts index 2472730df7..5d1713864a 100644 --- a/packages/fs/fs-local/src/fsio.ts +++ b/packages/fs/fs-local/src/fsio.ts @@ -196,6 +196,11 @@ function listingIoError(displayPath: string, error: unknown): FsError { return new FsError(`cannot list "${displayPath}": ${errorMessage(error)}`, 'FS_IO_ERROR', { cause: error }) } +async function resolveListedChildTarget(parent: LocalTarget, name: string): Promise { + const identity = await resolveLocalTarget(parent.targetKey, name) + return { displayPath: join(parent.displayPath, name), targetKey: identity.targetKey } +} + /** * List direct children of a directory in stable name order. Each child includes * a resolved target plus stat metadata when still available; file contents are @@ -225,7 +230,7 @@ export async function listDirectory(target: LocalTarget, signal?: AbortSignal): for (const entry of entries.sort((left, right) => left.name.localeCompare(right.name))) { throwIfAborted(signal, 'list') try { - const childTarget = await resolveLocalTarget(target.displayPath, entry.name) + const childTarget = await resolveListedChildTarget(target, entry.name) const childInfo = await probe(childTarget.targetKey) result.push({ name: entry.name, diff --git a/packages/fs/fs-local/tests/fsio.spec.ts b/packages/fs/fs-local/tests/fsio.spec.ts index 8d04a38d71..3a30f73ed2 100644 --- a/packages/fs/fs-local/tests/fsio.spec.ts +++ b/packages/fs/fs-local/tests/fsio.spec.ts @@ -6,7 +6,7 @@ */ import { afterEach, beforeEach, describe, expect, it } from 'vitest' -import { chmod, mkdtemp, readFile, rm, stat, symlink, writeFile, mkdir, readdir, realpath } from 'node:fs/promises' +import { chmod, mkdtemp, readFile, rm, stat, symlink, unlink, writeFile, mkdir, readdir, realpath } from 'node:fs/promises' import { tmpdir } from 'node:os' import { join } from 'node:path' import { createServer } from 'node:net' @@ -167,6 +167,32 @@ describe('listDirectory', () => { expect(entries.find(entry => entry.name === 'dir-skill')?.size).toBeUndefined() }) + it('derives child target keys from the listed parent identity', async () => { + const realOne = join(dir, 'real-one') + const realTwo = join(dir, 'real-two') + const link = join(dir, 'link') + await mkdir(realOne) + await mkdir(realTwo) + await writeFile(join(realOne, 'same.txt'), 'one') + await writeFile(join(realTwo, 'same.txt'), 'different two') + await symlink(realOne, link) + const target = await resolveLocalTarget(dir, 'link') + + await unlink(link) + await symlink(realTwo, link) + + const entries = await listDirectory(target) + expect(entries).toHaveLength(1) + expect(entries[0]).toMatchObject({ + name: 'same.txt', + target: { + displayPath: join(link, 'same.txt'), + targetKey: await realpath(join(realOne, 'same.txt')), + }, + size: 3, + }) + }) + it('rejects missing, non-directory, and aborted listing requests', async () => { await expect(listDirectory(localTarget(join(dir, 'missing')))).rejects.toMatchObject({ code: 'FS_NOT_FOUND' }) const file = join(dir, 'a.txt')