Files
orca/src/shared/mobile-markdown-document.test.ts
T
Jinwoo Hong fe34acda3b fix(mobile): truncate oversize markdown reads instead of failing them (#23676)
* fix(mobile): truncate oversize markdown reads instead of failing them

The desktop bridge refused markdown over a private 512 KiB cap with
file_too_large, which reached the phone as a generic runtime_error that
the reader discarded, so a 632 KB file showed "Couldn't load markdown".
Reads now return a UTF-8-boundary prefix under one shared 2 MiB budget,
marked truncated with the full byteLength and read-only. The phone shows
the truncation like file tabs do and maps refusal codes to real copy, so
an older desktop's refusal reads "File too large for mobile preview".

Claude-Session: https://claude.ai/code/session_01JNnE9qzUZMMnqpZWCqM3nb

* test(mobile): pin that a truncated markdown read is never editable

The read budget sits above the edit budget, so every truncated document is
already read-only as file_too_large. Pin that ordering so a future budget
change cannot make a prefix editable, and name the constant as the markdown
preview budget, separate from the file preview's own.

Claude-Session: https://claude.ai/code/session_01JNnE9qzUZMMnqpZWCqM3nb

* test(mobile): refuse saves from a truncated markdown read

A phone holding a truncated prefix must not write it back; the 256 KiB
save guard refuses it as file_too_large before any version check. The
shared budget test shrinks to the ordering it pins and names the bridge
test as the behavioural pin.

Claude-Session: https://claude.ai/code/session_01JNnE9qzUZMMnqpZWCqM3nb

* refactor(mobile): tighten markdown truncation types and measure once

The read truncation measures the document once. The disk fallback drops
its truncated-only read-only text, which the status line never showed,
and both truncation fields are optional there. A markdown doc's flag is
only ever true, and the schema comment names the hook, not line numbers.

Claude-Session: https://claude.ai/code/session_01JNnE9qzUZMMnqpZWCqM3nb

* docs(mobile): drop a stale disk-fallback comment

Claude-Session: https://claude.ai/code/session_01JNnE9qzUZMMnqpZWCqM3nb

* chore: retrigger CI after #23675 landed on main

Claude-Session: https://claude.ai/code/session_01JNnE9qzUZMMnqpZWCqM3nb
2026-09-28 16:58:15 -04:00

55 lines
1.8 KiB
TypeScript

import { describe, expect, it, vi } from 'vitest'
import {
hashMarkdownContent,
isMarkdownContentByteLengthOverLimit,
MOBILE_MARKDOWN_EDIT_MAX_BYTES,
MOBILE_MARKDOWN_READ_MAX_BYTES,
utf8ByteLength
} from './mobile-markdown-document'
describe('mobile markdown document byte accounting', () => {
it('measures UTF-8 bytes across multibyte code points', () => {
expect(utf8ByteLength('aé中😀')).toBe(10)
})
it('keeps markdown content limit checks bounded for oversized multibyte text', () => {
const oversized = '😀'.repeat(Math.floor(MOBILE_MARKDOWN_EDIT_MAX_BYTES / 4) + 1)
const accepted = '😀'.repeat(Math.floor(MOBILE_MARKDOWN_EDIT_MAX_BYTES / 4))
expect(isMarkdownContentByteLengthOverLimit(oversized, MOBILE_MARKDOWN_EDIT_MAX_BYTES)).toBe(
true
)
expect(isMarkdownContentByteLengthOverLimit(accepted, MOBILE_MARKDOWN_EDIT_MAX_BYTES)).toBe(
false
)
})
it('does not depend on TextEncoder for byte length checks', () => {
const OriginalTextEncoder = globalThis.TextEncoder
vi.stubGlobal(
'TextEncoder',
class {
encode(): Uint8Array {
throw new Error('full-buffer encoding should not be used')
}
}
)
try {
expect(utf8ByteLength('😀')).toBe(4)
expect(isMarkdownContentByteLengthOverLimit('😀', 3)).toBe(true)
} finally {
vi.stubGlobal('TextEncoder', OriginalTextEncoder)
}
})
it('keeps content hashes prefixed with exact byte length', () => {
expect(hashMarkdownContent('😀')).toMatch(/^content:4:/)
})
it('keeps the read budget above the edit budget', () => {
// Behavioural pin for truncated => not editable: the bridge's over-budget read test.
expect(MOBILE_MARKDOWN_READ_MAX_BYTES).toBeGreaterThan(MOBILE_MARKDOWN_EDIT_MAX_BYTES)
})
})