mirror of
https://github.com/NanmiCoder/cc-haha
synced 2026-07-16 13:03:31 +08:00
Guard best-effort Electron window-state persistence so an unwritable data directory cannot crash the main process. Validate portable data directories before app-mode persistence and surface failures back to settings so restart is not attempted after rollback. Tested: cd desktop && bun test electron/services/windows.test.ts electron/services/appMode.test.ts Tested: cd desktop && bun run test src/stores/settingsStore.test.ts -t "settingsStore app mode" --run Tested: cd desktop && bun run check:electron Tested: bun run check:desktop Not-tested: bun run check:native; bun run verify; coverage gates not run for scoped local handoff. Confidence: high Scope-risk: narrow
143 lines
5.3 KiB
TypeScript
143 lines
5.3 KiB
TypeScript
import fs from 'node:fs'
|
|
import os from 'node:os'
|
|
import path from 'node:path'
|
|
import { afterEach, describe, expect, it } from 'vitest'
|
|
import {
|
|
applyStartupPortableMode,
|
|
defaultPortableDir,
|
|
detectPortableDir,
|
|
determineStartupPortableDir,
|
|
dirHasPortableData,
|
|
getAppMode,
|
|
setAppMode,
|
|
type AppModeAppLike,
|
|
} from './appMode'
|
|
|
|
const tempDirs: string[] = []
|
|
|
|
function tempDir() {
|
|
const dir = fs.mkdtempSync(path.join(os.tmpdir(), 'cc-haha-app-mode-'))
|
|
tempDirs.push(dir)
|
|
return dir
|
|
}
|
|
|
|
function app(root = tempDir()): AppModeAppLike & { root: string } {
|
|
const exe = path.join(root, 'Claude Code Haha.app', 'Contents', 'MacOS', 'Claude Code Haha')
|
|
const userData = path.join(root, 'user-data')
|
|
fs.mkdirSync(path.dirname(exe), { recursive: true })
|
|
fs.writeFileSync(exe, '')
|
|
return {
|
|
root,
|
|
getPath(name) {
|
|
return name === 'exe' ? exe : userData
|
|
},
|
|
}
|
|
}
|
|
|
|
afterEach(() => {
|
|
for (const dir of tempDirs.splice(0)) {
|
|
fs.rmSync(dir, { recursive: true, force: true })
|
|
}
|
|
})
|
|
|
|
describe('Electron app mode service', () => {
|
|
it('detects portable data using the same sentinel files and directories as Tauri', () => {
|
|
const root = tempDir()
|
|
expect(dirHasPortableData(root)).toBe(false)
|
|
fs.writeFileSync(path.join(root, 'settings.json'), '{}')
|
|
expect(dirHasPortableData(root)).toBe(true)
|
|
fs.rmSync(path.join(root, 'settings.json'))
|
|
fs.mkdirSync(path.join(root, 'projects'))
|
|
expect(dirHasPortableData(root)).toBe(true)
|
|
})
|
|
|
|
it('resolves startup portable mode from default portable data or app-mode config', () => {
|
|
const fakeApp = app()
|
|
const defaultDir = defaultPortableDir(fakeApp)
|
|
fs.mkdirSync(defaultDir, { recursive: true })
|
|
fs.writeFileSync(path.join(defaultDir, 'settings.json'), '{}')
|
|
|
|
expect(determineStartupPortableDir(fakeApp, {})).toBe(defaultDir)
|
|
expect(determineStartupPortableDir(fakeApp, { CLAUDE_CONFIG_DIR: '/external' })).toBeNull()
|
|
|
|
fs.writeFileSync(path.join(defaultDir, 'app-mode.json'), JSON.stringify({ mode: 'default' }))
|
|
expect(determineStartupPortableDir(fakeApp, {})).toBeNull()
|
|
})
|
|
|
|
it('sets portable environment variables before sidecars start', () => {
|
|
const fakeApp = app()
|
|
const env: NodeJS.ProcessEnv = {}
|
|
const defaultDir = defaultPortableDir(fakeApp)
|
|
fs.mkdirSync(defaultDir, { recursive: true })
|
|
fs.writeFileSync(path.join(defaultDir, 'settings.json'), '{}')
|
|
|
|
expect(applyStartupPortableMode(fakeApp, env)).toBe(defaultDir)
|
|
expect(env.CLAUDE_CONFIG_DIR).toBe(defaultDir)
|
|
expect(env.CC_HAHA_APP_PORTABLE_DIR).toBe('1')
|
|
expect(env.WEBVIEW2_USER_DATA_FOLDER).toBe(path.join(defaultDir, 'EBWebView'))
|
|
})
|
|
|
|
it('returns the active app mode shape expected by settingsStore', () => {
|
|
const fakeApp = app()
|
|
|
|
expect(getAppMode(fakeApp, {})).toEqual({
|
|
mode: 'default',
|
|
portableDir: defaultPortableDir(fakeApp),
|
|
defaultPortableDir: defaultPortableDir(fakeApp),
|
|
activeConfigDir: fakeApp.getPath('userData'),
|
|
configDirSource: 'system',
|
|
})
|
|
expect(getAppMode(fakeApp, { CLAUDE_CONFIG_DIR: '/portable', CC_HAHA_APP_PORTABLE_DIR: '1' })).toMatchObject({
|
|
mode: 'portable',
|
|
portableDir: '/portable',
|
|
activeConfigDir: '/portable',
|
|
configDirSource: 'portable',
|
|
})
|
|
expect(getAppMode(fakeApp, { CLAUDE_CONFIG_DIR: '/external' })).toMatchObject({
|
|
configDirSource: 'environment',
|
|
})
|
|
})
|
|
|
|
it('writes app-mode.json to active, target portable, and system config dirs', () => {
|
|
const fakeApp = app()
|
|
const active = tempDir()
|
|
const selected = path.join(tempDir(), 'portable')
|
|
|
|
setAppMode(fakeApp, { mode: 'portable', portableDir: selected }, { CLAUDE_CONFIG_DIR: active })
|
|
|
|
const expected = { mode: 'portable', portable_dir: selected }
|
|
expect(JSON.parse(fs.readFileSync(path.join(active, 'app-mode.json'), 'utf8'))).toEqual(expected)
|
|
expect(JSON.parse(fs.readFileSync(path.join(selected, 'app-mode.json'), 'utf8'))).toEqual(expected)
|
|
expect(JSON.parse(fs.readFileSync(path.join(fakeApp.getPath('userData'), 'app-mode.json'), 'utf8'))).toEqual(expected)
|
|
|
|
setAppMode(fakeApp, { mode: 'default', portableDir: null }, { CLAUDE_CONFIG_DIR: active })
|
|
expect(JSON.parse(fs.readFileSync(path.join(active, 'app-mode.json'), 'utf8'))).toEqual({
|
|
mode: 'default',
|
|
portable_dir: null,
|
|
})
|
|
})
|
|
|
|
it('does not point existing config at a portable dir that cannot persist app-mode.json', () => {
|
|
const fakeApp = app()
|
|
const active = tempDir()
|
|
const selected = path.join(tempDir(), 'portable')
|
|
fs.mkdirSync(path.join(selected, 'app-mode.json'), { recursive: true })
|
|
|
|
expect(() => setAppMode(fakeApp, { mode: 'portable', portableDir: selected }, { CLAUDE_CONFIG_DIR: active }))
|
|
.toThrow()
|
|
expect(fs.existsSync(path.join(active, 'app-mode.json'))).toBe(false)
|
|
expect(fs.existsSync(path.join(fakeApp.getPath('userData'), 'app-mode.json'))).toBe(false)
|
|
})
|
|
|
|
it('reports whether the default portable dir already has data', () => {
|
|
const fakeApp = app()
|
|
expect(detectPortableDir(fakeApp)).toEqual({
|
|
defaultPortableDir: defaultPortableDir(fakeApp),
|
|
hasData: false,
|
|
})
|
|
fs.mkdirSync(defaultPortableDir(fakeApp), { recursive: true })
|
|
fs.writeFileSync(path.join(defaultPortableDir(fakeApp), '.mcp.json'), '{}')
|
|
expect(detectPortableDir(fakeApp).hasData).toBe(true)
|
|
})
|
|
})
|