# Testing Gotcha

This document is the source of truth for iOS and terminal UI verification.
Every UI change must update its relevant scenarios in the same commit. For
ordinary commits, run the build gates and every scenario affected by the
change. A release test means completing this entire checklist successfully on
the stated terminals, simulators, and devices; record failures as open issues
and do not sign off until every scenario passes.

## Test record

- Release/build:
- Commit:
- Date and tester:
- Xcode and iOS versions:
- Simulator/device:
- Gitea server version:
- Result and open issues:

Use a real test account with repositories, open and closed issues, open and
closed pull requests, multiple branches, commits, comments, and file changes.
Never paste a production token into logs, screenshots, source files, or this
document. The app stores entered tokens in the Apple Keychain.

Run both of these passes:

- Fresh install on a disposable simulator, including Add Server.
- Upgrade/reinstall over the previous build, preserving app data and Keychain.

## Build gates

From the repository root, all commands must pass without warnings:

```sh
cargo fmt --all -- --check
RUSTFLAGS="-D warnings" cargo check --workspace --all-targets
cargo clippy --workspace --all-targets -- -D warnings
cargo test --workspace
```

## Terminal UI

Run `cargo run -p gotcha-tui -- --server NAME` in a normal terminal and in a
Herdr pane. Use a non-production account with at least two server profiles and
representative repositories.

- [ ] The five numbered panes open Home, Issues, Repos, PRs, and Milestones;
      the selected pane and row remain visibly distinct at narrow and wide
      terminal sizes.
- [ ] `j`/`k`, arrows, `g`/`G`, Page Up/Down, Enter, and Backspace provide
      mutt-style list and history navigation; `q` exits immediately from every
      non-editor depth. `n`/`p` and scrolling past a boundary traverse
      paginated API results without losing history.
- [ ] Clicking a pane switches it, clicking a list row selects it,
      double-clicking opens it, and the mouse wheel scrolls the list.
- [ ] Home activity filters cycle with `v`; activity targets route to their
      repository, issue, pull request, or commit.
- [ ] A commit activity preview shows its commit count, short hashes, messages,
      authors, and timestamps as a readable list rather than raw JSON; opening
      it shows the head commit and its changed files.
- [ ] Repository favorites toggle with `*`, sort first per pane, and persist
      after relaunch. Commits switch branches with `b`; file trees, text and
      binary files, commit changes, pull changes, and per-file diffs open.
- [ ] Source files use grammar-based syntax colors selected by filename or
      shebang, with unknown text remaining legible. Commit and pull diffs show
      metadata, hunks, additions, and deletions in distinct syntax colors.
- [ ] Home activity content plus issue, pull request, milestone, and comment
      Markdown pseudo-renders headings, emphasis, strong text, lists, links,
      inline code, and fenced code while preserving metadata line breaks.
- [ ] Issue and pull filters apply. Issues can be created, edited,
      closed/reopened, and deleted after confirmation; issue comments can be
      added and the current user's comments edited.
- [ ] Milestones can be created, edited, closed/reopened, and deleted after
      confirmation; milestone details route to their issues and pulls.
- [ ] Server profiles can be added, authenticated, selected, edited, renamed,
      and deleted. Blank token on edit preserves the existing token; removing
      the last server leaves the server manager available.
- [ ] Select a different server, quit, and relaunch without `--server`; the TUI
      restores that server. Relaunch with `--server NAME`; the explicit server
      wins and becomes the restored server after a clean exit. Renaming or
      deleting the remembered profile leaves a valid selection.
- [ ] Editors move between fields with Tab/Shift-Tab, support cursor movement,
      Unicode insertion, Delete, and Backspace without navigating back, mask
      tokens, save with Ctrl-S, and cancel with Esc.
- [ ] The default overview refresh interval is five seconds. Changing it with
      `,` persists after relaunch; refresh preserves selection and pauses while
      editing, confirming, scrolling, or otherwise interacting.
- [ ] `cargo build --release -p gotcha-tui` produces
      `target/release/gotcha-tui`, and the installed executable starts from
      `PATH`.

