//! Textual-containment guards that keep the hand-written docs honest //! (milestone-11 ruling 3, repointed at the Diátaxis reference pages by //! milestone-13 ruling 7). They assert presence, not correctness — the same //! contract as openapi.zig's route guard. const std = @import("std"); const build_options = @import("build_options"); const docs = @import("docs_files"); const cli = @import("cli.zig"); const routes = @import("web/routes.zig"); const model = @import("config/model.zig"); test "every served operation has its own table row in docs/reference/api.md" { const gpa = std.testing.allocator; for (routes.table) |route| { // Matches one full method + path cell pair ("| GET | `/api/groups` |"), // so neither a same-path sibling method nor a longer-path prefix can // satisfy the check for a missing operation. const needle = try std.fmt.allocPrint(gpa, "| {s} | `{s}` |", .{ @tagName(route.method), route.pattern, }); defer gpa.free(needle); if (std.mem.indexOf(u8, docs.reference_api_md, needle) == null) { std.debug.print("operation row missing from docs/reference/api.md: {s}\n", .{needle}); return error.OperationMissingFromApiDoc; } } } test "every settings key appears in docs/reference/configuration.md" { const gpa = std.testing.allocator; var pairs: std.ArrayList(model.SettingPair) = .empty; defer { model.freeSettings(gpa, pairs.items); pairs.deinit(gpa); } try model.toSettings(.{}, gpa, &pairs); for (pairs.items) |pair| { // Matches the key's own leading table cell ("| `dns.port` |"), not a // bare mention: prose and the annotated example both name most keys, so // a substring search would stay green after a field's row is deleted. const needle = try std.fmt.allocPrint(gpa, "| `{s}` |", .{pair.key}); defer gpa.free(needle); if (std.mem.indexOf(u8, docs.reference_configuration_md, needle) == null) { std.debug.print("settings key row missing from docs/reference/configuration.md: {s}\n", .{needle}); return error.SettingsKeyMissingFromConfigDoc; } } } test "every cli subcommand has its own reference heading in docs/reference/cli.md" { const gpa = std.testing.allocator; for (cli.command_names) |entry| { // Anchors on the reference-section heading ("## `import FILE`" starts // with "## `import"), so prose mentions elsewhere cannot mask a removed // command section. const needle = try std.fmt.allocPrint(gpa, "## `{s}", .{entry.name}); defer gpa.free(needle); if (std.mem.indexOf(u8, docs.reference_cli_md, needle) == null) { std.debug.print("subcommand heading missing from docs/reference/cli.md: {s}\n", .{needle}); return error.SubcommandMissingFromCliDoc; } } } test "no doc page pastes a concrete version into a transcript" { const gpa = std.testing.allocator; // Anchored on the `nxdns ` prefix the transcripts print, not on the bare // version: a release numbered 1.0.0 would otherwise collide with the // 1.0.0.1 upstream address the pages use as an example. const built = try std.fmt.allocPrint(gpa, "nxdns {s}", .{build_options.version_string}); defer gpa.free(built); for (docs.pages) |page| { // The literal milestone 14 flagged, checked by name as well, so the // guard still bites on a page written against the old default after // `-Dversion-string` moves on. for ([_][]const u8{ built, "0.1.0-dev" }) |needle| { if (std.mem.indexOf(u8, page.text, needle) != null) { std.debug.print("version literal '{s}' in {s}: use the placeholder\n", .{ needle, page.path }); return error.VersionLiteralInDocPage; } } } } test "every version transcript prints the placeholder" { for (docs.version_transcript_pages) |page| { if (std.mem.indexOf(u8, page.text, "nxdns ") == null) { std.debug.print("transcript placeholder 'nxdns ' missing from {s}\n", .{page.path}); return error.VersionPlaceholderMissingFromDocPage; } } }