Skip to content

Commit 7a75fbd

Browse files
setup: Complete integration asset installation slice
Integration asset installation needed adapter-neutral asset bytes, request-level preflight, and end-to-end compatibility coverage. Use `Cow<'static, [u8]>`, preflight once per request, test staging cleanup and facade behavior, and record the resulting architecture and validation evidence. Co-authored-by: SCE <sce@crocoder.dev>
1 parent fcdee2d commit 7a75fbd

11 files changed

Lines changed: 493 additions & 43 deletions

File tree

cli/src/adapters/outbound/assets/embedded_integration_assets.rs

Lines changed: 4 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -2,6 +2,7 @@
22
//! adapter wrapping the existing generated embedded-asset catalog in
33
//! `services::setup`.
44
5+
use std::borrow::Cow;
56
use std::convert::Infallible;
67

78
use crate::application::ports::integration_asset_catalog::IntegrationAssetCatalog;
@@ -35,7 +36,7 @@ impl IntegrationAssetCatalog for EmbeddedIntegrationAssetCatalog {
3536
)
3637
.map(|asset| IntegrationAsset {
3738
relative_path: asset.relative_path.to_string(),
38-
bytes: asset.bytes,
39+
bytes: Cow::Borrowed(asset.bytes),
3940
})
4041
.collect();
4142

@@ -62,7 +63,7 @@ mod tests {
6263
)
6364
.map(|asset| IntegrationAsset {
6465
relative_path: asset.relative_path.to_string(),
65-
bytes: asset.bytes,
66+
bytes: Cow::Borrowed(asset.bytes),
6667
})
6768
.collect();
6869

@@ -87,7 +88,7 @@ mod tests {
8788
)
8889
.map(|asset| IntegrationAsset {
8990
relative_path: asset.relative_path.to_string(),
90-
bytes: asset.bytes,
91+
bytes: Cow::Borrowed(asset.bytes),
9192
})
9293
.collect();
9394

cli/src/adapters/outbound/filesystem/integration_installer.rs

