ca89dc5970
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>
78 lines
3.1 KiB
Python
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
|