Remove the App Group wholesale — one sandbox, one bookmark, one flag
Phase 2 of the one-app pivot (DESIGN 12 ▸ App-side state, re-ruled
2026-07-30; reworks 566deab). AppGroup retires; what remains is
AppStateHome — ordinary sandbox Application Support as the one home for
the registry, clipboard staging and template stores, keeping the
unit-test-host redirect (the test host is the app and would sweep real
state). Scalar defaults return to UserDefaults.standard.
BoardRecord's per-edition grant slots and openNow flags collapse to one
bookmark + one isOpenNow; the legacy-key decode and adopt-in-memory
paths go (nothing shipped with group-era records), while the founding
four-keys-required / defaults-for-everything-since decode policy stays —
a bookmarkless record decodes as the born-orphan row rather than
quarantining the list. needsReopen and the pre-anchored re-grant panel
are removed whole: the only state that flow served — a record granted by
a sibling sandbox — is unrepresentable now, and a dead bookmark of our
own was already the orphan case by explicit comment. The
indexOfRecord path fallback dies with it; path is never a key again.
The cross-process freshness stamp (mtime+size re-read) and
BoardEditionPresence with its popover "Also open in…" line retire; the
clipboard prune keeps its atomic .sweeping/ claim-then-delete, reframed
for crash residue and open -n copies rather than sibling editions. The
application-groups entitlement key is gone.
1880 tests in 317 suites green (13 cross-edition tests retired with
their subject).
Claude-Session: https://claude.ai/code/session_01SR4XGjmBE16ZUYWpfFHXwY
This commit is contained in:
@@ -8,9 +8,8 @@ import Testing
|
||||
///
|
||||
/// Every suite here drives a real store over a real temp board, with two things injected: a fake
|
||||
/// pasteboard (so a test never races the machine's one real pasteboard, nor every other test in the
|
||||
/// run) and a temp staging directory (so nothing goes near the shared App Group container, which is now
|
||||
/// the sibling edition's staging store too). Both seams exist exactly because those two claims are the
|
||||
/// ones worth pinning.
|
||||
/// run) and a temp staging directory (so nothing goes near the app's real Application Support home).
|
||||
/// Both seams exist exactly because those two claims are the ones worth pinning.
|
||||
|
||||
// MARK: - Test doubles
|
||||
|
||||
@@ -116,8 +115,8 @@ struct ClipboardHarness {
|
||||
/// The staged copy directories, sorted — "at most the current copy" is a claim about this list.
|
||||
///
|
||||
/// Hidden entries are excluded because the sweep keeps its own bookkeeping folder among them
|
||||
/// (`ClipboardStore.prune`'s claim-then-delete, which is what makes a concurrent sweep by the
|
||||
/// sibling edition safe). A staged copy is never hidden — its name is a lowercased UUID.
|
||||
/// (`ClipboardStore.prune`'s claim-then-delete). A staged copy is never hidden — its name is a
|
||||
/// lowercased UUID.
|
||||
func stagedCopyIDs() throws -> [String] {
|
||||
try FileManager.default.contentsOfDirectory(atPath: staging.path)
|
||||
.filter { !$0.hasPrefix(".") }
|
||||
@@ -416,17 +415,16 @@ struct ClipboardSweepTests {
|
||||
#expect(try harness.stagedCopyIDs().isEmpty)
|
||||
}
|
||||
|
||||
// MARK: The sibling edition's sweep
|
||||
// MARK: Two sweeps over one store
|
||||
//
|
||||
// The staging store now lives in the shared App Group container (12-editions.md ▸ Both editions
|
||||
// installed), so base and Pro sweep the same directory on their own launches, activations, copies
|
||||
// and pastes. Both compute the *same* answer — the keep set is the one `copyID` the machine-wide
|
||||
// pasteboard names — so they never disagree about what should go; what they can do is arrive at the
|
||||
// same doomed tree together. These two tests are the ruling's two clauses: atomic removals, and
|
||||
// One app, so the ordinary case is one sweeper — but the sweep is written claim-then-delete
|
||||
// anyway (`ClipboardStore.prune`), which is what makes a second sweeper a non-event: a second
|
||||
// copy of the app launched with `open -n` shares this container, and so does the next sweep after
|
||||
// a crash mid-delete. These two tests are that property's two halves: atomic removals, and
|
||||
// missing-entry = already swept.
|
||||
|
||||
@Test("Two editions sweeping the same store at once agree, and neither errors")
|
||||
func concurrentSweepsFromBothEditionsAgree() async throws {
|
||||
@Test("Two stores sweeping the same staging root at once agree, and neither errors")
|
||||
func concurrentSweepsAgree() async throws {
|
||||
let staging = FileManager.default.temporaryDirectory
|
||||
.appendingPathComponent("ClipboardTests-\(UUID().uuidString)", isDirectory: true)
|
||||
defer { try? FileManager.default.removeItem(at: staging) }
|
||||
@@ -443,9 +441,9 @@ struct ClipboardSweepTests {
|
||||
try Data("bytes".utf8).write(to: tree.appendingPathComponent("nested/file.txt", isDirectory: false))
|
||||
}
|
||||
|
||||
// Both editions read the *same* pasteboard, which is why both keep sets are `keep`. Modelled as
|
||||
// two stores over one staging root with pasteboards holding the same manifest, since two
|
||||
// processes are not something a unit test can have.
|
||||
// Both sweepers read the *same* machine-wide pasteboard, which is why both keep sets are
|
||||
// `keep`. Modelled as two stores over one staging root with pasteboards holding the same
|
||||
// manifest, since two processes are not something a unit test can have.
|
||||
let manifest = ClipboardManifest(
|
||||
copyID: "keep",
|
||||
boardRoot: URL(fileURLWithPath: "/Boards/Shared.kanban", isDirectory: true),
|
||||
@@ -500,7 +498,7 @@ struct ClipboardSweepTests {
|
||||
stagingRoot: staging,
|
||||
observesActivation: false
|
||||
)
|
||||
// The sibling got there first — which from this store's side is indistinguishable from the
|
||||
// Something got there first — which from this store's side is indistinguishable from the
|
||||
// directory listing simply being stale by the time it is walked.
|
||||
try FileManager.default.removeItem(at: doomed)
|
||||
clipboard.sweep()
|
||||
@@ -508,8 +506,8 @@ struct ClipboardSweepTests {
|
||||
|
||||
#expect(try FileManager.default.contentsOfDirectory(atPath: staging.path).isEmpty)
|
||||
|
||||
// And a staging root that has gone altogether — the sibling swept, then something removed the
|
||||
// shared folder — is nothing to do either, rather than a throw on the way to a no-op.
|
||||
// And a staging root that has gone altogether is nothing to do either, rather than a throw on
|
||||
// the way to a no-op.
|
||||
try FileManager.default.removeItem(at: staging)
|
||||
clipboard.sweep()
|
||||
await clipboard.stagingSettled()
|
||||
|
||||
Reference in New Issue
Block a user