import XCTest @testable import LChatCore // 🤖 Parity + drift gates for the default-command rule (the web's apps.js // defaultCommandFor + chat.js composer routing). The drift gates PARSE // web/static/apps/todo.js from the repo, so changing the web app's // defaultCommand, placeholder, or chatIntro fails here until Swift follows. final class AppDefaultCommandTests: XCTestCase { private var repoRoot: URL { URL(fileURLWithPath: #filePath) .deletingLastPathComponent() // Tests/LChatCoreTests .deletingLastPathComponent() // Tests .deletingLastPathComponent() // LChatCore .deletingLastPathComponent() // ios .deletingLastPathComponent() // repo } // The first "…"-quoted value after `key:` in a JS source. private func jsString(_ source: String, key: String) -> String? { guard let re = try? NSRegularExpression(pattern: key + "\\s*:\\s*\\n?\\s*\"([^\"]+)\""), let m = re.firstMatch(in: source, range: NSRange(source.startIndex..., in: source)), let r = Range(m.range(at: 1), in: source) else { return nil } return String(source[r]) } // ---- gating (apps.js defaultCommandFor) -------------------------------- func testSpecGating() { XCTAssertNotNil(AppDefaultCommand.spec(appID: "todo", hasBot: false)) XCTAssertNil(AppDefaultCommand.spec(appID: "todo", hasBot: true), "an LLM member makes plain text a real message again") XCTAssertNotNil(AppDefaultCommand.spec(appID: "remind", hasBot: false)) XCTAssertNil(AppDefaultCommand.spec(appID: "remind", hasBot: true)) XCTAssertNil(AppDefaultCommand.spec(appID: nil, hasBot: false)) } // Reminders' verb is its GLOBAL /remind (the web made it the default — no // local twin), so it both routes and leads the thread's typeahead. func testRemindSpecRoutesItsGlobalVerb() throws { let dc = try XCTUnwrap(AppDefaultCommand.spec(appID: "remind", hasBot: false)) XCTAssertEqual(AppDefaultCommand.routing("call mum in 2h", spec: dc), .event("call mum in 2h")) XCTAssertEqual(AppDefaultCommand.routing("/remind call mum in 2h", spec: dc), .event("call mum in 2h")) XCTAssertEqual(AppDefaultCommand.routing("/remind", spec: dc), .swallow) XCTAssertEqual(AppDefaultCommand.routing("/theme lagoon", spec: dc), .passthrough) XCTAssertEqual(AppDefaultCommand.localCommands(appID: "remind").map(\.name), ["/remind"]) } func testRemindSpecMatchesRemindJS() throws { let js = try String(contentsOf: repoRoot.appending(path: "web/static/apps/remind.js"), encoding: .utf8) let dc = try XCTUnwrap(AppDefaultCommand.specs["remind"]) XCTAssertEqual(jsString(js, key: "defaultCommand"), dc.verbs.first, "Swift default command has drifted from remind.js") XCTAssertEqual(jsString(js, key: "composerPlaceholder"), dc.placeholder, "Swift placeholder has drifted from remind.js") XCTAssertEqual(jsString(js, key: "chatIntro"), AppChatProjection.intros["remind"], "native Reminders intro has drifted from remind.js chatIntro") // The "when?" push-back mirrors the web's usage reply wording. XCTAssertTrue(js.contains(AppDefaultCommand.remindWhenPrompt), "remindWhenPrompt has drifted from remind.js's usage reply") } // ---- routing (chat.js composer) ---------------------------------------- func testPlainTextRoutes() throws { let dc = try XCTUnwrap(AppDefaultCommand.spec(appID: "todo", hasBot: false)) XCTAssertEqual(AppDefaultCommand.routing("buy milk", spec: dc), .event("buy milk")) XCTAssertEqual(AppDefaultCommand.routing(" buy milk ", spec: dc), .event("buy milk")) XCTAssertEqual(AppDefaultCommand.routing(" ", spec: dc), .passthrough, "empty drafts are the composer's no-op, not ours") } func testVerbsRouteWithVerbStripped() throws { let dc = try XCTUnwrap(AppDefaultCommand.spec(appID: "todo", hasBot: false)) XCTAssertEqual(AppDefaultCommand.routing("/add buy milk", spec: dc), .event("buy milk")) XCTAssertEqual(AppDefaultCommand.routing("/ADD buy milk", spec: dc), .event("buy milk")) // The global alias adds here too, exactly as the web runs /todo in-thread. XCTAssertEqual(AppDefaultCommand.routing("/todo buy milk", spec: dc), .event("buy milk")) } func testBareVerbIsSwallowedNeverPosted() throws { // The web answers a bare verb with a private usage reply; native has no // notice surface, so it consumes the draft-send instead — under no // circumstances may "/add" land in the thread as a chat bubble. let dc = try XCTUnwrap(AppDefaultCommand.spec(appID: "todo", hasBot: false)) XCTAssertEqual(AppDefaultCommand.routing("/add", spec: dc), .swallow) XCTAssertEqual(AppDefaultCommand.routing("/add ", spec: dc), .swallow) XCTAssertEqual(AppDefaultCommand.routing("/todo", spec: dc), .swallow) } func testOtherSlashTextPassesThrough() throws { let dc = try XCTUnwrap(AppDefaultCommand.spec(appID: "todo", hasBot: false)) XCTAssertEqual(AppDefaultCommand.routing("/theme lagoon", spec: dc), .passthrough, "a leading / always means command, never content") XCTAssertEqual(AppDefaultCommand.routing("/added to cart", spec: dc), .passthrough, "the verb must be its own token, not a prefix") } // ---- drift gates vs web/static/apps/todo.js ---------------------------- func testSpecMatchesTodoJS() throws { let js = try String(contentsOf: repoRoot.appending(path: "web/static/apps/todo.js"), encoding: .utf8) let dc = try XCTUnwrap(AppDefaultCommand.specs["todo"]) XCTAssertEqual(jsString(js, key: "defaultCommand"), dc.verbs.first, "Swift default command has drifted from todo.js") XCTAssertEqual(jsString(js, key: "composerPlaceholder"), dc.placeholder, "Swift placeholder has drifted from todo.js") } func testChatIntroMatchesTodoJS() throws { let js = try String(contentsOf: repoRoot.appending(path: "web/static/apps/todo.js"), encoding: .utf8) XCTAssertEqual(jsString(js, key: "chatIntro"), AppChatProjection.intros["todo"], "native To-do intro has drifted from todo.js chatIntro") } }