The foundation for a shareable team build. Replaces the .env.local-only dev config
with a real, per-teammate connection flow.
main:
- config-store.ts: token encrypted at rest via Electron safeStorage (OS keychain),
config JSON in userData. Token lives only in main; renderer gets everything but.
- resolveConfig: saved config > .env.local (dev) > null; ignored under COMMITEA_E2E.
pm-state repo defaults to `${repo}-pm-state`. resetClients() re-reads on change so
saving config takes effect without a restart. gitea:status gains `demo` (e2e).
- IPC: config:get (no token), config:test (authed read validates token+repo),
config:set (encrypt+save+reset), config:clear. Model bridge reads config.modelUrl
and probes reachability — chat is "configured" only if a model actually answers;
localhost default is dev-only (app.isPackaged gate).
renderer:
- ConnectScreen: real onboarding form (URL/owner/repo/PAT/optional model) → test →
save. AppShell gates on it: demo → shell (fixtures/e2e); configured → shell (real);
else → connect. Settings Connection card is real (repo/url/model/sidecar) with
Reconfigure + Disconnect. Chat cleanly disables with a "no model" state instead of
the scripted canned reply.
Verified: main + desktop typecheck clean, 14 fixture e2e green (demo mode unchanged),
live onboarding e2e: fresh app → connect form → validated PAT → real board (24 done /
10 open). COMMITEA_NO_ENV_LOCAL + COMMITEA_USERDATA are test hooks for the onboarding path.
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
57 lines
2.3 KiB
TypeScript
57 lines
2.3 KiB
TypeScript
import { dirname, join } from 'node:path'
|
|
import { mkdtempSync, readFileSync } from 'node:fs'
|
|
import { tmpdir } from 'node:os'
|
|
import { fileURLToPath } from 'node:url'
|
|
|
|
import { _electron as electron, expect, test } from '@playwright/test'
|
|
|
|
const here = dirname(fileURLToPath(import.meta.url))
|
|
const MAIN = join(here, '..', 'out', 'main', 'index.js')
|
|
|
|
/** Read the real token from .env.local (walking up) so the test can fill the form. */
|
|
function readToken(): string | null {
|
|
let dir = here
|
|
for (let i = 0; i < 6; i++) {
|
|
try {
|
|
const m = /^GITEA_TOKEN\s*=\s*(.+?)\s*$/m.exec(readFileSync(join(dir, '.env.local'), 'utf8'))
|
|
if (m) return m[1].trim()
|
|
} catch {
|
|
/* keep walking */
|
|
}
|
|
dir = dirname(dir)
|
|
}
|
|
return null
|
|
}
|
|
|
|
// Opt-in (GITEA_LIVE=1). Forces the onboarding path (COMMITEA_NO_ENV_LOCAL) with a
|
|
// fresh userData, fills the real connection form, and asserts it lands on real data.
|
|
test.describe('live onboarding', () => {
|
|
test('connect form → real board', async () => {
|
|
const token = process.env.GITEA_LIVE ? readToken() : null
|
|
test.skip(!token, 'GITEA_LIVE + a .env.local token required')
|
|
test.setTimeout(90_000)
|
|
|
|
const app = await electron.launch({
|
|
args: [MAIN],
|
|
env: { ...process.env, COMMITEA_NO_ENV_LOCAL: '1', COMMITEA_USERDATA: mkdtempSync(join(tmpdir(), 'commitea-')) },
|
|
})
|
|
const win = await app.firstWindow()
|
|
await win.waitForLoadState('domcontentloaded')
|
|
|
|
// the connection gate, not the app
|
|
await expect(win.getByText(/Connect your Gitea/)).toBeVisible({ timeout: 15000 })
|
|
await win.getByPlaceholder('your-org').fill('christian')
|
|
await win.getByPlaceholder('your-repo').fill('commitea')
|
|
await win.getByPlaceholder(/gitea PAT/).fill(token!)
|
|
await win.getByRole('button', { name: 'Connect' }).click()
|
|
|
|
// it validated + saved + reconciled into the real board — real issue titles prove it
|
|
const rail = win.getByRole('navigation', { name: 'Primary' })
|
|
await rail.getByRole('button', { name: 'The pot' }).click()
|
|
await expect(win.getByText(/Model router|SQLite cache bootstrap|Purity\/rebuild/).first()).toBeVisible({ timeout: 30000 })
|
|
await win.screenshot({ path: join(here, '.artifacts', 'screens', 'live-onboarding.png'), fullPage: true, animations: 'disabled' })
|
|
|
|
await app.close()
|
|
})
|
|
})
|