yams/tests/unit/common/hash_predicates_catch2_test.cpp
Trevon 60855b7b78
Some checks failed
Secret scan / gitleaks (push) Failing after 12s
Mirror source to Forgejo / mirror (push) Has been skipped
Tests / Static Analysis (push) Has been cancelled
Tests / Warm Conan & ccache (linux-arm64) (push) Has been cancelled
Tests / Warm Conan & ccache (macos-arm64) (push) Has been cancelled
Tests / Warm Conan & ccache (linux-x86_64) (push) Has been cancelled
Tests / Warm Conan & ccache (macos-x86_64) (push) Has been cancelled
Tests / Warm Conan & ccache (windows-x86_64) (push) Has been cancelled
Tests / warm (push) Has been cancelled
Tests / Tests (linux-arm64) (push) Has been cancelled
Tests / Tests (macos-arm64) (push) Has been cancelled
Tests / Tests (linux-x86_64-asan) (push) Has been cancelled
Tests / Tests (linux-x86_64-ubsan) (push) Has been cancelled
Tests / Tests (linux-x86_64) (push) Has been cancelled
Tests / Tests (windows-x86_64) (push) Has been cancelled
[11/14] Separate status presentation and centralize shared query helpers (#151)
* refactor(daemon): one table for repair operation wire codes

The code published under repair_current_operation_code was defined in
three places: a name->code chain in RepairService.cpp, an enum->name
switch beside it, and a code->label switch in the CLI. The two daemon
tables agreed with each other but the CLI copy had already drifted in
wording, and nothing tied the three together.

metric_keys.h now carries the single code<->name table next to the
metric key it describes, with constexpr lookups in both directions.
RepairService's name->code chain delegates to it; the enum->name
switch stays because it is keyed by the private enum. The CLI copy
goes in the next commit with the status renderer.

Signed-off-by: trvon <me@trevon.dev>

* refactor(cli): extract the daemon status renderer

showStatus() in daemon_command.cpp was 1,762 lines: two RPCs, a
bootstrap-file fallback, and two ~400/1,100-line render bodies that
wrote straight to std::cout from inside the command class, reachable
only through a live daemon. The render bodies read nothing from the
command except the expected data directory, the explicit-socket flag,
and a memory-sync fetch they triggered mid-render.

The bodies now live in src/cli/commands/daemon_status_render.cpp as
free functions over StatusResponse plus a small DaemonStatusRenderContext
(configured data dir, explicit-socket flag, optional MemorySyncResponse)
writing to an ostream. The readiness/label helpers that only they used
(humanizeToken, classifyReadiness, isEphemeralDataDir,
daemonDataDirFromStatus, paintStatus, neutralText) move with them.
DaemonCommand keeps the RPCs and the bootstrap fallback; the memory-sync
fetch becomes fetchMemorySyncStatus() and runs just before rendering
instead of in the middle of it, so the section prints in the same place
with the same bounded 3 s timeout.

The move is verbatim: after the mechanical substitutions
(std::cout -> os, expectedDataDir() -> ctx.configuredDataDir, the
memory-sync call, the explicit-socket flag, and the repair label
function) the two bodies match the originals character for character
modulo whitespace. The CLI's private repair-operation switch is gone;
the label now comes from the shared table in metric_keys.h with
underscores rendered as spaces, which reproduces the previous wording
("stuck docs", "block refs", "path tree").

tests/unit/cli/daemon_status_render_catch2_test.cpp drives both
renderers and the memory-sync section from synthetic responses:
overview rows, blockers with init progress, last error, data-dir
mismatch (and its suppression under --socket), temporary data dir,
the active repair operation label, every detailed section, and the
started/not-started memory-sync gate.

daemon_command.cpp: 4,082 -> 2,341 lines; daemon_status_render.cpp:
1,642 lines.

Signed-off-by: trvon <me@trevon.dev>

* refactor(app): one home for hash predicates, path aliasing, and metadata qualifiers

Three small helpers had grown parallel copies with quietly different
rules:

- "does this look like a hash" existed four times: search_service
  (8-64 hex), document_service (6-63), cat_command (6-64), plus a
  never-defined utils::looksLikeHash declaration in services.hpp. The
  8 vs 6 threshold is deliberate (D3): a bare query token must reach
  8 hex characters before it stops being text, while an explicit hash
  argument may be 6. yams/common/hash_predicates.h now names the two
  predicates apart (looksLikeHashQueryToken, looksLikePartialHashArgument)
  over one isHexDigits; document_service's 6..63 bound was deliberate
  for the name resolver (a full 64-hex name must not resolve as a prefix
  and delete by hash), so its two resolver sites now guard size() != 64
  explicitly, as the partial-hash resolution site already did.

- macOS /private aliasing had three incompatible policies: search
  appended the alias of /var only, grep canonicalized /var and /tmp
  to the /private spelling, and the delete resolver displayed
  /private/var as /var. yams/common/fs_utils.h now carries one alias
  table ({/var,/tmp} <-> /private/...) with three lexical policies over
  it (otherSpelling, canonicalSpelling, displaySpelling) and the
  platform-gated wrappers callers use. Declared behavior change, macOS
  only: search patterns under /tmp now also match their /private/tmp
  spelling, and the resolver displays /private/tmp/... as /tmp/....

- Inline key=value metadata filters were parsed twice: SearchService's
  extractStructuredMetadataQuery (exactly one '=', no quoting/grouping
  characters, deduplicated by key) and SearchEngine's
  parseMetadataFiltersFromQuery (any '=' token, duplicates kept). The
  stricter, tested parser moves into yams/search/query_qualifiers.hpp
  and both call it. Declared behavior change for the hybrid metadata
  component: tokens like name="x" or a=b=c stay in the query instead
  of becoming filters, and repeated keys collapse to the last value.

Tests: hash_predicates_catch2_test.cpp pins both thresholds and the
64-character bound; path_alias_catch2_test.cpp pins the alias table,
the three policies, and the platform gating; query_qualifiers_catch2_test.cpp
gains the structured-metadata cases (split, dedupe, malformed tokens,
filters-only). search_service, document_service, grep_service,
service_utilities, and resolve_submodule pass unchanged. Portability
allowlist anchors reflowed for the four files whose lines moved.

Signed-off-by: trvon <me@trevon.dev>

* fix(cli): flush status before optional collection

Render useful primary status before waiting for memory-sync details.
Keep both brief and detailed paths behind the tested ordering seam.

Signed-off-by: trvon <me@trevon.dev>

---------

Signed-off-by: trvon <me@trevon.dev>
2026-09-12 10:09:21 -04:00

40 lines
1.7 KiB
C++

// The two hash-prefix thresholds are a deliberate product choice (D3): a bare search token
// must reach 8 hex characters before it is treated as a hash, while an explicit hash argument
// (--hash, a resolver target) may be as short as 6. Both live in one header so the numbers
// cannot drift apart again.
#include <catch2/catch_test_macros.hpp>
#include <yams/common/hash_predicates.h>
using yams::common::isHexDigits;
using yams::common::looksLikeHashQueryToken;
using yams::common::looksLikePartialHashArgument;
TEST_CASE("isHexDigits accepts hex in either case and rejects anything else",
"[common][hash][catch2]") {
CHECK(isHexDigits("0123456789abcdefABCDEF"));
CHECK(isHexDigits(""));
CHECK_FALSE(isHexDigits("deadbeeg"));
CHECK_FALSE(isHexDigits("dead beef"));
CHECK_FALSE(isHexDigits("0x1234"));
}
TEST_CASE("query tokens need 8 hex characters before they look like a hash",
"[common][hash][catch2]") {
CHECK_FALSE(looksLikeHashQueryToken("abcdef"));
CHECK_FALSE(looksLikeHashQueryToken("abcdef1"));
CHECK(looksLikeHashQueryToken("abcdef12"));
CHECK(looksLikeHashQueryToken(std::string(64, 'a')));
CHECK_FALSE(looksLikeHashQueryToken(std::string(65, 'a')));
CHECK_FALSE(looksLikeHashQueryToken("abcdefg1"));
}
TEST_CASE("explicit hash arguments may be as short as 6 hex characters", "[common][hash][catch2]") {
CHECK_FALSE(looksLikePartialHashArgument("abcde"));
CHECK(looksLikePartialHashArgument("abcdef"));
CHECK(looksLikePartialHashArgument("abcdef1"));
CHECK(looksLikePartialHashArgument(std::string(64, 'f')));
CHECK_FALSE(looksLikePartialHashArgument(std::string(65, 'f')));
CHECK_FALSE(looksLikePartialHashArgument("abcdez"));
}