Implement bounded repository navigation with repo-lens
CI / build (debug, map[compiler:appleclang os:macos-latest]) (push) Canceled after 0s
CI / build (debug, map[compiler:clang os:ubuntu-latest]) (push) Canceled after 0s
CI / build (debug, map[compiler:gcc os:ubuntu-latest]) (push) Canceled after 0s
CI / build (debug, map[compiler:msvc os:windows-latest]) (push) Canceled after 0s
CI / build (release, map[compiler:appleclang os:macos-latest]) (push) Canceled after 0s
CI / build (release, map[compiler:clang os:ubuntu-latest]) (push) Canceled after 0s
CI / build (release, map[compiler:gcc os:ubuntu-latest]) (push) Canceled after 0s
CI / build (release, map[compiler:msvc os:windows-latest]) (push) Canceled after 0s
CI / build (sanitizers, map[compiler:appleclang os:macos-latest]) (push) Canceled after 0s
CI / build (sanitizers, map[compiler:clang os:ubuntu-latest]) (push) Canceled after 0s
CI / build (sanitizers, map[compiler:gcc os:ubuntu-latest]) (push) Canceled after 0s
CI / build (sanitizers, map[compiler:msvc os:windows-latest]) (push) Canceled after 0s

- Add map, find, and around commands with repository root discovery
- Stream literal ripgrep searches with match counts and result limits
- Add direct subprocess execution and explicit error returns
- Disable C++ exceptions and add navigation tests
- Support CMake installation and document builds and deployment
- Add a reusable agent skill and document skill discovery
This commit is contained in:
wilmc committed 2026-10-09 13:15:00 +02:00
1 parent 70c43b6a68
commit a0fbb9f50e
22 files changed
+1185 -112

No files matched your search

