From 955c5b73c5d97e041349c1480dd3ba5562243604 Mon Sep 17 00:00:00 2001 From: ethernet Date: Fri, 10 Jul 2026 12:04:39 -0400 Subject: [PATCH] test(desktop): move node tests to vitest as well --- apps/desktop/electron/backend-command.test.ts | 3 +- apps/desktop/electron/backend-env.test.ts | 3 +- apps/desktop/electron/backend-probes.test.ts | 3 +- apps/desktop/electron/backend-ready.test.ts | 3 +- .../electron/bootstrap-platform.test.ts | 3 +- .../desktop/electron/bootstrap-runner.test.ts | 3 +- .../electron/connection-config.test.ts | 3 +- apps/desktop/electron/dashboard-token.test.ts | 3 +- .../electron/desktop-uninstall.test.ts | 3 +- apps/desktop/electron/fs-read-dir.test.ts | 3 +- .../desktop/electron/gateway-ws-probe.test.ts | 3 +- apps/desktop/electron/git-review-ops.test.ts | 3 +- apps/desktop/electron/git-root.test.ts | 32 +- .../desktop/electron/git-worktree-ops.test.ts | 3 +- apps/desktop/electron/hardening.test.ts | 280 +++++++++--------- .../electron/link-title-window.test.ts | 3 +- .../electron/oauth-net-request.test.ts | 3 +- .../electron/profile-delete-routing.test.ts | 3 +- apps/desktop/electron/session-windows.test.ts | 3 +- .../electron/titlebar-overlay-width.test.ts | 3 +- apps/desktop/electron/update-count.test.ts | 3 +- apps/desktop/electron/update-marker.test.ts | 3 +- apps/desktop/electron/update-rebuild.test.ts | 3 +- apps/desktop/electron/update-relaunch.test.ts | 3 +- apps/desktop/electron/update-remote.test.ts | 3 +- .../electron/vscode-marketplace.test.ts | 3 +- apps/desktop/electron/window-state.test.ts | 21 +- .../electron/windows-child-options.test.ts | 3 +- .../electron/windows-child-process.test.ts | 2 +- .../electron/windows-hermes-path.test.ts | 2 +- .../desktop/electron/windows-user-env.test.ts | 3 +- apps/desktop/electron/workspace-cwd.test.ts | 3 +- .../electron/wsl-clipboard-image.test.ts | 3 +- apps/desktop/electron/wsl-path-bridge.test.ts | 2 +- apps/desktop/electron/zoom.test.ts | 2 +- apps/desktop/package.json | 7 +- apps/desktop/vitest.config.ts | 41 ++- 37 files changed, 267 insertions(+), 206 deletions(-) diff --git a/apps/desktop/electron/backend-command.test.ts b/apps/desktop/electron/backend-command.test.ts index cb37f248f665..2746ce78dafe 100644 --- a/apps/desktop/electron/backend-command.test.ts +++ b/apps/desktop/electron/backend-command.test.ts @@ -1,5 +1,6 @@ import assert from 'node:assert/strict' -import test from 'node:test' + +import { test } from 'vitest' import { dashboardFallbackArgs, serveBackendArgs, sourceDeclaresServe } from './backend-command' diff --git a/apps/desktop/electron/backend-env.test.ts b/apps/desktop/electron/backend-env.test.ts index 461697752623..e24a66ee396e 100644 --- a/apps/desktop/electron/backend-env.test.ts +++ b/apps/desktop/electron/backend-env.test.ts @@ -1,6 +1,7 @@ import assert from 'node:assert/strict' import path from 'node:path' -import test from 'node:test' + +import { test } from 'vitest' import { appendUniquePathEntries, diff --git a/apps/desktop/electron/backend-probes.test.ts b/apps/desktop/electron/backend-probes.test.ts index 1250d4a39836..43b39aafd862 100644 --- a/apps/desktop/electron/backend-probes.test.ts +++ b/apps/desktop/electron/backend-probes.test.ts @@ -9,7 +9,8 @@ import assert from 'node:assert/strict' import fs from 'node:fs' import os from 'node:os' import path from 'node:path' -import test from 'node:test' + +import { test } from 'vitest' import { canImportHermesCli, hermesRuntimeImportProbe, verifyHermesCli } from './backend-probes' diff --git a/apps/desktop/electron/backend-ready.test.ts b/apps/desktop/electron/backend-ready.test.ts index 40676f4c1eaa..eb090686241b 100644 --- a/apps/desktop/electron/backend-ready.test.ts +++ b/apps/desktop/electron/backend-ready.test.ts @@ -16,7 +16,8 @@ import { EventEmitter } from 'node:events' import fs from 'node:fs' import os from 'node:os' import path from 'node:path' -import test from 'node:test' + +import { test } from 'vitest' import { DEFAULT_PORT_ANNOUNCE_TIMEOUT_MS, diff --git a/apps/desktop/electron/bootstrap-platform.test.ts b/apps/desktop/electron/bootstrap-platform.test.ts index c4db7fd65b9c..9ba60314e65f 100644 --- a/apps/desktop/electron/bootstrap-platform.test.ts +++ b/apps/desktop/electron/bootstrap-platform.test.ts @@ -1,5 +1,6 @@ import assert from 'node:assert/strict' -import test from 'node:test' + +import { test } from 'vitest' import { bundledRuntimeImportCheck, diff --git a/apps/desktop/electron/bootstrap-runner.test.ts b/apps/desktop/electron/bootstrap-runner.test.ts index 7ba50c85e74f..eaffcac3ae08 100644 --- a/apps/desktop/electron/bootstrap-runner.test.ts +++ b/apps/desktop/electron/bootstrap-runner.test.ts @@ -2,7 +2,8 @@ import assert from 'node:assert/strict' import fs from 'node:fs' import os from 'node:os' import path from 'node:path' -import test from 'node:test' + +import { test } from 'vitest' import { buildPinArgs, diff --git a/apps/desktop/electron/connection-config.test.ts b/apps/desktop/electron/connection-config.test.ts index 518d49805df8..e28f6959dab2 100644 --- a/apps/desktop/electron/connection-config.test.ts +++ b/apps/desktop/electron/connection-config.test.ts @@ -11,7 +11,8 @@ */ import assert from 'node:assert/strict' -import test from 'node:test' + +import { test } from 'vitest' import { AT_COOKIE_VARIANTS, diff --git a/apps/desktop/electron/dashboard-token.test.ts b/apps/desktop/electron/dashboard-token.test.ts index d07a7e3b111e..e0a45eb36cc6 100644 --- a/apps/desktop/electron/dashboard-token.test.ts +++ b/apps/desktop/electron/dashboard-token.test.ts @@ -6,7 +6,8 @@ */ import assert from 'node:assert/strict' -import test from 'node:test' + +import { test } from 'vitest' import { adoptServedDashboardToken, diff --git a/apps/desktop/electron/desktop-uninstall.test.ts b/apps/desktop/electron/desktop-uninstall.test.ts index 258db6c0f3c8..6d296ee1db91 100644 --- a/apps/desktop/electron/desktop-uninstall.test.ts +++ b/apps/desktop/electron/desktop-uninstall.test.ts @@ -10,7 +10,8 @@ */ import assert from 'node:assert/strict' -import test from 'node:test' + +import { test } from 'vitest' import { buildPosixCleanupScript, diff --git a/apps/desktop/electron/fs-read-dir.test.ts b/apps/desktop/electron/fs-read-dir.test.ts index 67adc624671b..c77e60434ec1 100644 --- a/apps/desktop/electron/fs-read-dir.test.ts +++ b/apps/desktop/electron/fs-read-dir.test.ts @@ -2,9 +2,10 @@ import assert from 'node:assert/strict' import fs from 'node:fs' import os from 'node:os' import path from 'node:path' -import test from 'node:test' import { pathToFileURL } from 'node:url' +import { test } from 'vitest' + import { readDirForIpc } from './fs-read-dir' function mkTmpDir() { diff --git a/apps/desktop/electron/gateway-ws-probe.test.ts b/apps/desktop/electron/gateway-ws-probe.test.ts index f1e5ac941283..222d3c699c16 100644 --- a/apps/desktop/electron/gateway-ws-probe.test.ts +++ b/apps/desktop/electron/gateway-ws-probe.test.ts @@ -10,7 +10,8 @@ */ import assert from 'node:assert/strict' -import test from 'node:test' + +import { test } from 'vitest' import { probeGatewayWebSocket } from './gateway-ws-probe' diff --git a/apps/desktop/electron/git-review-ops.test.ts b/apps/desktop/electron/git-review-ops.test.ts index a88de4edf129..a06fcdace39f 100644 --- a/apps/desktop/electron/git-review-ops.test.ts +++ b/apps/desktop/electron/git-review-ops.test.ts @@ -1,5 +1,6 @@ import assert from 'node:assert/strict' -import test from 'node:test' + +import { test } from 'vitest' import { resolveRenamePath } from './git-review-ops' diff --git a/apps/desktop/electron/git-root.test.ts b/apps/desktop/electron/git-root.test.ts index cda56d51a49a..e4a1758c3d0c 100644 --- a/apps/desktop/electron/git-root.test.ts +++ b/apps/desktop/electron/git-root.test.ts @@ -2,9 +2,10 @@ import assert from 'node:assert/strict' import fs from 'node:fs' import os from 'node:os' import path from 'node:path' -import test from 'node:test' import { pathToFileURL } from 'node:url' +import { test } from 'vitest' + import { gitRootForIpc } from './git-root' function mkTmpDir() { @@ -19,20 +20,23 @@ test('gitRootForIpc returns null for invalid and device paths', async () => { assert.equal(await gitRootForIpc('file:///%E0%A4%A'), null) }) -test('gitRootForIpc resolves directories files missing descendants and file URLs', async t => { +test('gitRootForIpc resolves directories files missing descendants and file URLs', async () => { const root = mkTmpDir() - t.after(() => fs.rmSync(root, { recursive: true, force: true })) - const gitDir = path.join(root, '.git') - const srcDir = path.join(root, 'src') - const filePath = path.join(srcDir, 'index.ts') - fs.mkdirSync(gitDir) - fs.mkdirSync(srcDir) - fs.writeFileSync(filePath, 'export {}\n', 'utf8') + try { + const gitDir = path.join(root, '.git') + const srcDir = path.join(root, 'src') + const filePath = path.join(srcDir, 'index.ts') + fs.mkdirSync(gitDir) + fs.mkdirSync(srcDir) + fs.writeFileSync(filePath, 'export {}\n', 'utf8') - assert.equal(await gitRootForIpc(root), root) - assert.equal(await gitRootForIpc(srcDir), root) - assert.equal(await gitRootForIpc(filePath), root) - assert.equal(await gitRootForIpc(pathToFileURL(filePath).toString()), root) - assert.equal(await gitRootForIpc(path.join(srcDir, 'missing.ts')), root) + assert.equal(await gitRootForIpc(root), root) + assert.equal(await gitRootForIpc(srcDir), root) + assert.equal(await gitRootForIpc(filePath), root) + assert.equal(await gitRootForIpc(pathToFileURL(filePath).toString()), root) + assert.equal(await gitRootForIpc(path.join(srcDir, 'missing.ts')), root) + } finally { + fs.rmSync(root, { recursive: true, force: true }) + } }) diff --git a/apps/desktop/electron/git-worktree-ops.test.ts b/apps/desktop/electron/git-worktree-ops.test.ts index da32c0794d5e..a543ecd39f2f 100644 --- a/apps/desktop/electron/git-worktree-ops.test.ts +++ b/apps/desktop/electron/git-worktree-ops.test.ts @@ -3,7 +3,8 @@ import { execFileSync } from 'node:child_process' import fs from 'node:fs' import os from 'node:os' import path from 'node:path' -import test from 'node:test' + +import { test } from 'vitest' import { addWorktree, diff --git a/apps/desktop/electron/hardening.test.ts b/apps/desktop/electron/hardening.test.ts index 5d0dc5e21230..1a5852f720af 100644 --- a/apps/desktop/electron/hardening.test.ts +++ b/apps/desktop/electron/hardening.test.ts @@ -2,9 +2,10 @@ import assert from 'node:assert/strict' import fs from 'node:fs' import os from 'node:os' import path from 'node:path' -import test from 'node:test' import { pathToFileURL } from 'node:url' +import { test } from 'vitest' + import { DEFAULT_FETCH_TIMEOUT_MS, encryptDesktopSecret, @@ -122,171 +123,184 @@ test('resolveRequestedPathForIpc expands ~ to the home directory', () => { ) }) -test('resolveReadableFileForIpc validates existence type size and sensitivity', async t => { +test('resolveReadableFileForIpc validates existence type size and sensitivity', async () => { const tempDir = fs.mkdtempSync(path.join(os.tmpdir(), 'hermes-desktop-hardening-')) - t.after(() => fs.rmSync(tempDir, { recursive: true, force: true })) - const textPath = path.join(tempDir, 'notes.txt') - fs.writeFileSync(textPath, 'hello world', 'utf8') + try { + const textPath = path.join(tempDir, 'notes.txt') + fs.writeFileSync(textPath, 'hello world', 'utf8') - const fromRelative = await resolveReadableFileForIpc('notes.txt', { - baseDir: tempDir, - maxBytes: 256, - purpose: 'File preview' - }) - - assert.equal(fromRelative.resolvedPath, textPath) - assert.equal(fromRelative.stat.size, 11) - - const fromFileUrl = await resolveReadableFileForIpc(pathToFileURL(textPath).toString(), { - purpose: 'File preview' - }) - - assert.equal(fromFileUrl.resolvedPath, textPath) - - const spacedPath = path.join(tempDir, 'notes with spaces.txt') - fs.writeFileSync(spacedPath, 'space ok', 'utf8') - - const fromSpacedFileUrl = await resolveReadableFileForIpc(pathToFileURL(spacedPath).toString(), { - purpose: 'File preview' - }) - - assert.equal(fromSpacedFileUrl.resolvedPath, spacedPath) - - await assert.rejects( - resolveReadableFileForIpc('missing.txt', { + const fromRelative = await resolveReadableFileForIpc('notes.txt', { baseDir: tempDir, - purpose: 'Text preview' - }), - /file does not exist/ - ) - - const nestedDir = path.join(tempDir, 'directory') - fs.mkdirSync(nestedDir) - await assert.rejects( - resolveReadableFileForIpc(nestedDir, { - purpose: 'Text preview' - }), - /path points to a directory/ - ) - - const largePath = path.join(tempDir, 'large.txt') - fs.writeFileSync(largePath, 'x'.repeat(40), 'utf8') - await assert.rejects( - resolveReadableFileForIpc(largePath, { - maxBytes: 8, + maxBytes: 256, purpose: 'File preview' - }), - /file is too large/ - ) + }) - const envPath = path.join(tempDir, '.env') - fs.writeFileSync(envPath, 'SECRET_TOKEN=123', 'utf8') - await assert.rejects( - resolveReadableFileForIpc(envPath, { + assert.equal(fromRelative.resolvedPath, textPath) + assert.equal(fromRelative.stat.size, 11) + + const fromFileUrl = await resolveReadableFileForIpc(pathToFileURL(textPath).toString(), { purpose: 'File preview' - }), - /blocked for sensitive file/ - ) + }) - const envTemplatePath = path.join(tempDir, '.env.example') - fs.writeFileSync(envTemplatePath, 'EXAMPLE_TOKEN=value', 'utf8') + assert.equal(fromFileUrl.resolvedPath, textPath) - const envTemplate = await resolveReadableFileForIpc(envTemplatePath, { - purpose: 'File preview' - }) + const spacedPath = path.join(tempDir, 'notes with spaces.txt') + fs.writeFileSync(spacedPath, 'space ok', 'utf8') - assert.equal(envTemplate.resolvedPath, envTemplatePath) + const fromSpacedFileUrl = await resolveReadableFileForIpc(pathToFileURL(spacedPath).toString(), { + purpose: 'File preview' + }) + + assert.equal(fromSpacedFileUrl.resolvedPath, spacedPath) + + await assert.rejects( + resolveReadableFileForIpc('missing.txt', { + baseDir: tempDir, + purpose: 'Text preview' + }), + /file does not exist/ + ) + + const nestedDir = path.join(tempDir, 'directory') + fs.mkdirSync(nestedDir) + await assert.rejects( + resolveReadableFileForIpc(nestedDir, { + purpose: 'Text preview' + }), + /path points to a directory/ + ) + + const largePath = path.join(tempDir, 'large.txt') + fs.writeFileSync(largePath, 'x'.repeat(40), 'utf8') + await assert.rejects( + resolveReadableFileForIpc(largePath, { + maxBytes: 8, + purpose: 'File preview' + }), + /file is too large/ + ) + + const envPath = path.join(tempDir, '.env') + fs.writeFileSync(envPath, 'SECRET_TOKEN=123', 'utf8') + await assert.rejects( + resolveReadableFileForIpc(envPath, { + purpose: 'File preview' + }), + /blocked for sensitive file/ + ) + + const envTemplatePath = path.join(tempDir, '.env.example') + fs.writeFileSync(envTemplatePath, 'EXAMPLE_TOKEN=value', 'utf8') + + const envTemplate = await resolveReadableFileForIpc(envTemplatePath, { + purpose: 'File preview' + }) + + assert.equal(envTemplate.resolvedPath, envTemplatePath) + } finally { + fs.rmSync(tempDir, { recursive: true, force: true }) + } }) -test('resolveReadableFileForIpc blocks common sensitive files', async t => { +test('resolveReadableFileForIpc blocks common sensitive files', async () => { const tempDir = fs.mkdtempSync(path.join(os.tmpdir(), 'hermes-desktop-sensitive-')) - t.after(() => fs.rmSync(tempDir, { recursive: true, force: true })) - const sshDir = path.join(tempDir, '.ssh') - fs.mkdirSync(sshDir) + try { + const sshDir = path.join(tempDir, '.ssh') + fs.mkdirSync(sshDir) - const blockedFiles = [ - path.join(tempDir, '.env'), - path.join(tempDir, '.npmrc'), - path.join(sshDir, 'id_ed25519'), - path.join(tempDir, 'cert.pem'), - path.join(tempDir, 'cert.p12'), - path.join(tempDir, 'cert.pfx') - ] + const blockedFiles = [ + path.join(tempDir, '.env'), + path.join(tempDir, '.npmrc'), + path.join(sshDir, 'id_ed25519'), + path.join(tempDir, 'cert.pem'), + path.join(tempDir, 'cert.p12'), + path.join(tempDir, 'cert.pfx') + ] - for (const filePath of blockedFiles) { - fs.writeFileSync(filePath, 'secret', 'utf8') - await rejectsWithCode(resolveReadableFileForIpc(filePath, { purpose: 'File preview' }), 'sensitive-file') + for (const filePath of blockedFiles) { + fs.writeFileSync(filePath, 'secret', 'utf8') + await rejectsWithCode(resolveReadableFileForIpc(filePath, { purpose: 'File preview' }), 'sensitive-file') + } + + const allowed = path.join(tempDir, '.env.example') + fs.writeFileSync(allowed, 'EXAMPLE_TOKEN=value', 'utf8') + assert.equal((await resolveReadableFileForIpc(allowed, { purpose: 'File preview' })).resolvedPath, allowed) + } finally { + fs.rmSync(tempDir, { recursive: true, force: true }) } - - const allowed = path.join(tempDir, '.env.example') - fs.writeFileSync(allowed, 'EXAMPLE_TOKEN=value', 'utf8') - assert.equal((await resolveReadableFileForIpc(allowed, { purpose: 'File preview' })).resolvedPath, allowed) }) -test('resolveReadableFileForIpc blocks symlinks whose realpath is sensitive', async t => { +test('resolveReadableFileForIpc blocks symlinks whose realpath is sensitive', async () => { const tempDir = fs.mkdtempSync(path.join(os.tmpdir(), 'hermes-desktop-realpath-')) - t.after(() => fs.rmSync(tempDir, { recursive: true, force: true })) - - const envPath = path.join(tempDir, '.env') - const linkPath = path.join(tempDir, 'safe-name.txt') - fs.writeFileSync(envPath, 'SECRET_TOKEN=123', 'utf8') try { - fs.symlinkSync(envPath, linkPath, 'file') - } catch (error) { - if (error?.code === 'EPERM' || error?.code === 'EACCES') { - t.skip(`symlink creation is not permitted on this platform (${error.code})`) + const envPath = path.join(tempDir, '.env') + const linkPath = path.join(tempDir, 'safe-name.txt') + fs.writeFileSync(envPath, 'SECRET_TOKEN=123', 'utf8') - return + try { + fs.symlinkSync(envPath, linkPath, 'file') + } catch (error) { + if (error?.code === 'EPERM' || error?.code === 'EACCES') { + // symlink creation is not permitted on this platform — skip + return + } + + throw error } - throw error + await rejectsWithCode(resolveReadableFileForIpc(linkPath, { purpose: 'File preview' }), 'sensitive-file') + } finally { + fs.rmSync(tempDir, { recursive: true, force: true }) } - - await rejectsWithCode(resolveReadableFileForIpc(linkPath, { purpose: 'File preview' }), 'sensitive-file') }) -test('resolveDirectoryForIpc accepts directories and rejects invalid directory targets', async t => { +test('resolveDirectoryForIpc accepts directories and rejects invalid directory targets', async () => { const tempDir = fs.mkdtempSync(path.join(os.tmpdir(), 'hermes-desktop-dir-')) - t.after(() => fs.rmSync(tempDir, { recursive: true, force: true })) - - const directory = path.join(tempDir, 'project') - const filePath = path.join(tempDir, 'file.txt') - fs.mkdirSync(directory) - fs.writeFileSync(filePath, 'not a directory', 'utf8') - - const resolved = await resolveDirectoryForIpc(directory) - assert.equal(resolved.resolvedPath, directory) - assert.equal(resolved.stat.isDirectory(), true) - - await rejectsWithCode(resolveDirectoryForIpc(filePath), 'ENOTDIR') - await rejectsWithCode(resolveDirectoryForIpc(path.join(tempDir, 'missing')), 'ENOENT') - await rejectsWithCode(resolveDirectoryForIpc('\\\\?\\C:\\secret'), 'device-path') -}) - -test('resolveDirectoryForIpc accepts directory symlinks or junctions', async t => { - const tempDir = fs.mkdtempSync(path.join(os.tmpdir(), 'hermes-desktop-dir-link-')) - t.after(() => fs.rmSync(tempDir, { recursive: true, force: true })) - - const directory = path.join(tempDir, 'actual-project') - const linkPath = path.join(tempDir, 'linked-project') - fs.mkdirSync(directory) try { - fs.symlinkSync(directory, linkPath, process.platform === 'win32' ? 'junction' : 'dir') - } catch (error) { - if (error?.code === 'EPERM' || error?.code === 'EACCES') { - t.skip(`directory symlink creation is not permitted on this platform (${error.code})`) + const directory = path.join(tempDir, 'project') + const filePath = path.join(tempDir, 'file.txt') + fs.mkdirSync(directory) + fs.writeFileSync(filePath, 'not a directory', 'utf8') - return + const resolved = await resolveDirectoryForIpc(directory) + assert.equal(resolved.resolvedPath, directory) + assert.equal(resolved.stat.isDirectory(), true) + + await rejectsWithCode(resolveDirectoryForIpc(filePath), 'ENOTDIR') + await rejectsWithCode(resolveDirectoryForIpc(path.join(tempDir, 'missing')), 'ENOENT') + await rejectsWithCode(resolveDirectoryForIpc('\\\\?\\C:\\secret'), 'device-path') + } finally { + fs.rmSync(tempDir, { recursive: true, force: true }) + } +}) + +test('resolveDirectoryForIpc accepts directory symlinks or junctions', async () => { + const tempDir = fs.mkdtempSync(path.join(os.tmpdir(), 'hermes-desktop-dir-link-')) + + try { + const directory = path.join(tempDir, 'actual-project') + const linkPath = path.join(tempDir, 'linked-project') + fs.mkdirSync(directory) + + try { + fs.symlinkSync(directory, linkPath, process.platform === 'win32' ? 'junction' : 'dir') + } catch (error) { + if (error?.code === 'EPERM' || error?.code === 'EACCES') { + // directory symlink creation is not permitted on this platform — skip + return + } + + throw error } - throw error + const resolved = await resolveDirectoryForIpc(linkPath) + assert.equal(resolved.resolvedPath, linkPath) + assert.equal(resolved.stat.isDirectory(), true) + } finally { + fs.rmSync(tempDir, { recursive: true, force: true }) } - - const resolved = await resolveDirectoryForIpc(linkPath) - assert.equal(resolved.resolvedPath, linkPath) - assert.equal(resolved.stat.isDirectory(), true) }) diff --git a/apps/desktop/electron/link-title-window.test.ts b/apps/desktop/electron/link-title-window.test.ts index d67b3eb94ca7..f81190f47dca 100644 --- a/apps/desktop/electron/link-title-window.test.ts +++ b/apps/desktop/electron/link-title-window.test.ts @@ -1,5 +1,6 @@ import assert from 'node:assert/strict' -import test from 'node:test' + +import { test } from 'vitest' import { createLinkTitleWindow, diff --git a/apps/desktop/electron/oauth-net-request.test.ts b/apps/desktop/electron/oauth-net-request.test.ts index 31d9e257d7e1..631119363f04 100644 --- a/apps/desktop/electron/oauth-net-request.test.ts +++ b/apps/desktop/electron/oauth-net-request.test.ts @@ -5,7 +5,8 @@ */ import assert from 'node:assert/strict' -import test from 'node:test' + +import { test } from 'vitest' import { serializeJsonBody, setJsonRequestHeaders } from './oauth-net-request' diff --git a/apps/desktop/electron/profile-delete-routing.test.ts b/apps/desktop/electron/profile-delete-routing.test.ts index d8386d65666c..6b1bb01da1e4 100644 --- a/apps/desktop/electron/profile-delete-routing.test.ts +++ b/apps/desktop/electron/profile-delete-routing.test.ts @@ -1,5 +1,6 @@ import assert from 'node:assert/strict' -import test from 'node:test' + +import { test } from 'vitest' import { decideProfileDeleteAction, profileNameFromDeleteRequest, resolveRouteProfile } from './profile-delete-routing' diff --git a/apps/desktop/electron/session-windows.test.ts b/apps/desktop/electron/session-windows.test.ts index cf65d39ac5d4..fcfca8680730 100644 --- a/apps/desktop/electron/session-windows.test.ts +++ b/apps/desktop/electron/session-windows.test.ts @@ -1,5 +1,6 @@ import assert from 'node:assert/strict' -import test from 'node:test' + +import { test } from 'vitest' import { buildSessionWindowUrl, chatWindowWebPreferences, createSessionWindowRegistry } from './session-windows' diff --git a/apps/desktop/electron/titlebar-overlay-width.test.ts b/apps/desktop/electron/titlebar-overlay-width.test.ts index 543a14cd56f7..0e424fd5c3e8 100644 --- a/apps/desktop/electron/titlebar-overlay-width.test.ts +++ b/apps/desktop/electron/titlebar-overlay-width.test.ts @@ -1,5 +1,6 @@ import assert from 'node:assert/strict' -import test from 'node:test' + +import { test } from 'vitest' import { MACOS_TAHOE_DARWIN_MAJOR, diff --git a/apps/desktop/electron/update-count.test.ts b/apps/desktop/electron/update-count.test.ts index 2c95fbe7adef..b7e1e89a6583 100644 --- a/apps/desktop/electron/update-count.test.ts +++ b/apps/desktop/electron/update-count.test.ts @@ -1,5 +1,6 @@ import assert from 'node:assert/strict' -import test from 'node:test' + +import { test } from 'vitest' import { resolveBehindCount, shouldCountCommits } from './update-count' diff --git a/apps/desktop/electron/update-marker.test.ts b/apps/desktop/electron/update-marker.test.ts index e6a1da96c18e..3da49396cc8d 100644 --- a/apps/desktop/electron/update-marker.test.ts +++ b/apps/desktop/electron/update-marker.test.ts @@ -14,10 +14,11 @@ import fs from 'fs' import assert from 'node:assert/strict' -import test from 'node:test' import os from 'os' import path from 'path' +import { test } from 'vitest' + import { isPidAlive, markerPath, diff --git a/apps/desktop/electron/update-rebuild.test.ts b/apps/desktop/electron/update-rebuild.test.ts index c30ccab9e261..6c2d75245500 100644 --- a/apps/desktop/electron/update-rebuild.test.ts +++ b/apps/desktop/electron/update-rebuild.test.ts @@ -13,7 +13,8 @@ */ import assert from 'node:assert/strict' -import test from 'node:test' + +import { test } from 'vitest' import { runRebuildWithRetry, shouldRetryRebuild } from './update-rebuild' diff --git a/apps/desktop/electron/update-relaunch.test.ts b/apps/desktop/electron/update-relaunch.test.ts index 13f2d1df0b68..54e42eabf9b2 100644 --- a/apps/desktop/electron/update-relaunch.test.ts +++ b/apps/desktop/electron/update-relaunch.test.ts @@ -22,7 +22,8 @@ import { execFileSync } from 'node:child_process' import fs from 'node:fs' import os from 'node:os' import path from 'node:path' -import test from 'node:test' + +import { test } from 'vitest' import { buildRelaunchScript, diff --git a/apps/desktop/electron/update-remote.test.ts b/apps/desktop/electron/update-remote.test.ts index 9244b363f298..80dc8b1dd0b5 100644 --- a/apps/desktop/electron/update-remote.test.ts +++ b/apps/desktop/electron/update-remote.test.ts @@ -16,7 +16,8 @@ */ import assert from 'node:assert/strict' -import test from 'node:test' + +import { test } from 'vitest' import { canonicalGitHubRemote, diff --git a/apps/desktop/electron/vscode-marketplace.test.ts b/apps/desktop/electron/vscode-marketplace.test.ts index 6c8b8e6b76e6..adbdfbe6f027 100644 --- a/apps/desktop/electron/vscode-marketplace.test.ts +++ b/apps/desktop/electron/vscode-marketplace.test.ts @@ -1,5 +1,6 @@ import assert from 'node:assert' -import test from 'node:test' + +import { test } from 'vitest' import { __testing, extractThemes, readCentralDirectory } from './vscode-marketplace' diff --git a/apps/desktop/electron/window-state.test.ts b/apps/desktop/electron/window-state.test.ts index 25a4fcaa1196..40c8fe1798e2 100644 --- a/apps/desktop/electron/window-state.test.ts +++ b/apps/desktop/electron/window-state.test.ts @@ -5,7 +5,8 @@ */ import assert from 'node:assert/strict' -import test from 'node:test' + +import { test, vi } from 'vitest' import { computeWindowOptions, @@ -118,8 +119,8 @@ test('computeWindowOptions does not clamp when displays are unknown', () => { // ─── debounce ────────────────────────────────────────────────────────────── -test('debounce coalesces a burst into one trailing run', t => { - t.mock.timers.enable({ apis: ['setTimeout'] }) +test('debounce coalesces a burst into one trailing run', () => { + vi.useFakeTimers() let calls = 0 const d = debounce(() => { @@ -130,14 +131,16 @@ test('debounce coalesces a burst into one trailing run', t => { d() d() assert.equal(calls, 0) - t.mock.timers.tick(249) + vi.advanceTimersByTime(249) assert.equal(calls, 0) - t.mock.timers.tick(1) + vi.advanceTimersByTime(1) assert.equal(calls, 1) + + vi.useRealTimers() }) -test('debounce.flush runs now and cancels the pending timer', t => { - t.mock.timers.enable({ apis: ['setTimeout'] }) +test('debounce.flush runs now and cancels the pending timer', () => { + vi.useFakeTimers() let calls = 0 const d = debounce(() => { @@ -147,6 +150,8 @@ test('debounce.flush runs now and cancels the pending timer', t => { d() d.flush() assert.equal(calls, 1) - t.mock.timers.tick(1000) + vi.advanceTimersByTime(1000) assert.equal(calls, 1) + + vi.useRealTimers() }) diff --git a/apps/desktop/electron/windows-child-options.test.ts b/apps/desktop/electron/windows-child-options.test.ts index 76d46fda26bb..33fcac357783 100644 --- a/apps/desktop/electron/windows-child-options.test.ts +++ b/apps/desktop/electron/windows-child-options.test.ts @@ -1,5 +1,6 @@ import assert from 'node:assert/strict' -import test from 'node:test' + +import { test } from 'vitest' import { stopBackendChild } from './backend-child' import { hiddenWindowsChildOptions } from './windows-child-options' diff --git a/apps/desktop/electron/windows-child-process.test.ts b/apps/desktop/electron/windows-child-process.test.ts index f1d72dedc97a..f81c60ce9dd0 100644 --- a/apps/desktop/electron/windows-child-process.test.ts +++ b/apps/desktop/electron/windows-child-process.test.ts @@ -1,7 +1,7 @@ import assert from 'node:assert/strict' import fs from 'node:fs' import path from 'node:path' -import test from 'node:test' +import { test } from 'vitest' import { fileURLToPath } from 'node:url' const ELECTRON_DIR = path.dirname(fileURLToPath(import.meta.url)) diff --git a/apps/desktop/electron/windows-hermes-path.test.ts b/apps/desktop/electron/windows-hermes-path.test.ts index a83af63115fb..71a673151d94 100644 --- a/apps/desktop/electron/windows-hermes-path.test.ts +++ b/apps/desktop/electron/windows-hermes-path.test.ts @@ -13,7 +13,7 @@ // re-selected forever instead of falling through to bootstrap. import assert from 'node:assert/strict' -import test from 'node:test' +import { test } from 'vitest' import { buildPathExtCandidates, chooseUpdaterArgs, resolveVenvHermesCommand } from './windows-hermes-path' diff --git a/apps/desktop/electron/windows-user-env.test.ts b/apps/desktop/electron/windows-user-env.test.ts index 17c9a4a7c7e1..6b92650b60a5 100644 --- a/apps/desktop/electron/windows-user-env.test.ts +++ b/apps/desktop/electron/windows-user-env.test.ts @@ -1,5 +1,6 @@ import assert from 'node:assert/strict' -import { test } from 'node:test' + +import { test } from 'vitest' import { expandWindowsEnvRefs, parseRegQueryValue, readWindowsUserEnvVar } from './windows-user-env' diff --git a/apps/desktop/electron/workspace-cwd.test.ts b/apps/desktop/electron/workspace-cwd.test.ts index 6737c3582821..1377b9f7d783 100644 --- a/apps/desktop/electron/workspace-cwd.test.ts +++ b/apps/desktop/electron/workspace-cwd.test.ts @@ -6,7 +6,8 @@ import assert from 'node:assert/strict' import path from 'node:path' -import test from 'node:test' + +import { test } from 'vitest' import { isPackagedInstallPath } from './workspace-cwd' diff --git a/apps/desktop/electron/wsl-clipboard-image.test.ts b/apps/desktop/electron/wsl-clipboard-image.test.ts index 244db40f2a91..311d11ca7dac 100644 --- a/apps/desktop/electron/wsl-clipboard-image.test.ts +++ b/apps/desktop/electron/wsl-clipboard-image.test.ts @@ -1,5 +1,6 @@ import assert from 'node:assert/strict' -import test from 'node:test' + +import { test } from 'vitest' import { decodeClipboardImageBase64, diff --git a/apps/desktop/electron/wsl-path-bridge.test.ts b/apps/desktop/electron/wsl-path-bridge.test.ts index e6d2bfa87e7e..79505a1d394e 100644 --- a/apps/desktop/electron/wsl-path-bridge.test.ts +++ b/apps/desktop/electron/wsl-path-bridge.test.ts @@ -1,5 +1,5 @@ import assert from 'node:assert/strict' -import test from 'node:test' +import { test } from 'vitest' import { parseDefaultDistro, resolvePickerDefaultPath, wslPosixToWindowsAccessible } from './wsl-path-bridge' diff --git a/apps/desktop/electron/zoom.test.ts b/apps/desktop/electron/zoom.test.ts index 8be69e813a39..9401ca53f51e 100644 --- a/apps/desktop/electron/zoom.test.ts +++ b/apps/desktop/electron/zoom.test.ts @@ -7,7 +7,7 @@ import assert from 'node:assert/strict' import fs from 'node:fs' import path from 'node:path' -import test from 'node:test' +import { test } from 'vitest' import { fileURLToPath } from 'node:url' import { diff --git a/apps/desktop/package.json b/apps/desktop/package.json index 518dd5bbe632..0a4fd7f4e9cc 100644 --- a/apps/desktop/package.json +++ b/apps/desktop/package.json @@ -38,15 +38,16 @@ "test:desktop:nsis": "node scripts/test-desktop.mjs nsis", "test:desktop:existing": "node scripts/test-desktop.mjs existing", "test:desktop:fresh": "node scripts/test-desktop.mjs fresh", - "test:desktop:platforms": "node --test electron/bootstrap-platform.test.ts electron/hardening.test.ts electron/backend-env.test.ts electron/backend-probes.test.ts electron/backend-ready.test.ts electron/bootstrap-runner.test.ts electron/connection-config.test.ts electron/dashboard-token.test.ts electron/gateway-ws-probe.test.ts electron/oauth-net-request.test.ts electron/desktop-uninstall.test.ts electron/session-windows.test.ts electron/profile-delete-routing.test.ts electron/link-title-window.test.ts electron/workspace-cwd.test.ts electron/fs-read-dir.test.ts electron/git-root.test.ts electron/git-worktree-ops.test.ts electron/windows-child-options.test.ts electron/update-remote.test.ts electron/update-count.test.ts electron/update-rebuild.test.ts electron/update-marker.test.ts electron/update-relaunch.test.ts electron/windows-user-env.test.ts electron/wsl-clipboard-image.test.ts electron/titlebar-overlay-width.test.ts electron/window-state.test.ts electron/zoom.test.ts electron/windows-hermes-path.test.ts", "typecheck": "tsc -p . --noEmit && tsc -p tsconfig.electron.json --noEmit", "lint": "eslint src/ electron/", "lint:fix": "eslint src/ electron/ --fix", "fmt": "prettier --write 'src/**/*.{ts,tsx}' 'electron/**/*.ts' 'vite.config.ts'", "fix": "npm run lint:fix && npm run fmt", - "test:ui": "vitest run", - "check": "npm run typecheck && npm run test:ui && npm run test:desktop:all && npm run build" + "test:ui": "vitest run --project ui", + "test:desktop:platforms": "vitest run --project electron", + "test": "vitest run", "preview": "node scripts/assert-root-install.mjs && vite preview --host 127.0.0.1 --port 4174", + "check": "npm run typecheck && npm run test && npm run test:desktop:all && npm run build" }, "dependencies": { "@assistant-ui/react": "^0.12.28", diff --git a/apps/desktop/vitest.config.ts b/apps/desktop/vitest.config.ts index 4bdc3793fa53..db5eae32356a 100644 --- a/apps/desktop/vitest.config.ts +++ b/apps/desktop/vitest.config.ts @@ -1,19 +1,28 @@ -import { defineConfig } from "vitest/config"; -import react from "@vitejs/plugin-react"; -import path from "path"; +import { defineConfig, TestProjectConfiguration } from 'vitest/config' +import react from '@vitejs/plugin-react' +import path from 'path' + +const reactUi: TestProjectConfiguration = { + extends: './vite.config.ts', + test: { + name: 'ui', + environment: 'jsdom', + setupFiles: ['./vitest.setup.ts'], + include: ['src/**/*.test.{ts,tsx}'], + globals: true + } +} + +const electronNative: TestProjectConfiguration = { + test: { + name: 'electron', + environment: 'node', + include: ['electron/**/*.test.ts'] + } +} export default defineConfig({ - plugins: [react()], - resolve: { - alias: { - "@": path.resolve(__dirname, "./src"), - }, - }, test: { - environment: "jsdom", - setupFiles: ["./vitest.setup.ts"], - include: ["src/**/*.test.{ts,tsx}"], - globals: true - }, - -}); + projects: [reactUi, electronNative] + } +})