| | |
| | | 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) |
| | | if brace == -1: |
| | | fail(f"missing function body for: {signature}") |
| | | 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") |
| | | tag_editor = read("TagEditorView.swift") |
| | |
| | | require(data_layer, "static func moveApp(path: String, from sourceTag: String, to targetTag: String, color: Int, copy: Bool) -> TagEditorMutationResult", "moveApp must return a mutation result") |
| | | require(data_layer, "TagDatabase.markCustomMaintainedIfNeeded", "membership changes must mark maintained system tags") |
| | | require(data_layer, "static func reorderTags(_ names: [String]) -> Bool", "tag order persistence must return whether it saved") |
| | | require(data_layer, "guard ProEntitlementPolicy.isUnlocked(.persistentAppSorting) else { return false }", "tag order persistence must be Pro-gated") |
| | | reorder_tags_body = function_body(data_layer, "static func reorderTags(_ names: [String]) -> Bool") |
| | | if "ProEntitlementPolicy.isUnlocked(.persistentAppSorting)" in reorder_tags_body: |
| | | fail("tag list order persistence must be available to free users") |
| | | |
| | | require(tag_editor, "onCustomContainerQuotaExceeded", "TagEditorView must expose quota-blocked callback") |
| | | require(tag_editor, "onCustomContainerQuotaStatusChanged", "TagEditorView must expose quota status refresh callback") |
| | |
| | | require(tag_editor, "guard handleMutationResult(result) else { return }", "TagEditorView must not update local state before a save succeeds") |
| | | require(tag_editor, "blockedCustomContainerQuota", "TagEditorView must surface quota blocks") |
| | | |
| | | require(content, "persistTagOrderIfAllowed()", "ContentView must route tag order save through a Pro gate") |
| | | require(content, "pro.tagSorting.previewToast", "free tag sorting preview toast is missing") |
| | | require(content, "persistTagOrder()", "ContentView must persist tag list order") |
| | | persist_tag_order_body = function_body(content, "private func persistTagOrder()") |
| | | if "proEntitlement.isUnlocked" in persist_tag_order_body or "pro.tagSorting.previewToast" in persist_tag_order_body: |
| | | fail("free users must be able to persist tag list order without a Pro preview toast") |
| | | require(content, "presentCustomContainerQuotaPrompt", "ContentView must show custom container quota prompt") |
| | | require(content, "customContainerQuotaAccessory", "ContentView tag editor must show quota count") |
| | | require(content, "@State private var customContainerQuotaStatus = ProEntitlementPolicy.customContainerQuotaStatus()", "ContentView quota count must be backed by SwiftUI state") |
| | |
| | | require(preferences, "onCustomContainerQuotaStatusChanged: refreshSettingsCustomContainerQuotaStatus", "Settings must refresh quota count after TagEditorView saves") |
| | | require(preferences, "settingsCustomContainerQuotaOverviewStatus = quotaStatus", "Settings scanApps must initialize quota count from the same store") |
| | | require(preferences, 'title: tr("pro.feature.maintainableTagCount")', "Pro comparison must include maintainable tag count row") |
| | | require(preferences, 'title: tr("pro.feature.tagOrderPersistence")', "Pro comparison must include tag order row") |
| | | if 'title: tr("pro.feature.tagOrderPersistence")' in preferences: |
| | | fail("Pro comparison must not list tag list order; it is available to free users") |
| | | require(preferences, 'title: tr("pro.feature.containerAppOrderPersistence")', "Pro comparison must include container app order row") |
| | | |
| | | first_three = [ |
| | | first_two = [ |
| | | preferences.find('title: tr("pro.feature.maintainableTagCount")'), |
| | | preferences.find('title: tr("pro.feature.tagOrderPersistence")'), |
| | | preferences.find('title: tr("pro.feature.containerAppOrderPersistence")'), |
| | | ] |
| | | custom_color_index = preferences.find('title: tr("pro.feature.customTagColors")') |
| | | if any(index == -1 for index in first_three) or custom_color_index == -1: |
| | | if any(index == -1 for index in first_two) or custom_color_index == -1: |
| | | fail("could not locate Pro comparison feature rows") |
| | | if not (first_three[0] < first_three[1] < first_three[2] < custom_color_index): |
| | | fail("new Pro comparison rows must be the first three rows") |
| | | if not (first_two[0] < first_two[1] < custom_color_index): |
| | | fail("Pro comparison rows must begin with tag quota and container app order") |
| | | |
| | | required_l10n = [ |
| | | "pro.feature.customContainerQuota", |
| | |
| | | "pro.customContainers.quotaUnlimitedShort", |
| | | "pro.customContainers.quotaPrompt", |
| | | "pro.customContainers.manageExisting", |
| | | "pro.tagSorting.previewToast", |
| | | "pro.feature.maintainableTagCount", |
| | | "pro.feature.tagOrderPersistence", |
| | | "pro.feature.containerAppOrderPersistence", |
| | | "settings.proStatus.maxSixTags", |
| | | "settings.proStatus.unlimitedTags", |
| | |
| | | if zh_hans.get("settings.proStatus.unlimitedTags") != "✅没有限制": |
| | | fail("zh-Hans unlimited tag status must include the approved leading checkmark") |
| | | |
| | | print("PASS Pro custom container quota QA: quota gate, tag-order preview, Pro table rows, and 29-language keys are wired") |
| | | print("PASS Pro custom container quota QA: quota gate, free tag-order persistence, Pro table rows, and 29-language keys are wired") |
| | | PY |