Files
strukto-ai--mirage/python/tests/resource/gsheets/test_sheet_entry.py
T
bytecii ca89dc5970 fix(parity): github ref default, NAME_MAX byte budget, browser registry onto node's config shape
Two py<->ts divergences, one structural alignment, one swept-and-gated class
of config noise.

1. GitHub `ref` defaulted to the literal "main" in python where TypeScript
   resolves `config.ref ?? repoInfo.default_branch`, so every repository whose
   default branch is something else 404d on the one tree fetch the mount is
   built on and read as empty. `GitHubConfig.ref` is now optional, and
   `ensure_ref` resolves it lazily beside the existing `ensure_default_branch`
   -- python's constructor deliberately does no network. Every reader that
   needs a concrete ref goes through it. `mirage.accessor.github` is fully
   annotated and leaves mypy's untyped-defs allowlist.

2. `sanitize_label` capped characters where NAME_MAX counts bytes, which the
   constant's own comment already spelled out. gdocs/gsheets/gslides rendered
   ~367-byte filenames for a CJK title and gmail/email ~269; ext4 and APFS
   reject those. `sanitize_label`/`sanitizeLabel` grew a byte budget and every
   compose site passes what the date, id and suffix leave. Found alongside: TS
   email's hand-rolled copy of the sanitizer used JS's ASCII-only `\w` (so
   non-ASCII subjects became underscores there and stayed intact in python),
   and three sites re-composed `<subject>__<id>.<ext>` independently of
   readdir, so a search hit could name a path that does not exist.

3. The browser registry hand-rolled `normalizeFields` per entry and cast
   through 7 `*BrowserCtorConfig` interfaces -- hand-written twins of the real
   config types. It now delegates to a `normalizeXConfig` beside each config,
   as node's registry does; the interfaces and 22 double casts are gone.

4. 95 rename entries that only restated `snakeToCamel` deleted across 24
   files, with `scripts/check_normalize_renames.py` and a CI step to keep them
   from returning.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
2026-08-20 02:34:03 -07:00

78 lines
3.1 KiB
Python

# ========= Copyright 2026 @ Strukto.AI All Rights Reserved. =========
# Licensed under the Apache License, Version 2.0 (the "License");
# you may not use this file except in compliance with the License.
# You may obtain a copy of the License at
#
# http://www.apache.org/licenses/LICENSE-2.0
#
# Unless required by applicable law or agreed to in writing, software
# distributed under the License is distributed on an "AS IS" BASIS,
# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
# See the License for the specific language governing permissions and
# limitations under the License.
# ========= Copyright 2026 @ Strukto.AI All Rights Reserved. =========
from mirage.resource.gsheets.sheet_entry import (SheetEntry, make_filename,
sanitize_title)
from mirage.utils.sanitize import NAME_MAX_BYTES, byte_len
# A real Google file id is 44 characters, so this is the fixed overhead a
# title actually has to fit inside.
DOC_ID = "1" * 44
def test_sheet_entry_creation():
entry = SheetEntry(
id="abc123",
name="My Spreadsheet",
modified_time="2026-04-01T12:00:00.000Z",
created_time="2026-03-01T12:00:00.000Z",
owner="user@gmail.com",
owned_by_me=True,
can_edit=True,
filename="My_Spreadsheet__abc123.gsheet.json",
)
assert entry.id == "abc123"
assert entry.owned_by_me is True
assert entry.can_edit is True
def test_sanitize_title_basic():
assert sanitize_title("Hello World") == "Hello_World"
assert sanitize_title("My/Doc: A\\Test") == "My_Doc_A_Test"
assert sanitize_title("") == "Untitled"
def test_make_filename_with_and_without_a_date():
assert make_filename("My Spreadsheet", "abc123",
"2026-03-15T10:00:00Z") == \
"2026-03-15_My_Spreadsheet__abc123.gsheet.json"
assert make_filename("My Spreadsheet",
"abc123") == "My_Spreadsheet__abc123.gsheet.json"
def test_make_filename_fits_name_max_for_a_cjk_title():
# 100 characters of CJK is 300 bytes, which the character budget passed
# untouched: with the date, the id and the suffix the name came to 367
# bytes and ext4/APFS reject it with ENAMETOOLONG.
name = make_filename("会議の記録" * 40, DOC_ID, "2026-08-20T12:00:00Z")
assert byte_len(name) <= NAME_MAX_BYTES
assert name.startswith("2026-08-20_")
assert name.endswith(f"__{DOC_ID}.gsheet.json")
# The cut lands on a character boundary, never mid-sequence.
assert "\ufffd" not in name
def test_make_filename_leaves_an_ascii_title_on_the_char_budget():
name = make_filename("a" * 400, DOC_ID, "")
assert byte_len(name) <= NAME_MAX_BYTES
assert name == f"{'a' * 97}...__{DOC_ID}.gsheet.json"
def test_make_filename_keeps_the_id_when_it_leaves_no_room():
# The title is what gives, never the id: a trimmed id would stop
# addressing the spreadsheet. Same rule as gcal's event filenames.
long_id = "v" * (NAME_MAX_BYTES - 4)
name = make_filename("Some Title", long_id, "")
assert f"__{long_id}.gsheet.json" in name