Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
10 changes: 9 additions & 1 deletion .github/workflows/ci.yml
Original file line number Diff line number Diff line change
Expand Up @@ -320,7 +320,7 @@ jobs:
run: npm test --prefix js/packages/truapi-host

playground:
name: Playground (build + lint)
name: Playground (build + lint + unit)
runs-on: ubuntu-latest
needs: ts-client
env:
Expand All @@ -334,6 +334,10 @@ jobs:
with:
node-version: 22

- uses: oven-sh/setup-bun@0c5077e51419868618aeaa5fe8019c62421857d6 # v2.2.0
with:
bun-version: latest

- name: Download codegen output
uses: actions/download-artifact@3e5f45b2cfb9172054b4087a40e8e0b5a5461e7c # v8.0.1
with:
Expand Down Expand Up @@ -364,6 +368,10 @@ jobs:
working-directory: playground
run: yarn lint

- name: Unit tests
working-directory: playground
run: yarn test:unit

explorer:
name: Explorer (build + lint)
runs-on: ubuntu-latest
Expand Down
2 changes: 1 addition & 1 deletion Makefile
Original file line number Diff line number Diff line change
Expand Up @@ -249,7 +249,7 @@ check: ## Full verification suite (build, fmt, clippy, test, TS tests, playgroun
cargo test --workspace --all-features --all-targets
cd $(TRUAPI_PKG) && npm run build && npm test
cd $(HOST_WASM_PKG) && npm install --no-fund --no-audit && npm run build && npm test
cd $(PLAYGROUND) && yarn build && yarn lint
cd $(PLAYGROUND) && yarn build && yarn lint && yarn test:unit

clean: ## Remove local build/test artifacts without deleting dependencies.
cargo clean
Expand Down
4 changes: 3 additions & 1 deletion README.md
Original file line number Diff line number Diff line change
Expand Up @@ -200,7 +200,9 @@ does not provision or pair a signer-bot user.

To exercise the shared-core Chat path with the first-party TrUAPI Playground
worker, build and serve the local product, install its worker into the
simulator app's product storage, and open its native Chat application:
simulator app's product storage, and open its native Chat application. The
worker drives all six Chat methods, so a host without bot registration reports
that row red:

```bash
make ios-chat-run
Expand Down
2 changes: 1 addition & 1 deletion explorer/README.md
Original file line number Diff line number Diff line change
Expand Up @@ -16,7 +16,7 @@ Because the matrix is regenerated from `diagnosis-reports/` on every `dev` / `bu

### Data shape

[`src/data/compatibility-types.ts`](src/data/compatibility-types.ts) holds the schema. Each method row carries one `pass | fail | null` entry per host column; `null` means the method was absent from (or skipped in) that host's report. Methods with no measurement on any host are dropped from the matrix. Columns are labelled by host mode (`Web` / `Desktop` / `Android` / `iOS`); when two reports share a mode, the filename disambiguates the label.
[`src/data/compatibility-types.ts`](src/data/compatibility-types.ts) holds the schema. Each method row carries one `pass | fail | null` entry per host column; `null` means the method was absent from (or skipped in) that host's report. Methods with no measurement on any host carry no matrix row, and each compatibility section renders them as unreported gaps for its execution kind. Columns are labelled by host mode (`Web` / `Desktop` / `Android` / `iOS`); when two reports share a mode, the filename disambiguates the label.

### Standalone CLI

Expand Down
5 changes: 5 additions & 0 deletions explorer/src/data/types.ts
Original file line number Diff line number Diff line change
Expand Up @@ -17,9 +17,14 @@ export interface MethodInfo {
errorType?: string;
}

/** Trusted executable kind required to reach a service. */
export type ProductExecutionKind = "Spa" | "Chat";

/** A grouping of related methods. */
export interface ServiceInfo {
name: string;
/** Executable kind the host must attach, or unrestricted when absent. */
requiredExecution?: ProductExecutionKind;
methods: MethodInfo[];
}

