diff --git a/scripts/e2e/chat.test.ts b/scripts/e2e/chat.test.ts index 952b0f1a5..9e1a7482f 100644 --- a/scripts/e2e/chat.test.ts +++ b/scripts/e2e/chat.test.ts @@ -7,15 +7,15 @@ // The path proven: database setup (chat migrations apply) → hub boot // → sidecar boot → two sign-ups in one tenant (an invited principal // activated by the owner) → an inference catalog chain seeded with a -// placeholder key → a workbench launched (the anchor instance boots -// in-process, the go/no-go test) → both users posting messages and +// placeholder key → a workbench created (data only since CL-6330 — no +// host run, the go/no-go test) → both users posting messages and // reading back the converged, decoded timeline with sender identity → -// a second message proving the anchor keeps accepting mail with no -// relaunch → a settings patch that both updates the record and -// appends an audit event to the timeline → independent per-user -// read-state cursors → a second workbench's address mentioned in the -// first, fanning a copy into the mentioned run's mailbox and driving -// that run → the workbench kind filter. +// a second message proving the room keeps accepting mail → a settings +// patch that both updates the record and appends an audit event to +// the timeline → independent per-user read-state cursors → mentioning +// an already-resident agent participant twice, fanning each mention +// into its existing run and never minting a sibling (CL-6451) → the +// workbench kind filter. // // Structured as one shared-boot stack (`beforeAll`) with a separate // `test` per capability, rather than one long test: a real defect @@ -29,7 +29,6 @@ import { beforeAll, describe, expect, test } from "bun:test"; -import { formatRunAddress } from "@intx/types"; import { createHubAPI, seedCatalog, @@ -581,40 +580,100 @@ describe.skipIf(databaseUrl === undefined)("chat e2e", () => { ); }); + // CL-6330 deleted the workbench-anchor machinery: a bare `kind: + // "workbench"` room mints only a child tenant and settings rows — no + // host workflow, no run, nothing `/workflows/runs/:runId/events` can + // ever resolve. Only an invited agent (`kind: "chat"` or `POST + // .../invite`) is backed by a real `workflow_run`. This test used to + // mention a second bare workbench and poll its (nonexistent) run, + // which is exactly the 404 CL-6436 tracks. It now proves the CL-6451 + // contract this test was actually meant to guard: mentioning a + // participant already resident in the room drives that participant's + // existing run — twice, never minting a sibling. test("mention fan-out drives the mentioned run", async () => { - const secondWorkbench = await createWorkbench({ - kind: "workbench", - name: "mentioned", + const mentioned = await createWorkbench({ + kind: "chat", + definitionId: await echoDefinitionId(), }); - expectStatus("create second workbench", secondWorkbench, 201); - const secondWorkbenchId = stringField( - secondWorkbench.data, + expectStatus("create mentioned agent chat", mentioned, 201); + const mentionedWorkbenchId = stringField( + mentioned.data, "id", - "create second workbench", + "create mentioned agent chat", + ); + const mentionedParticipants = arrayField( + mentioned.data, + "participants", + "create mentioned agent chat", + ) as { address: string; handle: string }[]; + const echoParticipant = mentionedParticipants.find( + (participant) => participant.handle === "echo", ); - const secondWorkbenchAddress = formatRunAddress(secondWorkbenchId, domain); + if (echoParticipant === undefined) { + throw new Error( + `agent chat has no "echo" participant: ${JSON.stringify(mentionedParticipants)}`, + ); + } + const echoLocalPart = echoParticipant.address.split("@")[0]; + if (echoLocalPart === undefined || echoLocalPart === "") { + throw new Error(`malformed echo address: ${echoParticipant.address}`); + } - const patched = await api( - "PATCH", - `/api/tenants/${tenantId}/chat/workbenches/${workbenchId}/settings`, - { "chat/participants": [secondWorkbenchAddress] }, + const beforeFirst = highestSeq( + await runEvents(user1.cookies, echoLocalPart), + ); + await postMessage( user1.cookies, + mentionedWorkbenchId, + `hey @echo take a look ${crypto.randomUUID()}`, ); - expectStatus("add mentionable participant", patched, 200); - - const before = highestSeq( - await runEvents(user1.cookies, secondWorkbenchId), + const afterFirst = await waitForRunProgress( + user1.cookies, + echoLocalPart, + beforeFirst, ); + expect(afterFirst.length).toBeGreaterThan(0); - const mentionText = `hey @${secondWorkbenchId} take a look ${crypto.randomUUID()}`; - await postMessage(user1.cookies, workbenchId, mentionText); + // The resident-reuse claim (CL-6451): a second mention of the same + // already-resident participant drives the SAME run id further — + // never mints a sibling — so this polls that same `echoLocalPart` + // run again rather than any newly-discovered address, and confirms + // the room still lists exactly the one agent participant. + const beforeSecond = highestSeq( + await runEvents(user1.cookies, echoLocalPart), + ); + await postMessage( + user1.cookies, + mentionedWorkbenchId, + `hey @echo one more thing ${crypto.randomUUID()}`, + ); + const afterSecond = await waitForRunProgress( + user1.cookies, + echoLocalPart, + beforeSecond, + ); + expect(afterSecond.length).toBeGreaterThan(0); - const fresh = await waitForRunProgress( + const settingsAfterSecondMention = await api( + "GET", + `/api/tenants/${tenantId}/chat/workbenches/${mentionedWorkbenchId}/settings`, + undefined, user1.cookies, - secondWorkbenchId, - before, ); - expect(fresh.length).toBeGreaterThan(0); + expectStatus( + "get settings after second mention", + settingsAfterSecondMention, + 200, + ); + const participantsAfterSecondMention = arrayField( + settingsAfterSecondMention.data, + "participants", + "get settings after second mention", + ) as { address: string; handle: string }[]; + expect(participantsAfterSecondMention).toHaveLength(1); + expect(participantsAfterSecondMention[0]?.address).toBe( + echoParticipant.address, + ); }, 90_000); test("inviting the echo agent launches its own run, joins the workbench, and receives @mentions", async () => {