Skip to content

Commit 151d37e

Browse files
committed
fix: align Maestro test discovery order
1 parent 98376b9 commit 151d37e

2 files changed

Lines changed: 139 additions & 12 deletions

File tree

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

Lines changed: 75 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,4 @@
1-
import { test } from 'vitest';
1+
import { test, vi } from 'vitest';
22
import assert from 'node:assert/strict';
33
import fs from 'node:fs';
44
import os from 'node:os';
@@ -84,3 +84,77 @@ test('discoverReplayTestEntries includes Maestro yaml flows for Maestro test sui
8484
assert.equal(entries[0].title, 'Bottom Tabs - Dynamic');
8585
}
8686
});
87+
88+
test('discoverReplayTestEntries sorts Maestro directory flows by extension group then path', () => {
89+
const root = fs.mkdtempSync(path.join(os.tmpdir(), 'agent-device-test-discovery-maestro-sort-'));
90+
const flowFiles = ['10-legacy.ad', '30-zeta.yaml', '05-compat.ad', '20-beta.yml'];
91+
for (const fileName of flowFiles) {
92+
const body = fileName.endsWith('.ad') ? 'open "Demo"\n' : 'appId: demo\n---\n- launchApp\n';
93+
fs.writeFileSync(path.join(root, fileName), body);
94+
}
95+
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 [];
102+
});
103+
104+
try {
105+
const entries = discoverReplayTestEntries({
106+
inputs: [root],
107+
cwd: root,
108+
replayBackend: 'maestro',
109+
});
110+
111+
assert.deepEqual(
112+
entries.map((entry) => path.basename(entry.path)),
113+
['20-beta.yml', '30-zeta.yaml', '05-compat.ad', '10-legacy.ad'],
114+
);
115+
} finally {
116+
globSync.mockRestore();
117+
}
118+
});
119+
120+
test('discoverReplayTestEntries preserves explicit Maestro file order', () => {
121+
const root = fs.mkdtempSync(path.join(os.tmpdir(), 'agent-device-test-discovery-maestro-order-'));
122+
const second = path.join(root, '02-second.yaml');
123+
const first = path.join(root, '01-first.yaml');
124+
fs.writeFileSync(first, 'appId: demo\n---\n- launchApp\n');
125+
fs.writeFileSync(second, 'appId: demo\n---\n- launchApp\n');
126+
127+
const entries = discoverReplayTestEntries({
128+
inputs: [second, first],
129+
cwd: root,
130+
replayBackend: 'maestro',
131+
});
132+
133+
assert.deepEqual(
134+
entries.map((entry) => path.basename(entry.path)),
135+
['02-second.yaml', '01-first.yaml'],
136+
);
137+
});
138+
139+
test('discoverReplayTestEntries orders Maestro file inputs before expanded flows', () => {
140+
const root = fs.mkdtempSync(path.join(os.tmpdir(), 'agent-device-test-discovery-maestro-files-'));
141+
const suite = path.join(root, 'suite');
142+
const globSuite = path.join(root, 'glob-suite');
143+
fs.mkdirSync(suite);
144+
fs.mkdirSync(globSuite);
145+
const explicit = path.join(root, '99-explicit.yaml');
146+
fs.writeFileSync(explicit, 'appId: demo\n---\n- launchApp\n');
147+
fs.writeFileSync(path.join(suite, '01-directory.yaml'), 'appId: demo\n---\n- launchApp\n');
148+
fs.writeFileSync(path.join(globSuite, '02-glob.yaml'), 'appId: demo\n---\n- launchApp\n');
149+
150+
const entries = discoverReplayTestEntries({
151+
inputs: [suite, path.join(globSuite, '*.yaml'), explicit],
152+
cwd: root,
153+
replayBackend: 'maestro',
154+
});
155+
156+
assert.deepEqual(
157+
entries.map((entry) => path.basename(entry.path)),
158+
['99-explicit.yaml', '01-directory.yaml', '02-glob.yaml'],
159+
);
160+
});

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

Lines changed: 64 additions & 11 deletions
Original file line numberDiff line numberDiff line change
@@ -27,6 +27,8 @@ export type ReplayTestDiscoveryEntry =
2727

2828
export type ReplayTestRunEntry = Extract<ReplayTestDiscoveryEntry, { kind: 'run' }>;
2929