Build and install the simulator app:

```sh
cd ios
xcodegen generate

xcodebuild \
  -project Gotcha.xcodeproj \
  -scheme Gotcha \
  -configuration Debug \
  -sdk iphonesimulator \
  -destination 'platform=iOS Simulator,name=iPhone 17 Pro' \
  ARCHS=arm64 \
  ONLY_ACTIVE_ARCH=YES \
  build

gotcha_build_dir="$(
  xcodebuild \
    -project Gotcha.xcodeproj \
    -scheme Gotcha \
    -configuration Debug \
    -sdk iphonesimulator \
    -destination 'platform=iOS Simulator,name=iPhone 17 Pro' \
    -showBuildSettings -json \
    ARCHS=arm64 \
    ONLY_ACTIVE_ARCH=YES |
  plutil -extract 0.buildSettings.TARGET_BUILD_DIR raw -o - -
)"
xcrun simctl install booted "$gotcha_build_dir/Gotcha.app"
xcrun simctl launch booted de.rfc1437.gotcha
```

Before AltStore PAL release publication, create a signed Release archive and
inspect the packaged application rather than relying on Debug build settings:

```sh
cd ios
xcodebuild \
  -project Gotcha.xcodeproj \
  -scheme Gotcha \
  -configuration Release \
  -destination 'generic/platform=iOS' \
  -archivePath /private/tmp/Gotcha.xcarchive \
  archive
```

Verify the archived app reports version `1.0` and a positive build number, is
signed for distribution with the expected bundle identifier and entitlements,
contains `PrivacyInfo.xcprivacy`, and declares the correct export-compliance
answer before uploading it to App Store Connect for notarization.

Use the simulator for all development validation. Never install an Xcode-run or
otherwise development-signed build on a physical iPhone because that would
replace the correctly signed AltStore PAL installation. Physical-device release
testing begins only after the accepted Alternative Distribution Package and
source update are published, and installs or updates Gotcha through AltStore PAL.

## Add Server and native text editing

- [ ] With no configured server, Issues and Repos show the Servers screen and
      its empty state.
- [ ] The add button presents Add Server; Cancel dismisses it.
- [ ] API provider is a native single-selection menu with Gitea selected by
      default and Forgejo available; selecting either updates the displayed
      value, VoiceOver value, and URL example.
- [ ] Name, Server URL, and Access token use native text fields and suitable
      keyboards; Next advances between fields and Done submits.
- [ ] Long-press in every field shows the native loupe, insertion point, and
      selection handles.
- [ ] The standard edit menu offers Cut, Copy, Paste, Select, and Select All as
      applicable. There is no app-specific editing toolbar.
- [ ] Copy text from one field and paste it into another; verify the exact text.
- [ ] The token is obscured, remains editable, and does not trigger a password
      save prompt.
- [ ] Empty or invalid values show an error without adding a server.
- [ ] Valid Gitea credentials add and select the server; all data tabs load.
- [ ] Valid Forgejo credentials add and select the server; repositories,
      issues, milestones, pulls, activity, commits, files, and mutations use
      the same native screens and load successfully.
- [ ] Leave Gitea selected while adding a modern Forgejo server; provider
      discovery identifies Forgejo and the server works after relaunch.
- [ ] Terminate and relaunch the app; the selected server and Keychain token
      still work without re-entry.
- [ ] Open the server picker from a repository list and switch between at least
      two configured servers; every data tab changes to the selected server.
- [ ] On Home, the leading server button opens the same Servers screen as the
      other data tabs. Select a different server and verify Home reloads its
      title, contribution summary, filters, and activity for that server.
- [ ] With no selected server, Home still shows its server button and its empty
      state directs the user to that button.
- [ ] Swipe a server row from the trailing edge; Delete and Edit use native
      contextual actions with symbols. A full swipe invokes Delete, and both a
      full swipe and a Delete tap still require the same destructive alert.
- [ ] Cancel the delete alert; the row, selected server, saved token, favorites,
      and filters remain unchanged.
