From 32044a63e138e7b6a38ec801878d0db65cce6bbe Mon Sep 17 00:00:00 2001 From: Dotta Date: Fri, 30 Oct 2026 19:00:51 -0500 Subject: [PATCH] fix: scope status-card deletion notifications to its company --- doc/plans/2026-10-30-status-cards-sleep.md | 6 ++++ .../status-card-evaluation-work.test.ts | 32 +++++++++++++++++++ server/src/services/status-cards.ts | 6 ++-- 3 files changed, 42 insertions(+), 2 deletions(-) diff --git a/doc/plans/2026-10-30-status-cards-sleep.md b/doc/plans/2026-10-30-status-cards-sleep.md index 5b98962698b..61d451caed5 100644 --- a/doc/plans/2026-10-30-status-cards-sleep.md +++ b/doc/plans/2026-10-30-status-cards-sleep.md @@ -135,3 +135,9 @@ scheduled work after settlement. Real issue/comment producer acceptance measures due/next queries and actual searches under 100 writes, preserves both companies' hints, dispatches another company's saved deadline inside the batching window, and checks SQL quietness after authorized results. + +Card deletion resolves the card's owning company and emits only that company's +deadline-reconstruction hint. Every card mutation requires a company ID; only +instance-level settings retain wildcard ingress. Deleting an already-missing +card emits no hint. The real two-company regression proves deletion does not +re-run unchanged reactive searches or wake either summarizer, then stays quiet. diff --git a/server/src/__tests__/status-card-evaluation-work.test.ts b/server/src/__tests__/status-card-evaluation-work.test.ts index 6bb466dea89..e4588b16be1 100644 --- a/server/src/__tests__/status-card-evaluation-work.test.ts +++ b/server/src/__tests__/status-card-evaluation-work.test.ts @@ -144,6 +144,38 @@ const support = external ? { supported: true } : await getEmbeddedPostgresTestSu expect(errors).toEqual([]); }); + it("keeps card deletion scoped to its company and a missing deletion quiet", async () => { + const removed = await card(); + await card(); + const ownCompany = companyId, ownAgent = agentId; + companyId = randomUUID(); agentId = randomUUID(); + await db.insert(companies).values({ id: companyId, name: "Other", issuePrefix: `O${companyId.slice(0, 6)}` }); + await db.insert(agents).values({ id: agentId, companyId, name: "Other summarizer", role: "general", status: "idle", adapterType: "process", adapterConfig: {} }); + const other = await card(); + companyId = ownCompany; agentId = ownAgent; + const factory = companySearch.companySearchService; + const searchedCompanies: string[] = []; + vi.spyOn(companySearch, "companySearchService").mockImplementation(owner => { + const search = factory(owner), actual = search.search; + search.search = async (...args) => { searchedCompanies.push(args[0]); return actual(...args); }; + return search; + }); + await start(); await quiet(); searchedCompanies.length = 0; + const cards = statusCardService(db); + expect(await cards.remove(removed.id)).toMatchObject({ id: removed.id, companyId: ownCompany }); + await settled(); + // Deleting a card only reconstructs deadlines; it is not an issue change + // that warrants re-running any unchanged reactive card's saved search. + expect(searchedCompanies).toEqual([]); + expect(await cards.getById(other.id)).toMatchObject({ nextEvalAt: null, generatingIssueId: null, pendingChangeCount: 0 }); + expect(wakeup).not.toHaveBeenCalled(); + searchedCompanies.length = 0; + expect(await cards.remove(removed.id)).toBeNull(); + await quiet(); + expect(searchedCompanies).toEqual([]); + expect(errors).toEqual([]); + }); + it("coalesces one hundred committed source writes without delaying another company's saved deadline", async () => { const reactive = await card(); const originalCompany = companyId, originalAgent = agentId; diff --git a/server/src/services/status-cards.ts b/server/src/services/status-cards.ts index f9e6372ea2e..d4660227c28 100644 --- a/server/src/services/status-cards.ts +++ b/server/src/services/status-cards.ts @@ -158,7 +158,7 @@ export function statusCardService( const issuesSvc = () => deps.issuesSvc ?? issueService(db); const searchSvc = companySearchService(db); type Transaction = Parameters[0]>[0]; - function mutate(companyId: string | undefined, write: (tx: Transaction) => Promise) { + function mutate(companyId: string, write: (tx: Transaction) => Promise) { return db.transaction(async tx => { await notifyStatusCardEvaluation(tx, companyId); return write(tx); @@ -335,7 +335,9 @@ export function statusCardService( } async function remove(id: string) { - return mutate(undefined, tx => tx.delete(statusCards).where(eq(statusCards.id, id)).returning().then((rows) => rows[0] ?? null)); + const card = await getById(id); + if (!card) return null; + return mutate(card.companyId, tx => tx.delete(statusCards).where(eq(statusCards.id, id)).returning().then((rows) => rows[0] ?? null)); } async function listUpdates(cardId: string) {