Skip to content

Commit 0bab9e3

Browse files
authored
fix(restore): simplify persistence and harden cross-platform cleanup (#602)
## Summary - Follow up #578 by consolidating desktop persistence, restore reconciliation, lifecycle coordination, and regression coverage. - Preserve active drafts and attachments, request-scoped workspace ownership, deletion tombstones, renderer authority, and bounded shutdown behavior. - Fix the reported macOS cleanup failure with targeted BSD process queries and random-token-guarded process-group cleanup, without an unverified PID fallback. ## Platform hardening - Ignore development renderer origins in packaged Electron builds. - Preserve staged Tauri navigation authority and handle confirmed Windows session-end shutdown on the UI thread. - Bound workspace launch preflight, runtime startup, and health readiness. - Retain cleanup ownership after unexpected leaders exit and verify portable POSIX descendants by immutable identity or inherited launch token. - Add real Darwin-only process-group integration tests for macOS CI. ## Scope - 96 files changed. - 6,295 additions and 12,167 deletions, a net reduction of 5,872 lines from the merged implementation. - Consolidated duplicated tests while retaining focused race, durability, cleanup, and platform contracts. ## Validation - pm run typecheck - pm run typecheck --workspace @neuralnomads/codenomad - Electron native suite: 60 passed - Tauri suite: 49 passed - Focused server lifecycle/identity suite: 31 passed, 2 Darwin-only skipped on Windows - Focused UI restore/codec/reconciliation suite: 36 passed - Broader server suite: 59 passed, 3 platform skips - Broader UI suite: 97 passed, 1 skip; 2 Node 25 solid-toast loader failures reproduced on the merged baseline - git diff --check - Final limited gatekeeper: PASS for server/macOS, UI restore, and Electron/Tauri
1 parent e586ea3 commit 0bab9e3

156 files changed

Lines changed: 13999 additions & 13157 deletions

File tree

Some content is hidden

Large Commits have some content hidden by default. Use the searchbox below for content that may be hidden.

.github/workflows/comment-pr-artifacts.yml

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -93,6 +93,11 @@ jobs:
9393
return;
9494
}
9595
96+
if (matchedRun.conclusion !== 'success') {
97+
core.setFailed(`PR Build Validation run ${matchedRun.id} concluded ${matchedRun.conclusion}.`);
98+
return;
99+
}
100+
96101
const artifacts = await github.paginate(
97102
github.rest.actions.listWorkflowRunArtifacts,
98103
{ owner, repo, run_id: matchedRun.id, per_page: 100 }

.github/workflows/pr-build.yml

Lines changed: 117 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -46,7 +46,10 @@ jobs:
4646
fi
4747
4848
build:
49-
needs: authorize
49+
needs:
50+
- authorize
51+
- tests
52+
- tests-tauri-windows
5053
if: ${{ needs.authorize.outputs.allowed == 'true' && !github.event.pull_request.draft }}
5154
uses: ./.github/workflows/build-and-upload.yml
5255
with:
@@ -56,3 +59,116 @@ jobs:
5659
actions_artifacts_retention_days: 7
5760
actions_artifacts_name_prefix: pr-${{ github.event.pull_request.number }}-${{ github.event.pull_request.head.sha }}-
5861
set_versions: false
62+
63+
tests:
64+
needs: authorize
65+
if: ${{ needs.authorize.outputs.allowed == 'true' && !github.event.pull_request.draft }}
66+
runs-on: ubuntu-latest
67+
steps:
68+
- name: Checkout
69+
uses: actions/checkout@v4
70+
with:
71+
ref: ${{ github.event.pull_request.head.sha }}
72+
73+
- name: Setup Node
74+
uses: actions/setup-node@v4
75+
with:
76+
node-version: 22
77+
cache: npm
78+
79+
- name: Setup Rust
80+
uses: dtolnay/rust-toolchain@stable
81+
82+
- name: Install Linux test dependencies (Tauri)
83+
run: |
84+
sudo apt-get update
85+
sudo apt-get install -y \
86+
build-essential \
87+
pkg-config \
88+
libgtk-3-dev \
89+
libglib2.0-dev \
90+
libwebkit2gtk-4.1-dev \
91+
libsoup-3.0-dev \
92+
libayatana-appindicator3-dev \
93+
librsvg2-dev
94+
95+
- name: Install dependencies
96+
run: npm ci
97+
98+
- name: Typecheck desktop clients
99+
run: npm run typecheck
100+
101+
- name: Test Electron client state
102+
run: npm run test:native --workspace @neuralnomads/codenomad-electron-app
103+
104+
- name: Test changed runnable UI behavior
105+
run: >-
106+
node --import tsx --test
107+
packages/ui/src/lib/hooks/use-app-session-capture.test.ts
108+
packages/ui/src/lib/trailing-resync.test.ts
109+
packages/ui/src/stores/abort-created-workspace-cleanup.test.ts
110+
packages/ui/src/stores/app-session-reconciliation.test.ts
111+
packages/ui/src/stores/app-session-restore-gate.test.ts
112+
packages/ui/src/stores/app-session-restore-queue.test.ts
113+
packages/ui/src/stores/app-session-restore-timeout.test.ts
114+
packages/ui/src/stores/app-session-snapshot-merge.test.ts
115+
packages/ui/src/stores/restore-workspace-commit-gates.test.ts
116+
packages/ui/src/stores/client-state-codec.test.ts
117+
packages/ui/src/stores/client-state.test.ts
118+
packages/ui/src/stores/instances-restore-cancellation.test.ts
119+
packages/ui/src/stores/message-v2/message-hydration-authority.test.ts
120+
packages/ui/src/stores/session-generation-recovery.test.ts
121+
packages/ui/src/stores/session-metadata.test.ts
122+
packages/ui/src/stores/session-pagination.test.ts
123+
packages/ui/src/stores/workspace-list-reconciliation-fence.test.ts
124+
125+
- name: Test restore ownership integration
126+
run: >-
127+
node --conditions=browser --import tsx --test --test-force-exit
128+
packages/ui/src/stores/instances-restore-ownership.test.ts
129+
130+
- name: Test server
131+
run: node --import tsx --test "packages/server/src/**/*.test.ts"
132+
133+
- name: Prepare Tauri test resources
134+
run: >-
135+
npm run dev:prep --workspace @codenomad/tauri-app &&
136+
node -e "require('fs').mkdirSync('packages/tauri-app/src-tauri/resources/server',{recursive:true})"
137+
138+
- name: Test Tauri crate
139+
working-directory: packages/tauri-app/src-tauri
140+
run: cargo test --locked
141+
142+
tests-tauri-windows:
143+
needs: authorize
144+
if: ${{ needs.authorize.outputs.allowed == 'true' && !github.event.pull_request.draft }}
145+
runs-on: windows-latest
146+
steps:
147+
- name: Checkout
148+
uses: actions/checkout@v4
149+
with:
150+
ref: ${{ github.event.pull_request.head.sha }}
151+
152+
- name: Setup Node
153+
uses: actions/setup-node@v4
154+
with:
155+
node-version: 22
156+
cache: npm
157+
158+
- name: Setup Rust
159+
uses: dtolnay/rust-toolchain@stable
160+
161+
- name: Install dependencies
162+
run: npm ci
163+
164+
- name: Test Windows server spawn behavior
165+
run: node --import tsx --test packages/server/src/workspaces/__tests__/spawn.test.ts
166+
167+
- name: Prepare Tauri test resources
168+
run: >-
169+
npm run dev:prep --workspace @codenomad/tauri-app &&
170+
node -e "require('fs').mkdirSync('packages/tauri-app/src-tauri/resources/server',{recursive:true})"
171+
172+
- name: Test Tauri crate on Windows
173+
working-directory: packages/tauri-app/src-tauri
174+
run: cargo test --locked
Lines changed: 47 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,47 @@
1+
import { existsSync, writeFileSync } from "node:fs"
2+
import { CrossHostRegistration, createCrossHostOwner } from "./client-state-cross-host"
3+
import { getProcessStartIdentity } from "./client-state-process-identity"
4+
import { isPidAlive } from "./client-state-process"
5+
import { ClientStateManager } from "./client-state"
6+
7+
const [directory, startPath, readyPath, mode, userDataPath, participantReadyPath, participantContinuePath, legacyTauriDataPath, operation, payload] = process.argv.slice(2)
8+
if (!directory || !startPath) throw new Error("Expected election directory and start path")
9+
if (readyPath) writeFileSync(readyPath, "")
10+
while (!existsSync(startPath)) Atomics.wait(new Int32Array(new SharedArrayBuffer(4)), 0, 0, 5)
11+
12+
const manager = mode === "full" && userDataPath
13+
? new ClientStateManager(userDataPath, undefined, {
14+
crossHostElectionDirectory: directory,
15+
legacyTauriDataPath: legacyTauriDataPath || null,
16+
crossHostDependencies: {
17+
pidAlive: isPidAlive,
18+
processStartIdentity: getProcessStartIdentity,
19+
onParticipantPublished: participantReadyPath && participantContinuePath
20+
? () => {
21+
writeFileSync(participantReadyPath, "")
22+
while (!existsSync(participantContinuePath)) Atomics.wait(new Int32Array(new SharedArrayBuffer(4)), 0, 0, 5)
23+
}
24+
: undefined,
25+
},
26+
})
27+
: undefined
28+
const owner = manager ? undefined : createCrossHostOwner()
29+
const registration = owner && CrossHostRegistration.register(directory, owner, true, {
30+
pidAlive: mode === "retire-crash" ? () => false : isPidAlive,
31+
processStartIdentity: getProcessStartIdentity,
32+
onOwnerPrepared: mode === "owner-crash" ? () => process.exit(91) : undefined,
33+
onOwnerRetired: mode === "retire-crash" ? () => process.exit(91) : undefined,
34+
})
35+
if (manager?.isPrimary && operation === "save") {
36+
await manager.setRestoreEnabled(true)
37+
await manager.saveClientState(JSON.parse(payload))
38+
}
39+
process.stdout.write(`${JSON.stringify({
40+
acquired: manager?.isPrimary ?? Boolean(registration?.isPrimary),
41+
state: operation === "load" ? manager?.loadClientState() : undefined,
42+
})}\n`)
43+
process.stdin.resume()
44+
process.stdin.once("end", () => {
45+
if (manager) void manager.drainAndReleasePrimary().finally(() => process.exit())
46+
else registration?.release()
47+
})

0 commit comments

Comments
 (0)