Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
14 changes: 10 additions & 4 deletions .github/workflows/build.yml
Original file line number Diff line number Diff line change
Expand Up @@ -22,13 +22,18 @@ env:
jobs:
build-windows:
if: github.event.inputs.platform == 'all' || contains(github.event.inputs.platform, 'windows')
runs-on: windows-latest
runs-on: ${{ matrix.runner }}
strategy:
matrix:
arch: [x64]
include:
- arch: x64
platform: win-x64
runner: windows-latest
unpacked: win-unpacked
- arch: arm64
platform: win-arm64
runner: windows-11-arm
unpacked: win-arm64-unpacked
steps:
- uses: actions/checkout@v6

Expand Down Expand Up @@ -58,11 +63,12 @@ jobs:
npm_config_arch: ${{ matrix.arch }}

- name: Report RTK install token source
if: matrix.arch == 'x64'
shell: bash
run: |
echo "RTK runtime install token source: ${RTK_INSTALL_GITHUB_TOKEN_SOURCE}"

- name: Install Node Runtime
- name: Install Windows runtimes
run: pnpm run installRuntime:win:${{ matrix.arch }}
env:
GITHUB_TOKEN: ${{ env.RTK_INSTALL_GITHUB_TOKEN }}
Expand All @@ -81,7 +87,7 @@ jobs:
- name: Verify bundled plugins
shell: bash
run: |
pnpm run plugin:verify -- --name feishu --platform win32 --arch ${{ matrix.arch }} --plugin-root dist/win-unpacked/resources/app.asar.unpacked/plugins
pnpm run plugin:verify -- --name feishu --platform win32 --arch ${{ matrix.arch }} --plugin-root dist/${{ matrix.unpacked }}/resources/app.asar.unpacked/plugins

- name: Upload artifacts
uses: actions/upload-artifact@v6
Expand Down
80 changes: 80 additions & 0 deletions .github/workflows/windows-arm64-e2e.yml
Original file line number Diff line number Diff line change
@@ -0,0 +1,80 @@
name: Windows ARM64 E2E

on:
workflow_dispatch:

env:
FORCE_JAVASCRIPT_ACTIONS_TO_NODE24: 'true'

jobs:
windows-arm64-e2e:
runs-on: windows-11-arm
timeout-minutes: 120
steps:
- uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd
with:
persist-credentials: false

- name: Setup Node.js
uses: actions/setup-node@48b55a011bda9f5d6aeb4c2d9c7362e8dae4041e
with:
node-version: '24.14.1'
package-manager-cache: false

- name: Setup pnpm
uses: pnpm/action-setup@0e279bb959325dab635dd2c09392533439d90093

- name: Install dependencies
run: pnpm install

- name: Configure pnpm workspace for Windows arm64
run: pnpm run install:sharp
env:
TARGET_OS: win32
TARGET_ARCH: arm64

- name: Install Windows arm64 dependencies
run: pnpm install
env:
npm_config_build_from_source: true
npm_config_platform: win32
npm_config_arch: arm64

- name: Install Windows arm64 runtimes
run: pnpm run installRuntime:win:arm64

- name: Build Windows arm64 package
run: |
pnpm run build
pnpm run plugin:bundle:clean
pnpm run plugin:bundle -- --name feishu --platform win32 --arch arm64
pnpm exec electron-builder --win --arm64 --publish=never
env:
VITE_GITHUB_CLIENT_ID: ${{ secrets.DC_GITHUB_CLIENT_ID }}
VITE_GITHUB_CLIENT_SECRET: ${{ secrets.DC_GITHUB_CLIENT_SECRET }}
VITE_GITHUB_REDIRECT_URI: ${{ secrets.DC_GITHUB_REDIRECT_URI }}
VITE_PROVIDER_DB_URL: ${{ secrets.CDN_PROVIDER_DB_URL }}

- name: Verify bundled plugins
shell: bash
run: |
pnpm run plugin:verify -- --name feishu --platform win32 --arch arm64 --plugin-root dist/win-arm64-unpacked/resources/app.asar.unpacked/plugins

- name: Run packaged E2E smoke tests
run: pnpm run e2e:smoke:ci
env:
DEEPCHAT_E2E_APP_MODE: packaged
DEEPCHAT_E2E_EXECUTABLE_PATH: ${{ github.workspace }}/dist/win-arm64-unpacked/DeepChat.exe

