3 Commits

Author SHA256 Message Date
8a627f5bf5 docs: stage AGENTS.md draft for cproof
Project-level agent instructions destined for the cproof repo root.
Staged here while the layered context bundle settles; will move to
cproof in a follow-up.

Pins the cproof-context bundle to commit 22977846 and tells the agent
the load order: SKILL.md -> INDEX.md -> on-demand -> gotchas.md, plus
wip/<branch>.md when working on a listed feature branch.
2026-04-30 21:08:31 +03:00
22977846a3 docs: split context into layered, agent-oriented files
Replace the single file-structure.md with a stratified layout designed
for AI/agent skill consumption: tables and concrete identifiers over
prose, files loaded on demand, content separated by churn rate.

Layers:
- architecture/  stable structural reference (overview, source-map,
                 test-map, data-flow)
- patterns/      memory, commands, autocomplete, events, xmpp,
                 encryption, ui, plugins
- testing/       unit-tests, stubs, functional-tests, bench
- build/         local, docker, ci
- playbooks/     add-command, add-test, add-autocomplete,
                 add-event-handler, add-encryption
- gotchas.md     append-only dated entries (seven seed entries)
- wip/           branch-specific notes; deleted on merge to master

Stable layers describe cproof on master only. In-flight feature
branches (currently feat/ai) get a single file under wip/.

INDEX.md is the entry map with churn labels; SKILL.md is the
always-loaded skill hint pointing to it.
2026-04-30 20:52:06 +03:00
bea6ab6df5 Add file structure 2026-04-29 12:15:57 +00:00
30 changed files with 2416 additions and 0 deletions

65
AGENTS.md Normal file
View 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
View 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`. |

View File

@@ -1,2 +1,68 @@
# cproof-context # 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
View 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
View 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
View 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. |

View 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
View 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
View 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
View 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
View 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
View 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
View 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
View 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
View 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
View 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
View 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
View 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
View 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
View 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`.

View 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
View 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
View 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.

View 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
View 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
View 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.

View 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
View 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
View 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
View 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.