Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
118 commits
Select commit Hold shift + click to select a range
6e35594
hive M1: Settings > Computers on macOS with presence and Mac-to-Mac p…
austinywang Jul 14, 2026
ef3bdc5
Declare CmuxMobileShell and CmuxMobilePairedMac products on the app t…
austinywang Jul 14, 2026
c0db87b
Revert "Declare CmuxMobileShell and CmuxMobilePairedMac products on t…
austinywang Jul 14, 2026
cb93c83
Compose the computers directory inside CmuxHive so the app links only…
austinywang Jul 14, 2026
77a7cd4
Link CmuxAgentChat into cmuxTests directly
austinywang Jul 14, 2026
72450f9
Make the composition's retry-delay closure explicitly @Sendable
austinywang Jul 14, 2026
9ab3b36
Computers: copy this Mac's pairing link for Mac-to-Mac pairing
austinywang Jul 14, 2026
003e87e
Computers: drop the iPhone QR button from Pair This Mac
austinywang Jul 14, 2026
e144821
Computers: bordered style for Copy Pairing Link
austinywang Jul 15, 2026
d2349b5
hive M1: remote-Mac viewer — live workspaces, terminal, and input ove…
austinywang Jul 14, 2026
aa4f537
Fix viewer-branch CI: repair pbxproj merge, follow aux-window lint, d…
austinywang Jul 14, 2026
aa5b65b
Make viewer retry-delay closures explicitly @Sendable
austinywang Jul 14, 2026
136c70d
Computers: bordered style for the row Open button
austinywang Jul 15, 2026
03978d7
Move Mac-consumed mobile packages from Packages/iOS to Packages/Shared
austinywang Jul 15, 2026
7fd3089
Computers: replace pairing link with a 6-digit code, drop Refresh List
austinywang Jul 15, 2026
a2e1bc7
web: port React-19 script-tag patch to @stackframe/stack 2.8.108, pat…
austinywang Jul 15, 2026
ef575db
Merge remote-tracking branch 'origin/main' into issue-8001-hive-viewer
austinywang Jul 15, 2026
232893b
Repair pbxproj merge resolution: restore isa/closing braces on packag…
austinywang Jul 15, 2026
f8140db
Post-merge fixes: dedupe searchText into its extension file, restore …
austinywang Jul 15, 2026
3279466
Hive viewer fixes from dogfood: block self-pairing, fix span width re…
austinywang Jul 15, 2026
c5f54d4
Hive viewer: trust the Tailscale tunnel for Stack auth (Mac viewer only)
austinywang Jul 15, 2026
30220ad
Computers: configurable dual view — per-computer windows or main-side…
austinywang Jul 16, 2026
889f2d2
Trust Tailscale's IPv6 ULA range in the Mac viewer's Stack-auth chann…
austinywang Jul 16, 2026
82c831b
Hive viewer: dedicated tailnet-verified Tailscale transport factory
austinywang Jul 16, 2026
88c30c7
Hive: native mirror workspaces — remote Mac terminals in the real cmu…
austinywang Jul 16, 2026
b66f649
Hive mirrors: live topology reconcile, remote-grid sizing, computer s…
austinywang Jul 16, 2026
8c2c5a1
Hive: per-window computer scope; windows mode opens a native cmux window
austinywang Jul 16, 2026
8b07518
Hive mirrors: repaint from cached full frame on selection and size ev…
austinywang Jul 16, 2026
bfe96a6
Hive mirrors: font-fitted scale-to-fill for mirror surfaces
austinywang Jul 16, 2026
056aa6f
Host: boot never-realized surfaces headless on mobile.terminal.replay
austinywang Jul 17, 2026
f1c7d93
Merge remote-tracking branch 'origin/main' into issue-8001-hive-viewer
austinywang Jul 17, 2026
74348b2
Hive mirrors: scope selection repaints, prune dead terminals, revert …
austinywang Jul 17, 2026
799cf86
Hive mirrors: unwedge never-focused renderers so mirror panes paint
austinywang Jul 17, 2026
90ff3a5
Hive mirrors: show connection status instead of a blank sidebar
austinywang Jul 22, 2026
2d610eb
Hive viewer: close lifecycle and rendering review gaps
austinywang Aug 25, 2026
a7d022c
Hive mirrors: reset scope during teardown
austinywang Aug 25, 2026
294e7d9
Hive directory: avoid idle presence subscriptions
austinywang Aug 25, 2026
61c025b
Hive viewer: enforce pairing and recovery boundaries
austinywang Aug 25, 2026
3784099
Hive viewer: require verified host identity
austinywang Aug 25, 2026
12d61d0
Hive viewer: reconcile session routes and mirror titles
austinywang Aug 25, 2026
2648850
Hive viewer: add grid and event-router regression tests
austinywang Aug 25, 2026
9409643
Hive viewer: cover unverified Tailscale bearer rejection
austinywang Aug 25, 2026
9c4d29d
Hive viewer: bind render streams and fail closed on Tailscale
austinywang Aug 25, 2026
2f93d45
Hive directory: cover unpair sign-out race
austinywang Aug 25, 2026
03e35f1
Hive viewer: preserve scope across safe teardown
austinywang Aug 25, 2026
10ba591
Hive viewer: return the guarded Tailscale transport
austinywang Aug 25, 2026
1f3fee4
Fix Hive viewer auth and lifecycle recovery
austinywang Aug 25, 2026
2ff8667
Move pairing code formatter helpers to file scope
austinywang Aug 25, 2026
8b058f0
Test Tailscale peer admission boundaries
austinywang Aug 25, 2026
941f6aa
Bind Hive viewers to verified Tailscale peers
austinywang Aug 25, 2026
f5b59bc
Wire Hive route admission into the app target
austinywang Aug 25, 2026
ca9b868
Close Hive pairing and sidebar failure gaps
austinywang Aug 25, 2026
c875524
Repair Hive mirror retry and close cleanup
austinywang Aug 25, 2026
473aa2d
Keep legacy Tailscale bearer transport fail closed
austinywang Aug 25, 2026
36dacdb
Revoke Hive sessions on every sign-out path
austinywang Aug 25, 2026
0c3cf10
Scope Hive pairing to owned supported hosts
austinywang Aug 25, 2026
0b2e393
Propagate registry ownership into Hive rows
austinywang Aug 25, 2026
732dad9
Fix Hive identity and render update races
austinywang Aug 25, 2026
5901929
Gate Hive actions until transport admission
austinywang Aug 25, 2026
cfd86cd
Finish Hive gate and fallback correctness
austinywang Aug 25, 2026
06859f3
Finish Hive scoped updates and transport gating
austinywang Aug 25, 2026
5a1fffe
Harden Hive scope and replay recovery
austinywang Aug 25, 2026
64941da
Close Hive replay and subscription races
austinywang Aug 26, 2026
64e1655
Serialize overlapping Hive replay attempts
austinywang Aug 26, 2026
7ba6556
Merge origin/main into issue-8001-hive-viewer
austinywang Aug 26, 2026
3314c0c
Merge remote-tracking branch 'origin/main' into issue-8001-hive-viewer
austinywang Aug 26, 2026
a1fce39
Keep Hive compatibility test fixture scoped
austinywang Aug 26, 2026
c23d208
Merge remote-tracking branch 'origin/main' into issue-8001-hive-viewer
austinywang Aug 26, 2026
5f5392f
test: cover retrying a failed Hive session
austinywang Aug 26, 2026
3233011
fix: close Hive viewer review gaps
austinywang Aug 26, 2026
bb9dba6
fix: finish Hive review cleanup
austinywang Sep 1, 2026
95a341c
Merge remote-tracking branch 'origin/main' into issue-8001-hive-viewer
austinywang Sep 1, 2026
4746e2e
ci: update moved RPC determinism allowlist path
austinywang Sep 1, 2026
e035d60
Merge remote-tracking branch 'origin/main' into issue-8001-hive-viewer
austinywang Sep 1, 2026
024754f
Merge remote-tracking branch 'origin/main' into issue-8001-hive-viewer
austinywang Sep 1, 2026
66f5abf
Merge remote-tracking branch 'origin/main' into issue-8001-hive-viewer
austinywang Sep 3, 2026
31bff49
Merge remote-tracking branch 'origin/main' into issue-8001-hive-viewer
austinywang Sep 8, 2026
1591a9c
fix: finish Hive package relocation and split viewer files
austinywang Sep 8, 2026
72d1870
test: expose Hive snapshot event loss and bound async waits
austinywang Sep 8, 2026
def28b4
fix: preserve Hive events across snapshots and share host subscriptions
austinywang Sep 8, 2026
49149f9
fix: cache Sendable Hive font measurements
austinywang Sep 8, 2026
3693953
ci: run standalone Hive domain and AppKit package tests
austinywang Sep 8, 2026
f9ef065
test: expose stale Hive account admission and pairing reloads
austinywang Sep 8, 2026
922ab0a
fix: reject stale Hive scope reads before publishing paired rows
austinywang Sep 8, 2026
1845269
test: expose misrouted Hive replay envelope acceptance
austinywang Sep 8, 2026
594fe68
fix: require matching Hive replay workspace and surface identities
austinywang Sep 8, 2026
5bcb63c
ci: validate project syntax with the native macOS plist parser
austinywang Sep 8, 2026
851afad
fix: repair Hive Xcode group closure and quote extension filename
austinywang Sep 8, 2026
3131bf9
fix: handle clock errors in the Computers settings view task
austinywang Sep 8, 2026
2640e09
fix: reconcile extracted Hive helpers with current main declarations
austinywang Sep 8, 2026
58cd221
fix: use main canonical terminal startup for Hive replay
austinywang Sep 8, 2026
4b4488d
Merge remote-tracking branch 'origin/main' into issue-8001-hive-viewer
austinywang Sep 8, 2026
4148a4a
Merge remote-tracking branch 'origin/main' into issue-8001-hive-viewer
austinywang Sep 8, 2026
2f8091f
fix: handle all clock errors while attaching Hive mirrors
austinywang Sep 8, 2026
c72c06b
test: expose Hive pairing, scope refresh, input recovery, and admissi…
austinywang Sep 8, 2026
0eebc1f
fix: close Hive scope, refresh ownership, replay input, and admission…
austinywang Sep 8, 2026
9260576
fix: initialize Hive sidebar snapshots outside ViewBuilder statements
austinywang Sep 8, 2026
a03fef5
test: wire the Hive scope into the sidebar scale harness
austinywang Sep 8, 2026
278af10
fix: repair app-host test wiring and Release compilation
austinywang Sep 8, 2026
f6bb617
Merge remote-tracking branch 'origin/main' into issue-8001-hive-viewer
austinywang Sep 8, 2026
8bb5197
test: expose Hive UUID and Release route selection gaps
austinywang Sep 8, 2026
c8d136c
fix: unify Hive route policy and own mirror replacements
austinywang Sep 8, 2026
258b0a6
Merge remote-tracking branch 'origin/main' into issue-8001-hive-viewer
austinywang Sep 8, 2026
6eb3532
Merge remote-tracking branch 'origin/main' into issue-8001-hive-viewer
austinywang Sep 8, 2026
64cf110
Merge remote-tracking branch 'origin/main' into issue-8001-hive-viewer
austinywang Sep 8, 2026
d45d727
test: expose overlapping Hive replay and disconnect admission races
austinywang Sep 8, 2026
af1f46b
fix: serialize Hive replay ownership and revoke disconnect admission
austinywang Sep 8, 2026
f96ff8f
Merge remote-tracking branch 'origin/main' into issue-8001-hive-viewer
austinywang Sep 8, 2026
30036f3
fix: start Hive transport shutdown before draining cancelled workers
austinywang Sep 8, 2026
3de424c
Merge remote-tracking branch 'origin/main' into issue-8001-hive-viewer
austinywang Sep 8, 2026
5cc8e32
fix: serialize app-host tests and align Iroh lane expectations
austinywang Sep 8, 2026
3673845
Merge remote-tracking branch 'origin/main' into issue-8001-hive-viewer
austinywang Sep 8, 2026
48d2706
Merge remote-tracking branch 'origin/main' into issue-8001-hive-viewer
austinywang Sep 8, 2026
5e721aa
fix: use portable web script entrypoint guard
austinywang Sep 8, 2026
6c1035d
Merge origin/main into issue-8001-hive-viewer
austinywang Sep 9, 2026
6bdaa9e
fix: preserve managed app behavior and show Hive connection errors
austinywang Sep 9, 2026
fddc6af
fix: align Cloud client bootstrap with current API
austinywang Sep 9, 2026
cdd9bb5
fix: retain Hive input during attach and update determinism path
austinywang Sep 9, 2026
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
The table of contents is too big for display.
Diff view
Diff view
  •  
  •  
  •  
