yams/tests/unit/daemon/entity_graph_service_catch2_test.cpp
Trevon e4b32e98bd
Some checks failed
Secret scan / gitleaks (push) Failing after 8s
Mirror source to Forgejo / mirror (push) Has been skipped
Release Candidate / Validate promotion and preview release (push) Has been cancelled
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
fix: fence enrichment completion and reconcile promotion prerequisites (#157)
* test(ci): stabilize main matrix (#112)

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

* ci(release): preview experimental promotion (#111)

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

* ci(release): reconcile draft promotion safely (#113)

* ci(release): reconcile draft promotion

Create promotion visibility independently of release readiness.
Leave existing PRs untouched to preserve concurrent maintainer edits.
Validate against current main without masking candidate rejection.

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

* ci: isolate hook Git environments

Prevent temporary Git fixtures from modifying the calling worktree.
Clear repository-local hook variables before the unchanged build gates,
and isolate validator and fixture Git subprocesses from caller overrides.
Exercise linked-worktree config, ref and index preservation offline.

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

---------

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

* fix(ci): reject invalid candidate evidence

Explain missing main history without mutating refs or weakening promotion validation. Match experiment boolean and aggregation parsing, respect candidate-limit precedence, and return issues for malformed configuration or metric values.

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

* fix: preserve completion and retrieval contracts

Propagate derivation SQL failures, invalidate KG status caches, and avoid unused preview reads. Align explicit hash validation and export standalone client dependencies. Keep secret exceptions fixture-scoped and remove undefined behavior from status fixtures.

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

* fix: fence asynchronous enrichment completion

Coordinate graph and NL commits before acknowledging enrichment. Atomically classify embedding reuse, fence repair status writes, and preserve newer vectors on old failures. Upgrade content readiness triggers and keep repository counters coherent. Six affected ASan suites pass; this does not provide restart replay or generation-scoped vector storage.

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

* fix: bound semantic graph output reservation

Use retained neighbors for admission and node capacity instead of mixing corpus and pair counts. Preserve single-neighbor output and avoid quadratic reservation. Characterization regression fails three assertions before the fix; full component ASan suite passes afterward.

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

* fix: fence standalone embedding repair completion

Mint attempt tokens before reading canonical repair input and publish only current persisted document embeddings. Do not skip stale physical vectors. Make SQLite preparation fault injection connection-local and deterministic. Full component and metadata ASan suites pass.

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

* fix: preserve enrichment failure reporting

Admit pending embedding attempts before provider failures while preserving completed results. Route title no-op acknowledgement through the writer so flush reports SQL failures and duplicate jobs can retry. Four affected full ASan suites pass; host configuration hashes unchanged.

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

* test: align explicit hash prefix boundaries

Reject five characters and accept the supported six-character minimum. Full hybrid_search suite passes. Separate daemon shutdown sanitizer failure remains under investigation despite a passing targeted rerun.

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

* test: align UI hash validation boundary

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

---------

Signed-off-by: trvon <me@trevon.dev>
2026-09-12 18:53:12 -06:00

199 lines
7.4 KiB
C++

// Copyright (c) 2025 YAMS Contributors
// SPDX-License-Identifier: GPL-3.0-or-later
#include <catch2/catch_test_macros.hpp>
#include <filesystem>
#include <yams/core/uuid.h>
#include <yams/daemon/components/DaemonLifecycleFsm.h>
#include <yams/daemon/components/EntityGraphService.h>
#include <yams/daemon/components/InternalEventBus.h>
#include <yams/daemon/components/ServiceManager.h>
#include <yams/daemon/components/StateComponent.h>
#include <yams/daemon/daemon.h>
#include <yams/metadata/connection_pool.h>
#include <yams/metadata/database.h>
#include <yams/metadata/document_metadata.h>
#include <yams/metadata/knowledge_graph_store.h>
#include <yams/metadata/metadata_repository.h>
#include <yams/metadata/migration.h>
#include <chrono>
#include <thread>
using yams::daemon::EntityGraphService;
namespace {
struct EntityGraphFixture {
EntityGraphFixture() {
dir = std::filesystem::temp_directory_path() /
yams::core::generateId("entity-graph-completion-test");
std::filesystem::create_directories(dir);
dbPath = (dir / "entity-graph.db").string();
{
yams::metadata::Database db;
REQUIRE(db.open(dbPath, yams::metadata::ConnectionMode::Create));
yams::metadata::MigrationManager migrations(db);
REQUIRE(migrations.initialize());
migrations.registerMigrations(
yams::metadata::YamsMetadataMigrations::getAllMigrations());
REQUIRE(migrations.migrate());
db.close();
}
yams::metadata::ConnectionPoolConfig poolConfig;
poolConfig.minConnections = 1;
poolConfig.maxConnections = 2;
pool = std::make_shared<yams::metadata::ConnectionPool>(dbPath, poolConfig);
auto kgResult = yams::metadata::makeSqliteKnowledgeGraphStore(*pool);
REQUIRE(kgResult.has_value());
kg = std::move(kgResult.value());
metadata = std::make_shared<yams::metadata::MetadataRepository>(*pool);
}
~EntityGraphFixture() {
metadata.reset();
kg.reset();
pool->shutdown();
pool.reset();
std::error_code ec;
std::filesystem::remove_all(dir, ec);
}
std::filesystem::path dir;
std::string dbPath;
std::shared_ptr<yams::metadata::ConnectionPool> pool;
std::shared_ptr<yams::metadata::KnowledgeGraphStore> kg;
std::shared_ptr<yams::metadata::MetadataRepository> metadata;
};
} // namespace
TEST_CASE("EntityGraphService: full channel reports failed admission", "[daemon][kg-intent]") {
using namespace yams::daemon;
DaemonConfig config;
config.dataDir =
std::filesystem::temp_directory_path() / yams::core::generateId("kg-admission-test");
struct Cleanup {
std::filesystem::path path;
~Cleanup() {
std::error_code ec;
std::filesystem::remove_all(path, ec);
}
} cleanup{config.dataDir};
StateComponent state;
DaemonLifecycleFsm lifecycle;
ServiceManager services(config, state, lifecycle); // Not initialized; no corpus or daemon.
EntityGraphService svc(&services, 1); // Do not start the consumer.
auto& bus = InternalEventBus::instance();
auto channel =
bus.get_or_create_channel<InternalEventBus::EntityGraphJob>("entity_graph_jobs", 4096);
struct Drain {
decltype(channel) queue;
~Drain() {
InternalEventBus::EntityGraphJob job;
while (queue->try_pop(job)) {
}
}
} drain{channel};
while (channel->try_push(InternalEventBus::EntityGraphJob{})) {
}
EntityGraphService::Job job;
job.documentHash = "intent-hash";
job.documentDbId = 7;
job.knowledgeGraphToken = "intent-token";
job.knowledgeGraphCompletion = std::make_shared<KnowledgeGraphCompletion>(true, true);
auto rejected = svc.submitExtraction(job);
REQUIRE_FALSE(rejected.has_value());
CHECK(rejected.error().code == yams::ErrorCode::ResourceExhausted);
CHECK(svc.getStats().accepted == 0);
InternalEventBus::EntityGraphJob buffered;
while (channel->try_pop(buffered)) {
}
REQUIRE(svc.submitExtraction(job).has_value());
REQUIRE(channel->try_pop(buffered));
CHECK(buffered.documentDbId == 7);
CHECK(buffered.knowledgeGraphToken == "intent-token");
CHECK(buffered.knowledgeGraphCompletion == job.knowledgeGraphCompletion);
CHECK(svc.getStats().accepted == 1);
}
TEST_CASE("EntityGraphService: graph no-op waits for title-NL completion",
"[unit][daemon][kg-completion]") {
using namespace yams::daemon;
EntityGraphFixture fixture;
DaemonConfig config;
config.dataDir = fixture.dir / "daemon";
StateComponent state;
DaemonLifecycleFsm lifecycle;
ServiceManager services(config, state, lifecycle);
services.__test_setMetadataRepo(fixture.metadata);
REQUIRE(services.getDatabaseManager() != nullptr);
services.getDatabaseManager()->setKgStore(fixture.kg);
yams::metadata::DocumentInfo doc;
doc.filePath = "/kg/noop.txt";
doc.fileName = "noop.txt";
doc.sha256Hash = "entity-graph-noop-completion";
auto id = fixture.metadata->insertDocument(doc);
REQUIRE(id.has_value());
yams::metadata::BatchContentEntry content;
content.documentId = id.value();
content.contentText = "natural language content";
content.knowledgeGraphToken = "noop-shared-attempt";
REQUIRE(fixture.metadata->batchInsertContentAndIndex({content}).has_value());
auto completion = std::make_shared<KnowledgeGraphCompletion>(true, true);
EntityGraphService::Job job;
job.documentHash = doc.sha256Hash;
job.filePath = doc.filePath;
job.contentUtf8 = content.contentText;
job.mimeType = "text/plain";
job.documentDbId = id.value();
job.knowledgeGraphToken = content.knowledgeGraphToken;
job.knowledgeGraphCompletion = completion;
EntityGraphService service(&services, 1);
REQUIRE(service.testing_process(job));
auto marker = fixture.metadata->getMetadata(id.value(), "yams:kg_enrichment");
REQUIRE(marker.has_value());
REQUIRE(marker.value().has_value());
CHECK(marker.value()->value == "pending:" + content.knowledgeGraphToken);
CHECK(completion->markCommitted(KnowledgeGraphCompletionStage::TitleNl));
}
TEST_CASE("EntityGraphService: queue and process without services", "[daemon]") {
// Service with nullptr ServiceManager should not crash; processing will be counted as failed
EntityGraphService svc(nullptr, 1);
svc.start();
EntityGraphService::Job j;
j.documentHash = "deadbeef";
j.filePath = "/tmp/file.cpp";
j.contentUtf8 = "int main() { return 0; }";
j.language = "cpp";
auto r = svc.submitExtraction(j);
// submitExtraction may fail immediately when ServiceManager is nullptr on some builds.
// The key thing is it should not crash. If it succeeds, process will be counted as failed.
if (r.has_value()) {
// Give worker a short time slice
std::this_thread::sleep_for(std::chrono::milliseconds(50));
auto stats = svc.getStats();
CHECK(stats.accepted >= 1u);
CHECK(stats.processed >= 1u);
CHECK(stats.failed >= 1u);
} else {
// Submission failed immediately - that's also acceptable behavior
INFO("submitExtraction returned error: " << r.error().message);
SUCCEED("submitExtraction rejected job with nullptr ServiceManager (expected behavior)");
}
svc.stop();
}