Default new repo badge color to gray (#1952)

This commit is contained in:
Jinjing 2026-05-15 19:03:04 -07:00 committed by GitHub
parent 1258e78aa9
commit f24a968ea0
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
6 changed files with 318 additions and 35 deletions

View File

@ -10,6 +10,7 @@
*/
import { describe, expect, it, vi, beforeEach } from 'vitest'
import { DEFAULT_REPO_BADGE_COLOR } from '../../shared/constants'
const {
handleMock,
@ -87,7 +88,7 @@ type CreateResult =
| { error: string }
describe('repos:create', () => {
const handlers = new Map<string, (event: unknown, args: CreateArgs) => Promise<CreateResult>>()
const handlers = new Map<string, (event: unknown, args: unknown) => Promise<unknown>>()
const mockWindow = {
isDestroyed: () => false,
webContents: { send: vi.fn() }
@ -98,14 +99,14 @@ describe('repos:create', () => {
if (!handler) {
throw new Error('repos:create handler was never registered')
}
return handler(null, args)
return handler(null, args) as Promise<CreateResult>
}
beforeEach(() => {
handlers.clear()
handleMock.mockReset()
handleMock.mockImplementation((channel: string, handler: (...a: unknown[]) => unknown) => {
handlers.set(channel, handler as (event: unknown, args: CreateArgs) => Promise<CreateResult>)
handlers.set(channel, handler as (event: unknown, args: unknown) => Promise<unknown>)
})
removeHandlerMock.mockReset()
mockStore.getRepos.mockReset().mockReturnValue([])
@ -218,6 +219,15 @@ describe('repos:create', () => {
expect(result).toHaveProperty('repo.kind', 'folder')
})
it('defaults repos:create badgeColor to DEFAULT_REPO_BADGE_COLOR', async () => {
const result = await callCreate({ parentPath: '/tmp', name: 'default-color', kind: 'folder' })
expect(mockStore.addRepo).toHaveBeenCalledWith(
expect.objectContaining({ badgeColor: DEFAULT_REPO_BADGE_COLOR })
)
expect(result).toHaveProperty('repo.badgeColor', DEFAULT_REPO_BADGE_COLOR)
})
// ── git repo happy path ───────────────────────────────────────────
it('creates a git repo with an empty initial commit (in order)', async () => {
@ -376,4 +386,21 @@ describe('repos:create', () => {
expect(gitExecFileAsyncMock).not.toHaveBeenCalled()
expect(mockStore.addRepo).not.toHaveBeenCalled()
})
it('returns existing badgeColor unchanged on repos:create dedupe', async () => {
const existing = {
id: 'abc',
path: '/tmp/dupe-color',
displayName: 'dupe-color',
kind: 'git',
badgeColor: '#ef4444'
}
mockStore.getRepos.mockReturnValue([existing])
const result = await callCreate({ parentPath: '/tmp', name: 'dupe-color', kind: 'git' })
expect(result).toEqual({ repo: existing })
expect(result).toHaveProperty('repo.badgeColor', '#ef4444')
expect(mockStore.addRepo).not.toHaveBeenCalled()
})
})

View File

@ -4,28 +4,33 @@ fixture setup and mock plumbing can be shared. Splitting by line count would
duplicate the hoisted mocks and the `../git/repo` partial-real/partial-stub
setup. */
import { describe, expect, it, vi, beforeEach } from 'vitest'
import { EventEmitter } from 'events'
import type * as RepoModule from '../git/repo'
import { DEFAULT_REPO_BADGE_COLOR } from '../../shared/constants'
const { handleMock, mockStore, mockGitProvider, mockMultiplexer } = vi.hoisted(() => ({
handleMock: vi.fn(),
mockStore: {
getRepos: vi.fn().mockReturnValue([]),
addRepo: vi.fn(),
removeRepo: vi.fn(),
getRepo: vi.fn(),
updateRepo: vi.fn(),
getSshTarget: vi.fn()
},
mockGitProvider: {
isGitRepo: vi.fn().mockReturnValue(true),
isGitRepoAsync: vi.fn().mockResolvedValue({ isRepo: true, rootPath: null }),
exec: vi.fn().mockResolvedValue({ stdout: '', stderr: '' })
},
mockMultiplexer: {
request: vi.fn(),
notify: vi.fn()
}
}))
const { handleMock, mockStore, mockGitProvider, mockMultiplexer, gitSpawnMock } = vi.hoisted(
() => ({
handleMock: vi.fn(),
mockStore: {
getRepos: vi.fn().mockReturnValue([]),
addRepo: vi.fn(),
removeRepo: vi.fn(),
getRepo: vi.fn(),
updateRepo: vi.fn(),
getSshTarget: vi.fn()
},
mockGitProvider: {
isGitRepo: vi.fn().mockReturnValue(true),
isGitRepoAsync: vi.fn().mockResolvedValue({ isRepo: true, rootPath: null }),
exec: vi.fn().mockResolvedValue({ stdout: '', stderr: '' })
},
mockMultiplexer: {
request: vi.fn(),
notify: vi.fn()
},
gitSpawnMock: vi.fn()
})
)
vi.mock('electron', () => ({
dialog: { showOpenDialog: vi.fn() },
@ -53,6 +58,11 @@ vi.mock('../git/repo', async () => {
}
})
vi.mock('../git/runner', () => ({
gitExecFileAsync: vi.fn(),
gitSpawn: gitSpawnMock
}))
vi.mock('./filesystem-auth', () => ({
invalidateAuthorizedRootsCache: vi.fn()
}))
@ -93,8 +103,16 @@ describe('repos:addRemote', () => {
mockStore.getRepos.mockReset().mockReturnValue([])
mockStore.addRepo.mockReset()
mockStore.getSshTarget.mockReset()
mockStore.updateRepo.mockReset()
mockMultiplexer.request.mockReset()
mockMultiplexer.notify.mockReset()
gitSpawnMock.mockReset()
gitSpawnMock.mockImplementation(() => {
const proc = new EventEmitter() as EventEmitter & { stderr: EventEmitter }
proc.stderr = new EventEmitter()
queueMicrotask(() => proc.emit('close', 0, null))
return proc
})
mockWindow.webContents.send.mockReset()
registerRepoHandlers(mockWindow as never, mockStore as never)
@ -115,7 +133,8 @@ describe('repos:addRemote', () => {
path: '/home/user/project',
connectionId: 'conn-1',
kind: 'git',
displayName: 'project'
displayName: 'project',
badgeColor: DEFAULT_REPO_BADGE_COLOR
})
)
expect(result).toHaveProperty('repo.id')
@ -188,7 +207,8 @@ describe('repos:addRemote', () => {
expect(mockStore.addRepo).toHaveBeenCalledWith(
expect.objectContaining({
kind: 'folder',
path: '/home/user/documents'
path: '/home/user/documents',
badgeColor: DEFAULT_REPO_BADGE_COLOR
})
)
expect(result).toHaveProperty('repo.kind', 'folder')
@ -292,6 +312,104 @@ describe('repos:addRemote', () => {
})
})
describe('repos:add + repos:clone', () => {
const handlers = new Map<string, (_event: unknown, args: unknown) => unknown>()
const mockWindow = {
isDestroyed: () => false,
webContents: { send: vi.fn() }
}
beforeEach(() => {
handlers.clear()
handleMock.mockReset()
handleMock.mockImplementation((channel: string, handler: (...a: unknown[]) => unknown) => {
handlers.set(channel, handler)
})
mockStore.getRepos.mockReset().mockReturnValue([])
mockStore.addRepo.mockReset()
mockStore.updateRepo.mockReset()
mockWindow.webContents.send.mockReset()
gitSpawnMock.mockReset()
gitSpawnMock.mockImplementation(() => {
const proc = new EventEmitter() as EventEmitter & { stderr: EventEmitter }
proc.stderr = new EventEmitter()
queueMicrotask(() => proc.emit('close', 0, null))
return proc
})
registerRepoHandlers(mockWindow as never, mockStore as never)
})
it('defaults repos:add badgeColor to DEFAULT_REPO_BADGE_COLOR for folder repos', async () => {
const result = await handlers.get('repos:add')!(null, { path: '/tmp/from-add', kind: 'folder' })
expect(mockStore.addRepo).toHaveBeenCalledWith(
expect.objectContaining({ path: '/tmp/from-add', badgeColor: DEFAULT_REPO_BADGE_COLOR })
)
expect(result).toHaveProperty('repo.badgeColor', DEFAULT_REPO_BADGE_COLOR)
})
it('returns existing badgeColor unchanged on repos:add dedupe', async () => {
const existing = {
id: 'repo-add-existing',
path: '/tmp/from-add-existing',
displayName: 'from-add-existing',
kind: 'folder',
badgeColor: '#22c55e'
}
mockStore.getRepos.mockReturnValue([existing])
const result = await handlers.get('repos:add')!(null, {
path: '/tmp/from-add-existing',
kind: 'folder'
})
expect(result).toEqual({ repo: existing })
expect(result).toHaveProperty('repo.badgeColor', '#22c55e')
expect(mockStore.addRepo).not.toHaveBeenCalled()
})
it('defaults repos:clone badgeColor to DEFAULT_REPO_BADGE_COLOR', async () => {
const result = await handlers.get('repos:clone')!(null, {
url: 'https://example.com/orca.git',
destination: '/tmp'
})
expect(mockStore.addRepo).toHaveBeenCalledWith(
expect.objectContaining({
path: '/tmp/orca',
badgeColor: DEFAULT_REPO_BADGE_COLOR,
kind: 'git'
})
)
expect(result).toHaveProperty('badgeColor', DEFAULT_REPO_BADGE_COLOR)
})
it('preserves existing badgeColor when repos:clone upgrades folder->git after dedupe', async () => {
const existing = {
id: 'folder-repo',
path: '/tmp/orca',
displayName: 'orca',
badgeColor: '#8b5cf6',
addedAt: 1,
kind: 'folder'
}
const upgraded = { ...existing, kind: 'git' as const }
mockStore.getRepos.mockReturnValue([existing])
mockStore.updateRepo.mockReturnValue(upgraded)
const result = await handlers.get('repos:clone')!(null, {
url: 'https://example.com/orca.git',
destination: '/tmp'
})
expect(mockStore.updateRepo).toHaveBeenCalledWith(existing.id, { kind: 'git' })
expect(result).toEqual(upgraded)
expect(result).toHaveProperty('badgeColor', '#8b5cf6')
expect(mockStore.addRepo).not.toHaveBeenCalled()
})
})
describe('repos:getBaseRefDefault envelope', () => {
const handlers = new Map<string, (_event: unknown, args: unknown) => unknown>()
const mockWindow = {

View File

@ -7,7 +7,7 @@ import { randomUUID } from 'crypto'
import type { Store } from '../persistence'
import type { Repo, BaseRefDefaultResult, SparsePreset } from '../../shared/types'
import { isFolderRepo } from '../../shared/repo-kind'
import { REPO_COLORS } from '../../shared/constants'
import { DEFAULT_REPO_BADGE_COLOR } from '../../shared/constants'
import { invalidateAuthorizedRootsCache } from './filesystem-auth'
import type { ChildProcess } from 'child_process'
import { access, mkdir, readdir, rm } from 'fs/promises'
@ -107,7 +107,7 @@ export function registerRepoHandlers(mainWindow: BrowserWindow, store: Store): v
id: randomUUID(),
path: args.path,
displayName: getRepoName(args.path),
badgeColor: REPO_COLORS[store.getRepos().length % REPO_COLORS.length],
badgeColor: DEFAULT_REPO_BADGE_COLOR,
addedAt: Date.now(),
kind: repoKind
}
@ -206,7 +206,7 @@ export function registerRepoHandlers(mainWindow: BrowserWindow, store: Store): v
id: randomUUID(),
path: resolvedPath,
displayName,
badgeColor: REPO_COLORS[store.getRepos().length % REPO_COLORS.length],
badgeColor: DEFAULT_REPO_BADGE_COLOR,
addedAt: Date.now(),
kind: repoKind,
connectionId: args.connectionId
@ -405,7 +405,7 @@ export function registerRepoHandlers(mainWindow: BrowserWindow, store: Store): v
id: randomUUID(),
path: targetPath,
displayName: name,
badgeColor: REPO_COLORS[store.getRepos().length % REPO_COLORS.length],
badgeColor: DEFAULT_REPO_BADGE_COLOR,
addedAt: Date.now(),
kind: repoKind
}
@ -686,7 +686,7 @@ export function registerRepoHandlers(mainWindow: BrowserWindow, store: Store): v
id: randomUUID(),
path: clonePath,
displayName: getRepoName(clonePath),
badgeColor: REPO_COLORS[store.getRepos().length % REPO_COLORS.length],
badgeColor: DEFAULT_REPO_BADGE_COLOR,
addedAt: Date.now(),
kind: 'git'
}

View File

@ -1,7 +1,10 @@
/* eslint-disable max-lines -- Why: runtime behavior is stateful and cross-cutting, so these tests stay in one file to preserve the end-to-end invariants around handles, waits, and graph sync. */
import { afterEach, describe, expect, it, vi } from 'vitest'
import { EventEmitter } from 'events'
import { mkdtemp, rm } from 'fs/promises'
import type { WorktreeLineage, WorktreeMeta } from '../../shared/types'
import { addWorktree, listWorktrees, removeWorktree } from '../git/worktree'
import * as gitRunner from '../git/runner'
import {
createSetupRunnerScript,
getEffectiveHooks,
@ -18,6 +21,7 @@ import {
unregisterSshFilesystemProvider
} from '../providers/ssh-filesystem-dispatch'
import { registerSshGitProvider, unregisterSshGitProvider } from '../providers/ssh-git-dispatch'
import { DEFAULT_REPO_BADGE_COLOR } from '../../shared/constants'
const {
MOCK_GIT_WORKTREES,
@ -95,7 +99,9 @@ vi.mock('../ipc/worktree-logic', async (importOriginal) => {
})
vi.mock('../ipc/filesystem-auth', () => ({
invalidateAuthorizedRootsCache: invalidateAuthorizedRootsCacheMock
invalidateAuthorizedRootsCache: invalidateAuthorizedRootsCacheMock,
isENOENT: (error: unknown) =>
Boolean(error && typeof error === 'object' && 'code' in error && error.code === 'ENOENT')
}))
// Why: the CLI create-worktree path calls getDefaultBaseRef to resolve a
@ -866,6 +872,136 @@ describe('OrcaRuntimeService', () => {
expect(added).toHaveLength(0)
})
it('defaults runtime addRepo badgeColor to DEFAULT_REPO_BADGE_COLOR', async () => {
const added: Record<string, unknown>[] = []
const colorStore = {
...store,
getRepos: () => [...added] as never,
addRepo: (repo: Record<string, unknown>) => {
added.push(repo)
},
getRepo: (id: string) => added.find((repo) => repo.id === id) as never
}
const runtime = new OrcaRuntimeService(colorStore as never)
const repo = await runtime.addRepo('/tmp/runtime-add-default', 'folder')
expect(repo.badgeColor).toBe(DEFAULT_REPO_BADGE_COLOR)
expect(added).toEqual([expect.objectContaining({ badgeColor: DEFAULT_REPO_BADGE_COLOR })])
})
it('defaults runtime createRepo badgeColor to DEFAULT_REPO_BADGE_COLOR', async () => {
const added: Record<string, unknown>[] = []
const colorStore = {
...store,
getRepos: () => [...added] as never,
addRepo: (repo: Record<string, unknown>) => {
added.push(repo)
},
getRepo: (id: string) => added.find((repo) => repo.id === id) as never
}
const runtime = new OrcaRuntimeService(colorStore as never)
const parentDir = await mkdtemp('/tmp/orca-runtime-create-')
try {
const result = await runtime.createRepo(parentDir, 'runtime-create-default', 'folder')
if ('error' in result) {
throw new Error(result.error)
}
expect(result).toHaveProperty('repo.badgeColor', DEFAULT_REPO_BADGE_COLOR)
expect(added).toEqual([expect.objectContaining({ badgeColor: DEFAULT_REPO_BADGE_COLOR })])
} finally {
await rm(parentDir, { recursive: true, force: true })
}
})
it('preserves existing badgeColor on runtime createRepo dedupe', async () => {
const existing = {
id: 'runtime-existing-create',
path: '/tmp/runtime-existing-create',
displayName: 'runtime-existing-create',
badgeColor: '#14b8a6',
addedAt: 1,
kind: 'folder' as const
}
const colorStore = {
...store,
getRepos: () => [existing]
}
const runtime = new OrcaRuntimeService(colorStore as never)
const result = await runtime.createRepo('/tmp', 'runtime-existing-create', 'folder')
expect(result).toEqual({ repo: existing })
expect(result).toHaveProperty('repo.badgeColor', '#14b8a6')
})
it('defaults runtime cloneRepo badgeColor to DEFAULT_REPO_BADGE_COLOR', async () => {
const spawnSpy = vi.spyOn(gitRunner, 'wslAwareSpawn')
const added: Record<string, unknown>[] = []
const colorStore = {
...store,
getRepos: () => [...added] as never,
addRepo: (repo: Record<string, unknown>) => {
added.push(repo)
},
getRepo: (id: string) => added.find((repo) => repo.id === id) as never
}
spawnSpy.mockImplementation(() => {
const proc = new EventEmitter() as EventEmitter & { stderr: EventEmitter }
proc.stderr = new EventEmitter()
queueMicrotask(() => proc.emit('close', 0, null))
return proc as never
})
const runtime = new OrcaRuntimeService(colorStore as never)
try {
const repo = await runtime.cloneRepo('https://example.com/repo-badge-color.git', '/tmp')
expect(repo.badgeColor).toBe(DEFAULT_REPO_BADGE_COLOR)
expect(added).toEqual([expect.objectContaining({ badgeColor: DEFAULT_REPO_BADGE_COLOR })])
} finally {
spawnSpy.mockRestore()
}
})
it('preserves existing badgeColor on runtime cloneRepo folder->git dedupe upgrade', async () => {
const spawnSpy = vi.spyOn(gitRunner, 'wslAwareSpawn')
spawnSpy.mockImplementation(() => {
const proc = new EventEmitter() as EventEmitter & { stderr: EventEmitter }
proc.stderr = new EventEmitter()
queueMicrotask(() => proc.emit('close', 0, null))
return proc as never
})
const existing = {
id: 'runtime-folder-upgrade',
path: '/tmp/repo-badge-color',
displayName: 'repo-badge-color',
badgeColor: '#ec4899',
addedAt: 1,
kind: 'folder' as const
}
const updates: { id: string; updates: Record<string, unknown> }[] = []
const upgraded = { ...existing, kind: 'git' as const }
const colorStore = {
...store,
getRepos: () => [existing],
updateRepo: (id: string, repoUpdates: Record<string, unknown>) => {
updates.push({ id, updates: repoUpdates })
return upgraded as never
}
}
const runtime = new OrcaRuntimeService(colorStore as never)
try {
const repo = await runtime.cloneRepo('https://example.com/repo-badge-color.git', '/tmp')
expect(updates).toEqual([{ id: existing.id, updates: { kind: 'git' } }])
expect(repo).toEqual(upgraded)
expect(repo.badgeColor).toBe('#ec4899')
} finally {
spawnSpy.mockRestore()
}
})
it('associates controller PTYs with mixed-case Windows and UNC cwd paths', async () => {
vi.mocked(listWorktrees).mockResolvedValue([
{

View File

@ -219,7 +219,7 @@ import {
shouldRunSetupForCreate,
writeIssueCommand
} from '../hooks'
import { REPO_COLORS, getDefaultVoiceSettings } from '../../shared/constants'
import { DEFAULT_REPO_BADGE_COLOR, getDefaultVoiceSettings } from '../../shared/constants'
import { listRepoWorktrees } from '../repo-worktrees'
import { createWorktreeSymlinks } from '../ipc/worktree-symlinks'
import {
@ -4046,7 +4046,7 @@ export class OrcaRuntimeService {
id: randomUUID(),
path,
displayName: getRepoName(path),
badgeColor: REPO_COLORS[this.store.getRepos().length % REPO_COLORS.length],
badgeColor: DEFAULT_REPO_BADGE_COLOR,
addedAt: Date.now(),
kind
}
@ -4154,7 +4154,7 @@ export class OrcaRuntimeService {
id: randomUUID(),
path: targetPath,
displayName: trimmedName,
badgeColor: REPO_COLORS[this.store.getRepos().length % REPO_COLORS.length],
badgeColor: DEFAULT_REPO_BADGE_COLOR,
addedAt: Date.now(),
kind: repoKind
}
@ -4221,7 +4221,7 @@ export class OrcaRuntimeService {
id: randomUUID(),
path: clonePath,
displayName: getRepoName(clonePath),
badgeColor: REPO_COLORS[this.store.getRepos().length % REPO_COLORS.length],
badgeColor: DEFAULT_REPO_BADGE_COLOR,
addedAt: Date.now(),
kind: 'git'
}

View File

@ -120,6 +120,8 @@ export const REPO_COLORS = [
'#ec4899' // pink
] as const
export const DEFAULT_REPO_BADGE_COLOR = REPO_COLORS[0]
export function getDefaultNotificationSettings(): NotificationSettings {
return {
enabled: true,