Skip to content

SDK.Http: points, bulk points, run progress, history, delete etag, station-unreachable (epic-128 feature-001) - #31

Merged
rafalmaciag merged 3 commits into
masterfrom
feature/epic-128-f1-mcp-write-side
Oct 23, 2026
Merged

rafalmaciag merged 3 commits into
masterfrom
feature/epic-128-f1-mcp-write-side

Conversation

@rafalmaciag

Copy link
Copy Markdown
Contributor

SDK.Http side of epic-128 feature-001 "rw2 MCP write side" (docs/epics/epic-128-customer-part-twin-test/features/feature-001-rw2-mcp-write-side/design.md). rw2 will return these DTOs (ADR-1), so it pins the preview below.

What changed, by design section

§1.1 Points

  • New DTOs (one per file, Programs/): PointAxis, ProgramPoint, PointWrite (optional members carry [JsonIgnore(WhenWritingNull)], so a field left out stays off the wire under any serializer options), PointEditResult, PointsEditResult, RingStartResult, PointRefusal, PointsRefused, PointsRefusedException.
  • IProgramsApi.GetPointsAsync, GetPointAsync, PutPointAsync, PutPointsAsync, CalculateRingStartAsync. Errors: 400/422 → ValueRefusedException; a bulk 422 → PointsRefusedException, listing every name and reason; 409 → ProgramEtagMismatchException; 404 on calculate-start → BlockNotFoundException; anything else → HttpRequestException with the status and rw2's sentence.

§2.6–§2.7 Run progress

  • BlockRunState (string enum), BlockProgress, RunProgress.
  • GetRunProgressAsync, and WaitForRunAsync: a client-side poll every 1 s. The timeout must be 1–600 s; outside that range it throws ArgumentOutOfRangeException before any request is sent. It returns the first answer that is not Running, or the last answer when the timeout passes.

§3 Delete and history

  • DeleteAsync(programId, etag) sends If-Match. A 409 whose body has an etag property → ProgramEtagMismatchException; any other 409 → ProgramRunningException.
  • HistoryCommit, HistoryLog; Programs.GetHistoryAsync(id, limit?) and Repositories.GetHistoryAsync(id, limit?).

§4 Station unreachable (ADR-13)

  • StationUnreachableHandler, registered by AddRocketWelderClient, and StationUnreachableException : HttpRequestException (Station, MayHaveBeenApplied).
  • A connection or name-resolution failure gives "Station unreachable at {base}: {reason}. Nothing was done."
  • No answer within the timeout gives "Station at {base} did not answer within {n} s; the request may or may not have been applied — read the state before retrying."
  • Deviation: the handler owns the configured HttpClient.Timeout, and the client's own timeout becomes infinite. A DelegatingHandler gets one token that merges HttpClient's timeout with the caller's token, so it cannot tell the two apart. The handler buffers the response body inside its timeout, so the whole exchange is still bounded. A cancellation by the caller stays an OperationCanceledException.
  • Calling AddRocketWelderClient twice (rw2's AiTools does this after a host registration) shares one handler, so failures are never wrapped twice.

Tests

RocketWelder.SDK.Http.Tests: 152 passed / 0 failed / 0 skipped; master had 107/0/0. F001-U-001 … F001-U-014 are all implemented, with the scenario id in each test name.

Preview

RocketWelder.SDK.Http 3.1.2-preview.4b8abca (published by preview-publish.yml on the branch push; nuget.org serves it).

🤖 Generated with Claude Code

Rafal Maciag and others added 2 commits October 23, 2026 16:36
…ry, delete with etag (epic-128 feature-001)

design.md §1.1, §2.6-§2.7, §3: ProgramPoint/PointWrite/PointEditResult/PointsEditResult/RingStartResult/
PointRefusal/PointsRefused/PointsRefusedException; BlockRunState/BlockProgress/RunProgress; HistoryCommit/
HistoryLog. IProgramsApi gains GetPoints/GetPoint/PutPoint/PutPoints/CalculateRingStart, GetRunProgress,
WaitForRun (1 s client-side poll, 1-600 s), GetHistory and DeleteAsync(id, etag); IRepositoriesApi gains
GetHistory. Tests F001-U-001 to F001-U-012.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
…epic-128 feature-001)

