File ▸ Share… stages a board as a zip and hands it to the system share sheet
Duplicate's own posture — a faithful copy, `.git` the sole exclusion, attachments/comments/trash carried verbatim — staged to a temp directory (BoardShareStager, ditto-zipped via DittoZipArchiver) and presented through NSSharingServicePicker (BoardSharePresentation), anchored to the board window's toolbar or its center. Follows Duplicate/Save as Template's flush-then- cancellable-copy sequence under the banner's in-progress row (ShareBoardCommand, AppCommands.swift), menu-validated on focus alone rather than the read-only lock (a share is a read, Print's own posture) with the one carve-out an open inline title editor still needs. WriteOperation gains .shareBoard for the banner vocabulary; 11-command-nexus.md's File menu table gains the row. Claude-Session: https://claude.ai/code/session_014PtZdPwqZuqEDLc6wZMtEy
This commit is contained in:
@@ -0,0 +1,55 @@
|
||||
import Foundation
|
||||
import Testing
|
||||
@testable import Kanban
|
||||
|
||||
/// **The sandbox verification the card's design ruling asks for**: "prefer … `Process`+`/usr/bin/
|
||||
/// ditto` … VERIFY in a sandboxed build".
|
||||
///
|
||||
/// `KanbanTests` is host-application-hosted (`TEST_HOST` in `project.yml` points at the built
|
||||
/// `Lanework.app`), so this suite runs *inside* the real, sandboxed app process —
|
||||
/// `Kanban.entitlements`'s `com.apple.security.app-sandbox` and all — rather than in a bare XCTest
|
||||
/// bundle with none of the app's confinement. A green run here is empirical: `/usr/bin/ditto`,
|
||||
/// spawned as this app's child, can read a folder this process owns and write a `.zip` beside it,
|
||||
/// under the same entitlements a real File ▸ Share… run would have. That is the closest this suite
|
||||
/// can get to the real button without driving AppKit (`BoardSharePresentation`'s own boundary).
|
||||
@Suite("DittoZipArchiver")
|
||||
struct DittoZipArchiverTests {
|
||||
|
||||
@Test("A folder zips inside the sandbox, and the result is a real zip")
|
||||
func zipsInsideTheSandbox() throws {
|
||||
let fixture = try WriterFixture()
|
||||
defer { fixture.tearDown() }
|
||||
|
||||
let source = fixture.url("Payload")
|
||||
try FileManager.default.createDirectory(at: source, withIntermediateDirectories: true)
|
||||
try Data("hello from the sandbox\n".utf8).write(to: source.appendingPathComponent("note.txt"))
|
||||
try FileManager.default.createDirectory(
|
||||
at: source.appendingPathComponent("nested", isDirectory: true),
|
||||
withIntermediateDirectories: true
|
||||
)
|
||||
try Data([0x01, 0x02, 0x03]).write(to: source.appendingPathComponent("nested/data.bin"))
|
||||
|
||||
let destination = fixture.url("Payload.zip")
|
||||
try DittoZipArchiver.zip(contentsOf: source, to: destination)
|
||||
|
||||
#expect(FileManager.default.fileExists(atPath: destination.path))
|
||||
let bytes = try Data(contentsOf: destination)
|
||||
#expect(!bytes.isEmpty)
|
||||
// The local file header signature `PK\x03\x04` — proof this is an actual zip archive and
|
||||
// not, say, an empty file `ditto` merely touched.
|
||||
#expect(bytes.prefix(4) == Data([0x50, 0x4B, 0x03, 0x04]))
|
||||
}
|
||||
|
||||
@Test("A source that doesn't exist is a failure, not a silent empty archive")
|
||||
func missingSourceFails() throws {
|
||||
let fixture = try WriterFixture()
|
||||
defer { fixture.tearDown() }
|
||||
let missing = fixture.url("Never")
|
||||
let destination = fixture.url("Never.zip")
|
||||
|
||||
#expect(throws: DittoZipArchiver.Failure.self) {
|
||||
try DittoZipArchiver.zip(contentsOf: missing, to: destination)
|
||||
}
|
||||
#expect(!FileManager.default.fileExists(atPath: destination.path))
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user