- [ ] Tap Edit; API provider, name, and URL show the saved values. The secure
      token field is empty and says Leave unchanged so the stored token is not
      exposed to the UI.
- [ ] Save edits with the token field empty; the existing token still works.
      Then change the name, URL, provider, and token and save again; the server
      is revalidated, provider discovery is applied, URL-scoped favorites and
      filters follow the server, and all data tabs use the edited connection.
- [ ] Enter invalid edited credentials or an invalid URL; the editor stays open,
      shows the error, and preserves the previously working configuration.
- [ ] Confirm Delete; the row and Keychain token are removed. If it was selected,
      the next server is selected (or the previous last row); deleting the last
      server returns server-dependent tabs to their empty Servers screen.
- [ ] Terminate and relaunch after editing and deleting; edited details persist,
      deleted servers do not return, and remaining servers still authenticate.

## Native interaction and navigation

- [ ] Every list scrolls with normal drag, momentum, bounce, and scroll-bar
      behavior. Rows are flat, use system separators, and have no card-shaped
      rounded backgrounds.
- [ ] Pull past the top of Home, repository lists, issue lists, pull lists,
      commit history, changed files, issue details, pull details, and diffs.
      The native refresh control appears, reloads data, and disappears.
- [ ] On accounts/repositories with more than 30 results, Home activity,
      repositories, issues, pull requests, milestones, milestone contents,
      commit history, and pull-request files initially show one page plus a
      **Pull up or tap to load more**
      footer. Pull upward past the bottom and verify the footer changes to a
      native loading indicator, the next page appends without duplicates, and
      the footer disappears after the final page.
- [ ] Tap the load-more footer as an accessibility alternative and repeat while
      VoiceOver is running. It announces loading, ignores repeated activation
      while a request is active, preserves the current scroll position, and
      allows retry after an error.
- [ ] Pull down to refresh after loading multiple pages. The content resets to
      the new first page, filtering/branch changes also reset pagination, and
      switching History/Files never shows a stale load-more footer.
- [ ] Tap a row, scroll its detail, and use both the navigation-bar Back button
      and the left-edge interactive swipe to return.
- [ ] From a list scrolled well away from the top, open a detail and go Back.
      The same rows and scroll offset remain visible.
- [ ] Switch tabs while a tab has a pushed detail and a scrolled list; returning
      to the tab preserves its navigation stack and position.
- [ ] Rapidly change tabs, filters, and branches while content loads. The app
      neither crashes nor replaces a newer result with a cancelled request.
- [ ] Loading and error states remain dismissible and do not leave a refresh
      indicator or navigation-bar spinner running.

## Home

- [ ] Home shows the selected server name, contribution total, nine labeled
      months with gaps between them, and recent activity.
- [ ] The nine-month activity strip is horizontally centered beneath its title
      without clipping either edge.
- [ ] Activity rows have the correct icon, repository, summary, and date.
- [ ] The compact icon row below the activity graph sits flat on the normal
      background with no border or pill and defaults to the clock. Select the
      issue and pull-request icons in turn; each shows only matching recent
      activity or the native empty state, then the clock restores all rows.
      The people and bell icons remain separate, accessible Server Activity
      and Notifications destinations.
- [ ] Tap **Server Activity**. The pushed native list combines activity from
      every user visible to the selected server account in newest-first order,
      identifies each actor, supports pull-to-refresh and pagination without
      duplicates, and uses a native empty state when no activity is visible.
      Tap another user's repository, issue, pull request, and commit events;
      each opens the corresponding native detail screen and Back returns to
      Server Activity.
- [ ] Pull-request activity includes older creation and close events beyond the
      first activity-feed page without requiring a manual pull-up; the empty
      state appears only after every available page has been checked.
- [ ] Tap repository, issue, pull-request, and commit activity. Each opens the
      matching destination on the Home navigation stack. The navigation-bar
      Back button and left-edge interactive swipe return directly to Home.
- [ ] Non-linkable server activity does not navigate or appear tappable.

## Notifications

