From 550fa4fd88f1d0b03d35122796bcdb93b5ff21e7 Mon Sep 17 00:00:00 2001 From: Emil Lerch Date: Mon, 6 Jul 2026 12:46:57 -0700 Subject: [PATCH] add end to end hygiene test --- src/commands/audit/hygiene.zig | 64 ++++++++++++++++++++++++++++++++++ 1 file changed, 64 insertions(+) diff --git a/src/commands/audit/hygiene.zig b/src/commands/audit/hygiene.zig index d325268..b295cce 100644 --- a/src/commands/audit/hygiene.zig +++ b/src/commands/audit/hygiene.zig @@ -1633,3 +1633,67 @@ test "findPriceDateMismatches: duplicate HEAD identity collapses to one entry" { try std.testing.expectApproxEqAbs(@as(f64, 150), mismatches.items[0].old_price, 0.01); try std.testing.expectApproxEqAbs(@as(f64, 160), mismatches.items[0].new_price, 0.01); } + +test "runHygieneCheck: Section 6 flags an un-opted-in symbol's split, not an opted-in one" { + const allocator = std.testing.allocator; + const io = std.testing.io; + var tmp = std.testing.tmpDir(.{}); + defer tmp.cleanup(); + + // NVDA: held across its 2024 split, NOT opted in -> should flag. + // AMZN: held across its 2022 split, opted in via metadata -> should not. + try tmp.dir.writeFile(io, .{ + .sub_path = "portfolio.srf", + .data = + \\#!srfv1 + \\symbol::NVDA,shares:num:100,open_date::2020-01-01,open_price:num:40.00,account::Sample Brokerage + \\symbol::AMZN,shares:num:30,open_date::2019-01-01,open_price:num:90.00,account::Sample Brokerage + \\ + , + }); + // accounts.srf must exist and parse (an empty map is fine). + try tmp.dir.writeFile(io, .{ .sub_path = "accounts.srf", .data = "#!srfv1\n" }); + // Per-symbol opt-in: AMZN yes, NVDA absent (not opted in). + try tmp.dir.writeFile(io, .{ + .sub_path = "metadata.srf", + .data = "#!srfv1\nsymbol::AMZN,splits_current_through::2024-01-01\n", + }); + + var path_buf: [std.fs.max_path_bytes]u8 = undefined; + const dir_len = try tmp.dir.realPathFile(io, ".", &path_buf); + const dir = path_buf[0..dir_len]; + + // Seed the split cache: NVDA 10:1 (2024-06-10), AMZN 20:1 (2022-06-06). + var store = zfin.cache.Store.init(io, allocator, dir); + var nvda = [_]zfin.Split{.{ .date = zfin.Date.fromYmd(2024, 6, 10), .numerator = 10, .denominator = 1 }}; + store.write(zfin.Split, "NVDA", nvda[0..], .{ .seconds = zfin.cache.Ttl.splits }); + var amzn = [_]zfin.Split{.{ .date = zfin.Date.fromYmd(2022, 6, 6), .numerator = 20, .denominator = 1 }}; + store.write(zfin.Split, "AMZN", amzn[0..], .{ .seconds = zfin.cache.Ttl.splits }); + + // No API keys / no server -> hermetic (fetches can't reach a network). + var svc = zfin.DataService.init(io, allocator, .{ .cache_dir = dir }); + defer svc.deinit(); + + var env = try std.testing.environ.createMap(allocator); + defer env.deinit(); + + const pf_path = try std.fs.path.join(allocator, &.{ dir, "portfolio.srf" }); + defer allocator.free(pf_path); + + var aw: std.Io.Writer.Allocating = .init(allocator); + defer aw.deinit(); + + try runHygieneCheck(io, allocator, &env, &svc, pf_path, 3, false, zfin.Date.fromYmd(2026, 1, 1), 1_767_225_600, false, .never, &aw.writer); + + const output = aw.written(); + try std.testing.expect(std.mem.indexOf(u8, output, "Portfolio hygiene") != null); + + // Assert against Section 6 specifically (it's the last section, so + // slice from its header to end) - robust even if NVDA/AMZN surface + // in an earlier section. The un-opted-in NVDA is listed; the + // opted-in AMZN is not. + const sec6_start = std.mem.indexOf(u8, output, "Unhandled stock splits") orelse return error.Section6Missing; + const sec6 = output[sec6_start..]; + try std.testing.expect(std.mem.indexOf(u8, sec6, "NVDA") != null); + try std.testing.expect(std.mem.indexOf(u8, sec6, "AMZN") == null); +}