Skip to content

Commit 48a8679

Browse files
committed
Fix CLI to accept positional URI arguments and support init command
- Remove Required:true from URI flags in schemas, records, namespaces, watch, import/export commands to allow positional argument parsing via getURI() - Skip RPC connection in root Before hook for init and daemon commands since they manage configuration before the daemon is running - Add ArgsUsage hints to schema commands for better help text - Add Before hook to init command to prevent connection attempt during initialization
1 parent 2b9b6a4 commit 48a8679

11 files changed

Lines changed: 405 additions & 19 deletions

File tree

.claude/commands/xdb-e2e.md

Lines changed: 47 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,47 @@
1+
---
2+
description: Run the xdb agent-facing e2e suite in sandboxed temp daemons via parallel sub-agents.
3+
argument-hint: "[scenario-name | scenario,scenario]"
4+
---
5+
6+
Run the xdb end-to-end suite. `$ARGUMENTS` (may be empty) is an optional scenario filter.
7+
8+
## Hard rules — for you AND every sub-agent you spawn
9+
10+
1. **No edits to anything in the repo.** Not source, not `scenarios.yaml`, not `RUNBOOK.md`, not commit history. The only mutable state is `$T=$(mktemp -d)` per sub-agent.
11+
2. **Sub-agents are black-box runners.** If a scenario fails because the CLI is broken, that is the *finding*. Do not patch the code. Do not modify the YAML to make the test pass.
12+
3. **You (the orchestrator) do not patch either.** If multiple sub-agents report the same failure, surface it once and stop. Do not spawn a "fixer" agent.
13+
4. **Build is the only allowed write.** `make build` (or `go build -o bin/xdb ./cmd/xdb` from the cmd module if `make build` doesn't produce a binary). Nothing else.
14+
5. **Do not skip [tests/e2e/RUNBOOK.md](tests/e2e/RUNBOOK.md).** It contains the same hard rules in agent-facing form. Spawn sub-agents with it as the prompt verbatim — do not paraphrase or shorten.
15+
16+
## Steps
17+
18+
1. **Build if missing.** If `bin/xdb` does not exist, build it. If it exists, skip and tell the user.
19+
20+
2. **Spawn one sub-agent per scenario, in parallel** (one Agent tool call per scenario, all in a single message). Each agent owns its own `HOME=$T` so daemons cannot collide.
21+
22+
Read `tests/e2e/scenarios.yaml` first to enumerate scenario names. For each scenario matching `$ARGUMENTS` (empty = all):
23+
- `subagent_type: "general-purpose"`
24+
- `model: "haiku"` — runner is mechanical: read YAML, exec bash, diff outputs.
25+
- `description: "xdb e2e: <scenario-name>"`
26+
- `prompt`: the **full contents** of `tests/e2e/RUNBOOK.md` followed by:
27+
```
28+
Repo root: <absolute path>
29+
xdb binary: <absolute path>/bin/xdb
30+
Scenario filter: <single-scenario-name>
31+
32+
REMINDER: black-box runner only. No edits to any file in the repo. CLI bugs are FAILs to report, not problems to solve. Violating this is itself a test failure.
33+
```
34+
35+
3. **Single-scenario invocation.** If `$ARGUMENTS` names exactly one scenario, spawn just that one sub-agent (no fan-out).
36+
37+
4. **Cleanup verification.** After all sub-agents finish:
38+
- Run `git status --short`. If anything beyond untracked temp files appears (especially under `cmd/`, `api/`, `rpc/`, `core/`, `store/`, `tests/e2e/scenarios.yaml`, `tests/e2e/RUNBOOK.md`), a sub-agent overstepped — report which files and stop. **Do not auto-revert** without telling the user.
39+
- Run `pgrep -f "/tmp.*xdb.*daemon\|xdb-test.*daemon"`. If any stray daemon is still running, list it and ask the user before killing.
40+
41+
5. **Report.** Merge all sub-agent reports into one markdown table. Print only:
42+
- the table
43+
- the summary line (e.g. `8 scenarios: 5 PASS, 1 SKIP, 2 FAIL`)
44+
- any cleanup-verification anomalies from step 4
45+
- final `**suite passed**` or `**suite failed**`
46+
47+
Suppress build chatter and per-agent reasoning. Do not propose fixes for failing scenarios in this message — that's a separate task the user can request.

cmd/xdb/cli/app.go

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -75,6 +75,11 @@ func NewApp() *cli.Command {
7575
},
7676
},
7777
Before: func(ctx context.Context, cmd *cli.Command) (context.Context, error) {
78+
// Skip connection for commands that manage configuration or daemon
79+
cmdName := cmd.Name
80+
if cmdName == "init" || cmdName == "daemon" {
81+
return ctx, nil
82+
}
7883
return ctx, a.connect(cmd)
7984
},
8085
ExitErrHandler: func(_ context.Context, cmd *cli.Command, err error) {

cmd/xdb/cli/config.go

Lines changed: 5 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -230,17 +230,17 @@ func EnsureConfigAt(configPath string) (bool, error) {
230230
func LoadConfig(configPath string) (*Config, error) {
231231
if configPath == "" {
232232
configPath = DefaultConfigPath()
233-
234-
if _, err := EnsureConfigAt(configPath); err != nil {
235-
return nil, err
236-
}
237233
}
238234

239235
configPath = expandTilde(configPath)
240236

237+
if _, err := EnsureConfigAt(configPath); err != nil {
238+
return nil, fmt.Errorf("ensure config: %w", err)
239+
}
240+
241241
data, err := os.ReadFile(configPath) // #nosec G304 - configPath is from trusted CLI flag or hardcoded default
242242
if err != nil {
243-
return nil, fmt.Errorf("read config: %w", err)
243+
return nil, fmt.Errorf("read config (at %s): %w", configPath, err)
244244
}
245245

246246
cfg := NewDefaultConfig()

cmd/xdb/cli/import_export.go

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -21,7 +21,7 @@ func (a *App) importCmd() *cli.Command {
2121
Category: "operations",
2222
CustomHelpTemplate: commandHelpTemplate,
2323
Flags: []cli.Flag{
24-
&cli.StringFlag{Name: "uri", Usage: "Target schema URI", Required: true},
24+
&cli.StringFlag{Name: "uri", Usage: "Target schema URI"},
2525
&cli.StringFlag{Name: "file", Aliases: []string{"f"}, Usage: "Path to NDJSON file"},
2626
&cli.BoolFlag{Name: "create-only", Usage: "Use create instead of upsert"},
2727
},
@@ -36,7 +36,7 @@ func (a *App) exportCmd() *cli.Command {
3636
Category: "operations",
3737
CustomHelpTemplate: commandHelpTemplate,
3838
Flags: []cli.Flag{
39-
&cli.StringFlag{Name: "uri", Usage: "Schema URI", Required: true},
39+
&cli.StringFlag{Name: "uri", Usage: "Schema URI"},
4040
&cli.StringFlag{Name: "fields", Usage: "Comma-separated field mask"},
4141
&cli.StringFlag{Name: "output", Aliases: []string{"o"}, Usage: "Output format"},
4242
},

cmd/xdb/cli/init.go

Lines changed: 5 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -14,7 +14,11 @@ func initCmd() *cli.Command {
1414
Usage: "Initialize XDB and start the daemon",
1515
Category: "system",
1616
CustomHelpTemplate: commandHelpTemplate,
17-
Action: initAction,
17+
Before: func(ctx context.Context, _ *cli.Command) (context.Context, error) {
18+
// init creates the config file, so it doesn't need to connect to the daemon first
19+
return ctx, nil
20+
},
21+
Action: initAction,
1822
}
1923
}
2024

cmd/xdb/cli/namespaces.go

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -31,7 +31,7 @@ func (a *App) namespacesCmd() *cli.Command {
3131
Usage: "Get namespace details",
3232
CustomHelpTemplate: commandHelpTemplate,
3333
Flags: []cli.Flag{
34-
&cli.StringFlag{Name: "uri", Usage: "Namespace URI", Required: true},
34+
&cli.StringFlag{Name: "uri", Usage: "Namespace URI"},
3535
&cli.StringFlag{Name: "output", Aliases: []string{"o"}, Usage: "Output format"},
3636
},
3737
Action: a.namespaceGet,

cmd/xdb/cli/records.go

Lines changed: 15 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -21,41 +21,47 @@ func (a *App) recordsCmd() *cli.Command {
2121
Name: "create",
2222
Usage: "Create a new record (idempotent)",
2323
CustomHelpTemplate: commandHelpTemplate,
24+
ArgsUsage: "[URI]",
2425
Flags: recordMutationFlags(),
2526
Action: a.recordCreate,
2627
},
2728
{
2829
Name: "get",
2930
Usage: "Retrieve a record by URI",
3031
CustomHelpTemplate: commandHelpTemplate,
32+
ArgsUsage: "[URI]",
3133
Flags: recordReadFlags(),
3234
Action: a.recordGet,
3335
},
3436
{
3537
Name: "list",
3638
Usage: "List records in a schema",
3739
CustomHelpTemplate: commandHelpTemplate,
40+
ArgsUsage: "[SCHEMA_URI]",
3841
Flags: recordListFlags(),
3942
Action: a.recordList,
4043
},
4144
{
4245
Name: "update",
4346
Usage: "Update a record (patch semantics)",
4447
CustomHelpTemplate: commandHelpTemplate,
48+
ArgsUsage: "[URI]",
4549
Flags: recordMutationFlags(),
4650
Action: a.recordUpdate,
4751
},
4852
{
4953
Name: "upsert",
5054
Usage: "Create or replace a record",
5155
CustomHelpTemplate: commandHelpTemplate,
56+
ArgsUsage: "[URI]",
5257
Flags: recordMutationFlags(),
5358
Action: a.recordUpsert,
5459
},
5560
{
5661
Name: "delete",
5762
Usage: "Delete a record (idempotent, requires --force)",
5863
CustomHelpTemplate: commandHelpTemplate,
64+
ArgsUsage: "[URI]",
5965
Flags: recordDeleteFlags(),
6066
Action: a.recordDelete,
6167
},
@@ -198,6 +204,10 @@ func (a *App) recordDelete(ctx context.Context, cmd *cli.Command) error {
198204
return invalidArgError("records", "delete", err)
199205
}
200206

207+
if !cmd.Bool("force") {
208+
return invalidArgError("records", "delete", fmt.Errorf("delete requires --force to confirm"))
209+
}
210+
201211
if err := a.client.Call(ctx, "records.delete", &api.DeleteRecordRequest{
202212
URI: uri,
203213
}, nil); err != nil {
@@ -216,7 +226,7 @@ func (a *App) recordDelete(ctx context.Context, cmd *cli.Command) error {
216226

217227
func recordMutationFlags() []cli.Flag {
218228
return []cli.Flag{
219-
&cli.StringFlag{Name: "uri", Usage: "Record URI", Required: true},
229+
&cli.StringFlag{Name: "uri", Usage: "Record URI"},
220230
&cli.StringFlag{Name: "json", Usage: "Inline JSON payload"},
221231
&cli.StringFlag{Name: "file", Aliases: []string{"f"}, Usage: "Path to input file"},
222232
&cli.BoolFlag{Name: "dry-run", Usage: "Validate without writing"},
@@ -227,15 +237,15 @@ func recordMutationFlags() []cli.Flag {
227237

228238
func recordReadFlags() []cli.Flag {
229239
return []cli.Flag{
230-
&cli.StringFlag{Name: "uri", Usage: "Record URI", Required: true},
240+
&cli.StringFlag{Name: "uri", Usage: "Record URI"},
231241
&cli.StringFlag{Name: "fields", Usage: "Comma-separated field mask"},
232242
&cli.StringFlag{Name: "output", Aliases: []string{"o"}, Usage: "Output format"},
233243
}
234244
}
235245

236246
func recordListFlags() []cli.Flag {
237247
return []cli.Flag{
238-
&cli.StringFlag{Name: "uri", Usage: "Schema URI", Required: true},
248+
&cli.StringFlag{Name: "uri", Usage: "Schema URI"},
239249
&cli.StringFlag{Name: "filter", Usage: "Human-friendly filter expression"},
240250
&cli.StringFlag{Name: "query", Usage: "Structured JSON query"},
241251
&cli.StringFlag{Name: "fields", Usage: "Comma-separated field mask"},
@@ -248,8 +258,8 @@ func recordListFlags() []cli.Flag {
248258

249259
func recordDeleteFlags() []cli.Flag {
250260
return []cli.Flag{
251-
&cli.StringFlag{Name: "uri", Usage: "Record URI", Required: true},
252-
&cli.BoolFlag{Name: "force", Usage: "Confirm deletion", Required: true},
261+
&cli.StringFlag{Name: "uri", Usage: "Record URI"},
262+
&cli.BoolFlag{Name: "force", Usage: "Confirm deletion"},
253263
&cli.BoolFlag{Name: "dry-run", Usage: "Validate without deleting"},
254264
&cli.StringFlag{Name: "output", Aliases: []string{"o"}, Usage: "Output format"},
255265
&cli.BoolFlag{Name: "quiet", Usage: "Suppress output"},

cmd/xdb/cli/schemas.go

Lines changed: 9 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -20,34 +20,39 @@ func (a *App) schemasCmd() *cli.Command {
2020
Name: "create",
2121
Usage: "Create a new schema (idempotent)",
2222
CustomHelpTemplate: commandHelpTemplate,
23+
ArgsUsage: "[URI]",
2324
Flags: schemaMutationFlags(),
2425
Action: a.schemaCreate,
2526
},
2627
{
2728
Name: "get",
2829
Usage: "Retrieve a schema definition",
2930
CustomHelpTemplate: commandHelpTemplate,
31+
ArgsUsage: "[URI]",
3032
Flags: schemaReadFlags(),
3133
Action: a.schemaGet,
3234
},
3335
{
3436
Name: "list",
3537
Usage: "List schemas in a namespace",
3638
CustomHelpTemplate: commandHelpTemplate,
39+
ArgsUsage: "[NAMESPACE_URI]",
3740
Flags: schemaListFlags(),
3841
Action: a.schemaList,
3942
},
4043
{
4144
Name: "update",
4245
Usage: "Update a schema (patch semantics)",
4346
CustomHelpTemplate: commandHelpTemplate,
47+
ArgsUsage: "[URI]",
4448
Flags: schemaMutationFlags(),
4549
Action: a.schemaUpdate,
4650
},
4751
{
4852
Name: "delete",
4953
Usage: "Delete a schema (idempotent, requires --force)",
5054
CustomHelpTemplate: commandHelpTemplate,
55+
ArgsUsage: "[URI]",
5156
Flags: schemaDeleteFlags(),
5257
Action: a.schemaDelete,
5358
},
@@ -165,7 +170,7 @@ func (a *App) schemaDelete(ctx context.Context, cmd *cli.Command) error {
165170

166171
func schemaMutationFlags() []cli.Flag {
167172
return []cli.Flag{
168-
&cli.StringFlag{Name: "uri", Usage: "Schema URI", Required: true},
173+
&cli.StringFlag{Name: "uri", Usage: "Schema URI"},
169174
&cli.StringFlag{Name: "json", Usage: "Inline JSON payload"},
170175
&cli.StringFlag{Name: "file", Aliases: []string{"f"}, Usage: "Path to input file"},
171176
&cli.BoolFlag{Name: "dry-run", Usage: "Validate without writing"},
@@ -175,7 +180,7 @@ func schemaMutationFlags() []cli.Flag {
175180

176181
func schemaReadFlags() []cli.Flag {
177182
return []cli.Flag{
178-
&cli.StringFlag{Name: "uri", Usage: "Schema URI", Required: true},
183+
&cli.StringFlag{Name: "uri", Usage: "Schema URI"},
179184
&cli.StringFlag{Name: "output", Aliases: []string{"o"}, Usage: "Output format"},
180185
}
181186
}
@@ -191,8 +196,8 @@ func schemaListFlags() []cli.Flag {
191196

192197
func schemaDeleteFlags() []cli.Flag {
193198
return []cli.Flag{
194-
&cli.StringFlag{Name: "uri", Usage: "Schema URI", Required: true},
195-
&cli.BoolFlag{Name: "force", Usage: "Confirm deletion", Required: true},
199+
&cli.StringFlag{Name: "uri", Usage: "Schema URI"},
200+
&cli.BoolFlag{Name: "force", Usage: "Confirm deletion"},
196201
&cli.BoolFlag{Name: "cascade", Usage: "Delete schema and all records"},
197202
&cli.BoolFlag{Name: "dry-run", Usage: "Validate without deleting"},
198203
&cli.StringFlag{Name: "output", Aliases: []string{"o"}, Usage: "Output format"},

cmd/xdb/cli/watch.go

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -14,7 +14,7 @@ func watchCmd() *cli.Command {
1414
Category: "operations",
1515
CustomHelpTemplate: commandHelpTemplate,
1616
Flags: []cli.Flag{
17-
&cli.StringFlag{Name: "uri", Usage: "URI to watch", Required: true},
17+
&cli.StringFlag{Name: "uri", Usage: "URI to watch"},
1818
&cli.StringFlag{Name: "output", Aliases: []string{"o"}, Usage: "Output format"},
1919
},
2020
Action: watchAction,

tests/e2e/RUNBOOK.md

Lines changed: 68 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,68 @@
1+
# XDB E2E Runner
2+
3+
You are running the xdb end-to-end suite. **You are a black-box test runner. Your only job is to execute the spec and report what happened — never to fix what you find.**
4+
5+
## Hard rules (violating any of these is itself a test failure)
6+
7+
1. **No file edits anywhere in the repo.** Not source code, not `scenarios.yaml`, not even comments. Tools allowed: `Bash`, `Read`, `Grep`, `Glob`. **Do not** call `Edit`, `Write`, or any tool that mutates the working tree.
8+
2. **No `git` mutations.** No commits, checkouts, stashes, resets. `git status` / `git diff` for diagnostics only.
9+
3. **If a step fails because the CLI itself is broken** (wrong flag accepted, missing command, wrong error code), that is a **scenario FAIL**, not a problem for you to solve. Record it and move on.
10+
4. **If you cannot even start** (binary missing, `xdb init` exits non-zero, daemon won't bind), abort the entire run and emit `SUITE_FAILED` with the startup error in the report. Do not try to repair the binary, the config, or the daemon.
11+
5. **All mutable state lives under `$T` (your `mktemp -d`).** Never write outside it.
12+
13+
If you are tempted to "just fix this small thing to make the test pass" — stop. The test failing is the whole point.
14+
15+
All state lives in a temp dir you control. Use only Bash, Read, Grep, Glob.
16+
17+
## Inputs
18+
- `tests/e2e/scenarios.yaml` — the spec (read it).
19+
- Optional scenario filter passed in the prompt: a single `name`, a comma-separated list, or empty (run all).
20+
- Skip any scenario with `skip_if_unimplemented: true` and report it as SKIP.
21+
22+
## Setup (once)
23+
1. Create temp root: `T=$(mktemp -d)` and `mkdir -p "$T/home"`.
24+
2. Export an isolated env for every command you run:
25+
```
26+
HOME="$T/home"
27+
PATH="<repo>/bin:$PATH" # so `xdb` resolves to the freshly-built binary
28+
```
29+
3. Run `xdb init`. It must exit 0 and start the daemon. If exit ≠ 0, abort the run, print the stderr, and report.
30+
4. Verify with `xdb daemon status` (exit 0).
31+
5. Capture `NOW=$(date -u +%Y-%m-%dT%H:%M:%SZ)`.
32+
33+
## Per scenario (run sequentially)
34+
1. Generate a unique namespace: `NS="e2e-<scenario-slug>-$(openssl rand -hex 2)"`.
35+
2. For each `step` in order:
36+
- Substitute `$NS` and `$NOW` in `run`.
37+
- Execute via Bash with the isolated env from setup. Capture stdout, stderr, exit code.
38+
- Apply each assertion in `expect`:
39+
- `exit: N` — exit code must equal N.
40+
- `exit_nonzero: true` — exit code must be ≠ 0.
41+
- `stdout_contains: S` — S must appear in stdout.
42+
- `stderr_contains: S` — S must appear in stderr.
43+
- `stdout_empty: true` — stdout must be empty (whitespace-only counts as empty).
44+
- `json: {...}` — parse stdout as JSON; every key/value in the assertion must be present (deep partial match).
45+
- `ndjson_count: N` — stdout must contain exactly N non-empty lines, each parseable as JSON.
46+
- `ndjson_ids: [...]` — the set of `_id` values across NDJSON lines must equal this set (unordered).
47+
- `error: {...}` — parse stdout (or stderr if stdout is empty) as JSON envelope; partial-match on `code`, `resource`, `action`.
48+
- On the **first failing assertion** in a scenario: mark the scenario FAIL with `{step name, assertion, expected, actual (truncated to 400 chars)}`. Stop that scenario, move to the next.
49+
50+
## Teardown (always, even on failure or panic)
51+
1. `xdb daemon stop` (best effort — ignore errors).
52+
2. `rm -rf "$T"`.
53+
3. Confirm no `xdb` daemon process is still bound to `$T` (best effort `pgrep -af xdb` filtered to your temp path).
54+
55+
## Final report
56+
Output **only** a markdown table and a one-line summary. No commentary, no per-step logs unless something failed.
57+
58+
```
59+
| Scenario | Status | Failing step | Reason |
60+
|--------------------------------|--------|-----------------------|---------------------------------|
61+
| blog-publishing-flow | PASS | | |
62+
| validation-rejects-bad-types | FAIL | qty as a string ... | error.code: want SCHEMA_VIOLATION, got INTERNAL |
63+
| bulk-import-via-stdin | SKIP | (skip_if_unimplemented)| |
64+
65+
8 scenarios: 6 PASS, 1 FAIL, 1 SKIP
66+
```
67+
68+
If any scenario is FAIL, end your message with the literal line `SUITE_FAILED`. Otherwise end with `SUITE_PASSED`. The parent uses these markers to decide overall result.

0 commit comments

Comments
 (0)