Close the three holes the #1486 review found (#1488)

* Fix the three follow-ups from the #1486 review

Three defects shipped with the censorship-resilience merge, all
confirmed against main:

1. Source-manifest verification silently accepted added files.
   shasum -c checks only the files the manifest lists, and the Xcode
   project compiles every source file present in the tree — so a
   hostile mirror could pass verification by adding a file rather than
   modifying one. The manifest header and VERIFYING-A-BUILD.md now
   require the completeness check (git status --porcelain, or a path
   diff for tarballs) alongside the hash check.

2. A relay removed while Tor was bootstrapping reconnected anyway.
   dropRelays never subtracted from pendingTorConnectionURLs, and a
   custom relay passes the allow-list filter, so draining the pending
   queue resurrected a relay someone had explicitly deleted.

3. Turning Tor off mid-bootstrap read as 'network may be blocking tor'.
   shutdownCompletely left the detached 75s poll loop running, which
   then stamped bootstrapDidStall over the clean shutdown state; and
   the stall handler guarded on torEnforced, which is compile-time true
   in release, instead of the runtime preference. The poll loop is now
   generation-fenced (shutdown, dormancy, and restart each invalidate
   it) and the handler consults persistedTorPreference().

Both app-side fixes carry regression tests proven to fail pre-fix.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>

* Address Codex review: ignored files and manifest placement

git status --porcelain omits ignored paths, and .gitignore covers
build/ — a planted bitchat/build/Evil.swift would compile via the
synchronized group while the documented check stayed silent. The
checkout check now uses --ignored.

The downloaded manifest also has to live outside the tree, or it trips
the completeness checks itself; the doc now says so and references it
at /tmp throughout.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>

---------

Co-authored-by: jack <jackjackbits@users.noreply.github.com>
Co-authored-by: Claude Fable 5 <noreply@anthropic.com>
This commit is contained in:
jack
2026-07-26 21:01:38 +02:00
committed by GitHub
co-authored by jack Claude Fable 5
parent c1ce9029d8
commit 132120a88e
7 changed files with 135 additions and 23 deletions
@@ -1374,3 +1374,37 @@ private func makeImageData() throws -> Data {
return data
#endif
}
// MARK: - Tor Extension Tests
struct ChatViewModelTorExtensionTests {
/// Turning Tor off mid-bootstrap must not read as "the network is
/// blocking tor": `torEnforced` is a compile-time constant, so the stall
/// handler has to consult the runtime preference before announcing.
@Test @MainActor
func bootstrapStall_withTorPreferenceOff_announcesNothing() async {
let key = NetworkActivationService.torPreferenceKey
let previous = UserDefaults.standard.object(forKey: key)
defer {
if let previous {
UserDefaults.standard.set(previous, forKey: key)
} else {
UserDefaults.standard.removeObject(forKey: key)
}
}
let (viewModel, _) = makeTestableViewModel()
UserDefaults.standard.set(false, forKey: key)
viewModel.handleTorBootstrapDidStall()
try? await Task.sleep(nanoseconds: 50_000_000)
#expect(viewModel.torStallAnnounced == false)
// The same stall with the preference on (the persisted default) is
// exactly what must still be announced.
UserDefaults.standard.set(true, forKey: key)
viewModel.handleTorBootstrapDidStall()
try? await Task.sleep(nanoseconds: 50_000_000)
#expect(viewModel.torStallAnnounced == true)
}
}
@@ -44,6 +44,46 @@ final class NostrRelayManagerTests: XCTestCase {
XCTAssertTrue(context.sessionFactory.allConnections.allSatisfy { $0.cancelCallCount >= 1 })
}
/// A relay removed while its connection is queued behind Tor bootstrap
/// must stay removed: draining the pending set used to resurrect it,
/// because `dropRelays` never touched `pendingTorConnectionURLs` and a
/// custom relay is in neither the default set nor the allow-list filter.
func test_relayRemovedWhileWaitingForTor_staysRemovedWhenTorBecomesReady() async {
let customURL = "wss://custom-removed.example"
let center = NotificationCenter()
let customRelays = MutableRelayList(urls: [customURL])
let context = makeContext(
permission: .authorized,
userTorEnabled: true,
torEnforced: true,
torIsReady: false,
notificationCenter: center,
customRelays: customRelays
)
// Defaults plus the custom relay all queue while Tor bootstraps.
context.manager.connect()
XCTAssertTrue(context.sessionFactory.requestedURLs.isEmpty)
XCTAssertEqual(context.torWaiter.awaitCallCount, 1)
// The relay is removed by hand before Tor is ready.
customRelays.urls = []
center.post(name: NostrRelaySettings.didChangeNotification, object: nil)
// The settings sink hops through the main queue; let it land.
try? await Task.sleep(nanoseconds: 20_000_000)
context.torWaiter.resolve(true)
let defaultsConnected = await waitUntil {
context.sessionFactory.requestedURLs.count == self.expectedDefaultRelayCount
}
XCTAssertTrue(defaultsConnected)
XCTAssertFalse(
context.sessionFactory.requestedURLs.contains(customURL),
"a relay removed while Tor was bootstrapping must not reconnect when the pending queue drains"
)
}
func test_connect_waitsForTorReadinessBeforeCreatingSessions() async {
let context = makeContext(permission: .authorized, userTorEnabled: true, torEnforced: true, torIsReady: false)