Skip to content

test: pin the App Management socket contract (server/sockets/apps.js) - #6137

Merged
atomantic merged 1 commit into
mainfrom
claim/issue-6037
Sep 3, 2026
Merged

test: pin the App Management socket contract (server/sockets/apps.js)#6137
atomantic merged 1 commit into
mainfrom
claim/issue-6037

Conversation

@atomantic

@atomantic atomantic commented Sep 3, 2026

Copy link
Copy Markdown
Owner

Summary

server/sockets/apps.js owns three pieces of state nothing else can observe — the re-entrancy guard that stops two concurrent git pull / PM2 standardize runs against the same checkout, the resumable progress buffer a remounting client rehydrates from, and the ledger/broadcast/cleanup epilogue that must run even when the underlying service throws. Until now the only suite importing registerAppHandlers was server/services/updatePreflightParity.test.js, which covers just the PortOS-app preflight refusals, so all three were unpinned.

This adds server/sockets/apps.test.js — a test-only change, no production code touched.

Seven tests, driving the real handlers over a fake socket/io bus at the event boundary:

Collision guard

  • A second app:update for the same app while one is in flight is refused with duplicate: true, and appUpdater.updateApp is not called twice.
  • An app:update for a different app record that points at the same repoPath is refused too (the half of findConflictingOperation that an app-id lookup alone would miss), while an unrelated checkout is unaffected.
  • An app:standardize is refused while an update runs on the same checkout, and analysis never starts.

Active-operations buffer

  • A reconnecting client's connect-time push and its app:operations:list reply both carry the in-flight operation with repoPath stripped, and with step frames deduplicated last-write-wins per step id rather than one frame appended per emit.

Failure recovery

  • A rejecting appUpdater.updateApp still emits app:update:error, writes a success: false ledger row carrying the error message, broadcasts notifyAppsChanged('update', appId), emits no app:update:complete, and clears the operation.
  • A standardize analyze failure emits the analyze/error step and the error event, never reaches apply, and clears the operation.
  • A standardize apply failure emits the error event and leaves the app record untouched — pm2ProcessNames must not be persisted from a standardization that never wrote the config those names describe.

Notes

  • The in-flight cases park inside appUpdater.updateApp on a deferred promise rather than sleeping, so "operation is active" is deterministic with no timers.
  • activeAppOperations is module state that outlives an individual test, so startParkedUpdate releases its parked run from onTestFinished. Without that, one failing assertion would leave the guard armed and cascade into every later test in the file — verified by mutating the source and watching failures stay attributed to the one behavior broken.

Test plan

  • cd server && npx vitest run sockets/apps.test.js — 7 passed.
  • cd server && npx vitest run sockets/ services/updatePreflightParity.test.js — 5 files, 77 passed.
  • Full server suite: 1917/1919 files, 38712 tests passed. The one failure (routes/settings.secretsStrip.test.js, 10s timeout) is an unrelated contention flake from parallel suite runs on the same machine — it passes in isolation and touches no socket code.
  • Mutation-checked against the source: dropping the repoPath half of findConflictingOperation, un-stripping repoPath from the payload, removing the step dedupe, dropping the failure ledger row, removing the finally cleanup, and removing either standardize early-return each fail exactly the test(s) claiming that behavior, with the source restored afterward.

Closes #6037

https://claude.ai/code/session_01RA3pD5YM2dukQwbZ3pC6WA

#6037)

server/sockets/apps.js owns three pieces of state nothing else can observe
— the re-entrancy guard that stops two concurrent git pull / PM2 standardize
runs against the same checkout, the resumable progress buffer a remounting
client rehydrates from, and the ledger/broadcast/cleanup epilogue that must
run even when the underlying service throws. The only suite importing
registerAppHandlers covered just the PortOS-app preflight refusals, so all
three were unpinned.

Adds server/sockets/apps.test.js, driving the real handlers over a fake
socket/io bus at the event boundary:

- a duplicate update, an update on a different app record sharing the same
  repoPath, and a standardize racing an update are each refused with
  duplicate: true, while an unrelated checkout still proceeds
- a reconnecting client's connect-time push and its app:operations:list
  reply both carry the in-flight operation with repoPath stripped and step
  frames deduplicated last-write-wins per step id
- a rejecting appUpdater.updateApp still writes a success:false ledger row
  with the error message, broadcasts notifyAppsChanged, emits no completion
  frame, and clears the operation
- a standardize failing at analyze never reaches apply, and one failing at
  apply leaves the app record untouched — pm2ProcessNames must not be
  persisted from a run that never wrote the config those names describe

In-flight cases park inside appUpdater.updateApp on a deferred promise
rather than sleeping, so "operation is active" is deterministic with no
timers, and the parked run is released from onTestFinished because
activeAppOperations is module state that outlives a single test.

Claude-Session: https://claude.ai/code/session_01RA3pD5YM2dukQwbZ3pC6WA
@atomantic
atomantic merged commit 1a1f090 into main Sep 3, 2026
7 checks passed
@atomantic
atomantic deleted the claim/issue-6037 branch September 3, 2026 23:00
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

1 participant