Files
natsnet/tools/NatsNet.PortTracker/Commands/TestCommands.cs
Joseph Doherty 7a338dd510 feat: add audit-verified status updates with override tracking
Status updates (feature/test update and batch-update) now verify the
requested status against Roslyn audit classification. Mismatches
require --override "reason" to force. Overrides are logged to a new
status_overrides table and reviewable via 'override list' command.
2026-02-27 05:50:15 -05:00

295 lines
14 KiB
C#

using System.CommandLine;
using NatsNet.PortTracker.Audit;
using NatsNet.PortTracker.Data;
namespace NatsNet.PortTracker.Commands;
public static class TestCommands
{
public static Command Create(Option<string> dbOption, Option<string> schemaOption)
{
var testCommand = new Command("test", "Manage unit tests");
// list
var listModule = new Option<int?>("--module") { Description = "Filter by module ID" };
var listStatus = new Option<string?>("--status") { Description = "Filter by status" };
var listCmd = new Command("list", "List unit tests");
listCmd.Add(listModule);
listCmd.Add(listStatus);
listCmd.SetAction(parseResult =>
{
var dbPath = parseResult.GetValue(dbOption)!;
var moduleId = parseResult.GetValue(listModule);
var status = parseResult.GetValue(listStatus);
using var db = new Database(dbPath);
var sql = "SELECT t.id, t.name, t.status, t.module_id, m.name as module_name, t.go_method, t.dotnet_method FROM unit_tests t LEFT JOIN modules m ON t.module_id = m.id";
var parameters = new List<(string, object?)>();
var clauses = new List<string>();
if (moduleId is not null)
{
clauses.Add("t.module_id = @module");
parameters.Add(("@module", moduleId));
}
if (status is not null)
{
clauses.Add("t.status = @status");
parameters.Add(("@status", status));
}
if (clauses.Count > 0)
sql += " WHERE " + string.Join(" AND ", clauses);
sql += " ORDER BY m.name, t.name";
var rows = db.Query(sql, parameters.ToArray());
Console.WriteLine($"{"ID",-5} {"Name",-30} {"Status",-15} {"Module",-20} {"Go Method",-25} {"DotNet Method",-25}");
Console.WriteLine(new string('-', 120));
foreach (var row in rows)
{
Console.WriteLine($"{row["id"],-5} {Truncate(row["name"]?.ToString(), 29),-30} {row["status"],-15} {Truncate(row["module_name"]?.ToString(), 19),-20} {Truncate(row["go_method"]?.ToString(), 24),-25} {Truncate(row["dotnet_method"]?.ToString(), 24),-25}");
}
Console.WriteLine($"\nTotal: {rows.Count} tests");
});
// show
var showId = new Argument<int>("id") { Description = "Test ID" };
var showCmd = new Command("show", "Show test details");
showCmd.Add(showId);
showCmd.SetAction(parseResult =>
{
var dbPath = parseResult.GetValue(dbOption)!;
var id = parseResult.GetValue(showId);
using var db = new Database(dbPath);
var tests = db.Query(
"SELECT t.*, m.name as module_name FROM unit_tests t LEFT JOIN modules m ON t.module_id = m.id WHERE t.id = @id",
("@id", id));
if (tests.Count == 0)
{
Console.WriteLine($"Test {id} not found.");
return;
}
var t = tests[0];
Console.WriteLine($"Test #{t["id"]}: {t["name"]}");
Console.WriteLine($" Module: #{t["module_id"]} ({t["module_name"]})");
Console.WriteLine($" Feature: {(t["feature_id"] is not null ? $"#{t["feature_id"]}" : "(none)")}");
Console.WriteLine($" Status: {t["status"]}");
Console.WriteLine($" Go File: {t["go_file"]}");
Console.WriteLine($" Go Class: {t["go_class"]}");
Console.WriteLine($" Go Method: {t["go_method"]}");
Console.WriteLine($" Go Line: {t["go_line_number"]}");
Console.WriteLine($" Go LOC: {t["go_line_count"]}");
Console.WriteLine($" .NET: {t["dotnet_project"]} / {t["dotnet_class"]} / {t["dotnet_method"]}");
Console.WriteLine($" Notes: {t["notes"]}");
var deps = db.Query(
"SELECT d.target_type, d.target_id, d.dependency_kind FROM dependencies d WHERE d.source_type = 'unit_test' AND d.source_id = @id",
("@id", id));
Console.WriteLine($"\n Dependencies ({deps.Count}):");
foreach (var d in deps)
Console.WriteLine($" -> {d["target_type"]} #{d["target_id"]} [{d["dependency_kind"]}]");
});
// update
var updateId = new Argument<int>("id") { Description = "Test ID" };
var updateStatus = new Option<string>("--status") { Description = "New status", Required = true };
var updateOverride = new Option<string?>("--override") { Description = "Override audit mismatch with this comment" };
var updateCmd = new Command("update", "Update test status (audit-verified)");
updateCmd.Add(updateId);
updateCmd.Add(updateStatus);
updateCmd.Add(updateOverride);
updateCmd.SetAction(parseResult =>
{
var dbPath = parseResult.GetValue(dbOption)!;
var id = parseResult.GetValue(updateId);
var status = parseResult.GetValue(updateStatus)!;
var overrideComment = parseResult.GetValue(updateOverride);
using var db = new Database(dbPath);
var indexer = AuditVerifier.BuildIndexer("unit_tests");
var verifications = AuditVerifier.VerifyItems(db, indexer, "unit_tests",
" WHERE id = @id", [("@id", (object?)id)], status);
if (verifications.Count == 0)
{
Console.WriteLine($"Test {id} not found.");
return;
}
if (!AuditVerifier.CheckAndReport(verifications, status, overrideComment))
return;
var affected = db.Execute("UPDATE unit_tests SET status = @status WHERE id = @id",
("@status", status), ("@id", id));
Console.WriteLine(affected > 0 ? $"Test {id} updated to '{status}'." : $"Test {id} not found.");
var mismatches = verifications.Where(r => !r.Matches).ToList();
if (mismatches.Count > 0 && overrideComment is not null)
AuditVerifier.LogOverrides(db, "unit_tests", mismatches, status, overrideComment);
});
// map
var mapId = new Argument<int>("id") { Description = "Test ID" };
var mapProject = new Option<string?>("--project") { Description = "Target .NET project" };
var mapClass = new Option<string?>("--class") { Description = "Target .NET test class" };
var mapMethod = new Option<string?>("--method") { Description = "Target .NET test method" };
var mapCmd = new Command("map", "Map test to .NET test method");
mapCmd.Add(mapId);
mapCmd.Add(mapProject);
mapCmd.Add(mapClass);
mapCmd.Add(mapMethod);
mapCmd.SetAction(parseResult =>
{
var dbPath = parseResult.GetValue(dbOption)!;
var id = parseResult.GetValue(mapId);
var project = parseResult.GetValue(mapProject);
var cls = parseResult.GetValue(mapClass);
var method = parseResult.GetValue(mapMethod);
using var db = new Database(dbPath);
var affected = db.Execute(
"UPDATE unit_tests SET dotnet_project = COALESCE(@project, dotnet_project), dotnet_class = COALESCE(@cls, dotnet_class), dotnet_method = COALESCE(@method, dotnet_method) WHERE id = @id",
("@project", project), ("@cls", cls), ("@method", method), ("@id", id));
Console.WriteLine(affected > 0 ? $"Test {id} mapped." : $"Test {id} not found.");
});
testCommand.Add(listCmd);
testCommand.Add(showCmd);
testCommand.Add(updateCmd);
testCommand.Add(mapCmd);
testCommand.Add(CreateBatchUpdate(dbOption));
testCommand.Add(CreateBatchMap(dbOption));
return testCommand;
}
private static Command CreateBatchUpdate(Option<string> dbOption)
{
var cmd = new Command("batch-update", "Bulk update test status (audit-verified)");
var idsOpt = BatchFilters.IdsOption();
var moduleOpt = BatchFilters.ModuleOption();
var statusOpt = BatchFilters.StatusOption();
var executeOpt = BatchFilters.ExecuteOption();
var setStatus = new Option<string>("--set-status") { Description = "New status to set", Required = true };
var setNotes = new Option<string?>("--set-notes") { Description = "Notes to set" };
var overrideOpt = new Option<string?>("--override") { Description = "Override audit mismatches with this comment" };
cmd.Add(idsOpt);
cmd.Add(moduleOpt);
cmd.Add(statusOpt);
cmd.Add(executeOpt);
cmd.Add(setStatus);
cmd.Add(setNotes);
cmd.Add(overrideOpt);
cmd.SetAction(parseResult =>
{
var dbPath = parseResult.GetValue(dbOption)!;
var ids = parseResult.GetValue(idsOpt);
var module = parseResult.GetValue(moduleOpt);
var status = parseResult.GetValue(statusOpt);
var execute = parseResult.GetValue(executeOpt);
var newStatus = parseResult.GetValue(setStatus)!;
var notes = parseResult.GetValue(setNotes);
var overrideComment = parseResult.GetValue(overrideOpt);
if (string.IsNullOrWhiteSpace(ids) && module is null && string.IsNullOrWhiteSpace(status))
{
Console.WriteLine("Error: at least one filter (--ids, --module, --status) is required.");
return;
}
using var db = new Database(dbPath);
var (whereClause, filterParams) = BatchFilters.BuildWhereClause(ids, module, status);
// Audit verification
var indexer = AuditVerifier.BuildIndexer("unit_tests");
var verifications = AuditVerifier.VerifyItems(db, indexer, "unit_tests", whereClause, filterParams, newStatus);
if (!AuditVerifier.CheckAndReport(verifications, newStatus, overrideComment))
return;
var setClauses = new List<string> { "status = @newStatus" };
var updateParams = new List<(string, object?)> { ("@newStatus", newStatus) };
if (notes is not null)
{
setClauses.Add("notes = @newNotes");
updateParams.Add(("@newNotes", notes));
}
BatchFilters.PreviewOrExecute(db, "unit_tests",
"id, name, status, module_id, notes",
string.Join(", ", setClauses), updateParams,
whereClause, filterParams, execute);
// Log overrides after successful execute
if (execute)
{
var mismatches = verifications.Where(r => !r.Matches).ToList();
if (mismatches.Count > 0 && overrideComment is not null)
AuditVerifier.LogOverrides(db, "unit_tests", mismatches, newStatus, overrideComment);
}
});
return cmd;
}
private static Command CreateBatchMap(Option<string> dbOption)
{
var cmd = new Command("batch-map", "Bulk map tests to .NET test methods");
var idsOpt = BatchFilters.IdsOption();
var moduleOpt = BatchFilters.ModuleOption();
var statusOpt = BatchFilters.StatusOption();
var executeOpt = BatchFilters.ExecuteOption();
var setProject = new Option<string?>("--set-project") { Description = ".NET test project" };
var setClass = new Option<string?>("--set-class") { Description = ".NET test class" };
var setMethod = new Option<string?>("--set-method") { Description = ".NET test method" };
cmd.Add(idsOpt);
cmd.Add(moduleOpt);
cmd.Add(statusOpt);
cmd.Add(executeOpt);
cmd.Add(setProject);
cmd.Add(setClass);
cmd.Add(setMethod);
cmd.SetAction(parseResult =>
{
var dbPath = parseResult.GetValue(dbOption)!;
var ids = parseResult.GetValue(idsOpt);
var module = parseResult.GetValue(moduleOpt);
var status = parseResult.GetValue(statusOpt);
var execute = parseResult.GetValue(executeOpt);
var project = parseResult.GetValue(setProject);
var cls = parseResult.GetValue(setClass);
var method = parseResult.GetValue(setMethod);
if (string.IsNullOrWhiteSpace(ids) && module is null && string.IsNullOrWhiteSpace(status))
{
Console.WriteLine("Error: at least one filter (--ids, --module, --status) is required.");
return;
}
if (project is null && cls is null && method is null)
{
Console.WriteLine("Error: at least one of --set-project, --set-class, --set-method is required.");
return;
}
using var db = new Database(dbPath);
var (whereClause, filterParams) = BatchFilters.BuildWhereClause(ids, module, status);
var setClauses = new List<string>();
var updateParams = new List<(string, object?)>();
if (project is not null) { setClauses.Add("dotnet_project = @setProject"); updateParams.Add(("@setProject", project)); }
if (cls is not null) { setClauses.Add("dotnet_class = @setClass"); updateParams.Add(("@setClass", cls)); }
if (method is not null) { setClauses.Add("dotnet_method = @setMethod"); updateParams.Add(("@setMethod", method)); }
BatchFilters.PreviewOrExecute(db, "unit_tests",
"id, name, status, dotnet_project, dotnet_class, dotnet_method",
string.Join(", ", setClauses), updateParams,
whereClause, filterParams, execute);
});
return cmd;
}
private static string Truncate(string? s, int maxLen)
{
if (s is null) return "";
return s.Length <= maxLen ? s : s[..(maxLen - 2)] + "..";
}
}