Crafter.Review/tests/Stream/main.cpp

97 lines
6.3 KiB
C++
Raw Normal View History

// SPDX-License-Identifier: AGPL-3.0-only
// SPDX-FileCopyrightText: Copyright (C) 2026 Catcrafts®
// The claude stream: its envelopes.
import std;
import Crafter.Review;
namespace fs = std::filesystem;
using namespace Crafter::Review;
namespace {
std::int32_t Failures = 0;
void Check(std::string_view name, bool cond, std::string_view detail = {}) {
std::println("{}{}{}", cond ? " ok " : " FAIL ", name, !cond && !detail.empty() ? std::format(" ({})", detail) : std::string());
if (!cond) ++Failures;
}
// The result line mutated, or dropped when the mutation returns nullopt.
std::string Variant(const std::string& text, const std::function<std::optional<Json>(Json)>& mutate) {
std::vector<std::string> out;
for (const std::string& line : Dyn::SplitLines(text)) {
if (line.starts_with('{') && Dyn::Replace(line, " ", "").contains("\"type\":\"result\"")) {
const std::optional<Json> changed = mutate(Json::parse(line));
if (!changed) continue;
out.push_back(DumpSpaced(*changed));
} else {
out.push_back(line);
}
}
return Dyn::Join(out, "\n");
}
std::function<std::optional<Json>(Json)> Failed(const Json& update) {
return [update](Json o) -> std::optional<Json> {
o.update(update);
o.erase("structured_output");
return o;
};
}
std::string Status(const std::string& text, std::optional<std::int32_t> code, std::string_view err = {}) {
return Stream::Classify(Stream::ParseStream(text), code, err).first;
}
}
int main() {
std::println("stream envelopes");
const std::string text = Dyn::ReadText(fs::current_path() / "tests" / "Goldens" / "fixture" / "stream-success.jsonl");
const Json p = Stream::ParseStream(text);
Check("success: classify ok", Stream::Classify(p, 0, "") == std::pair<std::string, std::string>{"ok", ""});
const std::optional<Json> so = Stream::StructuredOutput(p.at("result"));
Check("success: structured_output parsed", so && so->at("verdict") == "SEND-READY" && so->at("coverage").at(0).at("item") == "R1");
Check("success: init parsed", Dyn::Truthy(p.at("init")) && Dyn::Get(p.at("init"), "mcp_servers") == Json::array());
Check("success: usage from the result line + firstTurn", p.at("turns") == 2 && p.at("firstTurn").at("cache_creation_input_tokens").get<std::int64_t>() > 0
&& p.at("usage").at("output").get<std::int64_t>() > 0 && !p.at("costUsd").is_null() && p.at("denials") == Json::array());
const Json w = Stream::Windows(p.at("rateLimit"));
Check("success: rate windows as percent", w.contains("five_hour") && w.at("five_hour").at("utilization").get<double>() > 0 && w.at("five_hour").at("utilization").get<double>() < 100
&& Dyn::Truthy(w.at("seven_day").at("resetsAt")), DumpCompactSorted(w));
Check("error_max_turns -> error", Status(Variant(text, Failed({{"subtype", "error_max_turns"}, {"is_error", true}, {"result", "Reached max turns (1)"}})), 1) == "error");
Check("429 -> limit", Status(Variant(text, Failed({{"subtype", "error_during_execution"}, {"is_error", true}, {"api_error_status", 429}, {"result", "API Error: 429 rate_limit_error"}})), 1) == "limit");
Check("401 -> auth", Status(Variant(text, Failed({{"subtype", "error_during_execution"}, {"is_error", true}, {"api_error_status", 401}, {"result", "API Error: 401 authentication_error"}})), 1) == "auth");
const std::string noResult = Variant(text, [](Json) -> std::optional<Json> { return std::nullopt; });
Check("403 in stderr, no result -> auth", Status(noResult, 1, "Error: 403 Forbidden") == "auth");
Check("no result line -> error", Status(noResult, 1) == "error");
Check("no result line + usage limit text -> limit", Status(noResult, 1, "You're out of extra usage") == "limit");
Check("timeout flag / exit 124 -> timeout", Stream::Classify(p, std::nullopt, "", true).first == "timeout" && Stream::Classify(p, 124, "").first == "timeout");
const std::string doubled = Variant(text, [](Json o) -> std::optional<Json> {
o["structured_output"] = DumpSpaced(o.at("structured_output"));
return o;
});
Check("double-encoded structured_output decoded", Stream::StructuredOutput(Stream::ParseStream(doubled).at("result")) == so);
const std::string inResult = Variant(text, [](Json o) -> std::optional<Json> {
const Json inner = o.at("structured_output");
o.erase("structured_output");
o["result"] = DumpSpaced(inner);
return o;
});
Check("structured output inside result text decoded", Stream::StructuredOutput(Stream::ParseStream(inResult).at("result")) == so);
const std::string noFindings = Variant(text, [](Json o) -> std::optional<Json> {
o["structured_output"] = {{"verdict", "SEND-READY"}};
return o;
});
Check("missing findings array -> error (no structured output)", Status(noFindings, 0) == "error");
Check("credit balance too low -> credits", Status(Variant(text, Failed({{"subtype", "error_during_execution"}, {"is_error", true}, {"api_error_status", 400},
{"result", "API Error: 400 Your credit balance is too low to access the Anthropic API."}})), 1) == "credits");
// The helper's spelling is a SQL call wherever it stands, and a stream
// line is cut at \n only: a raw U+2028 in a JSON string stays inside its
// line.
Check("this binary's helper is a SQL call, alone, after a path, in a compound command; another verb is not",
Stream::IsSqlCall("/app/bin/crafter-review sql <<'SQL'\nselect 1;\nSQL") && Stream::IsSqlCall("cd . && crafter-review sql 'select 1'")
&& Stream::IsSqlCall("/x/bin/crafter.review-exe-v/crafter-review sql") && !Stream::IsSqlCall("/app/bin/crafter-review sqlx") && !Stream::IsSqlCall("/app/bin/crafter-review totp f.json"));
const std::string split = "{\"type\":\"result\",\"subtype\":\"success\",\"is_error\":false,\"result\":\"a\u2028b\",\"structured_output\":{\"verdict\":\"SEND-READY\",\"findings\":[],\"notes\":[],\"coverage\":[]}}";
Check("a result line with a raw U+2028 in a string is still the result line", Stream::Classify(Stream::ParseStream(split), 0, "").first == "ok");
return Failures ? 1 : 0;
}