eleven / messenger Public

Clone
git clone https://amadan.net/eleven/messenger

Plain git — no account needed to clone.

Download

Download this file

1import XCTest
2@testable import LChatCore
3
4// 🤖 Parity + drift gates for the default-command rule (the web's apps.js
5// defaultCommandFor + chat.js composer routing). The drift gates PARSE
6// web/static/apps/todo.js from the repo, so changing the web app's
7// defaultCommand, placeholder, or chatIntro fails here until Swift follows.
8final class AppDefaultCommandTests: XCTestCase {
9 private var repoRoot: URL {
10 URL(fileURLWithPath: #filePath)
11 .deletingLastPathComponent() // Tests/LChatCoreTests
12 .deletingLastPathComponent() // Tests
13 .deletingLastPathComponent() // LChatCore
14 .deletingLastPathComponent() // ios
15 .deletingLastPathComponent() // repo
16 }
17
18 // The first "…"-quoted value after `key:` in a JS source.
19 private func jsString(_ source: String, key: String) -> String? {
20 guard let re = try? NSRegularExpression(pattern: key + "\\s*:\\s*\\n?\\s*\"([^\"]+)\""),
21 let m = re.firstMatch(in: source, range: NSRange(source.startIndex..., in: source)),
22 let r = Range(m.range(at: 1), in: source) else { return nil }
23 return String(source[r])
24 }
25
26 // ---- gating (apps.js defaultCommandFor) --------------------------------
27
28 func testSpecGating() {
29 XCTAssertNotNil(AppDefaultCommand.spec(appID: "todo", hasBot: false))
30 XCTAssertNil(AppDefaultCommand.spec(appID: "todo", hasBot: true),
31 "an LLM member makes plain text a real message again")
32 XCTAssertNotNil(AppDefaultCommand.spec(appID: "remind", hasBot: false))
33 XCTAssertNil(AppDefaultCommand.spec(appID: "remind", hasBot: true))
34 XCTAssertNil(AppDefaultCommand.spec(appID: nil, hasBot: false))
35 }
36
37 // Reminders' verb is its GLOBAL /remind (the web made it the default — no
38 // local twin), so it both routes and leads the thread's typeahead.
39 func testRemindSpecRoutesItsGlobalVerb() throws {
40 let dc = try XCTUnwrap(AppDefaultCommand.spec(appID: "remind", hasBot: false))
41 XCTAssertEqual(AppDefaultCommand.routing("call mum in 2h", spec: dc), .event("call mum in 2h"))
42 XCTAssertEqual(AppDefaultCommand.routing("/remind call mum in 2h", spec: dc), .event("call mum in 2h"))
43 XCTAssertEqual(AppDefaultCommand.routing("/remind", spec: dc), .swallow)
44 XCTAssertEqual(AppDefaultCommand.routing("/theme lagoon", spec: dc), .passthrough)
45 XCTAssertEqual(AppDefaultCommand.localCommands(appID: "remind").map(\.name), ["/remind"])
46 }
47
48 func testRemindSpecMatchesRemindJS() throws {
49 let js = try String(contentsOf: repoRoot.appending(path: "web/static/apps/remind.js"),
50 encoding: .utf8)
51 let dc = try XCTUnwrap(AppDefaultCommand.specs["remind"])
52 XCTAssertEqual(jsString(js, key: "defaultCommand"), dc.verbs.first,
53 "Swift default command has drifted from remind.js")
54 XCTAssertEqual(jsString(js, key: "composerPlaceholder"), dc.placeholder,
55 "Swift placeholder has drifted from remind.js")
56 XCTAssertEqual(jsString(js, key: "chatIntro"), AppChatProjection.intros["remind"],
57 "native Reminders intro has drifted from remind.js chatIntro")
58 // The "when?" push-back mirrors the web's usage reply wording.
59 XCTAssertTrue(js.contains(AppDefaultCommand.remindWhenPrompt),
60 "remindWhenPrompt has drifted from remind.js's usage reply")
61 }
62
63 // ---- routing (chat.js composer) ----------------------------------------
64
65 func testPlainTextRoutes() throws {
66 let dc = try XCTUnwrap(AppDefaultCommand.spec(appID: "todo", hasBot: false))
67 XCTAssertEqual(AppDefaultCommand.routing("buy milk", spec: dc), .event("buy milk"))
68 XCTAssertEqual(AppDefaultCommand.routing(" buy milk ", spec: dc), .event("buy milk"))
69 XCTAssertEqual(AppDefaultCommand.routing(" ", spec: dc), .passthrough,
70 "empty drafts are the composer's no-op, not ours")
71 }
72
73 func testVerbsRouteWithVerbStripped() throws {
74 let dc = try XCTUnwrap(AppDefaultCommand.spec(appID: "todo", hasBot: false))
75 XCTAssertEqual(AppDefaultCommand.routing("/add buy milk", spec: dc), .event("buy milk"))
76 XCTAssertEqual(AppDefaultCommand.routing("/ADD buy milk", spec: dc), .event("buy milk"))
77 // The global alias adds here too, exactly as the web runs /todo in-thread.
78 XCTAssertEqual(AppDefaultCommand.routing("/todo buy milk", spec: dc), .event("buy milk"))
79 }
80
81 func testBareVerbIsSwallowedNeverPosted() throws {
82 // The web answers a bare verb with a private usage reply; native has no
83 // notice surface, so it consumes the draft-send instead — under no
84 // circumstances may "/add" land in the thread as a chat bubble.
85 let dc = try XCTUnwrap(AppDefaultCommand.spec(appID: "todo", hasBot: false))
86 XCTAssertEqual(AppDefaultCommand.routing("/add", spec: dc), .swallow)
87 XCTAssertEqual(AppDefaultCommand.routing("/add ", spec: dc), .swallow)
88 XCTAssertEqual(AppDefaultCommand.routing("/todo", spec: dc), .swallow)
89 }
90
91 func testOtherSlashTextPassesThrough() throws {
92 let dc = try XCTUnwrap(AppDefaultCommand.spec(appID: "todo", hasBot: false))
93 XCTAssertEqual(AppDefaultCommand.routing("/theme lagoon", spec: dc), .passthrough,
94 "a leading / always means command, never content")
95 XCTAssertEqual(AppDefaultCommand.routing("/added to cart", spec: dc), .passthrough,
96 "the verb must be its own token, not a prefix")
97 }
98
99 // ---- drift gates vs web/static/apps/todo.js ----------------------------
100
101 func testSpecMatchesTodoJS() throws {
102 let js = try String(contentsOf: repoRoot.appending(path: "web/static/apps/todo.js"),
103 encoding: .utf8)
104 let dc = try XCTUnwrap(AppDefaultCommand.specs["todo"])
105 XCTAssertEqual(jsString(js, key: "defaultCommand"), dc.verbs.first,
106 "Swift default command has drifted from todo.js")
107 XCTAssertEqual(jsString(js, key: "composerPlaceholder"), dc.placeholder,
108 "Swift placeholder has drifted from todo.js")
109 }
110
111 func testChatIntroMatchesTodoJS() throws {
112 let js = try String(contentsOf: repoRoot.appending(path: "web/static/apps/todo.js"),
113 encoding: .utf8)
114 XCTAssertEqual(jsString(js, key: "chatIntro"), AppChatProjection.intros["todo"],
115 "native To-do intro has drifted from todo.js chatIntro")
116 }
117}
118