Skip to content

Add two-user integration tests for chat-module#24

Merged
at0m1x19 merged 16 commits into
masterfrom
add_integration_tests
May 26, 2026
Merged

Add two-user integration tests for chat-module#24
at0m1x19 merged 16 commits into
masterfrom
add_integration_tests

Conversation

@at0m1x19
Copy link
Copy Markdown
Contributor

@at0m1x19 at0m1x19 commented May 5, 2026

Adds end-to-end integration test for two-user chat (test_two_users_chat) plus the integration-tests CI job.

  • tests/integration/:pytest harness (conftest.py, _helpers.py, test_two_users_chat.py) + fixtures (deterministic nodekey + chat configs with placeholder ENR resolved at runtime).
  • .github/workflows/ci.yml: new integration-tests job (ubuntu-only, needs: build-and-test, uploads container logs on failure).

@at0m1x19 at0m1x19 self-assigned this May 5, 2026
Egor Rachkovskii added 5 commits May 6, 2026 12:11
…er removal; update CI to forward CLI output through pytest logs.
…o specific CLI rewrite SHA to support containerized daemons.
…t_arg` helper for consistent argument access.
@at0m1x19 at0m1x19 marked this pull request as ready for review May 7, 2026 22:57
Copy link
Copy Markdown

@fbarbu15 fbarbu15 left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Thanks, left some comments

Comment thread .github/workflows/ci.yml Outdated

- uses: actions/setup-python@v5
with:
python-version: "3.12"
Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

since it's a new job, why not go with newer python ex 3.14?

Copy link
Copy Markdown
Contributor Author

@at0m1x19 at0m1x19 May 8, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Bumped to 3.14. Done together with logos-integration-test-framework (PR #4 there) which now covers 3.11/3.12/3.13/3.14.

Comment thread .github/workflows/ci.yml Outdated
# Once framework's pyproject.toml repins to a SHA that includes the
# `network=` kwarg, this whole block can become plain `pip install -r ...`.
pip install --no-deps -r requirements.txt
pip install pytest>=8.0
Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

why not add pytest instead requirements.txt ?

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Resolved by upstream re-pin: now that logos-integration-test-framework master tracks logoscore@master pip resolution is no longer in conflict.

The workaround (pip install --no-deps -r requirements.txt && pip install pytest>=8.0) is gone. CI now runs plain pip install -r requirements.txt.

Comment thread tests/e2e/libs/helpers.py
@@ -0,0 +1,150 @@
from __future__ import annotations
Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I would put this file into a dedicated libs/helper folder to not be mixed with other root files

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

moved

Comment thread tests/e2e/conftest.py

from _helpers import ChatUser, setup_chat_user

FIXTURES_DIR = Path(__file__).parent / "fixtures"
Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

can we move all constants to a separate/dedicated file?

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

and thus have all other constants in this file as well

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Done — moved to tests/e2e/_constants.py

Comment thread tests/integration/conftest.py Outdated
try:
(log_dir / f"{container_name}.log").write_text(_docker_logs(container_name))
except OSError:
pass
Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

let's log a warning in this case

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Done

Comment thread tests/e2e/conftest.py
rest_host_port = pick_free_port()

cmd = [
"docker", "run", "-d", "--name", container_name,
Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

any reason to not use python docker sdk?

Copy link
Copy Markdown
Contributor Author

@at0m1x19 at0m1x19 May 8, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

subprocess.run(["docker", ...]) mirrors how LogoscoreDockerDaemon itself drives docker internally (also subprocess-based). Made it for consistency.

Also these docker calls are simple, SDK looks like overhead to me. It's easy to debug and we don't add one more dependency.

If calls become more complex we can switch any moment. WDYT?

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

yes, agreed

Comment thread tests/integration/conftest.py Outdated
config_a = _read_fixture("chat-a.json").replace("__BOOTSTRAP_ENR__", nwaku_bootstrap)
config_b = _read_fixture("chat-b.json").replace("__BOOTSTRAP_ENR__", nwaku_bootstrap)

alice_name = f"logoscore-alice-{uuid.uuid4().hex[:8]}"
Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

node_a and node_b or similar is better and more consistent

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

now it's saro/raya :)

daemon was chosen over node because LogoscoreDockerDaemon is a logoscore daemon process, while in our setup there's also a separate waku node. I thought it's a bit confusing.

a.client,
"newPrivateConversation",
b.intro_bundle,
hex_encode("hello-from-A-1"),
Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

hello-from-A-1 should be a reusable var so you can assert against it at line 73

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Fixed

convo_id_a = extract_convo_id(
parse_json_field(wait_event(nc_a, "chatNewConversation", timeout=20.0), 0)
)
convo_id_b = extract_convo_id(
Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

convo_id_b differs from convo_id_a ? I would expect them to match and in that case to add an extra assert convo_id_b == convo_id_a

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

They're different by design, libchat uses X3DH, where Initiator and Responder gets different local conversation ids

assert extract_message_content(parse_json_field(first_msg, 0)) == "hello-from-A-1"

# B → A reply
with subscribe(a.client, MODULE, "chatNewMessage") as nm_a:
Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

those blocks b->a / a->b have a lot in common, could we refactor them into a reusable function to make the test shorter and more readable?

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

done

@fbarbu15
Copy link
Copy Markdown

fbarbu15 commented May 8, 2026

Thanks, left some comments

Also I see the CI job is failing, is there a bug / issue?

Copy link
Copy Markdown
Collaborator

@igor-sirotin igor-sirotin left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Noice, thank you!

Comment thread tests/integration/conftest.py Outdated


@pytest.fixture(scope="module")
def chat_users(
Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This fixture looks very specific to test_two_users_can_chat. Like, if we'll want a test with 3 users and then with 5 users, we'll implement 2 more fixtures? 😄

I think such fixture should build 1 user only. And then alice/bob fixtures can be created ad-hoc.

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Refactored

Comment thread tests/integration/conftest.py Outdated
Comment on lines +184 to +185
alice_name = f"logoscore-alice-{uuid.uuid4().hex[:8]}"
bob_name = f"logoscore-bob-{uuid.uuid4().hex[:8]}"
Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Up to you, but we tend not use classic Alice/Bob/Charlie here, because it's not exactly the right case to use it. We use Saro/Raya/Pax: https://github.com/logos-messaging/specs/blob/master/informational/chat_cast.md

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Fixed

Comment thread tests/integration/README.md Outdated
@@ -0,0 +1,139 @@
# logos-chat-module integration tests
Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Wouldn't it not count as e2e tests, rather than integration? 🤔

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Sure, done

Comment thread tests/integration/README.md Outdated
Comment on lines +16 to +29
┌──────────── docker network "logoschat-it-<uuid>" ────────────┐
│ │
│ ┌─────────────────┐ ┌──────────────┐ ┌──────────────┐ │
│ │ nwaku-bootstrap │ │ logoscore- │ │ logoscore- │ │
│ │ 172.30.0.10:60000 │ alice │ │ bob │ │
│ │ deterministic │ │ port=60002 │ │ port=60003 │ │
│ │ ENR │ │ staticPeers= │ │ staticPeers= │ │
│ │ │ │ [bootENR] │ │ [bootENR] │ │
│ └─────────────────┘ └──────────────┘ └──────────────┘ │
└──────────────────────────────────────────────────────────────┘
▲ │ │
│ /vac/waku/relay/2.0.0 ─────────┘
pubsub: /waku/2/rs/2/1
Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

suggestion: replace with a mermaid diagram? (up to you)

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

done

Comment thread tests/integration/conftest.py Outdated
Comment on lines +181 to +182
config_a = _read_fixture("chat-a.json").replace("__BOOTSTRAP_ENR__", nwaku_bootstrap)
config_b = _read_fixture("chat-b.json").replace("__BOOTSTRAP_ENR__", nwaku_bootstrap)
Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Why do we need static JSON files? Can we generate them on the fly?
This would be essential for further scaling of the tests

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Done

Egor Rachkovskii added 2 commits May 8, 2026 23:10
@at0m1x19
Copy link
Copy Markdown
Contributor Author

at0m1x19 commented May 8, 2026

Thanks, left some comments

Also I see the CI job is failing, is there a bug / issue?

Was flaky cause of a regression in module-builder's codegen path. Should be fixed now.

@at0m1x19 at0m1x19 force-pushed the add_integration_tests branch from 87ea9ff to 1095a14 Compare May 10, 2026 13:05
Copy link
Copy Markdown

@fbarbu15 fbarbu15 left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

LGTM, but I still see the CI job failing

@at0m1x19 at0m1x19 force-pushed the add_integration_tests branch from d1fe25f to 87ea9ff Compare May 11, 2026 13:34
@at0m1x19
Copy link
Copy Markdown
Contributor Author

Defer libchat-callback emits to fix initChat RPC hang on ubuntu-latest

This change defers every impl->emitEvent(...) call inside the twelve libchat C callbacks through a QMetaObject::invokeMethod(..., Qt::QueuedConnection) helper. A small QObject m_emitRouter member on ChatModuleImpl serves as the receiver so Qt drops pending queued metacalls when the impl is destroyed, preventing use-after-free on teardown.

Why this is needed. Every libchat callback is invoked synchronously from inside a libchat C API call (chat_new, chat_start, …) which itself is invoked synchronously from a LOGOS_METHOD on ChatModuleImpl. The plugin host's Q_INVOKABLE slot is still on the stack when the callback emits. While that synchronous chain runs the host thread cannot reach its next event-loop iteration — and that iteration is when QLocalSocket flushes the QRO reply packet back to the daemon. On slow runners (ubuntu-latest: 4 vCPU shared, x86_64), the flush starvation exceeds the caller's 20 s waitForFinished deadline and core_service.callModuleMethod returns METHOD_FAILED → exit 4.

Plugin semantics are unchanged: each LOGOS_METHOD still returns true synchronously, listeners still receive the result event — just a few milliseconds later, after the QRO dispatch stack has unwound.

Evidence.

  • Pre-fix on 87ea9ff: pytest ERRORs at setup_chat_user with exit 4 (MethodError) after ~20 s. The plugin's own [chat_module] ChatModuleImpl: Chat context created successfully log line is present at +5 ms — proving the plugin completed initChat and true was returned to the proxy. The reply never reaches the daemon's RemoteLogosObject::callMethod, which times out.
  • After this commit: test_two_users_can_chat PASSED in ~10.9 s on ubuntu-latest, matching local-Mac timings. Bisecting against test_basic_module_cpp.returnTrue (no re-entrant emit, otherwise identical CI environment) passes in ~1.1 s, confirming the QRO transport itself is healthy — the bug is the re-entrant emit pattern in this plugin.

Scope. This is a plugin-local mitigation. The same anti-pattern is latent in any module that emits Qt events synchronously from inside a Q_INVOKABLE-dispatched C callback; an SDK-side hardening is proposed at logos-co/logos-cpp-sdk#60 so that the workaround in deferredEmit here can be removed in a follow-up once the SDK fix lands.

@igor-sirotin
Copy link
Copy Markdown
Collaborator

@at0m1x19 is it ready to merge?

@at0m1x19
Copy link
Copy Markdown
Contributor Author

@at0m1x19 is it ready to merge?

@igor-sirotin it’s ready from my point of view. Before the merge, I think it’s worth taking a look at the latest commit together with this for someone who ones this repo.

@at0m1x19 at0m1x19 merged commit 9b22b52 into master May 26, 2026
3 checks passed
@igor-sirotin igor-sirotin deleted the add_integration_tests branch May 26, 2026 20:16
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

3 participants