Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
34 changes: 23 additions & 11 deletions .agents/skills/local-verify/SKILL.md
Original file line number Diff line number Diff line change
Expand Up @@ -16,11 +16,16 @@ binary and run real subcommands against it.
go build -o bin/ncli ./cmd/ncli
```

## Fastest real handle: a live public relay
## Picking a relay to verify against

No need to spin up `ncli relay` or seed a local bbolt store for most
checks — `wss://relay.ohstr.com` is reachable from this sandbox and has a
constant stream of real events. `find`/`dump`/`miner check` all take
A public relay is the quickest handle for a read-path check, but don't
assume one is up: `wss://relay.ohstr.com` has returned a Cloudflare 404
(so the websocket upgrade fails with `bad handshake`) while
`wss://nos.lol` and `wss://relay.primal.net` still served events. Probe
before trusting a failure, and prefer a local relay you seed yourself
(see below) for anything you need to be repeatable.

`find`/`dump`/`miner check` all take
targets and filters the same two ways: a `--targets`/`-t` YAML file
declaring both together (see `examples/targets.yaml`), or `--relays`/`-s`
(comma-separated relay URLs/local `.db` paths) plus inline filter flags —
Expand Down Expand Up @@ -54,13 +59,20 @@ network — actual evidence, not a mock.
just dev relay # runs `ncli relay --config examples/relay/minimal.yaml`, db: ./data/db/notes.db
```

There's no dedicated `ncli publish` command — the checked-in
`data/db/notes.db` / `build/.dev/db/*.db` files are often empty (dev
scratch, not fixtures). For read-path checks (`dump`/`find` against
`.db` files), prefer syncing a few real events down from a public relay
into a local store first (via `apply` with a `stream` spec, or `dump`
from `wss://relay.ohstr.com` piped through a small script) rather than
assuming a checked-in `.db` has data.
The checked-in `data/db/notes.db` / `build/.dev/db/*.db` files are often
empty (dev scratch, not fixtures), so don't assume a checked-in `.db` has
data. To seed a relay yourself, sign events and publish them — no public
relay needed:

```sh
./bin/ncli id --save --label seed --json # needs NCLI_VAULT_PASSWORD
jq -nc '[range(0;5) | {kind:1, content:("seed " + (.|tostring)), created_at:((now|floor) - .), tags:[]}]' > unsigned.json
./bin/ncli id sign -e unsigned.json -o signed.json --identity seed
./bin/ncli publish -e signed.json -s ws://localhost:5500
```

For read-path checks (`dump`/`find` against `.db` files), you can also
sync events down into a local store via `apply` with a `stream` spec.

## Gotchas learned

Expand Down
31 changes: 21 additions & 10 deletions CONTRIBUTING.md
Original file line number Diff line number Diff line change
Expand Up @@ -23,16 +23,27 @@ See the [README](README.md) for the full `just` command list.
- Keep changes focused; unrelated formatting/refactors make review harder.
- Add or update tests for behavior changes.

`just test`/`just check` skip integration tests that hit live relays instead
of using mocks — `TestMultiRelaySync`/`TestNegSync_Integration` (public
Nostr relays) and the `cli/bunker` `TestLive_*` suite (`relay.ohstr.com`,
behind an `integration` build tag). Run them all with `just test-integration`
when working on relay sync, negentropy, or bunker/NIP-46 code; they're
excluded from `just check` and CI because their outcome depends on
third-party relay availability. `TestMultiRelaySync` in particular can
still fail against live relays even when connectivity is fine, since the
public firehose it samples sometimes includes spam events with dishonest
NIP-13 nonce tags that get correctly rejected — that's not a code bug.
One suite still hits a live relay: `cli/bunker`'s `TestLive_*`
(`relay.ohstr.com`, behind an `integration` build tag). Run it with `just
test-integration` when working on bunker/NIP-46 code. It's excluded from
`just check` and CI because its outcome depends on third-party relay
availability, and it skips itself when that relay is unreachable.

Everything else is hermetic. Streaming and fan-in are covered by
`TestStreamIntegration`'s real relay containers, and negentropy by
`TestSyncIntegration`'s `NegentropyPropagatesBetweenRelayInstances`, which
runs the same relay config twice and moves a known event set from one
instance to the other. `TestMultiRelaySync` used to stream from public
relays; it was removed as redundant with the container-based coverage and
unreliable against a live firehose.

Tests needing a realistic corpus should use `testdata/events.json` (339
signed events across 10 kinds) rather than fetching from a relay — see
`testdata/README.md`.

`integration/agent-eval` is a separate, manual harness: every round is a
real, billed Claude Code session. It is never run by `just check` or CI —
see its README before invoking it.

## Reporting issues

Expand Down
85 changes: 85 additions & 0 deletions client/fixtures_test.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,85 @@
package client

import (
"encoding/json"
"os"
"testing"

"github.com/ohstr/nmilat/nip01"
)

// fixtureEventsPath holds a mixed-kind set of real, signed events -- see
// testdata/README.md -- so a test can work against a realistic corpus
// without reaching a relay.
const fixtureEventsPath = "../testdata/events.json"

// fixtureEventKinds is the fixture's exact composition. Asserted so a
// regenerated fixture that silently drops a kind fails here rather than
// quietly weakening whatever test relies on that kind being present.
var fixtureEventKinds = map[int]int{
0: 40, // metadata -- ncli profile
1: 100, // text notes
3: 5, // contact lists -- following count
6: 30, // reposts
7: 40, // reactions
1984: 20, // reports
9735: 24, // zap receipts
10002: 30, // relay lists (NIP-65)
10063: 30, // blossom servers (BUD-03)
30023: 20, // long-form articles
}

func loadFixtureEvents(t *testing.T) []*nip01.Event {
t.Helper()
raw, err := os.ReadFile(fixtureEventsPath)
if err != nil {
t.Fatalf("failed to read %s: %v", fixtureEventsPath, err)
}
var events []*nip01.Event
if err := json.Unmarshal(raw, &events); err != nil {
t.Fatalf("failed to parse %s: %v", fixtureEventsPath, err)
}
return events
}

// TestFixtureEventsAreVerifiable guards the checked-in fixture: every event
// must still parse and cryptographically verify, so a corrupted or
// hand-edited events.json fails here rather than surfacing as a confusing
// failure in whichever test consumes it. No network -- Verify covers
// format, schnorr signature, the id-to-content binding, and NIP-13 for the
// few events that carry a nonce tag.
func TestFixtureEventsAreVerifiable(t *testing.T) {
events := loadFixtureEvents(t)

want := 0
for _, n := range fixtureEventKinds {
want += n
}
if len(events) != want {
t.Fatalf("fixture holds %d events, want %d", len(events), want)
}

seen := make(map[string]bool, len(events))
gotKinds := make(map[int]int, len(fixtureEventKinds))
for i, ev := range events {
gotKinds[ev.Kind]++
if seen[ev.ID] {
t.Errorf("event %d: duplicate id %s", i, ev.ID[:8])
}
seen[ev.ID] = true
if err := ev.Verify(); err != nil {
t.Errorf("event %d (kind %d, %s) failed verification: %v", i, ev.Kind, ev.ID[:8], err)
}
}

for kind, wantN := range fixtureEventKinds {
if gotKinds[kind] != wantN {
t.Errorf("kind %d: fixture has %d event(s), want %d", kind, gotKinds[kind], wantN)
}
}
for kind, gotN := range gotKinds {
if _, ok := fixtureEventKinds[kind]; !ok {
t.Errorf("kind %d: %d unexpected event(s) not in the documented mix", kind, gotN)
}
}
}
Loading
Loading