mirror of
https://github.com/outbackdingo/optimclaw.git
synced 2026-08-30 16:19:21 +00:00
fix(ci): disambiguate WASM bundle filenames to prevent tool/channel collision (#964)
* fix(ci): disambiguate WASM bundle filenames to prevent tool/channel collision When a tool and channel share the same name (e.g. slack, telegram), the CI build produced identical bundle filenames, causing the second to overwrite the first. Both manifests then pointed to the wrong binary. Prefix bundle filenames with the extension kind (tool-slack-... vs channel-slack-...) and parse the prefix when patching manifests, so each manifest receives the correct artifact URL and SHA256. Co-Authored-By: Claude Opus 4.6 <[email protected]> * test(registry): add installer tests for tool/channel name disambiguation Regression tests for the CI artifact collision fix (PR #964). Verifies: - extract_tar_gz rejects archives with wrong wasm name (the collision bug) - Tool bundle extracts slack-tool.wasm correctly - Channel bundle extracts slack.wasm correctly - Tool and channel manifests install to separate directories Co-Authored-By: Claude Opus 4.6 <[email protected]> * fix(ci): add kind validation and filter non-WASM checksum entries - Validate .kind is "tool" or "channel" before using in build-wasm-extensions (hard error) - Filter checksums.txt to *-wasm32-wasip2.tar.gz entries before parsing, avoiding noisy warnings from binary artifact entries in build-local-artifacts - Add kind validation with warning+skip in both checksum-parsing loops Co-Authored-By: Claude Opus 4.6 <[email protected]> * style: fix rustfmt formatting in installer tests Co-Authored-By: Claude Opus 4.6 <[email protected]> --------- Co-authored-by: Claude Opus 4.6 <[email protected]>
This commit is contained in:
co-authored by
Claude Opus 4.6
parent
977b7fde99
commit
81f7b64994
@@ -629,6 +629,7 @@ fn is_gzip(bytes: &[u8]) -> bool {
|
||||
}
|
||||
|
||||
/// Result of extracting a tar.gz bundle.
|
||||
#[derive(Debug)]
|
||||
struct ExtractResult {
|
||||
has_capabilities: bool,
|
||||
}
|
||||
@@ -1112,4 +1113,169 @@ mod tests {
|
||||
"ChecksumMismatch on version-pinned URL must remain a hard block"
|
||||
);
|
||||
}
|
||||
|
||||
// Regression tests for tool/channel artifact name collision (PR #964).
|
||||
// When a tool and channel share the same registry filename (e.g. slack.json),
|
||||
// CI produces kind-prefixed bundles (tool-slack-*.tar.gz vs channel-slack-*.tar.gz).
|
||||
// The files *inside* each archive use manifest.name (slack-tool.wasm vs slack.wasm).
|
||||
// These tests verify the installer extracts by manifest.name correctly.
|
||||
|
||||
fn build_test_tar_gz(wasm_name: &str, caps_name: Option<&str>) -> Vec<u8> {
|
||||
use flate2::Compression;
|
||||
use flate2::write::GzEncoder;
|
||||
use tar::Builder;
|
||||
|
||||
let mut encoder = GzEncoder::new(Vec::new(), Compression::default());
|
||||
{
|
||||
let mut builder = Builder::new(&mut encoder);
|
||||
|
||||
let wasm_data = b"\0asm\x01\x00\x00\x00";
|
||||
let mut header = tar::Header::new_gnu();
|
||||
header.set_size(wasm_data.len() as u64);
|
||||
header.set_cksum();
|
||||
builder
|
||||
.append_data(&mut header, wasm_name, &wasm_data[..])
|
||||
.unwrap();
|
||||
|
||||
if let Some(caps) = caps_name {
|
||||
let caps_data = br#"{"auth":null}"#;
|
||||
let mut header = tar::Header::new_gnu();
|
||||
header.set_size(caps_data.len() as u64);
|
||||
header.set_cksum();
|
||||
builder
|
||||
.append_data(&mut header, caps, &caps_data[..])
|
||||
.unwrap();
|
||||
}
|
||||
|
||||
builder.finish().unwrap();
|
||||
}
|
||||
encoder.finish().unwrap()
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_extract_rejects_archive_with_wrong_wasm_name() {
|
||||
// Simulates the collision bug: archive contains channel's slack.wasm,
|
||||
// but installer tries to extract tool's slack-tool.wasm.
|
||||
let gz_bytes = build_test_tar_gz("slack.wasm", Some("slack.capabilities.json"));
|
||||
|
||||
let tmp = tempfile::tempdir().unwrap();
|
||||
let result = extract_tar_gz(
|
||||
&gz_bytes,
|
||||
"slack-tool",
|
||||
&tmp.path().join("slack-tool.wasm"),
|
||||
&tmp.path().join("slack-tool.capabilities.json"),
|
||||
"test://url",
|
||||
);
|
||||
|
||||
let err = result.expect_err("should fail when archive has wrong wasm name");
|
||||
match err {
|
||||
RegistryError::DownloadFailed { reason, .. } => {
|
||||
assert!(
|
||||
reason.contains("slack-tool.wasm"),
|
||||
"error should mention expected filename: {}",
|
||||
reason
|
||||
);
|
||||
}
|
||||
other => panic!("expected DownloadFailed, got: {:?}", other),
|
||||
}
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_extract_correct_wasm_from_tool_bundle() {
|
||||
// Tool bundle contains slack-tool.wasm — extraction by name="slack-tool" succeeds.
|
||||
let gz_bytes = build_test_tar_gz("slack-tool.wasm", Some("slack-tool.capabilities.json"));
|
||||
|
||||
let tmp = tempfile::tempdir().unwrap();
|
||||
let wasm_path = tmp.path().join("slack-tool.wasm");
|
||||
let caps_path = tmp.path().join("slack-tool.capabilities.json");
|
||||
|
||||
let result = extract_tar_gz(
|
||||
&gz_bytes,
|
||||
"slack-tool",
|
||||
&wasm_path,
|
||||
&caps_path,
|
||||
"test://url",
|
||||
)
|
||||
.unwrap();
|
||||
|
||||
assert!(wasm_path.exists());
|
||||
assert!(caps_path.exists());
|
||||
assert!(result.has_capabilities);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_extract_correct_wasm_from_channel_bundle() {
|
||||
// Channel bundle contains slack.wasm — extraction by name="slack" succeeds.
|
||||
let gz_bytes = build_test_tar_gz("slack.wasm", Some("slack.capabilities.json"));
|
||||
|
||||
let tmp = tempfile::tempdir().unwrap();
|
||||
let wasm_path = tmp.path().join("slack.wasm");
|
||||
let caps_path = tmp.path().join("slack.capabilities.json");
|
||||
|
||||
let result =
|
||||
extract_tar_gz(&gz_bytes, "slack", &wasm_path, &caps_path, "test://url").unwrap();
|
||||
|
||||
assert!(wasm_path.exists());
|
||||
assert!(caps_path.exists());
|
||||
assert!(result.has_capabilities);
|
||||
}
|
||||
|
||||
#[tokio::test]
|
||||
async fn test_tool_and_channel_install_to_separate_directories() {
|
||||
// Tool and channel manifests with the same file_stem ("slack") install
|
||||
// to different directories without collision.
|
||||
let temp = tempfile::tempdir().expect("tempdir");
|
||||
let installer = RegistryInstaller::new(
|
||||
temp.path().to_path_buf(),
|
||||
temp.path().join("tools"),
|
||||
temp.path().join("channels"),
|
||||
);
|
||||
|
||||
let tool_manifest = test_manifest_with_kind(
|
||||
"slack-tool",
|
||||
"tools-src/slack",
|
||||
None,
|
||||
None,
|
||||
ManifestKind::Tool,
|
||||
);
|
||||
let channel_manifest = test_manifest_with_kind(
|
||||
"slack",
|
||||
"channels-src/slack",
|
||||
None,
|
||||
None,
|
||||
ManifestKind::Channel,
|
||||
);
|
||||
|
||||
// Both fail because source dirs don't exist, but the error path reveals
|
||||
// the target directory — tool goes to tools/, channel goes to channels/.
|
||||
let tool_err = installer
|
||||
.install_from_source(&tool_manifest, false)
|
||||
.await
|
||||
.expect_err("no source dir");
|
||||
let channel_err = installer
|
||||
.install_from_source(&channel_manifest, false)
|
||||
.await
|
||||
.expect_err("no source dir");
|
||||
|
||||
match tool_err {
|
||||
RegistryError::ManifestRead { path, .. } => {
|
||||
assert!(
|
||||
path.ends_with("tools-src/slack"),
|
||||
"tool should resolve to tools-src/slack, got: {}",
|
||||
path.display()
|
||||
);
|
||||
}
|
||||
other => panic!("expected ManifestRead for tool, got: {:?}", other),
|
||||
}
|
||||
match channel_err {
|
||||
RegistryError::ManifestRead { path, .. } => {
|
||||
assert!(
|
||||
path.ends_with("channels-src/slack"),
|
||||
"channel should resolve to channels-src/slack, got: {}",
|
||||
path.display()
|
||||
);
|
||||
}
|
||||
other => panic!("expected ManifestRead for channel, got: {:?}", other),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user