mirror of
https://github.com/stablyai/orca.git
synced 2026-10-25 00:03:59 +00:00
* fix(mobile): scope optimistic workspace removal to the deleted host A worktreeId repeats across hosts, so filtering the list on the bare id also removed the identically-named workspace belonging to the other host. Match on (worktreeId, hostId) through a named helper so the rule is testable. * fix(mobile): key host worktree rows consistently
806 lines
23 KiB
TypeScript
806 lines
23 KiB
TypeScript
import { join } from 'node:path'
|
|
import { describe, expect, it } from 'vitest'
|
|
import type { Worktree } from './workspace-list-sections'
|
|
import {
|
|
CREATE_GRACE_MS,
|
|
buildSections,
|
|
filterWorktrees,
|
|
getWorktreeStatus,
|
|
sortWorktrees
|
|
} from './workspace-list-sections'
|
|
import { DEFAULT_MOBILE_WORKSPACE_STATUSES } from './mobile-workspace-statuses'
|
|
import { getMobileWorkspaceLineageGroupKey } from './mobile-workspace-lineage'
|
|
|
|
function worktree(overrides: Partial<Worktree> = {}): Worktree {
|
|
const worktreePath = join('/tmp', 'orca', 'worktrees', 'feature')
|
|
return {
|
|
workspaceKind: 'git',
|
|
worktreeId: `repo-1::${worktreePath}`,
|
|
repoId: 'repo-1',
|
|
repo: 'orca',
|
|
branch: 'feature/mobile-parity',
|
|
displayName: 'feature',
|
|
path: worktreePath,
|
|
liveTerminalCount: 0,
|
|
hasAttachedPty: false,
|
|
preview: '',
|
|
unread: false,
|
|
isPinned: false,
|
|
linkedPR: null,
|
|
status: 'inactive',
|
|
agents: [],
|
|
...overrides
|
|
}
|
|
}
|
|
|
|
function withoutSectionListKeys(sections: ReturnType<typeof buildSections>) {
|
|
return sections.map((section) => ({
|
|
...section,
|
|
data: section.data.map(
|
|
({
|
|
sectionListKey: _sectionListKey,
|
|
lineageDepth: _lineageDepth,
|
|
lineageChildCount: _lineageChildCount,
|
|
lineageCollapsed: _lineageCollapsed,
|
|
isLastLineageChild: _isLastLineageChild,
|
|
...item
|
|
}) => item
|
|
)
|
|
}))
|
|
}
|
|
|
|
describe('filterWorktrees', () => {
|
|
it('hides archived worktrees', () => {
|
|
const visible = worktree({ worktreeId: 'visible' })
|
|
const archived = worktree({ worktreeId: 'archived', isArchived: true })
|
|
|
|
expect(
|
|
filterWorktrees(
|
|
[visible, archived],
|
|
{ filterRepoIds: new Set(), hideSleeping: false, hideDefaultBranch: false },
|
|
''
|
|
)
|
|
).toEqual([visible])
|
|
})
|
|
|
|
it('uses host sidebar activity for sleeping filtering when available', () => {
|
|
const visible = worktree({
|
|
worktreeId: 'visible',
|
|
status: 'inactive',
|
|
liveTerminalCount: 0,
|
|
hasHostSidebarActivity: true
|
|
})
|
|
const retainedPtyOnly = worktree({
|
|
worktreeId: 'retained-pty-only',
|
|
status: 'active',
|
|
liveTerminalCount: 3,
|
|
hasHostSidebarActivity: false
|
|
})
|
|
|
|
expect(
|
|
filterWorktrees(
|
|
[visible, retainedPtyOnly],
|
|
{ filterRepoIds: new Set(), hideSleeping: true, hideDefaultBranch: false },
|
|
''
|
|
)
|
|
).toEqual([visible])
|
|
})
|
|
|
|
it('uses the host-provided main-worktree flag for default branch hiding', () => {
|
|
const main = worktree({
|
|
worktreeId: 'main',
|
|
branch: 'main',
|
|
isMainWorktree: true
|
|
})
|
|
const featureNamedMain = worktree({
|
|
worktreeId: 'feature-main',
|
|
branch: 'main',
|
|
isMainWorktree: false
|
|
})
|
|
|
|
expect(
|
|
filterWorktrees(
|
|
[main, featureNamedMain],
|
|
{ filterRepoIds: new Set(), hideSleeping: false, hideDefaultBranch: true },
|
|
''
|
|
)
|
|
).toEqual([featureNamedMain])
|
|
})
|
|
|
|
it('keeps a sleeping main worktree visible under hide-sleeping by default (#8873)', () => {
|
|
const main = worktree({ worktreeId: 'main', branch: 'main', isMainWorktree: true })
|
|
const feature = worktree({ worktreeId: 'feature', isMainWorktree: false })
|
|
|
|
expect(
|
|
filterWorktrees(
|
|
[main, feature],
|
|
{ filterRepoIds: new Set(), hideSleeping: true, hideDefaultBranch: false },
|
|
''
|
|
)
|
|
).toEqual([main])
|
|
})
|
|
|
|
it('keeps a sleeping folder workspace visible under hide-sleeping', () => {
|
|
const folder = worktree({
|
|
workspaceKind: 'folder-workspace',
|
|
worktreeId: 'folder:workspace-1',
|
|
branch: '',
|
|
isMainWorktree: true
|
|
})
|
|
|
|
expect(
|
|
filterWorktrees(
|
|
[folder],
|
|
{ filterRepoIds: new Set(), hideSleeping: true, hideDefaultBranch: false },
|
|
''
|
|
)
|
|
).toEqual([folder])
|
|
})
|
|
|
|
it('re-hides the sleeping main worktree when the desktop setting is off', () => {
|
|
const main = worktree({ worktreeId: 'main', branch: 'main', isMainWorktree: true })
|
|
|
|
expect(
|
|
filterWorktrees(
|
|
[main],
|
|
{
|
|
filterRepoIds: new Set(),
|
|
hideSleeping: true,
|
|
hideDefaultBranch: false,
|
|
alwaysShowDefaultBranch: false
|
|
},
|
|
''
|
|
)
|
|
).toEqual([])
|
|
})
|
|
|
|
it('falls back to the branch heuristic for hosts that omit isMainWorktree', () => {
|
|
const legacyMain = worktree({ worktreeId: 'legacy-main', branch: 'refs/heads/master' })
|
|
|
|
expect(
|
|
filterWorktrees(
|
|
[legacyMain],
|
|
{ filterRepoIds: new Set(), hideSleeping: true, hideDefaultBranch: false },
|
|
''
|
|
)
|
|
).toEqual([legacyMain])
|
|
})
|
|
|
|
it('keeps a sleeping folder workspace on hosts that omit isMainWorktree', () => {
|
|
// A folder workspace has no branch, so the legacy branch heuristic can never
|
|
// recognise it — without its own arm, #8873 still reproduces on old desktops.
|
|
const legacyFolder = worktree({
|
|
workspaceKind: 'folder-workspace',
|
|
worktreeId: 'folder:legacy-1',
|
|
branch: ''
|
|
})
|
|
|
|
expect(
|
|
filterWorktrees(
|
|
[legacyFolder],
|
|
{ filterRepoIds: new Set(), hideSleeping: true, hideDefaultBranch: false },
|
|
''
|
|
)
|
|
).toEqual([legacyFolder])
|
|
})
|
|
|
|
it('lets hide-default-branch still win over the sleeping exemption', () => {
|
|
const main = worktree({ worktreeId: 'main', branch: 'main', isMainWorktree: true })
|
|
|
|
expect(
|
|
filterWorktrees(
|
|
[main],
|
|
{ filterRepoIds: new Set(), hideSleeping: true, hideDefaultBranch: true },
|
|
''
|
|
)
|
|
).toEqual([])
|
|
})
|
|
|
|
it('keeps folder workspaces when default branch hiding is enabled', () => {
|
|
const folder = worktree({
|
|
workspaceKind: 'folder-workspace',
|
|
worktreeId: 'folder:workspace-1',
|
|
branch: '',
|
|
isMainWorktree: true
|
|
})
|
|
|
|
expect(
|
|
filterWorktrees(
|
|
[folder],
|
|
{ filterRepoIds: new Set(), hideSleeping: false, hideDefaultBranch: true },
|
|
''
|
|
)
|
|
).toEqual([folder])
|
|
})
|
|
})
|
|
|
|
describe('getWorktreeStatus', () => {
|
|
it('uses host sidebar inactivity for the row status dot when available', () => {
|
|
expect(
|
|
getWorktreeStatus(
|
|
worktree({
|
|
status: 'active',
|
|
liveTerminalCount: 3,
|
|
hasHostSidebarActivity: false
|
|
})
|
|
)
|
|
).toBe('inactive')
|
|
})
|
|
|
|
it('marks host sidebar activity active when runtime status has not caught up', () => {
|
|
expect(
|
|
getWorktreeStatus(
|
|
worktree({
|
|
status: 'inactive',
|
|
liveTerminalCount: 0,
|
|
hasHostSidebarActivity: true
|
|
})
|
|
)
|
|
).toBe('active')
|
|
})
|
|
})
|
|
|
|
describe('buildSections', () => {
|
|
it('keys same-id rows by host inside a section', () => {
|
|
const worktreeId = 'repo-1::/work/orca'
|
|
const sections = buildSections(
|
|
[worktree({ worktreeId, hostId: 'local' }), worktree({ worktreeId, hostId: 'ssh:builder' })],
|
|
'name',
|
|
{ filterRepoIds: new Set(), hideSleeping: false, hideDefaultBranch: false },
|
|
'',
|
|
'none',
|
|
new Set()
|
|
)
|
|
const keys = sections[0]?.data.map((item) => item.sectionListKey) ?? []
|
|
|
|
expect(new Set(keys).size).toBe(2)
|
|
expect(keys).toEqual(
|
|
expect.arrayContaining([`all:local|${worktreeId}`, `all:ssh:builder|${worktreeId}`])
|
|
)
|
|
})
|
|
|
|
it('matches desktop Name sort by display name', () => {
|
|
const beta = worktree({ worktreeId: 'beta', displayName: 'Beta', repo: 'aaa' })
|
|
const alpha = worktree({ worktreeId: 'alpha', displayName: 'Alpha', repo: 'zzz' })
|
|
|
|
expect(sortWorktrees([beta, alpha], 'name').map((item) => item.worktreeId)).toEqual([
|
|
'alpha',
|
|
'beta'
|
|
])
|
|
})
|
|
|
|
it('uses desktop manual order ranks in Manual sort mode', () => {
|
|
const low = worktree({ worktreeId: 'low', displayName: 'low', manualOrder: 10 })
|
|
const high = worktree({ worktreeId: 'high', displayName: 'high', manualOrder: 30 })
|
|
const fallback = worktree({ worktreeId: 'fallback', displayName: 'fallback', sortOrder: 20 })
|
|
|
|
const sections = buildSections(
|
|
[low, high, fallback],
|
|
'manual',
|
|
{ filterRepoIds: new Set(), hideSleeping: false, hideDefaultBranch: false },
|
|
'',
|
|
'workspaceStatus',
|
|
new Set(),
|
|
new Map(),
|
|
DEFAULT_MOBILE_WORKSPACE_STATUSES
|
|
)
|
|
|
|
expect(sections[0]?.data.map((worktree) => worktree.worktreeId)).toEqual([
|
|
'high',
|
|
'fallback',
|
|
'low'
|
|
])
|
|
})
|
|
|
|
it('uses desktop display-name tie-breaks in Manual sort mode', () => {
|
|
const zed = worktree({ worktreeId: 'zed', displayName: 'Zed', manualOrder: 10 })
|
|
const alpha = worktree({ worktreeId: 'alpha', displayName: 'Alpha', manualOrder: 10 })
|
|
|
|
expect(sortWorktrees([zed, alpha], 'manual').map((item) => item.worktreeId)).toEqual([
|
|
'alpha',
|
|
'zed'
|
|
])
|
|
})
|
|
|
|
it('uses desktop-persisted smart order before mobile fallback signals', () => {
|
|
const desktopFirst = worktree({
|
|
worktreeId: 'desktop-first',
|
|
displayName: 'desktop-first',
|
|
sortOrder: 30,
|
|
status: 'inactive',
|
|
unread: false,
|
|
lastOutputAt: 1
|
|
})
|
|
const mobileFallbackFirst = worktree({
|
|
worktreeId: 'mobile-fallback-first',
|
|
displayName: 'mobile-fallback-first',
|
|
sortOrder: 10,
|
|
status: 'working',
|
|
unread: true,
|
|
lastOutputAt: 100
|
|
})
|
|
|
|
const sections = buildSections(
|
|
[mobileFallbackFirst, desktopFirst],
|
|
'smart',
|
|
{ filterRepoIds: new Set(), hideSleeping: false, hideDefaultBranch: false },
|
|
'',
|
|
'workspaceStatus',
|
|
new Set(),
|
|
new Map(),
|
|
DEFAULT_MOBILE_WORKSPACE_STATUSES
|
|
)
|
|
|
|
expect(sections.flatMap((section) => section.data.map((item) => item.worktreeId))).toEqual([
|
|
'desktop-first',
|
|
'mobile-fallback-first'
|
|
])
|
|
})
|
|
|
|
it('uses desktop display-name tie-breaks for equal persisted smart ranks', () => {
|
|
const zed = worktree({ worktreeId: 'zed', displayName: 'Zed', sortOrder: 20, unread: true })
|
|
const alpha = worktree({
|
|
worktreeId: 'alpha',
|
|
displayName: 'Alpha',
|
|
sortOrder: 20,
|
|
status: 'working',
|
|
lastOutputAt: 100
|
|
})
|
|
|
|
expect(sortWorktrees([zed, alpha], 'smart').map((item) => item.worktreeId)).toEqual([
|
|
'alpha',
|
|
'zed'
|
|
])
|
|
})
|
|
|
|
it('counts live terminal output as Recent activity for headless serve hosts', () => {
|
|
// Serve hosts only stamp lastActivityAt at creation, so output must rank.
|
|
const streamingOnServe = worktree({
|
|
worktreeId: 'streaming',
|
|
displayName: 'streaming',
|
|
lastActivityAt: 100,
|
|
lastOutputAt: 1_000
|
|
})
|
|
const touchedOnDesktop = worktree({
|
|
worktreeId: 'touched',
|
|
displayName: 'touched',
|
|
lastActivityAt: 200,
|
|
lastOutputAt: 1
|
|
})
|
|
|
|
expect(
|
|
sortWorktrees([touchedOnDesktop, streamingOnServe], 'recent').map((item) => item.worktreeId)
|
|
).toEqual(['streaming', 'touched'])
|
|
})
|
|
|
|
it('keeps desktop activity ranking in Recent when it is newest', () => {
|
|
const newerActivity = worktree({
|
|
worktreeId: 'newer-activity',
|
|
displayName: 'newer',
|
|
lastActivityAt: 2_000,
|
|
lastOutputAt: 1
|
|
})
|
|
const quietOlder = worktree({
|
|
worktreeId: 'quiet-older',
|
|
displayName: 'quiet',
|
|
lastActivityAt: 100,
|
|
lastOutputAt: 150
|
|
})
|
|
|
|
expect(
|
|
sortWorktrees([quietOlder, newerActivity], 'recent').map((item) => item.worktreeId)
|
|
).toEqual(['newer-activity', 'quiet-older'])
|
|
})
|
|
|
|
it('falls back to agent attention order in Smart when no desktop ranks exist', () => {
|
|
// Display names are deliberately reverse-alphabetical to prove status ranks.
|
|
const idle = worktree({ worktreeId: 'idle', displayName: 'Aardvark' })
|
|
const needsPermission = worktree({
|
|
worktreeId: 'needs-permission',
|
|
displayName: 'Zebra',
|
|
status: 'permission'
|
|
})
|
|
const working = worktree({ worktreeId: 'working', displayName: 'Yak', status: 'working' })
|
|
|
|
expect(
|
|
sortWorktrees([idle, working, needsPermission], 'smart').map((item) => item.worktreeId)
|
|
).toEqual(['needs-permission', 'working', 'idle'])
|
|
})
|
|
|
|
it('keeps desktop-ranked rows above unranked attention-fallback rows in Smart', () => {
|
|
const ranked = worktree({ worktreeId: 'ranked', displayName: 'Ranked', sortOrder: 5 })
|
|
const unrankedWorking = worktree({
|
|
worktreeId: 'unranked-working',
|
|
displayName: 'Working',
|
|
status: 'working'
|
|
})
|
|
|
|
expect(
|
|
sortWorktrees([unrankedWorking, ranked], 'smart').map((item) => item.worktreeId)
|
|
).toEqual(['ranked', 'unranked-working'])
|
|
})
|
|
|
|
it('matches desktop Recent create grace for newly-created workspaces', () => {
|
|
const now = 10_000
|
|
const created = worktree({
|
|
worktreeId: 'created',
|
|
displayName: 'created',
|
|
lastActivityAt: 100,
|
|
createdAt: now - 1_000
|
|
})
|
|
const active = worktree({
|
|
worktreeId: 'active',
|
|
displayName: 'active',
|
|
lastActivityAt: now + CREATE_GRACE_MS - 2_000
|
|
})
|
|
|
|
expect(sortWorktrees([active, created], 'recent', now).map((item) => item.worktreeId)).toEqual([
|
|
'created',
|
|
'active'
|
|
])
|
|
})
|
|
|
|
it('matches desktop Repo sort by repo display name then workspace display name', () => {
|
|
const betaAlpha = worktree({
|
|
worktreeId: 'beta-alpha',
|
|
repo: 'Beta Repo',
|
|
displayName: 'Alpha'
|
|
})
|
|
const alphaZed = worktree({
|
|
worktreeId: 'alpha-zed',
|
|
repo: 'Alpha Repo',
|
|
displayName: 'Zed'
|
|
})
|
|
const betaBravo = worktree({
|
|
worktreeId: 'beta-bravo',
|
|
repo: 'Beta Repo',
|
|
displayName: 'Bravo'
|
|
})
|
|
|
|
expect(
|
|
sortWorktrees([betaBravo, betaAlpha, alphaZed], 'repo').map((item) => item.worktreeId)
|
|
).toEqual(['alpha-zed', 'beta-alpha', 'beta-bravo'])
|
|
})
|
|
|
|
it('uses desktop localeCompare semantics for Repo sort names', () => {
|
|
const accentRepo = worktree({
|
|
worktreeId: 'accent-repo',
|
|
repo: 'áb repo',
|
|
displayName: 'Alpha'
|
|
})
|
|
const plainRepo = worktree({
|
|
worktreeId: 'plain-repo',
|
|
repo: 'ab repo',
|
|
displayName: 'Zed'
|
|
})
|
|
|
|
expect(sortWorktrees([accentRepo, plainRepo], 'repo').map((item) => item.worktreeId)).toEqual([
|
|
'plain-repo',
|
|
'accent-repo'
|
|
])
|
|
})
|
|
|
|
it('keeps a desktop-ranked parent and child stack above unrelated active rows', () => {
|
|
const parent = worktree({
|
|
worktreeId: 'parent',
|
|
displayName: 'Add agents to mobile',
|
|
repo: 'orca',
|
|
sortOrder: 30,
|
|
status: 'inactive'
|
|
})
|
|
const child = worktree({
|
|
worktreeId: 'child',
|
|
displayName: 'Agent Session History resume (PR2)',
|
|
repo: 'orca',
|
|
parentWorktreeId: 'parent',
|
|
sortOrder: 20,
|
|
status: 'inactive'
|
|
})
|
|
const unrelatedActive = worktree({
|
|
worktreeId: 'active',
|
|
displayName: 'Overlapping tui output',
|
|
repo: 'orca',
|
|
sortOrder: 10,
|
|
status: 'working',
|
|
unread: true,
|
|
lastOutputAt: 100
|
|
})
|
|
|
|
const sections = buildSections(
|
|
[unrelatedActive, child, parent],
|
|
'smart',
|
|
{ filterRepoIds: new Set(), hideSleeping: false, hideDefaultBranch: false },
|
|
'',
|
|
'repo',
|
|
new Set(),
|
|
new Map([['orca', 'repo-1']]),
|
|
DEFAULT_MOBILE_WORKSPACE_STATUSES
|
|
)
|
|
|
|
expect(sections[0]?.data.map((item) => item.worktreeId)).toEqual(['parent', 'child', 'active'])
|
|
expect(sections[0]?.data.map((item) => item.lineageDepth)).toEqual([0, 1, 0])
|
|
})
|
|
|
|
it('keeps the main workspace first inside repo-grouped sections like desktop', () => {
|
|
const child = worktree({
|
|
worktreeId: 'child',
|
|
displayName: 'Child',
|
|
repo: 'orca',
|
|
sortOrder: 30,
|
|
isMainWorktree: false
|
|
})
|
|
const main = worktree({
|
|
worktreeId: 'main',
|
|
displayName: 'Main',
|
|
repo: 'orca',
|
|
sortOrder: 10,
|
|
isMainWorktree: true
|
|
})
|
|
|
|
const sections = buildSections(
|
|
[child, main],
|
|
'smart',
|
|
{ filterRepoIds: new Set(), hideSleeping: false, hideDefaultBranch: false },
|
|
'',
|
|
'repo',
|
|
new Set(),
|
|
new Map([['orca', 'repo-1']])
|
|
)
|
|
|
|
expect(sections[0]?.data.map((item) => item.worktreeId)).toEqual(['main', 'child'])
|
|
})
|
|
|
|
it('renders empty repo sections from repo placeholders in repo grouping', () => {
|
|
const sections = buildSections(
|
|
[worktree({ repoId: 'repo-1', repo: 'orca' })],
|
|
'manual',
|
|
{ filterRepoIds: new Set(), hideSleeping: false, hideDefaultBranch: false },
|
|
'',
|
|
'repo',
|
|
new Set(),
|
|
new Map([
|
|
['orca', 'repo-1'],
|
|
['zoom-img', 'repo-missing']
|
|
])
|
|
)
|
|
|
|
expect(withoutSectionListKeys(sections)).toEqual([
|
|
{
|
|
key: 'repo:repo-1',
|
|
title: 'orca',
|
|
data: [worktree({ repoId: 'repo-1', repo: 'orca' })]
|
|
},
|
|
{ key: 'repo:repo-missing', title: 'zoom-img', data: [] }
|
|
])
|
|
})
|
|
|
|
it('does not render empty repo sections outside repo grouping', () => {
|
|
const sections = buildSections(
|
|
[],
|
|
'manual',
|
|
{ filterRepoIds: new Set(), hideSleeping: false, hideDefaultBranch: false },
|
|
'',
|
|
'none',
|
|
new Set(),
|
|
new Map([['zoom-img', 'repo-missing']])
|
|
)
|
|
|
|
expect(withoutSectionListKeys(sections)).toEqual([])
|
|
})
|
|
|
|
it('applies repo filters and search to empty repo sections', () => {
|
|
const sections = buildSections(
|
|
[],
|
|
'manual',
|
|
{
|
|
filterRepoIds: new Set(['repo-matching', 'repo-hidden']),
|
|
hideSleeping: false,
|
|
hideDefaultBranch: false
|
|
},
|
|
'zoom',
|
|
'repo',
|
|
new Set(),
|
|
new Map([
|
|
['zoom-img', 'repo-matching'],
|
|
['repo', 'repo-hidden'],
|
|
['zoom-hidden', 'repo-unfiltered']
|
|
])
|
|
)
|
|
|
|
expect(withoutSectionListKeys(sections)).toEqual([
|
|
{ key: 'repo:repo-matching', title: 'zoom-img', data: [] }
|
|
])
|
|
})
|
|
|
|
it('does not add an empty repo section when all of its worktrees are filtered out', () => {
|
|
const sleeping = worktree({
|
|
repoId: 'repo-sleeping',
|
|
repo: 'sleeping-repo',
|
|
hasHostSidebarActivity: false
|
|
})
|
|
const sections = buildSections(
|
|
[sleeping],
|
|
'manual',
|
|
{ filterRepoIds: new Set(), hideSleeping: true, hideDefaultBranch: false },
|
|
'',
|
|
'repo',
|
|
new Set(),
|
|
new Map([
|
|
['sleeping-repo', 'repo-sleeping'],
|
|
['empty-repo', 'repo-empty']
|
|
])
|
|
)
|
|
|
|
expect(withoutSectionListKeys(sections)).toEqual([
|
|
{ key: 'repo:repo-empty', title: 'empty-repo', data: [] }
|
|
])
|
|
})
|
|
|
|
it('groups by desktop workspace status labels and order', () => {
|
|
const review = worktree({
|
|
worktreeId: 'review',
|
|
workspaceStatus: 'in-review',
|
|
status: 'active'
|
|
})
|
|
const progress = worktree({
|
|
worktreeId: 'progress',
|
|
workspaceStatus: 'in-progress',
|
|
status: 'working'
|
|
})
|
|
|
|
const sections = buildSections(
|
|
[progress, review],
|
|
'manual',
|
|
{ filterRepoIds: new Set(), hideSleeping: false, hideDefaultBranch: false },
|
|
'',
|
|
'workspaceStatus',
|
|
new Set(),
|
|
new Map(),
|
|
DEFAULT_MOBILE_WORKSPACE_STATUSES
|
|
)
|
|
|
|
expect(sections.map((section) => ({ key: section.key, title: section.title }))).toEqual([
|
|
{ key: 'workspace-status:in-review', title: 'In review' },
|
|
{ key: 'workspace-status:in-progress', title: 'In progress' }
|
|
])
|
|
})
|
|
|
|
it('falls back to the default status catalog when desktop sends none', () => {
|
|
const progress = worktree({
|
|
worktreeId: 'progress',
|
|
workspaceStatus: 'in-progress',
|
|
status: 'working'
|
|
})
|
|
|
|
const sections = buildSections(
|
|
[progress],
|
|
'manual',
|
|
{ filterRepoIds: new Set(), hideSleeping: false, hideDefaultBranch: false },
|
|
'',
|
|
'workspaceStatus',
|
|
new Set(),
|
|
new Map(),
|
|
[]
|
|
)
|
|
|
|
expect(sections.map((section) => ({ key: section.key, title: section.title }))).toEqual([
|
|
{ key: 'workspace-status:in-progress', title: 'In progress' }
|
|
])
|
|
expect(sections[0]?.data.map((worktree) => worktree.worktreeId)).toEqual(['progress'])
|
|
})
|
|
|
|
it('keeps pinned worktrees in their canonical status group like desktop', () => {
|
|
const pinned = worktree({
|
|
worktreeId: 'pinned',
|
|
workspaceStatus: 'in-progress',
|
|
isPinned: true
|
|
})
|
|
|
|
const sections = buildSections(
|
|
[pinned],
|
|
'manual',
|
|
{ filterRepoIds: new Set(), hideSleeping: false, hideDefaultBranch: false },
|
|
'',
|
|
'workspaceStatus',
|
|
new Set(),
|
|
new Map(),
|
|
DEFAULT_MOBILE_WORKSPACE_STATUSES
|
|
)
|
|
|
|
expect(withoutSectionListKeys(sections)).toEqual([
|
|
{ key: 'pinned', title: 'Pinned', icon: 'pin', data: [pinned] },
|
|
{ key: 'workspace-status:in-progress', title: 'In progress', data: [pinned] }
|
|
])
|
|
})
|
|
|
|
it('renders one sorted All section when grouping is off like desktop', () => {
|
|
const inactiveFirst = worktree({
|
|
worktreeId: 'inactive-first',
|
|
displayName: 'inactive-first',
|
|
manualOrder: 30,
|
|
status: 'inactive'
|
|
})
|
|
const activeSecond = worktree({
|
|
worktreeId: 'active-second',
|
|
displayName: 'active-second',
|
|
manualOrder: 10,
|
|
status: 'working'
|
|
})
|
|
|
|
const sections = buildSections(
|
|
[activeSecond, inactiveFirst],
|
|
'manual',
|
|
{ filterRepoIds: new Set(), hideSleeping: false, hideDefaultBranch: false },
|
|
'',
|
|
'none',
|
|
new Set(),
|
|
new Map(),
|
|
DEFAULT_MOBILE_WORKSPACE_STATUSES
|
|
)
|
|
|
|
expect(sections.map((section) => section.key)).toEqual(['all'])
|
|
expect(sections[0]?.data.map((worktree) => worktree.worktreeId)).toEqual([
|
|
'inactive-first',
|
|
'active-second'
|
|
])
|
|
})
|
|
|
|
it('nests child workspaces under visible parents in grouped sections', () => {
|
|
const parent = worktree({
|
|
worktreeId: 'parent',
|
|
displayName: 'parent',
|
|
workspaceStatus: 'in-progress'
|
|
})
|
|
const child = worktree({
|
|
worktreeId: 'child',
|
|
displayName: 'child',
|
|
parentWorktreeId: 'parent',
|
|
workspaceStatus: 'in-progress'
|
|
})
|
|
|
|
const sections = buildSections(
|
|
[child, parent],
|
|
'manual',
|
|
{ filterRepoIds: new Set(), hideSleeping: false, hideDefaultBranch: false },
|
|
'',
|
|
'workspaceStatus',
|
|
new Set(),
|
|
new Map(),
|
|
DEFAULT_MOBILE_WORKSPACE_STATUSES
|
|
)
|
|
|
|
expect(sections[0]?.data.map((worktree) => worktree.worktreeId)).toEqual(['parent', 'child'])
|
|
expect(sections[0]?.data.map((worktree) => worktree.lineageDepth)).toEqual([0, 1])
|
|
})
|
|
|
|
it('collapses child workspaces under lineage parent rows', () => {
|
|
const parent = worktree({
|
|
worktreeId: 'parent',
|
|
displayName: 'parent',
|
|
workspaceStatus: 'in-progress'
|
|
})
|
|
const child = worktree({
|
|
worktreeId: 'child',
|
|
displayName: 'child',
|
|
parentWorktreeId: 'parent',
|
|
workspaceStatus: 'in-progress'
|
|
})
|
|
|
|
const sections = buildSections(
|
|
[child, parent],
|
|
'manual',
|
|
{ filterRepoIds: new Set(), hideSleeping: false, hideDefaultBranch: false },
|
|
'',
|
|
'workspaceStatus',
|
|
new Set(),
|
|
new Map(),
|
|
DEFAULT_MOBILE_WORKSPACE_STATUSES,
|
|
new Set([getMobileWorkspaceLineageGroupKey(parent)])
|
|
)
|
|
|
|
expect(sections[0]?.data.map((worktree) => worktree.worktreeId)).toEqual(['parent'])
|
|
expect(sections[0]?.data[0]?.lineageChildCount).toBe(1)
|
|
expect(sections[0]?.data[0]?.lineageCollapsed).toBe(true)
|
|
})
|
|
})
|