use std::collections::BTreeMap;
use std::sync::{Arc, Mutex};
use async_trait::async_trait;
use serde_json::{json, Value as JsonValue};
use harn_vm::{
compile_source, install_active_connector_clients, register_vm_stdlib, reset_thread_local_state,
ClientError, ConnectorClient, ProviderId, Vm, VmError,
};
#[derive(Default)]
struct RecordingClient {
calls: Mutex<Vec<(String, JsonValue)>>,
}
#[async_trait]
impl ConnectorClient for RecordingClient {
async fn call(&self, method: &str, args: JsonValue) -> Result<JsonValue, ClientError> {
self.calls
.lock()
.expect("recording client calls lock")
.push((method.to_string(), args));
Ok(match method {
"github.actions.workflow_dispatch" => json!({
"repo": "octo-org/octo-repo", "accepted": true, "run_id": 123,
"workflow_id": "release.yml", "workflow_path": ".github/workflows/release.yml",
"run_attempt": 1, "ref": "main", "head_sha": "abc", "head_branch": "main",
"event": "workflow_dispatch", "status": "queued", "conclusion": null,
"url": "https://github.com/octo-org/octo-repo/actions/runs/123",
"api_url": "https://api.github.com/repos/octo-org/octo-repo/actions/runs/123"
}),
"github.actions.runs" => {
json!({"repo": "octo-org/octo-repo", "total_count": 1, "runs": [{
"id": 123, "status": "completed"
}], "page": 1, "per_page": 30})
}
"github.actions.run" => json!({"id": 123, "status": "completed"}),
"github.actions.run_jobs" => {
json!({"repo": "octo-org/octo-repo", "run_id": 123, "total_count": 1, "jobs": [{
"id": 456, "name": "Full workspace"
}], "page": 1, "per_page": 100})
}
"github.actions.run_cancel" => json!({
"repo": "octo-org/octo-repo", "run_id": 123, "accepted": true,
"state": "cancel_requested"
}),
"repos.get_text" => json!({"text": "v0.8.4\n"}),
"github.release.latest" => json!({
"ok": true,
"tag_name": "v0.8.4",
"asset_names": ["harn-aarch64-apple-darwin.tar.gz"]
}),
"github.release.assets" => json!({
"ok": true,
"release_id": 501,
"asset_names": ["harn-x86_64-unknown-linux-gnu.tar.gz"]
}),
"github.pr.enable_auto_merge" => json!({
"state": "already_enabled",
"auto_merge": {"merge_method": "SQUASH"},
"pull_request": {"html_url": "https://github.com/octo-org/octo-repo/pull/7"}
}),
"issues.create_comment" => json!({"id": 9001}),
"issues.update" => json!({"number": 7, "state": "closed"}),
other => return Err(ClientError::MethodNotFound(other.to_string())),
})
}
}
fn run_with_github_client(source: &str, client: Arc<RecordingClient>) -> Result<String, String> {
reset_thread_local_state();
let chunk = compile_source(source)?;
let runtime = tokio::runtime::Builder::new_current_thread()
.enable_all()
.build()
.map_err(|error| error.to_string())?;
runtime.block_on(async {
let local = tokio::task::LocalSet::new();
local
.run_until(async {
let mut clients: BTreeMap<ProviderId, Arc<dyn ConnectorClient>> = BTreeMap::new();
clients.insert(ProviderId::from("github"), client);
install_active_connector_clients(clients);
let mut vm = Vm::new();
register_vm_stdlib(&mut vm);
vm.execute(&chunk)
.await
.map_err(|error: VmError| format!("{error:?}"))?;
Ok(vm.output().to_string())
})
.await
})
}
#[test]
fn github_stdlib_wrappers_dispatch_typed_connector_methods() {
let client = Arc::new(RecordingClient::default());
let output = run_with_github_client(
r#"
import {
close_pr,
enable_auto_merge,
latest_release,
read_file_at_ref,
release_assets,
workflow_dispatch,
workflow_run,
workflow_run_cancel,
workflow_run_jobs,
workflow_runs,
} from "std/connectors/github"
pipeline main(harness: Harness, task) {
const dispatch = workflow_dispatch(harness.net, "git@github.com:octo-org/octo-repo.git", "release.yml", "main", {version: "v0.8.4"})
assert_eq(dispatch.run_id, 123, "dispatch result")
assert_eq(workflow_runs(harness.net, "octo-org/octo-repo", {event: "workflow_dispatch"}).runs[0].id, 123, "runs result")
assert_eq(workflow_run(harness.net, "octo-org/octo-repo", 123).status, "completed", "run result")
assert_eq(workflow_run_jobs(harness.net, "octo-org/octo-repo", 123).jobs[0].id, 456, "jobs result")
assert_eq(workflow_run_cancel(harness.net, "octo-org/octo-repo", 123).accepted, true, "cancel result")
assert_eq(trim(read_file_at_ref(harness.net, "https://github.com/octo-org/octo-repo.git", ".harn-version", "main").text), "v0.8.4", "file text")
assert_eq(latest_release(harness.net, "octo-org/octo-repo").tag_name, "v0.8.4", "release tag")
assert_eq(release_assets(harness.net, "octo-org/octo-repo", 501).asset_names[0], "harn-x86_64-unknown-linux-gnu.tar.gz", "asset names")
const auto_merge = enable_auto_merge(harness.net, "octo-org/octo-repo", 7, {method: "squash"})
assert_eq(auto_merge.ok, true, "auto-merge ok")
assert_eq(auto_merge.state, "already_enabled", "auto-merge state")
assert_eq(auto_merge.strategy, "graphql_auto_merge", "auto-merge strategy")
const closed = close_pr(harness.net, "octo-org/octo-repo", 7, "Closing in favor of the release branch.")
assert_eq(closed.ok, true, "close ok")
assert_eq(closed.comment_posted, true, "close comment")
harness.stdio.log("github wrappers ok")
}
"#,
client.clone(),
)
.expect("Harn source should execute");
assert!(output.contains("[harn] github wrappers ok"));
let calls = client.calls.lock().expect("recording client calls lock");
let methods = calls
.iter()
.map(|(method, _)| method.as_str())
.collect::<Vec<_>>();
assert_eq!(
methods,
vec![
"github.actions.workflow_dispatch",
"github.actions.runs",
"github.actions.run",
"github.actions.run_jobs",
"github.actions.run_cancel",
"repos.get_text",
"github.release.latest",
"github.release.assets",
"github.pr.enable_auto_merge",
"issues.create_comment",
"issues.update",
]
);
assert_eq!(calls[0].1["repo"], "octo-org/octo-repo");
assert_eq!(calls[0].1["workflow_id"], "release.yml");
assert_eq!(calls[5].1["ref"], "main");
assert_eq!(calls[8].1["pull_number"], 7);
assert_eq!(calls[10].1["state"], "closed");
}