9dd9709e97
## What Adds the integration coverage `core.workflow` mirroring never had: create, rename, delete, restore and destroy, asserting the core row appears, follows the rename, disappears and comes back. No production code changes. The async `WorkflowCoreDualWriteListener` stays as the mirror for the workflow entity. ## Why this PR changed shape It originally replaced the listener with query hooks, to remove the last async best-effort write path. That was the wrong call, for three reasons found while reviewing it: 1. **It would have introduced drift, not removed it.** `workflow-trigger.workspace-service.ts` updates `lastPublishedVersionId` via `workflowRepository.update(...)` when a version is activated. That is a repository write, not an API mutation, so query hooks never fire for it and `core.workflow.lastPublishedVersionId` would have gone stale on **every workflow activation**. The consistency cron compares that field, so it would surface as `fieldMismatch` drift. The listener catches it because events are emitted at the ORM layer. 2. **Hooks are no more atomic than the listener here.** Workflow CRUD goes through the generic API, so there is no dedicated mutation to wrap and the generic runner holds no transaction: it commits, then runs post-hooks. Both approaches are post-commit, with the same drift guarantees. 3. **Events carry the full record; hook payloads do not.** `workspace-insert-query-builder` passes the full formatted result to the event while the client response is filtered by `returning`. That partial payload is what forced the re-fetches, the `coreWorkflowId` pre-hook injection, and the destroy pre-commit special case. All three were workarounds for a problem the listener does not have. The principle we settled on: **use the transaction where one exists, use the listener where there isn't one.** Post-hooks were the worst of both here. Version *content* writes keep their transactional mirror, since those go through dedicated mutations that own a transaction. ## Note on the test The mirror is asynchronous, so the assertions poll (20 attempts, 250ms) rather than reading core immediately after the mutation returns. Without that it would be racy. ## Verification - `nx typecheck twenty-server` green - `oxfmt` + `oxlint --type-aware` green