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].
This commit is contained in:
@@ -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 秒预算,并区分主动取消和请求超时。
|
||||
|
||||
@@ -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
|
||||
)
|
||||
}
|
||||
|
||||
@@ -135,7 +135,10 @@ struct FlowDiagnosticsSettingsView: View {
|
||||
from: historyStore.snapshot()
|
||||
)
|
||||
return PolishStyleLearningCorpusBuilder.trainingWindow(
|
||||
from: eligibleCorpus.examples
|
||||
from: eligibleCorpus.examples,
|
||||
maximumCharacterCount:
|
||||
PolishStyleLearningCorpusBuilder
|
||||
.trainingExtractionMaximumCharacterCount
|
||||
)
|
||||
}
|
||||
|
||||
|
||||
@@ -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)
|
||||
|
||||
@@ -134,7 +134,8 @@ private struct PolishStylesServiceUITestHarness: View {
|
||||
outputLanguage: language,
|
||||
minimumEffectiveCharacterCount:
|
||||
AppDistributionChannel.allowsInternalTools
|
||||
? 0
|
||||
? PolishStyleLearningCorpusBuilder
|
||||
.testBuildEffectiveCharacterCount
|
||||
: PolishStyleLearningCorpusBuilder
|
||||
.requiredEffectiveCharacterCount
|
||||
)
|
||||
|
||||
@@ -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?";
|
||||
|
||||
@@ -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" = "退出登录?";
|
||||
|
||||
@@ -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
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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 {
|
||||
|
||||
@@ -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(
|
||||
|
||||
@@ -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() {
|
||||
|
||||
Reference in New Issue
Block a user