Skip to content

Commit f280d2a

Browse files
Expand replay special actions with load-state waits
Co-authored-by: Shri Sukhani <shrisukhani@users.noreply.github.com>
1 parent 97aff6a commit f280d2a

2 files changed

Lines changed: 80 additions & 4 deletions

File tree

src/agent/shared/replay-special-actions.test.ts

Lines changed: 38 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -26,6 +26,7 @@ function createPage(overrides?: Record<string, unknown>) {
2626
goto: jest.fn().mockResolvedValue(undefined),
2727
reload: jest.fn().mockResolvedValue(undefined),
2828
waitForTimeout: jest.fn().mockResolvedValue(undefined),
29+
waitForLoadState: jest.fn().mockResolvedValue(undefined),
2930
...overrides,
3031
};
3132
}
@@ -144,6 +145,43 @@ describe("executeReplaySpecialAction", () => {
144145
expect(result).toBeNull();
145146
});
146147

148+
it("replays waitForLoadState with timeout argument", async () => {
149+
const page = createPage();
150+
151+
const result = await executeReplaySpecialAction({
152+
taskId: "task-loadstate",
153+
actionType: "waitForLoadState",
154+
arguments: ["networkidle", 2500],
155+
page: page as unknown as Page,
156+
});
157+
158+
expect(page.waitForLoadState).toHaveBeenCalledWith("networkidle", {
159+
timeout: 2500,
160+
});
161+
expect(markDomSnapshotDirty).toHaveBeenCalledWith(page);
162+
expect(result?.status).toBe("completed");
163+
expect(result?.output).toBe("Waited for load state: networkidle");
164+
});
165+
166+
it("fails extract replay when extracted object cannot be serialized", async () => {
167+
const circular: Record<string, unknown> = {};
168+
circular.self = circular;
169+
const extract = jest.fn().mockResolvedValue(circular);
170+
const page = createPage({
171+
extract,
172+
});
173+
174+
const result = await executeReplaySpecialAction({
175+
taskId: "task-circular-extract",
176+
actionType: "extract",
177+
instruction: "extract circular object",
178+
page: page as unknown as Page,
179+
});
180+
181+
expect(result?.status).toBe("failed");
182+
expect(result?.output).toContain("could not serialize extracted output");
183+
});
184+
147185
it("honors explicit retry metadata value", async () => {
148186
const page = createPage();
149187

src/agent/shared/replay-special-actions.ts

Lines changed: 42 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -18,6 +18,7 @@ export const REPLAY_SPECIAL_ACTION_TYPES: ReadonlySet<string> = new Set([
1818
"complete",
1919
"refreshPage",
2020
"wait",
21+
"waitForLoadState",
2122
"extract",
2223
"analyzePdf",
2324
]);
@@ -55,6 +56,10 @@ function asNumber(value: unknown): number | undefined {
5556
return undefined;
5657
}
5758

59+
function asFiniteNumber(value: unknown): number | undefined {
60+
return typeof value === "number" && Number.isFinite(value) ? value : undefined;
61+
}
62+
5863
function normalizeWaitMs(value: unknown): number {
5964
const parsed = asNumber(value);
6065
if (parsed === undefined) {
@@ -163,14 +168,28 @@ export async function executeReplaySpecialAction(
163168
}
164169
try {
165170
const extracted = await extractPage.extract(extractInstruction);
171+
let serializedExtracted = "";
172+
if (typeof extracted === "string") {
173+
serializedExtracted = extracted;
174+
} else {
175+
try {
176+
serializedExtracted = JSON.stringify(extracted);
177+
} catch (error) {
178+
const message = error instanceof Error ? error.message : String(error);
179+
return {
180+
taskId,
181+
status: TaskStatus.FAILED,
182+
steps: [],
183+
output: `Extract failed: could not serialize extracted output (${message})`,
184+
replayStepMeta: createReplayMeta(retries),
185+
};
186+
}
187+
}
166188
return {
167189
taskId,
168190
status: TaskStatus.COMPLETED,
169191
steps: [],
170-
output:
171-
typeof extracted === "string"
172-
? extracted
173-
: JSON.stringify(extracted),
192+
output: serializedExtracted,
174193
replayStepMeta: createReplayMeta(retries),
175194
};
176195
} catch (error) {
@@ -195,5 +214,24 @@ export async function executeReplaySpecialAction(
195214
};
196215
}
197216

217+
if (actionType === "waitForLoadState") {
218+
const waitUntil = asNonEmptyTrimmedString(actionArgs?.[0]) ?? "domcontentloaded";
219+
const timeoutMs = asFiniteNumber(actionArgs?.[1]);
220+
const options =
221+
timeoutMs !== undefined ? { timeout: timeoutMs } : undefined;
222+
await page.waitForLoadState(
223+
waitUntil as "domcontentloaded" | "load" | "networkidle",
224+
options
225+
);
226+
markDomSnapshotDirty(page);
227+
return {
228+
taskId,
229+
status: TaskStatus.COMPLETED,
230+
steps: [],
231+
output: `Waited for load state: ${waitUntil}`,
232+
replayStepMeta: createReplayMeta(retries),
233+
};
234+
}
235+
198236
return null;
199237
}

0 commit comments

Comments
 (0)