Live onboarding wizard: real discovery, repo pick, and bootstrap
The 4-step wizard is now fully live — nothing faked.
Backend (core + main):
- GiteaClient.createLabel (POST /labels).
- core/gitea/bootstrap.ts: SCHEMA_LABELS (the full est/p/deadline vocabulary with
colors+descriptions), applySchemaLabels (idempotent — creates only missing),
repoExists + ensurePmStateRepo (token-scoped, org vs personal endpoint). 7 tests.
- config:bootstrap IPC handler + preload bridge + BootstrapResult type.
Wizard:
- Connect step → live config.discover ('connected · N repos visible' is real).
- Repo step → owner dropdown + repo radios from the discovered repos.
- Bootstrap step → real: applies the label schema + ensures the pm-state sidecar,
shows the actual outcome (created vs already-present), then saves config.
- Gate: first run (no config) → wizard; reconnect (existing) → the compact card.
Removed the dead 'firstrun' dev view.
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
This commit is contained in:
91
packages/core/src/gitea/bootstrap.test.ts
Normal file
91
packages/core/src/gitea/bootstrap.test.ts
Normal file
@@ -0,0 +1,91 @@
|
||||
import { describe, expect, it } from 'vitest'
|
||||
|
||||
import { applySchemaLabels, ensurePmStateRepo, repoExists, SCHEMA_LABELS } from './bootstrap.js'
|
||||
import { GiteaApiError } from './types.js'
|
||||
import type { FetchLike } from './types.js'
|
||||
|
||||
describe('SCHEMA_LABELS', () => {
|
||||
it('covers the full vocabulary: 5 estimates + 4 priorities + hard deadline', () => {
|
||||
const names = SCHEMA_LABELS.map((l) => l.name)
|
||||
expect(names).toEqual(['est/1d', 'est/2d', 'est/3d', 'est/5d', 'est/8d', 'p/1', 'p/2', 'p/3', 'p/4', 'deadline/hard'])
|
||||
// colors are hex without '#'
|
||||
for (const l of SCHEMA_LABELS) expect(l.color).toMatch(/^[0-9a-f]{6}$/)
|
||||
})
|
||||
})
|
||||
|
||||
describe('applySchemaLabels', () => {
|
||||
it('creates only the missing labels (idempotent)', async () => {
|
||||
const created: string[] = []
|
||||
const client = {
|
||||
listLabels: async () => [
|
||||
{ id: 1, name: 'est/1d' },
|
||||
{ id: 2, name: 'p/1' },
|
||||
],
|
||||
createLabel: async (input: { name: string; color: string }) => {
|
||||
created.push(input.name)
|
||||
return { id: 99, name: input.name }
|
||||
},
|
||||
}
|
||||
const res = await applySchemaLabels(client)
|
||||
expect(res.existing).toEqual(['est/1d', 'p/1'])
|
||||
expect(res.created).toEqual(['est/2d', 'est/3d', 'est/5d', 'est/8d', 'p/2', 'p/3', 'p/4', 'deadline/hard'])
|
||||
expect(created).toEqual(res.created) // exactly those createLabel calls, no more
|
||||
})
|
||||
|
||||
it('creates nothing when the schema is already applied', async () => {
|
||||
const client = {
|
||||
listLabels: async () => SCHEMA_LABELS.map((l, i) => ({ id: i, name: l.name })),
|
||||
createLabel: async () => {
|
||||
throw new Error('should not create')
|
||||
},
|
||||
}
|
||||
const res = await applySchemaLabels(client)
|
||||
expect(res.created).toEqual([])
|
||||
expect(res.existing).toHaveLength(SCHEMA_LABELS.length)
|
||||
})
|
||||
})
|
||||
|
||||
/** A fetch stub routing repo-exists (GET) and repo-create (POST). */
|
||||
function stub(opts: { exists?: boolean; createStatus?: number }): { fetch: FetchLike; posts: { url: string }[] } {
|
||||
const posts: { url: string }[] = []
|
||||
const fetch: FetchLike = (url, init) => {
|
||||
const method = init?.method ?? 'GET'
|
||||
if (method === 'GET') {
|
||||
return Promise.resolve({ ok: !!opts.exists, status: opts.exists ? 200 : 404, json: () => Promise.resolve({}), text: () => Promise.resolve('') })
|
||||
}
|
||||
posts.push({ url })
|
||||
const status = opts.createStatus ?? 201
|
||||
return Promise.resolve({ ok: status < 400, status, json: () => Promise.resolve({}), text: () => Promise.resolve('') })
|
||||
}
|
||||
return { fetch, posts }
|
||||
}
|
||||
|
||||
describe('ensurePmStateRepo', () => {
|
||||
const conn = { baseUrl: 'https://gitea.example.io', token: 'pat' }
|
||||
|
||||
it('returns "exists" and creates nothing when the repo is already there', async () => {
|
||||
const { fetch, posts } = stub({ exists: true })
|
||||
const r = await ensurePmStateRepo(conn, { owner: 'christian', repo: 'commitea-pm-state', underOrg: false }, fetch)
|
||||
expect(r).toBe('exists')
|
||||
expect(posts).toEqual([])
|
||||
})
|
||||
|
||||
it('creates a personal repo via /user/repos when the owner is the token user', async () => {
|
||||
const { fetch, posts } = stub({ exists: false })
|
||||
const r = await ensurePmStateRepo(conn, { owner: 'christian', repo: 'commitea-pm-state', underOrg: false }, fetch)
|
||||
expect(r).toBe('created')
|
||||
expect(posts[0].url).toBe('https://gitea.example.io/api/v1/user/repos')
|
||||
})
|
||||
|
||||
it('creates an org repo via /orgs/{owner}/repos when the owner is an org', async () => {
|
||||
const { fetch, posts } = stub({ exists: false })
|
||||
const r = await ensurePmStateRepo(conn, { owner: 'NovelPad', repo: 'novelpad-pm-state', underOrg: true }, fetch)
|
||||
expect(r).toBe('created')
|
||||
expect(posts[0].url).toBe('https://gitea.example.io/api/v1/orgs/NovelPad/repos')
|
||||
})
|
||||
|
||||
it('surfaces a non-404 read failure as a GiteaApiError', async () => {
|
||||
const fetch: FetchLike = () => Promise.resolve({ ok: false, status: 403, json: () => Promise.resolve({}), text: () => Promise.resolve('') })
|
||||
await expect(repoExists(conn, 'x', 'y', fetch)).rejects.toBeInstanceOf(GiteaApiError)
|
||||
})
|
||||
})
|
||||
Reference in New Issue
Block a user