From 8a0524371d288194ab06deddd4206a4d3d34eee4 Mon Sep 17 00:00:00 2001 From: Laurance Walden <6620582+lwalden@users.noreply.github.com> Date: Wed, 10 Jun 2026 19:05:18 -0700 Subject: [PATCH] fix(functions): quarterly-audit stub logs honest skip Warning (S6-007) During the live paper-validation run, logs are evidence: the IncomeSleeve_QuarterlyAudit TODO stub logged "Quarterly quality audit complete" while doing nothing, which would falsify the run record in the Jul 1-7 audit window. The stub now logs a single explicit Warning that it is not implemented and was skipped (deferred per backlog, S7+). No Discord send, no cron change, no audit logic added; guard and catch/rethrow stay. Also introduces the internal Func clock seam (Default D1) on IncomeSleeveFunction - both timer methods read the injected clock instead of DateTime.UtcNow - so the day-of-month guard is unit-testable and S6-001 can rebase on it for the first-trading-weekday gate. Tests: 5 new in IncomeSleeveFunctionTests (TDD RED on the honesty assertions first); full suite 595 passed / 0 failed. Local host-boot smoke (tools/ci/host-boot-smoke.sh, PORT=7188, throwaway Azurite) green: all 4 timers registered, no failure signatures. Co-Authored-By: Claude Fable 5 --- .../IncomeSleeveFunction.cs | 24 +++- .../Functions/IncomeSleeveFunctionTests.cs | 130 ++++++++++++++++++ 2 files changed, 151 insertions(+), 3 deletions(-) create mode 100644 tests/TradingSystem.Tests/Functions/IncomeSleeveFunctionTests.cs diff --git a/src/TradingSystem.Functions/IncomeSleeveFunction.cs b/src/TradingSystem.Functions/IncomeSleeveFunction.cs index 214fc47..4865c69 100644 --- a/src/TradingSystem.Functions/IncomeSleeveFunction.cs +++ b/src/TradingSystem.Functions/IncomeSleeveFunction.cs @@ -12,13 +12,27 @@ public class IncomeSleeveFunction { private readonly ILogger _logger; private readonly TradingSystemConfig _config; + private readonly Func _utcNow; public IncomeSleeveFunction( ILogger logger, IOptions config) + : this(logger, config, () => DateTime.UtcNow) + { + } + + /// + /// Testable-clock seam (S6-007, Default D1): tests pin the day-of-month guard; + /// S6-001 reuses this seam for the first-trading-weekday gate. + /// + internal IncomeSleeveFunction( + ILogger logger, + IOptions config, + Func utcNow) { _logger = logger; _config = config.Value; + _utcNow = utcNow; } /// @@ -30,7 +44,7 @@ public async Task RunMonthlyReinvest( CancellationToken cancellationToken) { // Only run on the first weekday of the month - if (DateTime.UtcNow.Day > 7) return; + if (_utcNow().Day > 7) return; var runId = Guid.NewGuid().ToString("N")[..8]; _logger.LogInformation("Starting monthly income reinvest. RunId: {RunId}", runId); @@ -64,7 +78,7 @@ public async Task RunQuarterlyAudit( [TimerTrigger("0 0 14 1-7 1,4,7,10 1-5")] TimerInfo timer, CancellationToken cancellationToken) { - if (DateTime.UtcNow.Day > 7) return; + if (_utcNow().Day > 7) return; var runId = Guid.NewGuid().ToString("N")[..8]; _logger.LogInformation("Starting quarterly quality audit. RunId: {RunId}", runId); @@ -79,7 +93,11 @@ public async Task RunQuarterlyAudit( // 5. Generate reduction signals if needed // 6. Send report to owner - _logger.LogInformation("Quarterly quality audit complete. RunId: {RunId}", runId); + // S6-007 stub honesty: during the paper-validation run, logs are evidence. + // Do NOT log "complete" for work that never happened. + _logger.LogWarning( + "IncomeSleeve_QuarterlyAudit is not implemented — skipped (deferred per backlog, S7+). RunId: {RunId}", + runId); } catch (Exception ex) { diff --git a/tests/TradingSystem.Tests/Functions/IncomeSleeveFunctionTests.cs b/tests/TradingSystem.Tests/Functions/IncomeSleeveFunctionTests.cs new file mode 100644 index 0000000..48990f9 --- /dev/null +++ b/tests/TradingSystem.Tests/Functions/IncomeSleeveFunctionTests.cs @@ -0,0 +1,130 @@ +using System.Text.RegularExpressions; +using Microsoft.Azure.Functions.Worker; +using Microsoft.Extensions.Logging; +using Microsoft.Extensions.Options; +using Moq; +using TradingSystem.Core.Configuration; +using TradingSystem.Functions; +using Xunit; +using MsOptions = Microsoft.Extensions.Options.Options; + +namespace TradingSystem.Tests.Functions; + +/// +/// S6-007: quarterly-audit stub honesty. During the live paper-validation run, logs are +/// evidence — a TODO stub that logs "Quarterly quality audit complete" while doing nothing +/// would falsify the run record in the Jul 1–7 audit window. The stub must instead log an +/// explicit Warning that it is not implemented and was skipped. Also covers the internal +/// Func<DateTime> clock seam (Default D1) that S6-001 rebases on: both timer methods +/// read the injected clock instead of DateTime.UtcNow, so the day-of-month guard is testable. +/// No Discord send, no cron change, no behavior added. +/// +public class IncomeSleeveFunctionTests +{ + // ---------- harness ---------- + + private static (IncomeSleeveFunction Function, Mock> Logger) Build(DateTime pinnedUtcNow) + { + var logger = new Mock>(); + var config = MsOptions.Create(new TradingSystemConfig()); + var function = new IncomeSleeveFunction(logger.Object, config, () => pinnedUtcNow); + return (function, logger); + } + + private static List<(LogLevel Level, string Message)> CapturedLogs(Mock> logger) + { + var entries = new List<(LogLevel, string)>(); + foreach (var inv in logger.Invocations) + { + if (inv.Method.Name != nameof(ILogger.Log)) + continue; + var level = (LogLevel)inv.Arguments[0]; + var message = inv.Arguments[2]?.ToString() ?? string.Empty; + entries.Add((level, message)); + } + return entries; + } + + // ---------- quarterly audit: honesty (S6-007 test plan 1) ---------- + + [Fact] + public async Task RunQuarterlyAudit_WithinFirstWeek_LogsExactlyOneNotImplementedWarning() + { + // 2026-07-01 is a Wednesday with Day = 1 (≤ 7): inside the audit window. + var (function, logger) = Build(new DateTime(2026, 7, 1, 14, 0, 0, DateTimeKind.Utc)); + + await function.RunQuarterlyAudit(new TimerInfo(), CancellationToken.None); + + var warnings = CapturedLogs(logger) + .Where(e => e.Level == LogLevel.Warning) + .Select(e => e.Message) + .ToList(); + + var honest = warnings.Where(m => m.Contains("not implemented — skipped")).ToList(); + Assert.Single(honest); + + // The warning carries the same runId as the start-of-run Information line. + var startMessage = CapturedLogs(logger) + .Where(e => e.Level == LogLevel.Information) + .Select(e => e.Message) + .Single(m => m.Contains("Starting quarterly quality audit")); + var runId = Regex.Match(startMessage, @"RunId: (\w{8})").Groups[1].Value; + Assert.False(string.IsNullOrEmpty(runId)); + Assert.Contains($"RunId: {runId}", honest[0]); + } + + [Fact] + public async Task RunQuarterlyAudit_WithinFirstWeek_NoLongerLogsComplete() + { + var (function, logger) = Build(new DateTime(2026, 7, 1, 14, 0, 0, DateTimeKind.Utc)); + + await function.RunQuarterlyAudit(new TimerInfo(), CancellationToken.None); + + var informationMessages = CapturedLogs(logger) + .Where(e => e.Level == LogLevel.Information) + .Select(e => e.Message); + Assert.DoesNotContain(informationMessages, + m => m.Contains("complete", StringComparison.OrdinalIgnoreCase)); + } + + // ---------- quarterly audit: guard intact via clock seam (S6-007 test plan 2) ---------- + + [Fact] + public async Task RunQuarterlyAudit_AfterFirstWeek_ReturnsWithoutLoggingWarning() + { + // Day 8 (> 7): the existing guard must return before any logging. + var (function, logger) = Build(new DateTime(2026, 7, 8, 14, 0, 0, DateTimeKind.Utc)); + + await function.RunQuarterlyAudit(new TimerInfo(), CancellationToken.None); + + Assert.Empty(CapturedLogs(logger)); + } + + // ---------- monthly reinvest: untouched in this item (S6-007 test plan 4) ---------- + + [Fact] + public async Task RunMonthlyReinvest_WithinFirstWeek_BehaviorUnchanged_StillLogsComplete() + { + // S6-001 owns the reinvest wiring; this item must not change its behavior. + var (function, logger) = Build(new DateTime(2026, 7, 1, 13, 30, 0, DateTimeKind.Utc)); + + await function.RunMonthlyReinvest(new TimerInfo(), CancellationToken.None); + + var informationMessages = CapturedLogs(logger) + .Where(e => e.Level == LogLevel.Information) + .Select(e => e.Message) + .ToList(); + Assert.Contains(informationMessages, m => m.Contains("Monthly income reinvest complete")); + } + + [Fact] + public async Task RunMonthlyReinvest_AfterFirstWeek_GuardUsesInjectedClock() + { + // Proves the monthly method reads the seam (S6-001 rebases on this), guard intact. + var (function, logger) = Build(new DateTime(2026, 7, 8, 13, 30, 0, DateTimeKind.Utc)); + + await function.RunMonthlyReinvest(new TimerInfo(), CancellationToken.None); + + Assert.Empty(CapturedLogs(logger)); + } +}