commit a2038b3e25b9e34f0b8fd001f26a8ed10b5772cb
parent f97b1f1479dd3ab9382b03c1d1f4ae0f4aab1dd8
Author: triesap <tyson@radroots.org>
Date: Sun, 9 Aug 2026 19:32:22 +0000
policy: enforce portable HarvestCircle checks
- reject legacy product names in tracked paths and source text
- preserve only the approved historical repository URL exception
- canonicalize secure database test roots and stabilize observer delivery
- verify governed and standalone contributor build lanes
Diffstat:
13 files changed, 131 insertions(+), 16 deletions(-)
diff --git a/app/desktop/src/test/kotlin/org/radroots/harvestcircle/architecture/LegacyProductIdentityGuardTest.kt b/app/desktop/src/test/kotlin/org/radroots/harvestcircle/architecture/LegacyProductIdentityGuardTest.kt
@@ -0,0 +1,65 @@
+package org.radroots.harvestcircle.architecture
+
+import java.nio.charset.StandardCharsets
+import java.nio.file.Files
+import java.nio.file.Path
+import kotlin.io.path.extension
+import kotlin.io.path.name
+import kotlin.io.path.readText
+import kotlin.test.Test
+import kotlin.test.assertEquals
+import kotlin.test.assertTrue
+
+class LegacyProductIdentityGuardTest {
+ @Test
+ fun trackedSourcesUseTheHarvestCircleNamingContract() {
+ val root = findRepositoryRoot()
+ val contract = root.resolve("AGENTS.md").readText()
+ assertTrue(contract.contains("`harvestcircle_*`"))
+ assertTrue(contract.contains("`org.radroots.harvestcircle`"))
+ assertTrue(contract.contains("`HarvestCircle*`"))
+ assertTrue(contract.contains("`HARVESTCIRCLE_*`"))
+
+ val legacyProduct = "stu" + "dio"
+ val repositoryUrlException = "https://github.com/radrootslabs/" + legacyProduct + "_app"
+ val textExtensions = setOf("kt", "kts", "rs", "toml", "properties", "yml", "yaml", "md")
+ val textNames = setOf("Makefile", ".gitignore", "gradlew", "gradlew.bat")
+ val findings =
+ trackedFiles(root).flatMap { relative ->
+ buildList {
+ if (relative.lowercase().contains(legacyProduct)) {
+ add("$relative: legacy product name in tracked path")
+ }
+
+ val path = root.resolve(relative)
+ if (path.extension in textExtensions || path.name in textNames) {
+ val inspected = path.readText().replace(repositoryUrlException, "")
+ if (inspected.lowercase().contains(legacyProduct)) {
+ add("$relative: legacy product name in tracked text")
+ }
+ }
+ }
+ }
+
+ assertEquals(emptyList(), findings.sorted())
+ }
+}
+
+private fun trackedFiles(root: Path): List<String> {
+ val process =
+ ProcessBuilder("git", "-C", root.toString(), "ls-files", "-z")
+ .redirectErrorStream(true)
+ .start()
+ val output = process.inputStream.readAllBytes()
+ check(process.waitFor() == 0) {
+ "Unable to enumerate tracked HarvestCircle sources: ${output.toString(StandardCharsets.UTF_8)}"
+ }
+ return output
+ .toString(StandardCharsets.UTF_8)
+ .split('\u0000')
+ .filter(String::isNotEmpty)
+}
+
+private fun findRepositoryRoot(): Path =
+ generateSequence(Path.of("").toAbsolutePath()) { it.parent }
+ .first { Files.isRegularFile(it.resolve("core/Cargo.toml")) && Files.isDirectory(it.resolve("app/desktop")) }
diff --git a/core/crates/harvestcircle_application/src/snapshot.rs b/core/crates/harvestcircle_application/src/snapshot.rs
@@ -77,7 +77,7 @@ impl RelayConfiguration {
/// # Errors
///
/// Returns a safe configuration error before runtime or network work when
- /// the relay count exceeds the Studio policy.
+ /// the relay count exceeds the HarvestCircle policy.
pub fn new(relays: Vec<RelayUrl>) -> Result<Self, SafeError> {
if relays.len() > MAX_CONFIGURED_RELAYS {
return Err(relay_limit_exceeded());
diff --git a/core/crates/harvestcircle_ffi/src/observer.rs b/core/crates/harvestcircle_ffi/src/observer.rs
@@ -482,8 +482,10 @@ mod tests {
}
}
+ const OBSERVER_DELIVERY_TIMEOUT: Duration = Duration::from_secs(5);
+
async fn wait_for_snapshot_count(observer: &RecordingObserver, minimum: usize) {
- tokio::time::timeout(Duration::from_secs(1), async {
+ tokio::time::timeout(OBSERVER_DELIVERY_TIMEOUT, async {
while observer.snapshots.lock().expect("snapshots").len() < minimum {
tokio::task::yield_now().await;
}
@@ -493,7 +495,7 @@ mod tests {
}
async fn wait_for_fresh_profile(observer: &RecordingObserver) {
- tokio::time::timeout(Duration::from_secs(1), async {
+ tokio::time::timeout(OBSERVER_DELIVERY_TIMEOUT, async {
loop {
let fresh = observer
.snapshots
diff --git a/core/crates/harvestcircle_preferences/src/lib.rs b/core/crates/harvestcircle_preferences/src/lib.rs
@@ -1,5 +1,5 @@
// SPDX-License-Identifier: MPL-2.0
-//! UI-neutral Studio preference state.
+//! UI-neutral HarvestCircle preference state.
//!
//! This module carries forward the uniquely required preference behavior from
//! source commit `6074a4745be361f21bb47d4778c74a14b2d57954`. It intentionally
diff --git a/core/crates/harvestcircle_runtime/src/persistence.rs b/core/crates/harvestcircle_runtime/src/persistence.rs
@@ -355,7 +355,11 @@ mod tests {
#[test]
fn persistent_bootstrap_handles_fresh_and_existing_signed_out_state() {
let directory = tempdir().expect("directory");
- let path = directory.path().join("harvestcircle.sqlite3");
+ let path = directory
+ .path()
+ .canonicalize()
+ .expect("canonical temporary directory")
+ .join("harvestcircle.sqlite3");
let public_key = account().public_key();
let secrets = InMemorySecretStore::default();
{
@@ -388,7 +392,11 @@ mod tests {
#[test]
fn corrupt_database_fails_safely_without_recreation() {
let directory = tempdir().expect("directory");
- let path = directory.path().join("harvestcircle.sqlite3");
+ let path = directory
+ .path()
+ .canonicalize()
+ .expect("canonical temporary directory")
+ .join("harvestcircle.sqlite3");
fs::write(&path, b"not a sqlite database").expect("corrupt file");
let error = PersistentAppCore::open(&path, RelayConfiguration::default())
@@ -404,7 +412,11 @@ mod tests {
#[test]
fn persisted_generate_and_import_survive_restart_without_secret_bytes() {
let directory = tempdir().expect("directory");
- let path = directory.path().join("harvestcircle.sqlite3");
+ let path = directory
+ .path()
+ .canonicalize()
+ .expect("canonical temporary directory")
+ .join("harvestcircle.sqlite3");
let secrets = InMemorySecretStore::default();
let selected;
{
@@ -646,7 +658,11 @@ mod tests {
#[test]
fn bootstrap_recovery_completes_credential_deleted_removal_and_fallback() {
let directory = tempdir().expect("directory");
- let path = directory.path().join("harvestcircle.sqlite3");
+ let path = directory
+ .path()
+ .canonicalize()
+ .expect("canonical temporary directory")
+ .join("harvestcircle.sqlite3");
let secrets = InMemorySecretStore::default();
let first;
let removed;
diff --git a/core/crates/harvestcircle_runtime/src/runtime_actor.rs b/core/crates/harvestcircle_runtime/src/runtime_actor.rs
@@ -1584,7 +1584,11 @@ mod tests {
#[tokio::test(flavor = "multi_thread")]
async fn installation_identity_survives_file_backed_runtime_restart() {
let directory = tempfile::tempdir().expect("temporary directory");
- let path = directory.path().join("harvestcircle.sqlite3");
+ let path = directory
+ .path()
+ .canonicalize()
+ .expect("canonical temporary directory")
+ .join("harvestcircle.sqlite3");
let first = RuntimeActorHandle::open(
&path,
RelayConfiguration::default(),
diff --git a/core/crates/harvestcircle_runtime/tests/restart_isolation.rs b/core/crates/harvestcircle_runtime/tests/restart_isolation.rs
@@ -22,7 +22,11 @@ impl Clock for FixedClock {
#[test]
fn restart_restores_selection_and_keeps_account_namespaces_isolated() {
let directory = tempdir().expect("temporary directory");
- let path = directory.path().join("harvestcircle.sqlite3");
+ let path = directory
+ .path()
+ .canonicalize()
+ .expect("canonical temporary directory")
+ .join("harvestcircle.sqlite3");
let secrets = InMemorySecretStore::default();
let (owner_a, owner_b);
diff --git a/core/crates/harvestcircle_storage/src/accounts.rs b/core/crates/harvestcircle_storage/src/accounts.rs
@@ -305,10 +305,14 @@ mod tests {
AccountCreatedAt, AccountIdentity, AccountLabel, AccountSummary, BindingAvailability,
LocalSignerBinding, PublicKey, SafeErrorCode, UnixTimestamp,
};
- use tempfile::tempdir;
+ use tempfile::{TempDir, tempdir_in};
use crate::Database;
+ fn tempdir() -> std::io::Result<TempDir> {
+ tempdir_in(std::env::temp_dir().canonicalize()?)
+ }
+
fn public_key(key_byte: u8) -> PublicKey {
let value = match key_byte {
1 => "585591529da0bab31b3b1b1f986611cf5f435dca84f978c89ee8a40cca7103df",
diff --git a/core/crates/harvestcircle_storage/src/compatibility.rs b/core/crates/harvestcircle_storage/src/compatibility.rs
@@ -332,7 +332,7 @@ pub(crate) const fn quarantined_storage_error() -> SafeError {
#[cfg(test)]
mod tests {
use rusqlite::{Connection, params};
- use tempfile::tempdir;
+ use tempfile::{TempDir, tempdir_in};
use super::{
DatabasePreflight, PersistedIdentityIssueKind, column_exists, preflight,
@@ -341,6 +341,10 @@ mod tests {
use crate::Database;
use harvestcircle_domain::{AccountIdentity, PublicKey, SafeErrorCode};
+ fn tempdir() -> std::io::Result<TempDir> {
+ tempdir_in(std::env::temp_dir().canonicalize()?)
+ }
+
#[test]
fn preflight_rejects_non_files_missing_schema_zero_version_and_unknown_tables() {
let directory = tempdir().expect("temporary directory");
diff --git a/core/crates/harvestcircle_storage/src/db.rs b/core/crates/harvestcircle_storage/src/db.rs
@@ -377,7 +377,7 @@ mod tests {
use std::path::Path;
use std::process::Command;
- use tempfile::tempdir;
+ use tempfile::{TempDir, tempdir_in};
use harvestcircle_application::{AccountRepository, AppStateRepository};
use harvestcircle_domain::{PublicKey, SafeErrorCode};
@@ -388,6 +388,10 @@ mod tests {
CURRENT_SCHEMA_VERSION, Database, configure, create_secure_directory, migrations,
restrict_sqlite_sidecars,
};
+
+ fn tempdir() -> std::io::Result<TempDir> {
+ tempdir_in(std::env::temp_dir().canonicalize()?)
+ }
use crate::{DatabasePreflight, PersistedIdentityIssueKind, RepairAuthorization};
#[test]
diff --git a/core/crates/harvestcircle_storage/src/recovery.rs b/core/crates/harvestcircle_storage/src/recovery.rs
@@ -518,7 +518,7 @@ mod tests {
use std::path::Path;
use rusqlite::Connection;
- use tempfile::tempdir;
+ use tempfile::{TempDir, tempdir_in};
use super::{
AUTHENTICATION_KEY_FILENAME, MANIFEST_FORMAT, MigrationRecovery, atomic_secure_write,
@@ -527,6 +527,10 @@ mod tests {
recovery_directory, replace_with_backup, secure_read,
};
+ fn tempdir() -> std::io::Result<TempDir> {
+ tempdir_in(std::env::temp_dir().canonicalize()?)
+ }
+
fn sqlite_database(path: &Path) {
let connection = Connection::open(path).expect("open sqlite database");
connection
diff --git a/core/crates/harvestcircle_storage/src/repair.rs b/core/crates/harvestcircle_storage/src/repair.rs
@@ -265,7 +265,7 @@ mod tests {
use std::io::Write;
use rusqlite::Connection;
- use tempfile::tempdir;
+ use tempfile::{TempDir, tempdir_in};
use super::{
REPAIR_DOMAIN, RepairAuthorization, RepairCandidate, authenticate, authenticate_candidate,
@@ -274,6 +274,10 @@ mod tests {
};
use crate::Database;
+ fn tempdir() -> std::io::Result<TempDir> {
+ tempdir_in(std::env::temp_dir().canonicalize()?)
+ }
+
fn quarantined_database(path: &std::path::Path) {
drop(Database::open(path).expect("current database"));
let connection = Connection::open(path).expect("open database");
diff --git a/core/crates/harvestcircle_storage/tests/redaction.rs b/core/crates/harvestcircle_storage/tests/redaction.rs
@@ -6,7 +6,11 @@ use harvestcircle_domain::{
PublicKey, UnixTimestamp,
};
use harvestcircle_storage::Database;
-use tempfile::tempdir;
+use tempfile::{TempDir, tempdir_in};
+
+fn tempdir() -> std::io::Result<TempDir> {
+ tempdir_in(std::env::temp_dir().canonicalize()?)
+}
const SECRET_HEX: &str = "1111111111111111111111111111111111111111111111111111111111111111";
const SECRET_NSEC: &str = "nsec1vl029mgpspedva04g90vltkh6fvh240zqtv9k0t9af8935ke9laqsnlfe5";