mirror of
https://github.com/stablyai/orca.git
synced 2026-09-27 00:02:37 +00:00
* Add mobile image-diff previews via shared data-URI builder - Extracts a `buildImageDataUri` helper (src/shared/image-data-uri.ts) shared by the desktop ImageViewer and mobile, so both trim whitespace-wrapped base64 and skip non-previewable mimes (e.g. application/pdf) the same way. - Adds mobile-diff-image-preview.ts to render binary git.diff results (add/modify/ delete) as images instead of falling back to "Binary preview unavailable". - Extracts resolveMobileFileTabDoc to consolidate the session file-tab loading logic (diff/image/html/text) out of the route file for testability. * Fix stale binary image fallback for empty modified diffs and relay reads - mobileDiffImageDataUri now distinguishes a true deletion (modified side absent) from a modify whose binary bytes arrived empty (relay/size-cap cases), returning null instead of the stale pre-change image - readWorkingDiffFile passes the file path to bufferToBlob so relay working-tree reads can detect previewable image extensions instead of always reporting empty binary content - add mobile-file-tab-doc.test.ts covering diff/image/binary/text resolution paths * Regenerate skill bundle manifest for 1.4.144-rc.2 Co-authored-by: Orca <help@stably.ai> * fix(review): trim comments to AGENTS.md's one/two-line why-only rule Comments in mobile-diff-image-preview.ts and mobile-file-tab-doc.ts ran 3-6 lines and narrated mechanism instead of stating only the non-obvious reason, per AGENTS.md's "Code Comments: Document the Why, Briefly" rule. Co-authored-by: Orca <help@stably.ai> * Distinguish read failures from true deletions in binary diff results - Working-tree stat/readFile errors and relay reads previously collapsed onto the same empty-content signal as a genuine deletion, letting previewers fall back to stale original bytes on a failed read. - Add modifiedDeleted/missing flags through status.ts, git-handler-ops, and git-working-file-read so only proven deletions trigger the original-bytes fallback; failed reads now return null. - Tighten buildImageDataUri to accept only image/* mimes instead of special-casing application/pdf. * fix(relay): expect missing:false on index blob maxBuffer overflow readBlobAtIndex now returns a missing flag so staged deletions are distinct from size-capped binary reads; update the overflow test. * Allow opening deleted files to show pre-delete text or image diffs Deleted files can now be opened to view their pre-delete content via git.diff (including images via modifiedDeleted). Only unresolved conflicts remain unopenable. Centralizes the canOpen rule in canOpenMobileGitStatusEntry() to keep opener guards consistent across the mobile source control UI. --------- Co-authored-by: Orca <help@stably.ai>
136 lines
4.3 KiB
TypeScript
136 lines
4.3 KiB
TypeScript
import { describe, expect, expectTypeOf, it } from 'vitest'
|
|
import type { GitStatusResult } from '../../../src/shared/git-status-types'
|
|
import {
|
|
buildMobileSourceControlSections,
|
|
canOpenMobileGitStatusEntry,
|
|
countStagedEntries,
|
|
countUnstagedEntries,
|
|
getStageablePaths,
|
|
getUnstageablePaths,
|
|
isMobileGitDiscardableEntry,
|
|
isMobileGitStageableEntry,
|
|
isMobileGitTransientRefreshError,
|
|
isMobileGitUnavailable,
|
|
type MobileGitStatusEntry,
|
|
type MobileGitStatusResult
|
|
} from './mobile-git-status'
|
|
|
|
const entries: MobileGitStatusEntry[] = [
|
|
{ path: 'b.ts', status: 'modified', area: 'staged' },
|
|
{ path: 'a.ts', status: 'modified', area: 'unstaged' },
|
|
{ path: 'new.ts', status: 'untracked', area: 'untracked' }
|
|
]
|
|
|
|
describe('mobile source control status helpers', () => {
|
|
it('keeps the mobile RPC status type in lockstep with the shared git contract', () => {
|
|
expectTypeOf<MobileGitStatusResult>().toEqualTypeOf<GitStatusResult>()
|
|
})
|
|
|
|
it('builds sections in the mobile source control order', () => {
|
|
const sections = buildMobileSourceControlSections(entries)
|
|
|
|
expect(sections.map((section) => section.title)).toEqual([
|
|
'Changes',
|
|
'Untracked Files',
|
|
'Staged Changes'
|
|
])
|
|
})
|
|
|
|
it('computes actionable path sets', () => {
|
|
expect(countUnstagedEntries(entries)).toBe(2)
|
|
expect(countStagedEntries(entries)).toBe(1)
|
|
expect(getStageablePaths(entries)).toEqual(['a.ts', 'new.ts'])
|
|
expect(getUnstageablePaths(entries)).toEqual(['b.ts'])
|
|
})
|
|
|
|
it('keeps unresolved conflicts out of stage actions', () => {
|
|
const conflictedEntries: MobileGitStatusEntry[] = [
|
|
{ path: 'ready.ts', status: 'modified', area: 'unstaged' },
|
|
{
|
|
path: 'conflicted.ts',
|
|
status: 'modified',
|
|
area: 'unstaged',
|
|
conflictStatus: 'unresolved'
|
|
},
|
|
{
|
|
path: 'resolved.ts',
|
|
status: 'modified',
|
|
area: 'unstaged',
|
|
conflictStatus: 'resolved_locally'
|
|
}
|
|
]
|
|
|
|
expect(getStageablePaths(conflictedEntries)).toEqual(['ready.ts', 'resolved.ts'])
|
|
expect(isMobileGitStageableEntry(conflictedEntries[1])).toBe(false)
|
|
expect(isMobileGitDiscardableEntry(conflictedEntries[1])).toBe(false)
|
|
expect(isMobileGitDiscardableEntry(conflictedEntries[2])).toBe(false)
|
|
})
|
|
|
|
it('allows opening deleted files so pre-delete text/image diffs can load', () => {
|
|
expect(
|
|
canOpenMobileGitStatusEntry({ path: 'deleted.png', status: 'deleted', area: 'unstaged' })
|
|
).toBe(true)
|
|
expect(
|
|
canOpenMobileGitStatusEntry({ path: 'logo.png', status: 'modified', area: 'unstaged' })
|
|
).toBe(true)
|
|
expect(
|
|
canOpenMobileGitStatusEntry({
|
|
path: 'conflict.ts',
|
|
status: 'modified',
|
|
area: 'unstaged',
|
|
conflictStatus: 'unresolved'
|
|
})
|
|
).toBe(false)
|
|
expect(
|
|
canOpenMobileGitStatusEntry({
|
|
path: 'resolved.ts',
|
|
status: 'modified',
|
|
area: 'unstaged',
|
|
conflictStatus: 'resolved_locally'
|
|
})
|
|
).toBe(true)
|
|
})
|
|
|
|
it('sorts entries by desktop-compatible conflict rank, then path', () => {
|
|
const sections = buildMobileSourceControlSections([
|
|
{ path: 'zeta.ts', status: 'modified', area: 'unstaged' },
|
|
{
|
|
path: 'beta.ts',
|
|
status: 'modified',
|
|
area: 'unstaged',
|
|
conflictStatus: 'resolved_locally'
|
|
},
|
|
{
|
|
path: 'alpha.ts',
|
|
status: 'modified',
|
|
area: 'unstaged',
|
|
conflictStatus: 'unresolved'
|
|
},
|
|
{ path: 'aardvark.ts', status: 'added', area: 'unstaged' }
|
|
])
|
|
|
|
expect(sections[0].data.map((entry) => entry.path)).toEqual([
|
|
'alpha.ts',
|
|
'beta.ts',
|
|
'aardvark.ts',
|
|
'zeta.ts'
|
|
])
|
|
})
|
|
|
|
it('recognizes old-desktop unavailable responses', () => {
|
|
expect(isMobileGitUnavailable('forbidden', 'Method is not available to mobile clients')).toBe(
|
|
true
|
|
)
|
|
expect(isMobileGitUnavailable('method_not_found', 'Unknown method')).toBe(true)
|
|
expect(isMobileGitUnavailable('bad_request', 'Missing worktree selector')).toBe(false)
|
|
})
|
|
|
|
it('recognizes transient status refresh aborts', () => {
|
|
expect(isMobileGitTransientRefreshError('runtime_error', 'Aborting')).toBe(true)
|
|
expect(isMobileGitTransientRefreshError('request_aborted', 'request_aborted')).toBe(true)
|
|
expect(isMobileGitTransientRefreshError('runtime_error', 'fatal: not a git repository')).toBe(
|
|
false
|
|
)
|
|
})
|
|
})
|