diff --git a/nim-bindings/.gitignore b/nim-bindings/.gitignore deleted file mode 100644 index f3685e2..0000000 --- a/nim-bindings/.gitignore +++ /dev/null @@ -1,3 +0,0 @@ -nimble.develop -nimble.paths -nimbledeps diff --git a/nim-bindings/README.md b/nim-bindings/README.md deleted file mode 100644 index b97671b..0000000 --- a/nim-bindings/README.md +++ /dev/null @@ -1,8 +0,0 @@ -# Nim-bindings - -A Nim wrapping class that exposes LibChat functionality. - - -## Getting Started - -`nimble pingpong` - Run the pingpong example. \ No newline at end of file diff --git a/nim-bindings/bindings.nimble b/nim-bindings/bindings.nimble deleted file mode 100644 index b20ee5f..0000000 --- a/nim-bindings/bindings.nimble +++ /dev/null @@ -1,30 +0,0 @@ -# Package - -version = "0.1.0" -author = "libchat" -description = "Nim Bindings for LibChat" -license = "MIT" -srcDir = "src" -bin = @["libchat"] - - -# Dependencies - -requires "nim >= 2.2.4" -requires "results" - -proc buildRust() = - exec "cargo build --release --manifest-path ../Cargo.toml" - - -# Build Rust library before compiling Nim -before build: - buildRust() - -task pingpong, "Run pingpong example": - buildRust() - exec "nim c -r --path:src --passL:../target/release/liblibchat.a --passL:-lm examples/pingpong.nim" - -task test, "Run comprehensive all-endpoints test": - buildRust() - exec "nim c -r --path:src --passL:../target/release/liblibchat.a --passL:-lm tests/test_all_endpoints.nim" diff --git a/nim-bindings/config.nims b/nim-bindings/config.nims deleted file mode 100644 index 8ee48d2..0000000 --- a/nim-bindings/config.nims +++ /dev/null @@ -1,4 +0,0 @@ -# begin Nimble config (version 2) -when withDir(thisDir(), system.fileExists("nimble.paths")): - include "nimble.paths" -# end Nimble config diff --git a/nim-bindings/examples/pingpong.nim b/nim-bindings/examples/pingpong.nim deleted file mode 100644 index 3c7dfdc..0000000 --- a/nim-bindings/examples/pingpong.nim +++ /dev/null @@ -1,45 +0,0 @@ -import options -import results -import std/strutils - -import ../src/libchat - - -## Convert a string to seq[byte] -proc encode*(s: string): seq[byte] = - if s.len == 0: - return @[] - result = newSeq[byte](s.len) - copyMem(addr result[0], unsafeAddr s[0], s.len) - - -proc pingpong() = - - var raya = newConversationsContext("raya") - var saro = newConversationsContext("saro") - - - # Perform out of band Introduction - let intro = raya.createIntroductionBundle().expect("[Raya] Couldn't create intro bundle") - echo "Raya's Intro Bundle: ",intro - - var (convo_sr, payloads) = saro.createNewPrivateConvo(intro, encode("Hey Raya")).expect("[Saro] Couldn't create convo") - echo "ConvoId:: ", convo_sr - echo "Payload:: ", payloads - - ## Send Payloads to Raya - for p in payloads: - let res = raya.handlePayload(p.data) - if res.isOk: - let opt = res.get() - if opt.isSome: - let content_result = opt.get() - echo "RecvContent: ", content_result.conversationId, " ", content_result.data - else: - echo "Failed to handle payload: ", res.error - - echo "Done" - -when isMainModule: - pingpong() - diff --git a/nim-bindings/src/bindings.nim b/nim-bindings/src/bindings.nim deleted file mode 100644 index eb6053e..0000000 --- a/nim-bindings/src/bindings.nim +++ /dev/null @@ -1,232 +0,0 @@ -# Nim FFI bindings for libchat conversations library - -# Error codes (must match Rust ErrorCode enum) -const - ErrNone* = 0'i32 - ErrBadPtr* = -1'i32 - ErrBadConvoId* = -2'i32 - ErrBadIntro* = -3'i32 - ErrNotImplemented* = -4'i32 - ErrBufferExceeded* = -5'i32 - ErrUnknownError* = -6'i32 - -# Opaque handle type for Context -type ContextHandle* = pointer - -type - ## Slice for passing byte arrays to safer_ffi functions - SliceUint8* = object - `ptr`*: ptr uint8 - len*: csize_t - - ## Vector type returned by safer_ffi functions (must be freed) - VecUint8* = object - `ptr`*: ptr uint8 - len*: csize_t - cap*: csize_t - - ## repr_c::String type from safer_ffi - ReprCString* = object - `ptr`*: ptr char - len*: csize_t - cap*: csize_t - - ## Payload structure for FFI (matches Rust Payload struct) - Payload* = object - address*: ReprCString - data*: VecUint8 - - ## Vector of Payloads returned by safer_ffi functions - VecPayload* = object - `ptr`*: ptr Payload - len*: csize_t - cap*: csize_t - - ## Vector of Payloads returned by safer_ffi functions - VecString* = object - `ptr`*: ptr ReprCString - len*: csize_t - cap*: csize_t - - ## Result structure for create_intro_bundle - ## error_code is 0 on success, negative on error (see ErrorCode) - CreateIntroResult* = object - error_code*: int32 - intro_bytes*: VecUint8 - - ## Result structure for send_content - ## error_code is 0 on success, negative on error (see ErrorCode) - SendContentResult* = object - error_code*: int32 - payloads*: VecPayload - - ## Result structure for handle_payload - ## error_code is 0 on success, negative on error (see ErrorCode) - HandlePayloadResult* = object - error_code*: int32 - convo_id*: ReprCString - content*: VecUint8 - is_new_convo*: bool - - ## Result from create_new_private_convo - ## error_code is 0 on success, negative on error (see ErrorCode) - NewConvoResult* = object - error_code*: int32 - convo_id*: ReprCString - payloads*: VecPayload - - ## Result from list_conversations - ## error_code is 0 on success, negative on error (see ErrorCode) - ListConvoResult* = object - error_code*: int32 - convo_ids*: VecString - -# FFI function imports - -## Creates a new libchat Context -## Returns: Opaque handle to the context. Must be freed with destroy_context() -proc create_context*(name: ReprCString): ContextHandle {.importc.} - -## Returns the friendly name of the context's identity -## The result must be freed by the caller (repr_c::String ownership transfers) -proc installation_name*(ctx: ContextHandle): ReprCString {.importc.} - -## Destroys a context and frees its memory -## - handle must be a valid pointer from create_context() -## - handle must not be used after this call -proc destroy_context*(ctx: ContextHandle) {.importc.} - -## Free a ReprCString returned by any of the FFI functions -## - s must be an owned ReprCString value returned from an FFI function -## - s must not be used after this call -proc destroy_string*(s: ReprCString) {.importc.} - -## Creates an intro bundle for sharing with other users -## Returns: CreateIntroResult struct - check error_code field (0 = success, negative = error) -## The result must be freed with destroy_intro_result() -proc create_intro_bundle*( - ctx: ContextHandle, -): CreateIntroResult {.importc.} - -## Creates a new private conversation -## Returns: NewConvoResult struct - check error_code field (0 = success, negative = error) -## The result must be freed with destroy_convo_result() -proc create_new_private_convo*( - ctx: ContextHandle, - bundle: SliceUint8, - content: SliceUint8, -): NewConvoResult {.importc.} - -## Get the available conversation identifers. -## Returns: ListConvoResult struct - check error_code field (0 = success, negative = error) -## The result must be freed with destroy_list_result() -proc list_conversations*( - ctx: ContextHandle, -): ListConvoResult {.importc.} - -## Sends content to an existing conversation -## Returns: SendContentResult struct - check error_code field (0 = success, negative = error) -## The result must be freed with destroy_send_content_result() -proc send_content*( - ctx: ContextHandle, - convo_id: ReprCString, - content: SliceUint8, -): SendContentResult {.importc.} - -## Handles an incoming payload -## Returns: HandlePayloadResult struct - check error_code field (0 = success, negative = error) -## This call does not always generate content. If content is zero bytes long then there -## is no data, and the convo_id should be ignored. -## The result must be freed with destroy_handle_payload_result() -proc handle_payload*( - ctx: ContextHandle, - payload: SliceUint8, -): HandlePayloadResult {.importc.} - -## Free the result from create_intro_bundle -proc destroy_intro_result*(result: CreateIntroResult) {.importc.} - -## Free the result from create_new_private_convo -proc destroy_convo_result*(result: NewConvoResult) {.importc.} - -## Free the result from list_conversation -proc destroy_list_result*(result: ListConvoResult) {.importc.} - -## Free the result from send_content -proc destroy_send_content_result*(result: SendContentResult) {.importc.} - -## Free the result from handle_payload -proc destroy_handle_payload_result*(result: HandlePayloadResult) {.importc.} - -# ============================================================================ -# Helper functions -# ============================================================================ - -## Create a SliceRefUint8 from a string -proc toSlice*(s: string): SliceUint8 = - if s.len == 0: - SliceUint8(`ptr`: nil, len: 0) - else: - SliceUint8(`ptr`: cast[ptr uint8](unsafeAddr s[0]), len: csize_t(s.len)) - -## Create a SliceRefUint8 from a seq[byte] -proc toSlice*(s: seq[byte]): SliceUint8 = - if s.len == 0: - SliceUint8(`ptr`: nil, len: 0) - else: - SliceUint8(`ptr`: cast[ptr uint8](unsafeAddr s[0]), len: csize_t(s.len)) - -## Convert a ReprCString to a Nim string -proc `$`*(s: ReprCString): string = - if s.ptr == nil or s.len == 0: - return "" - result = newString(s.len) - copyMem(addr result[0], s.ptr, s.len) - -## Create a ReprCString from a Nim string for passing to FFI functions. -## WARNING: The returned ReprCString borrows from the input string. -## The input string must remain valid for the duration of the FFI call. -## cap is set to 0 to prevent Rust from attempting to deallocate Nim memory. -proc toReprCString*(s: string): ReprCString = - if s.len == 0: - ReprCString(`ptr`: nil, len: 0, cap: 0) - else: - ReprCString(`ptr`: cast[ptr char](unsafeAddr s[0]), len: csize_t(s.len), cap: 0) - -## Convert a VecUint8 to a seq[string] -proc toSeq*(v: VecString): seq[string] = - if v.ptr == nil or v.len == 0: - return @[] - result = newSeq[string](v.len) - let arr = cast[ptr UncheckedArray[ReprCString]](v.ptr) - for i in 0 ..< int(v.len): - result[i] = $arr[i] - -## Convert a VecUint8 to a seq[byte] -proc toSeq*(v: VecUint8): seq[byte] = - if v.ptr == nil or v.len == 0: - return @[] - result = newSeq[byte](v.len) - copyMem(addr result[0], v.ptr, v.len) - -## Access payloads from VecPayload -proc `[]`*(v: VecPayload, i: int): Payload = - assert i >= 0 and csize_t(i) < v.len - cast[ptr UncheckedArray[Payload]](v.ptr)[i] - -## Get length of VecPayload -proc len*(v: VecPayload): int = - int(v.len) - -## Iterator for VecPayload -iterator items*(v: VecPayload): Payload = - for i in 0 ..< v.len: - yield v[int(i)] - -## Convert a string to seq[byte] -proc toBytes*(s: string): seq[byte] = - if s.len == 0: - return @[] - result = newSeq[byte](s.len) - copyMem(addr result[0], unsafeAddr s[0], s.len) - diff --git a/nim-bindings/src/libchat.nim b/nim-bindings/src/libchat.nim deleted file mode 100644 index 90960c1..0000000 --- a/nim-bindings/src/libchat.nim +++ /dev/null @@ -1,135 +0,0 @@ -import std/options -import std/sequtils -import results -import bindings - -type - LibChat* = object - handle: ContextHandle - buffer_size: int - - PayloadResult* = object - address*: string - data*: seq[uint8] - -## Create a new conversations context -proc newConversationsContext*(name: string): LibChat = - - result.handle = create_context(name.toReprCString) - result.buffer_size = 256 - if result.handle.isNil: - raise newException(IOError, "Failed to create context") - -## Get the friendly name of this context's installation -proc getInstallationName*(ctx: LibChat): string = - if ctx.handle == nil: - return "" - let name = installation_name(ctx.handle) - defer: destroy_string(name) - result = $name - -## Destroy the context and free resources -proc destroy*(ctx: var LibChat) = - - if not ctx.handle.isNil: - destroy_context(ctx.handle) - ctx.handle = nil - -## Helper proc to create buffer of sufficient size -proc getBuffer*(ctx: LibChat): seq[byte] = - newSeq[byte](ctx.buffer_size) - -## Generate a Introduction Bundle -proc createIntroductionBundle*(ctx: LibChat): Result[seq[byte], string] = - if ctx.handle == nil: - return err("Context handle is nil") - - let res = create_intro_bundle(ctx.handle) - defer: destroy_intro_result(res) - - if res.error_code != ErrNone: - return err("Failed to create intro bundle: " & $res.error_code) - - return ok(res.intro_bytes.toSeq()) - -## Create a Private Convo -proc createNewPrivateConvo*(ctx: LibChat, bundle: seq[byte], content: seq[byte]): Result[(string, seq[PayloadResult]), string] = - if ctx.handle == nil: - return err("Context handle is nil") - - if bundle.len == 0: - return err("bundle is zero length") - if content.len == 0: - return err("content is zero length") - - let res = bindings.create_new_private_convo(ctx.handle, bundle.toSlice(), content.toSlice()) - defer: destroy_convo_result(res) - - if res.error_code != 0: - return err("Failed to create private convo: " & $res.error_code) - - var payloads = newSeq[PayloadResult](res.payloads.len) - for i in 0 ..< res.payloads.len: - let p = res.payloads[int(i)] - payloads[int(i)] = PayloadResult(address: $p.address, data: p.data.toSeq()) - - return ok(($res.convo_id, payloads)) - -proc listConversations*(ctx: LibChat): Result[seq[string], string] = - if ctx.handle == nil: - return err("Context handle is nil") - let res = bindings.list_conversations(ctx.handle) - - if res.error_code != 0: - result = err("Failed to list conversations: " & $res.error_code) - destroy_list_result(res) - return - - ok(res.convo_ids.toSeq()) - -## Send content to an existing conversation -proc sendContent*(ctx: LibChat, convoId: string, content: seq[byte]): Result[seq[PayloadResult], string] = - if ctx.handle == nil: - return err("Context handle is nil") - - if content.len == 0: - return err("content is zero length") - - let res = bindings.send_content(ctx.handle, convoId.toReprCString, content.toSlice()) - defer: destroy_send_content_result(res) - - if res.error_code != 0: - return err("Failed to send content: " & $res.error_code) - - let payloads = res.payloads.toSeq().mapIt(PayloadResult(address: $it.address, data: it.data.toSeq())) - return ok(payloads) - -type - ContentResult* = object - conversationId*: string - data*: seq[uint8] - isNewConvo*: bool - -## Handle an incoming payload and decrypt content -proc handlePayload*(ctx: LibChat, payload: seq[byte]): Result[Option[ContentResult], string] = - if ctx.handle == nil: - return err("Context handle is nil") - - if payload.len == 0: - return err("payload is zero length") - - let res = bindings.handle_payload(ctx.handle, payload.toSlice()) - defer: destroy_handle_payload_result(res) - - if res.error_code != ErrNone: - return err("Failed to handle payload: " & $res.error_code) - - let content = res.content.toSeq() - if content.len == 0: - return ok(none(ContentResult)) - - return ok(some(ContentResult( - conversationId: $res.convo_id, - data: content, - isNewConvo: res.is_new_convo - ))) diff --git a/nim-bindings/tests/test_all_endpoints.nim b/nim-bindings/tests/test_all_endpoints.nim deleted file mode 100644 index e9e3f54..0000000 --- a/nim-bindings/tests/test_all_endpoints.nim +++ /dev/null @@ -1,258 +0,0 @@ -# Comprehensive test for all FFI procs declared in bindings.nim. -# -# Design intent: By importing `bindings` directly and calling every importc -# proc at least once, the linker is forced to include ALL symbol references. -# This prevents link-time optimizations from stripping unused symbols and -# catches both link-time crashes (missing symbols) and runtime crashes -# (wrong ABI, segfaults on use). - -import bindings - -# --------------------------------------------------------------------------- -# Assertion helper -# --------------------------------------------------------------------------- - -proc check(cond: bool, msg: string) = - if not cond: - echo "FAIL: ", msg - quit(1) - echo "OK: ", msg - -# --------------------------------------------------------------------------- -# Section 1: Helper proc coverage -# --------------------------------------------------------------------------- - -proc testHelperProcs() = - echo "\n--- testHelperProcs ---" - - # toSlice(string) — non-empty and empty branches - let s = "hello" - let sl = toSlice(s) - check(sl.len == 5, "toSlice(string): correct len") - check(sl.`ptr` != nil, "toSlice(non-empty string): non-nil ptr") - - let emptySl = toSlice("") - check(emptySl.len == 0, "toSlice(empty string): len == 0") - check(emptySl.`ptr` == nil, "toSlice(empty string): ptr == nil") - - # toSlice(seq[byte]) — non-empty and empty branches - let b: seq[byte] = @[0x61'u8, 0x62'u8, 0x63'u8] - let bSl = toSlice(b) - check(bSl.len == 3, "toSlice(seq[byte]): correct len") - check(bSl.`ptr` != nil, "toSlice(non-empty seq[byte]): non-nil ptr") - - let emptyBSl = toSlice(newSeq[byte](0)) - check(emptyBSl.len == 0, "toSlice(empty seq[byte]): len == 0") - check(emptyBSl.`ptr` == nil, "toSlice(empty seq[byte]): ptr == nil") - - # toReprCString(string) and $(ReprCString) round-trip - let name = "testname" - let rcs = toReprCString(name) - check(rcs.len == csize_t(name.len), "toReprCString: correct len") - check(rcs.cap == 0, "toReprCString: cap == 0 (prevents Rust dealloc of Nim memory)") - check(rcs.`ptr` != nil, "toReprCString: non-nil ptr") - check($rcs == name, "$(ReprCString): round-trips to original string") - - let emptyRcs = toReprCString("") - check(emptyRcs.len == 0, "toReprCString(empty): len == 0") - check($emptyRcs == "", "$(empty ReprCString): returns empty string") - - # toBytes(string) - let bs = toBytes("abc") - check(bs.len == 3, "toBytes: correct length") - check(bs[0] == 0x61'u8, "toBytes: correct first byte") - - let emptyBs = toBytes("") - check(emptyBs.len == 0, "toBytes(empty): empty seq") - -# --------------------------------------------------------------------------- -# Section 2: create_context / installation_name / destroy_context -# --------------------------------------------------------------------------- - -proc testContextLifecycle() = - echo "\n--- testContextLifecycle ---" - - let ctx = create_context(toReprCString("lifecycle-test")) - check(ctx != nil, "create_context: returns non-nil handle") - - let iname = installation_name(ctx) - defer: destroy_string(iname) - let inameStr = $iname - check(inameStr.len > 0, "installation_name: returns non-empty name") - echo " installation name: ", inameStr - - destroy_context(ctx) - echo " destroy_context: no crash" - -# --------------------------------------------------------------------------- -# Section 3: Full two-party conversation flow -# --------------------------------------------------------------------------- -# Exercises: create_intro_bundle, create_new_private_convo, handle_payload, -# send_content, and all four destroy_* procs. -# VecPayload helpers ([], len, items) are also exercised here. - -proc testFullConversationFlow() = - echo "\n--- testFullConversationFlow ---" - - let aliceCtx = create_context(toReprCString("alice")) - check(aliceCtx != nil, "Alice: create_context non-nil") - - let bobCtx = create_context(toReprCString("bob")) - check(bobCtx != nil, "Bob: create_context non-nil") - - # --- create_intro_bundle --- - var bobIntroRes = create_intro_bundle(bobCtx) - check(bobIntroRes.error_code == ErrNone, - "create_intro_bundle: error_code == ErrNone") - check(bobIntroRes.intro_bytes.len > 0, - "create_intro_bundle: intro_bytes non-empty") - - # toSeq(VecUint8) - let introBytes = toSeq(bobIntroRes.intro_bytes) - check(introBytes.len > 0, "toSeq(VecUint8): produces non-empty seq") - - # destroy_intro_result - destroy_intro_result(bobIntroRes) - echo " destroy_intro_result: no crash" - - # --- create_new_private_convo --- - var convoRes = create_new_private_convo( - aliceCtx, - toSlice(introBytes), - toSlice("Hello, Bob!") - ) - check(convoRes.error_code == ErrNone, - "create_new_private_convo: error_code == ErrNone") - - let aliceConvoId = $convoRes.convo_id - check(aliceConvoId.len > 0, "create_new_private_convo: convo_id non-empty") - echo " Alice-Bob convo_id: ", aliceConvoId - - # len(VecPayload) - let numPayloads = len(convoRes.payloads) - check(numPayloads > 0, "len(VecPayload): > 0 payloads in new convo") - - # [](VecPayload, int): subscript access - let firstPayload = convoRes.payloads[0] - check(firstPayload.data.len > 0, "VecPayload[0].data: non-empty") - check(firstPayload.address.len > 0, "VecPayload[0].address: non-empty") - echo " first payload address: ", $firstPayload.address - - # items(VecPayload): collect bytes before destroy - var payloadDatas: seq[seq[byte]] = @[] - var iterCount = 0 - for p in convoRes.payloads: - payloadDatas.add(toSeq(p.data)) - inc iterCount - check(iterCount == numPayloads, - "items(VecPayload): iterator yields all payloads") - - # destroy_convo_result - destroy_convo_result(convoRes) - echo " destroy_convo_result: no crash" - - # --- handle_payload --- - var bobSawContent = false - var bobConvoId = "" - for pData in payloadDatas: - var hp = handle_payload(bobCtx, toSlice(pData)) - check(hp.error_code == ErrNone, "handle_payload: error_code == ErrNone") - - let content = toSeq(hp.content) - if content.len > 0: - bobConvoId = $hp.convo_id - check(bobConvoId.len > 0, - "handle_payload: convo_id non-empty when content present") - if not bobSawContent: - check(hp.is_new_convo, - "handle_payload: is_new_convo == true on first contact") - bobSawContent = true - echo " Bob received content in convo: ", bobConvoId - - destroy_handle_payload_result(hp) - - check(bobSawContent, "handle_payload: Bob received Alice's opening message") - echo " destroy_handle_payload_result: no crash" - - # --- send_content --- - var sendRes = send_content( - aliceCtx, - toReprCString(aliceConvoId), - toSlice("How are you, Bob?") - ) - check(sendRes.error_code == ErrNone, - "send_content: error_code == ErrNone for valid convo_id") - check(len(sendRes.payloads) > 0, - "send_content: returns at least one payload") - - var sendPayloadDatas: seq[seq[byte]] = @[] - for p in sendRes.payloads: - sendPayloadDatas.add(toSeq(p.data)) - - # destroy_send_content_result - destroy_send_content_result(sendRes) - echo " destroy_send_content_result: no crash" - - # Bob handles follow-up payloads - for pData in sendPayloadDatas: - var hp2 = handle_payload(bobCtx, toSlice(pData)) - check(hp2.error_code == ErrNone, - "handle_payload: Bob handles send_content payload without error") - destroy_handle_payload_result(hp2) - - destroy_context(aliceCtx) - destroy_context(bobCtx) - echo " both contexts destroyed: no crash" - -# --------------------------------------------------------------------------- -# Section 4: Error-case coverage -# --------------------------------------------------------------------------- -# Exercises destroy_* on error results (empty/null Vecs) to confirm they -# do not crash. - -proc testErrorCases() = - echo "\n--- testErrorCases ---" - - let ctx = create_context(toReprCString("error-tester")) - check(ctx != nil, "error-tester: create_context non-nil") - - # send_content with a nonexistent convo_id must fail - var badSend = send_content( - ctx, - toReprCString("00000000-0000-0000-0000-nonexistent"), - toSlice("payload") - ) - check(badSend.error_code != ErrNone, - "send_content(bad convo_id): error_code != ErrNone") - echo " send_content(bad convo_id) error_code: ", badSend.error_code - # Destroy error result to confirm destroy handles empty VecPayload - destroy_send_content_result(badSend) - echo " destroy_send_content_result(error result): no crash" - - # create_new_private_convo with garbage bytes must fail with ErrBadIntro - let badIntro: seq[byte] = @[0xDE'u8, 0xAD'u8, 0xBE'u8, 0xEF'u8] - var badConvo = create_new_private_convo( - ctx, - toSlice(badIntro), - toSlice("content") - ) - check(badConvo.error_code == ErrBadIntro, - "create_new_private_convo(bad intro): error_code == ErrBadIntro") - destroy_convo_result(badConvo) - echo " destroy_convo_result(error result): no crash" - - destroy_context(ctx) - -# --------------------------------------------------------------------------- -# Entry point -# --------------------------------------------------------------------------- - -when isMainModule: - echo "=== test_all_endpoints: begin ===" - - testHelperProcs() - testContextLifecycle() - testFullConversationFlow() - testErrorCases() - - echo "\n=== ALL TESTS PASSED ==="