Skip to content
Open
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
8 changes: 8 additions & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -30,6 +30,14 @@

- **`HYDRADB_GRAPH_COLLECTION`** sets the default graph collection (default `default`). It deliberately does not fall back to `HYDRADB_COLLECTION`: a context collection names a memory/knowledge partition and means nothing to a graph, so inheriting it would silently point Cypher at a collection you never chose.

### Fixed

- **`ingest` rejects an unknown `--kind`** instead of storing the text as a memory.
Only `knowledge` was matched, so `--kind knowlegde`, `--kind Knowledge` or an empty
`--kind ""` fell through to the memory path, sent `type=memory`, and printed
`✓ Memory added`. It now fails with the same `--kind must be one of: knowledge, memory`
error that `query`, `list` and `delete` already give.

### Internal

- `HydraDB.graph` is a hand-rolled `httpx` path rather than an SDK call: the pinned `hydradb-sdk==2.1.2` exposes `context`, `databases`, `connectors` and `webhooks` and has no `byog` resource, so those endpoints are unreachable through it. It reuses the wrapper's existing envelope unwrapping and error translation and raises the same `HydraDBClientError`, so `handle_api_error` treats a BYOG failure exactly like an SDK one. When the SDK grows a `byog` resource, that one class is reimplemented over it and no caller changes. The exact SDK pin (CONTRACT S2 rule 1) is unaffected — there is no generated name to be insulated from yet.
Expand Down
7 changes: 7 additions & 0 deletions src/hydradb_cli/commands/canonical.py
Original file line number Diff line number Diff line change
Expand Up @@ -144,6 +144,13 @@ def ingest(
) -> None:
"""Ingest a memory, knowledge text, or knowledge file(s)."""
tid, stid = resolve_scope_flags(database, collection, tenant_id, sub_tenant_id)
# Only `knowledge` is matched below; anything else used to fall through to
# the memory path, so a typo like `--kind knowlegde` stored the text as a
# memory and reported success. Reject it up front, the way delete does.
# `is not None`, not truthiness: `--kind ""` (an unset shell variable) must
# be rejected too, and only omitting the flag keeps the memory default.
if kind is not None and kind not in _impl.VALID_KINDS:
print_error(f"--kind must be one of: {', '.join(sorted(_impl.VALID_KINDS))}. Got '{kind}'.")
if files:
# Files are always knowledge sources. Reject every option that would be
# silently ignored rather than storing the file the wrong way. Only
Expand Down
33 changes: 33 additions & 0 deletions tests/test_cli_commands.py
Original file line number Diff line number Diff line change
Expand Up @@ -233,6 +233,39 @@ def test_ingest_knowledge_text(self):
assert "uploaded" in result.output.lower()
assert w.context.ingest.call_args.kwargs["kind"] == "knowledge"

def test_ingest_unknown_kind_fails(self):
# Anything but "knowledge" used to fall through to the memory path, so
# a typo stored the text in the wrong corpus and reported success.
_auth()
w = _wrapper()
with _patch_wrapper(w):
result = runner.invoke(app, ["ingest", "--kind", "knowlegde", "--text", "notes"])
assert result.exit_code != 0
assert "--kind must be one of" in result.output
w.context.ingest.assert_not_called()

def test_ingest_empty_kind_fails(self):
# An explicit empty value (an unset shell variable) is not the same as
# omitting the flag; it must not default to memory.
_auth()
w = _wrapper()
with _patch_wrapper(w):
result = runner.invoke(app, ["ingest", "--kind", "", "--text", "notes"])
assert result.exit_code != 0
assert "--kind must be one of" in result.output
w.context.ingest.assert_not_called()

def test_ingest_files_with_unknown_kind_fails(self, tmp_path):
_auth()
f = tmp_path / "a.txt"
f.write_text("aaa")
w = _wrapper()
with _patch_wrapper(w):
result = runner.invoke(app, ["ingest", str(f), "--kind", "knowlegde"])
assert result.exit_code != 0
assert "--kind must be one of" in result.output
w.context.ingest_many.assert_not_called()

def test_ingest_files_loops(self, tmp_path):
_auth()
f1 = tmp_path / "a.txt"
Expand Down