541 lines
22 KiB
JavaScript
541 lines
22 KiB
JavaScript
import { strict as assert } from 'node:assert'
|
|
import { spawnSync } from 'node:child_process'
|
|
import { lstat, mkdir, mkdtemp, readFile, readdir, realpath, rm, writeFile } from 'node:fs/promises'
|
|
import { tmpdir } from 'node:os'
|
|
import { dirname, join, resolve } from 'node:path'
|
|
import { setTimeout as delay } from 'node:timers/promises'
|
|
import { fileURLToPath } from 'node:url'
|
|
import { _electron as electron } from 'playwright'
|
|
|
|
const scriptDir = dirname(fileURLToPath(import.meta.url))
|
|
const packageRoot = resolve(scriptDir, '..')
|
|
const repoRoot = resolve(packageRoot, '../..')
|
|
const SOURCE_IDENTITY = '# Synthetic imported identity\n'
|
|
const TARGET_IDENTITY = '# Synthetic previous Desktop identity\n'
|
|
const SOURCE_CHAT = 'synthetic imported chat survives whole-profile transfer'
|
|
// A replace import performs two independently bounded receipt-verifier CLI
|
|
// calls (60 seconds each) around the mutating import. Windows CI cold starts
|
|
// can legitimately approach both bounds, so the E2E timeout must cover the
|
|
// product's advertised "few minutes" operation without becoming unbounded.
|
|
const PROFILE_IMPORT_APPLY_TIMEOUT_MS = 180_000
|
|
|
|
async function waitFor(check, label, timeoutMs = 90_000) {
|
|
const startedAt = Date.now()
|
|
let lastError
|
|
while (Date.now() - startedAt < timeoutMs) {
|
|
try {
|
|
const value = await check()
|
|
if (value) return value
|
|
} catch (error) {
|
|
lastError = error
|
|
}
|
|
await delay(250)
|
|
}
|
|
throw new Error(`Timed out waiting for ${label}: ${lastError?.message || lastError || ''}`)
|
|
}
|
|
|
|
function runPython(source, args) {
|
|
const result = spawnSync('uv', ['run', 'python', '-c', source, ...args], {
|
|
cwd: repoRoot,
|
|
encoding: 'utf8',
|
|
env: { ...process.env, UV_CACHE_DIR: join(tmpdir(), 'opensquilla-profile-import-uv-cache') },
|
|
})
|
|
if (result.status !== 0) {
|
|
throw new Error(`Python fixture command failed: ${result.stderr || result.stdout}`)
|
|
}
|
|
return result.stdout.trim()
|
|
}
|
|
|
|
function seedProfile(home, identity, chat) {
|
|
runPython(`
|
|
import json, sqlite3, sys
|
|
from pathlib import Path
|
|
home = Path(sys.argv[1]).resolve()
|
|
identity = sys.argv[2]
|
|
chat = sys.argv[3]
|
|
workspace = home / "workspace"
|
|
state = home / "state"
|
|
workspace.mkdir(parents=True, exist_ok=True)
|
|
state.mkdir(parents=True, exist_ok=True)
|
|
for name, value in {
|
|
"IDENTITY.md": identity,
|
|
"USER.md": "# Synthetic user\\n",
|
|
"SOUL.md": "# Synthetic soul\\n",
|
|
"MEMORY.md": "# Synthetic memory\\n",
|
|
}.items():
|
|
(workspace / name).write_text(value, encoding="utf-8", newline="")
|
|
(home / "config.toml").write_text(
|
|
"workspace_dir = " + json.dumps(str(workspace)) + "\\n"
|
|
+ "state_dir = " + json.dumps(str(state)) + "\\n"
|
|
+ "[llm]\\nprovider = \\"ollama\\"\\nmodel = \\"synthetic-import-model\\"\\n"
|
|
+ "base_url = \\"http://127.0.0.1:11434/v1\\"\\napi_key_env = \\"\\"\\n",
|
|
encoding="utf-8",
|
|
newline="",
|
|
)
|
|
with sqlite3.connect(state / "sessions.db") as connection:
|
|
connection.execute("CREATE TABLE synthetic_import_chat (id TEXT PRIMARY KEY, body TEXT NOT NULL)")
|
|
connection.execute("INSERT INTO synthetic_import_chat VALUES (?, ?)", ("session-1", chat))
|
|
assert connection.execute("PRAGMA quick_check").fetchone() == ("ok",)
|
|
`, [home, identity, chat])
|
|
}
|
|
|
|
async function writeProviderProfileConfig(home, settings) {
|
|
const workspace = join(home, 'workspace')
|
|
const state = join(home, 'state')
|
|
const lines = [
|
|
`workspace_dir = ${JSON.stringify(workspace)}`,
|
|
`state_dir = ${JSON.stringify(state)}`,
|
|
`search_provider = ${JSON.stringify(settings.searchProvider || 'duckduckgo')}`,
|
|
`search_api_key_env = ${JSON.stringify(settings.searchApiKeyEnv || '')}`,
|
|
'',
|
|
'[llm]',
|
|
`provider = ${JSON.stringify(settings.provider)}`,
|
|
`model = ${JSON.stringify(settings.model)}`,
|
|
`base_url = ${JSON.stringify(settings.baseUrl)}`,
|
|
`api_key_env = ${JSON.stringify(settings.apiKeyEnv || '')}`,
|
|
'',
|
|
'[squilla_router]',
|
|
`enabled = ${settings.routerEnabled === true ? 'true' : 'false'}`,
|
|
'default_tier = "c2"',
|
|
'confidence_threshold = 0.77',
|
|
'',
|
|
'[squilla_router.tiers.c0]',
|
|
`provider = ${JSON.stringify(settings.provider)}`,
|
|
'model = "synthetic-source-tier-model"',
|
|
'',
|
|
'[llm_ensemble]',
|
|
'enabled = false',
|
|
'selection_mode = "static_openrouter_b5"',
|
|
'',
|
|
'[privacy]',
|
|
`disable_network_observability = ${settings.disableNetworkObservability ? 'true' : 'false'}`,
|
|
'',
|
|
'[control_ui]',
|
|
'enabled = true',
|
|
'base_path = "/control"',
|
|
'',
|
|
]
|
|
await writeFile(join(home, 'config.toml'), lines.join('\n'), 'utf8')
|
|
}
|
|
|
|
async function seedDesktopCredential(userData, settings) {
|
|
await mkdir(userData, { recursive: true })
|
|
const now = '2026-07-12T00:00:00.000Z'
|
|
const credential = {
|
|
provider: settings.provider,
|
|
model: settings.model,
|
|
baseUrl: settings.baseUrl,
|
|
apiKeyEnv: settings.apiKeyEnv || '',
|
|
encryptedApiKey: settings.apiKey
|
|
? Buffer.from(settings.apiKey, 'utf8').toString('base64')
|
|
: '',
|
|
encryption: 'plain',
|
|
configAuthority: 'generated',
|
|
importTransactionId: '',
|
|
createdAt: now,
|
|
updatedAt: now,
|
|
}
|
|
const raw = `${JSON.stringify(credential, null, 2)}\n`
|
|
await writeFile(join(userData, 'desktop-credential.json'), raw, { mode: 0o600 })
|
|
return raw
|
|
}
|
|
|
|
async function snapshotTree(root) {
|
|
const result = {}
|
|
async function visit(path, relative = '') {
|
|
const info = await lstat(path)
|
|
assert.equal(info.isSymbolicLink(), false, `fixture cannot contain symlinks: ${path}`)
|
|
if (info.isDirectory()) {
|
|
result[`${relative || '.'}/`] = { type: 'directory', mode: info.mode }
|
|
for (const name of (await readdir(path)).sort()) {
|
|
await visit(join(path, name), relative ? `${relative}/${name}` : name)
|
|
}
|
|
return
|
|
}
|
|
assert.equal(info.isFile(), true)
|
|
result[relative] = {
|
|
type: 'file',
|
|
mode: info.mode,
|
|
bytes: (await readFile(path)).toString('base64'),
|
|
}
|
|
}
|
|
await visit(root)
|
|
return result
|
|
}
|
|
|
|
function launchEnvironment(isolatedHome, port) {
|
|
const inherited = { ...process.env }
|
|
for (const name of Object.keys(inherited)) {
|
|
if (name === 'DISPLAY' || name === 'XAUTHORITY') continue
|
|
const upperName = name.toUpperCase()
|
|
if (
|
|
name.startsWith('OPENSQUILLA_')
|
|
|| ['HTTP_PROXY', 'HTTPS_PROXY', 'ALL_PROXY', 'NO_PROXY'].includes(upperName)
|
|
|| /(?:API[_-]?KEY|TOKEN|SECRET|PASSWORD|CREDENTIAL|AUTH)/i.test(name)
|
|
|| /^(?:AWS|AZURE|GOOGLE|ANTHROPIC|OPENAI|OPENROUTER|MINIMAX|DEEPSEEK|GROQ|MISTRAL|COHERE|GEMINI|OLLAMA|XAI|MOONSHOT|DASHSCOPE|SILICONFLOW|ZHIPU|BAIDU|VOLCENGINE|TENCENT|ALIYUN|HF|HUGGINGFACE)_/i.test(name)
|
|
) delete inherited[name]
|
|
}
|
|
return {
|
|
...inherited,
|
|
HOME: isolatedHome,
|
|
USERPROFILE: isolatedHome,
|
|
OPENSQUILLA_DESKTOP_REPO_ROOT: repoRoot,
|
|
OPENSQUILLA_DESKTOP_SECRET_STORAGE: 'plain',
|
|
OPENSQUILLA_USER_STATE_DIR: join(isolatedHome, 'user-state'),
|
|
OPENSQUILLA_TEST_PROFILE_LOCK_ROOT: '1',
|
|
OPENSQUILLA_DESKTOP_GATEWAY_PORT: String(port),
|
|
OPENSQUILLA_DESKTOP_DISABLE_AUTO_UPDATE: '1',
|
|
OPENSQUILLA_OPENROUTER_LIVE_PRICING: '0',
|
|
UV_CACHE_DIR: join(isolatedHome, '.uv-cache'),
|
|
HTTP_PROXY: 'http://127.0.0.1:1',
|
|
HTTPS_PROXY: 'http://127.0.0.1:1',
|
|
ALL_PROXY: 'http://127.0.0.1:1',
|
|
NO_PROXY: '127.0.0.1,localhost',
|
|
http_proxy: 'http://127.0.0.1:1',
|
|
https_proxy: 'http://127.0.0.1:1',
|
|
all_proxy: 'http://127.0.0.1:1',
|
|
no_proxy: '127.0.0.1,localhost',
|
|
LANG: 'en_US.UTF-8',
|
|
LC_ALL: 'en_US.UTF-8',
|
|
}
|
|
}
|
|
|
|
async function launchDesktop(userData, isolatedHome, port) {
|
|
return await electron.launch({
|
|
args: ['--use-mock-keychain', `--user-data-dir=${userData}`, packageRoot],
|
|
env: launchEnvironment(isolatedHome, port),
|
|
})
|
|
}
|
|
|
|
async function onboardingPage(app) {
|
|
return await waitFor(async () => {
|
|
for (const page of app.windows()) {
|
|
if (page.isClosed()) continue
|
|
await page.waitForLoadState('domcontentloaded', { timeout: 5_000 }).catch(() => {})
|
|
if (await page.locator('#setup-form').count().catch(() => 0)) return page
|
|
}
|
|
return null
|
|
}, 'Desktop onboarding')
|
|
}
|
|
|
|
async function recoveryPage(app) {
|
|
return await waitFor(async () => {
|
|
for (const page of app.windows()) {
|
|
if (page.isClosed()) continue
|
|
await page.waitForLoadState('domcontentloaded', { timeout: 5_000 }).catch(() => {})
|
|
if (await page.locator('#recoveryPanel.visible').count().catch(() => 0)) return page
|
|
}
|
|
return null
|
|
}, 'recovery profile confirmation page')
|
|
}
|
|
|
|
async function controlPage(app) {
|
|
return await waitFor(async () => {
|
|
for (const page of app.windows()) {
|
|
if (page.isClosed()) continue
|
|
await page.waitForLoadState('domcontentloaded', { timeout: 5_000 }).catch(() => {})
|
|
let pathname = ''
|
|
try { pathname = new URL(page.url()).pathname } catch { pathname = '' }
|
|
if (!['/control/chat', '/control/chat/new'].includes(pathname)) continue
|
|
if (await page.locator('.chat-textarea').count().catch(() => 0)) return page
|
|
}
|
|
return null
|
|
}, 'Desktop Control UI', 120_000)
|
|
}
|
|
|
|
const root = await realpath(await mkdtemp(join(tmpdir(), 'opensquilla-profile-import-e2e-')))
|
|
let app = null
|
|
try {
|
|
// A single detected CLI profile remains unselected, and skipping performs no import.
|
|
const skipHome = join(root, 'skip-home')
|
|
const skipSource = join(skipHome, '.opensquilla')
|
|
const skipDesktopSource = join(root, 'skip-desktop-source')
|
|
const skipUserData = join(root, 'skip-user-data')
|
|
seedProfile(skipSource, SOURCE_IDENTITY, SOURCE_CHAT)
|
|
seedProfile(skipDesktopSource, '# Synthetic alternate Desktop identity\n', 'alternate chat')
|
|
const skipSourceBefore = await snapshotTree(skipSource)
|
|
const skipDesktopSourceBefore = await snapshotTree(skipDesktopSource)
|
|
app = await launchDesktop(skipUserData, skipHome, 18921)
|
|
let page = await onboardingPage(app)
|
|
await page.locator('[data-screen="5"].active').waitFor({ state: 'visible' })
|
|
assert.equal(await page.locator('#migrationSource').inputValue(), '')
|
|
assert.equal(await page.locator('#migrationSource option').count(), 2)
|
|
assert.equal(await page.locator('#migrationPreview').isDisabled(), true)
|
|
await app.evaluate(({ dialog }, selectedPath) => {
|
|
dialog.showOpenDialog = async () => ({ canceled: false, filePaths: [selectedPath] })
|
|
}, skipDesktopSource)
|
|
await page.locator('#migrationSourceKind').selectOption('desktop-home')
|
|
await page.locator('#migrationBrowse').click()
|
|
await waitFor(async () => (
|
|
await page.locator('#migrationSource option').count() === 3
|
|
), 'second explicitly browsed candidate')
|
|
assert.equal(await page.locator('#migrationSource').inputValue(), skipDesktopSource)
|
|
await page.locator('#migrationSource').selectOption('')
|
|
assert.equal(await page.locator('#migrationPreview').isDisabled(), true)
|
|
await page.locator('#migrationSkip').click()
|
|
await page.locator('[data-screen="0"].active').waitFor({ state: 'visible' })
|
|
assert.deepEqual(await snapshotTree(skipSource), skipSourceBefore)
|
|
assert.deepEqual(await snapshotTree(skipDesktopSource), skipDesktopSourceBefore)
|
|
assert.notEqual(
|
|
await readFile(join(skipUserData, 'opensquilla', 'workspace', 'IDENTITY.md'), 'utf8').catch(() => ''),
|
|
SOURCE_IDENTITY,
|
|
)
|
|
await app.close()
|
|
app = null
|
|
|
|
// A non-empty Desktop target is backed up and replaced as one profile; source stays read-only.
|
|
const importHome = join(root, 'import-home')
|
|
const source = join(importHome, '.opensquilla')
|
|
const userData = join(root, 'import-user-data')
|
|
const target = join(userData, 'opensquilla')
|
|
seedProfile(source, SOURCE_IDENTITY, SOURCE_CHAT)
|
|
seedProfile(target, TARGET_IDENTITY, 'synthetic previous Desktop chat')
|
|
const sourceBefore = await snapshotTree(source)
|
|
app = await launchDesktop(userData, importHome, 18922)
|
|
page = await onboardingPage(app)
|
|
await page.locator('[data-screen="5"].active').waitFor({ state: 'visible' })
|
|
assert.equal(await page.locator('#migrationSource').inputValue(), '')
|
|
await page.locator('#migrationSource').selectOption(source)
|
|
await waitFor(async () => !(await page.locator('#migrationPreview').isDisabled()), 'explicit source selection')
|
|
await page.locator('#migrationPreview').click()
|
|
try {
|
|
await waitFor(async () => !(await page.locator('#migrationImport').isDisabled()), 'whole-replace preview')
|
|
} catch (error) {
|
|
const diagnostics = await page.evaluate(() => ({
|
|
error: document.getElementById('error')?.textContent || '',
|
|
summary: document.getElementById('migrationSummary')?.textContent || '',
|
|
source: document.getElementById('migrationSource')?.value || '',
|
|
}))
|
|
throw new Error(`${error.message}; diagnostics=${JSON.stringify(diagnostics)}`)
|
|
}
|
|
await app.evaluate(({ dialog }) => {
|
|
dialog.showMessageBox = async () => ({ response: 1, checkboxChecked: false })
|
|
})
|
|
await page.locator('#migrationImport').click()
|
|
try {
|
|
await page.locator('#migrationDoneNote').waitFor({
|
|
state: 'visible',
|
|
timeout: PROFILE_IMPORT_APPLY_TIMEOUT_MS,
|
|
})
|
|
} catch (error) {
|
|
const renderer = await page.evaluate(() => ({
|
|
error: document.getElementById('error')?.textContent || '',
|
|
statusVisible: !document.getElementById('migrationStatus')?.hidden,
|
|
summaryVisible: !document.getElementById('migrationSummary')?.hidden,
|
|
})).catch(() => ({ error: '', statusVisible: false, summaryVisible: false }))
|
|
const pendingPhase = await readFile(
|
|
join(userData, 'migration-provider-setup.json'),
|
|
'utf8',
|
|
).then((raw) => JSON.parse(raw)?.phase || '').catch(() => '')
|
|
const receiptCount = await readdir(join(target, 'migration', 'opensquilla'))
|
|
.then((entries) => entries.length)
|
|
.catch(() => 0)
|
|
const backupCount = await readdir(userData)
|
|
.then((entries) => entries.filter((name) => name.startsWith('opensquilla.backup.')).length)
|
|
.catch(() => 0)
|
|
const migrationResult = await readFile(
|
|
join(userData, 'migration-last-result.json'),
|
|
'utf8',
|
|
).then((raw) => {
|
|
const value = JSON.parse(raw)
|
|
return {
|
|
ok: value?.ok === true,
|
|
migrationApplied: value?.migrationApplied === true,
|
|
restartOk: value?.restartOk === true,
|
|
requiresProviderSetup: value?.requiresProviderSetup === true,
|
|
detail: typeof value?.detail === 'string' ? value.detail : '',
|
|
}
|
|
}).catch(() => null)
|
|
const diagnostics = {
|
|
renderer,
|
|
pendingPhase,
|
|
receiptCount,
|
|
backupCount,
|
|
migrationResult,
|
|
importedIdentityPresent: await readFile(
|
|
join(target, 'workspace', 'IDENTITY.md'),
|
|
'utf8',
|
|
).then((value) => value === SOURCE_IDENTITY).catch(() => false),
|
|
}
|
|
const reportDir = process.env.CI_REPORT_DIR
|
|
if (reportDir) {
|
|
await mkdir(reportDir, { recursive: true })
|
|
await writeFile(
|
|
join(reportDir, 'profile-import-timeout.json'),
|
|
`${JSON.stringify(diagnostics, null, 2)}\n`,
|
|
'utf8',
|
|
)
|
|
}
|
|
throw new Error(`${error.message}; diagnostics=${JSON.stringify(diagnostics)}`)
|
|
}
|
|
assert.match(await page.locator('#migrationDoneNote').innerText(), /Import complete/i)
|
|
|
|
assert.deepEqual(await snapshotTree(source), sourceBefore, 'source bytes and permissions changed')
|
|
assert.equal(await readFile(join(source, '.opensquilla-imported.json'), 'utf8').catch(() => null), null)
|
|
assert.equal(await readFile(join(target, 'workspace', 'IDENTITY.md'), 'utf8'), SOURCE_IDENTITY)
|
|
const importedChat = runPython(`
|
|
import sqlite3, sys
|
|
with sqlite3.connect('file:' + sys.argv[1] + '?mode=ro', uri=True) as connection:
|
|
print(connection.execute('SELECT body FROM synthetic_import_chat WHERE id = ?', ('session-1',)).fetchone()[0])
|
|
`, [join(target, 'state', 'sessions.db')])
|
|
assert.equal(importedChat, SOURCE_CHAT)
|
|
const backups = (await readdir(userData)).filter((name) => name.startsWith('opensquilla.backup.'))
|
|
assert.equal(backups.length, 1)
|
|
assert.equal(
|
|
await readFile(join(userData, backups[0], 'workspace', 'IDENTITY.md'), 'utf8'),
|
|
TARGET_IDENTITY,
|
|
)
|
|
await app.close()
|
|
app = null
|
|
|
|
// Settings import with a required key must release exclusive admission before
|
|
// onboarding, preserve source config bytes, and retain the previous credential.
|
|
const settingsHome = join(root, 'settings-home')
|
|
const settingsSource = join(settingsHome, '.opensquilla')
|
|
const settingsUserData = join(root, 'settings-user-data')
|
|
const settingsTarget = join(settingsUserData, 'opensquilla')
|
|
seedProfile(settingsSource, SOURCE_IDENTITY, SOURCE_CHAT)
|
|
seedProfile(settingsTarget, TARGET_IDENTITY, 'synthetic previous settings chat')
|
|
await writeProviderProfileConfig(settingsSource, {
|
|
provider: 'openai',
|
|
model: 'gpt-5.4-mini',
|
|
baseUrl: 'https://api.openai.com/v1',
|
|
apiKeyEnv: 'OPENAI_API_KEY',
|
|
searchProvider: 'brave',
|
|
searchApiKeyEnv: 'BRAVE_API_KEY',
|
|
routerEnabled: false,
|
|
disableNetworkObservability: true,
|
|
})
|
|
const importedEnvBytes = Buffer.from(
|
|
'OPENAI_API_KEY="synthetic-source-env-key"\r\nTRAILING_VALUE=keep\r\n\r\n',
|
|
)
|
|
await writeFile(join(settingsSource, '.env'), importedEnvBytes)
|
|
await writeProviderProfileConfig(settingsTarget, {
|
|
provider: 'openai',
|
|
model: 'synthetic-old-target-model',
|
|
baseUrl: 'https://api.openai.com/v1',
|
|
apiKeyEnv: 'OPENAI_API_KEY',
|
|
routerEnabled: true,
|
|
disableNetworkObservability: false,
|
|
})
|
|
const oldCredential = await seedDesktopCredential(settingsUserData, {
|
|
provider: 'openai',
|
|
model: 'synthetic-old-target-model',
|
|
baseUrl: 'https://api.openai.com/v1',
|
|
apiKeyEnv: 'OPENAI_API_KEY',
|
|
apiKey: 'synthetic-old-target-key',
|
|
})
|
|
app = await launchDesktop(settingsUserData, settingsHome, 18924)
|
|
const settingsControl = await controlPage(app)
|
|
const settingsPreview = await settingsControl.evaluate(async (sourcePath) => (
|
|
await window.opensquillaDesktop.migrationSummary({ source: sourcePath })
|
|
), settingsSource)
|
|
assert.equal(settingsPreview.ok, false, JSON.stringify(settingsPreview))
|
|
assert.equal(typeof settingsPreview.previewId, 'string')
|
|
assert.equal(
|
|
settingsPreview.report.items.filter((item) => item.status === 'error').at(0)?.kind,
|
|
'preflight/target',
|
|
)
|
|
await app.evaluate(({ dialog }) => {
|
|
dialog.showMessageBox = async () => ({ response: 1, checkboxChecked: false })
|
|
})
|
|
await settingsControl.evaluate(({ previewId }) => {
|
|
void window.opensquillaDesktop.migrationRun({ previewId, overwrite: true })
|
|
return true
|
|
}, { previewId: settingsPreview.previewId })
|
|
|
|
const requiredKeyOnboarding = await onboardingPage(app)
|
|
await requiredKeyOnboarding.locator('[data-screen="0"].active').waitFor({
|
|
state: 'visible',
|
|
timeout: 90_000,
|
|
})
|
|
await requiredKeyOnboarding.locator('[data-screen="0"].active .next-button').click()
|
|
await requiredKeyOnboarding.locator('[data-screen="1"].active').waitFor({
|
|
state: 'visible',
|
|
timeout: 90_000,
|
|
})
|
|
assert.equal(await requiredKeyOnboarding.locator('#provider').inputValue(), 'openai')
|
|
assert.equal(await requiredKeyOnboarding.locator('#model').inputValue(), 'gpt-5.4-mini')
|
|
const importedConfigBeforeCredential = await readFile(join(settingsTarget, 'config.toml'))
|
|
assert.match(importedConfigBeforeCredential.toString('utf8'), /search_provider = "brave"/)
|
|
assert.match(importedConfigBeforeCredential.toString('utf8'), /confidence_threshold = 0\.77/)
|
|
assert.match(
|
|
importedConfigBeforeCredential.toString('utf8'),
|
|
/disable_network_observability = true/,
|
|
)
|
|
await requiredKeyOnboarding.locator('#apiKey').fill('synthetic-new-imported-key')
|
|
await requiredKeyOnboarding.locator('[data-screen="1"].active .next-button').click()
|
|
await requiredKeyOnboarding.locator('[data-screen="4"].active').waitFor({ state: 'visible' })
|
|
await requiredKeyOnboarding.locator('#finish').click()
|
|
|
|
const adopted = await waitFor(async () => {
|
|
const pending = await readFile(
|
|
join(settingsUserData, 'migration-provider-setup.json'),
|
|
'utf8',
|
|
).catch(() => null)
|
|
if (pending !== null) return null
|
|
const raw = await readFile(join(settingsUserData, 'desktop-credential.json'), 'utf8')
|
|
const credential = JSON.parse(raw)
|
|
return credential.configAuthority === 'profile' ? credential : null
|
|
}, 'required-key imported credential adoption')
|
|
assert.match(adopted.importTransactionId, /^[0-9a-f-]{36}$/i)
|
|
assert.equal(adopted.model, 'gpt-5.4-mini')
|
|
assert.equal(
|
|
Buffer.from(adopted.encryptedApiKey, 'base64').toString('utf8'),
|
|
'synthetic-new-imported-key',
|
|
)
|
|
assert.deepEqual(
|
|
await readFile(join(settingsTarget, 'config.toml')),
|
|
importedConfigBeforeCredential,
|
|
'provider adoption rewrote imported config.toml',
|
|
)
|
|
assert.deepEqual(
|
|
await readFile(join(settingsTarget, '.env')),
|
|
importedEnvBytes,
|
|
'provider adoption rewrote imported .env bytes',
|
|
)
|
|
const credentialBackup = join(
|
|
settingsUserData,
|
|
`desktop-credential.import-backup.${adopted.importTransactionId}.json`,
|
|
)
|
|
assert.equal(await readFile(credentialBackup, 'utf8'), oldCredential)
|
|
if (process.platform !== 'win32') {
|
|
assert.equal((await lstat(credentialBackup)).mode & 0o777, 0o600)
|
|
}
|
|
await app.close()
|
|
app = null
|
|
|
|
// A selected recovery H can use the app, but it cannot import another profile.
|
|
const recoveryHome = join(root, 'recovery-home')
|
|
const recoveryUserData = join(root, 'recovery-user-data')
|
|
const recoveryId = '12345678-1234-4234-8234-123456789abc'
|
|
await mkdir(join(recoveryUserData, 'recovery-profiles', recoveryId, 'opensquilla'), { recursive: true })
|
|
await writeFile(join(recoveryUserData, 'desktop-profile-context.json'), JSON.stringify({
|
|
schema_version: 1,
|
|
active_profile_kind: 'recovery',
|
|
active_recovery_id: recoveryId,
|
|
attention_acknowledgement: null,
|
|
updated_at: new Date().toISOString(),
|
|
}, null, 2))
|
|
app = await launchDesktop(recoveryUserData, recoveryHome, 18923)
|
|
page = await recoveryPage(app)
|
|
const rejected = await page.evaluate(() => window.opensquillaDesktop.migrationSummary())
|
|
assert.equal(rejected.ok, false)
|
|
assert.match(rejected.raw, /primary profile/i)
|
|
|
|
console.log(JSON.stringify({
|
|
explicitSelectionAndSkip: true,
|
|
multipleCandidates: true,
|
|
wholeReplacement: true,
|
|
sourceUnchanged: true,
|
|
identityAndChatImported: true,
|
|
settingsRequiredKeyCompleted: true,
|
|
importedConfigPreserved: true,
|
|
previousCredentialBackedUp: true,
|
|
recoveryProfileRejected: true,
|
|
}, null, 2))
|
|
} finally {
|
|
if (app) await app.close().catch(() => {})
|
|
await rm(root, { recursive: true, force: true })
|
|
}
|