mirror of
https://github.com/stablyai/orca.git
synced 2026-10-04 00:02:21 +00:00
* 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
55 lines
1.8 KiB
TypeScript
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)
|
|
})
|
|
})
|