Skip to content

Commit d54a4c3

Browse files
Thewookie-Engclaude
andcommitted
test(desktop): make the unit suite pass on a Windows host
The desktop suite is run by CI on macOS, where it passes; on Windows 19 tests failed for reasons in the host rather than the code under test. - 15 tests are skipped on win32: eleven build their fixture with symlink(), which Windows refuses without Developer Mode or elevation, and four assert POSIX mode bits (0o600/0o700) that Windows does not have. - Three path expectations were hard-coded with `/`; they now go through path.join or basename, matching the implementation. - The microphone-settings ipc test expected a settings pane only on macOS, but the handler has carried a Windows ms-settings URL since it was written. Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
1 parent 75d07ab commit d54a4c3

9 files changed

Lines changed: 185 additions & 143 deletions

File tree

‎apps/desktop/src/main/browser-agent/file-transfer.test.ts‎

Lines changed: 3 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -1,7 +1,7 @@
11
import { existsSync, mkdtempSync, readFileSync, writeFileSync } from 'node:fs'
22
import { open, rename, rm, truncate } from 'node:fs/promises'
33
import { tmpdir } from 'node:os'
4-
import { join } from 'node:path'
4+
import { basename, join } from 'node:path'
55
import { BROWSER_FILE_TRANSFER_MAX_BYTES } from '@sim/browser-protocol'
66
import { afterEach, beforeEach, describe, expect, it, vi } from 'vitest'
77

@@ -59,7 +59,7 @@ describe('stageUploadFiles', () => {
5959
body: JSON.stringify({ toolCallId: 'call-1', index: 0 }),
6060
signal,
6161
})
62-
expect(staged.map((path) => path.split('/').pop())).toEqual(['Q3 plan.pdf', 'granted.txt'])
62+
expect(staged.map((path) => basename(path))).toEqual(['Q3 plan.pdf', 'granted.txt'])
6363
expect(readFileSync(staged[0], 'utf8')).toBe('workspace bytes')
6464
expect(readFileSync(staged[1], 'utf8')).toBe('local bytes')
6565
expect(staged.every((path) => path.startsWith(join(temp, 'sim-browser-uploads')))).toBe(true)
@@ -83,7 +83,7 @@ describe('stageUploadFiles', () => {
8383
})
8484

8585
expect(staged.startsWith(join(temp, 'sim-browser-uploads'))).toBe(true)
86-
expect(staged.endsWith('/evil')).toBe(true)
86+
expect(basename(staged)).toBe('evil')
8787
})
8888

