Skip to content

πŸ› fix(edge): post-review cleanup β€” slog test race, doc truth-ups, pollInterval tolerance#50

Merged
scttbnsn merged 6 commits into
mainfrom
fix/post-review-cleanup
Jul 4, 2026
Merged

πŸ› fix(edge): post-review cleanup β€” slog test race, doc truth-ups, pollInterval tolerance#50
scttbnsn merged 6 commits into
mainfrom
fix/post-review-cleanup

Conversation

@scttbnsn

@scttbnsn scttbnsn commented Jul 4, 2026

Copy link
Copy Markdown
Contributor

Post-review cleanup from the tri-repo correctness/completeness sweep. One real test-race fix, one small robustness fix, one protocol-tolerance addition, and a batch of doc truth-ups where the docs had drifted from the code. No behavior change to the edge wire beyond tolerating one extra input shape.

Fix: data race in a parallel test

internal/edge/wire_contract_test.go's TestConnectHelloRejectedByController called slog.SetDefault() while marked t.Parallel() β€” a real race on the global logger that intermittently fails CI go test -race. Reworked it to not mutate global slog under parallelism. Verified with -race -count=20 -shuffle=on on internal/edge + internal/protocol (clean).

Fix: silent hello-rejection when the error payload is unparseable

internal/edge/client.go produced controller rejected hello: () with no log when the controller's error envelope failed to unmarshal. Now logs unconditionally with the raw payload.

Add: tolerate a numeric-string pollInterval in the welcome frame

The welcome frame carries a numeric pollInterval (drydock sends 300), and that stays the canonical shape. Added a tolerant decoder that also accepts a numeric string, since the integration docs describe three different pollInterval shapes across the REST + WS surfaces. Numeric decoding is unchanged β€” no ,string tag (that would break the real numeric frame). Covered by internal/protocol/welcome_test.go.

Docs truth-ups (code was right, docs were stale)

  • 404 behavior: drydock-integration.mdx, drydock-integration.md, authentication.mdx claimed a 404 from /api/portwing/ws makes the agent "loop reconnect failures indefinitely." The code treats 404 as fatal β†’ immediate exit (TestConnectFatal404). Corrected all three.
  • pollInterval wire shape: same docs claimed the welcome-frame pollInterval is the string "300"; it's numeric 300 (the string shape is drydock's REST AgentInfo only). Corrected.
  • SPEC.md: added the missing dd:container_delete_request/response row to the Β§3.3 message table (present in code/CHANGELOG/mdx, absent from SPEC).
  • Env exposure: RuntimeDetails.Env returns plaintext container env over GET /api/containers + edge sync, which contradicted README's "values never exposed." Behavior is by design (redaction is the consumer's job) so this is docs-only: scoped the README claim to the MCP inspect_container tool (which really does return only envCount) and added a security-model note flagging the surface for standalone/no-sockguard deploys. Both security-model.md and security-model.mdx updated in lockstep.
  • Ed25519 recommendation: reconciled authentication.mdx vs security-model.mdx on whether Ed25519 is "the recommended" auth.
  • COMPATIBILITY.md: the portwing-with-compose row omitted POST /networks/*/connect (the rule its own header flags as new). Added it.

Build + -race on the affected packages green.

scttbnsn added 6 commits July 4, 2026 12:38
TestConnectHelloRejectedByController called slog.SetDefault() while
marked t.Parallel(), racing the process-global logger against every
other parallel test in the package (reproduced live: DATA RACE on the
shared bytes.Buffer under `go test -race`, intermittently failing CI).

Drop t.Parallel() for this one test instead of widening a sleep: Go
only runs parallel-marked tests concurrently with each other, so a
sequential test never overlaps with another test's execution and can
safely swap the global logger for the duration of its run.
…unparseable

If the controller's TypeError envelope failed to json.Unmarshal into
ErrorMessage, the code silently skipped slog.Warn and returned
"controller rejected hello:  ()" with zero diagnostic information.
Now it logs unconditionally, including the raw env.Data, on unmarshal
failure. Retry/backoff semantics are unchanged.
Drydock's Edge Mode welcome frame sends pollInterval as a JSON number
(confirmed against app/api/portwing-ws.ts), and WelcomeMessage.PollInterval
correctly decodes that. But the ecosystem documents a numeric-string shape
too (Drydock's REST AgentInfo surface), and today a string value would
fail the whole WelcomeMessage unmarshal β€” silently dropping the
serverCompatLevel mismatch warning along with the poll interval.

Add a custom UnmarshalJSON that accepts either a JSON number or a numeric
string for pollInterval, keeping the real numeric frame working exactly
as before (this is NOT `,string`, which would break the numeric case).

- πŸ§ͺ test(protocol): regression tests for numeric, numeric-string, missing,
  and invalid pollInterval shapes, plus a round-trip test
…e shape

- πŸ› fix: SPEC.md Β§3.3 was missing the dd:container_delete_request /
  dd:container_delete_response row, present in code/CHANGELOG/the .mdx
  mirror but not the canonical spec table. Add it matching the
  DDContainerDeleteRequestMessage/DDContainerDeleteResponseMessage structs
  in internal/protocol/messages.go.
- πŸ› fix: "a 404 returns and the agent loops reconnect failures
  indefinitely" was wrong β€” a 404 on the WebSocket upgrade is wrapped in
  errFatal (internal/edge/client.go) and Run() returns immediately, so
  the agent process exits with code 1 on the first attempt
  (TestConnectFatal404). Corrected in drydock-integration.mdx,
  drydock-integration.md, and authentication.mdx.
- πŸ› fix: the welcome frame's pollInterval was documented as a bare
  integer string ("300"). Verified against drydock's
  app/api/portwing-ws.ts:679-683 β€” the welcome frame actually sends a
  JSON number. The string shape belongs to Drydock's REST AgentInfo
  surface, not the welcome frame. Corrected in both docs.
… env exposure

- πŸ› fix: README marketed inspect_container's "values never exposed" as
  if it covered the whole product; it only applies to that MCP tool.
  Reword to scope the claim correctly and note that GET /api/containers
  (and the edge WebSocket container sync) return full env var values by
  design β€” redaction is out of scope for Portwing, it's the downstream
  consumer's (Drydock's) job.
- πŸ“ add matching notes to docs/security-model.md and
  docs/content/docs/security-model.mdx (kept in lockstep per policy),
  flagging this as most relevant to standalone / no-Sockguard
  deployments where the caller/consumer trust boundary is wider.

No code behavior changes β€” this is documentation accuracy only.
The portwing-with-compose.yaml row omitted POST /networks/*/connect,
the rule the preset's own header flags as genuinely new vs
portwing.yaml. Add it to that row's added-rules list.

@biggest-littlest biggest-littlest left a comment

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

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

Post-review cleanup: slog test-race fix verified under -race -count=20 -shuffle, plus doc truth-ups and the pollInterval tolerant decoder. LGTM.

@ALARGECOMPANY ALARGECOMPANY left a comment

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

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

Confirmed: the race fix and the doc corrections (404 behavior, pollInterval wire shape, Env exposure scoping, COMPATIBILITY connect rule) all check out. Approving.

@scttbnsn scttbnsn merged commit 22f2140 into main Jul 4, 2026
37 of 38 checks passed
@scttbnsn scttbnsn deleted the fix/post-review-cleanup branch July 4, 2026 17:03
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.

3 participants