From 55004a4e454b3baee94643aefdad7303cdab1c42 Mon Sep 17 00:00:00 2001 From: Koji Wakayama Date: Thu, 11 Jun 2026 12:00:32 +0200 Subject: [PATCH 1/2] Add directory-entry listing and existence helpers to file discovery Extracted from the multi-agent staging branch (PR #2354) as standalone infrastructure: listDiscoveryDirectoryEntries lists immediate entries of a discovery directory and discoveryFileExists checks path existence, both fsAdapter-aware with Node fallback. No behavior changes to existing discovery. --- src/discovery/file-discovery.test.ts | 54 ++++++++++++++++++++++++ src/discovery/file-discovery.ts | 63 ++++++++++++++++++++++++++++ 2 files changed, 117 insertions(+) create mode 100644 src/discovery/file-discovery.test.ts diff --git a/src/discovery/file-discovery.test.ts b/src/discovery/file-discovery.test.ts new file mode 100644 index 0000000000..ef00e2c180 --- /dev/null +++ b/src/discovery/file-discovery.test.ts @@ -0,0 +1,54 @@ +import "#veryfront/schemas/_test-setup.ts"; +import { assertEquals } from "#veryfront/testing/assert.ts"; +import { discoveryFileExists, listDiscoveryDirectoryEntries } from "./file-discovery.ts"; +import type { FileDiscoveryContext } from "./types.ts"; +import type { FileSystemAdapter } from "#veryfront/platform/adapters/base.ts"; + +type FakeEntry = { name: string; isFile: boolean; isDirectory: boolean }; + +function fakeFsAdapter(tree: Record, files: Set): FileSystemAdapter { + return { + exists: (path: string) => Promise.resolve(path in tree || files.has(path)), + readDir: async function* (path: string) { + for (const entry of tree[path] ?? []) { + yield entry; + } + }, + readFile: (path: string) => Promise.resolve(`content:${path}`), + } as unknown as FileSystemAdapter; +} + +Deno.test("listDiscoveryDirectoryEntries reads top-level entries through an fsAdapter", async () => { + const fsAdapter = fakeFsAdapter( + { + "/agents": [ + { name: "lead.md", isFile: true, isDirectory: false }, + { name: "writer", isFile: false, isDirectory: true }, + ], + }, + new Set(), + ); + const context: FileDiscoveryContext = { platform: "node", fsAdapter }; + + const entries = await listDiscoveryDirectoryEntries("/agents", context); + + assertEquals(entries, [ + { name: "lead.md", isFile: true, isDirectory: false }, + { name: "writer", isFile: false, isDirectory: true }, + ]); +}); + +Deno.test("listDiscoveryDirectoryEntries returns empty for a missing dir via fsAdapter", async () => { + const fsAdapter = fakeFsAdapter({}, new Set()); + const context: FileDiscoveryContext = { platform: "node", fsAdapter }; + + assertEquals(await listDiscoveryDirectoryEntries("/missing", context), []); +}); + +Deno.test("discoveryFileExists resolves through an fsAdapter", async () => { + const fsAdapter = fakeFsAdapter({}, new Set(["/agents/writer/AGENT.md"])); + const context: FileDiscoveryContext = { platform: "node", fsAdapter }; + + assertEquals(await discoveryFileExists("/agents/writer/AGENT.md", context), true); + assertEquals(await discoveryFileExists("/agents/writer/SKILL.md", context), false); +}); diff --git a/src/discovery/file-discovery.ts b/src/discovery/file-discovery.ts index 4d595b1f9e..0114323320 100644 --- a/src/discovery/file-discovery.ts +++ b/src/discovery/file-discovery.ts @@ -107,3 +107,66 @@ export async function readDiscoveryTextFile( const { fs } = await getNodeDeps(context); return fs.readFileSync(path, "utf-8"); } + +/** A single top-level entry inside a discovery directory. */ +export type DiscoveryDirectoryEntry = { + name: string; + isFile: boolean; + isDirectory: boolean; +}; + +/** Lists the immediate (non-recursive) entries of a discovery directory. */ +export async function listDiscoveryDirectoryEntries( + dir: string, + context: FileDiscoveryContext, +): Promise { + const entries: DiscoveryDirectoryEntry[] = []; + + try { + if (context.fsAdapter) { + if (!(await context.fsAdapter.exists(dir))) return entries; + + for await (const entry of context.fsAdapter.readDir(dir)) { + entries.push({ + name: entry.name, + isFile: Boolean(entry.isFile), + isDirectory: Boolean(entry.isDirectory), + }); + } + + return entries; + } + + const { fs } = await getNodeDeps(context); + if (!fs.existsSync(dir)) return entries; + + for (const entry of fs.readdirSync(dir, { withFileTypes: true })) { + entries.push({ + name: entry.name, + isFile: entry.isFile(), + isDirectory: entry.isDirectory(), + }); + } + } catch (_) { + /* expected: directory may not exist or be unreadable */ + return entries; + } + + return entries; +} + +/** Returns true when a discovery file exists (fsAdapter-aware). */ +export async function discoveryFileExists( + path: string, + context: FileDiscoveryContext, +): Promise { + try { + if (context.fsAdapter) { + return await context.fsAdapter.exists(path); + } + const { fs } = await getNodeDeps(context); + return fs.existsSync(path); + } catch (_) { + return false; + } +} From 99699d3d56bab9b29fd7f6eb92704c01ba4f1a67 Mon Sep 17 00:00:00 2001 From: Koji Wakayama Date: Thu, 11 Jun 2026 14:32:55 +0200 Subject: [PATCH 2/2] Keep discovery entry typing honest The fsAdapter branch already exposes boolean entry properties, so pass them through directly and add a no-adapter tempdir test for the Node Dirent fallback where the contract differs. Constraint: Review requested the fsAdapter path avoid masking contract mistakes and the Node fallback be covered.\nRejected: Keep Boolean() coercion | it hides adapter shape errors instead of surfacing them.\nConfidence: high\nScope-risk: narrow\nTested: deno test --no-check --allow-all src/discovery/file-discovery.test.ts\nNot-tested: Full repository suite --- src/discovery/file-discovery.test.ts | 20 ++++++++++++++++++++ src/discovery/file-discovery.ts | 4 ++-- 2 files changed, 22 insertions(+), 2 deletions(-) diff --git a/src/discovery/file-discovery.test.ts b/src/discovery/file-discovery.test.ts index ef00e2c180..3e8856a82f 100644 --- a/src/discovery/file-discovery.test.ts +++ b/src/discovery/file-discovery.test.ts @@ -45,6 +45,26 @@ Deno.test("listDiscoveryDirectoryEntries returns empty for a missing dir via fsA assertEquals(await listDiscoveryDirectoryEntries("/missing", context), []); }); +Deno.test("listDiscoveryDirectoryEntries reads top-level entries through the Node fallback", async () => { + const root = await Deno.makeTempDir(); + try { + await Deno.writeTextFile(`${root}/lead.md`, "Lead"); + await Deno.mkdir(`${root}/writer`); + await Deno.writeTextFile(`${root}/writer/AGENT.md`, "Writer"); + const context: FileDiscoveryContext = { platform: "node" }; + + const entries = (await listDiscoveryDirectoryEntries(root, context)) + .sort((a, b) => a.name.localeCompare(b.name)); + + assertEquals(entries, [ + { name: "lead.md", isFile: true, isDirectory: false }, + { name: "writer", isFile: false, isDirectory: true }, + ]); + } finally { + await Deno.remove(root, { recursive: true }); + } +}); + Deno.test("discoveryFileExists resolves through an fsAdapter", async () => { const fsAdapter = fakeFsAdapter({}, new Set(["/agents/writer/AGENT.md"])); const context: FileDiscoveryContext = { platform: "node", fsAdapter }; diff --git a/src/discovery/file-discovery.ts b/src/discovery/file-discovery.ts index 0114323320..125d69b4c4 100644 --- a/src/discovery/file-discovery.ts +++ b/src/discovery/file-discovery.ts @@ -129,8 +129,8 @@ export async function listDiscoveryDirectoryEntries( for await (const entry of context.fsAdapter.readDir(dir)) { entries.push({ name: entry.name, - isFile: Boolean(entry.isFile), - isDirectory: Boolean(entry.isDirectory), + isFile: entry.isFile, + isDirectory: entry.isDirectory, }); }