fix: scope status-card deletion notifications to its company

This commit is contained in:
Dotta committed 2026-10-30 19:00:51 -05:00
1 parent cd4caaab26
commit 32044a63e1
3 files changed
+42 -2

No files matched your search

@@ -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.
@@ -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;
+4 -2
View File
@@ -158,7 +158,7 @@ export function statusCardService(
const issuesSvc = () => deps.issuesSvc ?? issueService(db);
const searchSvc = companySearchService(db);
type Transaction = Parameters<Parameters<Db["transaction"]>[0]>[0];
function mutate<T>(companyId: string | undefined, write: (tx: Transaction) => Promise<T>) {
function mutate<T>(companyId: string, write: (tx: Transaction) => Promise<T>) {
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) {