Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
10 changes: 9 additions & 1 deletion BitMatchTests/LocalTransferJournalTests.swift
Original file line number Diff line number Diff line change
Expand Up @@ -530,17 +530,24 @@ struct LocalTransferJournalTests {
let id = try journal.enqueue(sourceURL: f.source, destinationURLs: [f.destination], verificationMode: .quick,
cameraSettings: CameraLabelSettings(), reportSettings: ReportPrefs())
try journal.markRunning(id: id)
try journal.finish(id: id, results: [ResultRow(path: "file", status: "✅ Copied", size: 1, checksum: nil, destination: "Backup")],
try journal.finish(id: id, results: [ResultRow(path: "file", status: "✅ Match", size: 1, checksum: nil, destination: "Backup")],
summary: "Done", hadIssues: false)
let record = try #require(journal.records.first)
#expect(record.results.first?.status == "✅ Match")
let csv = try TransferHistoryDocument(record: record, asCSV: true)
let csvText = String(decoding: csv.data, as: UTF8.self)
#expect(csvText.contains("verification,transfer_state,transfer_summary"))
#expect(csvText.contains("\"Quick\",\"issues\""))
#expect(csvText.contains("Quick mode only compares file sizes"))
#expect(csvText.contains("reused,recorded_status"))
#expect(csvText.contains("\"Copied · contents not verified\""))
#expect(csvText.contains("\"✅ Match\""))
let json = try TransferHistoryDocument(record: record, asCSV: false)
let payload = try #require(JSONSerialization.jsonObject(with: json.data) as? [String: Any])
#expect(payload["state"] as? String == "issues")
let exportedRow = try #require((payload["results"] as? [[String: Any]])?.first)
#expect(exportedRow["status"] as? String == "Copied · contents not verified")
#expect(exportedRow["recordedStatus"] as? String == "✅ Match")
#expect(payload["source"] as? String == f.source.path)
#expect(payload["bookmark"] == nil)
#expect(!String(decoding: json.data, as: UTF8.self).contains(record.source.bookmark.base64EncodedString()))
Expand All @@ -566,6 +573,7 @@ struct LocalTransferJournalTests {
#expect(payload["clientName"] as? String == "Studio")
#expect(payload["ascMHLRequested"] as? Bool == true)
#expect((payload["results"] as? [[String: Any]])?.count == 1)
#expect((payload["results"] as? [[String: Any]])?.first?["recordedStatus"] == nil)

let csv = try TransferHistoryDocument(record: record, asCSV: true)
let csvText = String(decoding: csv.data, as: UTF8.self)
Expand Down
12 changes: 12 additions & 0 deletions BitMatchTests/ReportEvidenceBytesTests.swift
Original file line number Diff line number Diff line change
Expand Up @@ -206,6 +206,18 @@ final class ReportEvidenceBytesTests: XCTestCase {
XCTAssertTrue(csv.contains("Source Rereads Avoided,0"), csv)
}

func testCSVDoesNotCallAnUnverifiedCopyVerifiedBecauseItHasAHash() throws {
let row = ResultRow(path: "/card/clip.mov", status: ResultOutcome.copiedUnverified.statusText,
size: 10, checksum: "source-only-hash", destination: "Backup")
let csv = try ReportExporter.makeEnhancedCSV(
results: [row], started: Date(), duration: 1, filesPerSecond: 1,
photographerContext: nil, prefs: ReportPrefs(verificationMode: .standard)
)
let dataRow = try XCTUnwrap(csv.components(separatedBy: "\n").dropFirst().first)
XCTAssertTrue(dataRow.contains("✅ Copied"), dataRow)
XCTAssertFalse(dataRow.contains("Verified"), dataRow)
}

func testJSONUsesPhaseDurationSecondKeysAndQuickOmitsVerify() throws {
var prefs = ReportPrefs()
prefs.verificationMode = .quick
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -526,7 +526,7 @@ public enum EvidenceWriter: Sendable {
let camera = project?.camera ?? ""
let card = project?.card ?? ""
let packagePath = project?.packagePath ?? ""
let verified = isMatchStatus(result.status) && result.checksum?.isEmpty == false && prefs?.verificationMode != .quick
let verified = result.isVerifiedStatus && result.checksum?.isEmpty == false && prefs?.verificationMode != .quick
let details = verified ? "Verified" : result.status

// Per-file times are not measured; the column stays for existing
Expand Down
11 changes: 11 additions & 0 deletions Shared/Core/Models/TransferLibraryPresentation.swift
Original file line number Diff line number Diff line change
@@ -1,6 +1,17 @@
import Foundation
import BitMatchEngine

/// Keep older Quick rows' original status in the journal, but present their
/// successful copy as unverified everywhere a person reads it.
enum TransferHistoryResultPresentation {
static func statusText(for row: ResultRow, mode: VerificationMode) -> String {
if mode == .quick && row.isSuccessStatus {
return "Copied · contents not verified"
}
return row.status
}
}

/// What History shows for each journal record, and which records raise the
/// "review in History" banner on Mac, iPad and iPhone.
/// Pure values, so every platform shows the same state the same way.
Expand Down
40 changes: 35 additions & 5 deletions Shared/Core/Services/TransferHistoryDocument.swift
Original file line number Diff line number Diff line change
Expand Up @@ -18,12 +18,14 @@ struct TransferHistoryDocument: FileDocument {
results: record.results, includeModePlan: record.plannedFileCounts != nil)
if asCSV {
func quote(_ value: String) -> String { "\"" + value.replacingOccurrences(of: "\"", with: "\"\"") + "\"" }
let header = "source,destination,status,bytes,checksum,verification,transfer_state,transfer_summary,project,asc_mhl,client,reused\n"
let header = "source,destination,status,bytes,checksum,verification,transfer_state,transfer_summary,project,asc_mhl,client,reused,recorded_status\n"
let rows = record.results.map { row in
[row.path, row.destinationPath ?? row.destination ?? "", row.status, String(row.size), row.checksum ?? "",
[row.path, row.destinationPath ?? row.destination ?? "",
TransferHistoryResultPresentation.statusText(for: row, mode: record.verificationMode),
String(row.size), row.checksum ?? "",
record.verificationMode.rawValue, record.state.rawValue, record.summary,
record.reportSettings.projectName, record.generateASCMHL ? "requested" : "not requested",
record.reportSettings.clientName, row.wasReused ? "true" : "false"].map(quote).joined(separator: ",")
record.reportSettings.clientName, row.wasReused ? "true" : "false", row.status].map(quote).joined(separator: ",")
}
func pair(_ key: String, _ value: String) -> String { quote(key) + "," + quote(value) }
func detail(_ value: String) -> String {
Expand Down Expand Up @@ -80,7 +82,7 @@ struct TransferHistoryDocument: FileDocument {
let copyDurationSeconds: TimeInterval?
let verifyDurationSeconds: TimeInterval?
let performanceTelemetry: TransferPerformanceTelemetry?
let results: [ResultRow]
let results: [HistoryResult]
}
let encoder = JSONEncoder()
encoder.outputFormatting = [.prettyPrinted, .sortedKeys]
Expand All @@ -96,7 +98,35 @@ struct TransferHistoryDocument: FileDocument {
copyDurationSeconds: record.copyDurationSeconds,
verifyDurationSeconds: record.verifyDurationSeconds,
performanceTelemetry: record.performanceTelemetry,
results: record.results))
results: record.results.map { HistoryResult(row: $0, mode: record.verificationMode) }))
}
}
}

/// Export the interpreted status without changing the saved journal row.
/// `recordedStatus` retains old wording when it could overstate verification.
private struct HistoryResult: Encodable {
let id: UUID
let path: String
let status: String
let recordedStatus: String?
let size: Int64
let checksum: String?
let destination: String?
let destinationPath: String?
let clipIntegrity: ClipIntegrityFinding?
let wasReused: Bool?

init(row: ResultRow, mode: VerificationMode) {
id = row.id
path = row.path
status = TransferHistoryResultPresentation.statusText(for: row, mode: mode)
recordedStatus = status == row.status ? nil : row.status
size = row.size
checksum = row.checksum
destination = row.destination
destinationPath = row.destinationPath
clipIntegrity = row.clipIntegrity
wasReused = row.wasReused ? true : nil
}
}
11 changes: 0 additions & 11 deletions Shared/Views/TransferLibraryView.swift
Original file line number Diff line number Diff line change
Expand Up @@ -353,17 +353,6 @@ struct TransferLibraryView: View {
}
}

/// History keeps the original row status in exports, but Quick's older
/// "Match" wording must not be presented as content verification on screen.
enum TransferHistoryResultPresentation {
static func statusText(for row: ResultRow, mode: VerificationMode) -> String {
if mode == .quick && row.isSuccessStatus {
return "Copied · contents not verified"
}
return row.status
}
}

/// Attaches `.searchable` only when it makes sense, so the field never
/// appears empty over an empty Queue and never grabs focus on its own.
private struct OptionalSearchable: ViewModifier {
Expand Down
Loading