Files
orca/src/shared/worktree/removal-force-classification.test.ts
T
Neil 583ab1601b refactor(shared): group worktree, github, and linear modules into folders (#14437)
`src/shared` is a flat directory of ~1,150 entries. The worktree, github, and
linear domains accounted for 71 of them, so finding the module you wanted meant
scanning a wall of same-prefixed filenames.

Move each domain into its own folder and drop the now-redundant prefix:

    src/shared/github-pr-types.ts    -> src/shared/github/pull-request-types.ts
    src/shared/worktree-id.ts        -> src/shared/worktree/id.ts
    src/shared/linear-links.ts       -> src/shared/linear/links.ts

This follows the existing `network/` and `new-workspace/` convention in the
same directory, which also drop the prefix inside the folder.

Whole clusters move, including tests. Foldering only part of a domain would be
worse than flat: a reader would have to check both `github/` and the flat
directory, and `github-auth-types.ts` / `github-project-types.ts` are type
modules that belong with the rest. No files with these prefixes remain flat.

Import specifiers were rewritten by resolving each one to an absolute path and
recomputing it, not by string substitution, so the `@/../../shared/...` alias
forms are handled correctly. 501 specifiers across 298 files.

Two things `tsc` cannot catch, handled explicitly:

- `github-project-types.ts` carries its own `max-lines` bypass, so its baseline
  entry is REPOINTED to the new path rather than pruned. Pruning would drop the
  bypass and then flag the new path as a fresh violation. Ratchet stays at 345.
- `mobile/` is outside `pnpm typecheck` and cannot be typechecked here
  (`mobile/node_modules` is empty). Instead every relative specifier in the repo
  was resolved against the filesystem: 174 unresolved before this change and 174
  after — identical, so nothing broke in mobile either.

The pinned `tests/e2e/.cross-version-checkouts` fixtures are deliberately NOT
rewritten; they are a snapshot of an older release and still reference the old
paths.

Verified: cold `tsc --noEmit` green on node, cli, and web (buildinfo deleted
first — these projects are `composite: true` and reuse stale caches).
2026-08-13 20:44:16 -07:00

70 lines
3.1 KiB
TypeScript

import { describe, expect, it } from 'vitest'
import {
classifyWorktreeForceDeleteReason,
isProvenLivePtyRemovalError,
UNSTOPPED_PTY_REMOVAL_PREFIX,
WORKTREE_TEARDOWN_FORCE_HINT,
WORKTREE_TEARDOWN_TIMEOUT_PREFIX
} from './removal'
const liveError = `${UNSTOPPED_PTY_REMOVAL_PREFIX} repo-1::/w — still live: term_a. ${WORKTREE_TEARDOWN_FORCE_HINT}`
const unverifiableError = `${UNSTOPPED_PTY_REMOVAL_PREFIX} repo-1::/w — could not verify these exited: term_a (daemon socket closed). ${WORKTREE_TEARDOWN_FORCE_HINT}`
// Why (#11960): the desktop Force Delete button renders only when this classifier
// returns a reason. The PTY-teardown error tells the user to force-delete, so an
// unclassified message leaves them reading advice they cannot act on.
describe('classifyWorktreeForceDeleteReason for unstopped PTYs', () => {
it('offers force for a PTY that is still live', () => {
expect(classifyWorktreeForceDeleteReason(liveError)).toBe('unstopped-pty')
})
it('offers force when the stop could not be verified', () => {
expect(classifyWorktreeForceDeleteReason(unverifiableError)).toBe('unstopped-pty')
})
// Why (#11960): the ordinary delete confirmation already passes force:true to skip
// the dirty-file prompt. If that suppressed the offer, the most common desktop
// delete would hit the gate with no Force Delete button anywhere — the original
// dead end, restored.
it('still offers force when the failed attempt only set force', () => {
expect(classifyWorktreeForceDeleteReason(liveError, true)).toBe('unstopped-pty')
})
it('does not re-offer force once the waiver itself was already used', () => {
expect(classifyWorktreeForceDeleteReason(liveError, true, true)).toBeNull()
expect(classifyWorktreeForceDeleteReason(liveError, false, true)).toBeNull()
})
// Why: a wedged provider rejects the sweep before any per-PTY verdict exists, so the
// failure arrives worded as a teardown timeout. The waiver clears it exactly like an
// unproven stop, so leaving it unclassified hid the button for the wedge #11960 targeted.
it('offers force when the teardown sweep itself timed out', () => {
expect(
classifyWorktreeForceDeleteReason(
`${WORKTREE_TEARDOWN_TIMEOUT_PREFIX} repo-1::/w. ${WORKTREE_TEARDOWN_FORCE_HINT}`
)
).toBe('unstopped-pty')
})
it('leaves unrelated failures unclassified', () => {
expect(classifyWorktreeForceDeleteReason('some other failure')).toBeNull()
})
})
// The live verdict escalates the delete toast to "Force Delete will kill them", so it must
// come from Orca's own detail — not from the worktree id, which is a user-chosen path.
describe('isProvenLivePtyRemovalError', () => {
it('reads the verdict Orca actually recorded', () => {
expect(isProvenLivePtyRemovalError(liveError)).toBe(true)
expect(isProvenLivePtyRemovalError(unverifiableError)).toBe(false)
})
it('does not let a worktree path spell out a live verdict', () => {
expect(
isProvenLivePtyRemovalError(
`${UNSTOPPED_PTY_REMOVAL_PREFIX} repo-1::/Users/dev/still live: notes — could not verify these exited: term_a (daemon socket closed)`
)
).toBe(false)
})
})