Skip to content

fix(seed): seeded audit events carry "(unresolved)" as the actor, now visible on record History columns #500

Closed

Description

@mforce

Problem

Seeded records carry no usable actor on the audit trail. Every audit event written during seed --profile demo|simulation is stamped ActorEmail = "(unresolved)".

Measured against the demo seeder (probe run, 2026-08-10):

DailyEntry.Create x248 actor='(unresolved)'
Flock.Create       x3  actor='(unresolved)'
Product.Create     x3  actor='(unresolved)'
SalesOrder.Create  x2  actor='(unresolved)'

Why it happens

AuditWriter (src/Cluckwork.Infrastructure/Repositories/AuditWriter.cs) resolves its actor from ICurrentUser:

var actorId = user.IsResolved ? user.UserId : Guid.Empty;
var actorEmail = user.IsResolved ? user.Email : "(unresolved)";

Both seeders (DemoDataSeeder, SimulationDataSeeder) call tenant.Resolve(accountId) before invoking handlers — which is why the tenant guard doesn't throw — but neither ever calls currentUser.Resolve(...). CurrentUserContext.IsResolved is only ever set by TenantResolutionMiddleware on an HTTP request, so the CLI seed path always takes the fallback.

Why it matters now

This predates #494 — Product.Create and User.Create were already writing (unresolved) — but it was only ever visible on the admin-only /audit page.

#494 surfaces it on five main screens. The new "History" column on Flocks, Egg grades, Daily entry history, Sales and Expenses renders it verbatim, so demo data now reads "Created by (unresolved) on 2026-…" on ~256 rows. Anyone demoing the product sees it immediately.

Scope

  • Resolve ICurrentUser in both seeders before invoking any handler.
    • Demo: attribute to the default account's Owner.
    • Simulation: attribute to the persona that would realistically have done it — the seeder already creates sim-manager-N@, sim-sales-N@, sim-worker-N@ and an Owner, so daily entries can be attributed to workers, sales orders to sales personas, etc. Higher fidelity is the whole point of that fixture.
  • Add a test pinning that seeded rows carry a real actor — i.e. that no seeded audit event has ActorEmail = "(unresolved)". This is the guard that stops the fallback silently returning.
  • Consider whether "(unresolved)" should remain reachable at all for a tenant-resolved caller, or become a loud failure — a real request can never hit it (auth precedes tenant resolution), so today it is exclusively a seeder artifact.

Out of scope

Notes

Not a regression and nothing is broken: seeders don't throw, the full backend suite passes, and the k6 / Playwright harnesses are unaffected (k6 checks are presence-based; the one index-based Playwright selector is on the Audit table, untouched by #494).

Found while implementing #494; deliberately split out to keep that PR scoped.

Activity

  1. added 6 commits that reference this issue on Aug 12, 2026
    b51a756
    6b75b2e
    0d64ac0
    4454761
    e2db4d9
    3db184f
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Metadata

Metadata

Assignees

No one assigned

    Labels

    Projects

    No projects

      Milestone

      No milestone

      Relationships

      None yet

      Development

      No branches or pull requests

      Issue actions