// Decodes the XRPC answers the screenshots recorded with the app's own models, // and fails naming the first field each can't read. Run by ios/check-models.sh. import Foundation func check(_ type: T.Type, _ data: Data) throws { _ = try JSONDecoder().decode(type, from: data) } let readers: [String: (Data) throws -> Void] = [ "describeServer": { try check(ServerDescription.self, $0) }, "getThread": { try check(PullThread.self, $0) }, "getRepo": { try check(RepoDetail.self, $0) }, "getTree": { try check(Tree.self, $0) }, "getFile": { try check(FileAnswer.self, $0) }, "listItems": { try check(ItemList.self, $0) }, "getActorRepos": { try check(RepoList.self, $0) }, "getProfile": { try check(Profile.self, $0) }, "listBlocks": { try check(BlockList.self, $0) }, "getAccountData": { try check(AccountData.self, $0) }, "getFeed": { try check(Feed.self, $0) }, "listNotifications": { try check(NotificationList.self, $0) }, "listBranches": { try check(BranchList.self, $0) }, "listCommits": { try check(CommitList.self, $0) }, "getCommit": { try check(CommitAnswer.self, $0) }, "listReleases": { try check(ReleaseList.self, $0) }, "listRuns": { try check(RunList.self, $0) }, "getCheck": { try check(RunDetail.self, $0) }, ] let path = CommandLine.arguments[1] let fixtures = try JSONSerialization.jsonObject(with: Data(contentsOf: URL(fileURLWithPath: path))) as! [String: Any] let answers = fixtures["answers"] as! [String: Any] var read = 0 var failed = 0 for (url, answer) in answers.sorted(by: { $0.key < $1.key }) { guard url.hasPrefix("/xrpc/dev.knotless."), let name = url.dropFirst("/xrpc/dev.knotless.".count).split(separator: "?").first, let reader = readers[String(name)], let recorded = answer as? [String: Any], recorded["status"] as? Int == 200, let body = recorded["body"] as? String else { continue } do { try reader(Data(body.utf8)) read += 1 } catch { print("\(url)\n \(error)") failed += 1 } } print("\(read) answers read, \(failed) not") if failed > 0 || read == 0 { exit(1) } // What the merge section says of each state: nothing from the command line, and // the queue offered only to a pull it would take. func status(_ state: String, checks: String = "none", reviews: String? = nil, queueOnly: Bool = false) -> MergeStatus { MergeStatus(state: state, checks: checks, queueOnly: queueOnly, reviews: reviews, may: .init(merge: true, queue: true)) } let saying: [(MergeStatus, String, Bool)] = [ (status("clean", checks: "passed"), "Ready to merge", true), (status("clean", checks: "passed", queueOnly: true), "Ready for the merge queue", true), (status("conflicting"), "Conflicts with main", false), (status("draft"), "Still a draft", false), (status("blocked", checks: "failed"), "Checks failed", false), (status("blocked", checks: "running"), "Checks are running", true), (status("blocked", checks: "pending"), "Not checked yet", true), (status("blocked", checks: "passed", reviews: "blocked"), "Review required", false), ] var wrong = 0 func expect(_ held: Bool, _ what: String) { if !held { print(what) wrong += 1 } } for (s, title, queues) in saying { let said = Standing.of(s, base: "main") expect(said.title == title, "\(s.state)/\(s.checks ?? ""): says \(said.title), not \(title)") expect(Standing.canQueue(s) == queues, "\(title): the queue \(queues ? "isn't" : "is") offered") } let ejected = Standing.ejected(reason: "its batch failed its checks at 6f89484c63a3, on main at 226488be4afa: test (failure). Their logs: `knotless run list knotless://a/b --pull at://c --log`") expect(ejected.lines == ["Its batch failed its checks: test (failure)"], "ejected says \(ejected.lines)") let unreviewed = Standing.ejected(reason: "it is not reviewed as main asks: reviews 0 of 1. It waited 30m for that review. Anyone but its author who may write approves it with `knotless comment add knotless://a/b c --approve`; the owner, its author or not, merges past it with `--force`") expect(unreviewed.lines == ["It is not reviewed as main asks: reviews 0 of 1. It waited 30m for that review"], "ejected says \(unreviewed.lines)") for said in saying.map({ Standing.of($0.0, base: "main") }) + [ejected, unreviewed] { for line in said.lines where line.contains("knotless ") || line.contains("at://") || line.contains("--") { expect(false, "reads like the command line: \(line)") } } // The patterns and hues of web/src/lib/format.ts, and where a picture is found. func cells(_ seed: String) -> [[Int]] { Identicon.cells(seed).map { [$0.x, $0.y] } } let person = Person(did: "did:plc:a", handle: "a.test") func said(_ uri: String, reply: String? = nil) -> Comment { Comment(uri: uri, author: person, createdAt: "t", reply: reply) } expect(Comment.threaded([said("a"), said("b"), said("c", reply: "a"), said("d", reply: "gone")]).compactMap(\.uri) == ["a", "c", "b", "d"], "a thread's answers aren't under it") expect(RecentSearches.decode(RecentSearches.adding("b", to: "a\nb\nc")) == ["b", "a", "c"], "a repeated search isn't moved up") let fences = MarkdownFences.ranges("

