feat(agent): support Responses steering and project-aware tools
Use the dedicated TJWATER_AGENT_URL and Responses stream protocol. Keep running turns active while users steer or queue follow-ups and edit pending delivery. Refresh the model catalog before generation and preserve approval mode selection. Integrate scoped investigation messaging/presentation, geographic place selection, current Agent composer styles and supporting workbench contracts. Validation: 77 focused Vitest checks, 21 browser cases, lint, typecheck and production build. Preserve direct Server/Agent routing and project authentication.
This commit is contained in:
@@ -0,0 +1,95 @@
|
||||
import { expect, test } from "@playwright/test";
|
||||
import { mockAgentApi } from "./support/mock-agent-api";
|
||||
import { mockWorkbenchContext } from "./support/mock-workbench-context";
|
||||
|
||||
type SyncProbe = {
|
||||
available: boolean;
|
||||
connections: number;
|
||||
cursors: string[];
|
||||
warnings: number;
|
||||
disconnect: (scheduled: boolean) => void;
|
||||
};
|
||||
|
||||
declare global {
|
||||
interface Window { syncProbe: SyncProbe }
|
||||
}
|
||||
|
||||
test("scheduled renewals stay silent while a sustained outage warns and recovers", async ({ page }) => {
|
||||
await mockWorkbenchContext(page);
|
||||
await mockAgentApi(page);
|
||||
await page.route("**/api/v1/workbench/sync-snapshot", (route) => route.fulfill({ json: {
|
||||
schema: "workbench-sync-snapshot@1", event_cursor: 41, generated_at: "2026-10-12T02:00:00Z",
|
||||
runtime: { availability: "not_configured", source: "none", scenario_availability: "not_configured",
|
||||
artifact_availability: "not_configured", schedule_availability: "not_configured",
|
||||
work_order_availability: "not_configured", alert_availability: "not_configured",
|
||||
generated_at: "2026-10-12T02:00:00Z", conditions: [], alerts: [], scenarios: [],
|
||||
latest_analysis: null, work_orders: [] }, tasks: [], notifications: []
|
||||
} }));
|
||||
await page.addInitScript(() => {
|
||||
const originalFetch = window.fetch.bind(window);
|
||||
let stream: ReadableStreamDefaultController<Uint8Array> | undefined;
|
||||
window.syncProbe = {
|
||||
available: true, connections: 0, cursors: [], warnings: 0,
|
||||
disconnect: (scheduled) => {
|
||||
if (!stream) throw new Error("No active event stream");
|
||||
if (scheduled) {
|
||||
stream.enqueue(new TextEncoder().encode("event: reconnect\ndata: {}\n\n"));
|
||||
stream.close();
|
||||
} else {
|
||||
stream.error(new Error("Synthetic connection loss"));
|
||||
}
|
||||
stream = undefined;
|
||||
}
|
||||
};
|
||||
new MutationObserver(() => {
|
||||
if (document.body?.textContent?.includes("业务同步正在重连")) window.syncProbe.warnings += 1;
|
||||
}).observe(document, { subtree: true, childList: true, characterData: true });
|
||||
window.fetch = async (input, init) => {
|
||||
const url = typeof input === "string" ? input : input instanceof URL ? input.href : input.url;
|
||||
if (!url.includes("/api/v1/workbench/events")) return originalFetch(input, init);
|
||||
window.syncProbe.cursors.push(new Headers(init?.headers).get("Last-Event-ID") ?? "");
|
||||
if (!window.syncProbe.available) return new Response("{}", { status: 503 });
|
||||
window.syncProbe.connections += 1;
|
||||
const body = new ReadableStream<Uint8Array>({
|
||||
start(controller) { stream = controller; },
|
||||
cancel() { stream = undefined; }
|
||||
});
|
||||
return new Response(body, { headers: { "Content-Type": "text/event-stream" } });
|
||||
};
|
||||
});
|
||||
const now = new Date("2026-10-12T02:00:00Z");
|
||||
await page.clock.install({ time: now });
|
||||
await page.clock.pauseAt(now);
|
||||
await page.goto("/", { waitUntil: "domcontentloaded" });
|
||||
await expect.poll(() => page.evaluate(() => window.syncProbe.connections), { timeout: 15_000 }).toBe(1);
|
||||
|
||||
for (let cycle = 0; cycle < 3; cycle += 1) {
|
||||
await page.evaluate(() => window.syncProbe.disconnect(true));
|
||||
await page.clock.runFor(1_100);
|
||||
await expect.poll(() => page.evaluate(() => window.syncProbe.connections)).toBe(cycle + 2);
|
||||
expect(await page.evaluate(() => window.syncProbe.warnings)).toBe(0);
|
||||
}
|
||||
|
||||
await page.evaluate(() => {
|
||||
window.syncProbe.available = false;
|
||||
window.syncProbe.disconnect(false);
|
||||
});
|
||||
await page.clock.runFor(14_000);
|
||||
await expect(page.getByText("业务同步正在重连", { exact: true })).toHaveCount(0);
|
||||
await page.clock.runFor(1_100);
|
||||
await expect(page.getByText("业务同步正在重连", { exact: true })).toBeVisible();
|
||||
await page.clock.runFor(500);
|
||||
expect(await page.evaluate(() => window.syncProbe.warnings)).toBeGreaterThan(0);
|
||||
const notice = page.getByText("业务同步正在重连", { exact: true });
|
||||
await expect(notice.locator("xpath=ancestor::li[@data-sonner-toast]")).toHaveCSS("opacity", "1");
|
||||
await expect(notice).toBeInViewport();
|
||||
await page.screenshot({ path: test.info().outputPath("sustained-outage.png") });
|
||||
|
||||
await page.evaluate(() => { window.syncProbe.available = true; });
|
||||
await page.clock.runFor(16_000);
|
||||
await expect.poll(() => page.evaluate(() => window.syncProbe.connections)).toBe(5);
|
||||
await page.clock.runFor(500);
|
||||
await expect(page.getByText("业务同步正在重连", { exact: true })).toHaveCount(0);
|
||||
expect(await page.evaluate(() => window.syncProbe.cursors.every((cursor) => cursor === "41"))).toBe(true);
|
||||
await page.screenshot({ path: test.info().outputPath("recovered.png") });
|
||||
});
|
||||
Reference in New Issue
Block a user