fix(status-cards): remove a new card only when its compile cannot be queued

The create route's cleanup also wrapped the activity record written after
the compile was queued, so a logging failure deleted the card and left
its generation task queued. Only enqueueCompile failures now remove the
card; a regression test fails the creation activity record and checks
that the card and its uncancelled generation task remain.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
This commit is contained in:
DottaandClaude Opus 5.5 committed 2026-11-07 22:43:30 -06:00
1 parent cfe3decc7c
commit b3f8bbfdb9
2 files changed
+48 -5

No files matched your search

+41
View File
@@ -194,6 +194,47 @@ describeEmbeddedPostgres("status card routes", () => {
expect(await db.select().from(issues)).toEqual([]);
});
it("keeps a card with a queued compile when its activity record fails", async () => {
const company = await seedCompany();
await enableStatusCards();
await seedSummarizer(company.id);
// Fail only the card's creation activity record, which is written after the compile is queued.
const failingActivityDb = new Proxy(db, {
get(target, property) {
if (property === "insert") {
return (table: unknown) => {
const builder = target.insert(table as never);
if (table !== activityLog) return builder;
return new Proxy(builder, {
get(inner, key) {
if (key === "values") {
return (values: { action?: string }) => {
if (values?.action === "status_card.created") throw new Error("activity log unavailable");
return inner.values(values as never);
};
}
const value = Reflect.get(inner, key, inner);
return typeof value === "function" ? value.bind(inner) : value;
},
});
};
}
const value = Reflect.get(target, property, target);
return typeof value === "function" ? value.bind(target) : value;
},
}) as Db;
const response = await request(createApp(failingActivityDb, localBoardActor()))
.post(`/api/companies/${company.id}/status-cards`)
.send({ interestPrompt: "Recently updated launch tasks" });
expect(response.status).toBe(500);
const cards = await db.select().from(statusCards);
expect(cards).toHaveLength(1);
const generation = await db.select().from(issues).where(eq(issues.id, cards[0]!.generatingIssueId!)).then((rows) => rows[0]);
expect(generation).toBeDefined();
expect(generation!.status).not.toBe("cancelled");
});
it("creates, patches, archives, restores, lists updates, and deletes a card", async () => {
const company = await seedCompany();
await enableStatusCards();
+7 -5
View File
@@ -172,21 +172,23 @@ export function statusCardRoutes(db: Db, opts: { heartbeat?: IssueAssignmentWake
await assertCanMutate(req, companyId);
assertAgentPromptLimit(req, req.body.interestPrompt);
const actor = getActorInfo(req);
// Resolve the link before creating the card: the catch below deletes the card, so nothing that
// runs after the compile is queued may fail there.
// Resolve the link before creating the card, so a failed lookup leaves nothing behind.
const issuePrefix = await companyIssuePrefix(companyId);
const card = await service.create(companyId, req.body, {
agentId: actor.actorType === "agent" ? actor.actorId : null,
userId: actor.actorType === "user" ? actor.actorId : null,
});
// Only a failure to queue the compile removes the new card. Once the compile is queued, the card
// and its generation task exist together, so later failures must not delete the card alone.
let compile: Awaited<ReturnType<typeof enqueueCompile>>;
try {
const compile = await enqueueCompile(req, card.id);
await logMutation(req, companyId, "status_card.created", card.id, { state: card.state });
res.status(201).json(withHref(publicCard(compile.card), issuePrefix));
compile = await enqueueCompile(req, card.id);
} catch (error) {
await service.remove(card.id);
throw error;
}
await logMutation(req, companyId, "status_card.created", card.id, { state: card.state });
res.status(201).json(withHref(publicCard(compile.card), issuePrefix));
});
router.get("/status-cards/:id", async (req, res) => {