Original file line number Diff line number Diff line change
Expand Up @@ -5,7 +5,7 @@ Standalone cmux-owned windows must have one close-shortcut owner so Cmd+W closes
Report a failure when the diff introduces or materially changes:

- A user-visible `NSWindow`, `NSPanel`, `NSWindowController`, SwiftUI `Window`, or SwiftUI `WindowGroup` without a stable `cmux.*` window identifier.
- A `cmux.*` window identifier assignment that is missing from `cmuxAuxiliaryWindowIdentifiers` in `Sources/cmuxApp.swift`.
- A `cmux.*` window identifier assignment that is missing from `cmuxAuxiliaryWindowIdentifiers` in `Sources/CmuxAuxiliaryWindows.swift`.
- A new standalone debug, settings, preview, task, editor, browser, file, import, config, or inspector window that can become key but is not covered by `cmuxWindowShouldOwnCloseShortcut`.
- A custom Cmd+W, `performKeyEquivalent`, or close-menu workaround that bypasses the shared `cmuxWindowShouldOwnCloseShortcut` routing instead of registering the window identifier.

Expand Down
4 changes: 2 additions & 2 deletions .github/test-determinism-allowlist.txt
Original file line number Diff line number Diff line change
Expand Up @@ -2,10 +2,10 @@
# Format: relpath<TAB>rule<TAB>short reason
# A finding whose (path, rule) appears here is suppressed.
# Remove a line once the underlying test is determinized.
Packages/iOS/CmuxMobileRPC/Tests/CmuxMobileRPCTests/MobileCoreRPCTransportDrainTests.swift sleep-then-assert temporary outage debt owned by #8931; remove when determinized
Packages/Shared/CmuxMobileRPC/Tests/CmuxMobileRPCTests/MobileCoreRPCTransportDrainTests.swift sleep-then-assert temporary outage debt owned by #8931; remove when determinized
Comment thread
austinywang marked this conversation as resolved.
Packages/iOS/CmuxMobileShellUI/Tests/CmuxMobileShellUITests/OnboardingMacDiscoveryKeepAliveTests.swift sleep-then-assert temporary outage debt owned by #9163; remove when determinized
cmuxTests/WorkspaceForkConversationContextMenuTests.swift assert-on-duration grandfathered
cmuxTests/WorkspaceForkConversationContextMenuTests.swift sleep-then-assert grandfathered
tests/test_ci_sparkle_build_monotonic.sh live-network-host release pretag guard intentionally probes the published appcast and soft-passes offline
Packages/Shared/CMUXMobileCore/Tests/CMUXMobileCoreTests/CmxRetryAfterPolicyTests.swift sleep-then-assert upstream retry policy timing assertions
Packages/iOS/CmuxMobileShell/Tests/CmuxMobileShellTests/MobileTerminalLaneCoordinatorTests.swift sleep-then-assert upstream coordinator readiness timing assertion
Packages/Shared/CmuxMobileShell/Tests/CmuxMobileShellTests/MobileTerminalLaneCoordinatorTests.swift sleep-then-assert upstream coordinator readiness timing assertion
2 changes: 2 additions & 0 deletions .github/workflows/ci.yml
Original file line number Diff line number Diff line change
Expand Up @@ -1758,6 +1758,8 @@ jobs:
CmuxControlSocket
CmuxFoundation
CmuxGit
CmuxHive
CmuxHiveUI
CmuxMobileTerminalKit
CmuxMobileWorkspace
CmuxNotifications
Expand Down
6 changes: 3 additions & 3 deletions .github/workflows/test-ios.yml
Original file line number Diff line number Diff line change
Expand Up @@ -162,7 +162,7 @@ jobs:

