fix(sidebar): stamp lastActivityAt on first worktree discovery (#905)

Co-authored-by: Orca <help@stably.ai>
This commit is contained in:
Brennan Benson 2026-05-05 16:17:51 -07:00 committed by GitHub
parent 55d3a42079
commit 8de43cd3b4
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
10 changed files with 497 additions and 15 deletions

View File

@ -172,6 +172,7 @@ export function mergeWorktree(
isPinned: meta?.isPinned ?? false,
sortOrder: meta?.sortOrder ?? 0,
lastActivityAt: meta?.lastActivityAt ?? 0,
...(meta?.createdAt !== undefined ? { createdAt: meta.createdAt } : {}),
...(git.isSparse === true
? {
sparseDirectories: meta?.sparseDirectories,

View File

@ -204,8 +204,16 @@ export async function createRemoteWorktree(
}
const worktreeId = `${repo.id}::${created.path}`
const now = Date.now()
const metaUpdates: Partial<WorktreeMeta> = {
lastActivityAt: Date.now(),
lastActivityAt: now,
// Why: grants the new worktree a short grace window at the top of the
// Recent sort. During worktree creation (git fetch + add can take several
// seconds) other worktrees get ambient PTY bumps that would otherwise
// leave the newly-created one below them; the Recent comparator uses
// max(lastActivityAt, createdAt + GRACE_MS) to keep it on top until the
// window elapses. See smart-sort.ts `CREATE_GRACE_MS`.
createdAt: now,
...(shouldSetDisplayName(requestedName, branchName, sanitizedName)
? { displayName: requestedName }
: {})
@ -434,11 +442,15 @@ export async function createLocalWorktree(
}
const worktreeId = `${repo.id}::${created.path}`
const now = Date.now()
const metaUpdates: Partial<WorktreeMeta> = {
// Stamp activity so the worktree sorts into its final position
// immediately — prevents scroll-to-reveal racing with a later
// bumpWorktreeActivity that would re-sort the list.
lastActivityAt: Date.now(),
lastActivityAt: now,
// See createRemoteWorktree above: createdAt protects the newly-created
// worktree from ambient PTY bumps in other worktrees for CREATE_GRACE_MS.
createdAt: now,
...(shouldSetDisplayName(effectiveRequestedName, branchName, effectiveSanitizedName)
? { displayName: effectiveRequestedName }
: {}),

View File

@ -378,6 +378,136 @@ describe('registerWorktreeHandlers', () => {
expect(listWorktreesMock).not.toHaveBeenCalled()
})
it('stamps lastActivityAt on first discovery so newly-added worktrees sort to the top of Recent', async () => {
// Why: a worktree that exists on disk but has no persisted WorktreeMeta
// (e.g. a folder repo just added, or a pre-existing worktree in a
// newly-added git repo) would otherwise fall back to `lastActivityAt: 0`
// and rank dead last in the Recent sort.
listWorktreesMock.mockResolvedValue([
{
path: '/workspace/discovered-wt',
head: 'abc123',
branch: 'refs/heads/feature',
isBare: false,
isMainWorktree: false
}
])
store.getWorktreeMeta.mockReturnValue(undefined)
const stampedMeta = { lastActivityAt: 1_700_000_000_000 }
store.setWorktreeMeta.mockReturnValue(stampedMeta)
const listed = (await handlers['worktrees:list'](null, { repoId: 'repo-1' })) as {
id: string
lastActivityAt: number
}[]
expect(store.setWorktreeMeta).toHaveBeenCalledWith(
'repo-1::/workspace/discovered-wt',
expect.objectContaining({ lastActivityAt: expect.any(Number) })
)
expect(listed[0]).toMatchObject({
id: 'repo-1::/workspace/discovered-wt',
lastActivityAt: 1_700_000_000_000
})
})
it('does not re-stamp lastActivityAt when a worktree already has persisted meta', async () => {
// Why: only the *first* discovery should stamp. Re-stamping on every list
// would overwrite real activity and reshuffle the sidebar on refresh.
listWorktreesMock.mockResolvedValue([
{
path: '/workspace/existing-wt',
head: 'abc123',
branch: 'refs/heads/feature',
isBare: false,
isMainWorktree: false
}
])
store.getWorktreeMeta.mockReturnValue({
displayName: '',
comment: '',
linkedIssue: null,
linkedPR: null,
isArchived: false,
isUnread: false,
isPinned: false,
sortOrder: 0,
lastActivityAt: 42
})
const listed = (await handlers['worktrees:list'](null, { repoId: 'repo-1' })) as {
id: string
lastActivityAt: number
}[]
expect(store.setWorktreeMeta).not.toHaveBeenCalled()
expect(listed[0].lastActivityAt).toBe(42)
})
it('stamps lastActivityAt on first discovery for folder-mode repos', async () => {
// Why: folder repos produce a synthetic worktree that flows through the
// same list path. Without the stamp, adding a folder puts its card at the
// bottom of Recent even though the user just added it.
store.getRepos.mockReturnValue([
{
id: 'repo-1',
path: '/workspace/folder',
displayName: 'folder',
badgeColor: '#000',
addedAt: 0,
kind: 'folder'
}
])
store.getRepo.mockReturnValue({
id: 'repo-1',
path: '/workspace/folder',
displayName: 'folder',
badgeColor: '#000',
addedAt: 0,
kind: 'folder'
})
store.getWorktreeMeta.mockReturnValue(undefined)
store.setWorktreeMeta.mockReturnValue({ lastActivityAt: 1_700_000_000_000 })
await handlers['worktrees:list'](null, { repoId: 'repo-1' })
expect(store.setWorktreeMeta).toHaveBeenCalledWith(
'repo-1::/workspace/folder',
expect.objectContaining({ lastActivityAt: expect.any(Number) })
)
})
it('stamps lastActivityAt on first discovery via worktrees:listAll', async () => {
// Why: the stamping logic lives in both worktrees:list and worktrees:listAll.
// Without a dedicated test, a regression in the listAll loop would silently
// bury newly-discovered worktrees from the multi-repo sidebar view.
listWorktreesMock.mockResolvedValue([
{
path: '/workspace/discovered-wt',
head: 'abc123',
branch: 'refs/heads/feature',
isBare: false,
isMainWorktree: false
}
])
store.getWorktreeMeta.mockReturnValue(undefined)
store.setWorktreeMeta.mockReturnValue({ lastActivityAt: 1_700_000_000_000 })
const listed = (await handlers['worktrees:listAll'](null, undefined)) as {
id: string
lastActivityAt: number
}[]
expect(store.setWorktreeMeta).toHaveBeenCalledWith(
'repo-1::/workspace/discovered-wt',
expect.objectContaining({ lastActivityAt: expect.any(Number) })
)
expect(listed[0]).toMatchObject({
id: 'repo-1::/workspace/discovered-wt',
lastActivityAt: 1_700_000_000_000
})
})
it('skips past a suffix that already belongs to a PR after an initial branch conflict', async () => {
// Why: `gh pr list` is network-bound and previously fired on every single
// create, adding 13s to the happy path. We now only probe PR conflicts

View File

@ -41,6 +41,20 @@ import { removeWorktreeSymlinks } from './worktree-symlinks'
import { track } from '../telemetry/client'
import { workspaceSourceSchema, type WorkspaceSource } from '../../shared/telemetry-events'
// Why: worktrees discovered on disk (not created via Orca's UI) have no
// persisted WorktreeMeta, so mergeWorktree falls back to `lastActivityAt: 0`.
// That makes them sort to the bottom of "Recent" even though the user just
// added the repo / folder. Stamp discovery time the first time we see a
// worktree so its very existence counts as a recency signal. Subsequent
// list calls find the persisted meta and skip the stamp.
function resolveWorktreeMetaWithDiscoveryStamp(store: Store, worktreeId: string): WorktreeMeta {
const existing = store.getWorktreeMeta(worktreeId)
if (existing) {
return existing
}
return store.setWorktreeMeta(worktreeId, { lastActivityAt: Date.now() })
}
export function registerWorktreeHandlers(
mainWindow: BrowserWindow,
store: Store,
@ -86,7 +100,7 @@ export function registerWorktreeHandlers(
}
return gitWorktrees.map((gw) => {
const worktreeId = `${repo.id}::${gw.path}`
const meta = store.getWorktreeMeta(worktreeId)
const meta = resolveWorktreeMetaWithDiscoveryStamp(store, worktreeId)
return mergeWorktree(repo.id, gw, meta, repo.displayName)
})
} catch {
@ -128,7 +142,7 @@ export function registerWorktreeHandlers(
}
return gitWorktrees.map((gw) => {
const worktreeId = `${repo.id}::${gw.path}`
const meta = store.getWorktreeMeta(worktreeId)
const meta = resolveWorktreeMetaWithDiscoveryStamp(store, worktreeId)
return mergeWorktree(repo.id, gw, meta, repo.displayName)
})
})

View File

@ -1104,6 +1104,54 @@ describe('OrcaRuntimeService', () => {
expect(activateWorktree).toHaveBeenCalledWith('repo-1', expect.any(String), undefined)
})
it('stamps createdAt alongside lastActivityAt so CLI-created worktrees get the Recent-sort grace window', async () => {
// Why: parity with createLocalWorktree / createRemoteWorktree. Without
// createdAt, ambient PTY bumps in OTHER worktrees during the few seconds
// after creation can push the new worktree below them in Recent sort.
const runtime = new OrcaRuntimeService(store)
runtime.setNotifier({
worktreesChanged: vi.fn(),
reposChanged: vi.fn(),
activateWorktree: vi.fn(),
createTerminal: vi.fn(),
splitTerminal: vi.fn(),
renameTerminal: vi.fn(),
focusTerminal: vi.fn(),
closeTerminal: vi.fn(),
sleepWorktree: vi.fn(),
terminalFitOverrideChanged: vi.fn(),
terminalDriverChanged: vi.fn()
})
runtime.attachWindow(1)
computeWorktreePathMock.mockReturnValue('/tmp/workspaces/runtime-grace')
ensurePathWithinWorkspaceMock.mockReturnValue('/tmp/workspaces/runtime-grace')
vi.mocked(getEffectiveHooks).mockReturnValue({ scripts: {} })
vi.mocked(listWorktrees).mockResolvedValueOnce([
{
path: '/tmp/workspaces/runtime-grace',
head: 'def',
branch: 'runtime-grace',
isBare: false,
isMainWorktree: false
}
])
const before = Date.now()
const result = await runtime.createManagedWorktree({
repoSelector: 'id:repo-1',
name: 'runtime-grace'
})
const after = Date.now()
expect(result.worktree.createdAt).toBeDefined()
expect(result.worktree.createdAt).toBeGreaterThanOrEqual(before)
expect(result.worktree.createdAt).toBeLessThanOrEqual(after)
// Both fields must be stamped from the same `now` so the grace-window
// math (max(lastActivityAt, createdAt + GRACE_MS)) is well-defined.
expect(result.worktree.createdAt).toBe(result.worktree.lastActivityAt)
})
it('skips archive hooks for CLI worktree removal by default', async () => {
const runtime = new OrcaRuntimeService(store)
vi.mocked(getEffectiveHooks).mockReturnValue({

View File

@ -2967,8 +2967,14 @@ export class OrcaRuntimeService {
}
const worktreeId = `${repo.id}::${created.path}`
const now = Date.now()
const meta = this.store.setWorktreeMeta(worktreeId, {
lastActivityAt: Date.now(),
lastActivityAt: now,
// See createRemoteWorktree: createdAt grants the new worktree a grace
// window in Recent sort so ambient PTY bumps in OTHER worktrees can't
// push it down before the user has had a chance to notice it. Smart-sort
// uses max(lastActivityAt, createdAt + CREATE_GRACE_MS).
createdAt: now,
...(shouldSetDisplayName(requestedName, branchName, sanitizedName)
? { displayName: requestedName }
: {}),

View File

@ -1,7 +1,13 @@
/* eslint-disable max-lines */
import { afterEach, describe, expect, it, vi } from 'vitest'
import type { Repo, TerminalTab, Worktree } from '../../../../shared/types'
import { buildWorktreeComparator, computeSmartScore, type SmartSortOverride } from './smart-sort'
import {
buildWorktreeComparator,
computeSmartScore,
CREATE_GRACE_MS,
effectiveRecentActivity,
type SmartSortOverride
} from './smart-sort'
import type { AgentStatusEntry } from '../../../../shared/agent-status-types'
const NOW = new Date('2026-03-27T12:00:00.000Z').getTime()
@ -37,7 +43,8 @@ function makeWorktree(overrides: Partial<Worktree> = {}): Worktree {
isPinned: overrides.isPinned ?? false,
displayName: overrides.displayName ?? overrides.id ?? 'wt-1',
sortOrder: overrides.sortOrder ?? 0,
lastActivityAt: overrides.lastActivityAt ?? 0
lastActivityAt: overrides.lastActivityAt ?? 0,
...(overrides.createdAt !== undefined ? { createdAt: overrides.createdAt } : {})
}
}
@ -511,3 +518,100 @@ describe('buildWorktreeComparator — recent (lastActivityAt)', () => {
expect(worktrees.map((w) => w.id)).toEqual(['fresh-active', 'stale-high-order'])
})
})
describe('effectiveRecentActivity — create-grace floor', () => {
it('returns lastActivityAt when createdAt is absent', () => {
const wt = makeWorktree({ id: 'old', lastActivityAt: 12345 })
expect(effectiveRecentActivity(wt, NOW)).toBe(12345)
})
it('returns createdAt + CREATE_GRACE_MS when grace window exceeds lastActivityAt', () => {
const wt = makeWorktree({ id: 'fresh', lastActivityAt: NOW, createdAt: NOW })
expect(effectiveRecentActivity(wt, NOW)).toBe(NOW + CREATE_GRACE_MS)
})
it('returns lastActivityAt when grace window has elapsed', () => {
const wt = makeWorktree({
id: 'post-grace',
createdAt: NOW - CREATE_GRACE_MS - 60_000,
lastActivityAt: NOW - 1000
})
expect(effectiveRecentActivity(wt, NOW)).toBe(NOW - 1000)
})
it('returns lastActivityAt when real activity has surpassed the grace floor', () => {
// A user who interacted 3 minutes after create has lastActivityAt > createdAt + 3min,
// but createdAt + 5min still wins for the next 2 minutes.
const createdAt = NOW - 3 * 60 * 1000
const wt = makeWorktree({ id: 'used', createdAt, lastActivityAt: NOW - 60_000 })
// createdAt + GRACE_MS = NOW + 2min, which exceeds lastActivityAt (NOW - 1min).
expect(effectiveRecentActivity(wt, NOW)).toBe(createdAt + CREATE_GRACE_MS)
})
it('returns lastActivityAt once the grace window has elapsed even when no other activity has occurred', () => {
// Bug-fix case: a worktree created days ago that was never touched after
// creation. Without the time-bound check, the floor would still apply and
// the worktree would rank as `createdAt + 5min` forever, masking truly
// fresher worktrees.
const createdAt = NOW - CREATE_GRACE_MS - 1
const wt = makeWorktree({ id: 'untouched', createdAt, lastActivityAt: createdAt })
expect(effectiveRecentActivity(wt, NOW)).toBe(createdAt)
})
})
describe('buildWorktreeComparator — recent with createdAt grace window', () => {
it('keeps a newly-created worktree on top even when another worktree bumps lastActivityAt', () => {
// Simulates the bug: user creates a worktree at t=0, then an ambient PTY
// bump on a different worktree lands at t=+100ms. Without the grace
// window, the bumped worktree would outrank the new one by 100ms.
const newWorktree = makeWorktree({
id: 'new',
displayName: 'New',
createdAt: NOW,
lastActivityAt: NOW
})
const bumpedByAmbient = makeWorktree({
id: 'bumped',
displayName: 'Bumped',
lastActivityAt: NOW + 100
})
const worktrees = [bumpedByAmbient, newWorktree]
worktrees.sort(buildWorktreeComparator('recent', null, repoMap, null, NOW))
expect(worktrees.map((w) => w.id)).toEqual(['new', 'bumped'])
})
it('falls through to normal recency once the grace window has elapsed', () => {
const oldCreated = makeWorktree({
id: 'old-created',
displayName: 'Old created',
// Created longer ago than GRACE_MS so the floor has expired.
createdAt: NOW - CREATE_GRACE_MS - 10_000,
lastActivityAt: NOW - 30_000
})
const freshActivity = makeWorktree({
id: 'fresh-activity',
displayName: 'Fresh activity',
// No createdAt (discovered on disk), but has recent real activity.
lastActivityAt: NOW - 1000
})
const worktrees = [oldCreated, freshActivity]
worktrees.sort(buildWorktreeComparator('recent', null, repoMap, null, NOW))
expect(worktrees.map((w) => w.id)).toEqual(['fresh-activity', 'old-created'])
})
it('does not disturb ranking for worktrees without createdAt', () => {
// All existing worktrees (persisted before createdAt field existed) stay
// sorted by lastActivityAt alone.
const alpha = makeWorktree({ id: 'alpha', displayName: 'Alpha', lastActivityAt: 5000 })
const bravo = makeWorktree({ id: 'bravo', displayName: 'Bravo', lastActivityAt: 10_000 })
const worktrees = [alpha, bravo]
worktrees.sort(buildWorktreeComparator('recent', null, repoMap, null, NOW))
expect(worktrees.map((w) => w.id)).toEqual(['bravo', 'alpha'])
})
})

View File

@ -8,6 +8,38 @@ import {
type SortBy = 'name' | 'smart' | 'recent' | 'repo'
// Why: a newly-created worktree's lastActivityAt is stamped at the moment
// createLocalWorktree finishes git + setup-runner prep (often several seconds
// after the user clicked Create). During and after that window, ambient PTY
// bumps on OTHER worktrees (data flush, exit, reconnect) can push the new
// worktree below them in Recent sort. This grace period gives the new
// worktree a floor of `createdAt + CREATE_GRACE_MS` in the Recent comparator
// so it stays on top until the user has had a chance to notice it. 5 min is
// long enough for the user to interact, short enough that steady-state
// ordering resumes quickly.
export const CREATE_GRACE_MS = 5 * 60 * 1000
/**
* Rank a worktree in Recent sort using `lastActivityAt`, but with a floor of
* `createdAt + CREATE_GRACE_MS` *only during* the grace window (i.e. while
* `now < createdAt + CREATE_GRACE_MS`). Once the window has elapsed, returns
* `lastActivityAt` unchanged. Returns `lastActivityAt` unchanged for worktrees
* without `createdAt` (discovered on disk, or persisted before this field
* existed).
*/
export function effectiveRecentActivity(worktree: Worktree, now: number): number {
const { lastActivityAt, createdAt } = worktree
// Why bound by now: a worktree with createdAt set but no subsequent activity
// should not retain artificially-high recency forever; the floor exists to
// absorb the noisy creation window only. Without this bound, a worktree
// created days ago and never touched would keep ranking as if its activity
// were `createdAt + 5min`, masking truly fresher worktrees indefinitely.
if (createdAt === undefined || now >= createdAt + CREATE_GRACE_MS) {
return lastActivityAt
}
return Math.max(lastActivityAt, createdAt + CREATE_GRACE_MS)
}
type PRCacheEntry = { data: object | null; fetchedAt: number }
export type SmartSortOverride = {
worktree: Worktree
@ -276,18 +308,27 @@ export function buildWorktreeComparator(
)
return (
scoreB - scoreA ||
smartB.worktree.lastActivityAt - smartA.worktree.lastActivityAt ||
effectiveRecentActivity(smartB.worktree, now) -
effectiveRecentActivity(smartA.worktree, now) ||
a.displayName.localeCompare(b.displayName)
)
}
case 'recent':
// Why lastActivityAt (not sortOrder): sortOrder is a snapshot of the
// smart-sort ranking that only gets repersisted while the user is in
// "Smart" mode, so it's frozen in Recent mode and ignores new terminal
// events, meta edits, etc. lastActivityAt is the real "recency" signal
// — it's bumped by bumpWorktreeActivity (PTY spawn, background events)
// and by meaningful meta edits (comment, isUnread).
return b.lastActivityAt - a.lastActivityAt || a.displayName.localeCompare(b.displayName)
// Why effectiveRecentActivity (not raw lastActivityAt): newly-created
// worktrees get a CREATE_GRACE_MS floor on top of lastActivityAt so
// ambient PTY bumps in other worktrees don't immediately push them
// down. See CREATE_GRACE_MS above.
//
// Why not sortOrder: sortOrder is a snapshot of the smart-sort
// ranking that only gets repersisted while the user is in "Smart"
// mode, so it's frozen in Recent mode and ignores new terminal
// events, meta edits, etc. lastActivityAt is the real "recency"
// signal — bumped by bumpWorktreeActivity (PTY spawn, background
// events) and by meaningful meta edits (comment, isUnread).
return (
effectiveRecentActivity(b, now) - effectiveRecentActivity(a, now) ||
a.displayName.localeCompare(b.displayName)
)
case 'repo': {
const ra = repoMap.get(a.repoId)?.displayName ?? ''
const rb = repoMap.get(b.repoId)?.displayName ?? ''

View File

@ -101,6 +101,11 @@ export type Worktree = {
isPinned: boolean
sortOrder: number
lastActivityAt: number
/** Set once when Orca creates the worktree. Absent for worktrees discovered
* on disk or persisted before this field existed. Used by the sidebar to
* grant newly-created worktrees a short grace window at the top of Recent,
* immune to ambient PTY-bump reordering in other worktrees. */
createdAt?: number
sparseDirectories?: string[]
sparseBaseRef?: string
/** ID of the saved preset this worktree was created from, if any. Cleared
@ -121,6 +126,8 @@ export type WorktreeMeta = {
isPinned: boolean
sortOrder: number
lastActivityAt: number
/** See {@link Worktree.createdAt}. Persisted to orca-data.json. */
createdAt?: number
sparseDirectories?: string[]
sparseBaseRef?: string
sparsePresetId?: string

View File

@ -0,0 +1,119 @@
/**
* E2E test for newly-added worktrees sorting correctly in "Recent" mode.
*
* Why this exists:
* Before the fix in `src/main/ipc/worktrees.ts`, a worktree that existed
* on disk but had no persisted WorktreeMeta (the case for folder-mode
* repos and pre-existing worktrees discovered when adding a new git repo)
* fell back to `lastActivityAt: 0`. "Recent" sort orders by
* `lastActivityAt` descending, so those worktrees landed dead last
* even though the user had just added them.
*
* The `worktrees:list` / `worktrees:listAll` handlers now stamp
* `lastActivityAt = Date.now()` on first discovery. This test locks that
* behavior in end-to-end.
*/
import { mkdtempSync, mkdirSync, rmSync, writeFileSync } from 'fs'
import os from 'os'
import path from 'path'
import type { Page } from '@stablyai/playwright-test'
import { test, expect } from './helpers/orca-app'
import { waitForSessionReady, waitForActiveWorktree } from './helpers/store'
async function addFolderRepo(page: Page, folderPath: string): Promise<string> {
return page.evaluate(async (p) => {
const store = window.__store
if (!store) {
throw new Error('window.__store is unavailable')
}
// Why: go through the public addNonGitFolder path (not window.api.repos.add
// directly) so the test exercises the same flow the "Add Folder" dialog
// uses. That path fetches worktrees internally, which is what triggers the
// discovery stamp we're asserting about.
const repo = await store.getState().addNonGitFolder(p)
if (!repo) {
throw new Error('addNonGitFolder returned null')
}
return repo.id
}, folderPath)
}
async function readFolderWorktreeLastActivity(page: Page, repoId: string): Promise<number> {
return page.evaluate((id) => {
const store = window.__store
if (!store) {
throw new Error('window.__store is unavailable')
}
const worktree = store.getState().worktreesByRepo[id]?.[0]
if (!worktree) {
throw new Error(`No worktree found for repo ${id}`)
}
return worktree.lastActivityAt
}, repoId)
}
test.describe('Worktree Recent Sort', () => {
// Why: keep fixture tracking scoped to this describe block. Module-level
// shared arrays race if the file ever flips to parallel mode or another
// describe is added.
const createdFolderFixtures: string[] = []
function createFolderFixture(): string {
const dir = mkdtempSync(path.join(os.tmpdir(), 'orca-e2e-folder-'))
createdFolderFixtures.push(dir)
mkdirSync(path.join(dir, 'src'), { recursive: true })
writeFileSync(path.join(dir, 'README.md'), '# folder fixture\n')
return dir
}
test.beforeEach(async ({ orcaPage }) => {
await waitForSessionReady(orcaPage)
await waitForActiveWorktree(orcaPage)
})
test.afterEach(() => {
// Why: mkdtempSync fixtures leak unless we clean them up explicitly —
// matches the mkdtempSync/rmSync pairing used in helpers/orca-app.ts
// and helpers/orca-restart.ts.
while (createdFolderFixtures.length) {
const dir = createdFolderFixtures.pop()
if (dir) {
rmSync(dir, { recursive: true, force: true })
}
}
})
test('stamps lastActivityAt on a newly-added folder repo so it sorts to the top of Recent', async ({
orcaPage
}) => {
const folderPath = createFolderFixture()
const repoId = await addFolderRepo(orcaPage, folderPath)
const lastActivityAt = await readFolderWorktreeLastActivity(orcaPage, repoId)
// Why: the exact failure mode before the fix was `lastActivityAt === 0`
// (the fallback in mergeWorktree when meta is undefined). Asserting
// `> 0` captures that regression precisely without coupling to the
// wall-clock of the main process, which would introduce cross-process
// clock-skew flakiness in CI.
expect(lastActivityAt).toBeGreaterThan(0)
})
test('leaves lastActivityAt stable across repeated list refreshes', async ({ orcaPage }) => {
// Why: the stamp fires only on *first* discovery. Re-fetching must not
// overwrite it, or every sidebar refresh would reshuffle Recent order.
const folderPath = createFolderFixture()
const repoId = await addFolderRepo(orcaPage, folderPath)
const first = await readFolderWorktreeLastActivity(orcaPage, repoId)
await orcaPage.evaluate(async (id) => {
await window.__store?.getState().fetchWorktrees(id)
await window.__store?.getState().fetchWorktrees(id)
}, repoId)
const second = await readFolderWorktreeLastActivity(orcaPage, repoId)
expect(second).toBe(first)
})
})