Skip to content

Commit bc83d81

Browse files
fix: guard stale workspace preview paths
- guard file explorer directory loads, refreshes, previews, watches, and file opens with workspace-root validation so stale absolute paths do not hit IPC during startup or workspace switches - guard internal surface preview loads and watch subscriptions with the active workspace root so stale document surfaces are ignored cleanly - update the pane source tests to cover the workspace-scoped validation flow in both explorer and internal surfaces - validation: cd desktop && node --test src/components/panes/FileExplorerPane.test.mjs src/components/panes/InternalSurfacePane.test.mjs - validation: cd desktop && npm run typecheck
1 parent cc8cb23 commit bc83d81

File tree

4 files changed

+319
-65
lines changed

4 files changed

+319
-65
lines changed

desktop/src/components/panes/FileExplorerPane.test.mjs

Lines changed: 17 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -27,15 +27,19 @@ test("file explorer syncs the workspace root only when the selected workspace ch
2727
);
2828
assert.match(
2929
source,
30-
/window\.electronAPI\.fs\.listDirectory\(\s*targetPath \?\? null,\s*selectedWorkspaceId \?\? null,\s*\)/,
30+
/const validateWorkspaceScopedTargetPath = useCallback\(/,
31+
);
32+
assert.match(
33+
source,
34+
/const \{ allowed, targetPath: validatedTargetPath \} =\s*await validateWorkspaceScopedTargetPath\(targetPath \?\? null\);[\s\S]*window\.electronAPI\.fs\.listDirectory\(\s*validatedTargetPath,\s*selectedWorkspaceId \?\? null,\s*\)/,
3135
);
3236
assert.match(
3337
source,
3438
/const workspaceSessionKey = workspaceSessionKeyRef\.current;\s*const requestKey = \+\+directoryLoadRequestKeyRef\.current;/,
3539
);
3640
assert.match(
3741
source,
38-
/if \(\s*workspaceSessionKey !== workspaceSessionKeyRef\.current \|\|\s*requestKey !== directoryLoadRequestKeyRef\.current\s*\) \{\s*return;\s*\}/,
42+
/if \(\s*workspaceSessionKey !== workspaceSessionKeyRef\.current \|\|\s*requestKey !== directoryLoadRequestKeyRef\.current \|\|\s*!allowed\s*\) \{\s*return;\s*\}/,
3943
);
4044
assert.match(
4145
source,
@@ -57,15 +61,18 @@ test("file explorer refreshes the current directory and expanded folders to surf
5761
assert.match(source, /refreshTargets\.map\(\(targetPath\) =>/);
5862
assert.match(
5963
source,
60-
/window\.electronAPI\.fs\.listDirectory\(\s*targetPath,\s*selectedWorkspaceId \?\? null,\s*\)/,
64+
/const \{ allowed, targetPath: validatedTargetPath \} =\s*await validateWorkspaceScopedTargetPath\(targetPath\);[\s\S]*if \(!allowed \|\| !validatedTargetPath\) \{\s*return null;\s*\}[\s\S]*window\.electronAPI\.fs\.listDirectory\(\s*validatedTargetPath,\s*selectedWorkspaceId \?\? null,\s*\)/,
6165
);
6266
assert.match(
6367
source,
6468
/setDirectoryEntriesByPath\(\(current\) => \(\{\s*\.\.\.current,\s*\.\.\.refreshedEntriesByPath,\s*\}\)\);/,
6569
);
6670
assert.match(source, /const timer = window\.setInterval\(\(\) => \{\s*void refreshLoadedDirectories\(\);\s*\}, 1200\);/);
6771
assert.match(source, /window\.clearInterval\(timer\);/);
68-
assert.match(source, /\}, \[currentPath, expandedDirectoryPaths, selectedWorkspaceId\]\);/);
72+
assert.match(
73+
source,
74+
/\}, \[\s*currentPath,\s*expandedDirectoryPaths,\s*selectedWorkspaceId,\s*validateWorkspaceScopedTargetPath,\s*\]\);/,
75+
);
6976
});
7077

7178
test("file explorer live-refreshes inline previews from file watch events without overwriting dirty edits", async () => {
@@ -87,14 +94,17 @@ test("file explorer live-refreshes inline previews from file watch events withou
8794
source,
8895
/window\.electronAPI\.fs[\s\S]*\.watchFile\(/,
8996
);
90-
assert.match(source, /watchedPath,\s*selectedWorkspaceId \?\? null/);
9197
assert.match(
9298
source,
9399
/if \(\s*cancelled \|\|\s*refreshInFlight \|\|\s*isDirtyRef\.current \|\|\s*isSavingRef\.current\s*\) \{\s*return;\s*\}/,
94100
);
95101
assert.match(
96102
source,
97-
/const nextPreview = await window\.electronAPI\.fs\.readFilePreview\(\s*watchedPath,\s*selectedWorkspaceId \?\? null,\s*\);[\s\S]*setPreview\(nextPreview\);[\s\S]*setPreviewDraft\(nextPreview\.content \?\? ""\);/,
103+
/const \{ allowed, targetPath: validatedWatchedPath \} =\s*await validateWorkspaceScopedTargetPath\(watchedPath\);[\s\S]*if \(!allowed \|\| !validatedWatchedPath\) \{\s*return;\s*\}[\s\S]*const nextPreview = await window\.electronAPI\.fs\.readFilePreview\(\s*validatedWatchedPath,\s*selectedWorkspaceId \?\? null,\s*\);[\s\S]*setPreview\(nextPreview\);[\s\S]*setPreviewDraft\(nextPreview\.content \?\? ""\);/,
104+
);
105+
assert.match(
106+
source,
107+
/const \{ allowed, targetPath: validatedWatchedPath \} =\s*await validateWorkspaceScopedTargetPath\(watchedPath\);[\s\S]*if \(!allowed \|\| !validatedWatchedPath\) \{\s*return;\s*\}[\s\S]*window\.electronAPI\.fs\.watchFile\(\s*validatedWatchedPath,\s*selectedWorkspaceId \?\? null,\s*\)/,
98108
);
99109
assert.match(source, /void window\.electronAPI\.fs\.unwatchFile\(subscriptionId\);/);
100110
});
@@ -276,7 +286,7 @@ test("file explorer adds a markdown preview mode while keeping text editing inli
276286
assert.match(source, /window\.electronAPI\.ui\.openExternalUrl\(url\)/);
277287
assert.match(
278288
source,
279-
/window\.electronAPI\.fs\.readFilePreview\(\s*targetPath,\s*selectedWorkspaceId \?\? null,\s*\)/,
289+
/const \{ allowed, targetPath: validatedTargetPath \} =\s*await validateWorkspaceScopedTargetPath\(targetPath\);[\s\S]*window\.electronAPI\.fs\.readFilePreview\(\s*validatedTargetPath,\s*selectedWorkspaceId \?\? null,\s*\)/,
280290
);
281291
assert.match(
282292
source,

0 commit comments

Comments
 (0)