Move state root computation into PikaciRunStore
Intent: Eliminate the public pikaci_state_root free function from forge.rs and relocate the identical logic into a private state_root_for_repo function inside pikaci_store.rs. This makes PikaciRunStore the sole authority over where CI state lives on disk.
Affected files: crates/pika-news/src/forge.rs, crates/pika-news/src/pikaci_store.rs
Evidence
@@ -100,22 +101,6 @@ impl Drop for MirrorLockGuard {
-pub fn pikaci_state_root(repo: &ForgeRepoConfig) -> PathBuf {
- let repo_slug = repo
- .repo
- .chars()
- .map(|ch| match ch {
- 'a'..='z' | 'A'..='Z' | '0'..='9' | '-' | '_' => ch,
- _ => '-',
- })
- .collect::<String>();
- canonical_git_dir(repo)
- .parent()
- .unwrap_or_else(|| Path::new("."))
- .join("pikaci-state")
- .join(repo_slug)
-}
@@ -44,3 +70,19 @@ pub fn require_pikaci_run_store(...)
+fn state_root_for_repo(repo: &ForgeRepoConfig) -> PathBuf {
+ let repo_slug = repo
+ .repo
+ .chars()
+ .map(|ch| match ch {
+ 'a'..='z' | 'A'..='Z' | '0'..='9' | '-' | '_' => ch,
+ _ => '-',
+ })
+ .collect::<String>();
+ Path::new(&repo.canonical_git_dir)
+ .parent()
+ .unwrap_or_else(|| Path::new("."))
+ .join("pikaci-state")
+ .join(repo_slug)
+}
The pikaci_state_root function previously lived in forge.rs and was the only place that knew how to derive the on-disk state root from a ForgeRepoConfig. It is now a private function state_root_for_repo inside pikaci_store.rs.
A subtle but important change: the old code called canonical_git_dir(repo) to obtain the base path and then walked up to the parent. The new code reads repo.canonical_git_dir directly (Path::new(&repo.canonical_git_dir)), removing the dependency on forge::canonical_git_dir and the use crate::forge import entirely.
PikaciRunStore::from_forge_repo now delegates to this private helper:
pub fn from_forge_repo(repo: &ForgeRepoConfig) -> Self {
Self {
state_root: state_root_for_repo(repo),
}
}
Add test-only path accessor methods to PikaciRunStore
Intent: Provide a set of #[cfg(test)] helper methods on PikaciRunStore that encode the directory conventions for runs, jobs, and log files. This eliminates duplicated manual path joins scattered across test code.
Affected files: crates/pika-news/src/pikaci_store.rs
Evidence
@@ -28,6 +27,33 @@ impl PikaciRunStore {
+ #[cfg(test)]
+ pub fn run_dir(&self, run_id: &str) -> PathBuf {
+ self.state_root.join("runs").join(run_id)
+ }
+
+ #[cfg(test)]
+ pub fn prepared_outputs_path(&self, run_id: &str) -> PathBuf {
+ self.run_dir(run_id).join("prepared-outputs.json")
+ }
+
+ #[cfg(test)]
+ pub fn job_dir(&self, run_id: &str, job_id: &str) -> PathBuf {
+ self.run_dir(run_id).join("jobs").join(job_id)
+ }
+
+ #[cfg(test)]
+ pub fn host_log_path(&self, run_id: &str, job_id: &str) -> PathBuf {
+ self.job_dir(run_id, job_id).join("host.log")
+ }
+
+ #[cfg(test)]
+ pub fn guest_log_path(&self, run_id: &str, job_id: &str) -> PathBuf {
+ self.job_dir(run_id, job_id)
+ .join("artifacts")
+ .join("guest.log")
+ }
Five new methods are added, all gated behind #[cfg(test)]:
| Method | Returns |
run_dir(run_id) | <state_root>/runs/<run_id> |
prepared_outputs_path(run_id) | <run_dir>/prepared-outputs.json |
job_dir(run_id, job_id) | <run_dir>/jobs/<job_id> |
host_log_path(run_id, job_id) | <job_dir>/host.log |
guest_log_path(run_id, job_id) | <job_dir>/artifacts/guest.log |
These are deliberately test-only because production code reads paths through the pikaci crate's load_run_record / load_logs functions. The accessors exist solely to keep test assertions and fixture setup DRY and consistent with the canonical layout.
Update forge.rs production code to use PikaciRunStore
Intent: Replace the inline call to the now-removed pikaci_state_root with a PikaciRunStore construction, keeping the same runtime behavior while routing through the store's API.
Affected files: crates/pika-news/src/forge.rs
Evidence
@@ -343,9 +328,11 @@ where
- let structured_pikaci_state_root = structured_pikaci_target
- .as_ref()
- .map(|_| pikaci_state_root(repo));
+ let structured_pikaci_state_root = structured_pikaci_target.as_ref().map(|_| {
+ PikaciRunStore::from_forge_repo(repo)
+ .state_root()
+ .to_path_buf()
+ });
In the command-execution path, the old code called pikaci_state_root(repo) directly. The replacement constructs a PikaciRunStore and calls .state_root().to_path_buf(). The observable result is identical—a PathBuf pointing at the same directory—but the path derivation now goes through the store, ensuring a single source of truth.
A new use crate::pikaci_store::PikaciRunStore; import is added at the top of forge.rs to support this.
Migrate forge.rs tests to PikaciRunStore accessors
Intent: Replace hand-built path joins in forge test assertions with the new PikaciRunStore helper methods, making the tests resilient to future layout changes.
Affected files: crates/pika-news/src/forge.rs
Evidence
@@ -1610,10 +1598,13 @@ mod tests {
- let state_root = super::pikaci_state_root(&forge_repo);
- assert!(state_root.join("runs/pikaci-run-123/run.json").is_file());
- assert!(state_root
- .join("runs/pikaci-run-123/jobs/job-one/host.log")
+ let run_store = PikaciRunStore::from_forge_repo(&forge_repo);
+ assert!(run_store
+ .run_dir("pikaci-run-123")
+ .join("run.json")
+ .is_file());
+ assert!(run_store
+ .host_log_path("pikaci-run-123", "job-one")
.is_file());
@@ -1679,8 +1670,11 @@ mod tests {
- let state_root = super::pikaci_state_root(&forge_repo);
- assert!(state_root.join("runs/pikaci-run-456/run.json").is_file());
+ let run_store = PikaciRunStore::from_forge_repo(&forge_repo);
+ assert!(run_store
+ .run_dir("pikaci-run-456")
+ .join("run.json")
+ .is_file());
Two test functions are updated:
-
The first test previously asserted on state_root.join("runs/pikaci-run-123/run.json") and state_root.join("runs/pikaci-run-123/jobs/job-one/host.log"). It now uses run_store.run_dir("pikaci-run-123").join("run.json") and run_store.host_log_path("pikaci-run-123", "job-one").
-
The second test similarly replaces state_root.join("runs/pikaci-run-456/run.json") with run_store.run_dir("pikaci-run-456").join("run.json").
Note that run.json is still joined manually because there is no run_json_path accessor—this file is loaded by the pikaci crate's load_run_record, not by PikaciRunStore directly, so a dedicated accessor was not warranted.
Migrate web.rs test fixtures to PikaciRunStore accessors
Intent: Refactor the web module's test fixture builder and an additional test to use PikaciRunStore methods instead of raw path manipulation, completing the migration across all test code.
Affected files: crates/pika-news/src/web.rs
Evidence
@@ -4325,16 +4325,13 @@ mod tests {
fn write_pikaci_run_fixture(config: &Config, run_id: &str) {
- let state_root = PikaciRunStore::from_config(config)
- .expect("pikaci run store")
- .state_root()
- .to_path_buf();
- let run_dir = state_root.join("runs").join(run_id);
- let job_dir = run_dir.join("jobs").join("job-one");
- let prepared_outputs_path = run_dir.join("prepared-outputs.json");
+ let run_store = PikaciRunStore::from_config(config).expect("pikaci run store");
+ let run_dir = run_store.run_dir(run_id);
+ let job_dir = run_store.job_dir(run_id, "job-one");
+ let prepared_outputs_path = run_store.prepared_outputs_path(run_id);
fs::create_dir_all(&job_dir).expect("create pikaci fixture dir");
- let host_log = job_dir.join("host.log");
- let guest_log = job_dir.join("artifacts").join("guest.log");
+ let host_log = run_store.host_log_path(run_id, "job-one");
+ let guest_log = run_store.guest_log_path(run_id, "job-one");
@@ -5763,10 +5760,7 @@ mod tests {
let prepared_outputs_path = PikaciRunStore::from_config(&config)
.expect("pikaci run store")
- .state_root()
- .join("runs")
- .join("pikaci-run-invalid-prepared")
- .join("prepared-outputs.json");
+ .prepared_outputs_path("pikaci-run-invalid-prepared");
The write_pikaci_run_fixture helper was the most path-heavy call site in the test suite. It previously obtained state_root and manually built every path:
// Before
let run_dir = state_root.join("runs").join(run_id);
let job_dir = run_dir.join("jobs").join("job-one");
let host_log = job_dir.join("host.log");
let guest_log = job_dir.join("artifacts").join("guest.log");
After the change, every path is derived through PikaciRunStore:
// After
let run_dir = run_store.run_dir(run_id);
let job_dir = run_store.job_dir(run_id, "job-one");
let host_log = run_store.host_log_path(run_id, "job-one");
let guest_log = run_store.guest_log_path(run_id, "job-one");
A separate test that corrupts prepared-outputs.json also benefits, collapsing a four-segment join chain into a single prepared_outputs_path call. The net effect across both files is the elimination of all ad-hoc "runs" / "jobs" / "artifacts" string literals from test code.