Expand Down
37 changes: 21 additions & 16 deletions explorer/src/pages/CompatibilityPage.tsx
Original file line number Diff line number Diff line change
@@ -1,7 +1,7 @@
import { Fragment, useState } from "react";
import { Link, useOutletContext } from "react-router-dom";
import { Check, ChevronDown, Minus, X } from "lucide-react";
import type { VersionEntry } from "../data/types";
import type { ProductExecutionKind, VersionEntry } from "../data/types";
import { methodPath } from "../data/registry";
import { chatCompatibility, compatibility } from "../data/compatibility";
import type {
Expand Down Expand Up @@ -88,6 +88,7 @@ export default function CompatibilityPage() {
<CompatibilitySection
title="SPA compatibility"
description="API coverage measured from the visible SPA execution."
execution="Spa"
matrix={compatibility}
version={version}
expandedId={expandedId}
Expand All @@ -96,6 +97,7 @@ export default function CompatibilityPage() {
<CompatibilitySection
title="Chat compatibility"
description="Chat API coverage measured from the product's native Chat worker."
execution="Chat"
matrix={chatCompatibility}
version={version}
expandedId={expandedId}
Expand All @@ -109,13 +111,15 @@ export default function CompatibilityPage() {
function CompatibilitySection({
title,
description,
execution,
matrix,
version,
expandedId,
onToggle,
}: {
title: string;
description: string;
execution: ProductExecutionKind;
matrix: CompatibilityMatrix;
version: VersionEntry;
expandedId: string | null;
Expand Down Expand Up @@ -149,27 +153,28 @@ function CompatibilitySection({
</thead>
<tbody>
{version.services
.filter((service) =>
execution === "Chat"
? service.requiredExecution === "Chat"
: service.requiredExecution === undefined,
)
.map((service) => ({
name: service.name,
// Only methods the matrix actually measured. Methods absent from
// the matrix (e.g. skipped services) are dropped, and a service
// left with none is not rendered at all.
methods: service.methods.flatMap((m) => {
// Every generated method, measured or not. A method with no
// matrix row renders as not-reported across all hosts rather
// than vanishing, so an unexercised method reads as a gap
// instead of shrinking the denominator.
methods: service.methods.map((m) => {
const id = `${service.name}/${m.name}`;
const row = byId.get(id);
return row
? [
{
name: m.name,
id,
results: row.results,
details: row.details,
},
]
: [];
return {
name: m.name,
id,
results: row?.results,
details: row?.details,
};
}),
}))
.filter((service) => service.methods.length > 0)
.map((service, i) => (
<ServiceRows
key={service.name}
Expand Down
16 changes: 16 additions & 0 deletions ios/truapi-host/Sources/TrUAPIHost/TrUAPIHost.swift
Original file line number Diff line number Diff line change
Expand Up @@ -434,6 +434,12 @@ public protocol ChatHostBridge: AnyObject, Sendable {
func createRoom(roomId: String, name: String, icon: String) throws
-> ChatRoomRegistrationStatus

/// Register or resolve a native product Chat bot. The core has bounded and
/// normalized these arguments and screened the icon scheme; escaping them
/// for the surface that renders them is still the host's job.
func registerBot(botId: String, name: String, icon: String) throws
-> ChatBotRegistrationStatus

/// Persist a text message in native Chat storage.
func postTextMessage(roomId: String, text: String) throws -> String

Expand Down Expand Up @@ -484,6 +490,16 @@ private final class ChatCallbackAdapter: NativeChatCallbacks, @unchecked Sendabl
}
}

func registerBot(
botId: String,
name: String,
icon: String
) throws -> ChatBotRegistrationStatus {
try withHostRejection {
try bridge.registerBot(botId: botId, name: name, icon: icon)
}
}

func postTextMessage(roomId: String, text: String) throws -> String {
try withHostRejection {
try bridge.postTextMessage(roomId: roomId, text: text)
Expand Down
75 changes: 75 additions & 0 deletions ios/truapi-host/Sources/TrUAPIHost/truapi.swift
Original file line number Diff line number Diff line change
Expand Up @@ -4021,6 +4021,81 @@ public func FfiConverterTypeChatActionPayload_lower(_ value: ChatActionPayload)



/**
* Whether the bot was newly registered or already existed.
*/

public enum ChatBotRegistrationStatus: Equatable, Hashable {

/**
* The bot was registered.
*/
case new
/**
* A bot with this ID already existed.
*/
case exists





}

#if compiler(>=6)
extension ChatBotRegistrationStatus: Sendable {}
#endif

#if swift(>=5.8)
@_documentation(visibility: private)
#endif
public struct FfiConverterTypeChatBotRegistrationStatus: FfiConverterRustBuffer {
typealias SwiftType = ChatBotRegistrationStatus

public static func read(from buf: inout (data: Data, offset: Data.Index)) throws -> ChatBotRegistrationStatus {
let variant: Int32 = try readInt(&buf)
switch variant {

case 1: return .new

case 2: return .exists

default: throw UniffiInternalError.unexpectedEnumCase
}
}

public static func write(_ value: ChatBotRegistrationStatus, into buf: inout [UInt8]) {
switch value {


case .new:
writeInt(&buf, Int32(1))


case .exists:
writeInt(&buf, Int32(2))

}
}
}


#if swift(>=5.8)
@_documentation(visibility: private)
#endif
public func FfiConverterTypeChatBotRegistrationStatus_lift(_ buf: RustBuffer) throws -> ChatBotRegistrationStatus {
return try FfiConverterTypeChatBotRegistrationStatus.lift(buf)
}

#if swift(>=5.8)
@_documentation(visibility: private)
#endif
public func FfiConverterTypeChatBotRegistrationStatus_lower(_ value: ChatBotRegistrationStatus) -> RustBuffer {
return FfiConverterTypeChatBotRegistrationStatus.lower(value)
}



/**
* Content of a chat message -- one of several types.
*/
Expand Down
66 changes: 59 additions & 7 deletions ios/truapi-host/Sources/TrUAPIHost/truapi_server.swift
Original file line number Diff line number Diff line change
Expand Up @@ -1942,8 +1942,8 @@ public func FfiConverterTypeHostCallbacks_lower(_ value: HostCallbacks) -> UInt6
* Native Chat storage and UI adapter. Hosts that support the Chat modality
* pass an implementation to
* [`NativeTrUApiHostRuntime::open_product_execution`]; hosts that do not
* simply pass `None`. Callbacks run inline on the dispatcher thread and must
* return promptly without blocking.
* simply pass `None`. Callbacks run inline on the process-wide dispatch pool
* shared by every product execution, so one that blocks stalls the others.
*/
public protocol NativeChatCallbacks: AnyObject, Sendable {

Expand All @@ -1952,6 +1952,11 @@ public protocol NativeChatCallbacks: AnyObject, Sendable {
*/
func createRoom(roomId: String, name: String, icon: String) throws -> ChatRoomRegistrationStatus

/**
* Register or resolve a native product Chat bot.
*/
func registerBot(botId: String, name: String, icon: String) throws -> ChatBotRegistrationStatus

/**
* Persist a text message in native Chat storage.
*/
Expand All @@ -1972,8 +1977,8 @@ public protocol NativeChatCallbacks: AnyObject, Sendable {
* Native Chat storage and UI adapter. Hosts that support the Chat modality
* pass an implementation to
* [`NativeTrUApiHostRuntime::open_product_execution`]; hosts that do not
* simply pass `None`. Callbacks run inline on the dispatcher thread and must
* return promptly without blocking.
* simply pass `None`. Callbacks run inline on the process-wide dispatch pool
* shared by every product execution, so one that blocks stalls the others.
*/
open class NativeChatCallbacksImpl: NativeChatCallbacks, @unchecked Sendable {
fileprivate let handle: UInt64
Expand Down Expand Up @@ -2041,6 +2046,21 @@ open func createRoom(roomId: String, name: String, icon: String)throws -> ChatR
FfiConverterString.lower(icon),uniffiCallStatus
)
})
}

/**
* Register or resolve a native product Chat bot.
*/
open func registerBot(botId: String, name: String, icon: String)throws -> ChatBotRegistrationStatus {
return try FfiConverterTypeChatBotRegistrationStatus_lift(try rustCallWithError(FfiConverterTypeHostRejection_lift) {
uniffiCallStatus in
uniffi_truapi_server_fn_method_nativechatcallbacks_register_bot(
self.uniffiCloneHandle(),
FfiConverterString.lower(botId),
FfiConverterString.lower(name),
FfiConverterString.lower(icon),uniffiCallStatus
)
})
}

/**
Expand Down Expand Up @@ -2141,6 +2161,35 @@ fileprivate struct UniffiCallbackInterfaceNativeChatCallbacks {
lowerError: FfiConverterTypeHostRejection_lower
)
},
registerBot: { (
uniffiHandle: UInt64,
botId: RustBuffer,
name: RustBuffer,
icon: RustBuffer,
uniffiOutReturn: UnsafeMutablePointer<RustBuffer>,
uniffiCallStatus: UnsafeMutablePointer<RustCallStatus>
) in
let makeCall = {
() throws -> ChatBotRegistrationStatus in
guard let uniffiObj = try? FfiConverterTypeNativeChatCallbacks.handleMap.get(handle: uniffiHandle) else {
throw UniffiInternalError.unexpectedStaleHandle
}
return try uniffiObj.registerBot(
botId: try FfiConverterString.lift(botId),
name: try FfiConverterString.lift(name),
icon: try FfiConverterString.lift(icon)
)
}


let writeReturn = { uniffiOutReturn.pointee = FfiConverterTypeChatBotRegistrationStatus_lower($0) }
uniffiTraitInterfaceCallWithError(
callStatus: uniffiCallStatus,
makeCall: makeCall,
writeReturn: writeReturn,
lowerError: FfiConverterTypeHostRejection_lower
)
},
postTextMessage: { (
uniffiHandle: UInt64,
roomId: RustBuffer,
Expand Down Expand Up @@ -6270,13 +6319,16 @@ private let initializationResult: InitializationResult = {
if (uniffi_truapi_server_checksum_method_nativechatcallbacks_create_room() != 15676) {
return InitializationResult.apiChecksumMismatch
}
if (uniffi_truapi_server_checksum_method_nativechatcallbacks_post_text_message() != 10747) {
if (uniffi_truapi_server_checksum_method_nativechatcallbacks_register_bot() != 59357) {
return InitializationResult.apiChecksumMismatch
}
if (uniffi_truapi_server_checksum_method_nativechatcallbacks_post_text_message() != 49314) {
return InitializationResult.apiChecksumMismatch
}
if (uniffi_truapi_server_checksum_method_nativechatcallbacks_post_custom_message() != 33405) {
if (uniffi_truapi_server_checksum_method_nativechatcallbacks_post_custom_message() != 28844) {
return InitializationResult.apiChecksumMismatch
}
if (uniffi_truapi_server_checksum_method_nativechatcallbacks_list_rooms() != 21374) {
if (uniffi_truapi_server_checksum_method_nativechatcallbacks_list_rooms() != 37616) {
return InitializationResult.apiChecksumMismatch
}
if (uniffi_truapi_server_checksum_method_nativeproductexecution_device_encryption_key() != 18707) {
Expand Down
Loading