бейдж упоминаний в чат-листе, прямая навигация по @mention, тап на аватарку → профиль, RequestChats на UIKit
This commit is contained in:
308
RosettaTests/VoiceRecordingParityCheckerTests.swift
Normal file
308
RosettaTests/VoiceRecordingParityCheckerTests.swift
Normal file
@@ -0,0 +1,308 @@
|
||||
import CryptoKit
|
||||
import Foundation
|
||||
import XCTest
|
||||
|
||||
final class VoiceRecordingParityCheckerTests: XCTestCase {
|
||||
|
||||
func testVoiceRecordingParityBaselineHasNoBlockingFindings() throws {
|
||||
let root = URL(fileURLWithPath: #filePath)
|
||||
.deletingLastPathComponent()
|
||||
.deletingLastPathComponent()
|
||||
|
||||
let baselineURL = root.appendingPathComponent("docs/voice-recording-parity-baseline.json")
|
||||
let baselineData = try Data(contentsOf: baselineURL)
|
||||
let baselineAny = try JSONSerialization.jsonObject(with: baselineData, options: [])
|
||||
guard let baseline = baselineAny as? [String: Any] else {
|
||||
XCTFail("invalid baseline format")
|
||||
return
|
||||
}
|
||||
|
||||
var findings: [[String: String]] = []
|
||||
|
||||
let constants = baseline["constants"] as? [[String: Any]] ?? []
|
||||
for spec in constants {
|
||||
let id = spec["id"] as? String ?? "unknown"
|
||||
let severity = spec["severity"] as? String ?? "P1"
|
||||
let file = spec["file"] as? String ?? ""
|
||||
let pattern = spec["pattern"] as? String ?? ""
|
||||
let expected = spec["expected"] as? String ?? ""
|
||||
let rawMatch = spec["raw_match"] as? Bool ?? false
|
||||
|
||||
let rosettaText = try readText(root: root, relativePath: file)
|
||||
let actual = regexCapture(text: rosettaText, pattern: pattern, rawMatch: rawMatch)
|
||||
if actual == nil {
|
||||
findings.append([
|
||||
"severity": severity,
|
||||
"kind": "missing_pattern",
|
||||
"id": id,
|
||||
"evidence": file
|
||||
])
|
||||
} else if let actual, actual != expected {
|
||||
findings.append([
|
||||
"severity": severity,
|
||||
"kind": "value_mismatch",
|
||||
"id": id,
|
||||
"evidence": file,
|
||||
"actual": actual,
|
||||
"expected": expected
|
||||
])
|
||||
}
|
||||
|
||||
if let telegramFile = spec["telegram_file"] as? String,
|
||||
let telegramPattern = spec["telegram_pattern"] as? String {
|
||||
let telegramText = try readText(root: root, relativePath: telegramFile)
|
||||
if regexCapture(text: telegramText, pattern: telegramPattern, rawMatch: true) == nil {
|
||||
findings.append([
|
||||
"severity": severity,
|
||||
"kind": "telegram_reference_missing",
|
||||
"id": id,
|
||||
"evidence": telegramFile
|
||||
])
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
let geometry = baseline["geometry"] as? [[String: Any]] ?? []
|
||||
for spec in geometry {
|
||||
let id = spec["id"] as? String ?? "unknown"
|
||||
let severity = spec["severity"] as? String ?? "P1"
|
||||
let file = spec["file"] as? String ?? ""
|
||||
let pattern = spec["pattern"] as? String ?? ""
|
||||
let expected = spec["expected"] as? String ?? ""
|
||||
let rawMatch = spec["raw_match"] as? Bool ?? false
|
||||
|
||||
let rosettaText = try readText(root: root, relativePath: file)
|
||||
let actual = regexCapture(text: rosettaText, pattern: pattern, rawMatch: rawMatch)
|
||||
if actual == nil {
|
||||
findings.append([
|
||||
"severity": severity,
|
||||
"kind": "missing_pattern",
|
||||
"id": id,
|
||||
"evidence": file
|
||||
])
|
||||
} else if let actual, actual != expected {
|
||||
findings.append([
|
||||
"severity": severity,
|
||||
"kind": "value_mismatch",
|
||||
"id": id,
|
||||
"evidence": file,
|
||||
"actual": actual,
|
||||
"expected": expected
|
||||
])
|
||||
}
|
||||
|
||||
if let telegramFile = spec["telegram_file"] as? String,
|
||||
let telegramPattern = spec["telegram_pattern"] as? String {
|
||||
let telegramText = try readText(root: root, relativePath: telegramFile)
|
||||
if regexCapture(text: telegramText, pattern: telegramPattern, rawMatch: true) == nil {
|
||||
findings.append([
|
||||
"severity": severity,
|
||||
"kind": "telegram_reference_missing",
|
||||
"id": id,
|
||||
"evidence": telegramFile
|
||||
])
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
if let flow = baseline["flow"] as? [String: Any] {
|
||||
let flowSeverity = flow["severity"] as? String ?? "P1"
|
||||
let stateFile = flow["state_file"] as? String ?? ""
|
||||
let expectedStates = flow["expected_states"] as? [String] ?? []
|
||||
let stateText = try readText(root: root, relativePath: stateFile)
|
||||
let actualStates = regexMatches(text: stateText, pattern: "case\\s+([A-Za-z_][A-Za-z0-9_]*)")
|
||||
if actualStates != expectedStates {
|
||||
findings.append([
|
||||
"severity": flowSeverity,
|
||||
"kind": "state_machine_mismatch",
|
||||
"id": "flow_states",
|
||||
"evidence": stateFile
|
||||
])
|
||||
}
|
||||
|
||||
let requiredTransitions = flow["required_transitions"] as? [[String: Any]] ?? []
|
||||
for transition in requiredTransitions {
|
||||
let transitionId = transition["id"] as? String ?? "unknown"
|
||||
let transitionSeverity = transition["severity"] as? String ?? "P1"
|
||||
let transitionFile = transition["file"] as? String ?? ""
|
||||
let snippet = transition["snippet"] as? String ?? ""
|
||||
let transitionText = try readText(root: root, relativePath: transitionFile)
|
||||
if !transitionText.contains(snippet) {
|
||||
findings.append([
|
||||
"severity": transitionSeverity,
|
||||
"kind": "transition_missing",
|
||||
"id": transitionId,
|
||||
"evidence": transitionFile
|
||||
])
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
let accessibility = baseline["accessibility"] as? [[String: Any]] ?? []
|
||||
for spec in accessibility {
|
||||
let id = spec["id"] as? String ?? "unknown"
|
||||
let severity = spec["severity"] as? String ?? "P1"
|
||||
let file = spec["file"] as? String ?? ""
|
||||
let snippet = spec["snippet"] as? String ?? ""
|
||||
let text = try readText(root: root, relativePath: file)
|
||||
if !text.contains(snippet) {
|
||||
findings.append([
|
||||
"severity": severity,
|
||||
"kind": "accessibility_missing",
|
||||
"id": id,
|
||||
"evidence": file
|
||||
])
|
||||
}
|
||||
}
|
||||
|
||||
let animations = baseline["animations"] as? [[String: Any]] ?? []
|
||||
for spec in animations {
|
||||
let id = spec["id"] as? String ?? "unknown"
|
||||
let severity = spec["severity"] as? String ?? "P1"
|
||||
let file = spec["file"] as? String ?? ""
|
||||
let snippet = spec["snippet"] as? String ?? ""
|
||||
let text = try readText(root: root, relativePath: file)
|
||||
if !text.contains(snippet) {
|
||||
findings.append([
|
||||
"severity": severity,
|
||||
"kind": "animation_snippet_missing",
|
||||
"id": id,
|
||||
"evidence": file
|
||||
])
|
||||
}
|
||||
}
|
||||
|
||||
if let assets = baseline["assets"] as? [String: Any] {
|
||||
let imagesets = assets["imagesets"] as? [[String: Any]] ?? []
|
||||
for imageset in imagesets {
|
||||
let assetId = imageset["id"] as? String ?? "unknown"
|
||||
let severity = imageset["severity"] as? String ?? "P1"
|
||||
let path = imageset["path"] as? String ?? ""
|
||||
let files = imageset["files"] as? [[String: Any]] ?? []
|
||||
|
||||
let imagesetURL = root.appendingPathComponent(path)
|
||||
if !FileManager.default.fileExists(atPath: imagesetURL.path) {
|
||||
findings.append([
|
||||
"severity": severity,
|
||||
"kind": "asset_missing",
|
||||
"id": assetId,
|
||||
"evidence": path
|
||||
])
|
||||
continue
|
||||
}
|
||||
|
||||
for fileSpec in files {
|
||||
let fileName = fileSpec["name"] as? String ?? ""
|
||||
let expectedSha = fileSpec["sha256"] as? String ?? ""
|
||||
let fileURL = imagesetURL.appendingPathComponent(fileName)
|
||||
if !FileManager.default.fileExists(atPath: fileURL.path) {
|
||||
findings.append([
|
||||
"severity": severity,
|
||||
"kind": "asset_file_missing",
|
||||
"id": "\(assetId)/\(fileName)",
|
||||
"evidence": path
|
||||
])
|
||||
continue
|
||||
}
|
||||
|
||||
let actualSha = try sha256(fileURL)
|
||||
if actualSha != expectedSha {
|
||||
findings.append([
|
||||
"severity": severity,
|
||||
"kind": "asset_hash_mismatch",
|
||||
"id": "\(assetId)/\(fileName)",
|
||||
"evidence": path
|
||||
])
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
let lottie = assets["lottie"] as? [[String: Any]] ?? []
|
||||
for lottieSpec in lottie {
|
||||
let lottieId = lottieSpec["id"] as? String ?? "unknown"
|
||||
let severity = lottieSpec["severity"] as? String ?? "P1"
|
||||
let path = lottieSpec["path"] as? String ?? ""
|
||||
let expectedSha = lottieSpec["sha256"] as? String ?? ""
|
||||
let lottieURL = root.appendingPathComponent(path)
|
||||
|
||||
if !FileManager.default.fileExists(atPath: lottieURL.path) {
|
||||
findings.append([
|
||||
"severity": severity,
|
||||
"kind": "lottie_missing",
|
||||
"id": lottieId,
|
||||
"evidence": path
|
||||
])
|
||||
continue
|
||||
}
|
||||
|
||||
let actualSha = try sha256(lottieURL)
|
||||
if actualSha != expectedSha {
|
||||
findings.append([
|
||||
"severity": severity,
|
||||
"kind": "lottie_hash_mismatch",
|
||||
"id": lottieId,
|
||||
"evidence": path
|
||||
])
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
let blocking = findings.filter { finding in
|
||||
let severity = finding["severity"] ?? "P3"
|
||||
return severity == "P0" || severity == "P1"
|
||||
}
|
||||
|
||||
if !blocking.isEmpty {
|
||||
let data = try JSONSerialization.data(withJSONObject: blocking, options: [.prettyPrinted, .sortedKeys])
|
||||
let details = String(data: data, encoding: .utf8) ?? "<unprintable>"
|
||||
XCTFail("blocking voice parity findings:\n\(details)")
|
||||
}
|
||||
}
|
||||
|
||||
private func readText(root: URL, relativePath: String) throws -> String {
|
||||
let url = root.appendingPathComponent(relativePath)
|
||||
return try String(contentsOf: url, encoding: .utf8)
|
||||
}
|
||||
|
||||
private func sha256(_ url: URL) throws -> String {
|
||||
let data = try Data(contentsOf: url)
|
||||
let digest = SHA256.hash(data: data)
|
||||
return digest.map { String(format: "%02x", $0) }.joined()
|
||||
}
|
||||
|
||||
private func regexCapture(text: String, pattern: String, rawMatch: Bool) -> String? {
|
||||
guard let regex = try? NSRegularExpression(pattern: pattern, options: []) else {
|
||||
return nil
|
||||
}
|
||||
let nsText = text as NSString
|
||||
let range = NSRange(location: 0, length: nsText.length)
|
||||
guard let match = regex.firstMatch(in: text, options: [], range: range) else {
|
||||
return nil
|
||||
}
|
||||
if rawMatch {
|
||||
return pattern
|
||||
}
|
||||
guard match.numberOfRanges > 1 else {
|
||||
return nil
|
||||
}
|
||||
let captureRange = match.range(at: 1)
|
||||
guard captureRange.location != NSNotFound else {
|
||||
return nil
|
||||
}
|
||||
return nsText.substring(with: captureRange)
|
||||
}
|
||||
|
||||
private func regexMatches(text: String, pattern: String) -> [String] {
|
||||
guard let regex = try? NSRegularExpression(pattern: pattern, options: []) else {
|
||||
return []
|
||||
}
|
||||
let nsText = text as NSString
|
||||
let range = NSRange(location: 0, length: nsText.length)
|
||||
return regex.matches(in: text, options: [], range: range).compactMap { match in
|
||||
guard match.numberOfRanges > 1 else { return nil }
|
||||
let captureRange = match.range(at: 1)
|
||||
guard captureRange.location != NSNotFound else { return nil }
|
||||
return nsText.substring(with: captureRange)
|
||||
}
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user