refactor(timesheets): share timesheet read fetches between CLI and MCP - #75
Merged
Conversation
`ts get` and the MCP GetTimesheets tool each ran their own per-day fetch loop and disagreed on weekends; `ts suggest` and GetSuggestedTimesheets each refreshed and filtered suggestions; `ts check` and CheckWeek shared the fetch but duplicated the projection. One range read (`TimesheetLookup.ForRangeAsync`) now serves both, with the weekend policy an explicit argument, the refresh-then-read is shared and happens once, and both surfaces serialise the same `WeekCheckResult`. Every projection and envelope is unchanged: all existing goldens and CLI --json documents are byte-identical, and the CheckWeek parity row flips to ExpectParity. New goldens lock the weekend policy on both surfaces for a single Saturday. A new IL-based architecture test keeps TimesheetMcpTools off the API client except for a shrink-only allowlist (the delete write and the iteration pass-through). Closes #74 Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
There was a problem hiding this comment.
🟡 Changes recommended
Two moderate findings remain regarding weekend fixture validity and IL scanner coverage.
Get a fresh assessment by requesting another Copilot review.
Pull request overview
Refactors timesheet reads so CLI and MCP share fetch logic and week-check serialization while preserving existing output contracts.
Changes:
- Adds shared range, suggestion, and week-check abstractions.
- Updates parity tests, fixtures, and goldens.
- Adds IL-based enforcement against direct API usage.
File summaries
| File | Summary |
|---|---|
tests/SSW.TimePro.Cli.Tests/Features/Timesheets/TimesheetLookupReadTests.cs |
Tests shared lookup behavior. |
tests/SSW.TimePro.Cli.Integration/Mcp/ToolIlScanner.cs |
Scans MCP tool IL for direct API calls. Moderate finding (3 votes): unowned private helper callers may be ignored. |
tests/SSW.TimePro.Cli.Integration/Mcp/NorthwindApi.cs |
Provides MCP test API fixtures. |
tests/SSW.TimePro.Cli.Integration/Mcp/McpToolCatalog.cs |
Adds the weekend test case. Moderate finding (1 vote): the payload uses a non-Saturday date and may not validate date association. |
tests/SSW.TimePro.Cli.Integration/Mcp/McpCliParityTable.cs |
Defines parity cases and API allowlists. |
tests/SSW.TimePro.Cli.Integration/Mcp/CliMcpParityTests.cs |
Enforces CLI/MCP parity and delegation architecture. |
tests/SSW.TimePro.Cli.Integration/Goldens/Mcp/Tools/GetTimesheets.weekendDate.json |
Adds weekend tool output coverage. |
tests/SSW.TimePro.Cli.Integration/Goldens/Mcp/Parity/GetTimesheets.16.mcp.json |
Adds MCP weekend parity output. |
tests/SSW.TimePro.Cli.Integration/Goldens/Mcp/Parity/GetTimesheets.16.cli.json |
Adds CLI weekend parity output. |
tests/SSW.TimePro.Cli.Integration/Goldens/Mcp/Parity/GetTimesheets.15.mcp.json |
Adds MCP weekday parity output. |
tests/SSW.TimePro.Cli.Integration/Goldens/Mcp/Parity/GetTimesheets.15.cli.json |
Adds CLI weekday parity output. |
tests/SSW.TimePro.Cli.Integration/Goldens/Mcp/Parity/GetSuggestedTimesheets.17.mcp.json |
Adds MCP suggestion parity output. |
tests/SSW.TimePro.Cli.Integration/Goldens/Mcp/Parity/GetSuggestedTimesheets.17.cli.json |
Adds CLI suggestion parity output. |
src/SSW.TimePro.Cli/Features/Timesheets/WeekCheckResult.cs |
Defines the shared week-check document. |
src/SSW.TimePro.Cli/Features/Timesheets/TimesheetLookup.cs |
Implements shared range and suggestion reads. |
src/SSW.TimePro.Cli/Features/Timesheets/SuggestCommand.cs |
Uses the shared suggestion lookup. |
src/SSW.TimePro.Cli/Features/Timesheets/GetCommand.cs |
Uses the shared range lookup. |
src/SSW.TimePro.Cli/Features/Timesheets/CheckCommand.cs |
Uses the shared week-check result. |
src/SSW.TimePro.Cli/Features/Mcp/Tools/TimesheetMcpTools.cs |
Delegates MCP reads to shared logic. |
AGENTS.md |
Documents shared-read conventions. |
Review details
Suppressed comments (1)
tests/SSW.TimePro.Cli.Integration/Mcp/McpToolCatalog.cs:41
- This case uses the baseline
NorthwindApi.Day()response, whoseTimesheetItem.Dateand times are hard-coded to 2026-03-16. The new Saturday CLI golden therefore places Monday entries inside a 2026-03-21 day, so it does not exercise a realistic weekend row and can hide date-association regressions. Arrange a Saturday-specific payload and assert the returned entry date matchesWeekendDate.
// A Saturday: the tool's weekend policy answers with an empty array and never calls the API.
new("GetTimesheets", "weekendDate",
(h, ct) => h.Timesheets.GetTimesheets(NorthwindApi.WeekendDate, ct: ct))
{
HasApiErrorCase = false
},
- Files reviewed: 20/20 changed files
- Comments generated: 1
- Review effort level: Lite
💡 Add a code-review agent skill or configure MCP servers for context-aware, tailored reviews. Learn more in the docs.
Comment on lines
+40
to
+42
| var owner = Owner(method, toolMethods); | ||
| if (owner is not null) | ||
| callers.Add(owner); |
`ToolIlScanner` dropped any caller it could not map to a tool method, so an `ITimeProApiClient` call inside a plain private helper passed the architecture test. It now throws naming the method, with fixture-backed tests for both the direct call and the helper case. The Northwind timesheet-list stub also echoes the requested date, so the Saturday golden no longer carries Monday-dated rows. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
jernejk
force-pushed
the
refactor/timesheet-read-projections
branch
from
September 12, 2026 15:30
61f1455 to
8107286
Compare
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
Summary
Slice 4 of #38: the timesheet reads.
ts get/GetTimesheets,ts suggest/GetSuggestedTimesheetsandts check/CheckWeeknow share their fetch and, for the weekcheck, their result document. Extraction only — no wire shape changes, so this is a patch.
Closes #74
What changed
TimesheetLookup.ForRangeAsyncis the single per-day range read. The weekend disagreementbetween the two surfaces is now an explicit
WeekendPolicyargument rather than a duplicatedcontinue:ts getpassesInclude, the MCP tool passesSkip(so a weekend-only range stillanswers
[]without calling the API). Neither policy changed.TimesheetLookup.RefreshAndReadSuggestedAsyncis the shared refresh-then-read-then-filter, withthe refresh happening exactly once inside it, noted as the server-side write it is.
WeekCheckResult/WeekCheckDayis the week-coverage document bothts check --jsonandCheckWeekserialise;CheckCommand's privateDayJsonand the MCP anonymous object are gone.leaveTypekeeps its always-emitted annotation, so the CLI still writes it asnull.GetTimesheets(weekday and Saturday) andGetSuggestedTimesheetsbecomeexecutable rows with their permitted differences declared;
CheckWeekflips toExpectParity = true.TimesheetTools_DoNotCallTheApiClientDirectly_ExceptWhereAllowlisted.Constructor inspection cannot answer this (the shared services take the client as an argument),
so
ToolIlScannerwalks the tools' IL — including async state machines and lambdas — and mapscalls back to the owning tool. It fails closed: a call it cannot attribute to a tool method
throws rather than being dropped. Allowlist:
DeleteTimesheet(the delete write itself; itssuggestion pre-check is already shared) and
ListIterations(a one-endpoint pass-through on bothsurfaces, so there is nothing to share until the lookups slice). It may only shrink.
Saturday-dated rows instead of Monday ones.
How verified
Tests: unit 589, integration 299, both green (
+6and+7overorigin/main).Golden delta against
origin/mainis additions only — no existing golden changed:The branch was rebased after #73, which inserted a
CreateLeaverow ahead of these ones, so theparity indices shifted by one (
15/16/17->16/17/18). The moves are pure renames withbyte-identical content — verified by diffing each pre-rebase file against its new name, all six
identical(git's own-Mheuristic mislabels two of them because their whole content is[]).#71 tracks replacing the positional index in golden names with a stable key so a table insertion
stops renaming unrelated snapshots.
Forced failures, to show the new checks can fail:
WeekendPolicy.Includefails two goldens(
Tools/GetTimesheets.weekendDate.jsonand the Saturday parity golden) — the weekend policy islocked, not incidental.
_apicall insideGetSuggestedTimesheetsfails the architecture test:items {"GetSuggestedTimesheets"} are not part of the superset._apicall through a plain private helper used to pass (the scanner silentlydropped callers it could not attribute).
ToolIlScannerTestsnow pins both halves againstfixture tool types: the direct call is attributed to its tool, and the helper case throws
InvalidOperationExceptionnaming the method.Staging (
--tenant ssw-staging,apiUrlcontainsstaging,isProduction: false; no writesbeyond the server-side suggestion refresh), showing the two weekend policies for the same Saturday:
ts check --week --jsonand the MCPcheck_weekover stdio returned the same document, comparedin full rather than by eye (
json.load(cli) == json.load(mcp)->True); trimmed, with theemployee id redacted:
ts suggest 2026-09-11 --jsonand MCPget_suggested_timesheetsfor the same date both returned[]on staging (no suggestions there), so the shared refresh path ran end to end but the populatedgrouping is covered by the WireMock goldens, not by staging.
The staging runs above were executed before the rebase and the scanner change; neither touches the
read paths they exercise, and the full WireMock suite was re-run after both.
Not done here: the CLI/MCP envelope shapes still differ for
ts getandts suggest(declared perrow in the parity table); aligning them is a contract change, not this patch.
Risks
ToolIlScanneris the novel piece: it throws on an unknown opcode and on an unattributable callerrather than silently finding nothing, and the forced failures above show it detects calls through
async state machines and refuses private-helper indirection.
GetCommand's render helpers now takeread-only collection types; behaviour is unchanged and the existing human-output tests cover them.
🤖 Generated with Claude Code