#!/usr/bin/env bash
|
set -euo pipefail
|
|
ROOT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")/.." && pwd)"
|
|
python3 - "$ROOT_DIR" <<'PY'
|
import re
|
import sys
|
from pathlib import Path
|
|
root = Path(sys.argv[1])
|
app_dir = root / "Apptag"
|
|
|
def fail(message: str) -> None:
|
raise SystemExit(f"FAIL: {message}")
|
|
|
def read(name: str) -> str:
|
return (app_dir / name).read_text(encoding="utf-8")
|
|
|
def require(source: str, needle: str, message: str) -> None:
|
if needle not in source:
|
fail(message)
|
|
|
def function_body(source: str, signature: str) -> str:
|
start = source.find(signature)
|
if start == -1:
|
fail(f"missing function signature: {signature}")
|
brace = source.find("{", start)
|
depth = 0
|
for index in range(brace, len(source)):
|
char = source[index]
|
if char == "{":
|
depth += 1
|
elif char == "}":
|
depth -= 1
|
if depth == 0:
|
return source[brace + 1:index]
|
fail(f"unterminated function body for: {signature}")
|
|
|
pro = read("ProEntitlement.swift")
|
data_layer = read("DataLayer.swift")
|
content = read("ContentView.swift")
|
english = (app_dir / "Localization" / "en.json").read_text(encoding="utf-8")
|
chinese = (app_dir / "Localization" / "zh-Hans.json").read_text(encoding="utf-8")
|
|
require(pro, "static let freeNoteLimit = 3", "free note limit must be 3")
|
decision_body = function_body(pro, "static func noteSaveDecision(note: String, for path: String, in store: TagDatabase.Store? = nil) -> ProNoteSaveDecision")
|
require(decision_body, "manualNonEmptyNotePaths", "quota must count manual non-empty notes")
|
require(decision_body, "let hasExistingManualNote = manualNotePaths.contains(path)", "editing an existing note must not consume new quota")
|
require(decision_body, "guard manualNotePaths.count < limit else", "new notes beyond quota must be blocked")
|
require(decision_body, "case (true, false):", "deleting an existing note must release quota")
|
|
set_note_body = function_body(data_layer, "static func setAppNote(_ note: String, for path: String) -> ProNoteSaveDecision")
|
require(set_note_body, "ProEntitlementPolicy.noteSaveDecision", "note writes must ask Pro policy")
|
require(set_note_body, "guard case .allow = decision else", "blocked note writes must return before mutation")
|
require(set_note_body, "store.appNotes.removeValue(forKey: path)", "empty note must delete saved note")
|
require(set_note_body, "store.appNotes[path] = limited", "allowed non-empty note must save")
|
|
require(content, "presentProPrompt(for: .unlimitedNotes, noteQuotaStatus: status)", "blocked note save must show Pro prompt")
|
require(content, "noteQuotaHintText(for: context.app)", "editing bubble must show quota hint")
|
require(english, "3 app notes", "English UI must say 3 app notes")
|
require(chinese, "3 个应用备注", "Simplified Chinese UI must say 3 app notes")
|
if re.search(r"5[- ]?(note|app notes)|5 个应用备注|5 条备注", english + chinese):
|
fail("old 5-note copy remains in English or Simplified Chinese")
|
|
print("PASS Pro notes quota QA: free limit is 3, existing edits/deletes are handled, over-quota writes are blocked, and UI copy no longer says 5 notes")
|
PY
|