- [ ] Launch a fresh install. Gotcha does not request notification permission
      at launch. Open Settings and turn on **Background notifications**; only
      then does the standard iOS authorization sheet appear. Deny once and
      confirm Gotcha keeps the switch off, explains that notifications are
      disabled, and offers **Open Settings**. The **Notification Settings** row
      opens Gotcha's page in iOS Settings. Opening Settings and waiting for the
      authorization status to load must leave scrolling, tabs, and the switch
      responsive without repeated cell layout or a frozen app.
- [ ] Allow notifications. Settings reports the current system authorization,
      the app switch persists across relaunches, and iOS Settings remains the
      source of truth for alerts, sounds, Focus, and scheduled summaries.
      Turning the app switch off cancels pending alerts and background refresh.
- [ ] Tap the Home bell. Open and Closed each show the matching server
      notification threads with a type icon, title, repository, date, Dynamic
      Type layout, and accessible Open/Closed value. Pull to refresh, switch
      status repeatedly, and load a list longer than one page without duplicate
      or stale rows.
- [ ] Tap open and closed issue, pull-request, commit, and repository
      notifications. Each opens the native destination on the Home navigation
      stack; opening an unread thread marks it read, Back returns to the
      notification list, and the next refresh moves it from Open to Closed.
- [ ] After the first poll establishes a cursor, create or receive another
      server notification. Launch the Debug simulator build with
      `xcrun simctl launch booted de.rfc1437.gotcha --validate-background-notifications`,
      then background Gotcha. This explicit Debug-only mode grants the poll the
      same finite background execution time as an ordinary app transition;
      production builds continue to rely exclusively on `BGAppRefreshTask`.
      Confirm one ordinary-priority local notification is delivered with no
      private issue title or repository name on the Lock Screen. A repeated
      update replaces the same thread alert rather than stacking duplicates;
      tapping it selects the correct configured server, opens the originating
      item, and marks the Gitea thread read.
- [ ] Bring Gotcha to the foreground while a poll finds an update. It refreshes
      notification data without displaying a banner over the active app. Leave
      it backgrounded and confirm iOS, not an in-app timer, chooses subsequent
      refresh timing.

## Milestone navigation

- [ ] Open a milestone, then open one of its issues and one of its pull
      requests. For each detail, the navigation-bar Back button and left-edge
      interactive swipe return directly to that milestone, preserving its
      rows and scroll position.

## Home-screen widgets

- [ ] Upgrade over a build with an existing server. Launch Gotcha once, then
      add both **Recent Activity** and **Open Pull Requests** from the system
      widget gallery; the saved server and Keychain token work without re-entry.
- [ ] Both widgets are offered in medium and large system sizes and use native
      widget margins, typography, tint, relative update time, and light/dark
      appearances without clipping at the largest accessibility text size.
- [ ] In medium size, Recent Activity shows up to three latest activity rows
      and Open Pull Requests shows up to two open pull requests. In large size,
      they show up to ten and five rows respectively. Confirm both the widget
      gallery previews and live configured timelines use those family-specific
      row counts. Loading, empty, missing-server, and API-error states remain
      legible and do not expose account data while the device is locked.
- [ ] Add a second server profile, long-press each widget, choose **Edit
      Widget**, and assign a different server to each. The displayed server
      name and rows update independently; deleting a selected server changes
      that widget to its missing-server state instead of showing another
      server's data.
- [ ] Tap Recent Activity; Gotcha opens or foregrounds at the Home root. Tap
      Open Pull Requests; Gotcha opens or foregrounds at the PR root. Both
      routes discard a stale detail stack in the destination tab.
- [ ] Add, edit, select, and delete server profiles in Gotcha, then return to
      the Home Screen. Widget configuration choices and timelines refresh to
      match the persisted server list.

## Issues

- [ ] The repository list shows name, description, language, open count, update
      date, and current favorite state.
- [ ] Toggle a favorite and confirm it remains after refresh and relaunch but
      does not change the same repository's favorite in Repos or Milestones.
