Crafter.Review/tests/LensTools/main.cpp
Jorijn van der Graaf 5dab4b49d5
ci: the Flatpak workflow, local deps for the local build, the lens tools
- .forgejo/workflows/ci.yaml is Crafter.Build's workflow for this app: the
  base installed from the Catcrafts repo, the version stamped into the
  crafter-review module, the suite inside the build, then a smoke test of
  the app as it ships - the version and home, every library resolved and no
  RUNPATH outside /app, the bundled tools, a freeze and a dry run on a
  scratch repository. Publishing holds /srv/flatpak/.publish.lock and
  writes only this app's flatpakref. Unverified until a crafter-flatpak
  runner serves this repository.
- `run flatpak --deps=local` builds against the Crafter.Network and
  Crafter.Thread checkouts next to this one, as a native --local build
  does, while their commits are not on forgejo: a gitignored manifest copy
  with three dir sources side by side and --local.
- LensTools: every program the lens allowlist names, the shell and the MCP
  server's node and npx resolve on PATH; strict in the Flatpak build, where
  the suite runs in the sandbox that ships, a skip natively when this
  machine lacks one.

Built on the stable base (Crafter.Build 3.0.0) with --deps=local: the suite
passes inside flatpak-builder (25 passed, 2 skipped), and the workflow's
smoke test passes against that build.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-31 15:59:20 +01:00

42 lines
2.1 KiB
C++

// SPDX-License-Identifier: AGPL-3.0-only
// SPDX-FileCopyrightText: Copyright (C) 2026 Catcrafts®
// The programs a lens is allowed to run are there: every Bash(<program>
// ...:*) entry of the base allowlist (the git verbs included), the shell
// claude's Bash tool runs them with, and node and npx for the Playwright MCP
// server. The suite runs inside the Flatpak build, the sandbox that ships, so
// there every one must resolve on PATH; natively the test names what this
// machine lacks and skips.
#include <unistd.h>
import std;
import Crafter.Review;
namespace fs = std::filesystem;
using namespace Crafter::Review;
int main() {
std::println("lens tools: every program a lens's allowlist names is on PATH");
std::set<std::string> programs = {"bash", "node", "npx"};
for (const std::string& tool : Tools::BaseAllow()) {
if (!tool.starts_with("Bash(") || !tool.ends_with(":*)")) continue;
const std::string prefix = tool.substr(5, tool.size() - 8);
const std::string program = prefix.substr(0, prefix.find(' '));
// A shell builtin, never a file of its own.
if (program != "cd") programs.insert(program);
}
const std::vector<std::string> path = Dyn::Split(Platform::GetEnv("PATH").value_or(""), ":");
std::vector<std::string> missing;
for (const std::string& program : programs) {
const bool found = std::ranges::any_of(path, [&](const std::string& dir) { return !dir.empty() && access((fs::path(dir) / program).c_str(), X_OK) == 0; });
if (!found) missing.push_back(program);
}
const bool shipped = fs::exists("/.flatpak-info");
if (!missing.empty() && !shipped) {
std::println("skipped: not on this machine's PATH: {}", Dyn::Join(missing, ", "));
return 77;
}
const bool ok = missing.empty() && programs.size() >= 20;
std::println("{}every program a lens's allowlist names, its shell and the MCP server's node and npx resolve on PATH ({} programs){}", ok ? " ok " : " FAIL ", programs.size(),
missing.empty() ? std::string() : std::format(" (missing: {})", Dyn::Join(missing, ", ")));
return ok ? 0 : 1;
}