Skip to content

Commit 991f4c7

Browse files
authored
fix: match Maestro directory test order (#802)
* fix: match Maestro directory test order * test: cover Maestro nested directory discovery
1 parent 778349a commit 991f4c7

2 files changed

Lines changed: 122 additions & 16 deletions

File tree

src/daemon/handlers/__tests__/session-test-discovery.test.ts

Lines changed: 75 additions & 14 deletions
Original file line numberDiff line numberDiff line change
@@ -72,33 +72,43 @@ test('discoverReplayTestEntries includes Maestro yaml flows for Maestro test sui
7272
});
7373

7474
assert.deepEqual(
75-
entries.map((entry) => path.basename(entry.path)),
76-
['01-flow.yaml', '02-flow.yml', '03-flow.ad'],
75+
new Set(entries.map((entry) => path.basename(entry.path))),
76+
new Set(['01-flow.yaml', '02-flow.yml', '03-flow.ad']),
7777
);
7878
assert.deepEqual(
7979
entries.map((entry) => entry.kind),
8080
['run', 'run', 'run'],
8181
);
82-
assert.equal(entries[0]?.kind, 'run');
83-
if (entries[0]?.kind === 'run') {
84-
assert.equal(entries[0].title, 'Bottom Tabs - Dynamic');
82+
const namedFlow = entries.find((entry) => path.basename(entry.path) === '01-flow.yaml');
83+
assert.equal(namedFlow?.kind, 'run');
84+
if (namedFlow?.kind === 'run') {
85+
assert.equal(namedFlow.title, 'Bottom Tabs - Dynamic');
8586
}
8687
});
8788