30+
type ReplayTestInputSource = 'directory' | 'file' | 'glob';
31+
3032
export function discoverReplayTestEntries(params: {
3133
inputs: string[];
3234
cwd?: string;
@@ -36,11 +38,7 @@ export function discoverReplayTestEntries(params: {
3638
const { inputs, cwd, platformFilter, replayBackend } = params;
3739
const extensions = replayTestExtensions(replayBackend);
3840
const resolvedCwd = cwd ?? process.cwd();
39-
const filePaths = [
40-
...new Set(inputs.flatMap((input) => expandReplayTestInput(input, resolvedCwd, extensions))),
41-
]
42-
.map((entry) => path.normalize(entry))
43-
.sort((left, right) => left.localeCompare(right));
41+
const filePaths = discoverReplayTestFilePaths(inputs, resolvedCwd, extensions, replayBackend);
4442

4543
const entries: ReplayTestDiscoveryEntry[] = [];
4644
for (const filePath of filePaths) {
@@ -136,24 +134,57 @@ export function resolveReplayTestRetries(
136134
return Math.max(0, Math.min(MAX_REPLAY_TEST_RETRIES, resolved));
137135
}
138136

139-
function expandReplayTestInput(input: string, cwd: string, extensions: Set<string>): string[] {
137+
function discoverReplayTestFilePaths(
138+
inputs: string[],
139+
cwd: string,
140+
extensions: Set<string>,
141+
replayBackend: string | undefined,
142+
): string[] {
143+
if (!isMaestroReplayBackend(replayBackend)) {
144+
return [
145+
...new Set(inputs.flatMap((input) => expandReplayTestInput(input, cwd, extensions).paths)),
146+
]
147+
.map((entry) => path.normalize(entry))
148+
.sort((left, right) => left.localeCompare(right));
149+
}
150+
151+
const files: string[] = [];
152+
const expandedGroups: string[][] = [];
153+
for (const input of inputs) {
154+
const expanded = expandReplayTestInput(input, cwd, extensions);
155+
if (expanded.source === 'file') {
156+
files.push(...expanded.paths);
157+
} else {
158+
expandedGroups.push(sortMaestroExpandedReplayTestPaths(expanded.paths));
159+
}
160+
}
161+
162+
return uniqueNormalizedPaths([...files, ...expandedGroups.flat()]);
163+
}
164+
165+
function expandReplayTestInput(
166+
input: string,
167+
cwd: string,
168+
extensions: Set<string>,
169+
): { paths: string[]; source: ReplayTestInputSource } {
140170
const expandedInput = SessionStore.expandHome(input, cwd);
141171
if (fs.existsSync(expandedInput)) {
142172
const stat = fs.statSync(expandedInput);
143173
if (stat.isDirectory()) {
144-
return replayTestGlobPatterns(extensions).flatMap((pattern) =>
174+
const paths = replayTestGlobPatterns(extensions).flatMap((pattern) =>
145175
fs
146176
.globSync(pattern, { cwd: expandedInput })
147177
.map((match) => path.join(expandedInput, match)),
148178
);
179+
return { paths, source: 'directory' };
149180
}
150181
if (stat.isFile()) {
151182
if (!extensions.has(path.extname(expandedInput))) {
152183
throw new AppError('INVALID_ARGS', `test does not support this file type: ${input}`);
153184
}
154-
return [expandedInput];
185+
return { paths: [expandedInput], source: 'file' };
155186
}
156-
return [];
187+
return { paths: [], source: 'file' };
157188
}
158189

159190
if (!looksLikeGlob(input) && !looksLikeGlob(expandedInput)) {
@@ -165,21 +196,43 @@ function expandReplayTestInput(input: string, cwd: string, extensions: Set<strin
165196
cwd: path.isAbsolute(expandedInput) ? undefined : cwd,
166197
});
167198

168-
return matches
199+
const paths = matches
169200
.map((match) => (path.isAbsolute(match) ? match : path.resolve(cwd, match)))
170201
.filter((match) => extensions.has(path.extname(match)) && isExistingFile(match));
202+
return { paths, source: 'glob' };
171203
}
172204

173205
function replayTestExtensions(replayBackend: string | undefined): Set<string> {
174206
return isMaestroReplayBackend(replayBackend)
175-
? new Set(['.ad', '.yaml', '.yml'])
207+
? new Set(['.yaml', '.yml', '.ad'])
176208
: new Set(['.ad']);
177209
}
178210

179211
function replayTestGlobPatterns(extensions: Set<string>): string[] {
180212
return [...extensions].map((extension) => `**/*${extension}`);
181213
}
182214

215+
function sortMaestroExpandedReplayTestPaths(paths: string[]): string[] {
216+
return paths.map((entry) => path.normalize(entry)).sort(compareMaestroReplayTestPath);
217+
}
218+
219+
function compareMaestroReplayTestPath(left: string, right: string): number {
220+
const leftRank = maestroReplayTestExtensionRank(left);
221+
const rightRank = maestroReplayTestExtensionRank(right);
222+
if (leftRank !== rightRank) {
223+
return leftRank - rightRank;
224+
}
225+
return left.localeCompare(right);
226+
}
227+
228+
function maestroReplayTestExtensionRank(filePath: string): number {
229+
return path.extname(filePath) === '.ad' ? 1 : 0;
230+
}
231+
232+
function uniqueNormalizedPaths(paths: string[]): string[] {
233+
return [...new Set(paths.map((entry) => path.normalize(entry)))];
234+
}
235+
183236
function isMaestroReplayBackend(replayBackend: string | undefined): boolean {
184237
return replayBackend === 'maestro';
185238
}

0 commit comments

Comments
 (0)