Compare commits
3 Commits
master
...
draft/spli
| Author | SHA256 | Date | |
|---|---|---|---|
|
8a627f5bf5
|
|||
|
22977846a3
|
|||
| bea6ab6df5 |
65
AGENTS.md
Normal file
65
AGENTS.md
Normal file
@@ -0,0 +1,65 @@
|
||||
# AGENTS
|
||||
|
||||
> **Note:** this file is staged in `cproof-context` for review. Its
|
||||
> destination is the **cproof** repo root, not this one. It is **not**
|
||||
> agent instructions for the `cproof-context` repository itself.
|
||||
|
||||
Project-level agent instructions for cproof (terminal XMPP client, fork of
|
||||
Profanity).
|
||||
|
||||
## Agent context bundle
|
||||
|
||||
Detailed architecture, patterns, testing, build, playbooks, and known
|
||||
pitfalls for this project live in a separate repository:
|
||||
|
||||
- **Repo:** https://git.jabber.space/devs/cproof-context
|
||||
- **Pinned commit:** `22977846a3b2c1727ae6aeeaa1de70971bc0136e46b131febac7591df7d72e75`
|
||||
(branch `draft/split-context`)
|
||||
|
||||
Bump the pinned commit explicitly when the context bundle is updated. Do
|
||||
not track a moving branch.
|
||||
|
||||
## Load order
|
||||
|
||||
On session start (before doing meaningful work in this repo):
|
||||
|
||||
1. Read `SKILL.md` from the pinned commit — the always-loaded entry hint.
|
||||
2. Read `INDEX.md` — map of every context file with churn labels and
|
||||
"When to load" guidance.
|
||||
3. Pull individual files from the bundle on demand, driven by the task
|
||||
and the `INDEX.md` "When to load" column. Do **not** preload the whole
|
||||
bundle.
|
||||
4. Always scan `gotchas.md` before completing a non-trivial task.
|
||||
5. If working on a feature branch listed under `wip/` (currently
|
||||
`wip/feat-ai.md` for branch `feat/ai`), load that file as well — the
|
||||
stable bundle layers describe `master` only.
|
||||
|
||||
## Hard rules for this project
|
||||
|
||||
- **Build inside Docker.** Use `Dockerfile.debian` at the repo root. Do
|
||||
not run `./autogen.sh`, `./configure`, or `make` on the host shell. See
|
||||
`build/docker.md` in the context bundle.
|
||||
- **`make check` is the only routine test entry point.** Functional tests
|
||||
(`tests/functionaltests/`) and benches (`tests/bench/`) are not part of
|
||||
CI; run them locally inside the Docker image when needed.
|
||||
- **Treat path / identifier references in the context bundle as claims
|
||||
to verify.** They were authored against a specific cproof commit; grep
|
||||
the live tree before relying on them.
|
||||
- **`master` is the canonical baseline.** Feature-branch surface (e.g.
|
||||
`src/ai/` on `feat/ai`) is documented only under `wip/<branch>.md` in
|
||||
the context bundle. Do not assume branch-specific files exist on
|
||||
`master`.
|
||||
|
||||
## Where agent context does NOT live
|
||||
|
||||
- This file is the only project-level agent instruction. There is no
|
||||
parallel `CLAUDE.md`, `.cursorrules`, or per-tool variant.
|
||||
- User-specific preferences (language, commit-trailer policy, identity)
|
||||
belong in the agent runtime's user-memory, not in this file.
|
||||
|
||||
## Updating the bundle
|
||||
|
||||
Edits to architectural / pattern / test / build documentation land in the
|
||||
`cproof-context` repo, not here. After a context-bundle PR merges,
|
||||
update the pinned commit hash above in the same PR cycle that consumes
|
||||
the new content.
|
||||
77
INDEX.md
Normal file
77
INDEX.md
Normal file
@@ -0,0 +1,77 @@
|
||||
# INDEX
|
||||
|
||||
One-page map of every context file. Churn labels: **low** (years), **med**
|
||||
(months), **high** (weeks).
|
||||
|
||||
## Top level
|
||||
|
||||
| File | Churn | When to load |
|
||||
|---|---|---|
|
||||
| `README.md` | low | Never — repo metadata for humans. |
|
||||
| `SKILL.md` | low | Always (entry hint). |
|
||||
| `INDEX.md` | low | Always (this file). |
|
||||
|
||||
## architecture/ — structural reference
|
||||
|
||||
| File | Churn | When to load |
|
||||
|---|---|---|
|
||||
| `overview.md` | low | First load on any cproof task. |
|
||||
| `source-map.md` | low | When choosing where to put new code, or locating an unfamiliar module. |
|
||||
| `test-map.md` | low | When writing or running tests. |
|
||||
| `data-flow.md` | low | When reasoning about input → output paths (commands, events, UI updates). |
|
||||
|
||||
## patterns/ — conventions and idioms
|
||||
|
||||
| File | Churn | When to load |
|
||||
|---|---|---|
|
||||
| `memory.md` | low | Any C edit that allocates / frees. |
|
||||
| `commands.md` | med | Adding, changing, or understanding a `/command`. |
|
||||
| `autocomplete.md` | med | Anything that touches `cmd_ac.c` or registers an `Autocomplete`. |
|
||||
| `events.md` | med | Touching `src/event/`. |
|
||||
| `xmpp.md` | med | Touching `src/xmpp/` or any XMPP-gated code path. |
|
||||
| `encryption.md` | med | Touching `src/omemo/`, `src/otr/`, or `src/pgp/`. |
|
||||
| `ui.md` | med | Touching `src/ui/`. |
|
||||
| `plugins.md` | med | Touching `src/plugins/`, plugin C/Python API. |
|
||||
|
||||
## testing/ — how tests work
|
||||
|
||||
| File | Churn | When to load |
|
||||
|---|---|---|
|
||||
| `unit-tests.md` | med | Writing or extending unit tests. |
|
||||
| `stubs.md` | med | Adding a function whose callers are unit-tested, or wiring a new stub. |
|
||||
| `functional-tests.md` | med | Touching `tests/functionaltests/`. |
|
||||
| `bench.md` | low | Touching `tests/bench/` or interpreting bench output. |
|
||||
|
||||
## build/ — how to build
|
||||
|
||||
| File | Churn | When to load |
|
||||
|---|---|---|
|
||||
| `local.md` | low | Reference for `autogen.sh` / `configure` / `make` flags. |
|
||||
| `docker.md` | low | Always when building — canonical environment. |
|
||||
| `ci.md` | med | Diagnosing CI failures, reading CI logs. |
|
||||
|
||||
## playbooks/ — step-by-step recipes
|
||||
|
||||
| File | Churn | When to load |
|
||||
|---|---|---|
|
||||
| `add-command.md` | med | Adding a new `/command`. |
|
||||
| `add-test.md` | med | Adding a new unit test file. |
|
||||
| `add-autocomplete.md` | med | Adding autocomplete for an existing command. |
|
||||
| `add-event-handler.md` | med | Adding a server- or client-event handler. |
|
||||
| `add-encryption.md` | med | Touching encryption modules safely. |
|
||||
|
||||
## gotchas.md
|
||||
|
||||
| File | Churn | When to load |
|
||||
|---|---|---|
|
||||
| `gotchas.md` | high (append-only) | Always scan before finishing a task. |
|
||||
|
||||
## wip/ — branch-specific notes
|
||||
|
||||
Files describing in-flight feature branches. Each file is **deleted** when
|
||||
its branch merges to `master`; the surviving content (if any) is folded into
|
||||
the stable layers above.
|
||||
|
||||
| File | Churn | When to load |
|
||||
|---|---|---|
|
||||
| `wip/feat-ai.md` | high (deleted on merge) | Working on `feat/ai`, or trying to understand AI-related code that does not yet exist on `master`. |
|
||||
66
README.md
66
README.md
@@ -1,2 +1,68 @@
|
||||
# cproof-context
|
||||
|
||||
Agent-oriented context documentation for the [cproof](https://git.jabber.space/devs/cproof)
|
||||
project (a fork of Profanity, a terminal-based XMPP client written in C).
|
||||
|
||||
This repository is **not** human-onboarding documentation. It is structured for
|
||||
consumption by AI/agent skills — tables over prose, concrete identifiers over
|
||||
descriptions, layered files loaded on demand.
|
||||
|
||||
## Layout
|
||||
|
||||
| Path | Churn | Purpose |
|
||||
|---|---|---|
|
||||
| `SKILL.md` | low | Always-loaded entry hint. Points to `INDEX.md`. |
|
||||
| `INDEX.md` | low | One-page map of every doc with churn label. |
|
||||
| `architecture/` | low | Stable structural reference: source map, test map, data flow. |
|
||||
| `patterns/` | medium | Codified conventions: memory, commands, autocomplete, events, etc. |
|
||||
| `testing/` | medium | How tests are organised, written, and stubbed. |
|
||||
| `build/` | medium | Local, Docker, and CI build flows. |
|
||||
| `playbooks/` | medium | Step-by-step recipes for common tasks. |
|
||||
| `gotchas.md` | append-only | Dated entries: one per known pitfall. |
|
||||
| `wip/` | high | Branch-specific notes for in-flight features. Deleted on merge. |
|
||||
|
||||
See `INDEX.md` for the full file list.
|
||||
|
||||
## Consumer
|
||||
|
||||
Consumed by an agent skill that operates on the cproof codebase. The skill
|
||||
should pin this repo to a specific SHA (not a branch) and treat the loaded
|
||||
files as authoritative for their stated scope.
|
||||
|
||||
## Update flow
|
||||
|
||||
1. Edits land via PRs against `master`.
|
||||
2. Drift checks (path / identifier existence in cproof HEAD) run in CI before
|
||||
merge — see `architecture/test-map.md` and `build/ci.md` for details once
|
||||
wired up.
|
||||
3. After major cproof refactors, the skill consumer bumps the pinned SHA to a
|
||||
commit known to pass drift checks.
|
||||
|
||||
## Out of scope
|
||||
|
||||
- User-specific agent preferences (language, commit-trailer policy, etc.) —
|
||||
belong in agent memory, not here.
|
||||
- Git history snapshots, changelogs, PR summaries — derivable from `git log` /
|
||||
`gh pr`.
|
||||
- Aspirational guidance — this repo describes cproof as it is, not as it
|
||||
should be.
|
||||
|
||||
## Baseline and WIP
|
||||
|
||||
The stable layers (`architecture/`, `patterns/`, `testing/`, `build/`,
|
||||
`playbooks/`, `gotchas.md`) describe **cproof on `master`**. They do not
|
||||
mention symbols, files, or commits that exist only on a feature branch.
|
||||
|
||||
In-flight feature branches that materially change the surface (new module,
|
||||
new commands, new stubs) get a single file under `wip/<branch-slug>.md`. On
|
||||
merge to `master`, that file is deleted and any surviving content is folded
|
||||
into the stable layers.
|
||||
|
||||
Current WIP: `wip/feat-ai.md` (cproof branch `feat/ai`).
|
||||
|
||||
## Status
|
||||
|
||||
Layout is being authored on `draft/split-context`. The single
|
||||
`file-structure.md` (still present on `feat/add-context`) is being decomposed
|
||||
into the structure above; once the split is complete, `file-structure.md` is
|
||||
deleted.
|
||||
|
||||
35
SKILL.md
Normal file
35
SKILL.md
Normal file
@@ -0,0 +1,35 @@
|
||||
---
|
||||
name: cproof-context
|
||||
description: Architectural and conventional context for the cproof XMPP terminal client. Load INDEX.md first; pull individual files on demand based on the task at hand.
|
||||
---
|
||||
|
||||
# cproof skill entry
|
||||
|
||||
cproof is a terminal-based XMPP client in C, fork of Profanity (renaming WIP).
|
||||
Build: autotools + GLib. Tests: cmocka (unit) + custom runner (functional) +
|
||||
custom benches.
|
||||
|
||||
## How to use this context
|
||||
|
||||
1. Read `INDEX.md` for the full file map and churn labels.
|
||||
2. For any non-trivial task, pull only the files relevant to it:
|
||||
- Touching commands? → `patterns/commands.md`, `patterns/autocomplete.md`,
|
||||
`playbooks/add-command.md`.
|
||||
- Touching tests? → `testing/unit-tests.md`, `testing/stubs.md`,
|
||||
`playbooks/add-test.md`.
|
||||
- Touching XMPP? → `patterns/xmpp.md`.
|
||||
- Building? → `build/docker.md` (canonical) or `build/local.md`.
|
||||
3. Always consult `gotchas.md` before finishing — it lists known pitfalls.
|
||||
4. If the task is on a feature branch listed under `wip/`, load the matching
|
||||
WIP file as well — the stable layers describe `master` only.
|
||||
5. Treat path/identifier references in this context as claims to verify
|
||||
against cproof HEAD before acting on them.
|
||||
|
||||
## Hard rules
|
||||
|
||||
- Build inside Docker (`Dockerfile.debian`). Do not run `autogen.sh` /
|
||||
`./configure` / `make` on the host.
|
||||
- Do not mix architecture, patterns, status, and gotchas in a single doc — each
|
||||
has its own home.
|
||||
- Do not duplicate what `ls` / `grep` would answer cheaper. This context exists
|
||||
for invariants, decisions, and "why".
|
||||
76
architecture/data-flow.md
Normal file
76
architecture/data-flow.md
Normal file
@@ -0,0 +1,76 @@
|
||||
# Data flow
|
||||
|
||||
High-level paths through the codebase. Use this when reasoning about where a
|
||||
change should land or where a bug originates.
|
||||
|
||||
## User input → action
|
||||
|
||||
```
|
||||
keypress
|
||||
→ src/ui/inputwin.c (line editor, key dispatch)
|
||||
→ src/command/cmd_ac.c (autocomplete on Tab)
|
||||
→ src/command/cmd_defs.c (parse / lookup)
|
||||
→ src/command/cmd_funcs.c (handler: cmd_<name>)
|
||||
→ side effects: src/xmpp/, src/config/, src/ui/, src/event/
|
||||
```
|
||||
|
||||
- Tab triggers autocomplete via `cmd_ac.c` lookup in `ac_funcs` hash table
|
||||
(key: `/cmd`, value: per-command `_<cmd>_autocomplete` function).
|
||||
- Enter parses input via `parse_args()` (`src/tools/parser.c`), looks up the
|
||||
`Command*` via `cmd_get()` (`src/command/cmd_defs.c`), invokes the handler
|
||||
declared with `CMD_MAINFUNC` or one of the `CMD_SUBFUNCS`.
|
||||
|
||||
## Outgoing chat message
|
||||
|
||||
```
|
||||
cmd_msg / typing-into-chatwin
|
||||
→ src/xmpp/message.c (build message stanza)
|
||||
→ src/xmpp/stanza.c (assemble XML)
|
||||
→ libstrophe (send)
|
||||
→ src/chatlog.c (persist locally)
|
||||
→ src/ui/chatwin.c (echo into window)
|
||||
```
|
||||
|
||||
Encryption injects between message build and send:
|
||||
|
||||
- OMEMO via `src/xmpp/omemo.c` → `src/omemo/omemo.c` (libsignal-protocol).
|
||||
- OTR via `src/otr/otr.c` (libotr).
|
||||
- PGP / OX via `src/pgp/gpg.c` / `src/pgp/ox.c` → `src/xmpp/ox.c`.
|
||||
|
||||
## Incoming stanza
|
||||
|
||||
```
|
||||
libstrophe callback
|
||||
→ src/xmpp/{message,presence,iq,muc,roster}.c (parse, validate)
|
||||
→ src/event/server_events.c (sv_ev_*)
|
||||
→ src/ui/{chatwin,mucwin,console,...}.c (render)
|
||||
→ src/chatlog.c (persist)
|
||||
```
|
||||
|
||||
`server_events.c` is the chokepoint — every inbound XMPP event flows through a
|
||||
`sv_ev_*` function. UI updates and persistence fan out from there.
|
||||
|
||||
## Client-initiated events
|
||||
|
||||
`src/event/client_events.c` mirrors `server_events.c` for events that
|
||||
originate locally (UI actions, command-driven state changes) but need the
|
||||
same downstream fan-out.
|
||||
|
||||
## Connection state
|
||||
|
||||
- `src/xmpp/connection.c` owns the `jabber_conn_status_t` (declared in
|
||||
`connection.h`).
|
||||
- `connection_get_status()` returns the current state. Many code paths gate
|
||||
on `JABBER_CONNECTED` — see `patterns/xmpp.md`.
|
||||
- Tests must `will_return(connection_get_status, JABBER_CONNECTED)` (or the
|
||||
desired state) before exercising any code path that consults it. See
|
||||
`gotchas.md`.
|
||||
|
||||
## Plugin hook points
|
||||
|
||||
Plugin callbacks (`src/plugins/callbacks.c`) sit on:
|
||||
- Pre/post message send and receive (around `src/xmpp/message.c` flow).
|
||||
- Connection lifecycle (around `src/xmpp/session.c`).
|
||||
- Window-switch events (around `src/ui/window_list.c`).
|
||||
|
||||
See `patterns/plugins.md`.
|
||||
59
architecture/overview.md
Normal file
59
architecture/overview.md
Normal file
@@ -0,0 +1,59 @@
|
||||
# Overview
|
||||
|
||||
## What cproof is
|
||||
|
||||
- Terminal-based XMPP (chat) client written in C.
|
||||
- Fork of [Profanity](https://github.com/profanity-im/profanity); rename to
|
||||
cproof is **work in progress** — many internal symbols, copyright headers,
|
||||
and identifiers still say "Profanity".
|
||||
- Heavy GLib usage (strings, lists, hash tables, key files).
|
||||
- Optional integrations: OMEMO (libsignal-protocol), OTR (libotr), PGP/OX
|
||||
(gpgme), Python plugins, libnotify, libgcrypt, libreadline, ncurses.
|
||||
|
||||
## Build system
|
||||
|
||||
- Autotools: `configure.ac` + a top-level `Makefile.am`. No CMake.
|
||||
- `autogen.sh` regenerates the configure script.
|
||||
- `configure-debug` is a thin helper that invokes `./configure` with
|
||||
debug-friendly flags.
|
||||
- Canonical build environment: Docker (`Dockerfile.debian` at repo root).
|
||||
See `build/docker.md`.
|
||||
|
||||
## Test framework
|
||||
|
||||
- **Unit tests:** cmocka, run via `make check`. Live in `tests/unittests/`.
|
||||
- **Functional tests:** custom runner in `tests/functionaltests/` against a
|
||||
real running cproof instance.
|
||||
- **Benches:** custom runners in `tests/bench/<runner>/` for database /
|
||||
flatfile / long-message workloads.
|
||||
- See `architecture/test-map.md` and the `testing/` directory for detail.
|
||||
|
||||
## High-level module split
|
||||
|
||||
Source lives under `src/` in module directories — see
|
||||
`architecture/source-map.md` for the full table. Major chunks:
|
||||
|
||||
- **Command layer** (`src/command/`) — `/foo`-style commands the user types.
|
||||
- **XMPP layer** (`src/xmpp/`) — protocol, sessions, stanza building.
|
||||
- **UI layer** (`src/ui/`) — windows, buffers, status bar, input.
|
||||
- **Event layer** (`src/event/`) — bridges between XMPP and UI / commands.
|
||||
- **Encryption** (`src/omemo/`, `src/otr/`, `src/pgp/`) — three independent
|
||||
encryption stacks.
|
||||
- **Plugins** (`src/plugins/`) — C and Python plugin APIs.
|
||||
- **Tools** (`src/tools/`) — generic utilities (parser, autocomplete, HTTP,
|
||||
clipboard, editor).
|
||||
|
||||
Active feature branches (e.g. `feat/ai`, which adds `src/ai/`) are not
|
||||
described in the stable architecture files. See `wip/` for branch-specific
|
||||
notes.
|
||||
|
||||
## Entry points
|
||||
|
||||
| File | Role |
|
||||
|---|---|
|
||||
| `src/main.c` | Process entry. |
|
||||
| `src/profanity.c` | Application init/shutdown lifecycle. |
|
||||
| `src/common.h` | Project-wide macros (`auto_*`, `FREE_SET_NULL`, `ARRAY_SIZE`). |
|
||||
| `src/database.c` / `src/database.h` | Database (SQLite + flatfile) abstraction. |
|
||||
| `src/chatlog.c` | Chat history persistence. |
|
||||
| `src/log.c` | Application log. |
|
||||
38
architecture/source-map.md
Normal file
38
architecture/source-map.md
Normal file
@@ -0,0 +1,38 @@
|
||||
# Source map
|
||||
|
||||
## `src/` module directories
|
||||
|
||||
| Path | Purpose | Key files |
|
||||
|---|---|---|
|
||||
| `src/command/` | `/command` definition, dispatch, autocompletion. | `cmd_defs.c/h`, `cmd_funcs.c/h`, `cmd_ac.c/h` |
|
||||
| `src/config/` | Persistent configuration: accounts, prefs, themes, colors, CA file, scripts. | `accounts.c/h`, `preferences.c/h`, `theme.c/h`, `color.c/h`, `cafile.c/h`, `scripts.c/h`, `tlscerts.c/h`, `conflists.c/h`, `files.c/h`, `account.c/h` |
|
||||
| `src/event/` | Mediates between XMPP layer and UI/commands. | `client_events.c/h`, `server_events.c/h`, `common.c/h` |
|
||||
| `src/omemo/` | OMEMO encryption (libsignal-protocol). | `omemo.c/h`, `crypto.c/h`, `store.c/h` |
|
||||
| `src/otr/` | OTR encryption (libotr v4). | `otr.c/h`, `otrlib.h`, `otrlibv4.c` |
|
||||
| `src/pgp/` | OpenPGP / OX (gpgme). | `gpg.c/h`, `ox.c/h` |
|
||||
| `src/plugins/` | C and Python plugin support. | `plugins.c/h`, `c_plugins.c/h`, `python_plugins.c/h`, `c_api.c/h`, `python_api.c/h`, `api.c/h`, `profapi.c/h`, `callbacks.c/h`, `autocompleters.c/h`, `disco.c/h`, `settings.c/h`, `themes.c/h` |
|
||||
| `src/tools/` | Generic utilities. | `parser.c/h`, `autocomplete.c/h`, `clipboard.c/h`, `editor.c/h`, `http_*.c/h`, `aesgcm_download.c/h`, `bookmark_ignore.c/h`, `plugin_download.c/h` |
|
||||
| `src/ui/` | Windows, buffers, status bar, input. | `window.c/h`, `window_list.c/h`, `chatwin.c`, `mucwin.c`, `console.c`, `confwin.c`, `privwin.c`, `xmlwin.c`, `vcardwin.c`, `rosterwin.c`, `occupantswin.c`, `inputwin.c/h`, `statusbar.c/h`, `titlebar.c/h`, `tray.c/h`, `screen.c/h`, `notifier.c`, `core.c`, `buffer.c/h`, `ui.h`, `win_types.h` |
|
||||
| `src/xmpp/` | XMPP protocol layer. | `connection.c/h`, `session.c/h`, `roster.c/h`, `roster_list.c/h`, `muc.c/h`, `presence.c/h`, `message.c/h`, `iq.c/h`, `stanza.c/h`, `vcard.c/h`, `vcard_funcs.h`, `jid.c/h`, `capabilities.c/h`, `omemo.c/h`, `ox.c/h`, `blocking.c/h`, `bookmark.c/h`, `chat_session.c/h`, `chat_state.c/h`, `avatar.c/h`, `contact.c/h`, `form.c/h`, `resource.c/h` |
|
||||
|
||||
## Top-level files in `src/`
|
||||
|
||||
| File | Role |
|
||||
|---|---|
|
||||
| `main.c` | Process entry. |
|
||||
| `profanity.c` / `profanity.h` | Application init/shutdown. |
|
||||
| `common.h` / `common.c` | Project-wide macros and small helpers. |
|
||||
| `database.c` / `database.h` | Database facade (SQLite + flatfile). |
|
||||
| `chatlog.c` / `chatlog.h` | Chat log writer/reader. |
|
||||
| `log.c` / `log.h` | Application log. |
|
||||
| `config.h.in` | autoconf-generated config header (do not edit `config.h`). |
|
||||
| `gitversion.h.in` | Generated `gitversion.h` carrying build-time SHA. |
|
||||
|
||||
## Naming notes
|
||||
|
||||
- The fork-rename (Profanity → cproof) is incomplete. Many symbols, file
|
||||
paths, and headers still carry "Profanity" / "prof_" prefixes. Treat them as
|
||||
current and authoritative; do not rename opportunistically.
|
||||
- Encryption modules under `src/omemo/`, `src/otr/`, `src/pgp/` use module-
|
||||
internal helpers; the XMPP-side bridges live in `src/xmpp/omemo.c` and
|
||||
`src/xmpp/ox.c` (separate files with the same names — be precise about path).
|
||||
69
architecture/test-map.md
Normal file
69
architecture/test-map.md
Normal file
@@ -0,0 +1,69 @@
|
||||
# Test map
|
||||
|
||||
## Top-level test directories
|
||||
|
||||
| Path | Runner | Purpose |
|
||||
|---|---|---|
|
||||
| `tests/unittests/` | cmocka via `make check` | Unit tests with stubbed dependencies. |
|
||||
| `tests/functionaltests/` | custom binary, runs against a real cproof | End-to-end behaviour tests. |
|
||||
| `tests/bench/` | custom benches, one binary per runner | Database / flatfile / long-message benchmarks. |
|
||||
| `tests/prof_cmocka.h` | — | Thin cmocka wrapper used by all unit tests. |
|
||||
|
||||
## `tests/unittests/` layout
|
||||
|
||||
- `unittests.c` — single test runner. Registers every test via
|
||||
`cmocka_unit_test()` / `cmocka_unit_test_setup_teardown()`.
|
||||
- `helpers.c` / `helpers.h` — shared test utilities.
|
||||
- `test_<topic>.c` + `test_<topic>.h` — paired test files. The header
|
||||
declares each test function; the runner includes the header to register it.
|
||||
- Per-module subdirectories carry **stubs** for that module's public symbols:
|
||||
|
||||
| Stub directory | Stubs for |
|
||||
|---|---|
|
||||
| `tests/unittests/chatlog/` | `src/chatlog.c` |
|
||||
| `tests/unittests/command/` | `src/command/` |
|
||||
| `tests/unittests/config/` | `src/config/` (accounts, cafile, etc.) |
|
||||
| `tests/unittests/database/` | `src/database.c` |
|
||||
| `tests/unittests/event/` | `src/event/` |
|
||||
| `tests/unittests/log/` | `src/log.c` |
|
||||
| `tests/unittests/omemo/` | `src/omemo/` |
|
||||
| `tests/unittests/otr/` | `src/otr/` |
|
||||
| `tests/unittests/pgp/` | `src/pgp/` |
|
||||
| `tests/unittests/plugins/` | `src/plugins/` |
|
||||
| `tests/unittests/tools/` | `src/tools/` |
|
||||
| `tests/unittests/ui/` | `src/ui/` |
|
||||
| `tests/unittests/xmpp/` | `src/xmpp/` |
|
||||
| `tests/unittests/unittests/` | top-level utility stubs |
|
||||
|
||||
Branch-specific stubs (e.g. `tests/unittests/ai/` on `feat/ai`) are not
|
||||
listed here — see `wip/` for branch notes.
|
||||
|
||||
Each stub file is named `stub_<module>.c`. See `testing/stubs.md` for when
|
||||
to add or extend a stub.
|
||||
|
||||
## `tests/functionaltests/` layout
|
||||
|
||||
- `functionaltests.c` — runner.
|
||||
- `proftest.c` / `proftest.h` — fixture: spawns cproof under PTY, talks XMPP.
|
||||
- `test_<feature>.c` — feature suites: connect, disconnect, message, presence,
|
||||
carbons, roster, history, ping, autoping, lastactivity, software, disco,
|
||||
receipts, rooms, muc, chat_session, export_import.
|
||||
|
||||
Functional tests require:
|
||||
- A test XMPP server reachable from the test harness.
|
||||
- A pre-baked `.profrc` (see recent perf work).
|
||||
- See `testing/functional-tests.md` for prerequisites and runner flags.
|
||||
|
||||
## `tests/bench/` layout
|
||||
|
||||
Each runner is a separate directory + binary:
|
||||
|
||||
| Runner | Measures |
|
||||
|---|---|
|
||||
| `bench_runner` | Combined runner for general bench scenarios. |
|
||||
| `bench_export_import` | Database export/import throughput. |
|
||||
| `bench_failure_modes` | Flatfile parser failure / recovery paths. |
|
||||
| `bench_long_messages` | Flatfile parser with very long messages. |
|
||||
| `gen_history` | Generates synthetic chat history for benches. |
|
||||
|
||||
See `testing/bench.md` for invocation and interpretation.
|
||||
55
build/ci.md
Normal file
55
build/ci.md
Normal file
@@ -0,0 +1,55 @@
|
||||
# CI
|
||||
|
||||
## Workflows
|
||||
|
||||
`.github/workflows/`:
|
||||
|
||||
| Workflow | Purpose |
|
||||
|---|---|
|
||||
| `ci-code.yml` | Build + unit tests across the Linux distro matrix. |
|
||||
| `ci-api-docs.yml` | API/plugin docs build check. |
|
||||
|
||||
Plus an OpenBSD pipeline at `.builds/openbsd.yml` (sourcehut-style builds).
|
||||
|
||||
## Distro matrix
|
||||
|
||||
The code CI runs across the Dockerfiles at repo root: Debian, Ubuntu,
|
||||
Fedora, Tumbleweed, Arch. Each job builds the matching image, mounts the
|
||||
checkout, and runs `autogen.sh && configure && make && make check`.
|
||||
|
||||
## Log naming
|
||||
|
||||
CI logs in this project are named:
|
||||
|
||||
```
|
||||
ci-code-Linux (arch)-NNNN.log
|
||||
```
|
||||
|
||||
— where `arch` is the distro flavour (debian, ubuntu, fedora, tumbleweed,
|
||||
arch) and `NNNN` is the run number. Use the distro name to pick the right
|
||||
log when triaging matrix failures.
|
||||
|
||||
## Known matrix-specific notes
|
||||
|
||||
- **Arch (`Dockerfile.arch`):** Pikaur in the image was hit by a duplicated-flag
|
||||
failure (cf. cproof commit `0722dc9e3`). If `Dockerfile.arch` changes,
|
||||
re-check the Pikaur invocation.
|
||||
- **OpenBSD (`.builds/openbsd.yml`):** runs on sourcehut, not GitHub. Failures
|
||||
there don't appear in the GitHub PR check list.
|
||||
|
||||
## Reproducing a CI failure locally
|
||||
|
||||
1. Identify the failing distro from the log name.
|
||||
2. Build the matching Dockerfile locally — see `build/docker.md`.
|
||||
3. Run the same `autogen && configure && make && make check` chain inside the
|
||||
container.
|
||||
4. Compare versions of any drifting dependency between the image and the CI
|
||||
image (`apt list --installed` or equivalent inside both).
|
||||
|
||||
## What CI does not run
|
||||
|
||||
- Functional tests (`tests/functionaltests/`) — require a test XMPP server.
|
||||
- Benches (`tests/bench/`) — not part of CI.
|
||||
|
||||
If you need either, run them locally inside the Docker image — see
|
||||
`testing/functional-tests.md` and `testing/bench.md`.
|
||||
75
build/docker.md
Normal file
75
build/docker.md
Normal file
@@ -0,0 +1,75 @@
|
||||
# Docker build
|
||||
|
||||
Canonical build environment. **Use this; do not build on the host.**
|
||||
|
||||
## Dockerfiles
|
||||
|
||||
All at repo root:
|
||||
|
||||
| File | Base | Purpose |
|
||||
|---|---|---|
|
||||
| `Dockerfile.debian` | `debian:testing` | **Canonical.** Mirrors CI for Linux. |
|
||||
| `Dockerfile.ubuntu` | `ubuntu` | Alternate Linux for compatibility checks. |
|
||||
| `Dockerfile.fedora` | `fedora` | Fedora compatibility. |
|
||||
| `Dockerfile.tumbleweed` | `opensuse/tumbleweed` | openSUSE compatibility. |
|
||||
| `Dockerfile.arch` | `archlinux` | Arch CI matrix. |
|
||||
|
||||
Default: `Dockerfile.debian` for local dev, build, and `make check`.
|
||||
|
||||
## Build the image
|
||||
|
||||
```sh
|
||||
docker build -t cproof-debian -f Dockerfile.debian .
|
||||
```
|
||||
|
||||
`Dockerfile.debian` installs the autotools toolchain plus all optional
|
||||
dependencies (cmocka, libsignal-protocol-c, libotr5, gpgme, libcurl,
|
||||
libmicrohttpd, libnotify, ncurses, GLib, libgcrypt, libreadline, libssl,
|
||||
Python). Treat its `apt-get install` block as the authoritative dependency
|
||||
list.
|
||||
|
||||
## Build cproof inside the container
|
||||
|
||||
Bind-mount the working tree and run the autotools sequence:
|
||||
|
||||
```sh
|
||||
docker run --rm -it \
|
||||
-v "$PWD":/src -w /src \
|
||||
cproof-debian \
|
||||
bash -c './autogen.sh && ./configure && make -j$(nproc)'
|
||||
```
|
||||
|
||||
For tests:
|
||||
|
||||
```sh
|
||||
docker run --rm -it \
|
||||
-v "$PWD":/src -w /src \
|
||||
cproof-debian \
|
||||
bash -c './autogen.sh && ./configure && make -j$(nproc) check'
|
||||
```
|
||||
|
||||
`ccache` is enabled in the image (`CC="ccache gcc"`) — share a ccache volume
|
||||
between runs for faster rebuilds:
|
||||
|
||||
```sh
|
||||
-v "$HOME/.ccache":/root/.ccache
|
||||
```
|
||||
|
||||
## Why Docker
|
||||
|
||||
- Pinned OS + package set; the same toolchain CI uses.
|
||||
- Host glib / openssl / libstrophe drift cannot mask or invent failures.
|
||||
- Encryption-stack dependencies (libsignal-protocol-c especially) are
|
||||
awkward on host package managers; the image already has them.
|
||||
|
||||
## When to use a non-Debian image
|
||||
|
||||
- Reproducing a CI failure that only triggers on Arch / Fedora / Ubuntu /
|
||||
Tumbleweed. Use the matching Dockerfile, same bind-mount pattern.
|
||||
- Otherwise: stick with Debian.
|
||||
|
||||
## Hard rule
|
||||
|
||||
Do not run `./autogen.sh`, `./configure`, or `make` on the host shell unless
|
||||
you have an explicit reason and have verified the host toolchain matches the
|
||||
image. CI is the source of truth; CI uses Debian.
|
||||
68
build/local.md
Normal file
68
build/local.md
Normal file
@@ -0,0 +1,68 @@
|
||||
# Local build
|
||||
|
||||
Reference for `autogen.sh` / `configure` / `make` flags. **Do not run these
|
||||
on the host as a default workflow** — see `build/docker.md`. This file
|
||||
documents what the canonical Docker build does internally.
|
||||
|
||||
## Toolchain
|
||||
|
||||
- autotools: `autoconf`, `automake`, `libtool`, `autoconf-archive`.
|
||||
- C compiler: `gcc` (CI) or `clang`.
|
||||
- GLib 2, ncursesw, OpenSSL, libstrophe, libreadline.
|
||||
- Optional: libcmocka (tests), libsignal-protocol-c (OMEMO), libotr5 (OTR),
|
||||
gpgme (PGP), libcurl (HTTP), libmicrohttpd (HTTP server features),
|
||||
libnotify, libgcrypt, Python (plugins).
|
||||
|
||||
The full Debian package list is in `Dockerfile.debian` — treat that as the
|
||||
authoritative dependency list.
|
||||
|
||||
## Sequence
|
||||
|
||||
```sh
|
||||
./autogen.sh # generate configure from configure.ac
|
||||
./configure [opts]
|
||||
make -jN
|
||||
```
|
||||
|
||||
`autogen.sh` runs `aclocal`, `autoheader`, `automake`, `autoconf`, `libtoolize`.
|
||||
|
||||
## Useful `./configure` flags
|
||||
|
||||
| Flag | Effect |
|
||||
|---|---|
|
||||
| `--enable-debug` | Debug build (the `configure-debug` script wraps this). |
|
||||
| `--disable-omemo` / `--disable-otr` / `--disable-pgp` | Drop encryption stack. |
|
||||
| `--disable-python-plugins` | C-only plugins. |
|
||||
| `--disable-notifications` | No libnotify. |
|
||||
| `--prefix=<path>` | Install prefix. |
|
||||
|
||||
`./configure --help` prints the full set; flag names track upstream Profanity
|
||||
closely.
|
||||
|
||||
## `configure-debug`
|
||||
|
||||
Convenience script at repo root. Roughly:
|
||||
|
||||
```sh
|
||||
./autogen.sh
|
||||
./configure --enable-debug --prefix=$(pwd)/install
|
||||
```
|
||||
|
||||
Tweak in-script if you want different debug knobs.
|
||||
|
||||
## `make` targets
|
||||
|
||||
| Target | What |
|
||||
|---|---|
|
||||
| `make` | Builds the `profanity` binary (and `cproof` symlink, if rename has progressed to it). |
|
||||
| `make check` | Builds and runs the unit-test suite. |
|
||||
| `make clean` | Wipes object files. |
|
||||
| `make distclean` | Wipes generated configure artefacts too. |
|
||||
|
||||
## Why this is host-unfriendly
|
||||
|
||||
- Library version drift between the host (a workstation) and the canonical
|
||||
Debian build means the host build can succeed while CI fails (or vice
|
||||
versa) — bugs that don't reproduce in either env are very expensive.
|
||||
- `Dockerfile.debian` pins the OS and package set used by CI. Mirror it
|
||||
locally instead of fighting the host.
|
||||
114
gotchas.md
Normal file
114
gotchas.md
Normal file
@@ -0,0 +1,114 @@
|
||||
# Gotchas
|
||||
|
||||
Append-only log of known pitfalls. One entry per pitfall. Format:
|
||||
|
||||
```
|
||||
## YYYY-MM-DD — <short title>
|
||||
|
||||
<body — what bites, why, what to do instead>
|
||||
```
|
||||
|
||||
Scan this file before finishing any non-trivial task.
|
||||
|
||||
---
|
||||
|
||||
## 2026-04-30 — `g_strsplit` result must use `g_strfreev`
|
||||
|
||||
`g_strsplit` (and `g_strdupv`) return a `gchar**` whose elements are
|
||||
individually GLib-allocated. Free with `g_strfreev`, **not** `g_free`. The
|
||||
latter compiles fine and silently leaks the inner strings.
|
||||
|
||||
The cleanup macro is `auto_gcharv` (declared in `src/common.h`):
|
||||
|
||||
```c
|
||||
auto_gcharv gchar** parts = g_strsplit(line, " ", -1);
|
||||
```
|
||||
|
||||
Mixing `g_free` on a `gchar**` is the most common allocator-mismatch bug in
|
||||
this codebase.
|
||||
|
||||
---
|
||||
|
||||
## 2026-04-30 — `JABBER_CONNECTED` must be queued in unit tests
|
||||
|
||||
Most XMPP-touching code paths gate on `connection_get_status()`. The unit-
|
||||
test stub returns `mock()`, so every test reaching that gate must:
|
||||
|
||||
```c
|
||||
will_return(connection_get_status, JABBER_CONNECTED);
|
||||
// or
|
||||
will_return(connection_get_status, JABBER_DISCONNECTED);
|
||||
```
|
||||
|
||||
Forgetting this aborts cmocka with a "missing mock value" error. If you see
|
||||
that error, it's almost always `connection_get_status` you forgot.
|
||||
|
||||
---
|
||||
|
||||
## 2026-04-30 — Autocomplete callbacks must be stateless
|
||||
|
||||
Some older callbacks keep "last match" state in a file-static `_last_*_match`
|
||||
variable. Do not copy that pattern. A clean canonical example is
|
||||
`roster_contact_autocomplete` (`src/xmpp/roster_list.c`) — it delegates
|
||||
straight to `autocomplete_complete` against a roster-owned `Autocomplete`
|
||||
object and keeps no callback-local state.
|
||||
|
||||
Stateful callbacks misbehave under shift-tab cycling and break when two
|
||||
completers run concurrently in a layered command (e.g. `/foo set bar <tab>`
|
||||
followed by `/foo set baz <tab>`).
|
||||
|
||||
---
|
||||
|
||||
## 2026-04-30 — CWE-134 format-string discipline
|
||||
|
||||
Never pass user-controlled or arbitrary strings as the format argument to
|
||||
`printf`-family or GLib formatted-print functions. A recent compiler-
|
||||
hardening pass (cf. cproof commit `9ec01fa8c`) audited and tightened these
|
||||
sites; do not regress.
|
||||
|
||||
Right:
|
||||
|
||||
```c
|
||||
cons_show("%s", user_text);
|
||||
```
|
||||
|
||||
Wrong:
|
||||
|
||||
```c
|
||||
cons_show(user_text);
|
||||
```
|
||||
|
||||
The compiler does not always warn, especially across helper layers. Enable
|
||||
and respect `-Wformat -Wformat-security` warnings in any build you trust.
|
||||
|
||||
---
|
||||
|
||||
## 2026-04-30 — Build inside Docker, not on the host
|
||||
|
||||
Library version drift between a developer host and `Dockerfile.debian`
|
||||
(GLib, OpenSSL, libstrophe in particular) can mask or invent failures. CI
|
||||
is the source of truth; CI builds inside the Debian image. See
|
||||
`build/docker.md`.
|
||||
|
||||
If you absolutely must build on the host, mirror the package set in
|
||||
`Dockerfile.debian` exactly and pin versions — you will spend more time
|
||||
chasing version drift than the Docker build would have cost.
|
||||
|
||||
---
|
||||
|
||||
## 2026-04-30 — Beware of "Profanity" vs "cproof" naming
|
||||
|
||||
The fork-rename is incomplete. Many files, symbols, copyright headers, and
|
||||
config keys still say `Profanity` / `prof_` / `profanity`. These are
|
||||
authoritative as-is; do **not** rename them opportunistically alongside
|
||||
unrelated changes. Rename work, when it happens, will be a dedicated PR.
|
||||
|
||||
---
|
||||
|
||||
## 2026-04-30 — Encryption disable builds must compile
|
||||
|
||||
Each encryption stack (`HAVE_OMEMO`, `HAVE_LIBOTR`, `HAVE_LIBGPGME`) can be
|
||||
turned off at `configure` time. New code that calls into these modules
|
||||
must wrap calls in the matching `#ifdef` and provide a graceful fallback
|
||||
in the `#else`. CI does not always test every disable combination — verify
|
||||
locally with at least the off-build for each stack you touched.
|
||||
97
patterns/autocomplete.md
Normal file
97
patterns/autocomplete.md
Normal file
@@ -0,0 +1,97 @@
|
||||
# Autocomplete
|
||||
|
||||
## The `Autocomplete` type
|
||||
|
||||
`src/tools/autocomplete.h` defines an opaque `Autocomplete` (GList-backed
|
||||
sorted set with a stateful "current iterator" for cycling through matches).
|
||||
|
||||
| Function | Purpose |
|
||||
|---|---|
|
||||
| `autocomplete_new()` | Create empty AC. |
|
||||
| `autocomplete_clear(ac)` | Drop all entries (keep object). |
|
||||
| `autocomplete_free(ac)` | Destroy. |
|
||||
| `autocomplete_add(ac, item)` | Insert sorted. |
|
||||
| `autocomplete_add_unsorted(ac, item, reversed)` | Append, no sort. |
|
||||
| `autocomplete_add_all(ac, items)` | Bulk add from `char**`. |
|
||||
| `autocomplete_update(ac, items)` | Clear + add_all. |
|
||||
| `autocomplete_remove(ac, item)` | Drop one. |
|
||||
| `autocomplete_remove_all(ac, items)` | Drop many. |
|
||||
| `autocomplete_complete(ac, search, quote, previous)` | Cycle to next/previous match. |
|
||||
| `autocomplete_reset(ac)` | Reset cycle position. |
|
||||
| `autocomplete_contains(ac, value)` | Membership. |
|
||||
|
||||
## Two completion flavours used in commands
|
||||
|
||||
In `src/command/cmd_ac.c`:
|
||||
|
||||
```c
|
||||
autocomplete_param_with_ac(input, "/cmd sub", static_ac, quote, previous);
|
||||
autocomplete_param_with_func(input, "/cmd sub", callback, previous, ctx);
|
||||
```
|
||||
|
||||
| When to use | Choose |
|
||||
|---|---|
|
||||
| Fixed token set known at startup. | `autocomplete_param_with_ac` + a static `Autocomplete` (allocated in `cmd_ac_init`, freed in `cmd_ac_uninit`). |
|
||||
| Token set is dynamic / queried from state. | `autocomplete_param_with_func` + a callback `char* fn(const char* search, gboolean previous, void* ctx)`. |
|
||||
|
||||
## Static Autocompletes
|
||||
|
||||
Pattern (from `cmd_ac.c`):
|
||||
|
||||
```c
|
||||
// File-scope:
|
||||
static Autocomplete account_ac;
|
||||
|
||||
// In cmd_ac_init():
|
||||
account_ac = autocomplete_new();
|
||||
autocomplete_add(account_ac, "list");
|
||||
autocomplete_add(account_ac, "show");
|
||||
autocomplete_add(account_ac, "add");
|
||||
// ...
|
||||
|
||||
// Registered with:
|
||||
g_hash_table_insert(ac_funcs, "/account", _account_autocomplete);
|
||||
```
|
||||
|
||||
Static `Autocomplete` instances are added to a file-scope free-list array
|
||||
near the top of `cmd_ac.c` so `cmd_ac_uninit` can call `autocomplete_free`
|
||||
on each at shutdown. **Add new static ACs to that free-list.**
|
||||
|
||||
## Function-callback completers
|
||||
|
||||
```c
|
||||
char* roster_contact_autocomplete(const char* const search_str,
|
||||
gboolean previous, void* context);
|
||||
```
|
||||
|
||||
- Stateless across invocations — no module-level `_last_match` globals.
|
||||
- Returns a freshly-allocated `char*` (caller frees), or `NULL` when no more
|
||||
matches.
|
||||
- `previous` cycles backward (Shift+Tab).
|
||||
- `context` is whatever was passed in `autocomplete_param_with_func`'s last
|
||||
arg (often `NULL`).
|
||||
|
||||
`roster_contact_autocomplete` (`src/xmpp/roster_list.c`) is a clean
|
||||
canonical example: it delegates straight to `autocomplete_complete` against
|
||||
a roster-owned `Autocomplete` object — no callback-local state.
|
||||
|
||||
**Avoid the legacy "store previous match in a static" pattern.** Some older
|
||||
completers do it; do not copy them. Keeping callbacks stateless makes
|
||||
shift-tab cycling and concurrent completers safe.
|
||||
|
||||
## Per-command dispatcher
|
||||
|
||||
Every command's autocomplete is a single static function in `cmd_ac.c`:
|
||||
|
||||
```c
|
||||
static char* _account_autocomplete(ProfWin* window, const char* const input, gboolean previous);
|
||||
```
|
||||
|
||||
Registered in `cmd_ac_init`:
|
||||
|
||||
```c
|
||||
g_hash_table_insert(ac_funcs, "/account", _account_autocomplete);
|
||||
```
|
||||
|
||||
The dispatcher inspects the input prefix and picks the right
|
||||
`autocomplete_param_with_*` call.
|
||||
109
patterns/commands.md
Normal file
109
patterns/commands.md
Normal file
@@ -0,0 +1,109 @@
|
||||
# Commands
|
||||
|
||||
## Three files, three concerns
|
||||
|
||||
| File | Holds |
|
||||
|---|---|
|
||||
| `src/command/cmd_defs.c` (`cmd_defs.h`) | The big static array of `Command` records — every `/foo` is one entry. |
|
||||
| `src/command/cmd_funcs.c` (`cmd_funcs.h`) | Handler implementations: `gboolean cmd_<name>(ProfWin*, const char* const, gchar**)`. |
|
||||
| `src/command/cmd_ac.c` (`cmd_ac.h`) | Per-command autocomplete logic and registration. |
|
||||
|
||||
## Anatomy of a command record
|
||||
|
||||
Defined inline in `cmd_defs.c` using uppercase macros (declared in `cmd_defs.c`):
|
||||
|
||||
```c
|
||||
{ CMD_PREAMBLE("/caps",
|
||||
parse_args, 0, 1, NULL)
|
||||
CMD_MAINFUNC(cmd_caps)
|
||||
CMD_TAGS(
|
||||
CMD_TAG_DISCOVERY,
|
||||
CMD_TAG_CHAT,
|
||||
CMD_TAG_GROUPCHAT)
|
||||
CMD_SYN(
|
||||
"/caps",
|
||||
"/caps <fulljid>|<nick>")
|
||||
CMD_DESC("...")
|
||||
CMD_ARGS(
|
||||
{ "<fulljid>", "..." },
|
||||
{ "<nick>", "..." })
|
||||
CMD_EXAMPLES(
|
||||
"/caps user@host/res")
|
||||
}
|
||||
```
|
||||
|
||||
| Macro | Role |
|
||||
|---|---|
|
||||
| `CMD_PREAMBLE(name, parse_fn, min, max, pre_hook)` | Command name, parser, arg count bounds, optional pre-hook. |
|
||||
| `CMD_MAINFUNC(fn)` | Single handler for the whole command. |
|
||||
| `CMD_SUBFUNCS({"sub", fn}, ...)` | Dispatch by first argument. Use **instead of** `CMD_MAINFUNC` when the command has subcommands (e.g. `/status get`, `/status set`). |
|
||||
| `CMD_TAGS(...)` | One or more `CMD_TAG_*` (UI grouping, search). |
|
||||
| `CMD_SYN(...)` | Synopsis lines for `/help <cmd>`. |
|
||||
| `CMD_DESC(...)` | Long description. |
|
||||
| `CMD_ARGS({"name", "desc"}, ...)` | Argument table for help. |
|
||||
| `CMD_EXAMPLES(...)` | Examples shown in help. |
|
||||
|
||||
The full set of `CMD_*` macro definitions is at the top of `cmd_defs.c` —
|
||||
read them when authoring a new command.
|
||||
|
||||
## Argument parsing
|
||||
|
||||
`parse_args()` lives in `src/tools/parser.c`:
|
||||
|
||||
```c
|
||||
gchar** parse_args(const char* const inp, int min, int max, gboolean* result);
|
||||
```
|
||||
|
||||
- Splits on whitespace, honours quoted segments.
|
||||
- Returns `NULL` on parse failure (sets `*result = FALSE`).
|
||||
- The handler receives the result as `gchar** args` — free is owned by the
|
||||
caller in `cmd_defs.c`, the handler does **not** free.
|
||||
|
||||
Parser variants used in `CMD_PREAMBLE`:
|
||||
- `parse_args` — standard quoted-aware split.
|
||||
- `parse_args_with_freetext` — last argument is the rest of the line, no
|
||||
splitting (used by `/msg`, `/me`, etc.).
|
||||
- `parse_args_as_one` — concatenates remaining tokens into a single string.
|
||||
|
||||
## Handler signature
|
||||
|
||||
```c
|
||||
gboolean cmd_foo(ProfWin* window, const char* const command, gchar** args);
|
||||
```
|
||||
|
||||
Return `TRUE` on success, `FALSE` to print bad-usage. `command` is the literal
|
||||
`/foo` typed (useful when the same handler is registered under multiple names).
|
||||
|
||||
## Connection-state gates
|
||||
|
||||
Most commands that touch XMPP must verify connection state first:
|
||||
|
||||
```c
|
||||
if (connection_get_status() != JABBER_CONNECTED) {
|
||||
cons_show("You are not currently connected.");
|
||||
return TRUE;
|
||||
}
|
||||
```
|
||||
|
||||
`cons_bad_cmd_usage(cmd)` is the canonical "wrong arguments" path; pass the
|
||||
literal command name.
|
||||
|
||||
## Adding a new command
|
||||
|
||||
See `playbooks/add-command.md` for the full walkthrough. Short version:
|
||||
|
||||
1. New entry in `cmd_defs.c` with `CMD_PREAMBLE`/`CMD_MAINFUNC`/...
|
||||
2. Implement `cmd_<name>()` in `cmd_funcs.c` (declare in `cmd_funcs.h`).
|
||||
3. (If autocompletion needed) implement `_<name>_autocomplete()` in
|
||||
`cmd_ac.c` and register it via `g_hash_table_insert(ac_funcs, "/name", ...)`.
|
||||
4. Add a unit test pair `tests/unittests/test_cmd_<name>.{c,h}` and register
|
||||
tests in `tests/unittests/unittests.c`.
|
||||
|
||||
## Conventions
|
||||
|
||||
- `cmd_<name>` for handlers; `_<name>_autocomplete` (file-static) for the
|
||||
autocomplete callback.
|
||||
- Subcommand dispatch via `CMD_SUBFUNCS` is preferred over `if/else` chains
|
||||
inside `cmd_<name>` when possible.
|
||||
- Keep handlers in `cmd_funcs.c` thin: parse & validate args, then delegate
|
||||
to a domain module (`xmpp/`, `config/`, `ui/`).
|
||||
71
patterns/encryption.md
Normal file
71
patterns/encryption.md
Normal file
@@ -0,0 +1,71 @@
|
||||
# Encryption
|
||||
|
||||
Three independent stacks, all optional at build time (`./configure` flags):
|
||||
|
||||
| Module | Backend | XMPP bridge |
|
||||
|---|---|---|
|
||||
| `src/omemo/` | libsignal-protocol-c | `src/xmpp/omemo.c` |
|
||||
| `src/otr/` | libotr v4 | (in-stream, no separate xmpp bridge) |
|
||||
| `src/pgp/` | gpgme | `src/xmpp/ox.c` (for OX/XEP-0373) |
|
||||
|
||||
## Module layouts
|
||||
|
||||
### OMEMO (`src/omemo/`)
|
||||
|
||||
| File | Role |
|
||||
|---|---|
|
||||
| `omemo.c/h` | Public surface: session setup, encrypt/decrypt, key publish/fetch. |
|
||||
| `crypto.c/h` | AES-GCM payload encryption; libsignal-protocol crypto provider. |
|
||||
| `store.c/h` | Persistent identity / pre-key / signed pre-key / session storage. |
|
||||
|
||||
`src/xmpp/omemo.c` is the bridge — it talks to `src/omemo/omemo.c` and to
|
||||
libstrophe stanza building.
|
||||
|
||||
### OTR (`src/otr/`)
|
||||
|
||||
| File | Role |
|
||||
|---|---|
|
||||
| `otr.c/h` | Public surface: start/end/secure messaging, fingerprint mgmt. |
|
||||
| `otrlib.h` | Compatibility shim across libotr versions. |
|
||||
| `otrlibv4.c` | libotr v4-specific implementation. |
|
||||
|
||||
OTR runs in-band on the message stream, no separate XMPP bridge file.
|
||||
|
||||
### PGP / OX (`src/pgp/`)
|
||||
|
||||
| File | Role |
|
||||
|---|---|
|
||||
| `gpg.c/h` | Classic PGP signing/encrypting via gpgme. |
|
||||
| `ox.c/h` | XEP-0373 / XEP-0374 (OX): OpenPGP for XMPP. |
|
||||
|
||||
`src/xmpp/ox.c` is the OX bridge; `gpg.c` is invoked directly from message
|
||||
handlers.
|
||||
|
||||
## Conditional compilation
|
||||
|
||||
Each module is gated by `HAVE_OMEMO`, `HAVE_LIBOTR`, `HAVE_LIBGPGME` (set by
|
||||
`configure.ac`). Code that calls into a module guards with `#ifdef
|
||||
HAVE_<X>` and supplies a graceful fallback. When editing:
|
||||
|
||||
- Search for the existing `#ifdef` use of the symbol you are touching to
|
||||
confirm the gating macro.
|
||||
- Stubs in `tests/unittests/{omemo,otr,pgp}/stub_*.c` exist regardless — they
|
||||
satisfy the test linker even when the feature is "off" at runtime.
|
||||
|
||||
## Encryption-aware tests
|
||||
|
||||
`tests/unittests/test_forced_encryption.{c,h}` exercises the policy where
|
||||
sending plain text is refused if a session is OMEMO/OTR/PGP-locked. Touching
|
||||
encryption-policy code → check this test does not regress.
|
||||
|
||||
## Adding or modifying calls into encryption
|
||||
|
||||
1. Locate the bridge: outgoing call → typically `src/xmpp/omemo.c` /
|
||||
`src/xmpp/ox.c` / `src/otr/otr.c`.
|
||||
2. Confirm the `#ifdef HAVE_*` guard at the call site.
|
||||
3. If the module's public surface (header) changes, update the corresponding
|
||||
stub in `tests/unittests/{omemo,otr,pgp}/stub_*.c`.
|
||||
4. Run the relevant unit tests (`test_forced_encryption`, plus anything in
|
||||
`test_cmd_otr.c`, `test_cmd_pgp.c`).
|
||||
|
||||
See `playbooks/add-encryption.md`.
|
||||
59
patterns/events.md
Normal file
59
patterns/events.md
Normal file
@@ -0,0 +1,59 @@
|
||||
# Events
|
||||
|
||||
`src/event/` mediates between the XMPP protocol layer and consumers (UI,
|
||||
config, chatlog). Two main directions plus one common module.
|
||||
|
||||
## Files
|
||||
|
||||
| File | Purpose |
|
||||
|---|---|
|
||||
| `server_events.c/h` | `sv_ev_*` — events triggered by inbound stanzas. |
|
||||
| `client_events.c/h` | `cl_ev_*` — events triggered locally (UI / commands). |
|
||||
| `common.c/h` | Shared helpers used by both. |
|
||||
|
||||
## Server events (`sv_ev_*`)
|
||||
|
||||
Called from `src/xmpp/` parsers (`message.c`, `presence.c`, `iq.c`, `muc.c`,
|
||||
`roster.c`) once a stanza has been validated and decomposed. Each `sv_ev_*`:
|
||||
|
||||
1. Updates persistent state (chatlog, roster, accounts).
|
||||
2. Pushes UI updates (`chatwin_*`, `mucwin_*`, `cons_*`, `console_*`).
|
||||
3. May invoke plugin callbacks (`src/plugins/callbacks.c`).
|
||||
|
||||
Examples (current symbols, grep `sv_ev_` in `server_events.c`):
|
||||
- `sv_ev_incoming_message` — chat message arrived.
|
||||
- `sv_ev_room_message` — MUC message.
|
||||
- `sv_ev_presence_update` — presence change.
|
||||
- `sv_ev_roster_received` — roster pushed by server.
|
||||
|
||||
## Client events (`cl_ev_*`)
|
||||
|
||||
Called from `cmd_funcs.c` and UI code when the user does something locally
|
||||
that needs the same downstream fan-out as a server event.
|
||||
|
||||
Examples:
|
||||
- `cl_ev_send_msg` — outgoing chat message.
|
||||
- `cl_ev_send_presence` — presence update from `/status` or autoaway.
|
||||
- `cl_ev_disconnect` — `/disconnect` initiated locally.
|
||||
|
||||
## Adding an event handler
|
||||
|
||||
1. Decide direction: inbound (server) or outbound (client).
|
||||
2. Add `sv_ev_<name>` or `cl_ev_<name>` in the matching `.c` and declare in
|
||||
the matching `.h`.
|
||||
3. Call it from the trigger site (a parser in `src/xmpp/` for server events;
|
||||
a command handler or UI input handler for client events).
|
||||
4. Inside, perform the fan-out: state update → UI → plugin callback.
|
||||
5. If unit-tested, add a stub for it in `tests/unittests/event/stub_*.c`
|
||||
(or extend the existing stub).
|
||||
|
||||
See `playbooks/add-event-handler.md`.
|
||||
|
||||
## Conventions
|
||||
|
||||
- Keep XMPP-protocol concerns in `src/xmpp/`. The event module is a
|
||||
*dispatcher*, not a parser.
|
||||
- Keep UI rendering decisions in `src/ui/`. The event module *invokes* UI
|
||||
functions but does not draw.
|
||||
- Plugin callbacks are the **last** step — state must already be consistent
|
||||
before they run, since plugins may re-enter the codebase.
|
||||
65
patterns/memory.md
Normal file
65
patterns/memory.md
Normal file
@@ -0,0 +1,65 @@
|
||||
# Memory management
|
||||
|
||||
## `auto_*` macros
|
||||
|
||||
GCC/Clang `__cleanup__` attribute, declared in `src/common.h` (one exception:
|
||||
`auto_jid` lives in `src/xmpp/jid.h` because it needs the `Jid` type).
|
||||
|
||||
| Macro | Type | Cleanup | Use for |
|
||||
|---|---|---|---|
|
||||
| `auto_gchar` | `gchar*` | `auto_free_gchar()` | GLib strings (`g_strdup`, `g_strdup_printf`, etc.) |
|
||||
| `auto_gcharv` | `gchar**` | `auto_free_gcharv()` | GLib string arrays (`g_strsplit`, `g_strjoinv` arg) |
|
||||
| `auto_char` | `char*` | `auto_free_char()` | C strings (`strdup`, `malloc`'d) |
|
||||
| `auto_guchar` | `guchar*` | `auto_free_guchar()` | Unsigned char buffers (e.g. `g_base64_decode`) |
|
||||
| `auto_gfd` | `gint` | `auto_close_gfd()` | File descriptors held in a `gint` |
|
||||
| `auto_FILE` | `FILE*` | `auto_close_FILE()` | `FILE*` from `fopen` |
|
||||
| `auto_jid` | `Jid*` | `jid_auto_destroy()` | `Jid` structs (XMPP) |
|
||||
|
||||
Place the macro **before** the type, like `gboolean`-style attribute:
|
||||
|
||||
```c
|
||||
auto_gchar gchar* msg = g_strdup_printf("hello %s", name);
|
||||
auto_gcharv gchar** parts = g_strsplit(line, " ", -1);
|
||||
```
|
||||
|
||||
The cleanup runs at scope exit, including early returns. Do **not** call the
|
||||
matching `g_free`/`g_strfreev`/`fclose` manually — that double-frees.
|
||||
|
||||
## Manual cleanup helpers
|
||||
|
||||
In `common.h`:
|
||||
|
||||
| Macro | Behaviour |
|
||||
|---|---|
|
||||
| `FREE_SET_NULL(ptr)` | `free(ptr); ptr = NULL;` (use for `malloc`'d) |
|
||||
| `GFREE_SET_NULL(ptr)` | `g_free(ptr); ptr = NULL;` (use for GLib-allocated) |
|
||||
|
||||
Use these when the pointer is a struct field that must remain accessible
|
||||
after the free (so a later `if (x->p)` is safe).
|
||||
|
||||
## GLib free-function reference
|
||||
|
||||
| Allocator | Free with |
|
||||
|---|---|
|
||||
| `g_strdup`, `g_strdup_printf`, `g_strconcat`, ... | `g_free` (or `auto_gchar`) |
|
||||
| `g_strsplit`, `g_strdupv` | `g_strfreev` (or `auto_gcharv`) |
|
||||
| `g_list_*` of allocated items | `g_list_free_full(list, free_fn)` |
|
||||
| `g_hash_table_new[_full]` | `g_hash_table_destroy` (uses key/value destroy fns if supplied) |
|
||||
| `g_base64_decode` | `g_free` (or `auto_guchar`) |
|
||||
| `g_key_file_*` | `g_key_file_free` |
|
||||
|
||||
**Common pitfall:** mixing `g_free` and `free`. GLib uses its own allocator
|
||||
shim; never cross the boundary. If you got the buffer from a GLib function,
|
||||
free it with the matching GLib function.
|
||||
|
||||
**Common pitfall:** `g_strsplit` returns `gchar**` — free with `g_strfreev`,
|
||||
not `g_free`. (See `gotchas.md`.)
|
||||
|
||||
## Adding a new `auto_*`
|
||||
|
||||
1. Declare cleanup function: `void auto_close_foo(Foo** p);`
|
||||
2. Define the macro: `#define auto_foo __attribute__((__cleanup__(auto_close_foo)))`
|
||||
3. Place both in the header that owns the type — `common.h` for project-wide,
|
||||
the type's own header otherwise.
|
||||
4. The cleanup must tolerate `NULL` and idempotent re-entry; assign `*p = NULL`
|
||||
inside if you keep the variable accessible after free.
|
||||
67
patterns/plugins.md
Normal file
67
patterns/plugins.md
Normal file
@@ -0,0 +1,67 @@
|
||||
# Plugins
|
||||
|
||||
`src/plugins/` exposes both a C plugin ABI and a Python plugin runtime. Both
|
||||
funnel through a shared core that fires callbacks on cproof events.
|
||||
|
||||
## Files
|
||||
|
||||
| File | Role |
|
||||
|---|---|
|
||||
| `plugins.c/h` | Public surface: load/unload, list, dispatch hooks. |
|
||||
| `c_plugins.c/h` | C plugin loader (dlopen). |
|
||||
| `python_plugins.c/h` | Python plugin loader (embedded interpreter). |
|
||||
| `c_api.c/h` | C-side API exposed **to** plugins. |
|
||||
| `python_api.c/h` | Python-side API exposed **to** plugins. |
|
||||
| `api.c/h` | Shared API helpers. |
|
||||
| `profapi.c/h` | The `prof` Python module surface. |
|
||||
| `callbacks.c/h` | Hooks: pre/post message, pre/post presence, etc. |
|
||||
| `autocompleters.c/h` | Plugin-registered autocompleters. |
|
||||
| `disco.c/h` | Service-discovery feature contributions from plugins. |
|
||||
| `settings.c/h` | Per-plugin settings storage. |
|
||||
| `themes.c/h` | Plugin-supplied themes. |
|
||||
|
||||
## Hook points
|
||||
|
||||
Plugins fire on events around (cf. `src/plugins/callbacks.c`):
|
||||
- Pre/post message send, pre/post message receive.
|
||||
- Pre/post chat-message-display.
|
||||
- Pre/post connect, disconnect.
|
||||
- Pre/post quit.
|
||||
- Window-switch, room-join, room-leave.
|
||||
|
||||
The callback names follow `prof_pre_*` / `prof_post_*` in the plugin API
|
||||
surface; in cproof internals they are dispatched via
|
||||
`plugins_pre_chat_message_send()` and similar.
|
||||
|
||||
## API surface for plugins
|
||||
|
||||
C API (`c_api.h`) and Python API (`python_api.h`) are kept in sync — any
|
||||
new function in one should land in the other unless explicitly C-only or
|
||||
Python-only.
|
||||
|
||||
The Python wrapper is in `profapi.c/h` and is exposed as the `prof` module
|
||||
inside plugins.
|
||||
|
||||
## Adding a plugin hook point
|
||||
|
||||
1. Define dispatcher in `plugins.c/h`:
|
||||
`void plugins_<verb>_<event>(<args>);`
|
||||
2. Wire callback storage in `callbacks.c/h`.
|
||||
3. Expose to plugins via `c_api.c/h` and `python_api.c/h` (matching names).
|
||||
4. Call the dispatcher at the appropriate site (usually in `src/event/` or
|
||||
`src/xmpp/`).
|
||||
5. Document the hook in cproof's plugin docs (out of scope for this repo).
|
||||
|
||||
## Testing
|
||||
|
||||
`tests/unittests/test_plugins_disco.{c,h}` covers plugin-disco feature
|
||||
contribution; broader plugin-system testing is light. Stubs:
|
||||
`tests/unittests/plugins/stub_*.c`.
|
||||
|
||||
## Pitfalls
|
||||
|
||||
- Plugin callbacks may re-enter cproof via the C/Python API. Ensure state is
|
||||
consistent **before** firing a callback — see `patterns/events.md`.
|
||||
- The Python interpreter is global; plugins share state. Don't assume
|
||||
isolation.
|
||||
- Plugin loading is not sandboxed; treat plugins as trusted code.
|
||||
63
patterns/ui.md
Normal file
63
patterns/ui.md
Normal file
@@ -0,0 +1,63 @@
|
||||
# UI
|
||||
|
||||
`src/ui/` is built on ncurses (wide-char). It owns windows, the input line,
|
||||
status/title bars, and tray notifications.
|
||||
|
||||
## Window types
|
||||
|
||||
| File | Window | Notes |
|
||||
|---|---|---|
|
||||
| `chatwin.c` | One-on-one chat | One per peer JID. |
|
||||
| `mucwin.c` | MUC room | Tracks occupants list, subject, role. |
|
||||
| `privwin.c` | MUC private chat | Per-occupant private window inside a room. |
|
||||
| `console.c` | Console | Always present, index 1; system messages. |
|
||||
| `confwin.c` | Form / config | Used for ad-hoc commands and data forms. |
|
||||
| `xmlwin.c` | XML console | Raw stanza tracer. |
|
||||
| `vcardwin.c` | vCard view | Read-only viewer. |
|
||||
|
||||
`win_types.h` declares the window-type enum used to discriminate at runtime.
|
||||
`window.c/h` is the base type; `window_list.c/h` manages the open-windows
|
||||
collection.
|
||||
|
||||
## Buffers
|
||||
|
||||
`buffer.c/h` — append-only ring of rendered lines per window. UI rendering
|
||||
reads from the buffer; chatlog persists separately.
|
||||
|
||||
## Bars and chrome
|
||||
|
||||
| File | Role |
|
||||
|---|---|
|
||||
| `statusbar.c/h` | Bottom status line (window list, unread counts). |
|
||||
| `titlebar.c/h` | Top bar (current window title, encryption state). |
|
||||
| `tray.c/h` | System-tray notifications via libnotify. |
|
||||
| `screen.c/h` | Screen-level helpers (resize, refresh). |
|
||||
| `notifier.c` | Sound and visual notifications dispatch. |
|
||||
| `inputwin.c/h` | Input line: line editor, history, completion trigger. |
|
||||
| `core.c` | UI lifecycle (init / shutdown / refresh loop). |
|
||||
|
||||
## Roster panel
|
||||
|
||||
`rosterwin.c` and `occupantswin.c` render the side panels (roster and MUC
|
||||
occupants). They subscribe to roster / muc state changes via the event layer.
|
||||
|
||||
## Input → completion
|
||||
|
||||
`inputwin.c` calls into `cmd_ac.c` on Tab. See `patterns/autocomplete.md` for
|
||||
how that flow continues.
|
||||
|
||||
## Conventions
|
||||
|
||||
- Public UI calls used from outside `src/ui/`:
|
||||
- `cons_show("...")` — formatted line into the console.
|
||||
- `cons_bad_cmd_usage(cmd)` — print canonical "wrong usage" line.
|
||||
- `chatwin_*`, `mucwin_*`, `privwin_*` — write into a specific window type.
|
||||
- UI code does not block on XMPP. State queries go through getters in
|
||||
`src/xmpp/` and `src/config/`.
|
||||
- Color / theme lookups go through `src/config/color.c` and
|
||||
`src/config/theme.c` — do not hardcode ncurses attribute pairs.
|
||||
|
||||
## Testing
|
||||
|
||||
UI is heavily stubbed in unit tests — see `tests/unittests/ui/stub_ui.c` and
|
||||
sibling stubs. Real ncurses calls do not run under `make check`.
|
||||
95
patterns/xmpp.md
Normal file
95
patterns/xmpp.md
Normal file
@@ -0,0 +1,95 @@
|
||||
# XMPP layer
|
||||
|
||||
`src/xmpp/` wraps libstrophe and exposes higher-level operations to the
|
||||
command, event, and UI layers.
|
||||
|
||||
## Connection lifecycle
|
||||
|
||||
`src/xmpp/connection.c` owns the connection state machine. Public surface in
|
||||
`connection.h`:
|
||||
|
||||
- `jabber_conn_status_t connection_get_status(void);`
|
||||
- `jabber_conn_status_t connection_connect(const char* fulljid, const char* passwd, ...);`
|
||||
- `jabber_conn_status_t connection_register(...);`
|
||||
- `void connection_disconnect(void);`
|
||||
|
||||
`jabber_conn_status_t` values:
|
||||
|
||||
| Value | Meaning |
|
||||
|---|---|
|
||||
| `JABBER_DISCONNECTED` | Not connected. |
|
||||
| `JABBER_CONNECTING` | Connect in progress. |
|
||||
| `JABBER_CONNECTED` | Live session. |
|
||||
| `JABBER_DISCONNECTING` | Tearing down. |
|
||||
|
||||
**Most code paths gate on `JABBER_CONNECTED`.** A canonical command-handler
|
||||
guard:
|
||||
|
||||
```c
|
||||
if (connection_get_status() != JABBER_CONNECTED) {
|
||||
cons_show("You are not currently connected.");
|
||||
return TRUE;
|
||||
}
|
||||
```
|
||||
|
||||
In tests, `will_return(connection_get_status, JABBER_CONNECTED)` (or
|
||||
`JABBER_DISCONNECTED`) before the call. See `gotchas.md`.
|
||||
|
||||
## Stanza building
|
||||
|
||||
`src/xmpp/stanza.c` is the assembler. Helpers like
|
||||
`stanza_create_message(...)`, `stanza_create_presence(...)`, plus low-level
|
||||
`xmpp_stanza_*` from libstrophe.
|
||||
|
||||
Outgoing flow:
|
||||
- `cmd_funcs.c` or `client_events.c` → `xmpp/message.c` (or `presence.c`,
|
||||
`iq.c`) → `stanza.c` to build → libstrophe to send.
|
||||
|
||||
Incoming flow:
|
||||
- libstrophe handler → `xmpp/message.c` (or sibling) parses → `event/server_events.c`
|
||||
fans out → `ui/`, `chatlog.c`, plugin callbacks.
|
||||
|
||||
## JIDs
|
||||
|
||||
`src/xmpp/jid.h` defines `Jid` and the `auto_jid` cleanup macro. Use
|
||||
`auto_jid` rather than manual `jid_destroy()` whenever practical:
|
||||
|
||||
```c
|
||||
auto_jid Jid* parsed = jid_create(input);
|
||||
if (!parsed || !parsed->barejid) {
|
||||
return FALSE;
|
||||
}
|
||||
```
|
||||
|
||||
`Jid` exposes `barejid`, `fulljid`, `localpart`, `domainpart`, `resourcepart`.
|
||||
|
||||
## Sessions, MUC, presence, roster
|
||||
|
||||
| File | Responsibility |
|
||||
|---|---|
|
||||
| `session.c` | Logical XMPP session: account, fulljid, presence, settings. |
|
||||
| `muc.c` | Multi-user chat rooms (joins, occupants, configuration). |
|
||||
| `presence.c` | Presence stanzas, subscription requests. |
|
||||
| `roster.c` / `roster_list.c` | Roster fetch, push, contact lookup. |
|
||||
| `chat_session.c` | Per-conversation state (carbons, OTR channel, etc.). |
|
||||
| `chat_state.c` | Composing / paused / inactive notifications (XEP-0085). |
|
||||
| `bookmark.c` | Bookmarked rooms (XEP-0048). |
|
||||
| `blocking.c` | Block list (XEP-0191). |
|
||||
| `vcard.c` / `vcard_funcs.h` | vCard fetch / publish. |
|
||||
| `avatar.c` | Avatar publish / fetch (XEP-0084). |
|
||||
| `iq.c` | Generic IQ handling and pending-IQ map. |
|
||||
| `capabilities.c` | Entity capabilities cache (XEP-0115). |
|
||||
|
||||
## Encryption bridges
|
||||
|
||||
`src/xmpp/omemo.c` and `src/xmpp/ox.c` are *bridges* into the encryption
|
||||
modules — they live alongside same-named files in `src/omemo/` and
|
||||
`src/pgp/`. Be precise about which path you mean.
|
||||
|
||||
## Testing
|
||||
|
||||
When unit-testing code that calls into `xmpp/`, expect to:
|
||||
|
||||
- Mock `connection_get_status` with `will_return(...)`.
|
||||
- Stub session getters (`session_get_account_name`) and roster lookups.
|
||||
- Use stubs in `tests/unittests/xmpp/stub_*.c`. See `testing/stubs.md`.
|
||||
119
playbooks/add-autocomplete.md
Normal file
119
playbooks/add-autocomplete.md
Normal file
@@ -0,0 +1,119 @@
|
||||
# Playbook: add autocomplete to a command
|
||||
|
||||
Pre-req: command already exists in `cmd_defs.c`. If not, see
|
||||
`playbooks/add-command.md`.
|
||||
|
||||
## Choose the flavour
|
||||
|
||||
| Token set | Use |
|
||||
|---|---|
|
||||
| Static, known at startup. | `autocomplete_param_with_ac` + a static `Autocomplete`. |
|
||||
| Dynamic — depends on roster, accounts, DB query, etc. | `autocomplete_param_with_func` + a stateless callback. |
|
||||
|
||||
## Static token set
|
||||
|
||||
`src/command/cmd_ac.c`:
|
||||
|
||||
```c
|
||||
// 1. Declare the AC at file scope:
|
||||
static Autocomplete foo_ac;
|
||||
|
||||
// 2. Add it to the static array near the top of the file (the free-list
|
||||
// that cmd_ac_uninit walks at shutdown):
|
||||
static Autocomplete* all_acs[] = {
|
||||
// ... existing entries ...
|
||||
&foo_ac,
|
||||
};
|
||||
|
||||
// 3. Initialise in cmd_ac_init():
|
||||
foo_ac = autocomplete_new();
|
||||
autocomplete_add(foo_ac, "alpha");
|
||||
autocomplete_add(foo_ac, "beta");
|
||||
|
||||
// 4. Implement the dispatcher:
|
||||
static char*
|
||||
_foo_autocomplete(ProfWin* window, const char* const input, gboolean previous)
|
||||
{
|
||||
return autocomplete_param_with_ac(input, "/foo", foo_ac, TRUE, previous);
|
||||
}
|
||||
|
||||
// 5. Register:
|
||||
g_hash_table_insert(ac_funcs, "/foo", _foo_autocomplete);
|
||||
```
|
||||
|
||||
## Dynamic — function callback
|
||||
|
||||
If suggestions come from runtime state (roster, accounts, providers list):
|
||||
|
||||
```c
|
||||
// In a domain module (e.g. src/foo/foo.c), implement a stateless callback:
|
||||
char*
|
||||
foo_suggestions(const char* const search_str, gboolean previous, void* context)
|
||||
{
|
||||
// Compute and return a freshly-allocated char* (caller frees), or NULL.
|
||||
// No module-level "_last_match" globals — keep state on the stack /
|
||||
// derive deterministically from search_str and previous.
|
||||
}
|
||||
```
|
||||
|
||||
Wire into `cmd_ac.c`:
|
||||
|
||||
```c
|
||||
static char*
|
||||
_foo_autocomplete(ProfWin* window, const char* const input, gboolean previous)
|
||||
{
|
||||
return autocomplete_param_with_func(input, "/foo", foo_suggestions, previous, NULL);
|
||||
}
|
||||
|
||||
g_hash_table_insert(ac_funcs, "/foo", _foo_autocomplete);
|
||||
```
|
||||
|
||||
A clean canonical example is `roster_contact_autocomplete` in
|
||||
`src/xmpp/roster_list.c` — it delegates to `autocomplete_complete` against a
|
||||
roster-owned `Autocomplete` and keeps no callback-local state. **Do not
|
||||
copy older callbacks that keep state in a file-static `_last_match`
|
||||
variable** — those have known issues with shift-tab cycling and concurrent
|
||||
completers.
|
||||
|
||||
## Subcommand autocompletion
|
||||
|
||||
If `/foo` has subcommands, branch inside `_foo_autocomplete`:
|
||||
|
||||
```c
|
||||
static char*
|
||||
_foo_autocomplete(ProfWin* window, const char* const input, gboolean previous)
|
||||
{
|
||||
char* result = NULL;
|
||||
|
||||
// First the subcommand list itself:
|
||||
result = autocomplete_param_with_ac(input, "/foo", foo_subcommands_ac, TRUE, previous);
|
||||
if (result) return result;
|
||||
|
||||
// Then per-subcommand argument completion:
|
||||
if (g_str_has_prefix(input, "/foo set ")) {
|
||||
result = autocomplete_param_with_func(input, "/foo set", foo_set_options, previous, NULL);
|
||||
if (result) return result;
|
||||
}
|
||||
|
||||
return NULL;
|
||||
}
|
||||
```
|
||||
|
||||
`/roster` (`src/command/cmd_ac.c`, search for `_roster_autocomplete`) is a
|
||||
current example of this layered subcommand layout.
|
||||
|
||||
## Unit-test the callback
|
||||
|
||||
Stateless callbacks are easy to unit-test directly:
|
||||
|
||||
```c
|
||||
void
|
||||
test_foo_suggestions_returns_first_match(void** state)
|
||||
{
|
||||
char* r = foo_suggestions("al", FALSE, NULL);
|
||||
assert_string_equal(r, "alpha");
|
||||
free(r);
|
||||
}
|
||||
```
|
||||
|
||||
Add the test pair to `tests/unittests/` (see `playbooks/add-test.md`).
|
||||
161
playbooks/add-command.md
Normal file
161
playbooks/add-command.md
Normal file
@@ -0,0 +1,161 @@
|
||||
# Playbook: add a `/command`
|
||||
|
||||
End-to-end recipe for adding a new user-facing command. Example: `/foo`.
|
||||
|
||||
## 1. Define the handler
|
||||
|
||||
`src/command/cmd_funcs.h`:
|
||||
|
||||
```c
|
||||
gboolean cmd_foo(ProfWin* window, const char* const command, gchar** args);
|
||||
```
|
||||
|
||||
`src/command/cmd_funcs.c`:
|
||||
|
||||
```c
|
||||
gboolean
|
||||
cmd_foo(ProfWin* window, const char* const command, gchar** args)
|
||||
{
|
||||
if (connection_get_status() != JABBER_CONNECTED) {
|
||||
cons_show("You are not currently connected.");
|
||||
return TRUE;
|
||||
}
|
||||
if (!args[0]) {
|
||||
cons_bad_cmd_usage(command);
|
||||
return TRUE;
|
||||
}
|
||||
// ... do the thing ...
|
||||
return TRUE;
|
||||
}
|
||||
```
|
||||
|
||||
Keep the handler thin: validate, then delegate to a domain module.
|
||||
|
||||
## 2. Register the command
|
||||
|
||||
`src/command/cmd_defs.c` — add an entry to the static `Command` array:
|
||||
|
||||
```c
|
||||
{ CMD_PREAMBLE("/foo",
|
||||
parse_args, 0, 1, NULL)
|
||||
CMD_MAINFUNC(cmd_foo)
|
||||
CMD_TAGS(CMD_TAG_CHAT)
|
||||
CMD_SYN("/foo [<arg>]")
|
||||
CMD_DESC("Do the foo thing.")
|
||||
CMD_ARGS(
|
||||
{ "<arg>", "Optional argument to pass." })
|
||||
CMD_EXAMPLES("/foo bar")
|
||||
}
|
||||
```
|
||||
|
||||
For a command with subcommands, swap `CMD_MAINFUNC(cmd_foo)` for
|
||||
`CMD_SUBFUNCS({"sub", cmd_foo_sub}, ...)`.
|
||||
|
||||
## 3. Autocompletion (optional)
|
||||
|
||||
`src/command/cmd_ac.c`:
|
||||
|
||||
a. (If a static token list) declare and initialise an `Autocomplete`:
|
||||
|
||||
```c
|
||||
static Autocomplete foo_ac;
|
||||
|
||||
// in cmd_ac_init():
|
||||
foo_ac = autocomplete_new();
|
||||
autocomplete_add(foo_ac, "bar");
|
||||
autocomplete_add(foo_ac, "baz");
|
||||
|
||||
// add &foo_ac to the static free-list near top of file
|
||||
```
|
||||
|
||||
b. Implement the per-command dispatcher:
|
||||
|
||||
```c
|
||||
static char*
|
||||
_foo_autocomplete(ProfWin* window, const char* const input, gboolean previous)
|
||||
{
|
||||
return autocomplete_param_with_ac(input, "/foo", foo_ac, TRUE, previous);
|
||||
}
|
||||
```
|
||||
|
||||
c. Register it:
|
||||
|
||||
```c
|
||||
g_hash_table_insert(ac_funcs, "/foo", _foo_autocomplete);
|
||||
```
|
||||
|
||||
For dynamic suggestions, use `autocomplete_param_with_func` and a stateless
|
||||
callback. See `patterns/autocomplete.md`.
|
||||
|
||||
## 4. Unit test
|
||||
|
||||
Create `tests/unittests/test_cmd_foo.c` and `test_cmd_foo.h`:
|
||||
|
||||
`test_cmd_foo.h`:
|
||||
|
||||
```c
|
||||
void test_cmd_foo_when_disconnected_shows_message(void** state);
|
||||
void test_cmd_foo_when_no_arg_shows_usage(void** state);
|
||||
void test_cmd_foo_happy_path(void** state);
|
||||
```
|
||||
|
||||
`test_cmd_foo.c`:
|
||||
|
||||
```c
|
||||
#include "config.h"
|
||||
#include "prof_cmocka.h"
|
||||
#include "test_cmd_foo.h"
|
||||
// ... includes for stubs and the unit ...
|
||||
|
||||
void
|
||||
test_cmd_foo_when_disconnected_shows_message(void** state)
|
||||
{
|
||||
will_return(connection_get_status, JABBER_DISCONNECTED);
|
||||
expect_string(cons_show, msg, "You are not currently connected.");
|
||||
gchar* args[] = { NULL };
|
||||
assert_true(cmd_foo(NULL, "/foo", args));
|
||||
}
|
||||
```
|
||||
|
||||
## 5. Register the test
|
||||
|
||||
`tests/unittests/unittests.c`:
|
||||
|
||||
```c
|
||||
#include "test_cmd_foo.h"
|
||||
|
||||
// ... inside the tests[] array ...
|
||||
cmocka_unit_test(test_cmd_foo_when_disconnected_shows_message),
|
||||
cmocka_unit_test(test_cmd_foo_when_no_arg_shows_usage),
|
||||
cmocka_unit_test(test_cmd_foo_happy_path),
|
||||
```
|
||||
|
||||
## 6. Stubs
|
||||
|
||||
If `cmd_foo` calls a function that is not yet stubbed, add the stub. See
|
||||
`testing/stubs.md`.
|
||||
|
||||
## 7. Wire into Make
|
||||
|
||||
- `cmd_defs.c`, `cmd_funcs.c`, `cmd_ac.c` are already in the build.
|
||||
- New test file: add `tests/unittests/test_cmd_foo.c` to the unittests
|
||||
sources in `tests/unittests/Makefile.am` (or whatever wires it).
|
||||
- New stub file (if any): add to the same Makefile.
|
||||
|
||||
## 8. Build & check
|
||||
|
||||
Inside Docker (`build/docker.md`):
|
||||
|
||||
```sh
|
||||
./autogen.sh && ./configure && make -j$(nproc) && make check
|
||||
```
|
||||
|
||||
## 9. Help text
|
||||
|
||||
`/help foo` should now produce the synopsis / description / args from the
|
||||
`Command` entry. No separate help file to update.
|
||||
|
||||
## 10. Commit
|
||||
|
||||
Single commit, conventional-commit style, English. No AI-attribution
|
||||
trailer.
|
||||
103
playbooks/add-encryption.md
Normal file
103
playbooks/add-encryption.md
Normal file
@@ -0,0 +1,103 @@
|
||||
# Playbook: touch encryption modules safely
|
||||
|
||||
Encryption stacks (OMEMO, OTR, PGP/OX) are independent and each is gated
|
||||
behind a `configure` option. Edits must respect three things: the gating
|
||||
macro, the bridge file split, and the matching unit-test stubs.
|
||||
|
||||
See `patterns/encryption.md` for the architectural split.
|
||||
|
||||
## 1. Identify the bridge
|
||||
|
||||
| Stack | Module dir | Bridge file (XMPP side) |
|
||||
|---|---|---|
|
||||
| OMEMO | `src/omemo/` | `src/xmpp/omemo.c` |
|
||||
| OTR | `src/otr/` | (none — in-stream on `src/xmpp/message.c`) |
|
||||
| PGP / OX | `src/pgp/` | `src/xmpp/ox.c` |
|
||||
|
||||
Outbound calls (cproof → encryption) usually live in the bridge file.
|
||||
Inbound (decrypted message → cproof) usually surfaces in the bridge or
|
||||
directly in `src/xmpp/message.c`.
|
||||
|
||||
## 2. Identify the gating macro
|
||||
|
||||
```sh
|
||||
grep -n "HAVE_OMEMO\|HAVE_LIBOTR\|HAVE_LIBGPGME" src/xmpp/<bridge>.c
|
||||
```
|
||||
|
||||
Every call into the encryption module is wrapped:
|
||||
|
||||
```c
|
||||
#ifdef HAVE_OMEMO
|
||||
omemo_encrypt_message(jid, plaintext, &ciphertext);
|
||||
#else
|
||||
// graceful fallback or skip
|
||||
#endif
|
||||
```
|
||||
|
||||
When you add a new call, copy the existing guard. Do **not** drop the
|
||||
fallback — the build must succeed with the feature disabled.
|
||||
|
||||
## 3. Update the public surface (header)
|
||||
|
||||
If you add or change an exported function in `src/omemo/omemo.h` /
|
||||
`src/otr/otr.h` / `src/pgp/gpg.h` / `src/pgp/ox.h`, update **two** places:
|
||||
|
||||
1. The header itself.
|
||||
2. The corresponding stub: `tests/unittests/{omemo,otr,pgp}/stub_*.c`.
|
||||
|
||||
Keep the stub signature byte-for-byte identical to the header.
|
||||
|
||||
## 4. Stubs
|
||||
|
||||
Stubs exist regardless of whether a stack is enabled at runtime — they
|
||||
satisfy the unit-test linker. Three flavours, see `testing/stubs.md`.
|
||||
|
||||
If a unit test needs to drive the new function, switch the stub to `mock()`
|
||||
and queue with `will_return`. If a unit test needs to assert arguments,
|
||||
switch to `check_expected` and queue with `expect_string` / `expect_value`.
|
||||
|
||||
## 5. `test_forced_encryption`
|
||||
|
||||
`tests/unittests/test_forced_encryption.{c,h}` exercises the policy that
|
||||
refuses plaintext sends when a session is encryption-locked. **Run it after
|
||||
any encryption-policy change.**
|
||||
|
||||
## 6. Encryption-aware command tests
|
||||
|
||||
| Test file | Covers |
|
||||
|---|---|
|
||||
| `test_cmd_otr.c` / `.h` | `/otr` command surface. |
|
||||
| `test_cmd_pgp.c` / `.h` | `/pgp` command surface. |
|
||||
|
||||
(No `test_cmd_omemo.c` exists at the time of writing — confirm with `ls
|
||||
tests/unittests/test_cmd_*.c` before assuming.)
|
||||
|
||||
## 7. Build matrix
|
||||
|
||||
After changes, build with each encryption flag in turn:
|
||||
|
||||
```sh
|
||||
./configure --disable-omemo
|
||||
make check
|
||||
|
||||
./configure --disable-otr
|
||||
make check
|
||||
|
||||
./configure --disable-pgp
|
||||
make check
|
||||
```
|
||||
|
||||
(Inside Docker — see `build/docker.md`.)
|
||||
|
||||
CI does not necessarily run every disable combination; verify locally if
|
||||
your change affects gating logic.
|
||||
|
||||
## Conventions
|
||||
|
||||
- Never strip the `#ifdef HAVE_*` guard "for clarity". The disabled-build
|
||||
must still compile.
|
||||
- Encryption keys / fingerprints persist via the module's own store (`src/
|
||||
omemo/store.c`, libotr's keystore, gpgme's keyring). Do not invent
|
||||
parallel storage.
|
||||
- Outbound encryption sits between message build and stanza send — see
|
||||
`patterns/xmpp.md` for the flow.
|
||||
109
playbooks/add-event-handler.md
Normal file
109
playbooks/add-event-handler.md
Normal file
@@ -0,0 +1,109 @@
|
||||
# Playbook: add an event handler
|
||||
|
||||
`src/event/` mediates between `src/xmpp/` (and locally-triggered actions)
|
||||
and the rest of the codebase. Two directions:
|
||||
|
||||
- **Server events** (`sv_ev_*`) — triggered by inbound stanzas.
|
||||
- **Client events** (`cl_ev_*`) — triggered by local actions (UI, command).
|
||||
|
||||
See `patterns/events.md` for the conceptual split.
|
||||
|
||||
## Decide direction
|
||||
|
||||
| Trigger | Direction | File |
|
||||
|---|---|---|
|
||||
| Inbound stanza parsed in `src/xmpp/`. | Server. | `src/event/server_events.c/h` |
|
||||
| User typed a command, UI key, or internal timer. | Client. | `src/event/client_events.c/h` |
|
||||
|
||||
## Add the handler — server example
|
||||
|
||||
`src/event/server_events.h`:
|
||||
|
||||
```c
|
||||
void sv_ev_foo_received(const char* const from, const char* const payload);
|
||||
```
|
||||
|
||||
`src/event/server_events.c`:
|
||||
|
||||
```c
|
||||
void
|
||||
sv_ev_foo_received(const char* const from, const char* const payload)
|
||||
{
|
||||
// 1. Update persistent state.
|
||||
chatlog_msg_in(from, payload);
|
||||
|
||||
// 2. Push UI update.
|
||||
ProfChatWin* win = wins_get_chat(from);
|
||||
if (win) {
|
||||
chatwin_incoming_msg(win, payload, NULL, FALSE);
|
||||
} else {
|
||||
cons_show_incoming_message(from, payload);
|
||||
}
|
||||
|
||||
// 3. Plugin callbacks last — state must be consistent.
|
||||
plugins_post_chat_message_received(from, payload);
|
||||
}
|
||||
```
|
||||
|
||||
## Wire the trigger site
|
||||
|
||||
For a server event, the matching parser in `src/xmpp/` calls the new
|
||||
handler. For instance, if `<foo>` stanzas are parsed in
|
||||
`src/xmpp/message.c`:
|
||||
|
||||
```c
|
||||
// ... after parsing the stanza ...
|
||||
sv_ev_foo_received(from, payload);
|
||||
```
|
||||
|
||||
For a client event, call from `cmd_funcs.c` (or wherever the user-facing
|
||||
trigger lives).
|
||||
|
||||
## Stub the handler in tests
|
||||
|
||||
`tests/unittests/event/stub_*.c` — add a stub for the new function so units
|
||||
that exercise the trigger site link cleanly:
|
||||
|
||||
```c
|
||||
void
|
||||
sv_ev_foo_received(const char* const from, const char* const payload)
|
||||
{
|
||||
// pass-through (or check_expected, depending on test needs)
|
||||
}
|
||||
```
|
||||
|
||||
If a test needs to verify the handler is called with specific args, switch
|
||||
the stub to `check_expected` and the test queues `expect_string` / `expect_value`.
|
||||
|
||||
See `testing/stubs.md`.
|
||||
|
||||
## Plugin hook (if applicable)
|
||||
|
||||
If the event should fire a plugin callback, add a dispatcher in
|
||||
`src/plugins/plugins.c/h` (e.g. `plugins_post_foo_received`) and call it
|
||||
**last** in the handler — after state and UI are consistent. Keep C and
|
||||
Python plugin APIs in sync (`c_api.h` / `python_api.h`).
|
||||
|
||||
## Unit test the handler
|
||||
|
||||
`tests/unittests/test_server_events.c` (or `test_client_events.c`) holds
|
||||
event-side tests. Pattern:
|
||||
|
||||
```c
|
||||
void
|
||||
test_sv_ev_foo_received_writes_chatlog(void** state)
|
||||
{
|
||||
expect_string(chatlog_msg_in, jid, "alice@example.com");
|
||||
expect_string(chatlog_msg_in, msg, "hello");
|
||||
sv_ev_foo_received("alice@example.com", "hello");
|
||||
}
|
||||
```
|
||||
|
||||
## Conventions
|
||||
|
||||
- One handler, one event. Don't pile multiple unrelated events into the same
|
||||
function.
|
||||
- Order inside the handler: state → UI → plugins. Never call plugins before
|
||||
state is committed.
|
||||
- `sv_ev_*` and `cl_ev_*` should be straight-line — no XMPP protocol parsing,
|
||||
no UI rendering. They orchestrate; they don't implement.
|
||||
97
playbooks/add-test.md
Normal file
97
playbooks/add-test.md
Normal file
@@ -0,0 +1,97 @@
|
||||
# Playbook: add a unit test file
|
||||
|
||||
Recipe for a new `tests/unittests/test_<topic>.c` + `.h` pair.
|
||||
|
||||
## 1. Create the header
|
||||
|
||||
`tests/unittests/test_<topic>.h`:
|
||||
|
||||
```c
|
||||
#ifndef TEST_<TOPIC>_H
|
||||
#define TEST_<TOPIC>_H
|
||||
|
||||
void test_<topic>_<scenario_1>(void** state);
|
||||
void test_<topic>_<scenario_2>(void** state);
|
||||
|
||||
#endif
|
||||
```
|
||||
|
||||
One declaration per test function. Headers exist solely so `unittests.c` can
|
||||
include them and reference each function symbol.
|
||||
|
||||
## 2. Create the source
|
||||
|
||||
`tests/unittests/test_<topic>.c`:
|
||||
|
||||
```c
|
||||
#include "config.h"
|
||||
#include "prof_cmocka.h"
|
||||
|
||||
#include <stdarg.h>
|
||||
#include <stddef.h>
|
||||
#include <setjmp.h>
|
||||
#include <cmocka.h>
|
||||
|
||||
#include "test_<topic>.h"
|
||||
// ... includes for the unit under test and any types it needs ...
|
||||
|
||||
void
|
||||
test_<topic>_<scenario_1>(void** state)
|
||||
{
|
||||
// Arrange — `will_return`, `expect_string`, etc.
|
||||
// Act — call the unit
|
||||
// Assert — `assert_*`
|
||||
}
|
||||
```
|
||||
|
||||
## 3. Register in `unittests.c`
|
||||
|
||||
```c
|
||||
#include "test_<topic>.h"
|
||||
|
||||
// inside the tests[] array passed to cmocka_run_group_tests:
|
||||
cmocka_unit_test(test_<topic>_<scenario_1>),
|
||||
cmocka_unit_test(test_<topic>_<scenario_2>),
|
||||
```
|
||||
|
||||
If multiple tests share fixture, add a setup / teardown pair and use
|
||||
`cmocka_unit_test_setup_teardown` (or define a per-topic macro near the top
|
||||
of `unittests.c`, like `muc_unit_test`).
|
||||
|
||||
## 4. Stubs
|
||||
|
||||
For each external function the unit calls, ensure a stub exists in the
|
||||
matching `tests/unittests/<module>/stub_*.c`. Three flavours:
|
||||
|
||||
- **Pass-through** — no observation needed.
|
||||
- **`mock()` / `will_return`** — test injects return values.
|
||||
- **`check_expected()` / `expect_*`** — test asserts arguments.
|
||||
|
||||
See `testing/stubs.md`.
|
||||
|
||||
## 5. Wire into Make
|
||||
|
||||
`tests/unittests/Makefile.am` (or the active wiring file): add
|
||||
`test_<topic>.c` to the `unittests_SOURCES` (or equivalent) list. Same for
|
||||
any new stub file.
|
||||
|
||||
## 6. Build & run
|
||||
|
||||
Inside Docker:
|
||||
|
||||
```sh
|
||||
./autogen.sh && ./configure && make -j$(nproc) check
|
||||
```
|
||||
|
||||
Diagnose failures via `tests/unittests/unittests.log` and the cmocka stderr
|
||||
output.
|
||||
|
||||
## 7. Conventions
|
||||
|
||||
- Test functions: `test_<topic>_<scenario>` — never `test_topic1`,
|
||||
`test_topic2`. Names should describe the scenario.
|
||||
- One assertion focus per test (multiple `assert_*` calls are fine; multiple
|
||||
*unrelated* assertions are not).
|
||||
- Don't reuse stubs across topic suites unless the call truly is uniform —
|
||||
diverging behaviour is a strong sign you want a dedicated stub.
|
||||
- No I/O in unit tests. Filesystem, network, ncurses are all stubbed.
|
||||
49
testing/bench.md
Normal file
49
testing/bench.md
Normal file
@@ -0,0 +1,49 @@
|
||||
# Benchmarks
|
||||
|
||||
`tests/bench/` holds performance benches for storage / parser hot paths. Each
|
||||
runner is a separate binary built into its own subdirectory.
|
||||
|
||||
## Runners
|
||||
|
||||
| Runner | Path | Measures |
|
||||
|---|---|---|
|
||||
| `bench_runner` | `tests/bench/bench_runner/` | General-purpose runner; orchestrates other benches. |
|
||||
| `bench_export_import` | `tests/bench/bench_export_import/` | Database export/import throughput (SQLite + flatfile). |
|
||||
| `bench_failure_modes` | `tests/bench/bench_failure_modes/` | Flatfile parser error/recovery paths. |
|
||||
| `bench_long_messages` | `tests/bench/bench_long_messages/` | Flatfile parser on very long messages. |
|
||||
| `gen_history` | `tests/bench/gen_history/` | Generates synthetic chat history fixtures used by other benches. |
|
||||
|
||||
Each runner pulls in a small set of `src/*.c` files (`common.c`,
|
||||
`database.c`, `database_flatfile*.c`, etc.) — see the per-runner Makefile or
|
||||
top-level `Makefile.am` for exact dependencies.
|
||||
|
||||
## Stubs
|
||||
|
||||
Benches use their own minimal stubs (typically `bench_stubs.c`,
|
||||
`bench_common.c`, `bench_csv.c` per runner) — separate from
|
||||
`tests/unittests/` stubs to keep the bench builds small.
|
||||
|
||||
## Running
|
||||
|
||||
Benches are not part of `make check`. Build and invoke each runner directly
|
||||
once configured:
|
||||
|
||||
```sh
|
||||
make tests/bench/bench_runner/bench_runner
|
||||
./tests/bench/bench_runner/bench_runner [args]
|
||||
```
|
||||
|
||||
(Argument shape per-runner; check `--help`.)
|
||||
|
||||
## Output
|
||||
|
||||
Runners typically emit CSV (`bench_csv.c`) for downstream tooling. Treat
|
||||
output as machine-readable; do not parse the human header lines.
|
||||
|
||||
## When to add a bench
|
||||
|
||||
- A storage / parser hot path is sensitive to regression.
|
||||
- A perf claim in a PR needs to be reproducible.
|
||||
|
||||
If it's just a one-off measurement, run it locally and don't commit. The
|
||||
bench runners are for sustained, comparable measurement.
|
||||
62
testing/functional-tests.md
Normal file
62
testing/functional-tests.md
Normal file
@@ -0,0 +1,62 @@
|
||||
# Functional tests
|
||||
|
||||
`tests/functionaltests/` exercises a real cproof binary under a PTY against a
|
||||
real (test) XMPP server.
|
||||
|
||||
## Files
|
||||
|
||||
| File | Role |
|
||||
|---|---|
|
||||
| `functionaltests.c` | Runner (entry point). |
|
||||
| `proftest.c/h` | Fixture: spawn cproof under PTY, drive XMPP server, assertions. |
|
||||
| `test_<feature>.c` | Per-feature suites. |
|
||||
|
||||
Existing suites (grep for `test_*.c`):
|
||||
|
||||
- Connection / disconnection: `test_connect.c`, `test_disconnect.c`,
|
||||
`test_autoping.c`, `test_ping.c`.
|
||||
- Messaging: `test_message.c`, `test_chat_session.c`, `test_carbons.c`,
|
||||
`test_receipts.c`, `test_history.c`.
|
||||
- Presence and roster: `test_presence.c`, `test_roster.c`,
|
||||
`test_lastactivity.c`.
|
||||
- Discovery: `test_disco.c`, `test_software.c`.
|
||||
- MUC: `test_muc.c`, `test_rooms.c`.
|
||||
- Storage: `test_export_import.c`.
|
||||
|
||||
## Prerequisites
|
||||
|
||||
- A test XMPP server reachable from the harness (typically a local Prosody
|
||||
instance configured for the tests).
|
||||
- A pre-baked `.profrc` (recent perf work pre-bakes this for speed — see
|
||||
cproof commit `f84ed1bf6` for context).
|
||||
- PTY availability (`/dev/ptmx` on Linux).
|
||||
|
||||
## Running
|
||||
|
||||
Functional tests are not part of `make check` (which runs unit tests only).
|
||||
They are built and invoked via a separate target in `tests/functionaltests/`
|
||||
— inspect `tests/functionaltests/Makefile.am` for the current target name and
|
||||
arguments.
|
||||
|
||||
Recent perf work (commit `f84ed1bf6`) added:
|
||||
- Profrc pre-baking — fixture writes a known-good `.profrc` once, not per-test.
|
||||
- pty-close shutdown — fixture closes the PTY to terminate cproof faster than
|
||||
signalling.
|
||||
- Parallel port pools — separate XMPP servers / ports per parallel suite.
|
||||
|
||||
## When to add functional vs unit
|
||||
|
||||
| Test what | Where |
|
||||
|---|---|
|
||||
| Pure logic, internal API, given a state. | Unit. |
|
||||
| Stanza serialization / parsing round-trip with real libstrophe. | Functional. |
|
||||
| Connection state machine across multiple network events. | Functional. |
|
||||
| UI rendering (visible output). | **Neither** (UI stubbed in unit; not asserted in functional). |
|
||||
|
||||
## Failure-mode notes
|
||||
|
||||
- A leaked / orphan cproof process between suites poisons later runs. The
|
||||
fixture aggressively kills children — but if you abort a test by hand,
|
||||
`pkill -f tests/functionaltests` first.
|
||||
- Port-pool exhaustion under parallelism: bump the pool size, do not lower
|
||||
`-jN`.
|
||||
113
testing/stubs.md
Normal file
113
testing/stubs.md
Normal file
@@ -0,0 +1,113 @@
|
||||
# Stubs
|
||||
|
||||
Unit tests link against the unit-under-test plus a parallel set of "stub"
|
||||
implementations of every dependency. Stubs are organised by module under
|
||||
`tests/unittests/<module>/`.
|
||||
|
||||
## Layout
|
||||
|
||||
| Stub directory | Replaces |
|
||||
|---|---|
|
||||
| `tests/unittests/chatlog/` | `src/chatlog.c` |
|
||||
| `tests/unittests/command/` | parts of `src/command/` |
|
||||
| `tests/unittests/config/` | `src/config/` (accounts, cafile, ...) |
|
||||
| `tests/unittests/database/` | `src/database.c` |
|
||||
| `tests/unittests/event/` | `src/event/` |
|
||||
| `tests/unittests/log/` | `src/log.c` |
|
||||
| `tests/unittests/omemo/` | `src/omemo/` |
|
||||
| `tests/unittests/otr/` | `src/otr/` |
|
||||
| `tests/unittests/pgp/` | `src/pgp/` |
|
||||
| `tests/unittests/plugins/` | `src/plugins/` |
|
||||
| `tests/unittests/tools/` | `src/tools/` |
|
||||
| `tests/unittests/ui/` | `src/ui/` |
|
||||
| `tests/unittests/xmpp/` | `src/xmpp/` |
|
||||
| `tests/unittests/unittests/` | top-level utility stubs |
|
||||
|
||||
Files are named `stub_<module>.c` (sometimes split, e.g.
|
||||
`tests/unittests/config/stub_accounts.c`, `stub_cafile.c`).
|
||||
|
||||
## Two stub flavours
|
||||
|
||||
### Pass-through default
|
||||
|
||||
Returns a benign default (NULL, FALSE, 0). Used when the test does not care
|
||||
about the call.
|
||||
|
||||
```c
|
||||
void
|
||||
chatlog_msg_in(...)
|
||||
{
|
||||
// no-op
|
||||
}
|
||||
```
|
||||
|
||||
### `will_return`-driven
|
||||
|
||||
The stub pops a value queued by the test:
|
||||
|
||||
```c
|
||||
jabber_conn_status_t
|
||||
connection_get_status(void)
|
||||
{
|
||||
return (jabber_conn_status_t)mock();
|
||||
}
|
||||
```
|
||||
|
||||
Test side:
|
||||
|
||||
```c
|
||||
will_return(connection_get_status, JABBER_CONNECTED);
|
||||
```
|
||||
|
||||
If the stub is `will_return`-driven, **every test** that exercises a code
|
||||
path through it must queue a value, or cmocka aborts.
|
||||
|
||||
### `expect_*`-driven
|
||||
|
||||
The stub asserts argument values against expectations:
|
||||
|
||||
```c
|
||||
void
|
||||
cons_bad_cmd_usage(const char* const cmd)
|
||||
{
|
||||
check_expected(cmd);
|
||||
}
|
||||
```
|
||||
|
||||
Test side:
|
||||
|
||||
```c
|
||||
expect_string(cons_bad_cmd_usage, cmd, CMD_ACCOUNT);
|
||||
```
|
||||
|
||||
## When to add a stub
|
||||
|
||||
1. You call a new function from production code, and the call site is
|
||||
reachable from a unit test.
|
||||
2. `make check` link fails with an undefined-symbol error.
|
||||
|
||||
Procedure:
|
||||
|
||||
- Locate the matching stub module by callee path: `src/foo/bar.c` →
|
||||
`tests/unittests/foo/stub_bar.c`. Create the stub file if absent.
|
||||
- Stub the new function with the right flavour:
|
||||
- Pass-through if tests don't need to observe / drive it.
|
||||
- `mock()` if a test needs to inject a return value.
|
||||
- `check_expected()` if a test needs to assert an argument.
|
||||
- Wire the new stub `.c` into the build. See `tests/unittests/Makefile.am`
|
||||
(or whatever wires the stubs into the `unittests` target).
|
||||
|
||||
## When to extend an existing stub
|
||||
|
||||
If a stub already pass-through and a test now needs to drive it, **change the
|
||||
stub to `mock()`**. Then every test (existing and new) reaching that stub
|
||||
must `will_return` — be ready to update unrelated tests.
|
||||
|
||||
## Convention notes
|
||||
|
||||
- Stubs do **not** include real headers from `src/<module>/` if doing so
|
||||
would pull in the very symbol they replace. Forward-declare instead.
|
||||
- `tests/unittests/ui/stub_ai.c` is a UI-side stub for the AI client surface
|
||||
consumed by `src/ui/`.
|
||||
- The `tests/unittests/unittests/` directory holds top-level catch-alls that
|
||||
do not fit a single module.
|
||||
96
testing/unit-tests.md
Normal file
96
testing/unit-tests.md
Normal file
@@ -0,0 +1,96 @@
|
||||
# Unit tests
|
||||
|
||||
Framework: cmocka, accessed via `tests/prof_cmocka.h` (a thin compatibility
|
||||
wrapper). Run via `make check`.
|
||||
|
||||
## File layout
|
||||
|
||||
- One test file per topic: `tests/unittests/test_<topic>.c` + matching
|
||||
`test_<topic>.h`.
|
||||
- Header declares each test function (one per `cmocka_unit_test*`).
|
||||
- Runner: `tests/unittests/unittests.c` — `#include`s every header, calls
|
||||
`cmocka_run_group_tests()` over a single big array.
|
||||
|
||||
## Registration
|
||||
|
||||
Inside `unittests.c`, every test name appears in the array passed to
|
||||
`cmocka_run_group_tests`:
|
||||
|
||||
```c
|
||||
const struct CMUnitTest tests[] = {
|
||||
cmocka_unit_test(test_caps_show_basic),
|
||||
cmocka_unit_test_setup_teardown(test_join_room, muc_before_test, muc_after_test),
|
||||
// ...
|
||||
};
|
||||
```
|
||||
|
||||
Convenience macros are defined near the top, e.g.:
|
||||
|
||||
```c
|
||||
#define muc_unit_test(f) cmocka_unit_test_setup_teardown(f, muc_before_test, muc_after_test)
|
||||
```
|
||||
|
||||
## Test function shape
|
||||
|
||||
```c
|
||||
void
|
||||
test_account_set_jid_succeeds(void** state)
|
||||
{
|
||||
// Arrange
|
||||
will_return(connection_get_status, JABBER_CONNECTED);
|
||||
will_return(session_get_account_name, "alice@example.com");
|
||||
|
||||
ProfAccount* account = account_new("alice@example.com", ...);
|
||||
will_return(accounts_get_account, account);
|
||||
|
||||
// Act
|
||||
gchar* args[] = { "set", "alice@example.com", "jid", "alice@example.org", NULL };
|
||||
gboolean ok = cmd_account(NULL, CMD_ACCOUNT, args);
|
||||
|
||||
// Assert
|
||||
assert_true(ok);
|
||||
}
|
||||
```
|
||||
|
||||
## cmocka primitives in heavy use
|
||||
|
||||
| Primitive | When |
|
||||
|---|---|
|
||||
| `will_return(fn, value)` | Queue a return value for a stubbed function. Each call to the stub pops one. |
|
||||
| `will_return_count(fn, value, n)` | Queue the same value `n` times. |
|
||||
| `expect_string(fn, param, "...")` | Stub asserts that the named argument equals this string. |
|
||||
| `expect_value(fn, param, value)` | Stub asserts equality on a non-string. |
|
||||
| `expect_any(fn, param)` | Stub accepts any value (still pops the queue). |
|
||||
| `assert_true / assert_false / assert_string_equal / assert_int_equal` | Assertions inside the test. |
|
||||
| `cmocka_unit_test(fn)` | Plain registration. |
|
||||
| `cmocka_unit_test_setup_teardown(fn, setup, teardown)` | With per-test fixture. |
|
||||
|
||||
`expect_*` queues are checked at test end — missing calls fail the test.
|
||||
|
||||
## Group setup / teardown
|
||||
|
||||
For sets of tests sharing fixture: define a `_setup` and `_teardown` and
|
||||
register via `cmocka_unit_test_setup_teardown`. Examples in `unittests.c`:
|
||||
`muc_before_test`, `muc_after_test`, `keyhandlers_setup`, `keyhandlers_teardown`.
|
||||
|
||||
## What gets stubbed
|
||||
|
||||
Tests run against the production `.c` files of the unit under test, with
|
||||
stubs replacing all dependencies. See `testing/stubs.md` for the layout.
|
||||
Standard idiom: every external function the unit calls must have a stub
|
||||
returning either a fixed value or a `will_return`-driven one.
|
||||
|
||||
## Running a single test
|
||||
|
||||
`make check` runs everything. To narrow:
|
||||
|
||||
```sh
|
||||
./tests/unittests/unittests --filter <test_name_pattern>
|
||||
```
|
||||
|
||||
(Confirm via `unittests --help`; cmocka filter support depends on version.)
|
||||
|
||||
## See also
|
||||
|
||||
- `testing/stubs.md` — adding/extending stubs.
|
||||
- `playbooks/add-test.md` — adding a new test file end-to-end.
|
||||
84
wip/feat-ai.md
Normal file
84
wip/feat-ai.md
Normal file
@@ -0,0 +1,84 @@
|
||||
# WIP: `feat/ai` — AI assistant client
|
||||
|
||||
**Branch:** `feat/ai` in cproof.
|
||||
**Status:** in flight at the time of writing. Delete this file when the branch
|
||||
merges to `master`; fold the relevant bits into the stable layer.
|
||||
|
||||
This file describes the delta `feat/ai` adds on top of `master`. Load it only
|
||||
when working on the `feat/ai` branch.
|
||||
|
||||
## What the branch adds
|
||||
|
||||
A new module: `src/ai/`. A multi-provider AI assistant accessible via the
|
||||
`/ai` command (OpenAI, Perplexity).
|
||||
|
||||
## New files
|
||||
|
||||
| Path | Role |
|
||||
|---|---|
|
||||
| `src/ai/ai_client.c` | Provider abstraction, request/response, provider registry. |
|
||||
| `src/ai/ai_client.h` | Public surface. Notable: `char* ai_providers_find(const char*, gboolean, void*);` (stateless autocomplete callback). |
|
||||
| `tests/unittests/test_ai_client.c` | Unit tests for the AI client. |
|
||||
| `tests/unittests/test_ai_client.h` | Header pair for the above. |
|
||||
| `tests/unittests/ai/stub_ai_client.c` | Stubs for `ai_client.*` consumers in other unit tests. |
|
||||
| `tests/unittests/ui/stub_ai.c` | UI-side stub for the AI surface consumed by `src/ui/`. |
|
||||
|
||||
Plus added entries in:
|
||||
|
||||
| File | Addition |
|
||||
|---|---|
|
||||
| `src/command/cmd_defs.c` | `/ai` command record. |
|
||||
| `src/command/cmd_funcs.{c,h}` | `cmd_ai` handler. |
|
||||
| `src/command/cmd_ac.c` | `_ai_autocomplete` dispatcher; static `ai_subcommands_ac`, `ai_set_subcommands_ac`, `ai_remove_subcommands_ac`. |
|
||||
| `tests/unittests/unittests.c` | Test registrations for `test_ai_*`. |
|
||||
|
||||
## Subcommand layout
|
||||
|
||||
```
|
||||
/ai set {provider|token|org} <provider-name> <value>
|
||||
/ai remove provider <provider-name>
|
||||
/ai start <provider-name>
|
||||
/ai clear <provider-name>
|
||||
/ai correct <provider-name>
|
||||
/ai providers
|
||||
```
|
||||
|
||||
## Autocomplete
|
||||
|
||||
- `_ai_autocomplete` in `cmd_ac.c` is the per-command dispatcher.
|
||||
- Top-level subcommand list: `ai_subcommands_ac` → `set`, `remove`, `start`,
|
||||
`clear`, `correct`, `providers`.
|
||||
- Set subcommands: `ai_set_subcommands_ac` → `provider`, `token`, `org`.
|
||||
- Remove subcommands: `ai_remove_subcommands_ac` → `provider`.
|
||||
- Provider-name completion: `ai_providers_find()` (`src/ai/ai_client.c`) — a
|
||||
**stateless** callback (no `_last_match` global). On the `feat/ai`
|
||||
branch, this is the most current canonical example of a stateless
|
||||
autocomplete callback.
|
||||
|
||||
## CI deltas
|
||||
|
||||
`feat/ai` also brings (unrelated to AI itself):
|
||||
|
||||
- `0feacbc9d ci: simulate Pikaur flag duplication in Arch Linux CI` — adds a
|
||||
CI step that simulates the Pikaur duplicated-flag failure mode in the
|
||||
Arch Linux job. Survives independently of the AI feature.
|
||||
|
||||
If the AI work is merged but the CI delta is rebased separately, update
|
||||
`build/ci.md` to mention this commit alongside `0722dc9e3`.
|
||||
|
||||
## On merge — what to fold into stable layers
|
||||
|
||||
When `feat/ai` lands on `master`, edit:
|
||||
|
||||
| File | Change |
|
||||
|---|---|
|
||||
| `architecture/source-map.md` | Add `src/ai/` row to the module table. |
|
||||
| `architecture/overview.md` | Add AI to the optional integrations / module list; drop the "active feature branches" caveat. |
|
||||
| `architecture/test-map.md` | Add `tests/unittests/ai/` stub directory; mention `test_ai_client.{c,h}`. |
|
||||
| `patterns/autocomplete.md` | Optionally add `ai_providers_find` as a second canonical stateless-callback example (or leave `roster_contact_autocomplete` as the sole example). |
|
||||
| `playbooks/add-autocomplete.md` | Same. |
|
||||
| `gotchas.md` | Same. |
|
||||
| `build/ci.md` | Mention the Pikaur CI simulation commit alongside the original Pikaur fix. |
|
||||
|
||||
Then **delete this file**. Update `INDEX.md` to drop the `wip/` row if no
|
||||
other WIP entries remain.
|
||||
Reference in New Issue
Block a user