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
* 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>
154 lines
6.3 KiB
C++
154 lines
6.3 KiB
C++
// Catch2 tests for Query Qualifiers
|
|
// Migrated from GTest: query_qualifiers_test.cpp
|
|
|
|
#include <catch2/catch_test_macros.hpp>
|
|
|
|
#include <string>
|
|
#include <vector>
|
|
#include <yams/search/query_qualifiers.hpp>
|
|
|
|
using yams::search::ExtractScope;
|
|
using yams::search::ExtractScopeType;
|
|
using yams::search::ParsedQuery;
|
|
using yams::search::parseQueryQualifiers;
|
|
|
|
TEST_CASE("Query Qualifiers - ParsesLinesRangeAndNormalizesQuery", "[search][qualifiers][catch2]") {
|
|
const std::string q = "find error lines:1-50 in logs";
|
|
ParsedQuery parsed = parseQueryQualifiers(q);
|
|
|
|
CHECK(parsed.scope.type == ExtractScopeType::Lines);
|
|
CHECK(parsed.scope.range == "1-50");
|
|
CHECK(parsed.normalizedQuery == "find error in logs");
|
|
}
|
|
|
|
TEST_CASE("Query Qualifiers - ParsesPagesRangeAndNormalizesQuery", "[search][qualifiers][catch2]") {
|
|
const std::string q = "equation pages:10-12";
|
|
ParsedQuery parsed = parseQueryQualifiers(q);
|
|
|
|
CHECK(parsed.scope.type == ExtractScopeType::Pages);
|
|
CHECK(parsed.scope.range == "10-12");
|
|
CHECK(parsed.normalizedQuery == "equation");
|
|
}
|
|
|
|
TEST_CASE("Query Qualifiers - ParsesSectionWithDoubleQuotes", "[search][qualifiers][catch2]") {
|
|
const std::string q = R"(background section:"Chapter 2" details)";
|
|
ParsedQuery parsed = parseQueryQualifiers(q);
|
|
|
|
CHECK(parsed.scope.type == ExtractScopeType::Section);
|
|
CHECK(parsed.scope.section == "Chapter 2");
|
|
CHECK(parsed.normalizedQuery == "background details");
|
|
}
|
|
|
|
TEST_CASE("Query Qualifiers - ParsesSectionWithSingleQuotes", "[search][qualifiers][catch2]") {
|
|
const std::string q = "intro section:'Background' methods";
|
|
ParsedQuery parsed = parseQueryQualifiers(q);
|
|
|
|
CHECK(parsed.scope.type == ExtractScopeType::Section);
|
|
CHECK(parsed.scope.section == "Background");
|
|
CHECK(parsed.normalizedQuery == "intro methods");
|
|
}
|
|
|
|
TEST_CASE("Query Qualifiers - ParsesSelectorWithSpacesAfterColon", "[search][qualifiers][catch2]") {
|
|
const std::string q = R"(title selector: "div.main" content)";
|
|
ParsedQuery parsed = parseQueryQualifiers(q);
|
|
|
|
CHECK(parsed.scope.type == ExtractScopeType::Selector);
|
|
CHECK(parsed.scope.selector == "div.main");
|
|
CHECK(parsed.normalizedQuery == "title content");
|
|
}
|
|
|
|
TEST_CASE("Query Qualifiers - ParsesFiltersNameExtMime", "[search][qualifiers][catch2]") {
|
|
const std::string q = R"(refactor name:main.cpp ext:.cpp mime:text/x-c++ source code)";
|
|
ParsedQuery parsed = parseQueryQualifiers(q);
|
|
|
|
CHECK(parsed.scope.type == ExtractScopeType::All);
|
|
CHECK(parsed.scope.name == "main.cpp");
|
|
CHECK(parsed.scope.ext == ".cpp");
|
|
CHECK(parsed.scope.mime == "text/x-c++");
|
|
CHECK(parsed.normalizedQuery == "refactor source code");
|
|
}
|
|
|
|
TEST_CASE("Query Qualifiers - MixedQualifiersAndTextNormalization",
|
|
"[search][qualifiers][catch2]") {
|
|
const std::string q = R"(init lines:1-50 name:main.cpp ext:.cpp mime:text/x-c++ src project)";
|
|
ParsedQuery parsed = parseQueryQualifiers(q);
|
|
|
|
CHECK(parsed.scope.type == ExtractScopeType::Lines);
|
|
CHECK(parsed.scope.range == "1-50");
|
|
CHECK(parsed.scope.name == "main.cpp");
|
|
CHECK(parsed.scope.ext == ".cpp");
|
|
CHECK(parsed.scope.mime == "text/x-c++");
|
|
CHECK(parsed.normalizedQuery == "init src project");
|
|
}
|
|
|
|
TEST_CASE("Query Qualifiers - CaseInsensitiveKeysAreAccepted", "[search][qualifiers][catch2]") {
|
|
const std::string q = R"(topic Lines:5-7 Section:"Intro" Selector:"div#id")";
|
|
ParsedQuery parsed = parseQueryQualifiers(q);
|
|
|
|
CHECK(parsed.scope.type == ExtractScopeType::Selector);
|
|
CHECK(parsed.scope.range == "5-7");
|
|
CHECK(parsed.scope.section == "Intro");
|
|
CHECK(parsed.scope.selector == "div#id");
|
|
CHECK(parsed.normalizedQuery == "topic");
|
|
}
|
|
|
|
TEST_CASE("Query Qualifiers - OnlyQualifiersResultsInEmptyNormalizedQuery",
|
|
"[search][qualifiers][catch2]") {
|
|
const std::string q = R"(lines:1-3 pages:2 section:"Body")";
|
|
ParsedQuery parsed = parseQueryQualifiers(q);
|
|
|
|
CHECK(parsed.normalizedQuery.empty());
|
|
CHECK(parsed.scope.type == ExtractScopeType::Section);
|
|
CHECK(parsed.scope.range == "2");
|
|
CHECK(parsed.scope.section == "Body");
|
|
}
|
|
|
|
TEST_CASE("Query Qualifiers - MultipleScopesLastOneWinsForType", "[search][qualifiers][catch2]") {
|
|
const std::string q = R"(foo lines:1-10 pages:3-4 selector:"h1.title" bar)";
|
|
ParsedQuery parsed = parseQueryQualifiers(q);
|
|
|
|
CHECK(parsed.scope.type == ExtractScopeType::Selector);
|
|
CHECK(parsed.scope.selector == "h1.title");
|
|
CHECK(parsed.scope.range == "3-4");
|
|
CHECK(parsed.normalizedQuery == "foo bar");
|
|
}
|
|
|
|
// ---------------------------------------------------------------------------
|
|
// Structured metadata (key=value) extraction shared by SearchService (keyword path) and
|
|
// SearchEngine's metadata component.
|
|
// ---------------------------------------------------------------------------
|
|
|
|
using yams::search::extractStructuredMetadataQuery;
|
|
|
|
TEST_CASE("Structured metadata - splits key=value tokens from the residual query",
|
|
"[search][qualifiers][metadata][catch2]") {
|
|
auto parsed = extractStructuredMetadataQuery(" task=alpha report phase=checkpoint ");
|
|
REQUIRE(parsed.filters.size() == 2);
|
|
CHECK(parsed.filters[0].first == "phase");
|
|
CHECK(parsed.filters[0].second == "checkpoint");
|
|
CHECK(parsed.filters[1].first == "task");
|
|
CHECK(parsed.filters[1].second == "alpha");
|
|
CHECK(parsed.residualQuery == "report");
|
|
}
|
|
|
|
TEST_CASE("Structured metadata - duplicate keys keep the last value",
|
|
"[search][qualifiers][metadata][catch2]") {
|
|
auto parsed = extractStructuredMetadataQuery("task=one task=two");
|
|
REQUIRE(parsed.filters.size() == 1);
|
|
CHECK(parsed.filters[0].second == "two");
|
|
CHECK(parsed.residualQuery.empty());
|
|
}
|
|
|
|
TEST_CASE("Structured metadata - malformed or quoted tokens stay in the query",
|
|
"[search][qualifiers][metadata][catch2]") {
|
|
auto parsed = extractStructuredMetadataQuery("=x key= a=b=c name=\"quoted\" k[0]=v plain");
|
|
CHECK(parsed.filters.empty());
|
|
CHECK(parsed.residualQuery == "=x key= a=b=c name=\"quoted\" k[0]=v plain");
|
|
}
|
|
|
|
TEST_CASE("Structured metadata - filters only leaves an empty residual query",
|
|
"[search][qualifiers][metadata][catch2]") {
|
|
auto parsed = extractStructuredMetadataQuery("owner=codex source=decision");
|
|
REQUIRE(parsed.filters.size() == 2);
|
|
CHECK(parsed.residualQuery.empty());
|
|
}
|