Race-detector CI runs (-race) slow goroutines enough that a
prior sweeper goroutine (e.g. TestStartSweeper_TransientErrorDoesNotCrashLoop)
can still be running and incrementing pendingUploadsSweepErrors after
metricDelta() captures its baseline, but before the success-path sweeper
records its success metrics. The test then reads deltaError=1 instead of 0.
Fix: add waitForMetricDelta(t, deltaError, 0, 2*time.Second) before the
assertion, matching the polling pattern already used in the error-path
test (TestStartSweeper_RecordsMetricsOnError). This ensures the error
counter has settled before we assert on it.
Fixes molecule-core#22.
Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
Replace all text-ink-soft usages across canvas components and app pages.
ink-soft (#8d92a0) on dark zinc (#0e1014) yields ~2.2:1 contrast,
failing WCAG 2.1 AA minimum of 4.5:1 for normal text.
ink-mid (#c8c2b4) on dark zinc yields ~7.6:1 — well above AA.
text-ink-mid is already the semantic token for secondary/caption text
in the warm-paper light mode; the dark-mode override was the gap.
52 files, 268 replacements. No functional change beyond contrast.
Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
Canvas runs Next.js 15.5.15 (package-lock.json). Audit doc had
Next.js 14 App Router from before the upgrade. Also add
KeyboardShortcutsDialog.tsx to the directory structure tree.
Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
Cmd/Ctrl+Arrow Up/Down resizes node height (±10px, ±2px with Shift).
Cmd/Ctrl+Arrow Left/Right resizes node width (±10px, ±2px with Shift).
Uses the same onNodesChange('dimensions') path that NodeResizer uses
— no new store action needed. Respects min-width/min-height matching
the NodeResizer constraints (360×200 with children, 210×110 without).
The Arrow-key move shortcut now skips when a modifier key is held,
so Cmd/Ctrl+Arrow unambiguously means resize (not move).
Updates canvas audit doc: Node Rendering section updated and
the LOW node-resize item marked done. All Remaining Gaps items
are now complete.
Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
Issue #75 PR-D: two remaining `gh` CLI calls in .github/workflows/.
1. ci.yml canvas-deploy-reminder:
- Replaced `gh api POST repos/.../commits/.../comments` with writing
to GITHUB_STEP_SUMMARY. Gitea has no commit-comments API (confirmed
in issue #75), so the gh call always failed. GITHUB_STEP_SUMMARY works
on both GitHub Actions and Gitea Actions as the workflow-run summary
page, which is the natural place for post-deploy action items.
- Removed now-unnecessary GH_TOKEN env var and contents:write permission.
2. check-merge-group-trigger.yml:
- Converted to no-op stub. Gitea has no merge queue feature and no
merge_group: event type, so this workflow's lint would find nothing
to verify (all workflows vacuously pass). Keeping workflow+job name
unchanged preserves commit-status context names for branch protection
consumers. Dropped the merge_group: trigger since it would never fire
on Gitea. Dropped the full bash linter + gh api call.
Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
Target handle (top of card): Enter/Space extracts this node from
its parent, moving it to the root level.
Source handle (bottom of card): Enter/Space nests the currently
selected node as a child of this node (requires another node to be
selected first).
Both handles gain tabIndex=0, role="button", a descriptive aria-label,
and a blue focus ring so keyboard-only users can navigate the
workspace hierarchy without a mouse. Uses the existing nestNode store
action — no new API surface needed.
Updates the canvas audit doc to mark the LOW edge-anchor item done.
Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
Update go.mod require and main.go import to use the Gitea-hosted
module path go.moleculesai.app/plugin/gh-identity (migrated from
github.com/Molecule-AI/molecule-ai-plugin-gh-identity).
Follows the pattern of the org-template URL migrations (github.com ->
git.moleculesai.app) applied to Go module imports.
Fixes molecule-core#91.
Ref: molecule-internal#71.
Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
PUT /workspaces/:id/files and DELETE /workspaces/:id/files updated the
config volume but never restarted the container, so the running agent
continued serving stale file content from its in-memory cache. The
SecretsHandler already had this pattern (issue #15); TemplatesHandler
was missing it.
Fix: after every successful write/delete in WriteFile, DeleteFile, and
ReplaceFiles, call h.wh.RestartByID(workspaceID) asynchronously, guarded
by h.wh != nil (nil-tolerant for callers that only use read-only
surfaces). The RestartByID coalescing gate prevents thundering-herd on
concurrent requests.
Fixes#151.
Fixes#87 (duplicate effort closed — core-be also filed #183).
Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
The "Node Rendering" and "Drag and Drop" sections still said
"mouse only, no keyboard alternative" and "Keyboard alternative: None"
despite PR #182 (Arrow keys) being merged. Update both to reflect
the keyboard-accessible node drag.
Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
Issue #86: TestStartSweeper_RecordsMetricsOnSuccess fails in full-suite.
Root cause: two cooperating bugs in the sweeper test harness.
1. Sweeper loop called sweepOnce after ctx cancellation (double-increment).
When ctx was cancelled the loop's select received ctx.Done(), called
sweepOnce with the cancelled ctx, storage.Sweep returned context error,
and metrics.PendingUploadsSweepError() incremented the error counter a
SECOND time before the loop exited. Subsequent tests captured a polluted
error baseline and their deltaError assertions failed.
2. Tests called defer cancel() without waiting for the goroutine to exit.
The goroutine could still be blocked on Sweep (waiting for the next
ticker's C channel) when the next test called metricDelta(). If the
goroutine's Sweep returned during the next test's measurement window,
the shared metric counters mutated mid-baseline.
Fix (production code):
- Guard the ticker arm: if ctx.Err() != nil, continue instead of calling
sweepOnce. This prevents the post-cancellation sweep from running.
Fix (test harness):
- startSweeperWithInterval gains a done chan struct{} parameter. When the
loop exits the channel is closed exactly once.
- StartSweeperForTest starts the goroutine and returns the done channel,
allowing tests to drain it with <-done after cancel() — guaranteeing
the goroutine has fully terminated before the next test's baseline.
All 8 sweeper tests now use StartSweeperForTest and drain the done
channel before returning, ensuring stable metric baselines across the
full suite.
Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
os.Chmod(dst, 0o555) silently passes when os.Geteuid() == 0 because
root bypasses POSIX permission checks. A previous attempt to use a
symlink to /dev/full also fails: Go's os.MkdirAll resolves the symlink
during path traversal and the kernel allows mkdir("/dev/full") as a
device-table entry — io.Copy to /dev/full then succeeds with 0 bytes
written and returns nil.
The honest, consistent fix mirrors TestLocalResolver_CopyFileSourceUnreadable:
skip when running as root. The write-failure propagation logic is
exercised correctly in non-root CI environments.
Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>