- [ ] Open a repository; the issue list defaults to the saved Open/Closed filter.
- [ ] Repository issue rows and issue rows inside a milestone show a green open
      or purple closed icon beside the title; mixed milestone results use the
      correct icon per row and VoiceOver announces each state.
- [ ] Change the native filter menu between Open and Closed; the checkmark,
      rows, and persisted selection update.
- [ ] Select one and then multiple labels in the issue filter. Checkmarks and
      rows update without dismissing the menu; clearing every label restores
      the unfiltered label result.
- [ ] Open **Search Text** in the issue filter, enter a term, and verify the
      server-filtered rows and menu subtitle update. Combine it with Closed and
      one or more labels and verify every filter applies together.
- [ ] Select a milestone and then All Milestones; the checkmark and issue rows
      update. Relaunch and revisit repositories to verify label and milestone
      selections persist independently per repository and status persists.
- [ ] The issue-filter icon uses a neutral color for Open + All Milestones + no
      labels, and the app accent color whenever any non-default filter is set.
- [ ] In the issue filter panel, the Search Text, Status, Milestone, and Labels
      icons use the app accent color independently when their filter is
      non-default. Search text remains while navigating during the current app
      session but is empty after relaunch. **Clear Filters** restores empty
      search + Open + All Milestones + no labels, refreshes the rows, and
      returns every filter icon to neutral.
- [ ] Tap the add button. **New Issue** appears as a native modal with Cancel
      and Save, title and Markdown body fields, multi-select labels, a
      single-select optional milestone, a **Closed** switch that defaults off,
      and an optional inline due-date picker.
- [ ] In the new-issue body, enter headings, emphasis, a list, a link, and a
      fenced code block. **Write** syntax-highlights the Markdown and **Preview**
      renders it; switching repeatedly preserves the exact source text.
- [ ] Save is disabled for an empty or whitespace-only title. Create issues with
      no labels/milestone/due date and with multiple labels, a milestone, and a
      due date; each new issue appears in the list and opens its detail.
- [ ] Tap the pencil button on an issue. Change its title and Markdown body, replace and
      clear labels, select and clear its milestone, and add, change, and remove
      its due date. Turn **Closed** on and save, then edit it again and turn
      **Closed** off. Save, refresh, and relaunch to verify every change persists.
- [ ] In both the repository issue list and the issue section below a milestone,
      partially swipe an open issue to reveal **Close** and **Delete**. Cancel
      the destructive Delete confirmation and verify nothing changes. Full-swipe
      the same row to close it, then full-swipe the closed row to reopen it; each
      list refreshes only after the server mutation succeeds.
- [ ] Cancel both new and edited issues, including with the keyboard and date
      picker visible. Nothing is saved, the modal dismisses normally, and the
      issue list/detail remains usable with Dynamic Type and VoiceOver labels.
- [ ] Open both an open and a closed issue. A green open or purple closed icon
      appears beside the title and VoiceOver announces the state; author
      metadata, every colored label, Markdown body, and comments remain correct.
      Multiple labels wrap without clipping at large Dynamic Type sizes and
      VoiceOver announces each label. Links and selectable text use normal iOS
      interaction.
- [ ] Open issue #50 and verify its body renders separate paragraphs and a
      two-item unordered list. Add or edit a comment containing two paragraphs,
      an unordered list, emphasis, and a link; verify every Markdown block and
      inline style renders correctly after saving and after pull-to-refresh.
- [ ] Tap the **Add comment** icon on an issue. Enter headings, emphasis, a
      list, link, and fenced code block; **Write** syntax-highlights the
      Markdown, **Preview** renders it, and switching modes preserves the exact
      source. Save and verify the rendered comment appears after the detail
      refresh; Cancel leaves the issue unchanged.
- [ ] Each comment authored by the signed-in account has its own pencil button
      and comments by other accounts do not. Edit an owned comment in
      Write and Preview modes, save it, refresh and relaunch, and verify the
      Markdown change persists. Check Add/Edit/Cancel/Save with Dynamic Type
      and VoiceOver, including the empty-comment Save state.

## Repositories and commits

