docs: split context into layered, agent-oriented files
Replace the single file-structure.md with a stratified layout designed
for AI/agent skill consumption: tables and concrete identifiers over
prose, files loaded on demand, content separated by churn rate.
Layers:
- architecture/ stable structural reference (overview, source-map,
test-map, data-flow)
- patterns/ memory, commands, autocomplete, events, xmpp,
encryption, ui, plugins
- testing/ unit-tests, stubs, functional-tests, bench
- build/ local, docker, ci
- playbooks/ add-command, add-test, add-autocomplete,
add-event-handler, add-encryption
- gotchas.md append-only dated entries (seven seed entries)
- wip/ branch-specific notes; deleted on merge to master
Stable layers describe cproof on master only. In-flight feature
branches (currently feat/ai) get a single file under wip/.
INDEX.md is the entry map with churn labels; SKILL.md is the
always-loaded skill hint pointing to it.
This commit is contained in:
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.
|
||||
Reference in New Issue
Block a user