- name: Upload Windows arm64 artifacts
if: always()
uses: actions/upload-artifact@b7c566a772e6b6bfb58ed0dc250532a479d7789f
with:
name: deepchat-win-arm64-e2e-artifacts
if-no-files-found: warn
path: |
dist/*
!dist/win-unpacked
!dist/win-arm64-unpacked
test-results/e2e
playwright-report
28 changes: 28 additions & 0 deletions docs/features/windows-arm64-support/plan.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,28 @@
# Windows ARM64 Support Plan

## Architecture

- Validate a packaged/unpacked ARM64 build with a Windows ARM64 manual workflow running on GitHub's `windows-11-arm` runner and Playwright Electron smoke tests.
- Extend the manual build workflow's Windows matrix to produce `win-x64` and `win-arm64` artifacts while keeping the release workflow on Windows x64 only.
- Keep the Windows ARM64 runtime script explicit: install only verified native `uv`, `node`, and `ripgrep` artifacts.
- Provide a CI-specific E2E mode that runs only non-provider smoke specs against the runner profile.

## E2E Data Flow

1. The Playwright fixture launches DeepChat with the default Electron `userData` path for the current runner/user.
2. CI Playwright config matches only launch and settings-navigation smoke specs.
3. Chat, session persistence, and provider connectivity specs remain available for local/manual runs with configured providers.

## Runtime Behavior

- `installRuntime:win:arm64` calls `tiny-runtime-injector` directly for `uv`, `node`, and `ripgrep`.
- `ripgrep` is pinned to `15.1.0` for Windows ARM64 because the injector default `14.1.1` has no ARM64 Windows release asset.
- `rtk` is intentionally omitted until upstream ships a Windows ARM64 release asset; existing runtime consumers continue to detect missing bundled binaries and fall back to system/runtime-unavailable behavior.

## Validation

- Runtime fallback tests cover missing bundled runtime behavior.
- Existing RTK fallback coverage remains in place.
- Skill runtime tests cover the no-UV/no-system-Python auto-runtime failure path.
- The manual build workflow validates Windows x64 and Windows ARM64 artifact generation.
- The new manual workflow validates Windows ARM64 build, plugin bundle, app launch, route switching, and settings navigation.
28 changes: 28 additions & 0 deletions docs/features/windows-arm64-support/spec.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,28 @@
# Windows ARM64 Support Spec

## User Story

DeepChat maintainers need a reliable way to validate Windows ARM64 builds without owning Windows ARM64 hardware, so the project can ship a Windows ARM64 package only after it passes smoke coverage on a real ARM64 Windows runner.

## Acceptance Criteria

- A manual GitHub Actions workflow runs on `windows-11-arm` and builds the Windows ARM64 app.
- The workflow runs E2E smoke tests that do not require configured provider credentials.
- The E2E run uses the runner's default profile and validates launch, routing, and settings window behavior.
Comment thread
coderabbitai[bot] marked this conversation as resolved.
- The manual build workflow can produce both Windows x64 and Windows ARM64 artifacts.
- Windows ARM64 bundles only verified native runtimes: `uv`, `node`, and `ripgrep`.
- `rtk` is not bundled on Windows ARM64 until upstream provides a Windows ARM64 binary.
- Existing Windows x64, macOS, and Linux runtime install scripts remain strict.
- The Windows ARM64 workflow uploads build artifacts and E2E diagnostics.

## Non-Goals

- Enable Windows ARM64 in the release workflow only after the manual Windows ARM64 E2E workflow has passed.
- Not every optional runtime is bundled on Windows ARM64.
- Provider-backed chat requests must not run in this CI workflow.

## Constraints

- Keep CI smoke coverage provider-independent; provider-backed specs remain local/manual only.
- Keep local `pnpm run e2e:smoke` behavior compatible with existing manual smoke tests.
- Keep runtime fallback behavior aligned with existing `RuntimeHelper`, RTK, and skill runtime logic.
12 changes: 12 additions & 0 deletions docs/features/windows-arm64-support/tasks.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,12 @@
# Windows ARM64 Support Tasks

- [x] Add SDD spec, plan, and task tracking.
- [x] Verify Windows ARM64 runtime artifact availability.
- [x] Wire `installRuntime:win:arm64` to explicit `uv`, `node`, and `ripgrep` installation.
- [x] Add CI E2E support for non-provider smoke tests.
- [x] Keep E2E on the default runner profile.
- [x] Add packaged-app E2E launch mode.
- [x] Add Windows ARM64 manual GitHub Actions workflow.
- [x] Enable Windows ARM64 in the manual build workflow.
- [x] Add targeted unit coverage for runtime fallback paths.
- [ ] Enable Windows ARM64 in the release workflow after the manual workflow passes on GitHub.
3 changes: 2 additions & 1 deletion package.json
Original file line number Diff line number Diff line change
Expand Up @@ -20,6 +20,7 @@
"test:watch": "vitest --watch",
"test:ui": "vitest --ui",
"e2e:smoke": "playwright test -c test/e2e/playwright.config.ts",
"e2e:smoke:ci": "playwright test -c test/e2e/playwright.ci.config.ts",
"format:check": "oxfmt --check .",
"format": "oxfmt .",
"lint": "pnpm run lint:agent-cleanup && pnpm run lint:architecture && oxlint .",
Expand Down Expand Up @@ -59,7 +60,7 @@
"afterSign": "scripts/notarize.js",
"installRuntime": "npx -y tiny-runtime-injector --type uv --dir ./runtime/uv --runtime-version 0.9.18 && npx -y tiny-runtime-injector --type node --dir ./runtime/node && npx -y tiny-runtime-injector --type ripgrep --dir ./runtime/ripgrep && npx -y tiny-runtime-injector --type rtk --dir ./runtime/rtk",
"installRuntime:win:x64": "npx -y tiny-runtime-injector --type uv --dir ./runtime/uv --runtime-version 0.9.18 -a x64 -p win32 && npx -y tiny-runtime-injector --type node --dir ./runtime/node -a x64 -p win32 && npx -y tiny-runtime-injector --type ripgrep --dir ./runtime/ripgrep -a x64 -p win32 && npx -y tiny-runtime-injector --type rtk --dir ./runtime/rtk -a x64 -p win32",
"installRuntime:win:arm64": "npx -y tiny-runtime-injector --type uv --dir ./runtime/uv --runtime-version 0.9.18 -a arm64 -p win32 && npx -y tiny-runtime-injector --type node --dir ./runtime/node -a arm64 -p win32 && npx -y tiny-runtime-injector --type ripgrep --dir ./runtime/ripgrep -a arm64 -p win32 && npx -y tiny-runtime-injector --type rtk --dir ./runtime/rtk -a arm64 -p win32",
"installRuntime:win:arm64": "npx -y tiny-runtime-injector --type uv --dir ./runtime/uv --runtime-version 0.9.18 -a arm64 -p win32 && npx -y tiny-runtime-injector --type node --dir ./runtime/node -a arm64 -p win32 && npx -y tiny-runtime-injector --type ripgrep --dir ./runtime/ripgrep --runtime-version 15.1.0 -a arm64 -p win32",
"installRuntime:mac:arm64": "npx -y tiny-runtime-injector --type uv --dir ./runtime/uv --runtime-version 0.9.18 -a arm64 -p darwin && npx -y tiny-runtime-injector --type node --dir ./runtime/node -a arm64 -p darwin && npx -y tiny-runtime-injector --type ripgrep --dir ./runtime/ripgrep -a arm64 -p darwin && npx -y tiny-runtime-injector --type rtk --dir ./runtime/rtk -a arm64 -p darwin",
"installRuntime:mac:x64": "npx -y tiny-runtime-injector --type uv --dir ./runtime/uv --runtime-version 0.9.18 -a x64 -p darwin && npx -y tiny-runtime-injector --type node --dir ./runtime/node -a x64 -p darwin && npx -y tiny-runtime-injector --type ripgrep --dir ./runtime/ripgrep -a x64 -p darwin && npx -y tiny-runtime-injector --type rtk --dir ./runtime/rtk -a x64 -p darwin",
"installRuntime:linux:x64": "npx -y tiny-runtime-injector --type uv --dir ./runtime/uv --runtime-version 0.9.18 -a x64 -p linux && npx -y tiny-runtime-injector --type node --dir ./runtime/node -a x64 -p linux && npx -y tiny-runtime-injector --type ripgrep --dir ./runtime/ripgrep -a x64 -p linux && npx -y tiny-runtime-injector --type rtk --dir ./runtime/rtk -a x64 -p linux",
Expand Down
20 changes: 17 additions & 3 deletions test/e2e/README.md
Original file line number Diff line number Diff line change
@@ -1,9 +1,10 @@
# DeepChat E2E Smoke

This suite runs manual smoke regression against the real local desktop environment.
This suite runs manual smoke regression against the real local desktop environment by default.

It does not use mock providers, alternate `userData` directories, or E2E-only bootstrap state.
The tests run against the same local profile that the app normally uses.
The default `pnpm run e2e:smoke` command runs against the same local profile that the app normally
uses.

## Scope

Expand All @@ -21,7 +22,11 @@ The smoke suite currently targets the following real provider setup:
- Provider: `minimax`
- Model: `MiniMax-M2.7`

If you want to use a different provider or model, edit [testData.ts](./helpers/testData.ts).
If you want to use a different provider or model, set `DEEPCHAT_E2E_PROVIDER_ID` and
`DEEPCHAT_E2E_MODEL_ID`, or edit [testData.ts](./helpers/testData.ts).

The CI command runs only the launch and Settings navigation smoke specs. It does not send chat
requests and does not require provider credentials.

## Prerequisites

Expand All @@ -39,6 +44,13 @@ pnpm run build
pnpm run e2e:smoke
```

For CI-style validation without real credentials:

```bash
pnpm run build
pnpm run e2e:smoke:ci
```

Set `RUN_PROVIDER_INTEGRATION=true` before running `pnpm run e2e:smoke` if you also want the
live provider connectivity check in `05-settings-provider.smoke.spec.ts`.

Expand All @@ -55,3 +67,5 @@ The suite also attaches renderer console output and page errors to each test run
- Tests are additive only and avoid deleting existing user data.
- Settings checks use the real Settings window and the real provider configuration.
- The provider connectivity check is opt-in because it requires live credentials and network access.
- `pnpm run e2e:smoke:ci` uses the current profile and only runs non-provider smoke coverage; it is
intended for CI and Windows ARM64 validation.
51 changes: 48 additions & 3 deletions test/e2e/fixtures/electronApp.ts
Original file line number Diff line number Diff line change
Expand Up @@ -6,12 +6,20 @@ import {
type TestInfo
} from '@playwright/test'
import { existsSync } from 'node:fs'
import { arch } from 'node:os'
import { dirname, resolve } from 'node:path'
import { fileURLToPath } from 'node:url'

const FIXTURE_DIR = dirname(fileURLToPath(import.meta.url))
const REPO_ROOT = resolve(FIXTURE_DIR, '..', '..', '..')
const BUILT_MAIN_ENTRY = resolve(REPO_ROOT, 'out', 'main', 'index.js')
const BUILT_RENDERER_ENTRY = resolve(REPO_ROOT, 'out', 'renderer', 'index.html')
const WINDOWS_PACKAGED_EXECUTABLE = resolve(
REPO_ROOT,
'dist',
arch() === 'arm64' ? 'win-arm64-unpacked' : 'win-unpacked',
'DeepChat.exe'
)

const delay = (ms: number) => new Promise((resolve) => setTimeout(resolve, ms))

Expand Down Expand Up @@ -59,6 +67,20 @@ type ElectronFixtures = {
launchApp: () => Promise<ElectronAppInstance>
}

const resolvePackagedExecutable = (): string => {
if (process.env.DEEPCHAT_E2E_EXECUTABLE_PATH) {
return resolve(process.env.DEEPCHAT_E2E_EXECUTABLE_PATH)
}

if (process.platform === 'win32') {
return WINDOWS_PACKAGED_EXECUTABLE
}

throw new Error(
'DEEPCHAT_E2E_APP_MODE=packaged requires DEEPCHAT_E2E_EXECUTABLE_PATH on this platform.'
)
}

const attachDiagnostics = async (
testInfo: TestInfo,
consoleLogs: string[],
Expand All @@ -75,17 +97,31 @@ const attachDiagnostics = async (
})
}

const ensureBuiltAppExists = (): void => {
const ensureLaunchTargetExists = (): void => {
if (process.env.DEEPCHAT_E2E_APP_MODE === 'packaged') {
const executablePath = resolvePackagedExecutable()
if (!existsSync(executablePath)) {
throw new Error(`Packaged app executable not found at ${executablePath}.`)
}
return
}

if (!existsSync(BUILT_MAIN_ENTRY)) {
throw new Error(
`Built app entry not found at ${BUILT_MAIN_ENTRY}. Run "pnpm run build" before "pnpm run e2e:smoke".`
)
}

if (!existsSync(BUILT_RENDERER_ENTRY)) {
throw new Error(
`Built renderer entry not found at ${BUILT_RENDERER_ENTRY}. Run "pnpm run build" before "pnpm run e2e:smoke".`
)
}
}

export const test = base.extend<ElectronFixtures>({
launchApp: async ({}, use, testInfo) => {
ensureBuiltAppExists()
ensureLaunchTargetExists()

const consoleLogs: string[] = []
const pageErrors: string[] = []
Expand All @@ -112,10 +148,19 @@ export const test = base.extend<ElectronFixtures>({
const launchApp = async (): Promise<ElectronAppInstance> => {
launchCount += 1
const currentLaunch = launchCount
const packaged = process.env.DEEPCHAT_E2E_APP_MODE === 'packaged'

const electronApp = await electron.launch({
args: ['.'],
...(packaged
? {
executablePath: resolvePackagedExecutable(),
args: []
}
: {
args: ['.']
}),
cwd: REPO_ROOT,
env: process.env,
Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue | 🟠 Major | ⚡ Quick win

Restore isolated E2E user-data profile for launched app.

Line 160 forwards the host environment unchanged, and this fixture writes onboarding/provider/model config. In packaged smoke runs, that can persist into a real/default DeepChat profile and cause state leakage/flaky reruns.

Suggested fix
+      const launchEnv: NodeJS.ProcessEnv = { ...process.env }
+      if (process.env.CI === 'true' || shouldUseMockProvider()) {
+        launchEnv.DEEPCHAT_E2E = '1'
+        launchEnv.DEEPCHAT_E2E_USER_DATA_DIR =
+          launchEnv.DEEPCHAT_E2E_USER_DATA_DIR ??
+          resolve(testInfo.outputDir, `deepchat-e2e-profile-${currentLaunch}`)
+      }
+
       const electronApp = await electron.launch({
         ...(packaged
           ? {
               executablePath: resolvePackagedExecutable(),
               args: []
             }
           : {
               args: ['.']
             }),
         cwd: REPO_ROOT,
-        env: process.env,
+        env: launchEnv,
         timeout: 120_000
       })
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@test/e2e/fixtures/electronApp.ts` at line 160, The fixture currently forwards
the whole host environment via the env: process.env line which allows the app to
write into the developer's real profile; change the electronApp fixture to
create a temporary isolated user-data directory and set env to a shallow clone
of process.env with profile-related variables overridden (e.g., HOME and/or
USERPROFILE and XDG_CONFIG_HOME or the Electron-specific user-data-dir env/flag)
so the launched app uses that temp dir instead of the host profile; update the
code at the env: process.env site to use the cloned-and-overridden env object
and ensure the temp dir is created and cleaned up by the fixture.

timeout: 120_000
})

Expand Down
4 changes: 2 additions & 2 deletions test/e2e/helpers/testData.ts
Original file line number Diff line number Diff line change
@@ -1,5 +1,5 @@
export const E2E_TARGET_PROVIDER_ID = 'minimax'
export const E2E_TARGET_MODEL_ID = 'MiniMax-M2.7'
export const E2E_TARGET_PROVIDER_ID = process.env.DEEPCHAT_E2E_PROVIDER_ID ?? 'minimax'
export const E2E_TARGET_MODEL_ID = process.env.DEEPCHAT_E2E_MODEL_ID ?? 'MiniMax-M2.7'

export const createSmokeToken = (prefix: string): string =>
`${prefix}_${Date.now()}_${Math.random().toString(36).slice(2, 8)}`
Expand Down
9 changes: 9 additions & 0 deletions test/e2e/playwright.ci.config.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,9 @@
import { defineConfig } from '@playwright/test'
import baseConfig from './playwright.config'

export default defineConfig({
...baseConfig,
testMatch: ['01-launch.smoke.spec.ts', '04-settings-navigation.smoke.spec.ts'],
retries: process.env.CI ? 1 : 0,
reporter: [['list'], ['html', { open: 'never' }]]
})
21 changes: 21 additions & 0 deletions test/main/lib/runtimeHelper.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -41,4 +41,25 @@ describe('RuntimeHelper', () => {
'/mock/runtime/rtk/rtk.exe'
)
})

it('leaves runtime paths empty and PATH unchanged when bundled runtimes are missing', () => {
Object.defineProperty(process, 'platform', {
configurable: true,
value: 'win32'
})

vi.spyOn(fs, 'existsSync').mockReturnValue(false)

const helper = RuntimeHelper.getInstance()
helper.initializeRuntimes(true)

expect(helper.getNodeRuntimePath()).toBeNull()
expect(helper.getUvRuntimePath()).toBeNull()
expect(helper.getRipgrepRuntimePath()).toBeNull()
expect(helper.getRtkRuntimePath()).toBeNull()
expect(helper.getBundledRuntimeBinPaths()).toEqual([])
expect(helper.prependBundledRuntimeToEnv({ PATH: 'C:\\Windows\\System32' })).toEqual({
PATH: 'C:\\Windows\\System32'
})
})
})
Loading