diff --git a/tests/runner-e2e/chat-catalog-contract.ts b/tests/runner-e2e/chat-catalog-contract.ts index fe0a01f0c5e..36e73816d07 100644 --- a/tests/runner-e2e/chat-catalog-contract.ts +++ b/tests/runner-e2e/chat-catalog-contract.ts @@ -40,6 +40,7 @@ const originalClockExecutionIds = new Set([ "chat-features.runner-opencode.local.navigation-preferences.quick-search", "chat-features.runner-opencode.local.navigation-preferences.edit-profile", "chat-features.runner-opencode.local.navigation-preferences.broken-link", + "chat-features.runner-opencode.local.navigation-preferences.dismiss-announcement", "chat-features.runner-opencode.local.projects.share-project", "chat-features.runner-opencode.local.access.cli-authorization", "chat-features.runner-opencode.local.access.other-company", diff --git a/tests/runner-e2e/chat-catalog-descriptors.json b/tests/runner-e2e/chat-catalog-descriptors.json index d14ae9b3a2f..6e9416beb07 100644 --- a/tests/runner-e2e/chat-catalog-descriptors.json +++ b/tests/runner-e2e/chat-catalog-descriptors.json @@ -3587,7 +3587,7 @@ "../../ui/src/pages/Projects.tsx", "../../ui/src/components/AnnouncementWell.tsx" ], - "oracleVersion": "paperclip.chat-catalog.announcement-dismissal.v2", + "oracleVersion": "paperclip.chat-catalog.announcement-dismissal.v3", "expectedRunCount": 1, "turnTimeoutMs": 120000, "attemptTimeoutMs": 300000, diff --git a/tests/runner-e2e/chat-catalog-domains/identity-announcement-original-clock.test.ts b/tests/runner-e2e/chat-catalog-domains/identity-announcement-original-clock.test.ts new file mode 100644 index 00000000000..201edad773e --- /dev/null +++ b/tests/runner-e2e/chat-catalog-domains/identity-announcement-original-clock.test.ts @@ -0,0 +1,56 @@ +import { afterEach, expect, it, vi } from "vitest"; +import type { ChatCatalogContext } from "../chat-catalog-contract.js"; +import { navigationBoundaryIdentityCases } from "./identity-navigation-boundaries.js"; + +const definition = navigationBoundaryIdentityCases.find(row => row.scenarioId === "navigation-preferences.dismiss-announcement")!; +const boundary = new Error("First actual publication read; no human action performed"); +function fixture(replyAt = 151_600, deadline: number | undefined = 300_000) { + const read = vi.fn().mockRejectedValue(boundary); + const ctx = { observationDeadlineAt: deadline, + input: { execution: { id: "chat-features.runner-opencode.local.navigation-preferences.dismiss-announcement" }, + page: { url: () => "http://fixture.invalid/ACO/chats/ceo" }, api: { get: read } }, + refreshIssue: async () => {}, + issue: () => ({ conversationState: Date.now() >= replyAt ? "waiting" : "active" }), + allRuns: async () => [{ id: "native", status: Date.now() >= replyAt ? "succeeded" : "running" }], + } as unknown as ChatCatalogContext; + return { ctx, read }; +} +afterEach(() => vi.useRealTimers()); +it("actual announcement act admits the native reply after151.6s within its original clock before human dismissal", async () => { + vi.useFakeTimers(); vi.setSystemTime(0); + const { ctx, read } = fixture(); + const pending = definition.act!(ctx, { publication: { id: "publication" } }).catch(error => error); + await vi.advanceTimersByTimeAsync(151_000); expect(read).not.toHaveBeenCalled(); + await vi.advanceTimersByTimeAsync(1000); expect(await pending).toBe(boundary); + expect(read).toHaveBeenCalledExactlyOnceWith("/api/announcements/current"); + expect(definition.attemptTimeoutMs).toBe(300_000); +}); +it("consumes elapsed setup instead of granting a fresh wait budget", async () => { + vi.useFakeTimers(); vi.setSystemTime(200_000); + const { ctx, read } = fixture(280_000, 270_000); + const pending = definition.act!(ctx, {}).catch(error => error); + await vi.advanceTimersByTimeAsync(70_500); + expect(String(await pending)).toContain("Timed out waiting for initial identity response"); + expect(read).not.toHaveBeenCalled(); +}); +it.each(["failed", "cancelled", "timed_out"])("does not turn a %s native run into a human dismissal", async status => { + vi.useFakeTimers(); vi.setSystemTime(0); + const { ctx, read } = fixture(); ctx.allRuns = async () => [{ status }] as never; + await expect(definition.act!(ctx, {})).rejects.toThrow("Initial response failed"); + expect(read).not.toHaveBeenCalled(); +}); +it.each([undefined, NaN, Infinity, 200_000, 199_999])("rejects missing or exhausted original deadline %s before observing", async deadline => { + vi.useFakeTimers(); vi.setSystemTime(200_000); + const { ctx, read } = fixture(0); ctx.observationDeadlineAt = deadline; + await expect(definition.act!(ctx, {})).rejects.toThrow("Original native observation budget exhausted or missing"); + expect(read).not.toHaveBeenCalled(); +}); +it("rejects a successful observation that completes after the original deadline", async () => { + vi.useFakeTimers(); vi.setSystemTime(269_000); + const { ctx, read } = fixture(0, 270_000); + ctx.refreshIssue = async () => { vi.setSystemTime(270_001); }; + const pending = definition.act!(ctx, {}).catch(error => error); + await vi.advanceTimersByTimeAsync(1500); + expect(String(await pending)).toContain("Timed out waiting for initial identity response"); + expect(read).not.toHaveBeenCalled(); +}); diff --git a/tests/runner-e2e/chat-catalog-domains/identity-navigation-boundaries.ts b/tests/runner-e2e/chat-catalog-domains/identity-navigation-boundaries.ts index 76783899c28..9c137dfe1ef 100644 --- a/tests/runner-e2e/chat-catalog-domains/identity-navigation-boundaries.ts +++ b/tests/runner-e2e/chat-catalog-domains/identity-navigation-boundaries.ts @@ -122,9 +122,9 @@ export const navigationBoundaryIdentityCases: readonly ChatCatalogCase[] = [ observerBeforePublicationId: observerPublication?.id, invalidAuditCompanyStatus }; } catch (error) { try { await observer.dispose(); } finally { observers.delete(ctx); const id = otherCompanies.get(ctx); if (id) await ctx.input.api.delete(`/api/companies/${id}`, { allowNotFound: true }); otherCompanies.delete(ctx); } throw error; } }, gradeNavigationBoundary, { - sourceFiles: [...sources, "chat-announcement-fixture.ts", "harness-env.ts", "../../packages/shared/src/announcements.ts", "../../server/src/services/announcement-feed.ts", "../../server/src/routes/announcements.ts", "../../ui/src/pages/Projects.tsx", "../../ui/src/components/AnnouncementWell.tsx"], authentication: "human-session", oracleVersion: "paperclip.chat-catalog.announcement-dismissal.v2", + sourceFiles: [...sources, "chat-announcement-fixture.ts", "harness-env.ts", "../../packages/shared/src/announcements.ts", "../../server/src/services/announcement-feed.ts", "../../server/src/routes/announcements.ts", "../../ui/src/pages/Projects.tsx", "../../ui/src/components/AnnouncementWell.tsx"], authentication: "human-session", oracleVersion: "paperclip.chat-catalog.announcement-dismissal.v3", async act(ctx, state) { - await waitForReply(ctx); const publication = state.publication as Publication; const source = ctx.input.page.url(); + await waitForReply(ctx, chatCatalogOriginalPhaseDeadline(ctx)); const publication = state.publication as Publication; const source = ctx.input.page.url(); state.beforeHumanPublicationId = (await ctx.input.api.get("/api/announcements/current"))?.id; state.agentDismissStatus = await probeAgentPermission(ctx, `/api/announcements/${encodeURIComponent(publication.id)}/dismiss`, "POST", { companyId: ctx.input.fixtures.company.id }); const response = ctx.input.page.waitForResponse(row => row.request().method() === "POST" && new URL(row.url()).pathname === `/api/announcements/${publication.id}/dismiss`);