- [ ] The repository list and favorite behavior match the Issues tab. Toggle a
      favorite and confirm it persists without affecting Issues or Milestones.
- [ ] Open a repository; commit history initially selects **All**, visibly shows
      All in the navigation bar, and includes commits from multiple branches.
- [ ] Open the branch menu; All is checked. Select a branch and verify the label,
      checkmark, commits, and graph update, then return to All.
- [ ] Commit graph lanes align with their rows while scrolling; branch-out and
      merge-back connections use smooth, rounded curves that meet the correct
      lane and commit node without angular horizontal bars or overlaps.
- [ ] In **All** history, branch names use the accent color both at branch tips
      and at the first commit made on a pull-request branch, including a merged
      branch whose tip was deleted.
- [ ] Open a commit and verify its header shows the full title and description,
      full hash, author, committer, date, known branch/ref, and signature status
      above the Changed Files paths and statuses, with a thin system separator
      between the header and first file. Unsigned commits say **Unsigned**.
      Verify paragraphs, lists, links, and inline styles in a multi-line commit
      description use the repository file Preview Markdown presentation. Check
      that long values wrap and remain readable at the largest Dynamic Type
      accessibility size.
- [ ] Open a changed file and verify the diff title, old/new line numbers,
      monospaced text, addition/removal/hunk colors, vertical scrolling, and
      source-style horizontal scrolling for long lines without word wrapping,
      clipped or jumping text, or content hidden beneath the navigation bar.
- [ ] Long-press diff text and verify normal selection and copying.

## Repository files

- [ ] Switch a repository between **History** and **Files**. Each mode displays
      the expected content and switching back preserves normal navigation.
- [ ] In Files, traverse several nested folders using rows, including a folder
      and file whose names contain spaces (for example below Chezmoi's
      `private_Library`), the navigation-bar Back button, and the left-edge
      swipe. Folder contents, files, and titles match the repository hierarchy.
- [ ] In the repository root and in several nested folders, switch between
      **Files** and **History**. Each folder history contains only commits that
      affect that folder or its descendants. Switch back to Files and verify the
      same folder and navigation stack are preserved. Open a history commit and
      verify its changed-files detail opens normally.
- [ ] Open representative source files in several languages, including Rust,
      Swift, and a scripting or markup language. Keywords, strings, comments,
      types, and punctuation use plausible language-specific highlighting.
- [ ] Open a source file containing a line wider than the screen. It does not
      word-wrap, its first line starts below the navigation bar, and horizontal
      dragging pans smoothly across the complete line without blank, black,
      clipped, delayed, or jumping text. Vertical scrolling, selection, and
      copying still work.
- [ ] Open Markdown files using the supported extensions (`.md`, `.markdown`,
      `.mdown`, and `.mkd`). **Preview** is selected by default and renders
      headings, paragraphs, emphasis, links, lists, task lists, blockquotes,
      fenced code blocks, tables, and thematic rules as structured content.
- [ ] Switch a Markdown file from **Preview** to **Source**. The literal Markdown
      is syntax-highlighted, selectable, does not word-wrap, and scrolls
      horizontally for long lines. Switch back and verify the rendered preview
      is restored without stale or overlapping content.
- [ ] Switch Markdown files among **Preview**, **Source**, and **History** and
      switch other source and native-preview files between **Content** and
      **History**. Each history contains only commits that affect that exact
      file. Returning to content preserves the file path and restores its native
      preview or source presentation without stale or overlapping views.
- [ ] Open representative image, PDF, audio, and video files. Each uses the
      native preview appropriate to its media type and returns cleanly to Files.

## Pull requests

- [ ] The list defaults to the saved Open/Closed filter.
- [ ] Change the native filter menu between Open and Closed; the checkmark,
      rows, and persisted selection update.
- [ ] Open **Search Text** in the pull-request filter, enter a term, and verify
      the server-filtered rows and menu subtitle update. Combine it with Closed
      and a milestone and verify every filter applies together.
- [ ] Select a milestone and then All Milestones; the checkmark and pull-request
      rows update, and the milestone selection persists independently per
      server after relaunch.
