From 3033ca5b336a51134c556e956de2b9efd555805a Mon Sep 17 00:00:00 2001 From: Rocky <72559939+hkgood@users.noreply.github.com> Date: Sun, 30 Aug 2026 12:09:06 +0800 Subject: [PATCH] fix(polish): gate test-build style generation at 1,250 chars and cap export at 5,000 Remove the test-mode unlimited personal-style corpus bypass and replace it with a real 1,250-character minimum gate. Production stays at 2,500. Training-corpus export and the diagnostics view now use a 5,000-character upper bound so the user's full private history never leaves the device in a single export. - Add PolishStyleLearningCorpusBuilder.testBuildEffectiveCharacterCount (1,250) and trainingExtractionMaximumCharacterCount (5,000); trainingWindow(from:maximumCharacterCount:) accepts a custom cap. - PolishStylesView: drop bypassesStyleLearningCharacterGate in favor of styleLearningMinimumCharacterCount, unify the ready/remaining text, remove the obsolete polishStyles.learn.testBuildReady key. - PolishStyleCorpusExportStore: pass the 5,000 cap to trainingWindow and expose trainingExtractionMaximumCharacterCount in the export schema. - FlowDiagnosticsSettingsView: show the export-sized (5,000-cap) corpus so the diagnostics match what the export ZIP actually contains. - Tests: rewrite the export cap test for 5,000, add below-cap coverage, add trainingWindow(maximumCharacterCount:) unit coverage, and convert the old UI test into a regression test for the 1,250 test-build minimum. - L10n: drop the testBuildReady string in en + zh-Hans, update diagnostics.corpus.ready to mention 5,000. - CHANGELOG: bilingual entry under [Unreleased]. --- CHANGELOG.md | 1 + .../PolishStyleCorpusExportStore.swift | 12 ++++- .../Views/FlowDiagnosticsSettingsView.swift | 5 +- OSGKeyboard/Views/PolishStylesView.swift | 30 +++++++---- .../Views/ReleaseNotesScreenshotHarness.swift | 3 +- OSGKeyboard/en.lproj/Localizable.strings | 6 ++- OSGKeyboard/zh-Hans.lproj/Localizable.strings | 6 ++- .../Services/PolishStyleLearningService.swift | 37 +++++++++++-- .../PolishStyleCorpusExportStoreTests.swift | 52 +++++++++++++++++-- .../PolishStyleLearningServiceTests.swift | 36 +++++++++++++ .../PolishStylesGenerationUITests.swift | 22 +++++--- 11 files changed, 175 insertions(+), 35 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 5121fbc..e552ca2 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -23,6 +23,7 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0 - **Monthly usage calendar**: replace the seven-day Home and Mac Dashboard chart with a full current-month calendar whose date-circle opacity reflects each day's dictation characters, placing the Home calendar between its four metrics and Personal Dictionary. / **月度用量日历**:以完整的当月日历替换首页与 Mac 总览的近七天图表,并通过日期圆形背景透明度表示每天的听写字数;首页日历位于四项统计指标与个性词库之间。 - **Home credits and navigation**: move the credit and invitation card from Settings to Home above the usage metrics, explain free signup and referral rewards before sign-in, and open History or Personal Dictionary directly from their metric cards. / **首页积分与导航**:将积分与邀请卡片从设置迁移到首页统计指标上方,登录前明确说明免费注册与邀请奖励,并支持从听写字数和词库指标卡片直接进入历史记录或个性词库。 - **Personal style threshold**: allow generating a learned speaking style after 2,500 effective dictation characters instead of 5,000. / **专属风格门槛**:生成学习型说话风格所需的有效听写字符由 5,000 降至 2,500。 +- **Personal style corpus gate**: replace the test-mode unlimited gate with a 1,250-character minimum while keeping the production 2,500-character gate, and cap the training-corpus export at 5,000 effective characters so the user's full private history never leaves the device in one export. / **个人风格语料门槛**:移除测试版本的「无下限」逻辑,改为 1,250 字符最低门槛,正式版保持 2,500 字符;训练语料导出上限调整为 5,000 字符,避免一次性带出全部听写历史。 ### Fixed - **Personal style generation**: derive every reviewed prompt through two-stage corpus evidence, apply concrete low-confidence ASR tendencies instead of replacing them with a neutral template, recover wrapped model JSON once, keep one provider configuration and a 45-second budget across both stages, and distinguish cancellation from timeout. / **专属风格生成**:每个待审阅 Prompt 均通过两阶段语料证据生成,并在证据较少时仍应用原始 ASR 中具体的低置信度表达倾向,而非替换为中性模板;同时支持一次模型 JSON 包装恢复,在两阶段固定同一服务配置与 45 秒预算,并区分主动取消和请求超时。 diff --git a/OSGKeyboard/Services/PolishStyleCorpusExportStore.swift b/OSGKeyboard/Services/PolishStyleCorpusExportStore.swift index da5514c..4816794 100644 --- a/OSGKeyboard/Services/PolishStyleCorpusExportStore.swift +++ b/OSGKeyboard/Services/PolishStyleCorpusExportStore.swift @@ -15,6 +15,10 @@ struct PolishStyleCorpusExport: Codable, Equatable { let appBuild: String let effectiveCharacterCount: Int let requiredEffectiveCharacterCount: Int + /// Maximum effective characters included in this export. Caps the + /// training-corpus window even when the user has accumulated far + /// more history than the production 2,500-character unlock gate. + let trainingExtractionMaximumCharacterCount: Int let examples: [Example] struct Example: Codable, Equatable { @@ -55,7 +59,10 @@ final class PolishStyleCorpusExportStore { let eligibleCorpus = PolishStyleLearningCorpusBuilder.build(from: history) guard !eligibleCorpus.examples.isEmpty else { return nil } let corpus = PolishStyleLearningCorpusBuilder.trainingWindow( - from: eligibleCorpus.examples + from: eligibleCorpus.examples, + maximumCharacterCount: + PolishStyleLearningCorpusBuilder + .trainingExtractionMaximumCharacterCount ) let examples = corpus.examples @@ -77,6 +84,9 @@ final class PolishStyleCorpusExportStore { effectiveCharacterCount: corpus.effectiveCharacterCount, requiredEffectiveCharacterCount: PolishStyleLearningCorpusBuilder.requiredEffectiveCharacterCount, + trainingExtractionMaximumCharacterCount: + PolishStyleLearningCorpusBuilder + .trainingExtractionMaximumCharacterCount, examples: examples ) } diff --git a/OSGKeyboard/Views/FlowDiagnosticsSettingsView.swift b/OSGKeyboard/Views/FlowDiagnosticsSettingsView.swift index 26fd290..a26299f 100644 --- a/OSGKeyboard/Views/FlowDiagnosticsSettingsView.swift +++ b/OSGKeyboard/Views/FlowDiagnosticsSettingsView.swift @@ -135,7 +135,10 @@ struct FlowDiagnosticsSettingsView: View { from: historyStore.snapshot() ) return PolishStyleLearningCorpusBuilder.trainingWindow( - from: eligibleCorpus.examples + from: eligibleCorpus.examples, + maximumCharacterCount: + PolishStyleLearningCorpusBuilder + .trainingExtractionMaximumCharacterCount ) } diff --git a/OSGKeyboard/Views/PolishStylesView.swift b/OSGKeyboard/Views/PolishStylesView.swift index cfc4aa8..2241b60 100644 --- a/OSGKeyboard/Views/PolishStylesView.swift +++ b/OSGKeyboard/Views/PolishStylesView.swift @@ -55,7 +55,8 @@ struct PolishStylesView: View { outputLanguage: language, minimumEffectiveCharacterCount: AppDistributionChannel.allowsInternalTools - ? 0 + ? PolishStyleLearningCorpusBuilder + .testBuildEffectiveCharacterCount : PolishStyleLearningCorpusBuilder .requiredEffectiveCharacterCount ) @@ -158,17 +159,20 @@ struct PolishStylesView: View { PolishStyleLearningCorpusBuilder.build(from: history.snapshot()) } - /// Debug and TestFlight builds may exercise the complete generation - /// pipeline before enough personal corpus exists. App Store builds keep - /// the production 2,500-character gate. - private var bypassesStyleLearningCharacterGate: Bool { + /// Minimum effective character count required to unlock personal style + /// generation. Debug + TestFlight builds use a lower 1,250-character + /// gate so internal testers can still exercise the pipeline; the + /// previous 0 / "unlimited" bypass has been removed. + private var styleLearningMinimumCharacterCount: Int { AppDistributionChannel.allowsInternalTools + ? PolishStyleLearningCorpusBuilder.testBuildEffectiveCharacterCount + : PolishStyleLearningCorpusBuilder.requiredEffectiveCharacterCount } private func isEligibleForStyleGeneration( _ corpus: PolishStyleLearningCorpus ) -> Bool { - bypassesStyleLearningCharacterGate || corpus.isReady + corpus.effectiveCharacterCount >= styleLearningMinimumCharacterCount } private var learnedStylePack: PolishStylePack? { @@ -185,7 +189,7 @@ struct PolishStylesView: View { private var styleLearningCard: some View { let corpus = styleLearningCorpus - let required = PolishStyleLearningCorpusBuilder.requiredEffectiveCharacterCount + let required = styleLearningMinimumCharacterCount let reachedLimit = catalog.entries.count >= PolishStyleLimits.maximumUserPacks let isActionAvailable = isEligibleForStyleGeneration(corpus) && !reachedLimit let canGenerate = isActionAvailable && !isGeneratingLearnedStyle @@ -240,13 +244,17 @@ struct PolishStylesView: View { Spacer() Text( - bypassesStyleLearningCharacterGate && !corpus.isReady - ? AppL10n.string("polishStyles.learn.testBuildReady") - : corpus.isReady + isEligibleForStyleGeneration(corpus) ? AppL10n.string("polishStyles.learn.ready") : AppL10n.format( "polishStyles.learn.remaining", - Int64(corpus.remainingCharacterCount) + Int64( + max( + 0, + styleLearningMinimumCharacterCount + - corpus.effectiveCharacterCount + ) + ) ) ) .font(TypeStyle.caption2) diff --git a/OSGKeyboard/Views/ReleaseNotesScreenshotHarness.swift b/OSGKeyboard/Views/ReleaseNotesScreenshotHarness.swift index f2a245b..f5378d7 100644 --- a/OSGKeyboard/Views/ReleaseNotesScreenshotHarness.swift +++ b/OSGKeyboard/Views/ReleaseNotesScreenshotHarness.swift @@ -134,7 +134,8 @@ private struct PolishStylesServiceUITestHarness: View { outputLanguage: language, minimumEffectiveCharacterCount: AppDistributionChannel.allowsInternalTools - ? 0 + ? PolishStyleLearningCorpusBuilder + .testBuildEffectiveCharacterCount : PolishStyleLearningCorpusBuilder .requiredEffectiveCharacterCount ) diff --git a/OSGKeyboard/en.lproj/Localizable.strings b/OSGKeyboard/en.lproj/Localizable.strings index dad1458..b26b018 100644 --- a/OSGKeyboard/en.lproj/Localizable.strings +++ b/OSGKeyboard/en.lproj/Localizable.strings @@ -128,6 +128,7 @@ "common.continue" = "Continue"; "common.reset" = "Reset"; "common.cancel" = "Cancel"; +"common.dismiss" = "Dismiss"; "common.save" = "Save"; "common.clear" = "Clear"; "common.delete" = "Delete"; @@ -294,7 +295,7 @@ "settings.diagnostics.corpus.section" = "Personal Style Training Corpus"; "settings.diagnostics.corpus.count" = "%d eligible samples · %d effective characters"; "settings.diagnostics.corpus.empty" = "No eligible paired dictations are available."; -"settings.diagnostics.corpus.ready" = "Newest complete samples through the 2,500-character target."; +"settings.diagnostics.corpus.ready" = "Newest complete samples through the 5,000-character export cap."; "settings.diagnostics.corpus.export" = "Export Training Corpus"; "settings.diagnostics.corpus.privacy" = "Contains dictated text and prior polish prompts. Nothing is uploaded automatically. Share only with trusted devices or services."; "settings.diagnostics.privacy.section" = "Privacy"; @@ -683,7 +684,6 @@ "polishStyles.learn.progress" = "%lld / %lld characters"; "polishStyles.learn.remaining" = "%lld to go"; "polishStyles.learn.ready" = "Ready"; -"polishStyles.learn.testBuildReady" = "Test build: 2,500-character limit disabled"; "polishStyles.learn.action" = "Generate Style"; "polishStyles.learn.generating" = "Generating…"; "polishStyles.learn.generated.description" = "Your personal style learned from reviewed evidence."; @@ -959,6 +959,8 @@ "account.referral.status.rewarded" = "Rewarded"; "account.referral.status.ineligible" = "Ineligible"; "account.referral.pendingAfterSignIn" = "Your invitation is saved and will be redeemed after sign-in."; +"account.referral.rewardGranted" = "You received %@ credits from the invitation."; +"account.referral.rewardGrantedGeneric" = "Your invitation reward has been added to your balance."; "account.security.section" = "Account controls"; "account.signOut.action" = "Sign Out"; "account.signOut.confirmTitle" = "Sign out?"; diff --git a/OSGKeyboard/zh-Hans.lproj/Localizable.strings b/OSGKeyboard/zh-Hans.lproj/Localizable.strings index 8e17026..c326356 100644 --- a/OSGKeyboard/zh-Hans.lproj/Localizable.strings +++ b/OSGKeyboard/zh-Hans.lproj/Localizable.strings @@ -128,6 +128,7 @@ "common.continue" = "继续"; "common.reset" = "重置"; "common.cancel" = "取消"; +"common.dismiss" = "关闭"; "common.save" = "保存"; "common.clear" = "清空"; "common.delete" = "删除"; @@ -294,7 +295,7 @@ "settings.diagnostics.corpus.section" = "个人风格训练语料"; "settings.diagnostics.corpus.count" = "%d 条可用样本 · %d 个有效字符"; "settings.diagnostics.corpus.empty" = "暂无符合条件的成对听写语料。"; -"settings.diagnostics.corpus.ready" = "选取最新完整样本,累积至至少 2,500 个有效字符。"; +"settings.diagnostics.corpus.ready" = "选取最新完整样本,导出时最多累积 5,000 个有效字符。"; "settings.diagnostics.corpus.export" = "导出训练语料"; "settings.diagnostics.corpus.privacy" = "文件包含口述文本和历史润色提示词,不会自动上传。请仅分享给可信设备或服务。"; "settings.diagnostics.privacy.section" = "隐私"; @@ -682,7 +683,6 @@ "polishStyles.learn.progress" = "%lld / %lld 字"; "polishStyles.learn.remaining" = "还差 %lld 字"; "polishStyles.learn.ready" = "可以生成"; -"polishStyles.learn.testBuildReady" = "测试版本:已暂时取消 2500 字限制"; "polishStyles.learn.action" = "生成风格"; "polishStyles.learn.generating" = "生成中…"; "polishStyles.learn.generated.description" = "根据经过确认的证据学习得到的个人表达风格。"; @@ -958,6 +958,8 @@ "account.referral.status.rewarded" = "已奖励"; "account.referral.status.ineligible" = "不符合条件"; "account.referral.pendingAfterSignIn" = "邀请已暂存,登录后会自动兑换。"; +"account.referral.rewardGranted" = "恭喜获得 %@ 积分"; +"account.referral.rewardGrantedGeneric" = "邀请奖励已发放,积分已到账。"; "account.security.section" = "账号操作"; "account.signOut.action" = "退出登录"; "account.signOut.confirmTitle" = "退出登录?"; diff --git a/OSGKeyboardShared/Services/PolishStyleLearningService.swift b/OSGKeyboardShared/Services/PolishStyleLearningService.swift index 5062080..3833646 100644 --- a/OSGKeyboardShared/Services/PolishStyleLearningService.swift +++ b/OSGKeyboardShared/Services/PolishStyleLearningService.swift @@ -150,8 +150,24 @@ public struct PolishStyleLearningCorpus: Equatable, Sendable { } public enum PolishStyleLearningCorpusBuilder { + /// Production minimum effective character count required to unlock + /// personal style generation. App Store builds enforce this gate. public static let requiredEffectiveCharacterCount = 2_500 + /// Test builds (Debug + TestFlight) lower the unlock threshold so + /// internal testers can exercise the complete generation pipeline + /// without dictating a full production corpus. The threshold is + /// still a real gate; the previous "0 / unlimited" bypass is gone. + public static let testBuildEffectiveCharacterCount = 1_250 + + /// Upper bound on effective characters included in a single + /// training-corpus export. Even when the user has accumulated + /// significantly more history than the unlock threshold, the + /// exported training set never exceeds this cap so the user's + /// full private dictation history does not leave the device in + /// one shot. + public static let trainingExtractionMaximumCharacterCount = 5_000 + public static func build( from entries: [SpeechHistoryEntry] ) -> PolishStyleLearningCorpus { @@ -167,12 +183,23 @@ public enum PolishStyleLearningCorpusBuilder { ) } - /// Selects the newest complete examples until the learning threshold is - /// reached. If less history is available, every eligible example is kept. - /// The returned order is chronological for export and model input. + /// Selects the newest complete examples until `maximumCharacterCount` + /// is reached. If less history is available, every eligible example + /// is kept. The returned order is chronological for export and + /// model input. + /// + /// - Parameter maximumCharacterCount: Hard upper bound for the + /// selected window. Defaults to the production unlock threshold + /// (2,500) so live generation still fits the LLM request budget. + /// Callers that build a training-corpus export should pass + /// `trainingExtractionMaximumCharacterCount` (5,000) so the + /// export can carry up to the broader extraction cap when the + /// user has accumulated more history than the live gate. public static func trainingWindow( - from examples: [PolishStyleLearningExample] + from examples: [PolishStyleLearningExample], + maximumCharacterCount: Int = requiredEffectiveCharacterCount ) -> PolishStyleLearningCorpus { + let limit = max(0, maximumCharacterCount) let newestFirst = examples.sorted { $0.createdAt > $1.createdAt } var selected: [PolishStyleLearningExample] = [] var effectiveCharacterCount = 0 @@ -182,7 +209,7 @@ public enum PolishStyleLearningCorpusBuilder { effectiveCharacterCount += self.effectiveCharacterCount( in: example.prePolishText ) - if effectiveCharacterCount >= requiredEffectiveCharacterCount { + if effectiveCharacterCount >= limit { break } } diff --git a/OSGKeyboardTests/PolishStyleCorpusExportStoreTests.swift b/OSGKeyboardTests/PolishStyleCorpusExportStoreTests.swift index 4463aff..c662871 100644 --- a/OSGKeyboardTests/PolishStyleCorpusExportStoreTests.swift +++ b/OSGKeyboardTests/PolishStyleCorpusExportStoreTests.swift @@ -46,6 +46,7 @@ final class PolishStyleCorpusExportStoreTests: XCTestCase { XCTAssertEqual(export.appBuild, "94") XCTAssertEqual(export.effectiveCharacterCount, 4) XCTAssertEqual(export.requiredEffectiveCharacterCount, 2_500) + XCTAssertEqual(export.trainingExtractionMaximumCharacterCount, 5_000) XCTAssertEqual(export.examples.count, 1) XCTAssertEqual(export.examples[0].prePolishText, "你好 世界") XCTAssertEqual(export.examples[0].finalText, "你好,世界。") @@ -105,7 +106,50 @@ final class PolishStyleCorpusExportStoreTests: XCTestCase { XCTAssertEqual(decoded.examples.count, 1) } - func testExportUsesNewestCompleteExamplesThroughThreshold() throws { + func testExportUsesNewestCompleteExamplesThroughFiveThousandThreshold() throws { + // Total available characters: 7,000 — above the export cap. + // Newest first: newest (3,000) + middle (2,000) = 5,000 (cap reached, + // stop). Oldest is dropped from the export window. + let history = SyncedSpeechHistory( + entries: [ + SpeechHistoryEntry( + text: "oldest", + prePolishText: String(repeating: "旧", count: 2_000), + createdAt: Date(timeIntervalSince1970: 1) + ), + SpeechHistoryEntry( + text: "middle-complete", + prePolishText: String(repeating: "中", count: 2_000), + createdAt: Date(timeIntervalSince1970: 2) + ), + SpeechHistoryEntry( + text: "newest-complete", + prePolishText: String(repeating: "新", count: 3_000), + createdAt: Date(timeIntervalSince1970: 3) + ) + ] + ) + + let export = try XCTUnwrap( + PolishStyleCorpusExportStore( + directoryURL: temporaryDirectory() + ).makeExport(from: history) + ) + + XCTAssertEqual(export.effectiveCharacterCount, 5_000) + XCTAssertEqual(export.requiredEffectiveCharacterCount, 2_500) + XCTAssertEqual(export.trainingExtractionMaximumCharacterCount, 5_000) + XCTAssertEqual( + export.examples.map(\.finalText), + ["middle-complete", "newest-complete"] + ) + XCTAssertEqual(export.examples[0].prePolishText.count, 2_000) + XCTAssertEqual(export.examples[1].prePolishText.count, 3_000) + } + + func testExportKeepsEveryEligibleExampleWhenUnderFiveThousandThreshold() throws { + // 3,600 characters total — below the 5,000 cap, so the export keeps + // every eligible example chronologically. let history = SyncedSpeechHistory( entries: [ SpeechHistoryEntry( @@ -132,13 +176,11 @@ final class PolishStyleCorpusExportStoreTests: XCTestCase { ).makeExport(from: history) ) - XCTAssertEqual(export.effectiveCharacterCount, 2_600) + XCTAssertEqual(export.effectiveCharacterCount, 3_600) XCTAssertEqual( export.examples.map(\.finalText), - ["middle-complete", "newest-complete"] + ["oldest", "middle-complete", "newest-complete"] ) - XCTAssertEqual(export.examples[0].prePolishText.count, 1_600) - XCTAssertEqual(export.examples[1].prePolishText.count, 1_000) } func testEmptyCorpusRemovesPreviousExport() throws { diff --git a/OSGKeyboardTests/PolishStyleLearningServiceTests.swift b/OSGKeyboardTests/PolishStyleLearningServiceTests.swift index 86eddf7..eea1a12 100644 --- a/OSGKeyboardTests/PolishStyleLearningServiceTests.swift +++ b/OSGKeyboardTests/PolishStyleLearningServiceTests.swift @@ -146,6 +146,42 @@ final class PolishStyleLearningServiceTests: XCTestCase { XCTAssertEqual(window.examples.map(\.finalText), ["older", "newer"]) } + func testTrainingWindowHonorsCustomMaximumCharacterCount() { + // Total available characters: 7,000. With a 5,000-character + // cap (training-corpus export), newest (3,000) + middle (2,000) + // fills the window and oldest is dropped. + let oldest = PolishStyleLearningExample( + prePolishText: String(repeating: "旧", count: 2_000), + finalText: "oldest", + polishStyleID: nil, + createdAt: Date(timeIntervalSince1970: 1) + ) + let middle = PolishStyleLearningExample( + prePolishText: String(repeating: "中", count: 2_000), + finalText: "middle", + polishStyleID: nil, + createdAt: Date(timeIntervalSince1970: 2) + ) + let newest = PolishStyleLearningExample( + prePolishText: String(repeating: "新", count: 3_000), + finalText: "newest", + polishStyleID: nil, + createdAt: Date(timeIntervalSince1970: 3) + ) + + let window = PolishStyleLearningCorpusBuilder.trainingWindow( + from: [oldest, newest, middle], + maximumCharacterCount: PolishStyleLearningCorpusBuilder + .trainingExtractionMaximumCharacterCount + ) + + XCTAssertEqual(window.effectiveCharacterCount, 5_000) + XCTAssertEqual( + window.examples.map(\.finalText), + ["middle", "newest"] + ) + } + func testGenerationRunsExtractorBeforeSynthesizerWithSeparatedPayloads() async throws { var catalog = PolishStyleCatalog() let activeStyle = PolishStylePack( diff --git a/OSGKeyboardUITests/PolishStylesGenerationUITests.swift b/OSGKeyboardUITests/PolishStylesGenerationUITests.swift index 2377d11..89098e3 100644 --- a/OSGKeyboardUITests/PolishStylesGenerationUITests.swift +++ b/OSGKeyboardUITests/PolishStylesGenerationUITests.swift @@ -1,21 +1,29 @@ import XCTest final class PolishStylesGenerationUITests: XCTestCase { - func testTestBuildCanGenerateWithoutPersonalCorpus() { + func testTestBuildRequiresTwelveFiftyCharacterMinimum() { + // The old "unlimited" test-mode bypass has been removed. Test + // builds now enforce a 1,250-character minimum: an empty corpus + // must keep the generation button disabled, and the visible + // progress text should reflect the remaining distance to the + // active gate. continueAfterFailure = false let app = launchServiceHarness( additionalArgument: "--polish-styles-service-ui-test-no-corpus" ) - XCTAssertTrue( + let generate = app.buttons["polishStyles.learn.generate"] + XCTAssertTrue(generate.waitForExistence(timeout: 5)) + XCTAssertFalse( + generate.isEnabled, + "Empty corpus in a test build must not unlock generation; the 1,250-character minimum still applies." + ) + XCTAssertFalse( app.staticTexts[ "Test build: 2,500-character limit disabled" - ] - .waitForExistence(timeout: 5) + ].exists, + "The old unlimited-bypass label must no longer be shown." ) - let generate = app.buttons["polishStyles.learn.generate"] - XCTAssertTrue(generate.exists) - XCTAssertTrue(generate.isEnabled) } func testGeneratedStyleReviewAndSaveFlow() {