M1: audit log (Governance) + edit-distance metric
SharedKernel: - IAuditLog/AuditEvent — append-only audit contract any module writes through. - EditDistance (Levenshtein + normalized) — the north-star metric, available from day one; consumed at edit-and-approve in M5. Governance module (references SharedKernel only): - AuditEntry entity; internal GovernanceDbContext (schema "governance") + InitialGovernance migration; AuditLog implements IAuditLog. - GET /api/governance/audit — owner-only (ViewAuditLog), returns recent entries. Wiring (via the SharedKernel IAuditLog interface — no module references Governance): - OrgBoard records team.created, task.created, task.moved, task.assigned. - Identity records invitation.created, member.joined. Verified: build green; ArchitectureTests 8/8 (Governance references only SharedKernel; audit flows through the shared interface); IntegrationTests 20/20 — board flow now asserts task.created/task.moved appear in the audit log, plus EditDistance unit tests. Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
This commit is contained in:
@@ -30,6 +30,10 @@ public sealed class BoardFlowTests(PostgresFixture postgres) : IClassFixture<Pos
|
||||
|
||||
private sealed record BoardResponse(Guid TeamId, List<BoardColumn> Columns);
|
||||
|
||||
private sealed record AuditEntryResponse(
|
||||
Guid Id, string Action, string EntityType, Guid EntityId,
|
||||
Guid? ActorMemberId, string? Details, DateTimeOffset OccurredAtUtc);
|
||||
|
||||
[Fact]
|
||||
public async Task Owner_builds_board_and_member_is_scoped()
|
||||
{
|
||||
@@ -76,6 +80,12 @@ public sealed class BoardFlowTests(PostgresFixture postgres) : IClassFixture<Pos
|
||||
var cartable = await ownerClient.GetFromJsonAsync<List<TaskResponse>>("/api/orgboard/cartable");
|
||||
Assert.Contains(cartable!, i => i.Id == task.Id);
|
||||
|
||||
// The audit log (owner-only) recorded the task actions.
|
||||
var audit = await ownerClient.GetFromJsonAsync<List<AuditEntryResponse>>(
|
||||
$"/api/governance/audit?organizationId={owner.OrganizationId}");
|
||||
Assert.Contains(audit!, e => e.Action == "task.created" && e.EntityId == task.Id);
|
||||
Assert.Contains(audit!, e => e.Action == "task.moved" && e.EntityId == task.Id);
|
||||
|
||||
// Invite a Member at the org scope and accept.
|
||||
var invite = await PostOk<InviteResponse>(ownerClient, "/api/identity/invitations", new
|
||||
{
|
||||
|
||||
@@ -0,0 +1,26 @@
|
||||
using TeamUp.SharedKernel.Metrics;
|
||||
using Xunit;
|
||||
|
||||
namespace TeamUp.IntegrationTests;
|
||||
|
||||
/// <summary>Unit coverage for the north-star metric helper (no database).</summary>
|
||||
public sealed class EditDistanceTests
|
||||
{
|
||||
[Theory]
|
||||
[InlineData("", "", 0)]
|
||||
[InlineData("abc", "abc", 0)]
|
||||
[InlineData("", "abc", 3)]
|
||||
[InlineData("abc", "", 3)]
|
||||
[InlineData("kitten", "sitting", 3)]
|
||||
[InlineData("spec v1", "spec v2", 1)]
|
||||
public void Levenshtein_counts_edits(string a, string b, int expected) =>
|
||||
Assert.Equal(expected, EditDistance.Levenshtein(a, b));
|
||||
|
||||
[Fact]
|
||||
public void Normalized_is_zero_when_unchanged() =>
|
||||
Assert.Equal(0d, EditDistance.Normalized("approved as-is", "approved as-is"));
|
||||
|
||||
[Fact]
|
||||
public void Normalized_is_between_zero_and_one() =>
|
||||
Assert.InRange(EditDistance.Normalized("the AI wrote this", "the human edited this"), 0d, 1d);
|
||||
}
|
||||
Reference in New Issue
Block a user