When the smoke test fails, the Claude step is skipped, claude-test.sh never creates its output directory, and the usage step failed writing claude-usage.json there (runs 38781195327, 38787412095) instead of reporting that no session ran.
Run 38787412095 seeded the names, built the app and passed the smoke test's avatar and header checks in both layouts and the cards check with the one-hand UI. After the restart into the top bar, the app showed its one-time "SimpleX Lock" alert (the first launch with more than two chats, laNoticeAlert in ContentView.swift), which hid the list. The smoke test now taps its "Cancel" after that restart.
The backend now builds, and seeding reached the names (run 38781195327), but the directory could not join cinews_digest ("Error joining group cinews_digest"). The CLI's -s saves its server among the user's servers (a custom operator in the preset list, stored by agentServers), so ci_admin's next plain run put the invitation's queue on the local names server, which the directory reaches only through a preset proxy that has no route to localhost. After each -s command, that server's storage and proxy roles are now turned off in the peer's database, so later runs keep their queues on the preset servers and can still resolve names.
smp-server built and linked in the clone (run 38778506526), but cabal list-bin there first printed git's output from syncing the source packages ("HEAD is now at …"), so cp got all of it as one file name.
The clone compiled all of simplexmq, then failed on "does not exist: cbits/blst/src/server.c" (run 38776361371): cbits/blst and cbits/libbbs are git submodules, which cabal's own clones check out and this one did not.
Cabal builds the simplexmq source-repository-package from its sdist, which has apps/common/Web/static/index.html but not .well-known/, so smp-server's embedDir failed at compile time (run 38771835936). build-backend.sh now clones simplexmq at the commit cabal.project pins and builds exe:smp-server there, with the same cabal.project.local; its dependencies come from the same store.
The macOS 26 SDK step worked: every library linked. This push runs the name cases with Claude again.
On runner image 20261013.0424.1 the Command Line Tools' MacOSX.sdk points to a macOS 27 SDK, and the backend build's links failed on it ("tapi error: malformed file … unknown architecture arm64e.x1-macos") although DEVELOPER_DIR selects Xcode 26.6. use-macos26-sdk.sh points it back at the MacOSX26.5.sdk installed beside it, in all three macOS jobs.
This push runs the name cases with Claude again (run 4 failed before the app was built), after the smoke test with its new checks.
With the test directory stopped, the next "ci avatar" page waits 10 s before it fails. The smoke test taps "Show more" then, takes a screenshot that shows its colour, and requires it to be disabled. The smoke step gets BACKEND_DIR, so the ui helper can stop and start the directory. One layout only: the row's colour and state are the same in both.
The directory results must show no placeholder icon in both layouts. Then the smoke test joins ci_cards, and in both layouts: typing "ci seed" keeps the connect cards, the one-hand UI card and the banner (where shown); "Search in Directory" hides them, with the Directory header above the results; clearing the search brings the same cards back. The Claude sessions are told the profile now has that one chat.
ci_group and ci_channel get 64 px avatars from avatar.swift (a new side argument), small enough that both still arrive in one directory response; the backend's search probe fails if they do not, and the page sizes go to seed.md. The new channel ci_cards ("ci cards", also with a small avatar) is for the smoke test to join, so that the chat list shows its cards; no Claude case uses it.
The top-bar session runs N1, N4 and the full search on the chat list, where the two layouts place the "Connect to …" row and the Names and Directory parts differently. The other name cases, W4 with names, W7, W8 and G1 run once, with the one-hand UI. Run 3 covered the rest of the plan in both layouts.
claude-scope gets a cases line per layout. The prompt now describes joining as the app does it: "Open channel" / "Open group" opens the chat, and its bar joins. A run 3 session took that alert for a known entry. The prompt also points the session at the plan's rows in prompt.md.
The prompt now covers SimpleX names: setting the app up once (operators' names role off, the test names server added for names only, saved), what each seeded name is for, and the full search: type cinews, press Search, and record the parts the plan specifies (the chats already loaded, Names, Directory) with their headers and order, also in New chat. claude-scope assigns the names and contacts per layout and allows 900 turns; the session gets the names server's address.
names-resolver.py answers the smp-server's GET /v2/resolve/<[keccak-256 of the label].tld> from names.json (Keccak-256 written out, as hashlib has only SHA3; checked against known vectors), and "available" for other names. backend.sh starts it and an smp-server initialised with [NAMES] enable: on pointing at it, then seeds: cinews (listed channel), ciboth (a contact address and a channel), and contacts cialice and Robert (cibobby) that stay online with auto-accept, so the app can connect. Claims run with -s <names server>, which gives that one CLI process the local server with every role, names included, leaving the peers' preset servers as they are; a channel claims its name before it is listed. cinews_digest and cinews_weekly let one search show local chats, Names and Directory. A CLI /_connect plan checks every name before the app is built. seed.md lists the names and the server address.
The name cases need an SMP server with a [NAMES] resolver on the runner. smp-server comes from the simplexmq source-repository-package (simplexmq:exe:smp-server, buildable without client_library) and goes into the backend zip with the CLI and the directory service. This changes the backend artifact's name, so the next run rebuilds it once.
Run 38734049940 covered the plan in both layouts but left D11 unrecorded and W3 unrun after a compaction, and its D6/W6 taps met entries an earlier case had joined, so they showed the open alert instead of the join flow. Now report.md starts as a checklist of every case marked todo, the session re-reads it after a compaction, claude-scope names per layout the entries the joining cases use, and the session gets the link to paste. The prompt also says how to hold a search open (stop the directory), tap ambiguous labels by position, clear the field (focus, then Cancel), and that the keyboard capitalizes and autocorrects.
The step printed report.md and the guard log before the token search, so a leaked token would have reached the log first (GitHub masks the exact value there, but nothing from the session should be printed before the check). A dry run with a stub claude that writes the token into its report now prints nothing from it and deletes the files.
Written with simctl spawn defaults write, with and without a simulator restart, they changed nothing in runs 38729704027 and 38734049940: typing still capitalized, autocorrected ("Citester" became the profile name "Forester") and predicted. Removing them saves the restart; the Claude prompt says to check typed text in the tree.
Claude Code runs read-only commands (echo, cat, env) without asking in every permission mode, and Bash inherits CLAUDE_CODE_OAUTH_TOKEN, so allow rules alone could not stop a session from printing it (found on the desktop lane). claude-guard.py, a PreToolUse hook passed with --settings, lets through only one ui command with plain arguments and logs every decision to <layout>-guard.log, outside the session's directory. After each session, the session's files and the guard log are searched for the token (given to grep through a pipe); if it is there they are deleted before the upload and the step fails. Run 38729704027's session ran only ui commands and its files contain no token.
One Haiku 5.5 session per layout, one-hand UI first, each bounded by 700 turns, $10 and an auto-compact window of 200k tokens, so a long session does not re-read an ever larger context every turn. The validation session (run 38729704027) drove C2, D2 and D7 correctly in 55 turns for $0.03.
From the first session (run 38729704027): clear the search field with Cancel and check it before typing; the keyboard may still change words; a channel row shows the published subscriber count, not the listings' member records; with the one-hand UI later pages stack above earlier ones; disabled elements are marked; prompt.md is there to re-read after a compaction.
Run 38729704027 still capitalized and autocorrected typed text ("citester" became "Citester", "Neci" became "Neck"): the keyboard daemon reads these settings when it starts, so the simulator is restarted once after writing them.
After the smoke test, claude-test.sh runs one Claude Code session (pinned 2.1.300) per layout named in claude-scope, with the plan's cases, model, turns and budget from there. A session gets only Bash for the ui helper and Read/Write/Edit in its own directory (--restricted, dontAsk, no MCP), drives the app through ui, and writes report.md with screenshots; its transcript is stream-json. The app job declares the app-sim-claude environment; only the Claude step gets CLAUDE_CODE_OAUTH_TOKEN, and it runs only on workflow_dispatch or a head commit with [claude-test]. claude-usage.sh writes each session's turns, tokens and cost (estimated at API prices) to the job summary and claude-usage.json, even when the session fails. The plan is copied from the iOS directory search run. This run: C2, D2, D7 in the one-hand UI with Haiku 5.5, to validate the harness.
Set in com.apple.Preferences right after boot, before the app first shows the keyboard, so typed links (C4) and nonsense words (D10) reach the app as typed. The smoke test still compares typed text ignoring case.
After the M3 search in the one-hand UI, the header's y must be smaller than both results'. The search is then repeated with the top bar (ui layout top) and checked the same way, and the app is left in the one-hand UI for later steps. A layout the helper cannot set is reported as a warning, not a failure: that is the harness, not the app.
Adds tap-xy, swipe, clipboard, key, find, layout (sets the one-hand UI preference in the app group's defaults and relaunches), relaunch, text-size, directory stop/start, listings and requests (directory searches the app sent, from its log). Elements below the screen, such as keyboard keys, are counted instead of listed; shot also saves a smaller JPEG to look at and only takes plain names. The helper unsets CLAUDE_CODE_OAUTH_TOKEN, so nothing it runs inherits the token.
avatar.swift makes avatars the way the app makes profile images (JPEG 0.85 at scale 1, shrunk to at most 12500 characters). Three groups and three channels with such avatars match "ci avatar"; a CLI peer follows the search cursor to the end and records each page's entries, JSON and zstd sizes against the 10737-byte limit in seed.md. Also seeded: a long-named group and one with markdown in its description. Every entry is created with the profile JSON the app sends, and each approval is matched by the entry's name. backend.sh can also stop and start the directory and print its listings.
The admin was contact 3 of the directory, not 2 (run 38713931270), and a super user is matched by contact ID and name. The service now starts with a placeholder super user, the admin connects, and the service restarts with the ID read from its database. This run tests the group and channel registration, the CLI search, the app build and the smoke-test search.
Every run waited for macOS runners three times (core, backend, app), even
when core and backend only restored a cache, and those caches are evicted
within hours here. A plan job on ubuntu names both artifacts after the hash
of their sources and finds them in earlier runs of this branch in this
repository (actions: read); core and backend build only when none exists, and
the app job downloads from the run that built them. Artifacts are kept 30
days; the zip caches are dropped.
This run tests: the plan job, building and uploading both artifacts under
their new names (none exist yet), and the app job downloading them; then the
directory start with -y, the seeding, and the search in the app.
setup-haskell reports ~/.cabal/store, but without ~/.cabal cabal 3.10 keeps its store in XDG directories, so the saved cache held only dist-newstyle (run 38563035327: 401 MB). CABAL_DIR is set before Setup Haskell, so its cabal update, the build and the cache path agree. build-backend.sh prints the store size before building, as build-core.sh does.
The database is created by the CLI, so the service's own migration (20250924_directory_schema) asked for confirmation on stdin and the service exited. This run tests that the directory starts and the group and channel get listed.
The app is built with DIRECTORY_SERVICE_LINK set to the test directory's
short address (sed in the workflow). The smoke test types "ci seed", taps
"Search in Directory", accepts the first-search consent alert with "Search",
and expects the Directory results with #ci_group (Join group) and
#ci_channel (Join channel), retrying once if the search fails.
The steps now use scripts/ci/ios-sim/ui, which wraps AXe (tap by label or
value, type, wait, screenshot, compact accessibility tree) for scripts and
for an agent on the runner. Later steps get SIM_UDID and UI_OUT.
This run tests: the onboarding and typing steps on the new helper, the
consent alert, and the directory results in the app.
A new backend job builds simplex-chat and simplex-directory-service with the
server-capable configuration. The app job starts a chat relay and a directory
on the preset servers, registers and approves a group and a channel from a CLI
peer, and checks that a service request for "ci seed" finds both, before the
app is built. The app still uses the production directory link; the smoke
test is unchanged.
The cabal store cache now uses setup-haskell's store path, with a key prefix
per job, so rebuilds after Haskell changes reuse built dependencies.
This run tests: backend build, relay and directory start, registration and
approval over the preset servers, the service request, and that M2 still
passes.
Run 38448094338 reached the chat list and typed into the search field, which showed "Ci smoke" and the Search in Directory row, but the check looked for "ci smoke": the keyboard capitalizes the first letter. SwiftUI text fields expose no accessibility label, only the placeholder as the value of an empty field, so the fields are now tapped by value.
This run tests the smoke test end to end, with the core library restored from cache.
Adds the ios-sim workflow, triggered only by pushes to f/ci-ios-sim and by workflow_dispatch.
The core job builds lib:simplex-chat with cabal using the flake's iOS settings (swift JSON, client library, static roll-up archive, threaded RTS, CommonCrypto), packages it as the Hydra zip, and patches it for the simulator with scripts/ios/prepare.sh and mac2ios. The app job builds "SimpleX (iOS)" for the arm64 simulator against it and drives onboarding, the chat list and the search field with AXe, saving screenshots and accessibility trees.
This first run tests the whole pipeline, mainly whether the core library builds and links.
* core: release core library to simplex-chat-libs every time cabal version changes
* implement
* test build
* update libs release
* test library release
* create release commit, so the date is correct
* rework to create release in advance in maybe-release job
* remove test build
* move postgresql job back
* test build
* remove test build
---------
Co-authored-by: Evgeny @ SimpleX Chat <259188159+evgeny-simplex@users.noreply.github.com>
* core: cancel the chat callback on interrupt
* badges: add multi-use codes
* badges: add the managed group
* badges: document multi-use codes and the group
* core: sign test badges with whole-second expiry
* badges: rework tracker and reply texts
* core: fix races when sending and receiving files (in tests, or when server is very fast)
* update simplexmq
---------
Co-authored-by: Evgeny @ SimpleX Chat <259188159+evgeny-simplex@users.noreply.github.com>