+10 -1
View File
@@ -3,11 +3,17 @@ include(gtest)
add_executable(
repo-lens-test
test_main.cpp
navigation_test.cpp
../src/navigation.cpp
../src/process.cpp
../src/repo_tool_around.cpp
../src/repo_tool_find.cpp
../src/repo_tool_map.cpp
)
target_link_libraries(
repo-lens-test
gtest_main
gtest
)
if(ENABLE_ALL_REASONABLE_WARNINGS)
@@ -29,3 +35,6 @@ endif()
include(GoogleTest)
gtest_discover_tests(repo-lens-test)
target_include_directories(repo-lens-test PRIVATE "${PROJECT_SOURCE_DIR}/include")
target_compile_definitions(repo-lens-test PRIVATE REPO_TEST_EXECUTABLE="$<TARGET_FILE:repo-lens-test>")
+213
View File
@@ -0,0 +1,213 @@
#include "navigation.hpp"
#include "process.hpp"
#include "repo_tool_around.hpp"
#include "repo_tool_find.hpp"
#include "repo_tool_map.hpp"
#include <gtest/gtest.h>
#include <chrono>
#include <fstream>
#include <sstream>
TEST(Location, FinalColonAndNumericValidation)
{
const auto windows = rl::parse_location("C:\\src\\repo\\file.cpp:417");
ASSERT_TRUE(windows.has_value());
EXPECT_EQ(windows->file.string(), "C:\\src\\repo\\file.cpp");
EXPECT_EQ(windows->line, 417U);
EXPECT_EQ(rl::parse_location("src/file.cpp:1")->line, 1U);
for (const auto value : {"file", ":1", "file:0", "file:-1", "file:12x", "file:", "file:999999999999999999999999"})
{
EXPECT_FALSE(rl::parse_location(value).has_value()) << value;
}
}
TEST(Map, HeadingsRankBeforeBodyAndBoundariesAreRespected)
{
const std::vector<std::string> lines{"# Map", "## Other", "cloth mentioned here", "### CLOTH", "preferred route", "### Next", "unrelated route"};
const auto output = rl::map_sections(lines, "cloth");
EXPECT_TRUE(output.starts_with("### CLOTH\npreferred route\n"));
EXPECT_NE(output.find("cloth mentioned here"), std::string::npos);
EXPECT_EQ(output.find("unrelated route"), std::string::npos);
EXPECT_EQ(rl::map_sections(lines, "missing"), "No relevant project-map section found.\n");
}
TEST(Map, LimitsSectionsLinesAndBytes)
{
EXPECT_NE(rl::map_sections({"## route 1", "## route 2", "## route 3", "## route 4"}, "route").find("TRUNCATED"), std::string::npos);
std::vector<std::string> lines(200, "route");
lines[0] = "## route";
const auto output = rl::map_sections(lines, "route");
EXPECT_LT(output.size(), 1000U);
EXPECT_NE(output.find("TRUNCATED"), std::string::npos);
EXPECT_LT(rl::map_sections({"## route", std::string(20000, 'x')}, "route").size(), 16460U);
}
TEST(Map, FencedHeadingsAreNotSectionBoundaries)
{
const auto output = rl::map_sections({"## route", "```markdown", "## example", "```", "tail", "## other", "omit"}, "route");
EXPECT_NE(output.find("tail"), std::string::npos);
EXPECT_EQ(output.find("omit"), std::string::npos);
}
namespace
{
class Repository : public ::testing::Test
{
protected:
std::filesystem::path previous;
std::filesystem::path root;
std::ostringstream output;
std::ostringstream errors;
std::streambuf* old_output = nullptr;
std::streambuf* old_errors = nullptr;
void SetUp() override
{
std::error_code code;
previous = std::filesystem::current_path(code);
ASSERT_FALSE(code) << code.message();
const auto temporary = std::filesystem::temp_directory_path(code);
ASSERT_FALSE(code) << code.message();
root = temporary / ("repo-lens-test-" + std::to_string(std::chrono::steady_clock::now().time_since_epoch().count()));
std::filesystem::create_directories(root / ".git", code);
ASSERT_FALSE(code) << code.message();
std::filesystem::create_directories(root / "nested", code);
ASSERT_FALSE(code) << code.message();
std::filesystem::create_directories(root / "docs", code);
ASSERT_FALSE(code) << code.message();
std::ofstream(root / "docs" / "PROJECTMAP.md") << "## route\nPath: src/\n";
std::ofstream file(root / "file with spaces.txt");
for (int i = 1; i <= 80; ++i)
{
file << "source " << i << '\n';
}
std::filesystem::current_path(root / "nested", code);
ASSERT_FALSE(code) << code.message();
old_output = std::cout.rdbuf(output.rdbuf());
old_errors = std::cerr.rdbuf(errors.rdbuf());
}
void TearDown() override
{
if (old_output)
{
std::cout.rdbuf(old_output);
}
if (old_errors)
{
std::cerr.rdbuf(old_errors);
}
std::error_code code;
std::filesystem::current_path(previous, code);
EXPECT_FALSE(code) << code.message();
std::filesystem::remove_all(root, code);
EXPECT_FALSE(code) << code.message();
}
};
} // namespace
TEST_F(Repository, RootDiscoveryAndDefaultSourceWindow)
{
std::filesystem::path found;
std::string error;
ASSERT_TRUE(rl::repository_root(found, error)) << error;
EXPECT_EQ(found, root);
const std::vector<std::string_view> args{"file with spaces.txt:40"};
EXPECT_EQ(rl::RepoLensToolAround{}.execute(args), 0);
EXPECT_TRUE(output.str().starts_with("FILE file with spaces.txt\nLINES 10-70\nTARGET 40\n10 | source 10\n"));
EXPECT_TRUE(output.str().ends_with("70 | source 70\n"));
}
TEST_F(Repository, SourceWindowClampsAtBothEnds)
{
const std::vector<std::string_view> start{"file with spaces.txt:1"};
EXPECT_EQ(rl::RepoLensToolAround{}.execute(start), 0);
EXPECT_NE(output.str().find("LINES 1-31"), std::string::npos);
output.str("");
const auto absolute = (root / "file with spaces.txt").string() + ":80";
const std::vector<std::string_view> end{absolute, "--before", "0", "--after", "30"};
EXPECT_EQ(rl::RepoLensToolAround{}.execute(end), 0);
EXPECT_NE(output.str().find("LINES 80-80"), std::string::npos);
}
TEST_F(Repository, InvalidAroundRequestsFail)
{
for (const auto location : {"missing:1", "file with spaces.txt:81", "file with spaces.txt:0"})
{
const std::vector<std::string_view> args{location};
EXPECT_NE(rl::RepoLensToolAround{}.execute(args), 0);
}
const std::vector<std::string_view> too_large{"file with spaces.txt:1", "--before", "1001"};
EXPECT_NE(rl::RepoLensToolAround{}.execute(too_large), 0);
const std::vector<std::string_view> negative{"file with spaces.txt:1", "--after", "-1"};
EXPECT_NE(rl::RepoLensToolAround{}.execute(negative), 0);
const std::vector<std::string_view> missing{"file with spaces.txt:1", "--after"};
EXPECT_NE(rl::RepoLensToolAround{}.execute(missing), 0);
}
TEST_F(Repository, MapWorksFromNestedDirectory)
{
const std::vector<std::string_view> args{"ROUTE"};
EXPECT_EQ(rl::RepoLensToolMap{}.execute(args), 0);
EXPECT_EQ(output.str(), "## route\nPath: src/\n");
}
TEST_F(Repository, FindLiteralTruncationAndNoMatches)
{
const auto version = rl::run_process({"rg", "--version"}, root, [](std::string_view) {});
if (version.exit_code != 0)
{
GTEST_SKIP() << "ripgrep unavailable: " << version.error;
}
{
std::ofstream file(root / "matches.txt");
for (int i = 0; i < 25; ++i)
{
file << "literal.* literal.*\n";
}
std::ofstream(root / ".gitignore") << "ignored.txt\n";
std::ofstream(root / "ignored.txt") << "literal.*\n";
}
const std::vector<std::string_view> args{"literal.*"};
EXPECT_EQ(rl::RepoLensToolFind{}.execute(args), 0);
EXPECT_NE(output.str().find("MATCHES 50\nSHOWN 20\nTRUNCATED"), std::string::npos);
EXPECT_NE(output.str().find("matches.txt:1\n"), std::string::npos);
EXPECT_EQ(output.str().find("ignored.txt:"), std::string::npos);
output.str("");
const std::vector<std::string_view> absent{"nothing matches this"};
EXPECT_EQ(rl::RepoLensToolFind{}.execute(absent), 0);
EXPECT_NE(output.str().find("MATCHES 0\nSHOWN 0"), std::string::npos);
}
TEST_F(Repository, ProcessPreservesArgumentsStreamsAndExitCode)
{
std::string captured;
const auto executable = std::filesystem::path(REPO_TEST_EXECUTABLE).string();
const std::string argument = R"(spaces "quotes" & $() \)";
const auto result = rl::run_process({executable, "--process-fixture", argument}, root,
[&](std::string_view chunk)
{
captured += chunk;
});
EXPECT_EQ(result.exit_code, 7);
EXPECT_EQ(captured, argument);
EXPECT_EQ(result.error, "fixture stderr");
}
TEST_F(Repository, RecoverableFailuresReturnErrors)
{
std::vector<std::string> lines;
std::string error;
EXPECT_FALSE(rl::read_lines(root / "missing", lines, error));
EXPECT_NE(error.find("Cannot open file"), std::string::npos);
const auto missing = rl::run_process({"repo-lens-missing-executable-12345"}, root, [](std::string_view) {});
EXPECT_NE(missing.exit_code, 0);
EXPECT_FALSE(missing.error.empty());
const auto empty = rl::run_process({}, root, [](std::string_view) {});
EXPECT_NE(empty.exit_code, 0);
std::error_code code;
std::filesystem::remove(root / ".git", code);
ASSERT_FALSE(code) << code.message();
std::filesystem::path found;
EXPECT_FALSE(rl::repository_root(found, error));
EXPECT_NE(error.find("No repository root"), std::string::npos);
}
+12 -3
View File
@@ -1,6 +1,15 @@
#include <iostream>
#include <string_view>
#include <gtest/gtest.h>
TEST(testmain, example)
auto main(int argc, char* argv[]) -> int
{
EXPECT_EQ(42, 7*6);
}
if (argc == 3 && std::string_view(argv[1]) == "--process-fixture")
{
std::cout << argv[2];
std::cerr << "fixture stderr";
return 7;
}
::testing::InitGoogleTest(&argc, argv);
return RUN_ALL_TESTS();
}