- name: Run CmuxMobilePairedMac package tests
run: |
swift test --package-path Packages/iOS/CmuxMobilePairedMac
swift test --package-path Packages/Shared/CmuxMobilePairedMac

- name: Run CmuxMobileChanges package tests
run: |
Expand All @@ -173,13 +173,13 @@ jobs:
# iOS shell replay/liveness regressions live in this package target.
# Keep them under the iOS package gate so PR CI enforces the mobile
# terminal mirror behavior without relying on local-only SwiftPM runs.
swift test --package-path Packages/iOS/CmuxMobileShell
swift test --package-path Packages/Shared/CmuxMobileShell

- name: Run CmuxMobileShellModel package tests
run: |
# Pure shell-model queue and ordering regressions live in this package.
# Keep them beside the shell package gate so input framing stays covered.
swift test --package-path Packages/iOS/CmuxMobileShellModel
swift test --package-path Packages/Shared/CmuxMobileShellModel

ios-simulator:
needs: detect-ios-changes
Expand Down
3 changes: 1 addition & 2 deletions CLI/CLIError.swift
Original file line number Diff line number Diff line change
@@ -1,5 +1,4 @@
import Foundation

/// Carries CLI failure metadata independently of executable startup.
struct CLIError: Error, CustomStringConvertible {
enum SocketFailureKind: Equatable {
case pathMissing
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,127 @@
public import Foundation

/// A short-lived 6-digit Mac-to-Mac pairing code advertised through the team
/// device registry.
///
/// The host Mac publishes the code (and its expiry) as instance labels on its
/// own `/api/devices` registration; another Mac claims it by typing the code,
/// matching it against the registry rows it can already see, and pairing with
/// the matching instance's advertised routes. The code is a rendezvous
/// selector, not a bearer secret: registry rows (including routes) are already
/// team-visible, and the pairing RPC still enforces account identity.
///
/// ```swift
/// let minted = CmxPairingCode(code: "042117", expiresAt: expiry)
/// body["instanceLabels"] = minted.instanceLabels
/// // …on the claiming Mac:
/// let active = CmxPairingCode.active(in: instance.labels, now: Date())
/// ```
public struct CmxPairingCode: Equatable, Sendable {
/// Instance label carrying the 6-digit code.
public static let codeLabelKey = "pairing_code"
/// Instance label carrying the code's ISO 8601 expiry timestamp.
public static let expiresAtLabelKey = "pairing_code_expires_at"

/// The 6-digit, zero-padded code the user reads off the host Mac.
public var code: String
/// When the code stops being claimable.
public var expiresAt: Date

/// Creates a pairing code value.
/// - Parameters:
/// - code: The 6-digit, zero-padded code string.
/// - expiresAt: When the code stops being claimable.
public init(code: String, expiresAt: Date) {
self.code = code
self.expiresAt = expiresAt
}

/// Generates a fresh 6-digit code (zero-padded, `000000`–`999999`)
/// expiring `ttl` from `now`.
///
/// - Parameters:
/// - ttl: Code lifetime.
/// - now: Mint-side clock, injected for testability.
/// - generator: Randomness source, injected for testability.
/// - Returns: The minted code value.
// lint:allow namespace-type — this type owns code/expiry state; this is a value factory.
public static func minted(
ttl: TimeInterval,
now: Date,
using generator: inout some RandomNumberGenerator
) -> CmxPairingCode {
CmxPairingCode(
code: String(format: "%06d", Int.random(in: 0...999_999, using: &generator)),
expiresAt: now.addingTimeInterval(ttl)
)
}

/// The wire form: the instance labels a registration POST advertises.
public var instanceLabels: [String: String] {
[
Self.codeLabelKey: code,
Self.expiresAtLabelKey: Self.iso8601.string(from: expiresAt),
]
}

/// Decodes an unexpired pairing code from registry instance labels.
///
/// A missing or unparseable expiry yields `nil` — a claim must never
/// succeed against a code whose lifetime cannot be verified.
///
/// - Parameters:
/// - labels: The instance's string labels as returned by the registry.
/// - now: The claim-side clock, injected for testability.
/// - Returns: The active code, or `nil` when absent or expired.
// lint:allow namespace-type — this type owns code/expiry state; this decodes that value.
public static func active(in labels: [String: String], now: Date) -> CmxPairingCode? {
guard
let code = labels[codeLabelKey]?.trimmingCharacters(in: .whitespacesAndNewlines),
!code.isEmpty,
let rawExpiry = labels[expiresAtLabelKey],
let expiresAt = Self.parseISO8601(rawExpiry),
expiresAt > now
else { return nil }
return CmxPairingCode(code: code, expiresAt: expiresAt)
}

/// The user-typed claim input normalized for matching: digits only, so
/// `"042 117"` and `"042117"` claim the same code.
/// - Parameter rawInput: The text the user typed.
/// - Returns: The digit string, or `nil` unless exactly 6 digits remain.
// lint:allow namespace-type — normalization is part of this value's claim grammar.
public static func normalizedClaimInput(_ rawInput: String) -> String? {
let digits = String(rawInput.compactMap { character -> Character? in
guard character.unicodeScalars.count == 1,
let scalar = character.unicodeScalars.first,
(48...57).contains(scalar.value) else {
return nil
}
return character
})
return digits.count == 6 ? digits : nil
}

/// Encoder for the expiry label; fractional seconds match the registry's
/// own `toISOString()` timestamps.
private static var iso8601: ISO8601DateFormatter {
let formatter = ISO8601DateFormatter()
formatter.formatOptions = [.withInternetDateTime, .withFractionalSeconds]
return formatter
}

/// Lenient ISO 8601 parse (fractional and whole-second forms).
// lint:allow static-helper — formatter construction stays inside the value's
// Sendable boundary and is only used on registry label reads.
private static func parseISO8601(_ raw: String) -> Date? {
let trimmed = raw.trimmingCharacters(in: .whitespacesAndNewlines)
guard !trimmed.isEmpty else { return nil }
let fractional = ISO8601DateFormatter()
fractional.formatOptions = [.withInternetDateTime, .withFractionalSeconds]
if let date = fractional.date(from: trimmed) { return date }
let whole = ISO8601DateFormatter()
whole.formatOptions = [.withInternetDateTime]
return whole.date(from: trimmed)
}

}
Original file line number Diff line number Diff line change
Expand Up @@ -13,7 +13,9 @@ extension String {
return clipped
}

var renderGridEstimatedCellWidth: Int {
/// The estimated total grid-cell width of the string (wide glyphs count 2,
/// combining marks 0), matching the host's span-width accounting.
public var renderGridEstimatedCellWidth: Int {
reduce(0) { width, character in
width + character.renderGridEstimatedCellWidth
}
Expand All @@ -25,13 +27,18 @@ extension MobileTerminalRenderGridFrame.RowSpan {
text.unicodeScalars.contains { $0.isRenderGridWidthSensitiveScalar }
}

var gridCellWidth: Int {
/// The span's total width in grid cells: the wire `cell_width` when the
/// host sent one, else the text's estimated width. This is the column
/// advance to the next span, not a per-glyph width.
public var gridCellWidth: Int {
cellWidth ?? max(1, text.renderGridEstimatedCellWidth)
}
}

extension Character {
var renderGridEstimatedCellWidth: Int {
/// The grid-cell width of one character (0 combining, 2 wide, else 1),
/// matching the host's span-width accounting.
public var renderGridEstimatedCellWidth: Int {
let scalars = unicodeScalars
guard scalars.contains(where: { !$0.isRenderGridZeroWidthScalar }) else {
return 0
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,68 @@
import Foundation
import Testing
@testable import CMUXMobileCore

/// Deterministic generator so minted codes are reproducible in tests.
private struct SeededGenerator: RandomNumberGenerator {
var state: UInt64
mutating func next() -> UInt64 {
state = state &* 6_364_136_223_846_793_005 &+ 1_442_695_040_888_963_407
return state
}
}

struct CmxPairingCodeTests {
private let now = Date(timeIntervalSince1970: 1_000)

@Test func mintedProducesSixDigitZeroPaddedCodeAndTTLExpiry() {
var generator = SeededGenerator(state: 7)
let minted = CmxPairingCode.minted(ttl: 600, now: now, using: &generator)
#expect(minted.code.count == 6)
let allDigits = minted.code.allSatisfy(\.isNumber)
#expect(allDigits)
#expect(minted.expiresAt == now.addingTimeInterval(600))
}

@Test func instanceLabelsRoundTripThroughActive() {
let minted = CmxPairingCode(code: "042117", expiresAt: now.addingTimeInterval(600))
let decoded = CmxPairingCode.active(in: minted.instanceLabels, now: now)
#expect(decoded == minted)
}

@Test func activeRejectsExpiredMissingOrUnparseableExpiry() {
let expired = CmxPairingCode(code: "042117", expiresAt: now.addingTimeInterval(-1))
#expect(CmxPairingCode.active(in: expired.instanceLabels, now: now) == nil)
#expect(CmxPairingCode.active(
in: [CmxPairingCode.codeLabelKey: "042117"],
now: now
) == nil)
#expect(CmxPairingCode.active(
in: [
CmxPairingCode.codeLabelKey: "042117",
CmxPairingCode.expiresAtLabelKey: "not-a-date",
],
now: now
) == nil)
#expect(CmxPairingCode.active(in: [:], now: now) == nil)
}

@Test func activeAcceptsWholeSecondExpiryForm() {
let labels = [
CmxPairingCode.codeLabelKey: "042117",
CmxPairingCode.expiresAtLabelKey: "1970-01-01T00:33:20Z",
]
let decoded = CmxPairingCode.active(in: labels, now: now)
#expect(decoded?.code == "042117")
#expect(decoded?.expiresAt == Date(timeIntervalSince1970: 2_000))
}

@Test func normalizedClaimInputAcceptsExactlySixDigits() {
#expect(CmxPairingCode.normalizedClaimInput("042117") == "042117")
#expect(CmxPairingCode.normalizedClaimInput(" 042 117 ") == "042117")
#expect(CmxPairingCode.normalizedClaimInput("042-117") == "042117")
#expect(CmxPairingCode.normalizedClaimInput("42117") == nil)
#expect(CmxPairingCode.normalizedClaimInput("0421178") == nil)
#expect(CmxPairingCode.normalizedClaimInput("042117") == nil)
#expect(CmxPairingCode.normalizedClaimInput("") == nil)
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -15,7 +15,7 @@ let package = Package(
),
],
dependencies: [
.package(path: "../../Shared/CMUXMobileCore"),
.package(path: "../CMUXMobileCore"),
],
targets: [
.target(
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -15,7 +15,7 @@ let package = Package(
),
],
dependencies: [
.package(path: "../../Shared/CMUXMobileCore"),
.package(path: "../CMUXMobileCore"),
.package(path: "../CmuxMobileShellModel"),
.package(path: "../CmuxMobileSupport"),
],
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -19,11 +19,11 @@ let package = Package(
),
],
dependencies: [
.package(path: "../../Shared/CMUXMobileCore"),
.package(path: "../../Shared/CmuxAgentChat"),
.package(path: "../CmuxMobileChanges"),
.package(path: "../CMUXMobileCore"),
.package(path: "../CmuxAgentChat"),
.package(path: "../../iOS/CmuxMobileChanges"),
.package(path: "../CmuxMobileDiagnostics"),
.package(path: "../CmuxMobileBrowserStream"),
.package(path: "../../iOS/CmuxMobileBrowserStream"),
.package(path: "../CmuxMobilePairedMac"),
.package(path: "../CmuxMobileRPC"),
.package(path: "../CmuxMobileShellModel"),
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -643,16 +643,24 @@ public actor DeviceRegistryService: DeviceRegistryRefreshing {
value = try? CmxAttachRoute(from: decoder)
}
}
struct FailableLabel: Decodable {
let value: String?
init(from decoder: any Decoder) throws {
value = try? decoder.singleValueContainer().decode(String.self)
}
}
struct Instance: Decodable {
let tag: String?
let routes: [FailableRoute]?
let lastSeenAt: String?
let labels: [String: FailableLabel]?
}
struct Device: Decodable {
let deviceId: String
let platform: String?
let displayName: String?
let lastSeenAt: String?
let isOwnedByCurrentUser: Bool?
let instances: [Instance]?
}
struct ListResponse: Decodable {
Expand All @@ -669,7 +677,8 @@ public actor DeviceRegistryService: DeviceRegistryRefreshing {
return RegistryAppInstance(
tag: tag?.isEmpty == false ? tag! : "default",
routes: (instance.routes ?? []).compactMap(\.value),
lastSeenAt: Self.parseTimestamp(instance.lastSeenAt)
lastSeenAt: Self.parseTimestamp(instance.lastSeenAt),
labels: (instance.labels ?? [:]).compactMapValues(\.value)
)
}
return RegistryDevice(
Expand All @@ -678,7 +687,8 @@ public actor DeviceRegistryService: DeviceRegistryRefreshing {
? device.platform! : "mac",
displayName: device.displayName,
lastSeenAt: Self.parseTimestamp(device.lastSeenAt),
instances: instances
instances: instances,
isOwnedByCurrentUser: device.isOwnedByCurrentUser ?? false
)
}
}
Expand Down
Loading
Loading