88-
test('discoverReplayTestEntries sorts Maestro directory flows by extension group then path', () => {
89+
test('discoverReplayTestEntries preserves Maestro directory filesystem order', () => {
8990
const root = fs.mkdtempSync(path.join(os.tmpdir(), 'agent-device-test-discovery-maestro-sort-'));
9091
const flowFiles = ['10-legacy.ad', '30-zeta.yaml', '05-compat.ad', '20-beta.yml'];
9192
for (const fileName of flowFiles) {
9293
const body = fileName.endsWith('.ad') ? 'open "Demo"\n' : 'appId: demo\n---\n- launchApp\n';
9394
fs.writeFileSync(path.join(root, fileName), body);
9495
}
9596

96-
const globSync = vi.spyOn(fs, 'globSync').mockImplementation((pattern, options) => {
97-
assert.equal((options as { cwd?: string } | undefined)?.cwd, root);
98-
if (pattern === '**/*.yaml') return ['30-zeta.yaml'];
99-
if (pattern === '**/*.yml') return ['20-beta.yml'];
100-
if (pattern === '**/*.ad') return ['10-legacy.ad', '05-compat.ad'];
101-
return [];
97+
const opendirSync = vi.spyOn(fs, 'opendirSync').mockImplementation((directory) => {
98+
assert.equal(directory, root);
99+
let index = 0;
100+
return {
101+
readSync: () => {
102+
const name = flowFiles[index++];
103+
if (!name) return null;
104+
return {
105+
name,
106+
isDirectory: () => false,
107+
isFile: () => true,
108+
} as fs.Dirent;
109+
},
110+
closeSync: () => {},
111+
} as fs.Dir;
102112
});
103113

104114
try {
@@ -110,10 +120,61 @@ test('discoverReplayTestEntries sorts Maestro directory flows by extension group
110120

111121
assert.deepEqual(
112122
entries.map((entry) => path.basename(entry.path)),
113-
['20-beta.yml', '30-zeta.yaml', '05-compat.ad', '10-legacy.ad'],
123+
['10-legacy.ad', '30-zeta.yaml', '05-compat.ad', '20-beta.yml'],
124+
);
125+
} finally {
126+
opendirSync.mockRestore();
127+
}
128+
});
129+
130+
test('discoverReplayTestEntries preserves Maestro nested directory DFS order', () => {
131+
const root = fs.mkdtempSync(path.join(os.tmpdir(), 'agent-device-test-discovery-maestro-dfs-'));
132+
const nested = path.join(root, 'nested');
133+
fs.mkdirSync(nested, { recursive: true });
134+
fs.writeFileSync(path.join(root, '30-root-a.yaml'), 'appId: demo\n---\n- launchApp\n');
135+
fs.writeFileSync(path.join(nested, '10-child.yml'), 'appId: demo\n---\n- launchApp\n');
136+
fs.writeFileSync(path.join(root, '20-root-c.ad'), 'open "Demo"\n');
137+
138+
type MockDirEntry = { name: string; directory: boolean };
139+
const opendirSync = vi.spyOn(fs, 'opendirSync').mockImplementation((directory) => {
140+
let entries: MockDirEntry[] = [];
141+
if (directory === root) {
142+
entries = [
143+
{ name: '30-root-a.yaml', directory: false },
144+
{ name: 'nested', directory: true },
145+
{ name: '20-root-c.ad', directory: false },
146+
];
147+
} else if (directory === nested) {
148+
entries = [{ name: '10-child.yml', directory: false }];
149+
}
150+
let index = 0;
151+
return {
152+
readSync: () => {
153+
const entry = entries[index++];
154+
if (!entry) return null;
155+
return {
156+
name: entry.name,
157+
isDirectory: () => entry.directory,
158+
isFile: () => !entry.directory,
159+
} as fs.Dirent;
160+
},
161+
closeSync: () => {},
162+
} as fs.Dir;
163+
});
164+
165+
try {
166+
const entries = discoverReplayTestEntries({
167+
inputs: [root],
168+
cwd: root,
169+
replayBackend: 'maestro',
170+
});
171+
172+
assert.deepEqual(
173+
entries.map((entry) => path.relative(root, entry.path)),
174+
['30-root-a.yaml', path.join('nested', '10-child.yml'), '20-root-c.ad'],
114175
);
115176
} finally {
116-
globSync.mockRestore();
177+
opendirSync.mockRestore();
117178
}
118179
});
119180

src/daemon/handlers/session-test-discovery.ts

Lines changed: 47 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -151,17 +151,62 @@ function discoverReplayTestFilePaths(
151151
const files: string[] = [];
152152
const expandedGroups: string[][] = [];
153153
for (const input of inputs) {
154-
const expanded = expandReplayTestInput(input, cwd, extensions);
154+
const expanded = expandMaestroReplayTestInput(input, cwd, extensions);
155155
if (expanded.source === 'file') {
156156
files.push(...expanded.paths);
157157
} else {
158-
expandedGroups.push(sortMaestroExpandedReplayTestPaths(expanded.paths));
158+
expandedGroups.push(
159+
expanded.source === 'directory'
160+
? uniqueNormalizedPaths(expanded.paths)
161+
: sortMaestroExpandedReplayTestPaths(expanded.paths),
162+
);
159163
}
160164
}
161165

162166
return uniqueNormalizedPaths([...files, ...expandedGroups.flat()]);
163167
}
164168

169+
function expandMaestroReplayTestInput(
170+
input: string,
171+
cwd: string,
172+
extensions: Set<string>,
173+
): { paths: string[]; source: ReplayTestInputSource } {
174+
const expandedInput = SessionStore.expandHome(input, cwd);
175+
if (fs.existsSync(expandedInput) && fs.statSync(expandedInput).isDirectory()) {
176+
return {
177+
paths: readMaestroDirectoryReplayTestPaths(expandedInput, extensions),
178+
source: 'directory',
179+
};
180+
}
181+
182+
return expandReplayTestInput(input, cwd, extensions);
183+
}
184+
185+
function readMaestroDirectoryReplayTestPaths(
186+
directoryPath: string,
187+
extensions: Set<string>,
188+
): string[] {
189+
const paths: string[] = [];
190+
// Maestro's Java Files.walk follows native directory iteration order. Keep
191+
// this unsorted so folder suites, and sharding derived from them, match
192+
// Maestro on the same machine even though order can differ across hosts.
193+
const directory = fs.opendirSync(directoryPath);
194+
try {
195+
let entry: fs.Dirent | null;
196+
while ((entry = directory.readSync()) !== null) {
197+
const filePath = path.join(directoryPath, entry.name);
198+
if (entry.isDirectory()) {
199+
paths.push(...readMaestroDirectoryReplayTestPaths(filePath, extensions));
200+
} else if (entry.isFile() && extensions.has(path.extname(entry.name))) {
201+
paths.push(filePath);
202+
}
203+
}
204+
} finally {
205+
directory.closeSync();
206+
}
207+
return paths;
208+
}
209+
165210
function expandReplayTestInput(
166211
input: string,
167212
cwd: string,

0 commit comments

Comments
 (0)