mirror of
https://github.com/stablyai/orca.git
synced 2026-10-19 16:03:26 +00:00
* chore(lint): upgrade oxlint to 1.71 and enable 7 new rules Upgrade oxlint 1.67.0 -> 1.71.0 (1.72 was blocked by the repo's 3-day minimum-release-age supply-chain guard; nothing here needs it). The bump is a no-op on the existing config. Enable 3 error rules (backlog autofixed to zero in this commit) and 4 warn rules (surface signal without gating CI): error (autofixed, behavior-preserving): - unicorn/prefer-node-protocol (~1531 sites: bare builtin -> node:) - typescript/no-import-type-side-effects (~36: all-inline-type -> import type) - unicorn/no-array-reverse (19: copy-then-reverse -> toReversed) warn (real signal, current fires are test-only/correct): - unicorn/no-array-fill-with-reference-type (aliasing footgun guard) - typescript/no-unsafe-function-type (bans bare Function type) - unicorn/prefer-array-flat-map (map().flat() -> flatMap()) - unicorn/prefer-regexp-test (.match() in bool ctx -> .test()) mobile/.oxlintrc.json extends root, so it inherits all 7; the autofix ran from root and covered mobile/ too. Verification (all green): oxlint 0 errors (root+mobile+aux configs), oxfmt clean, typecheck (node+cli+web), vitest 22795 passed / 0 failed, builds (electron-vite + web + cli) succeed. node: rewrites confirmed to skip embedded SSH/CLI string payloads (AST-only); all toReversed sites verified to operate on fresh copies or write-once locals. * chore(lint): bump mobile oxlint to 1.71 so inherited rules parse mobile/ is a standalone pnpm project pinning its own oxlint@1.67, which lacks unicorn/no-array-fill-with-reference-type (needs >=1.70). Since mobile/.oxlintrc.json extends the root config, mobile CI's 'cd mobile && oxlint' failed to parse the new rule. Bump mobile to match root (1.71). Verified in mobile/: oxlint 0 errors, oxfmt --check clean, tsc --noEmit pass, vitest 978 passed / 0 failed. Co-authored-by: Orca <help@stably.ai> --------- Co-authored-by: Orca <help@stably.ai>
215 lines
9.0 KiB
TypeScript
215 lines
9.0 KiB
TypeScript
import { describe, it, expect, beforeEach, afterEach, vi } from 'vitest'
|
|
import { existsSync, mkdirSync, mkdtempSync, readFileSync, rmSync, writeFileSync } from 'node:fs'
|
|
import { join } from 'node:path'
|
|
import { tmpdir } from 'node:os'
|
|
// Import from the production source of truth so a filename rename can't silently
|
|
// pass these tests against stale names.
|
|
import { DEVICE_REGISTRY_FILENAME, E2EE_KEYPAIR_FILENAME } from './mobile-pairing-files'
|
|
|
|
// Mutable userData the electron mock resolves. We flip it mid-test to simulate
|
|
// app.setName('Orca') changing how app.getPath('userData') resolves (e.g. from
|
|
// lowercase 'orca' to uppercase 'Orca' on a case-sensitive filesystem) — the
|
|
// divergence that drops paired devices. We use two genuinely distinct directory
|
|
// names rather than case variants so the assertion is deterministic regardless
|
|
// of whether the test host's filesystem is case-sensitive.
|
|
const appState = { userData: '' }
|
|
|
|
vi.mock('electron', () => ({
|
|
app: { getPath: () => appState.userData },
|
|
safeStorage: {
|
|
isEncryptionAvailable: () => false,
|
|
encryptString: (plaintext: string) => Buffer.from(plaintext, 'utf-8'),
|
|
decryptString: (ciphertext: Buffer) => ciphertext.toString('utf-8')
|
|
}
|
|
}))
|
|
|
|
describe('mobile pairing userData path stability', () => {
|
|
let root: string
|
|
// The path persistence captures early, before app.setName().
|
|
let canonicalDir: string
|
|
// The path app.getPath('userData') resolves to after app.setName() — a
|
|
// distinct directory standing in for the post-rename resolution.
|
|
let lateDir: string
|
|
|
|
beforeEach(() => {
|
|
root = mkdtempSync(join(tmpdir(), 'orca-pairing-path-'))
|
|
canonicalDir = join(root, 'userdata-early')
|
|
lateDir = join(root, 'userdata-late')
|
|
mkdirSync(canonicalDir, { recursive: true })
|
|
mkdirSync(lateDir, { recursive: true })
|
|
vi.resetModules()
|
|
})
|
|
|
|
afterEach(() => {
|
|
rmSync(root, { recursive: true, force: true })
|
|
vi.resetModules()
|
|
})
|
|
|
|
it('keeps returning the path captured before app.setName changes resolution', async () => {
|
|
appState.userData = canonicalDir
|
|
const { initDataPath, getCanonicalUserDataPath } = await import('../persistence')
|
|
initDataPath()
|
|
|
|
// app.setName('Orca') happens later in startup, changing late resolution.
|
|
appState.userData = lateDir
|
|
|
|
expect(getCanonicalUserDataPath()).toBe(canonicalDir)
|
|
const { app } = await import('electron')
|
|
expect(getCanonicalUserDataPath()).not.toBe(app.getPath('userData'))
|
|
})
|
|
|
|
it('writes DeviceRegistry + E2EE keypair under the canonical path, not the late one', async () => {
|
|
appState.userData = canonicalDir
|
|
const { initDataPath, getCanonicalUserDataPath } = await import('../persistence')
|
|
initDataPath()
|
|
|
|
appState.userData = lateDir // app.setName('Orca') has run by the time the runtime starts
|
|
|
|
const { DeviceRegistry } = await import('./device-registry')
|
|
const { loadOrCreateE2EEKeypair } = await import('./e2ee-keypair')
|
|
|
|
// Mirrors OrcaRuntimeRpcServer.start(): both read from the same userDataPath.
|
|
const registry = new DeviceRegistry(getCanonicalUserDataPath())
|
|
registry.addDevice('iPhone')
|
|
loadOrCreateE2EEKeypair(getCanonicalUserDataPath())
|
|
|
|
// Pairing credentials land beside orca-data.json so they survive restarts/updates.
|
|
expect(existsSync(join(canonicalDir, DEVICE_REGISTRY_FILENAME))).toBe(true)
|
|
expect(existsSync(join(canonicalDir, E2EE_KEYPAIR_FILENAME))).toBe(true)
|
|
// The bug being guarded: the late path would have captured these instead.
|
|
expect(existsSync(join(lateDir, DEVICE_REGISTRY_FILENAME))).toBe(false)
|
|
expect(existsSync(join(lateDir, E2EE_KEYPAIR_FILENAME))).toBe(false)
|
|
})
|
|
|
|
it('migrates existing mobile pairing files from the late path as an all-or-nothing pair', async () => {
|
|
appState.userData = canonicalDir
|
|
const {
|
|
initDataPath,
|
|
getCanonicalUserDataPath,
|
|
migrateMobilePairingDataToCanonicalUserDataPath
|
|
} = await import('../persistence')
|
|
initDataPath()
|
|
|
|
appState.userData = lateDir
|
|
const lateDevices = JSON.stringify([
|
|
{
|
|
deviceId: 'late-phone',
|
|
name: 'iPhone',
|
|
token: 'late-token',
|
|
scope: 'mobile',
|
|
pairedAt: 1,
|
|
lastSeenAt: 2
|
|
}
|
|
])
|
|
const lateKeypair = JSON.stringify({
|
|
v: 1,
|
|
publicKeyB64: Buffer.from(new Uint8Array(32).fill(1)).toString('base64'),
|
|
secretKeyB64: Buffer.from(new Uint8Array(32).fill(2)).toString('base64')
|
|
})
|
|
writeFileSync(join(lateDir, DEVICE_REGISTRY_FILENAME), lateDevices)
|
|
writeFileSync(join(lateDir, E2EE_KEYPAIR_FILENAME), lateKeypair)
|
|
|
|
migrateMobilePairingDataToCanonicalUserDataPath(appState.userData)
|
|
|
|
expect(readFileSync(join(canonicalDir, DEVICE_REGISTRY_FILENAME), 'utf-8')).toBe(lateDevices)
|
|
expect(readFileSync(join(canonicalDir, E2EE_KEYPAIR_FILENAME), 'utf-8')).toBe(lateKeypair)
|
|
|
|
const { DeviceRegistry } = await import('./device-registry')
|
|
const registry = new DeviceRegistry(getCanonicalUserDataPath())
|
|
expect(registry.getDevice('late-phone')?.token).toBe('late-token')
|
|
|
|
writeFileSync(join(lateDir, DEVICE_REGISTRY_FILENAME), JSON.stringify([]))
|
|
migrateMobilePairingDataToCanonicalUserDataPath(appState.userData)
|
|
expect(readFileSync(join(canonicalDir, DEVICE_REGISTRY_FILENAME), 'utf-8')).toBe(lateDevices)
|
|
})
|
|
|
|
it('skips legacy migration when only part of the canonical credential pair exists', async () => {
|
|
appState.userData = canonicalDir
|
|
const { initDataPath, migrateMobilePairingDataToCanonicalUserDataPath } =
|
|
await import('../persistence')
|
|
initDataPath()
|
|
|
|
appState.userData = lateDir
|
|
const lateDevices = JSON.stringify([
|
|
{
|
|
deviceId: 'late-phone',
|
|
name: 'iPhone',
|
|
token: 'late-token',
|
|
scope: 'mobile',
|
|
pairedAt: 1,
|
|
lastSeenAt: 2
|
|
}
|
|
])
|
|
const lateKeypair = JSON.stringify({
|
|
v: 1,
|
|
publicKeyB64: Buffer.from(new Uint8Array(32).fill(1)).toString('base64'),
|
|
secretKeyB64: Buffer.from(new Uint8Array(32).fill(2)).toString('base64')
|
|
})
|
|
const canonicalKeypair = JSON.stringify({
|
|
v: 1,
|
|
publicKeyB64: Buffer.from(new Uint8Array(32).fill(3)).toString('base64'),
|
|
secretKeyB64: Buffer.from(new Uint8Array(32).fill(4)).toString('base64')
|
|
})
|
|
writeFileSync(join(lateDir, DEVICE_REGISTRY_FILENAME), lateDevices)
|
|
writeFileSync(join(lateDir, E2EE_KEYPAIR_FILENAME), lateKeypair)
|
|
writeFileSync(join(canonicalDir, E2EE_KEYPAIR_FILENAME), canonicalKeypair)
|
|
|
|
migrateMobilePairingDataToCanonicalUserDataPath(appState.userData)
|
|
|
|
expect(existsSync(join(canonicalDir, DEVICE_REGISTRY_FILENAME))).toBe(false)
|
|
expect(readFileSync(join(canonicalDir, E2EE_KEYPAIR_FILENAME), 'utf-8')).toBe(canonicalKeypair)
|
|
})
|
|
|
|
it('no-ops when the source path equals the canonical path (no rename happened)', async () => {
|
|
// Case-insensitive filesystems (macOS/Windows) resolve both paths to the same
|
|
// dir, so migration must be a clean no-op rather than copy a file onto itself.
|
|
appState.userData = canonicalDir
|
|
const { initDataPath, migrateMobilePairingDataToCanonicalUserDataPath } =
|
|
await import('../persistence')
|
|
initDataPath()
|
|
|
|
const devices = JSON.stringify([
|
|
{ deviceId: 'phone', name: 'iPhone', token: 't', scope: 'mobile', pairedAt: 1, lastSeenAt: 2 }
|
|
])
|
|
writeFileSync(join(canonicalDir, DEVICE_REGISTRY_FILENAME), devices)
|
|
|
|
expect(() => migrateMobilePairingDataToCanonicalUserDataPath(canonicalDir)).not.toThrow()
|
|
expect(readFileSync(join(canonicalDir, DEVICE_REGISTRY_FILENAME), 'utf-8')).toBe(devices)
|
|
})
|
|
|
|
it('no-ops on a fresh install with no legacy pairing files to migrate', async () => {
|
|
appState.userData = canonicalDir
|
|
const { initDataPath, migrateMobilePairingDataToCanonicalUserDataPath } =
|
|
await import('../persistence')
|
|
initDataPath()
|
|
|
|
appState.userData = lateDir
|
|
expect(() => migrateMobilePairingDataToCanonicalUserDataPath(appState.userData)).not.toThrow()
|
|
expect(existsSync(join(canonicalDir, DEVICE_REGISTRY_FILENAME))).toBe(false)
|
|
expect(existsSync(join(canonicalDir, E2EE_KEYPAIR_FILENAME))).toBe(false)
|
|
})
|
|
|
|
it('a previously paired device is still found after a restart on the canonical path', async () => {
|
|
// First launch: pair a device while userData resolves to the canonical path.
|
|
appState.userData = canonicalDir
|
|
{
|
|
const { initDataPath, getCanonicalUserDataPath } = await import('../persistence')
|
|
initDataPath()
|
|
appState.userData = lateDir
|
|
const { DeviceRegistry } = await import('./device-registry')
|
|
new DeviceRegistry(getCanonicalUserDataPath()).addDevice('iPhone')
|
|
}
|
|
|
|
// Second launch (e.g. after an update): fresh module state, path captured again.
|
|
vi.resetModules()
|
|
appState.userData = canonicalDir
|
|
const { initDataPath, getCanonicalUserDataPath } = await import('../persistence')
|
|
initDataPath()
|
|
appState.userData = lateDir
|
|
const { DeviceRegistry } = await import('./device-registry')
|
|
const registry = new DeviceRegistry(getCanonicalUserDataPath())
|
|
|
|
expect(registry.listDevices().map((d) => d.name)).toContain('iPhone')
|
|
})
|
|
})
|