- [ ] The pull-request filter icon uses a neutral color for Open + All
      Milestones and the app accent color whenever either filter is non-default.
- [ ] In the pull-request filter panel, Search Text, Status, and Milestone icons
      use the app accent color independently when non-default. Search text
      remains while navigating during the current app session but is empty
      after relaunch. **Clear Filters** restores empty search + Open + All
      Milestones, refreshes the rows, and returns every icon to neutral.
- [ ] Each row shows repository/number, title, author/update metadata, comment
      count, and draft/merged state where applicable. Open rows show a green
      icon, closed rows show a purple icon, and VoiceOver announces the state.
- [ ] Open a pull request and verify title, metadata, Markdown body, comments,
      changed-file reference, and matching open/closed state icon with a
      VoiceOver state announcement. Its file paths, statuses, left alignment,
      separators, and disclosure indicators match the commit changed-file list.
- [ ] Open a changed file and run the same diff checks as for a commit.

## Milestones

- [ ] Milestones uses the server → repository → milestone flow and includes
      both open and closed milestones.
- [ ] Toggle a repository favorite and confirm it persists after refresh and
      relaunch without affecting the same repository in Issues or Repos.
- [ ] Each milestone shows its title, description, state, due date, issue
      counts, and a green/amber closed/open progress bar. Lists and details show
      a green open or purple closed icon and VoiceOver announces the state.
- [ ] A milestone description containing multiple paragraphs, a list, emphasis,
      and a link uses the repository file Preview Markdown presentation in both
      the milestone list and detail.
- [ ] Open a milestone and verify every assigned issue and pull request is
      listed; tapping either opens its normal detail.
- [ ] Partially swipe an open milestone to reveal **Close** and **Delete**;
      full-swipe closes it as the primary action, and full-swiping a closed
      milestone reopens it. Delete an empty milestone after confirming. Attempt
      to delete a milestone with assigned issues or pull requests and verify an
      informational dialog explains that the assignments must be removed first.
- [ ] Tap the add button. **New Milestone** has native Cancel and Save controls,
      title and multiline description fields, a **Closed** switch that defaults
      off, and an optional inline date picker. Save is disabled for an empty or
      whitespace-only title. Create milestones both without and with a due date
      and verify each appears open.
- [ ] Tap the pencil button on a milestone. Change its title and description, add and
      change its due date, then turn Due Date off and save to clear it. Turn
      **Closed** on and save; verify the list and detail show the closed state.
      Edit it again, turn **Closed** off, and verify it reopens. Refresh and
      relaunch after each save to verify persistence; Cancel leaves every value
      unchanged. Exercise the form with the keyboard, Dynamic Type, and
      VoiceOver labels.
- [ ] Repository issue lists and issue details show the assigned milestone with
      a flag icon instead of a text label.
- [ ] Repositories without milestones show the native empty state.

## Settings, accessibility, and lifecycle

- [ ] Settings opens from the gear button on Home and is not a tab.
- [ ] Appearance changes between Auto, Light, and Dark immediately; Auto follows
      the simulator system appearance.
- [ ] Icon and appearance settings remain selected after relaunch.
- [ ] Test Light and Dark appearances for readable text, separators, graph lines,
      diff colors, menus, selection, loading, empty, and error states.
- [ ] Test at the default and at least one larger Dynamic Type size. Text remains
      readable without hiding required controls.
- [ ] VoiceOver announces tabs, navigation controls, filters, favorites, rows,
      fields, and changed-file statuses meaningfully and in a usable order.
- [ ] Rotate, background/foreground, terminate/relaunch, and temporarily disable
      networking. The app recovers without losing preferences or credentials.

## Release sign-off

- [ ] All build gates passed.
- [ ] A signed Release archive passed the version, identity, entitlement,
      privacy-manifest, and export-compliance checks above.
- [ ] Fresh-install and upgrade passes completed through AltStore PAL on an
      eligible physical iPhone.
- [ ] Every supported iOS version and required device class completed.
- [ ] Every scenario passed; any discovered failures were resolved and retested.
