Compare commits
3 Commits
skills-exa
...
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.
|
||||
@@ -1,304 +0,0 @@
|
||||
---
|
||||
name: command-system
|
||||
description: Use when writing, modifying, or debugging CProof command functions (cmd_funcs.c), command definitions (cmd_defs.c), or autocomplete (cmd_ac.c). Documents the command dispatch architecture, critical argument indexing rules (args[0] contains subcommand name), and common patterns.
|
||||
---
|
||||
|
||||
# CProof Command System
|
||||
|
||||
## When to Use This Skill
|
||||
|
||||
Use this skill when:
|
||||
- Writing new command handler functions in `cmd_funcs.c`
|
||||
- Adding/modifying command definitions in `cmd_defs.c`
|
||||
- Implementing autocomplete for commands in `cmd_ac.c`
|
||||
- Debugging command argument parsing issues
|
||||
|
||||
## Core Architecture
|
||||
|
||||
### Command Structure ([`Command`](src/command/cmd_defs.h:64))
|
||||
|
||||
```c
|
||||
typedef struct cmd_t {
|
||||
gchar* cmd; // e.g., "/ai"
|
||||
gchar** (*parser)(const char*, int, int, gboolean*); // parse_args, parse_args_with_freetext, parse_args_as_one
|
||||
int min_args; int max_args;
|
||||
void (*setting_func)(void); // Shows current settings in console (or NULL)
|
||||
struct { const char* cmd; gboolean (*func)(ProfWin*, const char*, gchar**); } sub_funcs[50];
|
||||
gboolean (*func)(ProfWin*, const char*, gchar**); // Main handler (or NULL)
|
||||
CommandHelp help; // Tags, synopsis, desc, args, examples
|
||||
} Command;
|
||||
```
|
||||
|
||||
### Help Structure ([`CommandHelp`](src/command/cmd_funcs.h:43))
|
||||
|
||||
```c
|
||||
typedef struct cmd_help_t {
|
||||
gchar* tags[20]; // Command categories (NULL-terminated)
|
||||
gchar* synopsis[50]; // Usage examples (NULL-terminated)
|
||||
gchar* desc; // Description text
|
||||
gchar* args[128][2]; // Argument names and descriptions ({NULL,NULL}-terminated)
|
||||
gchar* examples[20]; // Example commands (NULL-terminated)
|
||||
} CommandHelp;
|
||||
```
|
||||
|
||||
### Available Tags ([`cmd_defs.c:82-89`](src/command/cmd_defs.c:82))
|
||||
|
||||
| Tag | Description |
|
||||
|-----|-------------|
|
||||
| `CMD_TAG_CHAT` | Chat commands |
|
||||
| `CMD_TAG_GROUPCHAT` | Group chat commands |
|
||||
| `CMD_TAG_ROSTER` | Roster management |
|
||||
| `CMD_TAG_PRESENCE` | Presence commands |
|
||||
| `CMD_TAG_CONNECTION` | Connection management |
|
||||
| `CMD_TAG_DISCOVERY` | Discovery commands |
|
||||
| `CMD_TAG_UI` | UI display commands |
|
||||
| `CMD_TAG_PLUGINS` | Plugin commands |
|
||||
|
||||
### Dispatch Flow ([`_cmd_execute()`](src/command/cmd_funcs.c:8261))
|
||||
|
||||
```
|
||||
User: /ai set provider openai https://api.openai.com
|
||||
|
|
||||
v
|
||||
parse_args("set provider openai https://...", 0, 5) → {"set","provider","openai","https://...",NULL}
|
||||
|
|
||||
v
|
||||
args[0]="set" matches cmd->sub_funcs[0].cmd="set"
|
||||
|
|
||||
v
|
||||
cmd_ai_set(window, "/ai", args) ← args[0] is "set", NOT "provider"!
|
||||
```
|
||||
|
||||
**CRITICAL: Subcommand functions receive the FULL args array. args[0] contains the subcommand name itself. All parameter accesses must be shifted +1.**
|
||||
|
||||
## Argument Indexing Reference
|
||||
|
||||
| User Input | args[0] | args[1] | args[2] | args[3] |
|
||||
|------------|---------|---------|---------|---------|
|
||||
| `/ai` | NULL | - | - | - |
|
||||
| `/ai set provider openai url` | `"set"` | `"provider"` | `"openai"` | `"url"` |
|
||||
| `/ai set token openai sk-xxx` | `"set"` | `"token"` | `"openai"` | `"sk-xxx"` |
|
||||
| `/ai start openai/gpt-4o` | `"start"` | `"openai/gpt-4o"` | - | - |
|
||||
| `/ai remove provider foo` | `"remove"` | `"provider"` | `"foo"` | - |
|
||||
| `/tls trust` | `"trust"` | - | - | - |
|
||||
| `/connect account server foo` | `"account"` | `"server"` | `"foo"` | - |
|
||||
|
||||
## Correct Subcommand Function Pattern
|
||||
|
||||
```c
|
||||
gboolean cmd_ai_set(ProfWin* window, const char* const command, gchar** args)
|
||||
{
|
||||
// args[0] = "set" (subcommand name)
|
||||
// args[1] = next subcommand (provider/token/org)
|
||||
|
||||
if (args[1] == NULL) { cons_bad_cmd_usage(command); return TRUE; }
|
||||
|
||||
if (g_strcmp0(args[1], "provider") == 0) {
|
||||
// args[2] = name, args[3] = url
|
||||
if (g_strv_length(args) < 4) { cons_bad_cmd_usage(command); return TRUE; }
|
||||
ai_add_provider(args[2], args[3], NULL);
|
||||
cons_show("Provider '%s' configured with URL: %s", args[2], args[3]);
|
||||
} else if (g_strcmp0(args[1], "token") == 0) {
|
||||
// args[2] = provider, args[3] = token
|
||||
if (g_strv_length(args) < 4) { cons_bad_cmd_usage(command); return TRUE; }
|
||||
ai_set_provider_key(args[2], args[3]);
|
||||
} else if (g_strcmp0(args[1], "org") == 0) {
|
||||
// args[2] = provider, args[3] = org_id
|
||||
if (g_strv_length(args) < 4) { cons_bad_cmd_usage(command); return TRUE; }
|
||||
AIProvider* p = ai_get_provider(args[2]);
|
||||
if (p) { g_free(p->org_id); p->org_id = g_strdup(args[3]); }
|
||||
} else {
|
||||
cons_bad_cmd_usage(command); return TRUE;
|
||||
}
|
||||
cons_show("");
|
||||
return TRUE;
|
||||
}
|
||||
```
|
||||
|
||||
### Common Bug (WRONG)
|
||||
```c
|
||||
// BUG: args[0] is "set", not "provider"
|
||||
if (g_strcmp0(args[0], "provider") == 0) { ... }
|
||||
// BUG: args[1] is "provider", not the name
|
||||
ai_add_provider(args[1], args[2], NULL);
|
||||
```
|
||||
|
||||
## Command Definition Pattern ([`cmd_defs.c`](src/command/cmd_defs.c))
|
||||
|
||||
### Macros ([`cmd_defs.c:91-98`](src/command/cmd_defs.c:91))
|
||||
|
||||
```c
|
||||
#define CMD_PREAMBLE(c, p, min, max, set) .cmd = c, .parser = p, .min_args = min, .max_args = max, .setting_func = set,
|
||||
#define CMD_MAINFUNC(f) .func = f,
|
||||
#define CMD_SUBFUNCS(...) .sub_funcs = { __VA_ARGS__, { NULL, NULL } },
|
||||
#define CMD_TAGS(...) .help.tags = { __VA_ARGS__, NULL },
|
||||
#define CMD_SYN(...) .help.synopsis = { __VA_ARGS__, NULL },
|
||||
#define CMD_DESC(d) .help.desc = d,
|
||||
#define CMD_ARGS(...) .help.args = { __VA_ARGS__, { NULL, NULL } },
|
||||
#define CMD_EXAMPLES(...) .help.examples = { __VA_ARGS__, NULL }
|
||||
```
|
||||
|
||||
### Example Definition
|
||||
|
||||
```c
|
||||
{ CMD_PREAMBLE("/ai", parse_args, 0, 5, NULL)
|
||||
CMD_SUBFUNCS({ "set", cmd_ai_set }, { "remove", cmd_ai_remove },
|
||||
{ "start", cmd_ai_start }, { "clear", cmd_ai_clear },
|
||||
{ "correct", cmd_ai_correct }, { "providers", cmd_ai_providers })
|
||||
CMD_MAINFUNC(cmd_ai)
|
||||
CMD_TAGS(CMD_TAG_CHAT)
|
||||
CMD_SYN("/ai", "/ai set provider <name> <url>", "/ai set token <provider> <token>")
|
||||
CMD_DESC("Interact with AI models via OpenAI-compatible APIs.")
|
||||
CMD_ARGS({ "", "Display current AI settings" },
|
||||
{ "set provider <name> <url>", "Add or update a provider" })
|
||||
CMD_EXAMPLES("/ai", "/ai set token openai sk-xxx", "/ai start openai/gpt-4o")
|
||||
},
|
||||
```
|
||||
|
||||
## Autocomplete Pattern ([`cmd_ac.c`](src/command/cmd_ac.c))
|
||||
|
||||
### Initialization
|
||||
```c
|
||||
// In cmd_ac_init():
|
||||
autocomplete_add(ai_subcommands_ac, "set");
|
||||
autocomplete_add(ai_subcommands_ac, "remove");
|
||||
autocomplete_add(ai_set_subcommands_ac, "provider");
|
||||
autocomplete_add(ai_set_subcommands_ac, "token");
|
||||
autocomplete_add(ai_set_subcommands_ac, "org");
|
||||
g_hash_table_insert(ac_funcs, "/ai", _ai_autocomplete);
|
||||
```
|
||||
|
||||
### Callback
|
||||
```c
|
||||
static char* _ai_autocomplete(ProfWin* window, const char* const input, gboolean previous)
|
||||
{
|
||||
char* result = NULL;
|
||||
result = autocomplete_param_with_ac(input, "/ai", ai_subcommands_ac, TRUE, previous);
|
||||
if (result) return result;
|
||||
result = autocomplete_param_with_ac(input, "/ai set", ai_set_subcommands_ac, TRUE, previous);
|
||||
if (result) return result;
|
||||
// Dynamic: provider names from ai_list_providers()
|
||||
return NULL;
|
||||
}
|
||||
```
|
||||
|
||||
## Common Patterns
|
||||
|
||||
### Window Type Access
|
||||
```c
|
||||
ProfAiWin* aiwin = (window && window->type == WIN_AI) ? (ProfAiWin*)window : wins_get_ai();
|
||||
// Or switch:
|
||||
switch (window->type) {
|
||||
case WIN_CHAT: { ProfChatWin* w = (ProfChatWin*)window; /* w->barejid */ break; }
|
||||
case WIN_MUC: { ProfMucWin* w = (ProfMucWin*)window; /* w->roomjid */ break; }
|
||||
}
|
||||
```
|
||||
|
||||
### Connection Check
|
||||
```c
|
||||
if (connection_get_status() != JABBER_CONNECTED) {
|
||||
cons_show("You are not currently connected.");
|
||||
return TRUE;
|
||||
}
|
||||
```
|
||||
|
||||
### Options Parsing
|
||||
```c
|
||||
gchar* opt_keys[] = { "server", "port", "tls", NULL };
|
||||
GHashTable* opts = parse_options(&args[args[0] ? 1 : 0], opt_keys, &parsed);
|
||||
// &args[args[0] ? 1 : 0] skips args[0] if present
|
||||
```
|
||||
|
||||
### Memory Management
|
||||
```c
|
||||
auto_gcharv gchar** args = cmd->parser(inp, min, max, &result); // Auto-freed
|
||||
auto_gchar gchar* path = prefs_get_string(PREF_SOME_PREF); // Auto-freed
|
||||
```
|
||||
|
||||
## Argument Parsers ([`parser.c`](src/tools/parser.c))
|
||||
|
||||
| Function | Description |
|
||||
|----------|-------------|
|
||||
| `parse_args()` | Standard argument parsing, splits on spaces |
|
||||
| `parse_args_with_freetext()` | Last argument captures everything after `max` tokens |
|
||||
| `parse_args_as_one()` | Everything after first space becomes single argument |
|
||||
|
||||
## Console Output Functions ([`ui.h`](src/ui/ui.h))
|
||||
|
||||
| Function | Description |
|
||||
|----------|-------------|
|
||||
| `cons_show(const char* msg, ...)` | Print formatted message to console |
|
||||
| `cons_show_error(const char* cmd, ...)` | Print error message |
|
||||
| `cons_bad_cmd_usage(const char* cmd)` | Print usage error |
|
||||
|
||||
## Adding a New Command: Step-by-Step
|
||||
|
||||
### Step 1: Declare Function in `cmd_funcs.h`
|
||||
|
||||
```c
|
||||
gboolean cmd_mycommand(ProfWin* window, const char* const command, gchar** args);
|
||||
```
|
||||
|
||||
### Step 2: Implement Function in `cmd_funcs.c`
|
||||
|
||||
```c
|
||||
gboolean
|
||||
cmd_mycommand(ProfWin* window, const char* const command, gchar** args)
|
||||
{
|
||||
// args[0] = first argument (or NULL if none)
|
||||
if (args[0] == NULL) {
|
||||
cons_show("No argument provided.");
|
||||
return TRUE;
|
||||
}
|
||||
cons_show("Got: %s", args[0]);
|
||||
return TRUE;
|
||||
}
|
||||
```
|
||||
|
||||
### Step 3: Add Definition to `command_defs[]` in `cmd_defs.c`
|
||||
|
||||
Find the end of the array (look for `};` after the last entry) and add:
|
||||
|
||||
```c
|
||||
{ CMD_PREAMBLE("/mycommand",
|
||||
parse_args, 0, 1, NULL)
|
||||
CMD_MAINFUNC(cmd_mycommand)
|
||||
CMD_TAGS(CMD_TAG_CHAT)
|
||||
CMD_SYN("/mycommand [<arg>]")
|
||||
CMD_DESC("My new command description.")
|
||||
CMD_ARGS({ "<arg>", "An argument" })
|
||||
CMD_EXAMPLES("/mycommand hello")
|
||||
},
|
||||
```
|
||||
|
||||
### Step 4: Add Autocomplete
|
||||
|
||||
In `cmd_ac.c`:
|
||||
|
||||
1. Add static function declaration at top:
|
||||
```c
|
||||
static char* _mycommand_autocomplete(ProfWin* window, const char* const input, gboolean previous);
|
||||
```
|
||||
|
||||
2. Register in `cmd_ac_init()`:
|
||||
```c
|
||||
g_hash_table_insert(ac_funcs, "/mycommand", _mycommand_autocomplete);
|
||||
```
|
||||
|
||||
3. Implement the function:
|
||||
```c
|
||||
static char*
|
||||
_mycommand_autocomplete(ProfWin* window, const char* const input, gboolean previous)
|
||||
{
|
||||
return NULL; // No autocomplete
|
||||
}
|
||||
```
|
||||
|
||||
## Key Files
|
||||
- [`src/command/cmd_defs.c`](src/command/cmd_defs.c) - Command definitions
|
||||
- [`src/command/cmd_defs.h`](src/command/cmd_defs.h) - Type definitions
|
||||
- [`src/command/cmd_funcs.c`](src/command/cmd_funcs.c) - Handler implementations
|
||||
- [`src/command/cmd_funcs.h`](src/command/cmd_funcs.h) - Function declarations
|
||||
- [`src/command/cmd_ac.c`](src/command/cmd_ac.c) - Autocomplete
|
||||
- [`src/tools/parser.c`](src/tools/parser.c) - Argument parsing
|
||||
- [`src/ui/ui.h`](src/ui/ui.h) - Console output functions
|
||||
@@ -1,129 +0,0 @@
|
||||
---
|
||||
name: cproof-structure
|
||||
description: Always use this skill before editing CProof program. It contains file structure and expected code style.
|
||||
---
|
||||
|
||||
# Cproof Structure
|
||||
|
||||
## CProof Project Structure (for skill context)
|
||||
|
||||
**CProof** is a terminal-based XMPP (chat) client written in C. It uses autotools for building and cmocka for unit testing. CProof is a fork of Profanity, renaming is WIP.
|
||||
|
||||
### Build System
|
||||
|
||||
- `configure.ac` / `Makefile.am` - Autotools build configuration
|
||||
- `autogen.sh` - Script to generate configure scripts
|
||||
- `configure-debug` - Debug build helper
|
||||
- `tests/prof_cmocka.h` - Test framework header (cmocka wrapper)
|
||||
|
||||
### Source Directory (`src/`)
|
||||
|
||||
| Directory | Purpose |
|
||||
|-----------|---------|
|
||||
| `src/ai/` | AI client implementation (`ai_client.c/h`) - OpenAI/Perplexity provider support |
|
||||
| `src/command/` | Command handling (`cmd_ac.c/h` - autocomplete, `cmd_defs.c/h` - command definitions, `cmd_funcs.c/h` - command implementations) |
|
||||
| `src/config/` | Configuration management (accounts, preferences, themes, colors, CA certs, scripts) |
|
||||
| `src/event/` | Event handling (client events, server events, common) |
|
||||
| `src/omemo/` | OMEMO encryption (crypto, store) |
|
||||
| `src/otr/` | OTR encryption (`otr.c/h`, `otrlib.c/h`, `otrlibv4.c`) |
|
||||
| `src/pgp/` | PGP/OpenPGP integration (`gpg.c/h`, `ox.c/h`) |
|
||||
| `src/plugins/` | Plugin system (C API, Python API, autocompleters, callbacks, disco, settings, themes) |
|
||||
| `src/tools/` | Utility functions (autocomplete, parser, clipboard, editor, HTTP download/upload, bookmark ignore) |
|
||||
| `src/ui/` | User interface (buffers, windows, input, chat/muc/console windows, statusbar, tray, window management) |
|
||||
| `src/xmpp/` | XMPP protocol layer (connection, roster, muc, presence, message, stanza, vcard, session, JID, capabilities, OMEMO, Ox, blocking, bookmark, chat state, avatar) |
|
||||
|
||||
**Key files at `src/` root:**
|
||||
- `main.c` - Entry point
|
||||
- `profanity.c` - Application initialization/shutdown
|
||||
- `common.h` - Common types/macros (including `auto_gchar` for memory management)
|
||||
- `database.c` - Database layer
|
||||
- `chatlog.c` - Chat log handling
|
||||
- `log.c` - Logging
|
||||
|
||||
### Tests Directory (`tests/`)
|
||||
|
||||
| Directory | Purpose |
|
||||
|-----------|---------|
|
||||
| `tests/functionaltests/` | Integration tests (requires running CProof instance) |
|
||||
| `tests/unittests/` | Unit tests (cmocka-based, run via `make check`) |
|
||||
|
||||
**Unit test structure:**
|
||||
- `tests/unittests/unittests.c` - Test runner (registers all tests)
|
||||
- `tests/unittests/test_*.c/h` - Test files paired with headers
|
||||
- `tests/unittests/helpers.c/h` - Test utilities
|
||||
- `tests/unittests/<module>/stub_*.c` - Stub implementations for dependencies
|
||||
|
||||
**Stub directories:**
|
||||
- `unittests/ai/stub_ai_client.c` - AI client stubs
|
||||
- `unittests/config/stub_accounts.c`, `stub_cafile.c`
|
||||
- `unittests/database/stub_database.c`
|
||||
- `unittests/log/stub_log.c`
|
||||
- `unittests/omemo/stub_omemo.c`
|
||||
- `unittests/otr/stub_otr.c`
|
||||
- `unittests/pgp/stub_gpg.c`, `stub_ox.c`
|
||||
- `unittests/tools/stub_*.c`
|
||||
- `unittests/ui/stub_ai.c`, `stub_ui.c`, `stub_vcardwin.c`
|
||||
- `unittests/xmpp/stub_*.c`
|
||||
|
||||
### Key Patterns
|
||||
|
||||
**Memory Management:**
|
||||
- Uses `auto_gchar` macro (from `common.h`) for automatic GString/GString cleanup
|
||||
- `g_free()`, `g_list_free_full()`, `g_hash_table_destroy()` for manual cleanup
|
||||
- `FREE_SET_NULL()` / `GFREE_SET_NULL()` macros
|
||||
|
||||
**Autocomplete System:**
|
||||
- `Autocomplete` type (GList-based) defined in `tools/autocomplete.h`
|
||||
- Functions: `autocomplete_new()`, `autocomplete_add()`, `autocomplete_complete()`, `autocomplete_free()`
|
||||
- `autocomplete_param_with_ac()` - Complete using an Autocomplete object
|
||||
- `autocomplete_param_with_func()` - Complete using a callback function
|
||||
- `_autocomplete_param_common()` - Internal helper
|
||||
|
||||
**Command System:**
|
||||
- Commands defined in `cmd_defs.c` with `CMD_MAINFUNC()` and `CMD_TAGS()` macros
|
||||
- Autocomplete registered in `cmd_ac.c` via `g_hash_table_insert(ac_funcs, "/cmd", _cmd_autocomplete)`
|
||||
- `parse_args()` from `tools/parser.c` splits input into args array
|
||||
|
||||
**Testing Patterns:**
|
||||
- Use `will_return(func, value)` before calling `func()` to mock return values
|
||||
- Use `expect_string()` for struct field verification
|
||||
- Setup/teardown via `cmocka_unit_test_setup_teardown(test_func, setup, teardown)`, centralized in unittests.c
|
||||
- Mock `connection_get_status()` with `will_return(connection_get_status, JABBER_CONNECTED)` if connection is needed
|
||||
|
||||
### Current Work Context
|
||||
|
||||
The `/ai` command autocomplete was recently added with:
|
||||
- `ai_subcommands_ac` - Top-level subcommands: set, remove, start, clear, correct, providers
|
||||
- `ai_set_subcommands_ac` - Set subcommands: provider, token, org
|
||||
- `ai_remove_subcommands_ac` - Remove subcommands: provider
|
||||
- `ai_providers_find()` - Provider name autocomplete callback (stateless, no `_last_provider_match`)
|
||||
- Tests in `tests/unittests/test_ai_client.c` and `tests/unittests/test_ai_client.h`
|
||||
|
||||
## Full List of `auto_` Macros (Automatic Memory Management)
|
||||
|
||||
These macros use GCC's `__cleanup__` attribute to automatically free/close resources when they go out of scope.
|
||||
|
||||
| Macro | Type | Cleanup Function | Description |
|
||||
|-------|------|-----------------|-------------|
|
||||
| `auto_gchar` | `gchar*` | `auto_free_gchar()` | Automatically frees a GLib character string |
|
||||
| `auto_gcharv` | `gchar**` | `auto_free_gcharv()` | Automatically frees a GLib character string array (e.g., from `g_strsplit`) |
|
||||
| `auto_char` | `char*` | `auto_free_char()` | Automatically frees a standard C string (from `strdup`) |
|
||||
| `auto_guchar` | `guchar*` | `auto_free_guchar()` | Automatically frees a GLib unsigned char string (e.g., base64 decoded) |
|
||||
| `auto_gfd` | `gint*` | `auto_close_gfd()` | Automatically closes a GFileDescriptor |
|
||||
| `auto_FILE` | `FILE*` | `auto_close_FILE()` | Automatically closes a FILE stream |
|
||||
| `auto_jid` | `Jid*` | `jid_auto_destroy()` | Automatically frees a Jid struct (defined only in `src/xmpp/jid.h`) |
|
||||
|
||||
**Example usage:**
|
||||
```c
|
||||
auto_gchar gchar* myString = g_strdup("Hello, world!");
|
||||
// myString is automatically freed when scope exits
|
||||
```
|
||||
|
||||
## License declaration
|
||||
|
||||
License declaration must be concise, such as one below. Or, even better, not present in new files: commentary about file details is much more important.
|
||||
```
|
||||
* Copyright (C) 2026 CProof Developers
|
||||
*
|
||||
* SPDX-License-Identifier: GPL-3.0-or-later WITH OpenSSL-exception
|
||||
```
|
||||
@@ -1,414 +0,0 @@
|
||||
---
|
||||
name: writing-prs-issues-commits
|
||||
description: Guidelines for writing effective PR descriptions, GitHub issues, and git commit messages following the inverted pyramid style. Use only when you work on them.
|
||||
---
|
||||
|
||||
# Writing Effective PRs, Issues, and Git Commit Messages
|
||||
|
||||
## Core Principle: Inverted Pyramid
|
||||
|
||||
All three artifacts (PRs, issues, commit messages) should follow the **inverted pyramid** style: the most important information comes first, with progressively finer details below.
|
||||
|
||||
> When scrolling through a commit history or PR list, readers need to quickly determine relevance. The "what" and "why" must be immediately visible.
|
||||
|
||||
---
|
||||
|
||||
## 1. Git Commit Messages
|
||||
|
||||
### Structure
|
||||
|
||||
```
|
||||
<type>: <short summary (50 chars max)>
|
||||
|
||||
<blank line>
|
||||
|
||||
<one paragraph describing the change and its motivation>
|
||||
|
||||
<optional: implementation details, debugging process, architecture notes>
|
||||
```
|
||||
|
||||
### Rules
|
||||
|
||||
1. **Subject line**: imperative mood, present tense, 50 characters or less
|
||||
- Good: `feat(ai): add AI client with multi-provider support`
|
||||
- Bad: `fixed the bug with the ai client that was causing issues`
|
||||
|
||||
2. **First paragraph**: state WHAT was changed and WHY in 2-3 sentences. No code references.
|
||||
|
||||
3. **Optional "How" section**: implementation details, debugging process, architecture decisions. This is "extra-credit" reading.
|
||||
|
||||
4. **Never bury the lead**: if the commit changes one file, the subject line should say what it does. Don't make readers scroll to find out.
|
||||
|
||||
### Anti-Patterns (from David Thompson's "Favorite Commit")
|
||||
|
||||
| Anti-Pattern | Example | Fix |
|
||||
|---|---|---|
|
||||
| Burying the change | 6 paragraphs before showing the diff | Put the summary first |
|
||||
| Unexplained problem | "I introduced some tests..." without saying what broke | State the problem explicitly |
|
||||
| Unlinked code references | "removing the .with_content() matchers" | Name the file or link to it |
|
||||
|
||||
### Example
|
||||
|
||||
```
|
||||
feat(ai): add AI client with multi-provider support
|
||||
|
||||
Add an AI client module that integrates with OpenAI-compatible API
|
||||
providers (OpenAI, Perplexity, and custom providers) to provide
|
||||
AI-assisted responses within the CProof client.
|
||||
|
||||
The implementation includes provider management, session handling,
|
||||
async HTTP request handling via libcurl, and a dedicated AI window
|
||||
for displaying conversations.
|
||||
|
||||
Architecture:
|
||||
- Async design: HTTP requests run on a separate thread to avoid
|
||||
blocking the main UI loop
|
||||
- Reference counting: Both AIProvider and AISession use ref counting
|
||||
for safe shared ownership
|
||||
- Response size limit: 10MB cap on HTTP responses to prevent OOM
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## 2. GitHub Issues
|
||||
|
||||
### Structure
|
||||
|
||||
```markdown
|
||||
# <type>: <issue title>
|
||||
|
||||
## Motivation
|
||||
|
||||
<Why does this matter? Who benefits?>
|
||||
|
||||
## Proposed Solution
|
||||
|
||||
<What should happen? Keep it implementation-agnostic.>
|
||||
|
||||
## Acceptance Criteria
|
||||
|
||||
- [ ] AC-1: <verifiable condition>
|
||||
- [ ] AC-2: <verifiable condition>
|
||||
- [ ] ...
|
||||
|
||||
## Open Questions
|
||||
|
||||
<Unresolved decisions that need input>
|
||||
|
||||
## Related
|
||||
|
||||
<Links to related issues, docs, or external resources>
|
||||
```
|
||||
|
||||
### Rules
|
||||
|
||||
1. **Title**: clear, specific, action-oriented. Use conventional commit prefixes where appropriate (`feat:`, `fix:`, `docs:`, etc.)
|
||||
|
||||
2. **Motivation first**: explain WHY this matters before describing WHAT to build. Focus on user value, not technical details.
|
||||
|
||||
3. **Acceptance criteria**: verifiable, testable conditions. Use function names or command syntax where helpful, but avoid implementation specifics (no "use GHashTable" or "add this function signature").
|
||||
|
||||
4. **Keep it general**: describe the feature, not the implementation. The "how" is for the PR/commit, not the issue.
|
||||
|
||||
5. **Commands over code**: when describing user-facing behavior, use command syntax (`/ai start`) rather than function names (`ai_window_create()`).
|
||||
|
||||
### Anti-Patterns
|
||||
|
||||
| Anti-Pattern | Example | Fix |
|
||||
|---|---|---|
|
||||
| Implementation details in issue | "Add a GHashTable in ai_client.c" | Describe the feature: "Users can manage multiple AI providers" |
|
||||
| Vague acceptance criteria | "Make it work" | "Running `/ai start` opens a dedicated AI conversation window" |
|
||||
| No motivation | Jumping straight to the solution | Explain WHY this matters to users |
|
||||
|
||||
### Example
|
||||
|
||||
```markdown
|
||||
# feat(ai): AI Client with Multi-Provider Support
|
||||
|
||||
## Motivation
|
||||
|
||||
CProof users want AI-assisted responses without leaving the chat client.
|
||||
This feature enables users to query AI models directly from within CProof
|
||||
for quick answers, message drafting, or knowledge retrieval.
|
||||
|
||||
### Privacy First
|
||||
|
||||
- No telemetry: No data is sent to CProof or any third party except
|
||||
the configured AI provider
|
||||
- Local-first: Any OpenAI-compatible API endpoint works, including
|
||||
local servers (Ollama, LM Studio, vLLM)
|
||||
- User-controlled keys: API keys are stored per-provider in the
|
||||
preferences file, never hardcoded
|
||||
|
||||
## Proposed Feature
|
||||
|
||||
Add an AI client module to CProof that lets users interact with AI
|
||||
providers from within the chat interface.
|
||||
|
||||
A user should be able to:
|
||||
1. Start an AI session with `/ai start`
|
||||
2. See a dedicated AI window open for the conversation
|
||||
3. Send messages and receive AI responses
|
||||
4. Switch between providers and models
|
||||
5. Use tab-completion for provider names (`/ai s<tab>`)
|
||||
6. Have providers and API keys persist across CProof sessions
|
||||
|
||||
## Acceptance Criteria
|
||||
|
||||
### Core Functionality
|
||||
|
||||
- [ ] AC-1: Running `/ai start` opens a dedicated AI conversation window
|
||||
- [ ] AC-2: The AI window displays the user's prompt and the AI's response
|
||||
- [ ] AC-3: AI responses are properly formatted, including multiline responses
|
||||
- [ ] AC-4: The conversation maintains history within a session
|
||||
- [ ] AC-5: Running `/ai stop` closes the AI window and ends the session
|
||||
|
||||
### Provider Management
|
||||
|
||||
- [ ] AC-6: Default providers are available out of the box
|
||||
- [ ] AC-7: Users can add custom providers with `/ai provider add <name> <url>`
|
||||
- [ ] AC-8: Users can list available providers with `/ai provider list`
|
||||
|
||||
### API Key Management
|
||||
|
||||
- [ ] AC-9: Users can set an API key for a provider with `/ai key <provider> <key>`
|
||||
- [ ] AC-10: API keys persist across CProof sessions
|
||||
- [ ] AC-11: API keys are not displayed in plain text when listed
|
||||
|
||||
### Autocomplete
|
||||
|
||||
- [ ] AC-12: `/ai s<tab>` autocompletes to available providers
|
||||
- [ ] AC-13: Autocomplete works for custom providers added by the user
|
||||
|
||||
### Error Handling
|
||||
|
||||
- [ ] AC-14: If the API key is invalid, the user sees a clear error message
|
||||
- [ ] AC-15: If the provider is unreachable, the user sees a clear error message
|
||||
|
||||
## Open Questions
|
||||
|
||||
1. Should we support streaming responses (displaying text as it arrives)?
|
||||
2. Should there be a rate limit or timeout for AI responses?
|
||||
|
||||
## Related
|
||||
|
||||
- [OpenAI Responses API](https://platform.openai.com/docs/api-reference/responses)
|
||||
- [Ollama](https://ollama.com/) (local server)
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## 3. Pull Request Descriptions
|
||||
|
||||
### Structure
|
||||
|
||||
```markdown
|
||||
# <type>: <PR title>
|
||||
|
||||
## Introduced change
|
||||
|
||||
<2-3 sentences: what this PR does and why it matters>
|
||||
|
||||
### Capabilities
|
||||
|
||||
<Bullet list of key features/capabilities>
|
||||
|
||||
## Reasoning behind the change
|
||||
|
||||
<Motivation, design decisions, trade-offs considered>
|
||||
|
||||
## Implementation details
|
||||
|
||||
<Architecture overview, key implementation details, code snippets if helpful>
|
||||
|
||||
## Testing
|
||||
|
||||
<How to run tests, what was tested>
|
||||
|
||||
## Screenshots (if UI changes)
|
||||
|
||||
<Before/after images>
|
||||
|
||||
Resolves #<issue-number>
|
||||
```
|
||||
|
||||
### Rules
|
||||
|
||||
1. **"Introduced change"**: the first section should answer "what" without requiring the reader to scroll. Use bullet points for capabilities.
|
||||
|
||||
2. **"Reasoning behind the change"**: explain design decisions, trade-offs, and alternatives considered. This is where you justify your approach.
|
||||
|
||||
3. **"Implementation details"**: architecture diagrams, code snippets, data structures. This is "extra-credit" reading for reviewers who want to understand the internals.
|
||||
|
||||
4. **No git diff stat**: don't include "X files changed, +Y lines". That's visible in the PR UI.
|
||||
|
||||
5. **Commands over code**: when describing user-facing behavior, use command syntax rather than function names.
|
||||
|
||||
6. **Implicit sections**: the sections flow naturally — "Introduced change" covers what, "Reasoning" covers why, "Implementation details" covers how. Don't label them explicitly as "What/Why/How".
|
||||
|
||||
### Anti-Patterns
|
||||
|
||||
| Anti-Pattern | Example | Fix |
|
||||
|---|---|---|
|
||||
| Git diff stat | "20 files changed, +2400 insertions" | Remove — visible in PR UI |
|
||||
| Implementation in "What" | "Added ai_client.c with 843 lines" | Describe the feature: "Core AI client with provider management" |
|
||||
| No "Why" section | Just listing changes | Explain design decisions and trade-offs |
|
||||
|
||||
### Example
|
||||
|
||||
```markdown
|
||||
# feat(ai): add AI client with multi-provider support and UI
|
||||
|
||||
## Introduced change
|
||||
|
||||
An AI client module that integrates with OpenAI-compatible API providers
|
||||
to deliver AI-assisted responses within the CProof chat client. Users can
|
||||
create AI sessions, send prompts, and view responses in a dedicated AI
|
||||
window — all from within the terminal UI.
|
||||
|
||||
### Capabilities
|
||||
|
||||
- **Multi-provider support**: Ships with OpenAI and Perplexity as defaults;
|
||||
add custom providers via `/ai provider add`
|
||||
- **Per-provider API keys**: Each provider's API key is stored in the
|
||||
preferences system
|
||||
- **Conversation history**: Sessions maintain message history (user/assistant
|
||||
turns) for context-aware responses
|
||||
- **Async requests**: HTTP calls run on a background thread; responses and
|
||||
errors invoke callbacks on the main UI thread
|
||||
- **Model selection**: Switch between models per session (e.g., `gpt-4`, `sonar`)
|
||||
- **Provider autocomplete**: Tab-completion for provider names in commands
|
||||
|
||||
## Reasoning behind the change
|
||||
|
||||
CProof is an XMPP client focused on privacy and usability. Adding AI support
|
||||
gives users a way to get quick answers, message drafting assistance, or
|
||||
knowledge retrieval without leaving the chat client.
|
||||
|
||||
The design prioritizes:
|
||||
|
||||
1. **Privacy**: API keys are stored per-provider in preferences, not hardcoded.
|
||||
Users control which providers they use. No data is shared with us, local
|
||||
OpenAI-compatible servers are also supported.
|
||||
2. **Extensibility**: The provider abstraction (`AIProvider` struct with name,
|
||||
URL, org_id) makes it trivial to add new providers.
|
||||
3. **Non-blocking UI**: Async HTTP ensures the terminal UI remains responsive
|
||||
during API calls.
|
||||
4. **Safety**: Response size capped at 10MB; reference counting prevents
|
||||
use-after-free on shared objects.
|
||||
|
||||
## Implementation details
|
||||
|
||||
### Architecture Overview
|
||||
|
||||
```
|
||||
┌─────────────────────────────────────────────────────────────┐
|
||||
│ UI Layer │
|
||||
│ ┌──────────────┐ ┌──────────────┐ ┌───────────────────┐ │
|
||||
│ │ ProfAiWin │ │ /ai command │ │ Provider autocomplete│ │
|
||||
│ │ (window.c) │ │ (cmd_funcs) │ │ (cmd_ac.c) │ │
|
||||
│ └──────┬───────┘ └──────┬───────┘ └────────┬──────────┘ │
|
||||
│ │ │ │ │
|
||||
│ ▼ ▼ │ │
|
||||
│ ┌──────────────────────────────────────────────────┐ │
|
||||
│ │ AI Client (ai_client.c) │ │
|
||||
│ │ ┌────────────┐ ┌────────────┐ ┌───────────┐ │ │
|
||||
│ │ │ Providers │ │ Sessions │ │ curl HTTP│ │ │
|
||||
│ │ │ (GHashTable)│ │(ref-counted)│ │(async) │ │ │
|
||||
│ │ └────────────┘ └────────────┘ └───────────┘ │ │
|
||||
│ └──────────────────────────────────────────────────┘ │
|
||||
│ │ │
|
||||
│ ▼ │
|
||||
│ ┌──────────────────────────────────────────────────┐ │
|
||||
│ │ Preferences (config/preferences.c) │ │
|
||||
│ │ Stores: api_keys[provider_name] │ │
|
||||
│ └──────────────────────────────────────────────────┘ │
|
||||
└─────────────────────────────────────────────────────────────┘
|
||||
```
|
||||
|
||||
### Key Implementation Details
|
||||
|
||||
**Provider Management** — Providers are stored in a `GHashTable` keyed by name.
|
||||
Each provider has a reference count for safe shared ownership:
|
||||
|
||||
```c
|
||||
typedef struct ai_provider_t {
|
||||
gchar* name;
|
||||
gchar* api_url;
|
||||
gchar* org_id;
|
||||
GList* models;
|
||||
guint ref_count;
|
||||
} AIProvider;
|
||||
```
|
||||
|
||||
Default providers are registered during `ai_client_init()`:
|
||||
|
||||
| Provider | URL |
|
||||
|---|---|
|
||||
| `openai` | `https://api.openai.com/v1/responses` |
|
||||
| `perplexity` | `https://api.perplexity.ai/v1/responses` |
|
||||
|
||||
**Session Lifecycle** — Sessions track conversation history and are reference-counted:
|
||||
|
||||
```c
|
||||
typedef struct ai_session_t {
|
||||
gchar* provider_name;
|
||||
AIProvider* provider;
|
||||
gchar* model;
|
||||
gchar* api_key;
|
||||
GList* history; // GList of AIMessage*
|
||||
guint ref_count;
|
||||
} AISession;
|
||||
```
|
||||
|
||||
#### Async HTTP Request
|
||||
|
||||
`ai_send_prompt()` spawns a GThread that:
|
||||
1. Builds a JSON body from the session's conversation history
|
||||
2. Sends a POST request via libcurl with the provider's API key
|
||||
3. Invokes the user's callback on the main thread with the response or error
|
||||
|
||||
Response size is capped at 10MB to prevent OOM conditions.
|
||||
|
||||
#### UI Integration
|
||||
|
||||
A new `ProfAiWin` window type displays AI conversations. Responses are streamed
|
||||
line-by-line; errors are displayed in red.
|
||||
|
||||
#### Testing
|
||||
|
||||
Unit tests cover:
|
||||
- Provider CRUD (add, remove, list, update)
|
||||
- Session lifecycle (create, ref/unref, message history, clear)
|
||||
- API key get/set
|
||||
- JSON string escaping (special chars, backslashes, percent signs)
|
||||
- Provider autocomplete (forward, backward, partial match, case sensitivity)
|
||||
|
||||
Resolves #110
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## Quick Reference
|
||||
|
||||
### Conventional Commits Prefixes
|
||||
|
||||
| Prefix | Meaning |
|
||||
|---|---|
|
||||
| `feat` | New feature |
|
||||
| `fix` | Bug fix |
|
||||
| `docs` | Documentation changes |
|
||||
| `style` | Code style changes (formatting, semicolons, etc.) |
|
||||
| `refactor` | Code changes that neither fix a bug nor add a feature |
|
||||
| `test` | Adding or modifying tests |
|
||||
| `chore` | Build process, auxiliary tools, CI changes |
|
||||
| `perf` | Performance improvements |
|
||||
| `ci` | CI configuration changes |
|
||||
| `build` | Build system or dependency changes |
|
||||
|
||||
### File Naming
|
||||
|
||||
- CProof (not Profanity) — this is a fork
|
||||
- Use snake_case for files: `ai_client.c`, `ai_client.h`
|
||||
- Use snake_case for functions: `ai_session_create()`
|
||||
- Use PascalCase for types: `AIProvider`, `AISession`
|
||||
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