design.md §4, ADR-13: StationUnreachableHandler maps a connection or name-resolution failure to
StationUnreachableException ("... at {base}: {reason}. Nothing was done.") and no answer within the
timeout to "... may or may not have been applied". The handler owns the configured HttpClient.Timeout
(the client's own becomes infinite) because a DelegatingHandler cannot tell HttpClient's timeout from the
caller's cancellation; a caller's cancellation stays an OperationCanceledException. Registering twice
shares one handler. Tests F001-U-013, F001-U-014.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>

@rafalmaciag rafalmaciag left a comment

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

dotnet-reviewer (epic-128 feature-001), verdict APPROVE-WITH-FIXES. Tests run locally: RocketWelder.SDK.Http.Tests 152 passed / 0 failed / 0 skipped.

Spec alignment is complete:

  • Every method and DTO of design §1.1, §2.6, §2.7, §3 and §4 is present under the exact name.
  • Wire shape: camelCase, joints a number array, null PointWrite members omitted via attributes, enum names for BlockRunState/RunProgress.State.
  • The wait refuses timeouts outside 1–600 s before sending anything.
  • DeleteAsync(id, etag) tells the etag 409 from the running 409.
  • Boundaries: no new PackageReference or ProjectReference; SDK.Http still references no rw2 assembly (context-map E1/E2).
  • Timeout deviation, verified by reading the code:
    • the body is buffered inside the timer;
    • a caller's cancellation still surfaces as OperationCanceledException;
    • the guard against a second registration holds;
    • the response is disposed on every throw path.

One MAJOR is a test gap on the body-stall bound. The rest are MINOR or NIT; see the line comments.

Not in the diff (follow-ups):

  • csharp/Directory.Build.props fallback 2.28.0 is below the published 3.1.1, contrary to its own comment. The preview and the release are unaffected because preview-publish.yml:92 passes /p:Version. It is a release-time bump, not this PR.
  • The RobotPose XML doc says meters/radians, while the wire is mm/deg (pre-existing).
  • design.md §4 and §2.6 need doc-keeper updates for deviations 1 and 4.

Full report: docs epics/epic-128-customer-part-twin-test/features/feature-001-rw2-mcp-write-side/review-sdk-pr31.md.

…e-001)

- F-1: test that a response whose headers arrive but whose body stalls is cut by the handler's timer
  (StationUnreachableException, MayHaveBeenApplied). Mutation-checked: without LoadIntoBufferAsync it fails.
- F-2/F-3: WaitForRunAsync runs on an injected TimeProvider (internal ProgramsApi ctor parameter,
  TimeProvider.System by default). F001-U-007/U-008 assert the 1 s poll spacing and the return at the
  deadline on a manual clock, with no real sleeps.
- F-7: every poll after the first is cut to the time left; a cut poll returns the last answer.
- F-4: StationUnreachableHandler and its constructor are internal (InternalsVisibleTo for the tests).
- F-6: the RunProgress.ArcOnSeconds doc states ADR-8.
- F-8: the handler settings are one immutable StationEndpoint swapped by reference.
- F-9: the Directory.Build.props fallback version is 3.2.0 (lead ruling on F-5).

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>

@rafalmaciag rafalmaciag left a comment

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

dotnet-reviewer re-check of 6112f25: APPROVE. Posted as COMMENT because GitHub refuses an approval from the PR author's own account.

Tests run locally: RocketWelder.SDK.Http.Tests 155 passed / 0 failed / 0 skipped.

Every finding is resolved and its thread closed:

  • F-1: the body-stall test is load-bearing. I mutation-checked it in a scratch copy: with LoadIntoBufferAsync removed, it fails.
  • F-2/F-3: the wait runs on an injected TimeProvider. The tests drive a manual clock, assert poll spacing of 0/1/2 s, and use no real sleeps.
  • F-4: the handler is internal, and no public member of v3.1.1 was removed.
  • F-5: the release is 3.2.0, by the lead's ruling.
  • F-6: the ArcOnSeconds doc now states ADR-8.
  • F-7: every poll after the first is cut to the time left. This is acceptable; one note is left on the thread, with no change asked.
  • F-8: the handler reads one immutable StationEndpoint, swapped by reference.
  • F-9: the fallback version is 3.2.0.

Doc follow-up for the doc-keeper, not this PR: design.md §4 and §2.6 still describe the pre-deviation shape (F-11).

@rafalmaciag
rafalmaciag merged commit 1d23ae9 into master Oct 23, 2026
5 checks passed
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant