additional tests/bump coverage floor

This commit is contained in:
Emil Lerch 2026-06-25 14:43:55 -07:00
parent c3c990fa68
commit 097fe68d35
Signed by: lobo
GPG key ID: A7B62D657EF764F8
6 changed files with 334 additions and 0 deletions

View file

@ -5811,3 +5811,103 @@ test "printChangeLine: no ANSI when color=false" {
try printChangeLine(&w, c, false, cli.CLR_POSITIVE);
try std.testing.expect(std.mem.indexOf(u8, w.buffered(), "\x1b[") == null);
}
test "printReport: empty report says no changes" {
var buf: [512]u8 = undefined;
var w: std.Io.Writer = .fixed(&buf);
var account_totals = std.StringHashMap(Report.AccountTotal).init(testing.allocator);
defer account_totals.deinit();
var cash_attr = std.StringHashMap(f64).init(testing.allocator);
defer cash_attr.deinit();
var changes = [_]Change{};
const report = Report{
.changes = changes[0..],
.account_totals = account_totals,
.cash_attributed_by_account = cash_attr,
};
try printReport(&w, &report, "portfolio.srf", false);
const out = w.buffered();
try testing.expect(std.mem.indexOf(u8, out, "No changes detected.") != null);
}
test "printReport: full report renders every section and sub-printer" {
var arena_state = std.heap.ArenaAllocator.init(testing.allocator);
defer arena_state.deinit();
const arena = arena_state.allocator();
// One change of (nearly) every kind, exercising every section and
// each line-printer. Values are arbitrary but internally consistent
// for the CD-interest recompute (cash_delta - face = implied interest).
var changes = [_]Change{
.{ .kind = .new_stock, .symbol = "AAPL", .account = "Roth", .security_type = .stock, .delta_shares = 10, .unit_value = 150 },
.{ .kind = .new_cash, .symbol = "CASH", .account = "Roth", .security_type = .cash, .delta_shares = 2000, .unit_value = 1 },
.{ .kind = .new_drip_lot, .symbol = "VTI", .account = "Roth", .security_type = .stock, .delta_shares = 2, .unit_value = 200 },
.{ .kind = .drip_confirmed, .symbol = "SCHD", .account = "Roth", .security_type = .stock, .delta_shares = 1, .unit_value = 75 },
.{ .kind = .rollup_delta, .symbol = "VOO", .account = "Brokerage", .security_type = .stock, .delta_shares = 0.5, .unit_value = 400 },
.{ .kind = .cd_matured, .symbol = "CD1", .account = "CD Acct", .security_type = .cd, .face_value = 10000, .maturity_date = Date.fromYmd(2026, 5, 1) },
.{ .kind = .cash_delta, .symbol = "CASH", .account = "CD Acct", .security_type = .cash, .delta_shares = 10500, .unit_value = 1 },
.{ .kind = .cash_contribution, .symbol = "CASH", .account = "HSA", .security_type = .cash, .delta_shares = 300, .unit_value = 1 },
.{ .kind = .transfer_in, .symbol = "VTI", .account = "Acct B", .security_type = .stock, .delta_shares = 5, .unit_value = 100, .transfer_attributed = 500, .transfer_from = "Acct A", .transfer_date = Date.fromYmd(2026, 5, 2), .transfer_note = "rollover" },
.{ .kind = .transfer_out, .symbol = "", .account = "Acct A", .security_type = .cash, .transfer_attributed = 500, .transfer_date = Date.fromYmd(2026, 5, 2) },
.{ .kind = .partial_transfer_in, .symbol = "SPY", .account = "Acct B", .security_type = .stock, .delta_shares = 10, .unit_value = 100, .transfer_attributed = 600, .transfer_from = "Acct A", .transfer_date = Date.fromYmd(2026, 5, 2) },
.{ .kind = .price_only, .symbol = "VTI", .account = "Roth", .security_type = .stock, .old_price = 100, .new_price = 110 },
.{ .kind = .lot_edited, .symbol = "BND", .account = "Trust", .security_type = .stock },
.{ .kind = .flagged, .symbol = "XYZ", .account = "Roth", .security_type = .stock, .detail = "manual edit" },
.{ .kind = .lot_removed, .symbol = "OLD", .account = "Brokerage", .security_type = .stock, .face_value = 2500 },
.{ .kind = .drip_negative, .symbol = "ABC", .account = "Roth", .security_type = .stock, .delta_shares = -1, .unit_value = 50 },
.{ .kind = .unmatched_transfer, .symbol = "", .account = "Acct C", .security_type = .cash, .transfer_attributed = 750, .transfer_from = "Acct D", .transfer_date = Date.fromYmd(2026, 5, 3), .transfer_note = "unmatched wire" },
};
var account_totals = std.StringHashMap(Report.AccountTotal).init(arena);
try account_totals.put("Roth", .{ .new_money = 3800, .drip_confirmed = 475, .rollup = 0, .cash_delta = 0 });
try account_totals.put("CD Acct", .{ .cash_delta = 10500 });
try account_totals.put("", .{}); // exercises "(no account)" label + all-zero summary cells
const cash_attr = std.StringHashMap(f64).init(arena);
const report = Report{
.changes = changes[0..],
.account_totals = account_totals,
.cash_attributed_by_account = cash_attr,
};
var buf: [16384]u8 = undefined;
var w: std.Io.Writer = .fixed(&buf);
try printReport(&w, &report, "portfolio.srf (+1 more)", false);
const out = w.buffered();
// Section headers.
try testing.expect(std.mem.indexOf(u8, out, "Portfolio contributions report") != null);
try testing.expect(std.mem.indexOf(u8, out, "== New contributions / purchases ==") != null);
try testing.expect(std.mem.indexOf(u8, out, "== DRIP (confirmed") != null);
try testing.expect(std.mem.indexOf(u8, out, "== Rollup share deltas") != null);
try testing.expect(std.mem.indexOf(u8, out, "== CD events ==") != null);
try testing.expect(std.mem.indexOf(u8, out, "== Cash deltas") != null);
try testing.expect(std.mem.indexOf(u8, out, "== Transfers (matched") != null);
try testing.expect(std.mem.indexOf(u8, out, "== Price-only updates") != null);
try testing.expect(std.mem.indexOf(u8, out, "== Lot edits") != null);
try testing.expect(std.mem.indexOf(u8, out, "== Flagged for review ==") != null);
try testing.expect(std.mem.indexOf(u8, out, "== Summary by account ==") != null);
try testing.expect(std.mem.indexOf(u8, out, "Grand total:") != null);
// Sub-printer content.
try testing.expect(std.mem.indexOf(u8, out, "AAPL") != null); // printChangeLine
try testing.expect(std.mem.indexOf(u8, out, "matured") != null); // printCdLine
try testing.expect(std.mem.indexOf(u8, out, "implied interest") != null); // CD interest line
try testing.expect(std.mem.indexOf(u8, out, "Implied interest captured") != null);
try testing.expect(std.mem.indexOf(u8, out, "Acct A -> Acct B") != null); // printTransferLine
try testing.expect(std.mem.indexOf(u8, out, "rollover") != null); // transfer note
try testing.expect(std.mem.indexOf(u8, out, "rest from transfer") != null); // printPartialTransferLine
try testing.expect(std.mem.indexOf(u8, out, "price ") != null); // printPriceOnlyLine
try testing.expect(std.mem.indexOf(u8, out, "manual edit") != null); // printFlaggedLine flagged
try testing.expect(std.mem.indexOf(u8, out, "lot removed") != null); // printFlaggedLine lot_removed
try testing.expect(std.mem.indexOf(u8, out, "Transfer 2026-05-03") != null); // printUnmatchedTransferLine
try testing.expect(std.mem.indexOf(u8, out, "unmatched wire") != null);
try testing.expect(std.mem.indexOf(u8, out, "(no account)") != null); // summary no-account label
try testing.expect(std.mem.indexOf(u8, out, "may include CD maturity") != null); // printCashDeltaLine hint
// Color path: a second render with color=true emits ANSI escapes.
var cbuf: [16384]u8 = undefined;
var cw: std.Io.Writer = .fixed(&cbuf);
try printReport(&cw, &report, "portfolio.srf", true);
try testing.expect(std.mem.indexOf(u8, cw.buffered(), "\x1b[") != null);
}

View file

@ -1097,6 +1097,14 @@ test "reportFetchError: long symbol still classifies correctly (bufPrint fallbac
// formatProvenanceMessage
test "kindFromSource: maps provenance strings to FallbackKind" {
try std.testing.expectEqual(FallbackKind.wikidata, kindFromSource("wikidata"));
try std.testing.expectEqual(FallbackKind.edgar_fallback, kindFromSource("edgar_fallback"));
try std.testing.expectEqual(FallbackKind.none, kindFromSource("")); // empty -> none
try std.testing.expectEqual(FallbackKind.none, kindFromSource("polygon")); // unknown -> none
try std.testing.expectEqual(FallbackKind.none, kindFromSource("Wikidata")); // case-sensitive
}
test "formatProvenanceMessage: wikidata -> 'classified via Wikidata' line" {
var buf: [256]u8 = undefined;
const msg = formatProvenanceMessage(&buf, "AAPL", .wikidata, null) orelse return error.Format;

View file

@ -352,3 +352,40 @@ test "display: high concentration emits color when enabled" {
const o = w.buffered();
try std.testing.expect(std.mem.indexOf(u8, o, "\x1b[") != null);
}
test "display: warning band (5-8%) renders the total line" {
var buf: [4096]u8 = undefined;
var w: std.Io.Writer = .fixed(&buf);
// 6% total -> in [warn_threshold, flag_threshold) -> WARNING color.
const result: exposure.ExposureResult = .{
.symbol = "AAPL",
.total_value = 100_000,
.direct_value = 6_000,
.lookthrough_value = 0,
.contributions = &.{},
.unresolved_holdings = 0,
};
try display(result, "portfolio.srf", true, &w);
const o = w.buffered();
try std.testing.expect(std.mem.indexOf(u8, o, "Total exposure") != null);
try std.testing.expect(std.mem.indexOf(u8, o, "$6,000") != null);
try std.testing.expect(std.mem.indexOf(u8, o, "\x1b[") != null);
}
test "display: accent band (<5%) renders the total line" {
var buf: [4096]u8 = undefined;
var w: std.Io.Writer = .fixed(&buf);
// 3% total -> below warn_threshold -> ACCENT color.
const result: exposure.ExposureResult = .{
.symbol = "AAPL",
.total_value = 100_000,
.direct_value = 3_000,
.lookthrough_value = 0,
.contributions = &.{},
.unresolved_holdings = 0,
};
try display(result, "portfolio.srf", false, &w);
const o = w.buffered();
try std.testing.expect(std.mem.indexOf(u8, o, "Total exposure") != null);
try std.testing.expect(std.mem.indexOf(u8, o, "$3,000") != null);
}

View file

@ -454,3 +454,86 @@ test "loadMergedSeries: imported values only" {
try std.testing.expectEqual(@as(f64, 1_280_000), s.points[0].value);
try std.testing.expectEqual(Date.fromYmd(2020, 6, 1), s.points[2].date);
}
// Rendering tests
test "buildCpiView maps Shiller annual data to YearCpi" {
const view = try buildCpiView(std.testing.allocator);
defer std.testing.allocator.free(view);
try std.testing.expectEqual(shiller.annual_returns.len, view.len);
try std.testing.expect(view.len > 0);
try std.testing.expectEqual(shiller.annual_returns[0].year, view[0].year);
try std.testing.expectEqual(shiller.annual_returns[0].cpi_inflation, view[0].cpi);
}
test "renderHeader: absolute nominal step" {
var buf: [256]u8 = undefined;
var w: std.Io.Writer = .fixed(&buf);
try renderHeader(&w, false, .{ .absolute = 1_000_000 }, false, 0, &.{});
const out = w.buffered();
try std.testing.expect(std.mem.indexOf(u8, out, "$1,000,000") != null);
try std.testing.expect(std.mem.indexOf(u8, out, "(nominal)") != null);
}
test "renderHeader: absolute real step shows reference year" {
var buf: [256]u8 = undefined;
var w: std.Io.Writer = .fixed(&buf);
try renderHeader(&w, false, .{ .absolute = 500_000 }, true, 2020, &.{});
const out = w.buffered();
try std.testing.expect(std.mem.indexOf(u8, out, "(real, reference year: 2020)") != null);
}
test "renderHeader: relative step reads the starting point" {
var buf: [256]u8 = undefined;
var w: std.Io.Writer = .fixed(&buf);
const series = [_]milestones.Point{.{ .date = Date.fromYmd(2014, 7, 3), .value = 1_280_000 }};
try renderHeader(&w, false, .{ .relative = 2.0 }, false, 0, &series);
const out = w.buffered();
try std.testing.expect(std.mem.indexOf(u8, out, "x from") != null);
try std.testing.expect(std.mem.indexOf(u8, out, "$1,280,000") != null);
try std.testing.expect(std.mem.indexOf(u8, out, "2014-07-03") != null);
}
test "renderNoCrossings: reports series max and start" {
var buf: [256]u8 = undefined;
var w: std.Io.Writer = .fixed(&buf);
const series = [_]milestones.Point{
.{ .date = Date.fromYmd(2020, 1, 1), .value = 500_000 },
.{ .date = Date.fromYmd(2021, 1, 1), .value = 750_000 },
};
try renderNoCrossings(&w, false, &series);
const out = w.buffered();
try std.testing.expect(std.mem.indexOf(u8, out, "No milestones reached") != null);
try std.testing.expect(std.mem.indexOf(u8, out, "$750,000") != null); // max
try std.testing.expect(std.mem.indexOf(u8, out, "$500,000") != null); // start
}
test "renderTable: absolute step with starting-row footnote" {
var buf: [2048]u8 = undefined;
var w: std.Io.Writer = .fixed(&buf);
const crossings = [_]milestones.Crossing{
.{ .index = 1, .threshold = 1_000_000, .date = Date.fromYmd(2020, 1, 1), .days_since_prev = null, .days_since_first = 0, .is_start = true },
.{ .index = 2, .threshold = 2_000_000, .date = Date.fromYmd(2022, 6, 15), .days_since_prev = 896, .days_since_first = 896, .is_start = false },
};
try renderTable(&w, false, .{ .absolute = 1_000_000 }, &crossings);
const out = w.buffered();
try std.testing.expect(std.mem.indexOf(u8, out, "Milestone") != null);
try std.testing.expect(std.mem.indexOf(u8, out, "Date Crossed") != null);
try std.testing.expect(std.mem.indexOf(u8, out, "$2,000,000") != null);
try std.testing.expect(std.mem.indexOf(u8, out, "896 days") != null);
try std.testing.expect(std.mem.indexOf(u8, out, "starting value") != null); // footnote
}
test "renderTable: relative step renders the Multiple column" {
var buf: [2048]u8 = undefined;
var w: std.Io.Writer = .fixed(&buf);
const crossings = [_]milestones.Crossing{
.{ .index = 1, .threshold = 1_000_000, .date = Date.fromYmd(2020, 1, 1), .days_since_prev = null, .days_since_first = 0, .is_start = true },
.{ .index = 2, .threshold = 2_000_000, .date = Date.fromYmd(2022, 6, 15), .days_since_prev = 896, .days_since_first = 896, .is_start = false },
};
try renderTable(&w, false, .{ .relative = 2.0 }, &crossings);
const out = w.buffered();
try std.testing.expect(std.mem.indexOf(u8, out, "Multiple") != null);
try std.testing.expect(std.mem.indexOf(u8, out, "Threshold") != null);
try std.testing.expect(std.mem.indexOf(u8, out, "896 days") != null);
}

View file

@ -1016,3 +1016,52 @@ test "render: emits reweight footnote when any flag set" {
const out = w.buffered();
try testing.expect(std.mem.indexOf(u8, out, "Reweighted") != null);
}
test "renderStatusGrid: renders labels across severity variants" {
const dummy = struct {
fn run(ctx: observations.CheckCtx) observations.CheckResult {
_ = ctx;
return .pass;
}
};
const checks = [_]observations.Check{
.{ .name = "c1", .label = "Concentration", .run = dummy.run },
.{ .name = "c2", .label = "Sector drift", .run = dummy.run },
.{ .name = "c3", .label = "Cash drag", .run = dummy.run },
.{ .name = "c4", .label = "Bond ladder", .run = dummy.run },
.{ .name = "c5", .label = "Tax location", .run = dummy.run },
};
// pass / warn / flag / skipped / err across two rows (3 cells per row),
// exercising every rank, glyph, and worst-severity branch.
var pending = [_]observations.PendingCheck{
.{ .check = &checks[0], .state = .{ .complete = .pass } },
.{ .check = &checks[1], .state = .{ .complete = .{ .warn = &.{} } } },
.{ .check = &checks[2], .state = .{ .complete = .{ .flag = &.{} } } },
.{ .check = &checks[3], .state = .{ .complete = .skipped } },
.{ .check = &checks[4], .state = .{ .complete = .{ .err = "boom" } } },
};
const panel = observations.CheckPanel{
.allocator = testing.allocator,
.io = std.testing.io,
.pending = &pending,
};
var buf: [4096]u8 = undefined;
var w: std.Io.Writer = .fixed(&buf);
try renderStatusGrid(&w, false, panel);
const out = w.buffered();
try testing.expect(std.mem.indexOf(u8, out, "Concentration") != null);
try testing.expect(std.mem.indexOf(u8, out, "Bond ladder") != null);
try testing.expect(std.mem.indexOf(u8, out, "Tax location") != null);
}
test "renderStatusGrid: empty panel writes nothing" {
const panel = observations.CheckPanel{
.allocator = testing.allocator,
.io = std.testing.io,
.pending = &.{},
};
var buf: [64]u8 = undefined;
var w: std.Io.Writer = .fixed(&buf);
try renderStatusGrid(&w, false, panel);
try testing.expectEqual(@as(usize, 0), w.buffered().len);
}

View file

@ -470,6 +470,63 @@ test "buildUrl" {
try std.testing.expectEqualStrings("https://api.example.com/v1/data?symbol=AAPL&apikey=test123", url);
}
test "buildUrl percent-encodes reserved characters in values" {
const allocator = std.testing.allocator;
// Value contains a space, '&', '=' (all must be encoded) and '/'
// (allowed in query values, so left as-is).
const url = try buildUrl(allocator, "https://api.example.com/q", &.{
.{ "name", "a b&c=d/e" },
});
defer allocator.free(url);
try std.testing.expect(std.mem.indexOf(u8, url, "%20") != null); // space
try std.testing.expect(std.mem.indexOf(u8, url, "%26") != null); // &
try std.testing.expect(std.mem.indexOf(u8, url, "%3D") != null or std.mem.indexOf(u8, url, "%3d") != null); // =
try std.testing.expect(std.mem.indexOf(u8, url, "d/e") != null); // '/' preserved
}
test "classifyResponse maps each HTTP status to its HttpError" {
const allocator = std.testing.allocator;
const Case = struct { status: std.http.Status, expected: HttpError };
const cases = [_]Case{
.{ .status = .too_many_requests, .expected = HttpError.RateLimited },
.{ .status = .unauthorized, .expected = HttpError.Unauthorized },
.{ .status = .forbidden, .expected = HttpError.Unauthorized },
.{ .status = .payment_required, .expected = HttpError.PaymentRequired },
.{ .status = .not_found, .expected = HttpError.NotFound },
.{ .status = .internal_server_error, .expected = HttpError.ServerError },
.{ .status = .bad_gateway, .expected = HttpError.ServerError },
.{ .status = .service_unavailable, .expected = HttpError.ServerError },
.{ .status = .gateway_timeout, .expected = HttpError.ServerError },
.{ .status = .bad_request, .expected = HttpError.InvalidResponse },
};
for (cases) |c| {
// On the non-ok path classifyResponse frees body + etag itself,
// so the test must not free them again.
const resp = Response{
.status = c.status,
.body = try allocator.dupe(u8, "rejection body"),
.etag = try allocator.dupe(u8, "\"etag\""),
.allocator = allocator,
};
try std.testing.expectError(c.expected, Client.classifyResponse(resp));
}
}
test "classifyResponse passes 200 OK through unchanged" {
const allocator = std.testing.allocator;
const resp = Response{
.status = .ok,
.body = try allocator.dupe(u8, "payload"),
.etag = null,
.allocator = allocator,
};
// On the ok path the body is not freed - the caller still owns it.
var out = try Client.classifyResponse(resp);
defer out.deinit();
try std.testing.expectEqual(std.http.Status.ok, out.status);
try std.testing.expectEqualStrings("payload", out.body);
}
test "parseSha256Etag: quoted form" {
const hex = parseSha256Etag("\"sha256:0402d084abcbd4e40993ebe1e55e0beb400ad77c8c5354a46b047c821e36d3b9\"") orelse unreachable;
try std.testing.expectEqualStrings("0402d084abcbd4e40993ebe1e55e0beb400ad77c8c5354a46b047c821e36d3b9", hex);