Lines changed: 54 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -44,6 +44,10 @@ pub(crate) struct FilesystemIntegrationInstaller;
4444
impl IntegrationInstaller for FilesystemIntegrationInstaller {
4545
type Error = anyhow::Error;
4646

47+
fn preflight(&self, repository_root: &Path) -> Result<()> {
48+
ensure_directory_is_writable(repository_root, "setup repository root")
49+
}
50+
4751
fn install(
4852
&self,
4953
repository_root: &Path,
@@ -65,8 +69,6 @@ fn install_with_rename<F>(
6569
where
6670
F: FnMut(&Path, &Path) -> io::Result<()>,
6771
{
68-
ensure_directory_is_writable(repository_root, "setup repository root")?;
69-
7072
let destination_root = destination_root_for(repository_root, target);
7173
let staging_root = create_staging_root(repository_root, target)?;
7274

@@ -147,7 +149,7 @@ fn write_assets_to_staging(staging_root: &Path, assets: &[IntegrationAsset]) ->
147149
)
148150
})?;
149151

150-
fs::write(&destination, asset.bytes).with_context(|| {
152+
fs::write(&destination, asset.bytes.as_ref()).with_context(|| {
151153
format!(
152154
"Failed to write staged embedded asset '{}'",
153155
destination.display()
@@ -236,7 +238,7 @@ mod tests {
236238
fn asset(relative_path: &str, bytes: &'static [u8]) -> IntegrationAsset {
237239
IntegrationAsset {
238240
relative_path: relative_path.to_string(),
239-
bytes,
241+
bytes: std::borrow::Cow::Borrowed(bytes),
240242
}
241243
}
242244

@@ -268,6 +270,27 @@ mod tests {
268270
let _ = fs::remove_dir_all(&repo);
269271
}
270272

273+
#[test]
274+
fn owned_asset_bytes_reach_the_installer_unchanged() {
275+
let repo = unique_temp_dir("owned-bytes");
276+
let assets = vec![IntegrationAsset {
277+
relative_path: "owned/asset.bin".to_string(),
278+
bytes: std::borrow::Cow::Owned(vec![1, 2, 3]),
279+
}];
280+
281+
let installed = FilesystemIntegrationInstaller
282+
.install(&repo, IntegrationTarget::Claude, &assets)
283+
.expect("install should succeed");
284+
285+
assert_eq!(
286+
fs::read(installed.destination_root.join("owned/asset.bin"))
287+
.expect("read installed asset"),
288+
vec![1, 2, 3]
289+
);
290+
291+
let _ = fs::remove_dir_all(&repo);
292+
}
293+
271294
#[test]
272295
fn install_rejects_absolute_and_parent_component_paths() {
273296
let repo = unique_temp_dir("invalid-path");
@@ -286,6 +309,33 @@ mod tests {
286309
let _ = fs::remove_dir_all(&repo);
287310
}
288311

312+
#[test]
313+
fn install_cleans_up_staging_after_write_failure() {
314+
let repo = unique_temp_dir("write-failure");
315+
let assets = vec![
316+
asset("collision", b"file"),
317+
asset("collision/child.txt", b"child"),
318+
];
319+
320+
FilesystemIntegrationInstaller
321+
.install(&repo, IntegrationTarget::Claude, &assets)
322+
.expect_err("conflicting asset paths should fail during staging");
323+
assert!(!InstallTargetPaths::new(&repo).claude_target_dir().exists());
324+
325+
let leftover_staging = fs::read_dir(&repo)
326+
.expect("read repo root")
327+
.filter_map(std::result::Result::ok)
328+
.any(|entry| {
329+
entry
330+
.file_name()
331+
.to_string_lossy()
332+
.starts_with(".sce-setup-staging-")
333+
});
334+
assert!(!leftover_staging, "staging directory should be cleaned up");
335+
336+
let _ = fs::remove_dir_all(&repo);
337+
}
338+
289339
#[test]
290340
fn install_replaces_an_existing_target_without_a_backup() {
291341
let repo = unique_temp_dir("replace-existing");

cli/src/application/ports/integration_installer.rs

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -22,6 +22,8 @@ pub(crate) struct InstalledIntegrationTarget {
2222
pub(crate) trait IntegrationInstaller {
2323
type Error;
2424

25+
fn preflight(&self, repository_root: &Path) -> Result<(), Self::Error>;
26+
2527
fn install(
2628
&self,
2729
repository_root: &Path,

cli/src/application/use_cases/install_integration_assets.rs

Lines changed: 61 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -43,6 +43,10 @@ impl<C: IntegrationAssetCatalog, I: IntegrationInstaller> InstallIntegrationAsse
4343
optional_workflows: &[String],
4444
) -> Result<InstallIntegrationAssetsReport, InstallIntegrationAssetsError<C::Error, I::Error>>
4545
{
46+
self.installer
47+
.preflight(repository_root)
48+
.map_err(InstallIntegrationAssetsError::Installer)?;
49+
4650
let mut targets = Vec::new();
4751

4852
for &target in selection.targets() {
@@ -95,28 +99,40 @@ mod tests {
9599

96100
Ok(vec![IntegrationAsset {
97101
relative_path: "file.txt".to_string(),
98-
bytes: b"content",
102+
bytes: std::borrow::Cow::Borrowed(b"content"),
99103
}])
100104
}
101105
}
102106

103107
#[derive(Default)]
104108
struct FakeInstaller {
105-
calls: RefCell<Vec<(PathBuf, IntegrationTarget, Vec<IntegrationAsset>)>>,
109+
preflight_calls: RefCell<Vec<PathBuf>>,
110+
install_calls: RefCell<Vec<(PathBuf, IntegrationTarget, Vec<IntegrationAsset>)>>,
111+
preflight_error: Option<&'static str>,
106112
}
107113

108114
impl IntegrationInstaller for FakeInstaller {
109115
type Error = &'static str;
110116

117+
fn preflight(&self, repository_root: &Path) -> Result<(), Self::Error> {
118+
self.preflight_calls
119+
.borrow_mut()
120+
.push(repository_root.to_path_buf());
121+
122+
self.preflight_error.map_or(Ok(()), Err)
123+
}
124+
111125
fn install(
112126
&self,
113127
repository_root: &Path,
114128
target: IntegrationTarget,
115129
assets: &[IntegrationAsset],
116130
) -> Result<InstalledIntegrationTarget, Self::Error> {
117-
self.calls
118-
.borrow_mut()
119-
.push((repository_root.to_path_buf(), target, assets.to_vec()));
131+
self.install_calls.borrow_mut().push((
132+
repository_root.to_path_buf(),
133+
target,
134+
assets.to_vec(),
135+
));
120136

121137
Ok(InstalledIntegrationTarget {
122138
target,
@@ -150,7 +166,13 @@ mod tests {
150166
assert_eq!(catalog_calls[0].0, IntegrationTarget::Claude);
151167
assert_eq!(catalog_calls[0].1, optional_workflows);
152168

153-
let installer_calls = use_case.installer.calls.borrow();
169+
let preflight_calls = use_case.installer.preflight_calls.borrow();
170+
assert_eq!(
171+
preflight_calls.as_slice(),
172+
std::slice::from_ref(&repository_root),
173+
);
174+
175+
let installer_calls = use_case.installer.install_calls.borrow();
154176
assert_eq!(installer_calls.len(), 1);
155177
assert_eq!(installer_calls[0].0, repository_root);
156178
assert_eq!(installer_calls[0].1, IntegrationTarget::Claude);
@@ -181,7 +203,13 @@ mod tests {
181203
]
182204
);
183205

184-
let installer_calls = use_case.installer.calls.borrow();
206+
let preflight_calls = use_case.installer.preflight_calls.borrow();
207+
assert_eq!(
208+
preflight_calls.as_slice(),
209+
std::slice::from_ref(&repository_root),
210+
);
211+
212+
let installer_calls = use_case.installer.install_calls.borrow();
185213
let installer_order: Vec<IntegrationTarget> = installer_calls
186214
.iter()
187215
.map(|(_, target, _)| *target)
@@ -221,11 +249,36 @@ mod tests {
221249
vec![IntegrationTarget::OpenCode, IntegrationTarget::Claude]
222250
);
223251

224-
let installer_calls = use_case.installer.calls.borrow();
252+
let installer_calls = use_case.installer.install_calls.borrow();
225253
let installer_order: Vec<IntegrationTarget> = installer_calls
226254
.iter()
227255
.map(|(_, target, _)| *target)
228256
.collect();
229257
assert_eq!(installer_order, vec![IntegrationTarget::OpenCode]);
230258
}
259+
260+
#[test]
261+
fn preflight_error_prevents_catalog_and_install_calls() {
262+
let catalog = FakeCatalog::default();
263+
let installer = FakeInstaller {
264+
preflight_calls: RefCell::new(Vec::new()),
265+
install_calls: RefCell::new(Vec::new()),
266+
preflight_error: Some("preflight failed"),
267+
};
268+
let use_case = InstallIntegrationAssets::new(catalog, installer);
269+
let repository_root = PathBuf::from("/repo");
270+
271+
let result = use_case.execute(&repository_root, IntegrationTargetSelection::All, &[]);
272+
273+
assert!(matches!(
274+
result,
275+
Err(InstallIntegrationAssetsError::Installer("preflight failed"))
276+
));
277+
assert_eq!(
278+
use_case.installer.preflight_calls.borrow().as_slice(),
279+
[repository_root]
280+
);
281+
assert!(use_case.catalog.calls.borrow().is_empty());
282+
assert!(use_case.installer.install_calls.borrow().is_empty());
283+
}
231284
}
Lines changed: 3 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1,10 +1,11 @@
11
//! A single embedded integration asset to be installed into a repository.
22
3+
use std::borrow::Cow;
4+
35
/// An embedded asset destined for a repository-relative path within an
46
/// integration target's install root.
57
#[derive(Clone, Debug, Eq, PartialEq)]
6-
#[allow(dead_code)] // consumed starting with the IntegrationAssetCatalog port (T02)
78
pub(crate) struct IntegrationAsset {
89
pub(crate) relative_path: String,
9-
pub(crate) bytes: &'static [u8],
10+
pub(crate) bytes: Cow<'static, [u8]>,
1011
}

0 commit comments

Comments
 (0)