mirror of
https://github.com/instructkr/claw-code.git
synced 2026-06-04 21:47:10 +08:00
fix: expose complete version provenance
Generated with https://github.com/Yeachan-Heo/gajae-code Co-authored-by: Gajae Code <dev@gajae-code.com>
This commit is contained in:
@@ -6386,7 +6386,7 @@ Original filing (2026-04-18): the session emitted `SessionStart hook (completed)
|
||||
436. **DONE — `claw init` scaffolds safe project settings and reports partial/deferred artifacts** — fixed 2026-06-04 in `fix: scaffold safe init settings`. The starter `.claw.json` and new `.claw/settings.json` template now both use `permissions.defaultMode:"acceptEdits"` instead of unsafe `dontAsk`. Fresh init materializes `.claw/settings.json`, keeps `.claw/sessions/` deferred until the first successful session save, and emits per-artifact entries for `.claw/`, `.claw/settings.json`, `.claw/sessions/`, `.claw.json`, `.gitignore`, and `CLAUDE.md`. When `.claw/` already exists but its settings template is missing, init creates `.claw/settings.json` without overwriting existing files and reports `.claw/` as `partial` rather than `skipped`; idempotent reruns keep existing artifacts skipped and session storage deferred. JSON init output now includes `partial[]` and `deferred[]` alongside `created[]`, `updated[]`, and `skipped[]`, and init help/USAGE document the artifact statuses. Regression coverage: `initialize_repo_creates_expected_files_and_gitignore_entries`, `initialize_repo_is_idempotent_and_preserves_existing_files`, `artifacts_with_status_partitions_fresh_and_idempotent_runs`, and `init_json_envelope_has_hint_and_already_initialized_783`.
|
||||
|
||||
|
||||
437. **`version --output-format json` omits build provenance fields — no `is_dirty`, `branch`, `commit_date`, `commit_timestamp`, `rustc_version`; `git_sha` is truncated to 7 chars instead of full 40-char hash; sibling: `executable_path` leaks the build host's path (`/tmp/claw-dog-0530/...`) into runtime output** — dogfooded 2026-05-11 by Jobdori on `8cf628a5` in response to Clawhip pinpoint nudge at `1503320791582900344`. Reproduction: `claw version --output-format json` returns `{"build_date":"2026-05-11","executable_path":"/tmp/claw-dog-0530/rust/target/release/claw","git_sha":"b98b9a7","kind":"version","message":"Claw Code\n Version 0.1.0\n Git SHA b98b9a7\n Target aarch64-apple-darwin\n Build date 2026-05-11","target":"aarch64-apple-darwin","version":"0.1.0"}`. Critical provenance fields missing: (a) **`is_dirty`** — was the working tree clean at build time? Automation that pins on build provenance cannot tell if the binary was built from a clean commit or includes uncommitted changes; (b) **`branch`** — was this built from `main`, `dev/rust`, a release tag, or a feature branch? The `git_sha` alone doesn't reveal the integration point; (c) **`commit_date` / `commit_timestamp`** — only `build_date` (when the binary was compiled) is exposed; the commit itself might be days/weeks older if the build happened later. Reproducibility audits need both; (d) **`rustc_version`** — what Rust compiler version produced this binary? Critical for security advisories (e.g., known regressions in specific rustc versions); (e) **`git_sha` truncated to 7 chars** ("b98b9a7" instead of full "b98b9a71..."): 7-char shas have known collision rates in large repos and prevent unambiguous git rev-parse round-trip. **Sibling: `executable_path` leaks build-host path.** The `executable_path` field returns `/tmp/claw-dog-0530/rust/target/release/claw` — the directory where the binary was compiled, embedded into the binary metadata. For a binary copied/installed/symlinked to a different location, this field still reports the build path, not the actual invocation path. Either the field should reflect the runtime path via `std::env::current_exe()` at runtime (not compile-time), or it should be dropped to avoid leaking compile-host filesystem layout. **Sibling: prose `message` field duplicates structured data.** The `message` field still contains the entire text-mode prose version block (`"Claw Code\n Version 0.1.0\n Git SHA b98b9a7\n..."`) — every field present as structured JSON (`version`, `git_sha`, `target`, `build_date`) is also embedded in the prose. Same issue as #391 (`version json includes prose message field`) which was closed as "fixed" — the prose remains. **Required fix shape:** (a) add `is_dirty:bool`, `branch:string|null`, `commit_date:string` (ISO-8601), `commit_timestamp:int` (Unix epoch), `rustc_version:string` to the JSON envelope; (b) preserve full 40-char `git_sha` and add `git_sha_short:string` as a derived field if 7-char form is needed for UX; (c) `executable_path` should be `std::env::current_exe()` at runtime, not the compile-time path; (d) drop the prose `message` field from JSON or rename it `human_readable:string` and make it explicitly secondary to the structured fields; (e) re-verify #391 closure — the prose `message` is still present, the fix didn't fully land. **Why this matters:** version surface is the canonical provenance probe for security audits, build reproducibility, and bug-report metadata. Missing `is_dirty` means automated triage cannot distinguish "issue against a clean main commit" from "issue against a developer's uncommitted hack". Truncated `git_sha` blocks unambiguous git lookup. Leaked `executable_path` exposes build-host layout. Cross-references #391 (version prose duplication — apparently not fully fixed), #334 (version json omits build_date — fixed, but partial scope), #100 (commit identity audit). Source: Jobdori live dogfood, `8cf628a5`, 2026-05-11.
|
||||
437. **DONE — `version --output-format json` exposes complete build provenance without duplicating prose** — fixed 2026-06-04 in `fix: expose complete version provenance`. Build metadata now records the full 40-character `git_sha`, separate derived `git_sha_short`, `is_dirty`, `branch`, ISO-8601 `commit_date`, Unix `commit_timestamp`, `rustc_version`, target, and build date. Version JSON exposes those fields at top level and mirrors them under `binary_provenance`; `workspace_git_sha` is also a full SHA and `workspace_match` now compares full commit identities. `executable_path` is resolved at runtime with `std::env::current_exe()` instead of reporting a compile-host path. The prose report is no longer duplicated in JSON as `message`; JSON callers get the secondary text block as `human_readable`. Docs in `USAGE.md` and `rust/README.md` describe the provenance contract. Regression coverage: `version_emits_json_when_requested`, `version_status_doctor_include_binary_provenance_797`, `resumed_version_and_init_emit_structured_json_when_requested`, and `resumed_version_command_emits_structured_json`.
|
||||
|
||||
|
||||
438. **Memory file discovery only recognizes `CLAUDE.md` — `AGENTS.md` (industry convention used by OpenCode/Codex/Aider/Cursor) and `CLAW.md` (project's own brand name) are silently ignored despite being present in the workspace** — dogfooded 2026-05-11 by Jobdori on `d3a982dd` in response to Clawhip pinpoint nudge at `1503328341422244012`. Reproduction (fresh empty dir, isolated `CLAW_CONFIG_HOME`): create three files in cwd — `CLAUDE.md` (marker `MARKER-FROM-CLAUDE-MD`), `AGENTS.md` (marker `MARKER-FROM-AGENTS-MD`), `CLAW.md` (marker `MARKER-FROM-CLAW-MD`). Run `claw status --output-format json` → `workspace.memory_file_count: 1`. Run `claw system-prompt --output-format json` and search the `message` field for each marker: only `MARKER-FROM-CLAUDE-MD` is found; `MARKER-FROM-AGENTS-MD` and `MARKER-FROM-CLAW-MD` are absent. `claw-code` exclusively recognizes the Claude-branded filename inherited from upstream Claude Code; the project's own `CLAW.md` brand name and the cross-tool industry convention `AGENTS.md` are both silently dropped. **Three sibling implications:** (a) **brand-consistency gap**: a project rebranded from Claude Code to Claw Code that introduces `CLAUDE.md` as its only memory file is internally inconsistent. Users naturally expect `claw <subcommand>` to read `CLAW.md`. (b) **industry-convention gap**: `AGENTS.md` is the convergent convention for OpenCode (oh-my-opencode/sisyphus), OpenAI Codex CLI, Aider, Cursor, Continue.dev, and most ACP harnesses. Users with mixed-tool workflows maintain a shared `AGENTS.md` and expect every AI coding tool to honor it. (c) **silent failure mode**: there is no warning when `AGENTS.md` or `CLAW.md` exist but are not loaded. Users who copy-paste `AGENTS.md` from another tool's docs see `memory_file_count` stay at 0 or 1 and have to guess why their instructions aren't applied. **Required fix shape:** (a) discover and load **`CLAUDE.md`, `CLAW.md`, `AGENTS.md`** in that priority order (existing config-precedence pattern); (b) all three contribute to `memory_file_count` with `memory_files:[{path, source:"claude_md"|"claw_md"|"agents_md", chars}]` array exposed in `status --output-format json`; (c) when multiple files exist, merge or document the precedence: project-specific `CLAUDE.md`/`CLAW.md` overrides industry-shared `AGENTS.md`; (d) `claw doctor --output-format json` adds a `memory` check that warns when `AGENTS.md` exists but is not the loaded variant (alerting users that they may be relying on the wrong file); (e) regression test: workspace with all three files results in `memory_file_count >= 1` and the system prompt contains markers from at least the highest-precedence file. **Why this matters:** `AGENTS.md` is the lingua-franca instruction file for cross-tool AI coding workflows. A team using OpenCode for one project and Claw Code for another keeps their conventions in a shared `AGENTS.md`. Forcing them to also maintain a `CLAUDE.md` for claw-code (with identical content) is friction that breaks the value proposition of a fork. Cross-references #438 itself (the multi-file convention), and AGENTS.md ecosystem references in oh-my-opencode/sisyphus docs. Source: Jobdori live dogfood, `d3a982dd`, 2026-05-11.
|
||||
|
||||
1
USAGE.md
1
USAGE.md
@@ -51,6 +51,7 @@ cd rust
|
||||
```
|
||||
|
||||
**Note:** Diagnostic verbs (`doctor`, `status`, `sandbox`, `version`) support `--output-format json` for machine-readable output. Invalid suffix arguments (e.g., `--json`) are now rejected at parse time rather than falling through to prompt dispatch.
|
||||
`version --output-format json` reports structured build provenance including full `git_sha`, derived `git_sha_short`, `is_dirty`, `branch`, `commit_date`, `commit_timestamp`, `rustc_version`, runtime `executable_path`, and `binary_provenance`; JSON keeps the prose report in `human_readable` instead of duplicating it under `message`.
|
||||
|
||||
### Initialize a repository
|
||||
|
||||
|
||||
@@ -148,6 +148,7 @@ Top-level commands:
|
||||
|
||||
`claw acp` is a local discoverability surface for editor-first users: it reports the current ACP/Zed status without starting the runtime. As of April 16, 2026, claw-code does **not** ship an ACP/Zed daemon or JSON-RPC entrypoint yet, and `claw acp serve` is only a status alias until the real protocol surface lands. Status queries exit 0 and expose the same machine-readable contract via `--output-format json`; malformed ACP invocations exit 1 with `kind: unsupported_acp_invocation`.
|
||||
`--output-format` accepts `text` or `json` in any casing. `CLAW_OUTPUT_FORMAT=json` selects JSON as the default for non-interactive commands, explicit flags override it, repeated flags warn on stderr, and status JSON exposes `format_source`, `format_raw`, and `format_overridden`. Help and doctor output also surface `CLAW_LOG` / `RUST_LOG` as the logging environment knobs.
|
||||
`claw version --output-format json` is the provenance probe for automation: it reports full `git_sha`, derived `git_sha_short`, `is_dirty`, `branch`, `commit_date`, `commit_timestamp`, `rustc_version`, runtime `executable_path`, and `binary_provenance`; the text report is available as `human_readable` instead of a duplicate `message` field.
|
||||
Shorthand prompt mode honors the POSIX `--` end-of-flags separator, so `claw -- "-prompt-with-dash"` and unknown dash-prefixed non-flag text stay on the prompt path instead of being treated as CLI options.
|
||||
`claw dump-manifests` is self-contained: it emits the Rust resolver inventory for the selected workspace (commands, tools, agents, skills, and bootstrap phases) without requiring an upstream Claude Code TypeScript checkout. Use `--manifests-dir PATH` only to scope resolver discovery to another directory.
|
||||
|
||||
|
||||
@@ -1,10 +1,9 @@
|
||||
use std::env;
|
||||
use std::process::Command;
|
||||
|
||||
fn main() {
|
||||
// Get git SHA (short hash)
|
||||
let git_sha = Command::new("git")
|
||||
.args(["rev-parse", "--short", "HEAD"])
|
||||
fn command_output(program: &str, args: &[&str]) -> Option<String> {
|
||||
Command::new(program)
|
||||
.args(args)
|
||||
.output()
|
||||
.ok()
|
||||
.and_then(|output| {
|
||||
@@ -14,11 +13,37 @@ fn main() {
|
||||
None
|
||||
}
|
||||
})
|
||||
.map_or_else(|| "unknown".to_string(), |s| s.trim().to_string());
|
||||
.map(|value| value.trim().to_string())
|
||||
.filter(|value| !value.is_empty())
|
||||
}
|
||||
|
||||
fn main() {
|
||||
let git_sha =
|
||||
command_output("git", &["rev-parse", "HEAD"]).unwrap_or_else(|| "unknown".to_string());
|
||||
let git_sha_short = command_output("git", &["rev-parse", "--short=12", "HEAD"])
|
||||
.or_else(|| git_sha.get(..git_sha.len().min(12)).map(str::to_string))
|
||||
.unwrap_or_else(|| "unknown".to_string());
|
||||
let git_dirty = command_output("git", &["status", "--porcelain"])
|
||||
.map(|status| (!status.trim().is_empty()).to_string())
|
||||
.unwrap_or_else(|| "false".to_string());
|
||||
let git_branch = command_output("git", &["branch", "--show-current"])
|
||||
.unwrap_or_else(|| "unknown".to_string());
|
||||
let git_commit_date = command_output("git", &["show", "-s", "--format=%cI", "HEAD"])
|
||||
.unwrap_or_else(|| "unknown".to_string());
|
||||
let git_commit_timestamp = command_output("git", &["show", "-s", "--format=%ct", "HEAD"])
|
||||
.unwrap_or_else(|| "unknown".to_string());
|
||||
let rustc_version =
|
||||
command_output("rustc", &["--version"]).unwrap_or_else(|| "unknown".to_string());
|
||||
|
||||
println!("cargo:rustc-env=GIT_SHA={git_sha}");
|
||||
println!("cargo:rustc-env=GIT_SHA_SHORT={git_sha_short}");
|
||||
println!("cargo:rustc-env=GIT_DIRTY={git_dirty}");
|
||||
println!("cargo:rustc-env=GIT_BRANCH={git_branch}");
|
||||
println!("cargo:rustc-env=GIT_COMMIT_DATE={git_commit_date}");
|
||||
println!("cargo:rustc-env=GIT_COMMIT_TIMESTAMP={git_commit_timestamp}");
|
||||
println!("cargo:rustc-env=RUSTC_VERSION={rustc_version}");
|
||||
|
||||
// TARGET is always set by Cargo during build
|
||||
// TARGET is always set by Cargo during build.
|
||||
let target = env::var("TARGET").unwrap_or_else(|_| "unknown".to_string());
|
||||
println!("cargo:rustc-env=TARGET={target}");
|
||||
|
||||
@@ -35,23 +60,12 @@ fn main() {
|
||||
})
|
||||
.or_else(|| std::env::var("BUILD_DATE").ok())
|
||||
.unwrap_or_else(|| {
|
||||
// Fall back to current date via `date` command
|
||||
Command::new("date")
|
||||
.args(["+%Y-%m-%d"])
|
||||
.output()
|
||||
.ok()
|
||||
.and_then(|o| {
|
||||
if o.status.success() {
|
||||
String::from_utf8(o.stdout).ok()
|
||||
} else {
|
||||
None
|
||||
}
|
||||
})
|
||||
.map_or_else(|| "unknown".to_string(), |s| s.trim().to_string())
|
||||
command_output("date", &["+%Y-%m-%d"]).unwrap_or_else(|| "unknown".to_string())
|
||||
});
|
||||
println!("cargo:rustc-env=BUILD_DATE={build_date}");
|
||||
|
||||
// Rerun if git state changes
|
||||
println!("cargo:rerun-if-changed=.git/HEAD");
|
||||
println!("cargo:rerun-if-changed=.git/refs");
|
||||
// Rerun if git state changes. Paths are relative to this package root.
|
||||
println!("cargo:rerun-if-changed=../../../.git/HEAD");
|
||||
println!("cargo:rerun-if-changed=../../../.git/refs");
|
||||
println!("cargo:rerun-if-changed=../../../.git/index");
|
||||
}
|
||||
|
||||
@@ -268,6 +268,12 @@ const DEFAULT_OAUTH_CALLBACK_PORT: u16 = 4545;
|
||||
const VERSION: &str = env!("CARGO_PKG_VERSION");
|
||||
const BUILD_TARGET: Option<&str> = option_env!("TARGET");
|
||||
const GIT_SHA: Option<&str> = option_env!("GIT_SHA");
|
||||
const GIT_SHA_SHORT: Option<&str> = option_env!("GIT_SHA_SHORT");
|
||||
const GIT_DIRTY: Option<&str> = option_env!("GIT_DIRTY");
|
||||
const GIT_BRANCH: Option<&str> = option_env!("GIT_BRANCH");
|
||||
const GIT_COMMIT_DATE: Option<&str> = option_env!("GIT_COMMIT_DATE");
|
||||
const GIT_COMMIT_TIMESTAMP: Option<&str> = option_env!("GIT_COMMIT_TIMESTAMP");
|
||||
const RUSTC_VERSION: Option<&str> = option_env!("RUSTC_VERSION");
|
||||
const INTERNAL_PROGRESS_HEARTBEAT_INTERVAL: Duration = Duration::from_secs(3);
|
||||
const POST_TOOL_STALL_TIMEOUT: Duration = Duration::from_secs(10);
|
||||
const PRIMARY_SESSION_EXTENSION: &str = "jsonl";
|
||||
@@ -4452,9 +4458,15 @@ fn version_json_value() -> serde_json::Value {
|
||||
"kind": "version",
|
||||
"action": "show",
|
||||
"status": "ok",
|
||||
"message": render_version_report(),
|
||||
"human_readable": render_version_report(),
|
||||
"version": VERSION,
|
||||
"git_sha": binary_provenance.git_sha,
|
||||
"git_sha_short": binary_provenance.git_sha_short,
|
||||
"is_dirty": binary_provenance.is_dirty,
|
||||
"branch": binary_provenance.branch,
|
||||
"commit_date": binary_provenance.commit_date,
|
||||
"commit_timestamp": binary_provenance.commit_timestamp,
|
||||
"rustc_version": binary_provenance.rustc_version,
|
||||
"target": binary_provenance.target,
|
||||
"build_date": binary_provenance.build_date,
|
||||
"executable_path": binary_provenance.executable_path,
|
||||
@@ -4693,6 +4705,12 @@ struct StatusContext {
|
||||
#[derive(Debug, Clone, PartialEq, Eq)]
|
||||
struct BinaryProvenance {
|
||||
git_sha: Option<String>,
|
||||
git_sha_short: Option<String>,
|
||||
is_dirty: bool,
|
||||
branch: Option<String>,
|
||||
commit_date: String,
|
||||
commit_timestamp: i64,
|
||||
rustc_version: String,
|
||||
target: Option<String>,
|
||||
build_date: String,
|
||||
executable_path: Option<String>,
|
||||
@@ -4714,6 +4732,12 @@ impl BinaryProvenance {
|
||||
json!({
|
||||
"status": self.status(),
|
||||
"git_sha": self.git_sha,
|
||||
"git_sha_short": self.git_sha_short,
|
||||
"is_dirty": self.is_dirty,
|
||||
"branch": self.branch,
|
||||
"commit_date": self.commit_date,
|
||||
"commit_timestamp": self.commit_timestamp,
|
||||
"rustc_version": self.rustc_version,
|
||||
"target": self.target,
|
||||
"build_date": self.build_date,
|
||||
"executable_path": self.executable_path,
|
||||
@@ -4733,18 +4757,35 @@ fn known_build_metadata(value: Option<&str>) -> Option<String> {
|
||||
}
|
||||
}
|
||||
|
||||
fn parse_build_bool(value: Option<&str>) -> bool {
|
||||
value
|
||||
.map(str::trim)
|
||||
.is_some_and(|value| value.eq_ignore_ascii_case("true") || value == "1")
|
||||
}
|
||||
|
||||
fn parse_build_timestamp(value: Option<&str>) -> i64 {
|
||||
value
|
||||
.and_then(|value| value.trim().parse::<i64>().ok())
|
||||
.unwrap_or(0)
|
||||
}
|
||||
|
||||
fn binary_provenance_for(cwd: Option<&Path>) -> BinaryProvenance {
|
||||
let git_sha = known_build_metadata(GIT_SHA);
|
||||
let git_sha_short = known_build_metadata(GIT_SHA_SHORT).or_else(|| {
|
||||
git_sha
|
||||
.as_ref()
|
||||
.map(|sha| sha.chars().take(12).collect::<String>())
|
||||
});
|
||||
let target = known_build_metadata(BUILD_TARGET);
|
||||
let workspace_git_sha = cwd.and_then(|cwd| {
|
||||
run_git_capture_in(cwd, &["rev-parse", "--short", "HEAD"])
|
||||
run_git_capture_in(cwd, &["rev-parse", "HEAD"])
|
||||
.map(|sha| sha.trim().to_string())
|
||||
.filter(|sha| !sha.is_empty())
|
||||
});
|
||||
let workspace_match = git_sha
|
||||
.as_deref()
|
||||
.zip(workspace_git_sha.as_deref())
|
||||
.map(|(binary, workspace)| binary.starts_with(workspace) || workspace.starts_with(binary));
|
||||
.map(|(binary, workspace)| binary == workspace);
|
||||
let hint = if git_sha.is_none() {
|
||||
Some(
|
||||
"Build metadata did not include a git SHA; rebuild from a git checkout before filing provenance-sensitive dogfood reports."
|
||||
@@ -4760,6 +4801,12 @@ fn binary_provenance_for(cwd: Option<&Path>) -> BinaryProvenance {
|
||||
};
|
||||
BinaryProvenance {
|
||||
git_sha,
|
||||
git_sha_short,
|
||||
is_dirty: parse_build_bool(GIT_DIRTY),
|
||||
branch: known_build_metadata(GIT_BRANCH),
|
||||
commit_date: known_build_metadata(GIT_COMMIT_DATE).unwrap_or_else(|| "unknown".to_string()),
|
||||
commit_timestamp: parse_build_timestamp(GIT_COMMIT_TIMESTAMP),
|
||||
rustc_version: known_build_metadata(RUSTC_VERSION).unwrap_or_else(|| "unknown".to_string()),
|
||||
target,
|
||||
build_date: DEFAULT_DATE.to_string(),
|
||||
executable_path: env::current_exe()
|
||||
@@ -10285,10 +10332,12 @@ fn parse_titled_body(value: &str) -> Option<(String, String)> {
|
||||
}
|
||||
|
||||
fn render_version_report() -> String {
|
||||
let git_sha = GIT_SHA.unwrap_or("unknown");
|
||||
let git_sha = GIT_SHA_SHORT.or(GIT_SHA).unwrap_or("unknown");
|
||||
let target = BUILD_TARGET.unwrap_or("unknown");
|
||||
let branch = GIT_BRANCH.unwrap_or("unknown");
|
||||
let dirty = GIT_DIRTY.unwrap_or("unknown");
|
||||
format!(
|
||||
"Claw Code\n Version {VERSION}\n Git SHA {git_sha}\n Target {target}\n Build date {DEFAULT_DATE}"
|
||||
"Claw Code\n Version {VERSION}\n Git SHA {git_sha}\n Branch {branch}\n Dirty {dirty}\n Target {target}\n Build date {DEFAULT_DATE}"
|
||||
)
|
||||
}
|
||||
|
||||
|
||||
@@ -270,29 +270,88 @@ fn version_emits_json_when_requested() {
|
||||
"version JSON must have action:show (#711)"
|
||||
);
|
||||
assert_eq!(parsed["version"], env!("CARGO_PKG_VERSION"));
|
||||
// Provenance fields must be present for binary identification (#507).
|
||||
// Provenance fields must be present for binary identification (#507/#437).
|
||||
assert!(
|
||||
parsed.get("message").is_none(),
|
||||
"version JSON should not duplicate the text report in legacy message; use human_readable instead: {parsed}"
|
||||
);
|
||||
assert!(
|
||||
parsed["human_readable"]
|
||||
.as_str()
|
||||
.is_some_and(|text| text.contains("Claw Code")),
|
||||
"version JSON should keep text output only in human_readable: {parsed}"
|
||||
);
|
||||
let git_sha = parsed["git_sha"]
|
||||
.as_str()
|
||||
.expect("git_sha must be the full build commit SHA in version JSON");
|
||||
assert_eq!(git_sha.len(), 40, "git_sha must not be truncated: {parsed}");
|
||||
assert!(
|
||||
git_sha.chars().all(|ch| ch.is_ascii_hexdigit()),
|
||||
"git_sha must be a hex commit id: {parsed}"
|
||||
);
|
||||
let git_sha_short = parsed["git_sha_short"]
|
||||
.as_str()
|
||||
.expect("version JSON should expose the short SHA as a separate derived field");
|
||||
assert!(
|
||||
git_sha.starts_with(git_sha_short),
|
||||
"git_sha_short should derive from git_sha: {parsed}"
|
||||
);
|
||||
assert!(
|
||||
parsed["is_dirty"].is_boolean(),
|
||||
"is_dirty should be boolean: {parsed}"
|
||||
);
|
||||
assert!(
|
||||
parsed["branch"].is_string() || parsed["branch"].is_null(),
|
||||
"branch should be string|null: {parsed}"
|
||||
);
|
||||
assert!(
|
||||
parsed["commit_date"]
|
||||
.as_str()
|
||||
.is_some_and(|date| date != "unknown" && date.contains('T')),
|
||||
"commit_date should be an ISO-8601 commit timestamp string: {parsed}"
|
||||
);
|
||||
assert!(
|
||||
parsed["commit_timestamp"].as_i64().is_some_and(|ts| ts > 0),
|
||||
"commit_timestamp should be a positive Unix timestamp: {parsed}"
|
||||
);
|
||||
assert!(
|
||||
parsed["rustc_version"]
|
||||
.as_str()
|
||||
.is_some_and(|version| version.starts_with("rustc ")),
|
||||
"rustc_version should identify the compiler: {parsed}"
|
||||
);
|
||||
assert!(
|
||||
parsed["build_date"].is_string(),
|
||||
"build_date must be a string in version JSON"
|
||||
);
|
||||
assert!(
|
||||
parsed["executable_path"].is_string(),
|
||||
"executable_path must be a string in version JSON so callers can identify which binary is running"
|
||||
parsed["executable_path"].as_str().is_some_and(|path| !path.is_empty()),
|
||||
"executable_path must be a runtime path string so callers can identify which binary is running"
|
||||
);
|
||||
let binary_provenance = parsed["binary_provenance"]
|
||||
.as_object()
|
||||
.expect("version JSON must include binary_provenance object (#797)");
|
||||
.expect("version JSON must include binary_provenance object (#797/#437)");
|
||||
assert!(matches!(
|
||||
binary_provenance["status"].as_str(),
|
||||
Some("known" | "unknown")
|
||||
));
|
||||
assert_eq!(binary_provenance["git_sha"], parsed["git_sha"]);
|
||||
assert_eq!(binary_provenance["target"], parsed["target"]);
|
||||
assert_eq!(binary_provenance["build_date"], parsed["build_date"]);
|
||||
assert_eq!(
|
||||
binary_provenance["executable_path"],
|
||||
parsed["executable_path"]
|
||||
);
|
||||
for key in [
|
||||
"git_sha",
|
||||
"git_sha_short",
|
||||
"is_dirty",
|
||||
"branch",
|
||||
"commit_date",
|
||||
"commit_timestamp",
|
||||
"rustc_version",
|
||||
"target",
|
||||
"build_date",
|
||||
"executable_path",
|
||||
] {
|
||||
assert_eq!(
|
||||
binary_provenance[key], parsed[key],
|
||||
"binary_provenance.{key} should mirror top-level version field"
|
||||
);
|
||||
}
|
||||
assert!(
|
||||
binary_provenance["hint"].is_string() || binary_provenance["hint"].is_null(),
|
||||
"binary provenance must classify missing/stale lineage with a structured hint field"
|
||||
@@ -334,6 +393,14 @@ fn version_status_doctor_include_binary_provenance_797() {
|
||||
version["binary_provenance"]["workspace_match"].is_boolean()
|
||||
|| version["binary_provenance"]["workspace_match"].is_null()
|
||||
);
|
||||
let workspace_git_sha = version["binary_provenance"]["workspace_git_sha"]
|
||||
.as_str()
|
||||
.expect("workspace git sha should be a string");
|
||||
assert_eq!(
|
||||
workspace_git_sha.len(),
|
||||
40,
|
||||
"workspace_git_sha should be a full SHA, not a truncated prefix: {version}"
|
||||
);
|
||||
|
||||
let status = assert_json_command(&root, &["--output-format", "json", "status"]);
|
||||
assert_eq!(status["kind"], "status");
|
||||
@@ -1518,6 +1585,11 @@ fn resumed_version_and_init_emit_structured_json_when_requested() {
|
||||
);
|
||||
assert_eq!(version["kind"], "version");
|
||||
assert_eq!(version["version"], env!("CARGO_PKG_VERSION"));
|
||||
assert!(
|
||||
version.get("message").is_none(),
|
||||
"resumed /version JSON should not include legacy prose message: {version}"
|
||||
);
|
||||
assert!(version["human_readable"].as_str().is_some());
|
||||
|
||||
let init = assert_json_command(
|
||||
&root,
|
||||
|
||||
@@ -463,6 +463,9 @@ fn resumed_version_command_emits_structured_json() {
|
||||
assert!(parsed["version"].as_str().is_some());
|
||||
assert!(parsed["git_sha"].as_str().is_some());
|
||||
assert!(parsed["target"].as_str().is_some());
|
||||
assert!(parsed["git_sha_short"].as_str().is_some());
|
||||
assert!(parsed.get("message").is_none());
|
||||
assert!(parsed["human_readable"].as_str().is_some());
|
||||
}
|
||||
|
||||
#[test]
|
||||
|
||||
Reference in New Issue
Block a user