8989
it('refuses a workspace file over the transfer ceiling', async () => {

‎apps/desktop/src/main/browser-credentials/vault.test.ts‎

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -86,7 +86,8 @@ describe('CredentialVault', () => {
8686
expect(JSON.parse(onDisk)).toMatchObject({ version: 1, ciphertext: expect.any(String) })
8787
})
8888

89-
it('writes the vault file owner-only', async () => {
89+
// Windows has no POSIX mode bits to assert on.
90+
it.skipIf(process.platform === 'win32')('writes the vault file owner-only', async () => {
9091
const vault = new CredentialVault(vaultPath, encryption())
9192
await vault.importCredentials(CANDIDATES, 'keep-existing')
9293

‎apps/desktop/src/main/browser-import/chromium-profiles.test.ts‎

Lines changed: 21 additions & 14 deletions
Original file line numberDiff line numberDiff line change
@@ -48,6 +48,10 @@ async function writeLocalState(
4848
)
4949
}
5050

51+
// Creating a symlink on Windows needs Developer Mode or elevation, which the
52+
// test runner cannot assume.
53+
const NO_SYMLINKS = process.platform === 'win32'
54+
5155
describe('listBrowserProfiles', () => {
5256
it('returns display names, default profile first, with namespaced ids', async () => {
5357
await addProfile(CHROME, 'Profile 2')
@@ -81,7 +85,7 @@ describe('listBrowserProfiles', () => {
8185
expect(profiles.map(({ id }) => id)).toEqual(['chrome:Default'])
8286
})
8387

84-
it('refuses a profile directory redirected through a symlink', async () => {
88+
it.skipIf(NO_SYMLINKS)('refuses a profile directory redirected through a symlink', async () => {
8589
const outsideProfile = join(home, 'outside-profile')
8690
await mkdir(outsideProfile, { recursive: true })
8791
await writeFile(join(outsideProfile, 'Login Data'), '')
@@ -93,20 +97,23 @@ describe('listBrowserProfiles', () => {
9397
await expect(listBrowserProfiles(CHROME, home)).resolves.toEqual([])
9498
})
9599

96-
it('refuses symlinked, hard-linked, and non-file password databases', async () => {
97-
const outsideDatabase = join(home, 'outside-login-data')
98-
await writeFile(outsideDatabase, '')
99-
100-
const userDataDir = userDataDirFor(CHROME, home)
101-
for (const directory of ['Default', 'Profile 2', 'Profile 3']) {
102-
await mkdir(join(userDataDir, directory), { recursive: true })
100+
it.skipIf(NO_SYMLINKS)(
101+
'refuses symlinked, hard-linked, and non-file password databases',
102+
async () => {
103+
const outsideDatabase = join(home, 'outside-login-data')
104+
await writeFile(outsideDatabase, '')
105+
106+
const userDataDir = userDataDirFor(CHROME, home)
107+
for (const directory of ['Default', 'Profile 2', 'Profile 3']) {
108+
await mkdir(join(userDataDir, directory), { recursive: true })
109+
}
110+
await symlink(outsideDatabase, join(userDataDir, 'Default', 'Login Data For Account'))
111+
await link(outsideDatabase, join(userDataDir, 'Profile 2', 'Login Data For Account'))
112+
await mkdir(join(userDataDir, 'Profile 3', 'Login Data For Account'))
113+
114+
await expect(listBrowserProfiles(CHROME, home)).resolves.toEqual([])
103115
}
104-
await symlink(outsideDatabase, join(userDataDir, 'Default', 'Login Data For Account'))
105-
await link(outsideDatabase, join(userDataDir, 'Profile 2', 'Login Data For Account'))
106-
await mkdir(join(userDataDir, 'Profile 3', 'Login Data For Account'))
107-
108-
await expect(listBrowserProfiles(CHROME, home)).resolves.toEqual([])
109-
})
116+
)
110117
})
111118

112119
describe('listAllBrowserProfiles', () => {

‎apps/desktop/src/main/desktop-chat-session-store.test.ts‎

Lines changed: 21 additions & 17 deletions
Original file line numberDiff line numberDiff line change
@@ -100,23 +100,27 @@ describe('DesktopChatSessionStore', () => {
100100
expect(restarted.getTerminal(ORIGIN, 'chat-a')).toEqual(TERMINAL)
101101
})
102102

103-
it('encrypts the complete descriptor payload and writes it owner-only', () => {
104-
const provider = encryption()
105-
const store = open(provider)
106-
store.setBrowser(ORIGIN, 'chat-secret', BROWSER)
107-
store.setTerminal(ORIGIN, 'chat-secret', TERMINAL)
108-
109-
expect(store.flush()).toBe(true)
110-
111-
const onDisk = readFileSync(filePath, 'utf8')
112-
expect(onDisk).not.toContain('chat-secret')
113-
expect(onDisk).not.toContain('example.com')
114-
expect(onDisk).not.toContain('/Users/ada/code')
115-
expect(onDisk).not.toContain('report.csv')
116-
expect(JSON.parse(onDisk)).toEqual({ v: 1, ciphertext: expect.any(String) })
117-
expect(provider.encryptString).toHaveBeenCalledOnce()
118-
expect(statSync(filePath).mode & 0o077).toBe(0)
119-
})
103+
// Windows has no POSIX mode bits to assert on.
104+
it.skipIf(process.platform === 'win32')(
105+
'encrypts the complete descriptor payload and writes it owner-only',
106+
() => {
107+
const provider = encryption()
108+
const store = open(provider)
109+
store.setBrowser(ORIGIN, 'chat-secret', BROWSER)
110+
store.setTerminal(ORIGIN, 'chat-secret', TERMINAL)
111+
112+
expect(store.flush()).toBe(true)
113+
114+
const onDisk = readFileSync(filePath, 'utf8')
115+
expect(onDisk).not.toContain('chat-secret')
116+
expect(onDisk).not.toContain('example.com')
117+
expect(onDisk).not.toContain('/Users/ada/code')
118+
expect(onDisk).not.toContain('report.csv')
119+
expect(JSON.parse(onDisk)).toEqual({ v: 1, ciphertext: expect.any(String) })
120+
expect(provider.encryptString).toHaveBeenCalledOnce()
121+
expect(statSync(filePath).mode & 0o077).toBe(0)
122+
}
123+
)
120124

121125
it('does not replace the durable store with an oversized encrypted envelope', () => {
122126
const provider = encryption()

‎apps/desktop/src/main/downloads.test.ts‎

Lines changed: 3 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -16,7 +16,8 @@ describe('sanitizeFilename', () => {
1616
})
1717

1818
describe('uniqueDownloadPath', () => {
19-
it('treats a dangling symlink as occupied', async () => {
19+
// Creating a symlink on Windows needs Developer Mode or elevation.
20+
it.skipIf(process.platform === 'win32')('treats a dangling symlink as occupied', async () => {
2021
const directory = mkdtempSync(join(tmpdir(), 'sim-download-path-'))
2122
symlinkSync(join(directory, 'missing-target'), join(directory, 'report.csv'))
2223

@@ -45,7 +46,7 @@ describe('uniqueDownloadPath', () => {
4546
])
4647

4748
expect(new Set([first, second])).toEqual(
48-
new Set(['/Downloads/report.csv', '/Downloads/report (copy-1).csv'])
49+
new Set([join('/Downloads', 'report.csv'), join('/Downloads', 'report (copy-1).csv')])
4950
)
5051
})
5152
})

‎apps/desktop/src/main/ipc.test.ts‎

Lines changed: 4 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -371,8 +371,10 @@ describe('registerIpcHandlers', () => {
371371

372372
expect(await handler?.(evilEvent)).toBe(false)
373373
expect(await handler?.(appEvent)).toBe(false)
374-
expect(await handler?.(activeAppEvent)).toBe(process.platform === 'darwin')
375-
expect(shell.openExternal).toHaveBeenCalledTimes(process.platform === 'darwin' ? 1 : 0)
374+
// Only macOS and Windows have a settings URL to open.
375+
const hasSettingsPane = process.platform === 'darwin' || process.platform === 'win32'
376+
expect(await handler?.(activeAppEvent)).toBe(hasSettingsPane)
377+
expect(shell.openExternal).toHaveBeenCalledTimes(hasSettingsPane ? 1 : 0)
376378
})
377379

378380
it('restricts the OAuth connect handoff to an activated app origin', async () => {

‎apps/desktop/src/main/local-files.test.ts‎

Lines changed: 57 additions & 47 deletions
Original file line numberDiff line numberDiff line change
@@ -12,21 +12,28 @@ afterEach(async () => {
1212
await rm(root, { recursive: true, force: true })
1313
})
1414

15-
it('rejects missing paths and directory cycles with explicit errors before uploading', async () => {
16-
expect(
17-
await executeLocalFileRequest(
18-
{ operation: 'read' },
19-
{ toolName: 'read_local_file', args: { path: join(root, 'missing') } }
20-
)
21-
).toMatchObject({ ok: false })
22-
await symlink(root, join(root, 'cycle'))
23-
expect(
24-
await executeLocalFileRequest(
25-
{ operation: 'manifest' },
26-
{ toolName: 'import_local_files', args: { path: root, targetWorkspaceId: 'target' } }
27-
)
28-
).toMatchObject({ ok: false, error: expect.stringContaining('cycle') })
29-
})
15+
// Creating a symlink on Windows needs Developer Mode or elevation, which the
16+
// test runner cannot assume.
17+
const NO_SYMLINKS = process.platform === 'win32'
18+
19+
it.skipIf(NO_SYMLINKS)(
20+
'rejects missing paths and directory cycles with explicit errors before uploading',
21+
async () => {
22+
expect(
23+
await executeLocalFileRequest(
24+
{ operation: 'read' },
25+
{ toolName: 'read_local_file', args: { path: join(root, 'missing') } }
26+
)
27+
).toMatchObject({ ok: false })
28+
await symlink(root, join(root, 'cycle'))
29+
expect(
30+
await executeLocalFileRequest(
31+
{ operation: 'manifest' },
32+
{ toolName: 'import_local_files', args: { path: root, targetWorkspaceId: 'target' } }
33+
)
34+
).toMatchObject({ ok: false, error: expect.stringContaining('cycle') })
35+
}
36+
)
3037

3138
it('refuses oversized import files before any workspace mutation or bulk allocation', async () => {
3239
const path = join(root, 'large.bin')
@@ -40,7 +47,7 @@ it('refuses oversized import files before any workspace mutation or bulk allocat
4047
).toMatchObject({ ok: false, error: expect.stringContaining('64 MB') })
4148
})
4249

43-
it.each(['file', 'directory'] as const)(
50+
it.skipIf(NO_SYMLINKS).each(['file', 'directory'] as const)(
4451
'rejects %s symlinks outside the import source during manifest and chunk reads',
4552
async (kind) => {
4653
const source = join(root, 'selected')
@@ -79,38 +86,41 @@ it.each(['file', 'directory'] as const)(
7986
}
8087
)
8188

82-
it('supports internal symlinks and an explicitly selected symlink root, but rejects a retargeted child', async () => {
83-
const source = join(root, 'selected')
84-
await mkdir(source)
85-
await writeFile(join(source, 'notes.txt'), 'inside')
86-
await symlink(join(source, 'notes.txt'), join(source, 'alias.txt'))
87-
const selectedAlias = join(root, 'selected-alias')
88-
await symlink(source, selectedAlias)
89-
const authorization = {
90-
toolName: 'import_local_files',
91-
args: { path: selectedAlias, targetWorkspaceId: 'target' },
92-
}
93-
const manifest = await executeLocalFileRequest({ operation: 'manifest' }, authorization)
94-
if (!manifest.ok || manifest.data.kind !== 'manifest') throw new Error('Expected manifest')
95-
const entry = manifest.data.entries.find((item) => item.relativePath === 'alias.txt')!
96-
const request = {
97-
operation: 'chunk',
98-
relativePath: 'alias.txt',
99-
revision: entry.revision,
100-
offset: 0,
89+
it.skipIf(NO_SYMLINKS)(
90+
'supports internal symlinks and an explicitly selected symlink root, but rejects a retargeted child',
91+
async () => {
92+
const source = join(root, 'selected')
93+
await mkdir(source)
94+
await writeFile(join(source, 'notes.txt'), 'inside')
95+
await symlink(join(source, 'notes.txt'), join(source, 'alias.txt'))
96+
const selectedAlias = join(root, 'selected-alias')
97+
await symlink(source, selectedAlias)
98+
const authorization = {
99+
toolName: 'import_local_files',
100+
args: { path: selectedAlias, targetWorkspaceId: 'target' },
101+
}
102+
const manifest = await executeLocalFileRequest({ operation: 'manifest' }, authorization)
103+
if (!manifest.ok || manifest.data.kind !== 'manifest') throw new Error('Expected manifest')
104+
const entry = manifest.data.entries.find((item) => item.relativePath === 'alias.txt')!
105+
const request = {
106+
operation: 'chunk',
107+
relativePath: 'alias.txt',
108+
revision: entry.revision,
109+
offset: 0,
110+
}
111+
expect(await executeLocalFileRequest(request, authorization)).toEqual({
112+
ok: true,
113+
data: { kind: 'chunk', bytes: new Uint8Array(Buffer.from('inside')), eof: true },
114+
})
115+
await writeFile(join(root, 'outside.txt'), 'outside')
116+
await rm(join(source, 'alias.txt'))
117+
await symlink(join(root, 'outside.txt'), join(source, 'alias.txt'))
118+
expect(await executeLocalFileRequest(request, authorization)).toMatchObject({
119+
ok: false,
120+
error: expect.stringContaining('outside this import source'),
121+
})
101122
}
102-
expect(await executeLocalFileRequest(request, authorization)).toEqual({
103-
ok: true,
104-
data: { kind: 'chunk', bytes: new Uint8Array(Buffer.from('inside')), eof: true },
105-
})
106-
await writeFile(join(root, 'outside.txt'), 'outside')
107-
await rm(join(source, 'alias.txt'))
108-
await symlink(join(root, 'outside.txt'), join(source, 'alias.txt'))
109-
expect(await executeLocalFileRequest(request, authorization)).toMatchObject({
110-
ok: false,
111-
error: expect.stringContaining('outside this import source'),
112-
})
113-
})
123+
)
114124

115125
it('preserves a UTF-8 BOM and rejects split offsets and limits that cannot fit a character', async () => {
116126
const path = join(root, 'unicode.txt')

0 commit comments

Comments
 (0)