CTO 2026-05-19 directive on forensic a99ab0a1 (reno-stars >50MB
upload that surfaced "signal timed out" when the real cause was
file-size + a fixed 60s client timeout):
"if its file size issue, should have error that instead saying
timeout which is wrong"
Bundles the cap raise + the wrong-reason fix in ONE PR because the
two are coupled — bumping the server alone would still leak the
fixed-60s timeout for legitimate slow uploads; fixing the client
alone would 413 every >50MB attempt.
Server (push-mode, EC2 workspace):
- workspace-server/internal/handlers/chat_files.go:
chatUploadMaxBytes 50→100 MB
httpClient.Timeout 120→1200 s (matches the new slow-uplink budget)
- workspace/internal_chat_uploads.py:
CHAT_UPLOAD_MAX_BYTES 50→100 MB
CHAT_UPLOAD_MAX_FILE_BYTES 25→100 MB (aligned with total so a
single legitimate large file succeeds end-to-end)
Canvas:
- canvas/src/components/tabs/chat/uploads.ts:
MAX_UPLOAD_BYTES 100 MB constant + FileTooLargeError class
pre-flight gate: file-size violation throws BEFORE any fetch,
with the actionable "File too large (got X MB) — limit is 100MB"
computeUploadTimeoutMs: 60s floor + 100 KB/s scaled deadline
(was a fixed 60s — the root cause of the forensic)
- canvas/src/components/tabs/chat/hooks/useChatSend.ts:
mapUploadErrorToReason: routes each cause to ITS OWN message
(FileTooLargeError | TimeoutError | server-Error | fallback)
no conflation between file-size and connection-too-slow
Tests:
- workspace-server chat_files_test.go: pins 100 MB constant,
asserts sub-cap forwards + over-cap non-2xx
- canvas uploads.cap.test.ts (10 cases): pre-flight gate, exact-cap
edge, scaled-timeout curve, server-413 propagation, AbortSignal
shape — explicit negative on "TimeoutError ≠ FileTooLargeError"
- canvas useChatSend.errorReason.test.ts (5 cases): per-cause
message contract, explicit negatives that guard against the
wrong-reason conflation
Test harness mirror:
- tests/harness/cf-proxy/nginx.conf: client_max_body_size 50m→100m
(this is the harness mirror; the production CF / nginx tier is
out-of-repo. If prod still caps at 50m, this mirror passes while
prod 413s — surface to ops.)
Follow-up (SSOT, NOT in this PR):
The 100 MB constant now lives in THREE mirror sites (canvas TS +
workspace Python + platform Go). Per feedback_no_single_source_of_truth,
the proper fix is exposing the cap via GET /uploads/limits so the
client fetches the live value. Filing as a separate issue.
References:
- task #295 (internal tracker; CTO-authorized this work)
- forensic a99ab0a1 (reno-stars 2026-05-19)
- feedback_surface_actionable_failure_reason_to_user (CTO 2026-05-17)
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Tests
This repo uses the standard monorepo testing convention: unit tests live with their package, cross-component E2E tests live here.
Where to find tests
| Scope | Location |
|---|---|
| Go unit + integration (platform, CLI, handlers) | workspace-server/**/*_test.go — run with cd workspace-server && go test -race ./... |
| TypeScript unit (canvas components, hooks, store) | canvas/src/**/__tests__/ — run with cd canvas && npm test -- --run |
| TypeScript unit (MCP server handlers) | mcp-server/src/__tests__/ — run with cd mcp-server && npx jest |
| Python unit (workspace runtime, adapters) | workspace/tests/ — run with cd workspace && python3 -m pytest |
| Python unit (SDK: plugin + remote agent) | sdk/python/tests/ — run with cd sdk/python && python3 -m pytest |
| Cross-component E2E (spans platform + runtime + HTTP) | tests/e2e/ ← you are here |
Why split this way
- Go requires co-located
_test.gofiles to access unexported symbols. - Per-package test commands keep the inner loop fast — changing canvas doesn't re-run Go tests.
tests/e2e/covers scenarios that no single package owns: a full workspace lifecycle, A2A across two provisioned agents, delegation chains, bundle round-trips.
Running E2E
Every E2E script here assumes the platform is running at localhost:8080 and (where noted) provisioned agents are online. See the header comment of each .sh for specifics.
Cleaning up rogue test workspaces
If an E2E run aborts before its teardown runs (Ctrl-C, crash, CI timeout),
the platform can be left with workspaces whose config volume is stale or
empty — Docker's unless-stopped restart policy then spins those
containers in a FileNotFoundError loop. The platform's pre-flight check
(#17) marks such workspaces failed on the next restart, but a manual
cleanup is useful:
bash scripts/cleanup-rogue-workspaces.sh # deletes ws with id/name starting aaaaaaaa-, bbbbbbbb-, cccccccc-, test-ws-
MOLECULE_URL=http://host:8080 bash scripts/cleanup-rogue-workspaces.sh
The script DELETEs each matching workspace via the API and
force-removes the ws-<id[:12]> container as a belt-and-suspenders
fallback.