a

if a < b && c {\n}\n
plain\n
") expect(fences == ["if a < b && c {\n}": "keyword:0,2"], "a fence's colours weren't found") expect(Identicon.hue("did:plc:abe24xewga4qdbzjcxmuzdt3") == 276, "a hue differs from the pages'") expect(cells("did:plc:abe24xewga4qdbzjcxmuzdt3") == [[0,1],[4,1],[0,3],[4,3],[0,4],[4,4],[1,0],[3,0],[1,2],[3,2],[2,2],[2,4]], "a pattern differs from the pages'") expect(Identicon.hue("Claude Opus 5.5") == 358, "a hue differs from the pages'") expect(cells("Claude Opus 5.5") == [[0,3],[4,3],[1,0],[3,0],[1,1],[3,1],[1,3],[3,3],[2,0],[2,2]], "a pattern differs from the pages'") let place = MarkdownPlace(repo: RepoRef(did: "did:plc:o", handle: "o.test", name: "r"), ref: "main") expect(place.pictures("../assets/logo.svg?v=1").first?.absoluteString == "https://knotless.dev/o.test/r/raw/main/assets/logo.svg?v=1", "a relative picture: \(place.pictures("../assets/logo.svg?v=1"))") expect(place.pictures("javascript:alert(1)").isEmpty, "only http and https pictures load") let blob = "https://pds.test/xrpc/com.atproto.sync.getBlob?did=did:plc:a&cid=bafa" let said = MarkdownPlace(repo: RepoRef(did: "did:plc:o", handle: "o.test", name: "r"), subject: "at://did:plc:a/dev.knotless.comment/1") expect(said.pictures(blob).first?.path == "/xrpc/dev.knotless.getAttachment" && said.pictures(blob).count == 2, "an attached picture: \(said.pictures(blob))") expect(place.pictures(blob).map(\.absoluteString) == [blob], "another's picture stays where it is") // The notifications among preferences of other kinds, which it passes over. let answered = #"{"preferences":[{"$type":"dev.knotless.getPreferences#appearancePref","theme":"dark"},{"$type":"dev.knotless.getPreferences#notificationPref","reasons":[{"reason":"star","enabled":false}],"followedOnly":true},{"$type":"dev.knotless.later"}]}"# let heard = try JSONDecoder().decode(Preferences.self, from: Data(answered.utf8)).notifications expect(heard == NotificationChoices(reasons: [.init(reason: "star", enabled: false)], followedOnly: true), "the notifications weren't found among the preferences: \(String(describing: heard))") var table = MarkdownTable(header: "| a | b | c |", dashes: "|:--|--:|:-:|") expect(table?.header == ["a", "b", "c"] && table?.alignments == [.leading, .trailing, .center], "a table's head: \(String(describing: table))") table?.add("| one \\| two | `x` |") table?.add("| 1 | 2 | 3 | 4 |") expect(table?.rows == [["one | two", "`x`", ""], ["1", "2", "3"]], "a table's rows: \(String(describing: table?.rows))") expect(MarkdownTable(header: "a | b", dashes: "--- | ---") != nil, "a table with no pipes at its ends") expect(MarkdownTable(header: "| a | b |", dashes: "| --- |") == nil, "a table whose columns don't match") expect(MarkdownTable(header: "a | b", dashes: "not | dashes") == nil, "words as a table's dashes") expect(MarkdownTable(header: "just words", dashes: "---") == nil, "a rule as a table's dashes") // The tasks web/src/lib/tasks.ts finds, each ticked on its own line. let written = "- [ ] one\n* [x] two\n3. [X] three\n4) [ ] four\n> - [ ] quoted\n - [ ] under\n-[ ] no\n- [ ]no\n- [y] no\n```\n- [ ] fenced\n```\n~~~\n- [x] fenced\n~~~\n- [ ]" expect(MarkdownTasks.all(written).map(\.done) == [false, true, true, false, false, false, false], "the tasks found: \(MarkdownTasks.all(written))") expect(MarkdownTasks.ticked("- [ ] a\n```\n- [ ] b\n```\n- [ ] c", 1, done: true) == "- [ ] a\n```\n- [ ] b\n```\n- [x] c", "a tick rewrote another line") expect(MarkdownTasks.ticked("é\n- [x] 🦊 a\r\n", 0, done: false) == "é\n- [ ] 🦊 a\r\n", "an untick changed more than its mark") expect(MarkdownTasks.ticked("- [ ] a", 1, done: true) == nil, "a tick of no task") // The folds the web pages draw, `
` to `
`. let folded = "a\n
\n Claude \n\n```\n
\n```\n
in
\n
\n
\n\nb".components(separatedBy: "\n") expect(MarkdownFolds.fold(folded, at: 0, end: folded.count) == nil, "a line opening no fold") expect(MarkdownFolds.fold(folded, at: 1, end: folded.count) == .init(summary: "Claude", open: true, inside: 3..<10, next: 11), "a fold with fenced code and folds inside: \(String(describing: MarkdownFolds.fold(folded, at: 1, end: folded.count)))") expect(MarkdownFolds.fold(folded, at: 7, end: folded.count) == .init(summary: "in", open: false, inside: 8..<8, next: 8), "a fold on one line") expect(MarkdownFolds.fold(folded, at: 8, end: 9) == .init(summary: "Details", open: false, inside: 9..<9, next: 9), "a fold left open, with no summary") expect(MarkdownFolds.fold(["
words"], at: 0, end: 1) == nil, "words beside a fold's tag") if wrong > 0 { exit(1) } // Every write the app makes, for check-writes.mjs to hold to its lexicon. let repo = RepoRef(did: "did:plc:bcgltzqazw5tb6k2g3ttenbj", handle: "alice.test", name: "tally") let pull = "at://did:plc:bcgltzqazw5tb6k2g3ttenbj/dev.knotless.pull/3mzlqso3vpyqg" let choices = NotificationChoices(reasons: [.init(reason: "star", enabled: false), .init(reason: "mention", enabled: true)], followedOnly: true) let writes: [Write] = [ Writes.comment(repo: repo, subject: pull, body: "Looks right", verdict: nil), Writes.comment(repo: repo, subject: pull, body: "", verdict: "approve"), Writes.comment(repo: repo, subject: pull, body: "Not yet", verdict: "changes"), Writes.issue(repo: repo, title: "It breaks", body: ""), Writes.issue(repo: repo, title: "It breaks", body: "On empty files"), Writes.react(repo: repo, subject: pull, content: "+1", undo: false), Writes.react(repo: repo, subject: pull, content: "+1", undo: true), Writes.setState(repo: repo, subject: pull, state: "closed"), Writes.setState(repo: repo, subject: pull, state: "open"), Writes.merge(repo: repo, pull: pull), Writes.queue(repo: repo, pull: pull), Writes.leaveQueue(repo: repo, pull: pull), Writes.star(repo: repo, undo: false), Writes.star(repo: repo, undo: true), Writes.fork(repo: repo), Writes.hear(repo: repo, level: "all"), Writes.hear(repo: repo, level: "releases"), Writes.hear(repo: repo, level: "ignore"), Writes.hear(repo: repo, level: "participating"), Writes.follow(did: repo.did, undo: false), Writes.follow(did: repo.did, undo: true), Writes.rerun(repo: repo, pull: pull, ref: nil, workflow: nil), Writes.rerun(repo: repo, pull: pull, ref: nil, workflow: "test"), Writes.rerun(repo: repo, pull: nil, ref: "refs/heads/main", workflow: "deploy"), Writes.seen(at: "2026-11-05T12:00:00.000Z"), Writes.edit(repo: repo, subject: pull, body: "- [x] Done"), Writes.registerPush(token: "0a1b", environment: "sandbox"), Writes.unregisterPush(token: "0a1b"), Writes.report(subject: repo.did, reason: "spam", text: ""), Writes.report(subject: pull, reason: "rude", text: "Unkind to a new contributor"), Writes.block(subject: repo.did, undo: false), Writes.block(subject: repo.did, undo: true), Writes.block(subject: "bob.test", repo: repo.uri, undo: false), Writes.block(subject: repo.did, repo: repo.uri, undo: true), Writes.deleteData(did: repo.did, limit: 100), Writes.notifications(choices), ] let made = writes.map { ["method": $0.method, "input": $0.body] as [String: Any] } try JSONSerialization.data(withJSONObject: made).write(to: URL(fileURLWithPath: CommandLine.arguments[2]))