Fix fsync of read-only files on POSIX (#14235)

* fix(files): fsync read-only files on POSIX

* test(e2e): add golden E2E tests for POSIX profile index fsync

Validates that profile index files are properly persisted on POSIX systems, including with restrictive umask settings. These are release-blocking golden tests for Linux and macOS.

* test(terminal): wait for fish child ownership before stdin write

Fish 4.8 withdraws DECSET 2031 before spawning the child, so the
shell-contracts harness could send hello into an intermediate prompt
and hang waiting for CHILD-READ. Wait for the child's CHILD-READY
marker and answer split DA1/CPR/OSC queries across chunk boundaries.

* test(e2e): verify profile index persists to disk with restrictive umask

Strengthen the POSIX fsync test to verify the rebuilt index is actually
written to disk and has correct permissions under a restrictive umask,
not just cached in memory.
This commit is contained in:
Jinjing
2026-08-13 00:58:55 -07:00
committed by GitHub
parent 2c28b6c92c
commit 7c93aed6dc
10 changed files with 291 additions and 23 deletions
@@ -66,12 +66,14 @@ jobs:
run: |
xvfb-run --auto-servernum env SKIP_BUILD=1 ORCA_E2E_FORWARD_APP_LOGS=1 pnpm run test:e2e -- tests/e2e/golden-core-flows.spec.ts
xvfb-run --auto-servernum env SKIP_BUILD=1 ORCA_E2E_FORWARD_APP_LOGS=1 pnpm run test:e2e:terminal-rendering-golden
xvfb-run --auto-servernum env SKIP_BUILD=1 ORCA_E2E_FORWARD_APP_LOGS=1 pnpm run test:e2e:posix-profile-index-golden
- name: Run golden E2E tests on macOS
if: runner.os == 'macOS'
run: |
env SKIP_BUILD=1 ORCA_E2E_FORWARD_APP_LOGS=1 pnpm run test:e2e -- tests/e2e/golden-core-flows.spec.ts
env SKIP_BUILD=1 ORCA_E2E_FORWARD_APP_LOGS=1 pnpm run test:e2e:terminal-rendering-golden
env SKIP_BUILD=1 ORCA_E2E_FORWARD_APP_LOGS=1 pnpm run test:e2e:posix-profile-index-golden
- name: Run golden E2E tests on Windows
if: runner.os == 'Windows'
+7 -3
View File
@@ -783,7 +783,7 @@ jobs:
{
echo "## Release E2E Signal"
echo ""
echo "- Platform golden E2E is release-blocking: terminal rendering on Linux/macOS and fresh startup on Windows."
echo "- Platform golden E2E is release-blocking: terminal rendering and restrictive-umask profile writes on Linux/macOS, plus fresh startup on Windows."
echo "- Full E2E runs separately after publication and cannot change the release result."
echo "- Terminal rendering release evidence is diagnostic/non-blocking."
echo ""
@@ -871,11 +871,15 @@ jobs:
- name: Run terminal rendering golden on Linux
if: runner.os == 'Linux'
run: xvfb-run --auto-servernum env SKIP_BUILD=1 ORCA_E2E_FORWARD_APP_LOGS=1 pnpm run test:e2e:terminal-rendering-golden
run: |
xvfb-run --auto-servernum env SKIP_BUILD=1 ORCA_E2E_FORWARD_APP_LOGS=1 pnpm run test:e2e:terminal-rendering-golden
xvfb-run --auto-servernum env SKIP_BUILD=1 ORCA_E2E_FORWARD_APP_LOGS=1 pnpm run test:e2e:posix-profile-index-golden
- name: Run terminal rendering golden on macOS
if: runner.os == 'macOS'
run: env SKIP_BUILD=1 ORCA_E2E_FORWARD_APP_LOGS=1 pnpm run test:e2e:terminal-rendering-golden
run: |
env SKIP_BUILD=1 ORCA_E2E_FORWARD_APP_LOGS=1 pnpm run test:e2e:terminal-rendering-golden
env SKIP_BUILD=1 ORCA_E2E_FORWARD_APP_LOGS=1 pnpm run test:e2e:posix-profile-index-golden
- name: Run fresh-startup golden on Windows
if: runner.os == 'Windows'
@@ -80,6 +80,7 @@ describe('Electron runtime package contract', () => {
'build:linux',
'test:e2e',
'test:e2e:terminal-rendering-golden',
'test:e2e:posix-profile-index-golden',
'test:e2e:terminal-rendering-release-evidence',
'test:e2e:headful'
]
@@ -543,6 +544,15 @@ describe('Electron runtime package contract', () => {
expect(packageScripts['test:e2e:windows-fresh-startup-golden']).toContain(
'@windows-fresh-startup-golden'
)
expect(packageScripts['test:e2e:posix-profile-index-golden']).toContain(
'golden-posix-profile-index-fsync.spec.ts'
)
expect(packageScripts['test:e2e:posix-profile-index-golden']).toContain(
'golden-posix-fresh-startup.spec.ts'
)
expect(packageScripts['test:e2e:posix-profile-index-golden']).toContain(
'@posix-profile-index-golden'
)
expect(packageScripts['test:e2e:terminal-rendering-release-evidence']).toContain(
'terminal-opencode-emoji-table-rendering.spec.ts'
)
@@ -557,7 +567,11 @@ describe('Electron runtime package contract', () => {
expect(goldenRunSteps.get('linux')?.run).toContain(
'pnpm run test:e2e:terminal-rendering-golden'
)
expect(goldenRunSteps.get('linux')?.run).toContain(
'pnpm run test:e2e:posix-profile-index-golden'
)
expect(goldenRunSteps.get('mac')?.run).toContain('pnpm run test:e2e:terminal-rendering-golden')
expect(goldenRunSteps.get('mac')?.run).toContain('pnpm run test:e2e:posix-profile-index-golden')
expect(goldenRunSteps.get('windows')).toMatchObject({
if: "runner.os == 'Windows'",
shell: 'pwsh'
@@ -574,9 +588,16 @@ describe('Electron runtime package contract', () => {
expect(publishReleaseNeeds).not.toContain('terminal-rendering-release-evidence')
expect(releaseGoldenJob['continue-on-error']).toBeUndefined()
expect(releaseGoldenMatrix).toEqual(goldenMatrix)
expect(releaseGoldenJob.steps.map((step) => step.run ?? '')).toContain(
'xvfb-run --auto-servernum env SKIP_BUILD=1 ORCA_E2E_FORWARD_APP_LOGS=1 pnpm run test:e2e:terminal-rendering-golden'
const releaseLinuxRunStep = releaseGoldenJob.steps.find(
(step) => step.name === 'Run terminal rendering golden on Linux'
)
expect(releaseLinuxRunStep.run).toContain('pnpm run test:e2e:terminal-rendering-golden')
expect(releaseLinuxRunStep.run).toContain('pnpm run test:e2e:posix-profile-index-golden')
const releaseMacRunStep = releaseGoldenJob.steps.find(
(step) => step.name === 'Run terminal rendering golden on macOS'
)
expect(releaseMacRunStep.run).toContain('pnpm run test:e2e:terminal-rendering-golden')
expect(releaseMacRunStep.run).toContain('pnpm run test:e2e:posix-profile-index-golden')
const releaseWindowsRunStep = releaseGoldenJob.steps.find(
(step) => step.name === 'Run fresh-startup golden on Windows'
)
+1
View File
@@ -87,6 +87,7 @@
"test:e2e:multi-client-navigation": "node config/scripts/run-multi-client-navigation-e2e.mjs",
"test:e2e:floating-mobile-emulator": "pnpm run ensure:electron-runtime && npx playwright test tests/e2e/floating-mobile-emulator-tab.spec.ts --config tests/playwright.config.ts --project electron-headless --workers=1",
"test:e2e:terminal-rendering-golden": "pnpm run ensure:electron-runtime && npx playwright test tests/e2e/terminal-raw-emoji-table-scroll-restore.spec.ts tests/e2e/terminal-webgl-atlas-budget.spec.ts --grep @terminal-rendering-golden --config tests/playwright.config.ts --project electron-headless --workers=1",
"test:e2e:posix-profile-index-golden": "pnpm run ensure:electron-runtime && npx playwright test tests/e2e/golden-posix-fresh-startup.spec.ts tests/e2e/golden-posix-profile-index-fsync.spec.ts --grep @posix-profile-index-golden --config tests/playwright.config.ts --project electron-headless --workers=1",
"test:e2e:windows-fresh-startup-golden": "pnpm run ensure:electron-runtime && npx playwright test tests/e2e/golden-windows-fresh-startup.spec.ts --grep @windows-fresh-startup-golden --config tests/playwright.config.ts --project electron-headless --workers=1",
"test:e2e:terminal-rendering-release-evidence": "pnpm run ensure:electron-runtime && npx playwright test tests/e2e/terminal-opencode-emoji-table-rendering.spec.ts tests/e2e/terminal-long-table-scroll-restore.spec.ts --config tests/playwright.config.ts --project electron-headless --workers=2",
"test:e2e:terminal-perf": "pnpm run ensure:electron-runtime && npx playwright test tests/e2e/terminal-typing-latency.spec.ts tests/e2e/terminal-foreground-redraw-freeze.spec.ts tests/e2e/terminal-output-scheduler.spec.ts tests/e2e/terminal-hidden-tui-visual-restore.spec.ts tests/e2e/artificial-opencode-terminal-load.spec.ts --config tests/playwright.config.ts --project electron-headless --workers=2",
@@ -3,6 +3,7 @@ import { existsSync, mkdtempSync, readFileSync, rmSync, writeFileSync, mkdirSync
import { join } from 'node:path'
import { tmpdir } from 'node:os'
import {
createDefaultLocalOrcaProfile,
DEFAULT_LOCAL_ORCA_PROFILE_ID,
DEFAULT_LOCAL_ORCA_PROFILE_NAME,
ORCA_PROFILE_INDEX_SCHEMA_VERSION,
@@ -154,6 +155,27 @@ describe('profile index store', () => {
expect(() => setActiveOrcaProfile('missing-profile')).toThrow('unknown_orca_profile')
})
const posixIt = process.platform === 'win32' ? it.skip : it
posixIt('writes a fresh profile index when umask removes owner-write permission', async () => {
const store = await loadProfileIndexStore()
const indexPath = store.getOrcaProfileIndexPath()
const profile = createDefaultLocalOrcaProfile(1)
const index: OrcaProfileIndex = {
schemaVersion: ORCA_PROFILE_INDEX_SCHEMA_VERSION,
activeProfileId: profile.id,
profiles: [profile]
}
const originalUmask = process.umask(0o200)
try {
expect(() => store.writeProfileIndex(indexPath, index)).not.toThrow()
} finally {
process.umask(originalUmask)
}
expect(readJson(indexPath)).toEqual(index)
})
it('recovers a corrupted profile index from the backup copy', async () => {
const store = await loadProfileIndexStore()
store.ensureActiveOrcaProfile()
@@ -39,6 +39,20 @@ const COLOR_SCHEME_REPORT_PREFIX = '\x1b[?997'
const ARM_2031 = '\x1b[?2031h'
const WITHDRAW_2031 = '\x1b[?2031l'
const LEAF_1 = '11111111-1111-4111-8111-111111111111'
const DA1_REPLY = '\x1b[?62;4;6;22c'
const DSR_REPLY = '\x1b[1;1R'
const OSC_10_REPLY = '\x1b]10;rgb:eeee/eeee/eeee\x1b\\'
const OSC_11_REPLY = '\x1b]11;rgb:1e1e/1e1e/1e1e\x1b\\'
const TERMINAL_QUERY_REPLIES: readonly (readonly [string, string])[] = [
['\x1b[0c', DA1_REPLY],
['\x1b[c', DA1_REPLY],
['\x1b[6n', DSR_REPLY],
['\x1b]10;?\x07', OSC_10_REPLY],
['\x1b]10;?\x1b\\', OSC_10_REPLY],
['\x1b]11;?\x07', OSC_11_REPLY],
['\x1b]11;?\x1b\\', OSC_11_REPLY]
]
const QUERY_CARRY_LENGTH = Math.max(...TERMINAL_QUERY_REPLIES.map(([query]) => query.length)) - 1
type MutableState = Record<string, unknown>
let mockStoreState: MutableState = {}
@@ -216,6 +230,114 @@ async function waitUntil(predicate: () => boolean, timeoutMs: number): Promise<b
return false
}
function createTerminalQueryResponder(write: (reply: string) => void): {
accept: (chunk: string) => void
getCarryLength: () => number
} {
let carry = ''
return {
accept: (chunk) => {
const carriedLength = carry.length
const scan = carry + chunk
carry = scan.slice(-QUERY_CARRY_LENGTH)
for (let at = 0; at < scan.length; at += 1) {
for (const [query, reply] of TERMINAL_QUERY_REPLIES) {
if (!scan.startsWith(query, at)) {
continue
}
if (at + query.length > carriedLength) {
write(reply)
}
at += query.length - 1
break
}
}
},
getCarryLength: () => carry.length
}
}
function allChunkPartitions(input: string): string[][] {
if (input.length === 0) {
return [[]]
}
const partitions: string[][] = []
for (let end = 1; end <= input.length; end += 1) {
for (const suffix of allChunkPartitions(input.slice(end))) {
partitions.push([input.slice(0, end), ...suffix])
}
}
return partitions
}
function collectTerminalQueryReplies(chunks: readonly string[]): string[] {
const replies: string[] = []
const responder = createTerminalQueryResponder((reply) => replies.push(reply))
chunks.forEach(responder.accept)
return replies
}
describe('terminal query responder', () => {
it.each([
['OSC 10 BEL', '\x1b]10;?\x07', OSC_10_REPLY],
['OSC 10 ST', '\x1b]10;?\x1b\\', OSC_10_REPLY],
['OSC 11 BEL', '\x1b]11;?\x07', OSC_11_REPLY],
['OSC 11 ST', '\x1b]11;?\x1b\\', OSC_11_REPLY]
])('answers each complete %s query once across every partition', (_label, query, reply) => {
for (const chunks of allChunkPartitions(query)) {
expect(collectTerminalQueryReplies(chunks)).toEqual([reply])
}
})
it.each([
['DA1 with omitted parameter', '\x1b[c', DA1_REPLY],
['DA1 with zero parameter', '\x1b[0c', DA1_REPLY],
['DSR cursor position', '\x1b[6n', DSR_REPLY]
])('preserves %s across every partition', (_label, query, reply) => {
for (const chunks of allChunkPartitions(query)) {
expect(collectTerminalQueryReplies(chunks)).toEqual([reply])
}
})
it.each([
['unterminated OSC 10', '\x1b]10;?'],
['unterminated OSC 11 ST', '\x1b]11;?\x1b'],
['OSC 10 extra query marker', '\x1b]10;??\x07'],
['OSC 11 stacked query', '\x1b]11;?;?\x07'],
['OSC 10 malformed body', '\x1b]10;?x\x07'],
['OSC 11 malformed ST body', '\x1b]11;?x\x1b\\'],
['OSC slot lookalike', '\x1b]110;?\x07']
])('rejects %s across every partition', (_label, input) => {
for (const chunks of allChunkPartitions(input)) {
expect(collectTerminalQueryReplies(chunks)).toEqual([])
}
})
it('answers concatenated queries once each in source order across every partition', () => {
const input = '\x1b]10;?\x07\x1b]11;?\x07'
for (const chunks of allChunkPartitions(input)) {
expect(collectTerminalQueryReplies(chunks)).toEqual([OSC_10_REPLY, OSC_11_REPLY])
}
})
it('bounds carry while rejecting a long unterminated query lookalike', () => {
const replies: string[] = []
const responder = createTerminalQueryResponder((reply) => replies.push(reply))
let maxCarryLength = 0
for (const fragment of `\x1b]10;?${'x'.repeat(10_000)}`) {
responder.accept(fragment)
maxCarryLength = Math.max(maxCarryLength, responder.getCarryLength())
}
for (const fragment of '\x1b]10;?\x07') {
responder.accept(fragment)
}
expect(maxCarryLength).toBe(QUERY_CARRY_LENGTH)
expect(responder.getCarryLength()).toBeLessThanOrEqual(QUERY_CARRY_LENGTH)
expect(replies).toEqual([OSC_10_REPLY])
})
})
describe('fish never receives a color-scheme report it did not query (#9993)', () => {
let configHome: string | null = null
@@ -375,7 +497,8 @@ describe('fish never receives a color-scheme report it did not query (#9993)', (
" if (!buffered.includes('\\n')) return\n" +
" process.stdout.write('CHILD-READ:' + JSON.stringify(buffered) + '\\n')\n" +
' process.exit(0)\n' +
'})\n'
'})\n' +
"process.stdout.write('CHILD-READY\\n')\n"
)
const term = nodePty.spawn(FISH_BIN as string, ['-l', '-i'], {
@@ -398,20 +521,16 @@ describe('fish never receives a color-scheme report it did not query (#9993)', (
let rendered = ''
const { transport, sent, emit } = createPtyBackedTransport((data) => term.write(data))
const answerTerminalQueries = createTerminalQueryResponder((reply) => term.write(reply))
transportFactoryQueue.push(transport)
// Why answered here: no real xterm is attached, and fish blocks its first prompt ~10s
// on DA1 and re-probes every prompt. These are harness bytes, never renderer output.
term.onData((chunk) => {
rendered += chunk
if (chunk.includes('\x1b[0c') || chunk.includes('\x1b[c')) {
term.write('\x1b[?62;4;6;22c')
}
if (chunk.includes('\x1b[6n')) {
term.write('\x1b[1;1R')
}
if (chunk.includes('\x1b]10;?') || chunk.includes('\x1b]11;?')) {
term.write('\x1b]11;rgb:1e1e/1e1e/1e1e\x1b\\')
// Maximal fragmentation keeps query handling independent of node-pty chunk boundaries.
for (const fragment of chunk) {
answerTerminalQueries.accept(fragment)
}
emit(chunk)
})
@@ -444,8 +563,10 @@ describe('fish never receives a color-scheme report it did not query (#9993)', (
// Withdrawal #1: `sleep` owns the tty now, so the next line is typed ahead.
expect(await waitUntil(() => countOf(rendered, WITHDRAW_2031) >= 1, 5_000)).toBe(true)
term.write('"$ORCA_NODE_BIN" "$ORCA_CHILD_SCRIPT"\r')
// Withdrawal #2: fish re-armed for the prompt and handed the tty to the child.
// Withdrawal #2: fish re-armed for the prompt and accepted the child command.
expect(await waitUntil(() => countOf(rendered, WITHDRAW_2031) >= 2, 5_000)).toBe(true)
// DECSET withdrawal precedes fish's child spawn; the child's marker is the ownership signal.
expect(await waitUntil(() => rendered.includes('CHILD-READY'), 5_000)).toBe(true)
const renderedBeforeChildInput = rendered.length
// Canonical mode buffers this in the tty, so it queues behind anything already
+44 -7
View File
@@ -1,23 +1,33 @@
import { mkdtempSync, rmSync, writeFileSync } from 'node:fs'
import { chmodSync, mkdtempSync, rmSync, writeFileSync } from 'node:fs'
import type * as NodeFs from 'node:fs'
import { tmpdir } from 'node:os'
import { join } from 'node:path'
import { afterEach, describe, expect, it, vi } from 'vitest'
const openedPaths = vi.hoisted(() => [] as { path: string; flags: string | number }[])
const openedPaths = vi.hoisted(
() => [] as { path: string; flags: string | number; ownerWritable: boolean }[]
)
vi.mock('node:fs', async () => {
const actual = await vi.importActual<typeof NodeFs>('node:fs')
return {
...actual,
openSync: (path: NodeFs.PathLike, flags: string | number, mode?: NodeFs.Mode) => {
openedPaths.push({ path: String(path), flags })
openedPaths.push({
path: String(path),
flags,
ownerWritable: Boolean(actual.statSync(path).mode & 0o200)
})
return actual.openSync(path, flags, mode)
}
}
})
import { bestEffortFsyncDirectorySync, fsyncFileSync } from './secure-file'
import {
bestEffortFsyncDirectorySync,
fsyncFileSync,
writeDurableSecureJsonFile
} from './secure-file'
const createdPaths: string[] = []
@@ -29,7 +39,8 @@ afterEach(() => {
})
describe('secure file fsync flags', () => {
it('opens files read/write before fsync', () => {
const windowsIt = process.platform === 'win32' ? it : it.skip
windowsIt('opens files read/write before fsync on Windows', () => {
const directory = mkdtempSync(join(tmpdir(), 'orca-file-fsync-'))
createdPaths.push(directory)
const path = join(directory, 'record.json')
@@ -37,16 +48,42 @@ describe('secure file fsync flags', () => {
fsyncFileSync(path)
expect(openedPaths).toEqual([{ path, flags: 'r+' }])
expect(openedPaths).toMatchObject([{ path, flags: 'r+' }])
})
const posixIt = process.platform === 'win32' ? it.skip : it
posixIt('fsyncs files without owner-write permission', () => {
const directory = mkdtempSync(join(tmpdir(), 'orca-file-fsync-read-only-'))
createdPaths.push(directory)
const path = join(directory, 'record.json')
writeFileSync(path, '{}')
chmodSync(path, 0o400)
expect(() => fsyncFileSync(path)).not.toThrow()
expect(openedPaths).toEqual([{ path, flags: 'r', ownerWritable: false }])
})
posixIt('durably writes when umask removes owner-write from the temporary file', () => {
const directory = mkdtempSync(join(tmpdir(), 'orca-durable-file-fsync-read-only-'))
createdPaths.push(directory)
const path = join(directory, 'record.json')
const originalUmask = process.umask(0o200)
try {
expect(() => writeDurableSecureJsonFile(path, { ok: true })).not.toThrow()
} finally {
process.umask(originalUmask)
}
expect(openedPaths[0]).toMatchObject({ flags: 'r', ownerWritable: false })
})
posixIt('opens directories read-only before fsync', () => {
const directory = mkdtempSync(join(tmpdir(), 'orca-directory-fsync-'))
createdPaths.push(directory)
bestEffortFsyncDirectorySync(directory)
expect(openedPaths).toEqual([{ path: directory, flags: 'r' }])
expect(openedPaths).toMatchObject([{ path: directory, flags: 'r' }])
})
})
+1 -1
View File
@@ -144,7 +144,7 @@ function fsyncPathSync(path: string, flags: 'r' | 'r+'): void {
export function fsyncFileSync(path: string): void {
// FlushFileBuffers requires a write-capable handle on Windows.
fsyncPathSync(path, 'r+')
fsyncPathSync(path, process.platform === 'win32' ? 'r+' : 'r')
}
export function bestEffortFsyncDirectorySync(directory: string): void {
@@ -0,0 +1,15 @@
import { expect, test } from './helpers/orca-app'
test.skip(process.platform === 'win32', 'POSIX fresh-startup golden; Windows has its own suite')
test.describe('POSIX fresh startup golden', () => {
test.use({ dismissOnboarding: false, seedTestRepo: false })
test('fresh profile reaches onboarding normally @posix-profile-index-golden', async ({
orcaPage
}) => {
await expect(orcaPage.getByRole('heading', { name: /Pick your default agent/i })).toBeVisible({
timeout: 30_000
})
})
})
@@ -0,0 +1,45 @@
import { readFileSync, rmSync, statSync } from 'node:fs'
import path from 'node:path'
import { expect, test } from './helpers/orca-app'
test.use({ seedTestRepo: false })
test.skip(process.platform === 'win32', 'Restrictive-umask fsync regression is POSIX-only')
test('recreates a fresh profile index on disk with a restrictive umask @posix-profile-index-golden', async ({
electronApp,
orcaPage
}) => {
const userDataDir = await electronApp.evaluate(({ app }) => app.getPath('userData'))
const indexPath = path.join(userDataDir, 'orca-profile-index.json')
// Why: the index tmp file is written with 0o666 & ~umask, so a umask that clears
// owner-write is what made the pre-fix fsync open(path, 'r+') fail with EACCES.
const originalUmask = await electronApp.evaluate(() => process.umask(0o200))
try {
rmSync(indexPath, { force: true })
rmSync(`${indexPath}.bak`, { force: true })
// Why: orcaProfiles:list reads the index from disk on every call and rebuilds
// it when missing, so this drives the real create + fsync + rename path.
const listed = await orcaPage.evaluate(async () => {
const result = await window.api.orcaProfiles.list()
window.__store!.getState().openSettingsPage()
return result
})
expect(listed.profiles.length).toBeGreaterThan(0)
// The rebuilt index must be on disk and complete — a cached in-memory list is
// not proof that the restrictive-umask write survived fsync.
const persisted = JSON.parse(readFileSync(indexPath, 'utf-8'))
expect(persisted.activeProfileId).toBe(listed.activeProfileId)
expect(persisted.profiles.map((profile: { id: string }) => profile.id)).toEqual(
listed.profiles.map((profile) => profile.id)
)
// Owner-write cleared proves the file was created under the restrictive umask
// rather than left over from startup's normal-umask write.
expect(statSync(indexPath).mode & 0o200).toBe(0)
} finally {
await electronApp.evaluate((_electron, umask) => process.umask(umask), originalUmask)
}
await expect(orcaPage.getByPlaceholder('Search settings')).toBeVisible()
})