mod support;
use std::path::{Path, PathBuf};
use support::{run, runner_manager};
fn repository_root() -> PathBuf {
Path::new(env!("CARGO_MANIFEST_DIR"))
.join("..")
.join("..")
.canonicalize()
.expect("the repository root must exist")
}
fn document(name: &str) -> String {
let path = repository_root().join(name);
std::fs::read_to_string(&path)
.unwrap_or_else(|error| panic!("cannot read {}: {error}", path.display()))
.replace("\r\n", "\n")
}
const WSL_HEADING: &str = "\n## Run Linux jobs too: manage a WSL2 distribution\n";
fn wsl_section(source: &str) -> &str {
let start = source.find(WSL_HEADING).unwrap_or_else(|| {
panic!(
"README.md must carry a `## Run Linux jobs too: manage a WSL2 distribution` \
section. It is the procedure `b3-acceptance-docs`'s first Definition-of-Done \
item is about, and without it a new operator has nothing to follow."
)
});
let after = start + WSL_HEADING.len();
let end = source[after..]
.find("\n## ")
.map_or(source.len(), |offset| after + offset);
let section = &source[after..end];
assert!(
section.len() > 2_000,
"the WSL section is {} bytes, which is too short to be the procedure this test \
reads. Either the section was gutted or the range above stopped early.",
section.len()
);
section
}
fn wsl_procedure() -> String {
let source = document("README.md");
wsl_section(&source).to_string()
}
#[test]
fn the_procedure_states_every_prerequisite_a_new_operator_has_to_meet() {
let section = wsl_procedure();
for (needle, why) in [
(
"WSL2",
"the distribution has to be WSL2, and WSL1 is refused",
),
(
"systemd=true",
"systemd is a hard prerequisite and `/etc/wsl.conf` is where an operator \
enables it",
),
(
"/etc/wsl.conf",
"naming the setting without naming the file leaves the operator searching",
),
(
"wsl --terminate",
"the wsl.conf change takes effect on the next start, and nothing else in the \
procedure restarts the distribution",
),
(
"root",
"the provider installs a system service and needs root",
),
(
"ARM",
"the published Linux architectures are a prerequisite, not a detail",
),
(
"browser",
"the one interactive step is the sign-in, and it happens on Windows",
),
(
"Windows",
"the whole family refuses on anything else, and that has to be stated first",
),
] {
assert!(
section.contains(needle),
"the prerequisites must name {needle:?}: {why}"
);
}
}
#[test]
fn the_procedure_promises_the_four_manual_steps_this_feature_removes() {
let section = wsl_procedure();
for named in [
"staging folder",
"systemd unit",
"scheduled task",
"credential store",
] {
assert!(
section.contains(named),
"the section must say plainly that {named:?} is not something the operator \
creates or copies from. `b3-acceptance-docs`: a new operator configures a \
second Linux host \"without any undocumented staging directory, PowerShell \
token extraction, manual systemd unit or manual scheduled task\"."
);
}
assert!(
section.contains("it is a defect in the product"),
"and it must say what to conclude if a step ever does ask for one, or the \
paragraph is a description rather than a promise"
);
for forbidden in [
"schtasks",
"New-ScheduledTask",
"systemctl enable",
"systemctl daemon-reload",
"Get-Credential",
"CryptUnprotectData",
] {
assert!(
!section.contains(forbidden),
"the procedure asks the operator to run {forbidden:?}, which is exactly one of \
the manual steps this feature exists to remove"
);
}
}
#[test]
fn the_procedure_says_each_host_signs_in_separately_and_why_sharing_fails() {
let section = wsl_procedure();
assert!(
section.contains("Each host signs in separately")
|| section.contains("Each host needs its own sign-in"),
"the section must state the rule as a rule, not leave it to be inferred from an \
example"
);
assert!(
section.contains("renew"),
"and it must give the reason: GitHub invalidates both halves of a token pair when \
either half is renewed. Without the mechanism, \"sign in twice\" reads like \
bureaucracy and the first operator to hit a rate limit will try copying the \
credential."
);
for consequence in ["logging each other out", "does not work"] {
assert!(
section.contains(consequence),
"the consequence must be stated too: {consequence:?} is what an operator needs \
to know before deciding to copy a token"
);
}
assert!(
section.contains("never written down on Windows")
|| section.contains("is never written down on the"),
"and the guarantee the design makes in return: the issued credential does not touch \
this machine's disk"
);
}
#[test]
fn the_procedure_covers_every_topic_the_task_lists() {
let section = wsl_procedure();
for (topic, needle) in [
("fresh install", "wsl install --distribution Ubuntu"),
("adoption", "adopted rather than reinstalled"),
("policy setup through --host", "--host wsl:Ubuntu repo add"),
("capacity", "host set-capacity"),
("status", "wsl status --distribution Ubuntu"),
("the JSON document", "--json"),
("detach", "wsl detach --distribution Ubuntu"),
("login availability", "after that user logs on"),
("docker diagnostics", "Docker is diagnosed, not installed"),
("recovery", "run `wsl install` again"),
] {
assert!(
section.contains(needle),
"the section must cover {topic}, and the text that would show it does \
({needle:?}) is absent"
);
}
assert!(
section.contains("A preflight failure changed nothing at all"),
"recovery starts with knowing whether anything was changed"
);
assert!(
section.contains("deliberately not called `uninstall`"),
"`detach`'s name is a decision the 2026-09-06 review made on purpose, and the \
section is where an operator learns it deletes no Linux data"
);
}
fn documented_commands(section: &str) -> Vec<String> {
section
.lines()
.map(str::trim)
.filter(|line| line.starts_with("runner-manager "))
.map(|line| match line.find(" #") {
Some(offset) => line[..offset].trim().to_string(),
None => line.to_string(),
})
.collect()
}
fn arguments_for(line: &str) -> Vec<String> {
line.split_whitespace()
.map(|token| match token {
"OWNER/REPO" => "owner/repo",
"NAME" => "Ubuntu",
"N" => "1",
other => other,
})
.map(str::to_string)
.collect()
}
#[test]
fn every_command_the_procedure_prints_is_accepted_by_the_real_parser() {
let section = wsl_procedure();
let commands = documented_commands(§ion);
assert!(
commands.len() >= 10,
"only {} commands were parsed out of the WSL section, which means this scan is \
broken rather than that the procedure has almost none: {commands:?}",
commands.len()
);
let temporary = tempfile::tempdir().expect("a temporary directory");
for line in &commands {
let arguments = arguments_for(line);
let (binary, arguments) = arguments
.split_first()
.expect("a documented line has at least one token");
assert_eq!(
binary, "runner-manager",
"every command line in this section must invoke the product: {line}"
);
let mut command = runner_manager(temporary.path());
command.args(arguments);
command.arg("--help");
let outcome = run(command);
assert_eq!(
outcome.code, 0,
"the README documents `{line}`, and the real parser refuses it (exit {}):\n{}\n\
An operator copies these lines; one the binary does not accept is a defect in \
the product, not in its documentation.",
outcome.code, outcome.stderr
);
}
}
fn newest_entry(changelog: &str) -> (String, (u64, u64, u64)) {
let heading = changelog
.lines()
.find_map(|line| line.strip_prefix("## "))
.expect("CHANGELOG.md must carry at least one `## <version>` entry")
.trim()
.to_string();
let parts: Vec<u64> = heading
.split('.')
.map(|part| {
part.parse::<u64>().unwrap_or_else(|_| {
panic!(
"the newest changelog entry is headed {heading:?}, which is not the \
strict `X.Y.Z` the release workflow accepts. `release.sh` refuses a \
pre-release or build-metadata suffix, so a heading it would refuse \
names no release this repository can publish."
)
})
})
.collect();
assert_eq!(
parts.len(),
3,
"the newest changelog entry must be headed with an `X.Y.Z` version, not {heading:?}"
);
(heading, (parts[0], parts[1], parts[2]))
}
fn workspace_version() -> (u64, u64, u64) {
let manifest = document("Cargo.toml");
let line = manifest
.lines()
.skip_while(|line| line.trim() != "[workspace.package]")
.skip(1)
.take_while(|line| !line.trim_start().starts_with('['))
.find_map(|line| line.trim().strip_prefix("version = "))
.expect("the root manifest must carry a `[workspace.package]` version");
let text = line.trim().trim_matches('"');
let parts: Vec<u64> = text
.split('.')
.map(|part| part.parse().expect("the manifest version is X.Y.Z"))
.collect();
assert_eq!(
parts.len(),
3,
"the manifest version must be `X.Y.Z`, not {text:?}"
);
(parts[0], parts[1], parts[2])
}
#[test]
fn the_changelog_documents_this_feature_under_a_version_the_release_can_publish() {
let changelog = document("CHANGELOG.md");
let (heading, newest) = newest_entry(&changelog);
assert!(
newest >= workspace_version(),
"the newest changelog entry is {heading}, which is older than the version the \
workspace pins. release.yml's `validate` job refuses a dispatch whose version is \
not the newest entry here, and step 4 is what writes that version into \
`Cargo.toml` -- so the only way to reach this state is to have edited one of the \
two by hand. Add the entry for {heading}'s successor, or put the manifest back."
);
let entry = changelog
.split("\n## ")
.find(|entry| entry.starts_with("0.4.0\n"))
.expect("the 0.4.0 WSL feature entry remains in the changelog");
for named in [
"wsl list",
"wsl install",
"wsl status",
"wsl detach",
"--host local|wsl:NAME",
] {
assert!(
entry.contains(named),
"the {heading} entry must name `{named}`. An operator reads a release note to \
decide whether to upgrade, and a new command surface that is not in it is one \
nobody upgrading will look for."
);
}
for promise in [
"own GitHub credential",
"convergent",
"after that user logs on",
"no workload dependency",
"unchanged",
] {
assert!(
entry.to_lowercase().contains(&promise.to_lowercase()),
"the {heading} entry must state {promise:?}: it is one of the promises \
`03-security-and-lifecycle.md` makes, and a release note that omits it \
describes a different release"
);
}
}
#[test]
fn the_changelog_uses_no_em_dash() {
let changelog = document("CHANGELOG.md");
let offenders: Vec<&str> = changelog
.lines()
.filter(|line| line.contains('\u{2014}'))
.collect();
assert!(
offenders.is_empty(),
"CHANGELOG.md contains the em dash character on {} line(s). It is user-facing \
markdown and is held to `the_readme_uses_no_em_dash`'s convention:\n{}",
offenders.len(),
offenders.join("\n")
);
}