|
10 | 10 | import java.nio.file.Files; |
11 | 11 | import java.nio.file.Path; |
12 | 12 | import java.nio.file.Paths; |
| 13 | +import java.util.ArrayList; |
13 | 14 | import java.util.HashMap; |
14 | 15 | import java.util.List; |
15 | 16 | import java.util.Map; |
| 17 | +import java.util.logging.Logger; |
| 18 | +import java.util.regex.Matcher; |
16 | 19 | import java.util.regex.Pattern; |
17 | 20 |
|
18 | 21 | import com.github.copilot.sdk.json.CopilotClientOptions; |
|
50 | 53 | */ |
51 | 54 | public class E2ETestContext implements AutoCloseable { |
52 | 55 |
|
| 56 | + private static final Logger LOG = Logger.getLogger(E2ETestContext.class.getName()); |
53 | 57 | private static final Pattern SNAKE_CASE = Pattern.compile("[^a-zA-Z0-9]"); |
| 58 | + private static final Pattern USER_CONTENT_PATTERN = Pattern |
| 59 | + .compile("^\\s+-\\s+role:\\s+user\\s*$\\s+content:\\s*(.+?)$", Pattern.MULTILINE); |
54 | 60 |
|
55 | 61 | private final String cliPath; |
56 | 62 | private final Path homeDir; |
57 | 63 | private final Path workDir; |
58 | 64 | private String proxyUrl; |
59 | 65 | private final CapiProxy proxy; |
60 | 66 | private final Path repoRoot; |
| 67 | + private Path currentSnapshotFile; |
61 | 68 |
|
62 | 69 | private E2ETestContext(String cliPath, Path homeDir, Path workDir, String proxyUrl, CapiProxy proxy, |
63 | 70 | Path repoRoot) { |
@@ -139,9 +146,71 @@ public void configureForTest(String testFile, String testName) throws IOExceptio |
139 | 146 | // Convert test method names to lowercase snake_case for snapshot filenames |
140 | 147 | // to avoid case collisions on case-insensitive filesystems (macOS/Windows) |
141 | 148 | String sanitizedName = SNAKE_CASE.matcher(testName).replaceAll("_").toLowerCase(); |
142 | | - String snapshotPath = repoRoot.resolve("test").resolve("snapshots").resolve(testFile) |
143 | | - .resolve(sanitizedName + ".yaml").toString(); |
144 | | - proxy.configure(snapshotPath, workDir.toString()); |
| 149 | + Path snapshotFile = repoRoot.resolve("test").resolve("snapshots").resolve(testFile) |
| 150 | + .resolve(sanitizedName + ".yaml"); |
| 151 | + |
| 152 | + // Validate snapshot exists - fail fast with a clear message |
| 153 | + if (!Files.exists(snapshotFile)) { |
| 154 | + Path snapshotsDir = repoRoot.resolve("test").resolve("snapshots").resolve(testFile); |
| 155 | + String availableSnapshots = ""; |
| 156 | + if (Files.exists(snapshotsDir)) { |
| 157 | + try (var files = Files.list(snapshotsDir)) { |
| 158 | + availableSnapshots = files.filter(p -> p.toString().endsWith(".yaml")) |
| 159 | + .map(p -> p.getFileName().toString().replace(".yaml", "")).sorted() |
| 160 | + .reduce((a, b) -> a + ", " + b).orElse("<none>"); |
| 161 | + } |
| 162 | + } |
| 163 | + throw new IOException(String.format( |
| 164 | + "Snapshot file not found: %s%n" + "Category: %s, Test: %s (sanitized: %s)%n" |
| 165 | + + "Available snapshots in '%s/': %s%n" |
| 166 | + + "Ensure the snapshot exists and the test name matches exactly.", |
| 167 | + snapshotFile, testFile, testName, sanitizedName, testFile, availableSnapshots)); |
| 168 | + } |
| 169 | + |
| 170 | + this.currentSnapshotFile = snapshotFile; |
| 171 | + proxy.configure(snapshotFile.toString(), workDir.toString()); |
| 172 | + |
| 173 | + // Log expected prompts to help debug prompt mismatch issues |
| 174 | + List<String> expectedPrompts = getExpectedUserPrompts(); |
| 175 | + if (!expectedPrompts.isEmpty()) { |
| 176 | + LOG.info(() -> String.format("Configured snapshot '%s/%s' expects prompts: %s", testFile, sanitizedName, |
| 177 | + expectedPrompts)); |
| 178 | + } |
| 179 | + } |
| 180 | + |
| 181 | + /** |
| 182 | + * Gets the expected user prompts from the current snapshot file. |
| 183 | + * <p> |
| 184 | + * This is useful for debugging when tests fail with "No cached response found" |
| 185 | + * errors from CapiProxy. The prompts in your test must match these exactly. |
| 186 | + * </p> |
| 187 | + * |
| 188 | + * @return list of expected user prompt strings, or empty list if none found |
| 189 | + */ |
| 190 | + public List<String> getExpectedUserPrompts() { |
| 191 | + if (currentSnapshotFile == null || !Files.exists(currentSnapshotFile)) { |
| 192 | + return List.of(); |
| 193 | + } |
| 194 | + try { |
| 195 | + String content = Files.readString(currentSnapshotFile); |
| 196 | + List<String> prompts = new ArrayList<>(); |
| 197 | + Matcher matcher = USER_CONTENT_PATTERN.matcher(content); |
| 198 | + while (matcher.find()) { |
| 199 | + String prompt = matcher.group(1).trim(); |
| 200 | + // Remove quotes if present |
| 201 | + if ((prompt.startsWith("\"") && prompt.endsWith("\"")) |
| 202 | + || (prompt.startsWith("'") && prompt.endsWith("'"))) { |
| 203 | + prompt = prompt.substring(1, prompt.length() - 1); |
| 204 | + } |
| 205 | + if (!prompts.contains(prompt)) { |
| 206 | + prompts.add(prompt); |
| 207 | + } |
| 208 | + } |
| 209 | + return prompts; |
| 210 | + } catch (IOException e) { |
| 211 | + LOG.warning("Failed to read snapshot file: " + e.getMessage()); |
| 212 | + return List.of(); |
| 213 | + } |
145 | 214 | } |
146 | 215 |
|
147 | 216 | /** |
|
0 commit comments