From b67770ab576d6d6c1715007cad95e7612eb116bb Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Micha=C5=82=20Pierzcha=C5=82a?= Date: Sat, 13 Jun 2026 20:30:36 +0200 Subject: [PATCH 1/2] fix: match Maestro directory test order --- .../__tests__/session-test-discovery.test.ts | 38 +++++++++------ src/daemon/handlers/session-test-discovery.ts | 46 ++++++++++++++++++- 2 files changed, 68 insertions(+), 16 deletions(-) diff --git a/src/daemon/handlers/__tests__/session-test-discovery.test.ts b/src/daemon/handlers/__tests__/session-test-discovery.test.ts index eb54f4702..366907985 100644 --- a/src/daemon/handlers/__tests__/session-test-discovery.test.ts +++ b/src/daemon/handlers/__tests__/session-test-discovery.test.ts @@ -72,20 +72,21 @@ test('discoverReplayTestEntries includes Maestro yaml flows for Maestro test sui }); assert.deepEqual( - entries.map((entry) => path.basename(entry.path)), - ['01-flow.yaml', '02-flow.yml', '03-flow.ad'], + new Set(entries.map((entry) => path.basename(entry.path))), + new Set(['01-flow.yaml', '02-flow.yml', '03-flow.ad']), ); assert.deepEqual( entries.map((entry) => entry.kind), ['run', 'run', 'run'], ); - assert.equal(entries[0]?.kind, 'run'); - if (entries[0]?.kind === 'run') { - assert.equal(entries[0].title, 'Bottom Tabs - Dynamic'); + const namedFlow = entries.find((entry) => path.basename(entry.path) === '01-flow.yaml'); + assert.equal(namedFlow?.kind, 'run'); + if (namedFlow?.kind === 'run') { + assert.equal(namedFlow.title, 'Bottom Tabs - Dynamic'); } }); -test('discoverReplayTestEntries sorts Maestro directory flows by extension group then path', () => { +test('discoverReplayTestEntries preserves Maestro directory filesystem order', () => { const root = fs.mkdtempSync(path.join(os.tmpdir(), 'agent-device-test-discovery-maestro-sort-')); const flowFiles = ['10-legacy.ad', '30-zeta.yaml', '05-compat.ad', '20-beta.yml']; for (const fileName of flowFiles) { @@ -93,12 +94,21 @@ test('discoverReplayTestEntries sorts Maestro directory flows by extension group fs.writeFileSync(path.join(root, fileName), body); } - const globSync = vi.spyOn(fs, 'globSync').mockImplementation((pattern, options) => { - assert.equal((options as { cwd?: string } | undefined)?.cwd, root); - if (pattern === '**/*.yaml') return ['30-zeta.yaml']; - if (pattern === '**/*.yml') return ['20-beta.yml']; - if (pattern === '**/*.ad') return ['10-legacy.ad', '05-compat.ad']; - return []; + const opendirSync = vi.spyOn(fs, 'opendirSync').mockImplementation((directory) => { + assert.equal(directory, root); + let index = 0; + return { + readSync: () => { + const name = flowFiles[index++]; + if (!name) return null; + return { + name, + isDirectory: () => false, + isFile: () => true, + } as fs.Dirent; + }, + closeSync: () => {}, + } as fs.Dir; }); try { @@ -110,10 +120,10 @@ test('discoverReplayTestEntries sorts Maestro directory flows by extension group assert.deepEqual( entries.map((entry) => path.basename(entry.path)), - ['20-beta.yml', '30-zeta.yaml', '05-compat.ad', '10-legacy.ad'], + ['10-legacy.ad', '30-zeta.yaml', '05-compat.ad', '20-beta.yml'], ); } finally { - globSync.mockRestore(); + opendirSync.mockRestore(); } }); diff --git a/src/daemon/handlers/session-test-discovery.ts b/src/daemon/handlers/session-test-discovery.ts index 3846d312d..9457edebe 100644 --- a/src/daemon/handlers/session-test-discovery.ts +++ b/src/daemon/handlers/session-test-discovery.ts @@ -151,17 +151,59 @@ function discoverReplayTestFilePaths( const files: string[] = []; const expandedGroups: string[][] = []; for (const input of inputs) { - const expanded = expandReplayTestInput(input, cwd, extensions); + const expanded = expandMaestroReplayTestInput(input, cwd, extensions); if (expanded.source === 'file') { files.push(...expanded.paths); } else { - expandedGroups.push(sortMaestroExpandedReplayTestPaths(expanded.paths)); + expandedGroups.push( + expanded.source === 'directory' + ? uniqueNormalizedPaths(expanded.paths) + : sortMaestroExpandedReplayTestPaths(expanded.paths), + ); } } return uniqueNormalizedPaths([...files, ...expandedGroups.flat()]); } +function expandMaestroReplayTestInput( + input: string, + cwd: string, + extensions: Set, +): { paths: string[]; source: ReplayTestInputSource } { + const expandedInput = SessionStore.expandHome(input, cwd); + if (fs.existsSync(expandedInput) && fs.statSync(expandedInput).isDirectory()) { + return { + paths: readMaestroDirectoryReplayTestPaths(expandedInput, extensions), + source: 'directory', + }; + } + + return expandReplayTestInput(input, cwd, extensions); +} + +function readMaestroDirectoryReplayTestPaths( + directoryPath: string, + extensions: Set, +): string[] { + const paths: string[] = []; + const directory = fs.opendirSync(directoryPath); + try { + let entry: fs.Dirent | null; + while ((entry = directory.readSync()) !== null) { + const filePath = path.join(directoryPath, entry.name); + if (entry.isDirectory()) { + paths.push(...readMaestroDirectoryReplayTestPaths(filePath, extensions)); + } else if (entry.isFile() && extensions.has(path.extname(entry.name))) { + paths.push(filePath); + } + } + } finally { + directory.closeSync(); + } + return paths; +} + function expandReplayTestInput( input: string, cwd: string, From e5654c3b4a295ce1dea5d69c56b906da0c0d8014 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Micha=C5=82=20Pierzcha=C5=82a?= Date: Sat, 13 Jun 2026 20:41:50 +0200 Subject: [PATCH 2/2] test: cover Maestro nested directory discovery --- .../__tests__/session-test-discovery.test.ts | 51 +++++++++++++++++++ src/daemon/handlers/session-test-discovery.ts | 3 ++ 2 files changed, 54 insertions(+) diff --git a/src/daemon/handlers/__tests__/session-test-discovery.test.ts b/src/daemon/handlers/__tests__/session-test-discovery.test.ts index 366907985..d2ec3e00a 100644 --- a/src/daemon/handlers/__tests__/session-test-discovery.test.ts +++ b/src/daemon/handlers/__tests__/session-test-discovery.test.ts @@ -127,6 +127,57 @@ test('discoverReplayTestEntries preserves Maestro directory filesystem order', ( } }); +test('discoverReplayTestEntries preserves Maestro nested directory DFS order', () => { + const root = fs.mkdtempSync(path.join(os.tmpdir(), 'agent-device-test-discovery-maestro-dfs-')); + const nested = path.join(root, 'nested'); + fs.mkdirSync(nested, { recursive: true }); + fs.writeFileSync(path.join(root, '30-root-a.yaml'), 'appId: demo\n---\n- launchApp\n'); + fs.writeFileSync(path.join(nested, '10-child.yml'), 'appId: demo\n---\n- launchApp\n'); + fs.writeFileSync(path.join(root, '20-root-c.ad'), 'open "Demo"\n'); + + type MockDirEntry = { name: string; directory: boolean }; + const opendirSync = vi.spyOn(fs, 'opendirSync').mockImplementation((directory) => { + let entries: MockDirEntry[] = []; + if (directory === root) { + entries = [ + { name: '30-root-a.yaml', directory: false }, + { name: 'nested', directory: true }, + { name: '20-root-c.ad', directory: false }, + ]; + } else if (directory === nested) { + entries = [{ name: '10-child.yml', directory: false }]; + } + let index = 0; + return { + readSync: () => { + const entry = entries[index++]; + if (!entry) return null; + return { + name: entry.name, + isDirectory: () => entry.directory, + isFile: () => !entry.directory, + } as fs.Dirent; + }, + closeSync: () => {}, + } as fs.Dir; + }); + + try { + const entries = discoverReplayTestEntries({ + inputs: [root], + cwd: root, + replayBackend: 'maestro', + }); + + assert.deepEqual( + entries.map((entry) => path.relative(root, entry.path)), + ['30-root-a.yaml', path.join('nested', '10-child.yml'), '20-root-c.ad'], + ); + } finally { + opendirSync.mockRestore(); + } +}); + test('discoverReplayTestEntries preserves explicit Maestro file order', () => { const root = fs.mkdtempSync(path.join(os.tmpdir(), 'agent-device-test-discovery-maestro-order-')); const second = path.join(root, '02-second.yaml'); diff --git a/src/daemon/handlers/session-test-discovery.ts b/src/daemon/handlers/session-test-discovery.ts index 9457edebe..55765f8e9 100644 --- a/src/daemon/handlers/session-test-discovery.ts +++ b/src/daemon/handlers/session-test-discovery.ts @@ -187,6 +187,9 @@ function readMaestroDirectoryReplayTestPaths( extensions: Set, ): string[] { const paths: string[] = []; + // Maestro's Java Files.walk follows native directory iteration order. Keep + // this unsorted so folder suites, and sharding derived from them, match + // Maestro on the same machine even though order can differ across hosts. const directory = fs.opendirSync(directoryPath); try { let entry: fs.Dirent | null;