chore(wip): checkpoint P8/C avant chantier Codex inter-agents
Sauvegarde de l'arbre de travail en cours (persistance P8, conversations C-series, write-portal frontend, médiation d'entrée) avant d'attaquer le support de la délégation inter-agents pour les profils Codex. Le round-trip inter-agent question/réponse est couvert sans tokens par les tests loopback existants (state::mcp_e2e_loopback_tests). Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
This commit is contained in:
@ -1312,7 +1312,9 @@ async fn launch_conventionfile_injects_project_memory_in_order() {
|
||||
"memory section present: {doc}"
|
||||
);
|
||||
assert!(
|
||||
doc.contains("- [Git optionnel](.ideai/memory/git-optional.md) — git reste un simple tool (project)"),
|
||||
doc.contains(
|
||||
"- [Git optionnel](.ideai/memory/git-optional.md) — git reste un simple tool (project)"
|
||||
),
|
||||
"first memory line exact format: {doc}"
|
||||
);
|
||||
assert!(
|
||||
@ -1589,23 +1591,23 @@ async fn launch_without_mcp_writes_no_mcp_config_and_leaves_spec_untouched() {
|
||||
async fn launch_mcp_config_file_writes_declaration_at_run_dir_target() {
|
||||
// Test 2 — ConfigFile { target: ".mcp.json" } ⇒ a file is written at
|
||||
// <run_dir>/.mcp.json with a non-empty, coherent MCP server declaration.
|
||||
let (launch, agent, fs, _pty, _bus, _sessions, _tr, _session) =
|
||||
launch_fixture_with_profile(
|
||||
profile_with_mcp(
|
||||
pid(9),
|
||||
McpConfigStrategy::config_file(".mcp.json").unwrap(),
|
||||
),
|
||||
Some(ContextInjectionPlan::File {
|
||||
target: "CLAUDE.md".to_owned(),
|
||||
}),
|
||||
);
|
||||
let (launch, agent, fs, _pty, _bus, _sessions, _tr, _session) = launch_fixture_with_profile(
|
||||
profile_with_mcp(pid(9), McpConfigStrategy::config_file(".mcp.json").unwrap()),
|
||||
Some(ContextInjectionPlan::File {
|
||||
target: "CLAUDE.md".to_owned(),
|
||||
}),
|
||||
);
|
||||
|
||||
launch.execute(launch_input(agent.id)).await.unwrap();
|
||||
|
||||
let run_dir = format!("/home/me/proj/.ideai/run/{}", agent.id);
|
||||
let mcp = writes_ending_with(&fs, "/.mcp.json");
|
||||
assert_eq!(mcp.len(), 1, "exactly one MCP config file written");
|
||||
assert_eq!(mcp[0].0, format!("{run_dir}/.mcp.json"), "written at run dir");
|
||||
assert_eq!(
|
||||
mcp[0].0,
|
||||
format!("{run_dir}/.mcp.json"),
|
||||
"written at run dir"
|
||||
);
|
||||
|
||||
let body = String::from_utf8(mcp[0].1.clone()).unwrap();
|
||||
assert!(!body.trim().is_empty(), "MCP declaration is non-empty");
|
||||
@ -1615,25 +1617,20 @@ async fn launch_mcp_config_file_writes_declaration_at_run_dir_target() {
|
||||
"MCP declaration must declare the IdeA MCP server, got: {body}"
|
||||
);
|
||||
// It is valid JSON.
|
||||
let _: serde_json::Value =
|
||||
serde_json::from_str(&body).expect("MCP declaration is valid JSON");
|
||||
let _: serde_json::Value = serde_json::from_str(&body).expect("MCP declaration is valid JSON");
|
||||
}
|
||||
|
||||
#[tokio::test]
|
||||
async fn launch_mcp_config_file_is_non_clobbering() {
|
||||
// Test 3 — if <run_dir>/.mcp.json already exists, the launch must NOT overwrite
|
||||
// it: no write is recorded against that path.
|
||||
let profile = profile_with_mcp(
|
||||
pid(9),
|
||||
McpConfigStrategy::config_file(".mcp.json").unwrap(),
|
||||
let profile = profile_with_mcp(pid(9), McpConfigStrategy::config_file(".mcp.json").unwrap());
|
||||
let (launch, agent, fs, _pty, _bus, _sessions, _tr, _session) = launch_fixture_with_profile(
|
||||
profile,
|
||||
Some(ContextInjectionPlan::File {
|
||||
target: "CLAUDE.md".to_owned(),
|
||||
}),
|
||||
);
|
||||
let (launch, agent, fs, _pty, _bus, _sessions, _tr, _session) =
|
||||
launch_fixture_with_profile(
|
||||
profile,
|
||||
Some(ContextInjectionPlan::File {
|
||||
target: "CLAUDE.md".to_owned(),
|
||||
}),
|
||||
);
|
||||
// Pre-existing MCP config file on disk.
|
||||
let run_dir = format!("/home/me/proj/.ideai/run/{}", agent.id);
|
||||
fs.mark_existing(&format!("{run_dir}/.mcp.json"));
|
||||
@ -1649,17 +1646,13 @@ async fn launch_mcp_config_file_is_non_clobbering() {
|
||||
#[tokio::test]
|
||||
async fn launch_mcp_config_file_write_failure_is_best_effort() {
|
||||
// Test 4 — a write failure on the MCP config file must NOT fail the launch.
|
||||
let profile = profile_with_mcp(
|
||||
pid(9),
|
||||
McpConfigStrategy::config_file(".mcp.json").unwrap(),
|
||||
let profile = profile_with_mcp(pid(9), McpConfigStrategy::config_file(".mcp.json").unwrap());
|
||||
let (launch, agent, fs, pty, _bus, _sessions, _tr, _session) = launch_fixture_with_profile(
|
||||
profile,
|
||||
Some(ContextInjectionPlan::File {
|
||||
target: "CLAUDE.md".to_owned(),
|
||||
}),
|
||||
);
|
||||
let (launch, agent, fs, pty, _bus, _sessions, _tr, _session) =
|
||||
launch_fixture_with_profile(
|
||||
profile,
|
||||
Some(ContextInjectionPlan::File {
|
||||
target: "CLAUDE.md".to_owned(),
|
||||
}),
|
||||
);
|
||||
// Force the MCP config write to fail.
|
||||
fs.fail_write_suffix("/.mcp.json");
|
||||
|
||||
@ -1676,13 +1669,12 @@ async fn launch_mcp_config_file_write_failure_is_best_effort() {
|
||||
async fn launch_mcp_flag_appends_flag_and_path_to_args() {
|
||||
// Test 5 — Flag { flag: "--mcp-config" } ⇒ spec.args carries the flag followed
|
||||
// by the run-dir config path.
|
||||
let (launch, agent, _fs, pty, _bus, _sessions, _tr, _session) =
|
||||
launch_fixture_with_profile(
|
||||
profile_with_mcp(pid(9), McpConfigStrategy::flag("--mcp-config").unwrap()),
|
||||
Some(ContextInjectionPlan::File {
|
||||
target: "CLAUDE.md".to_owned(),
|
||||
}),
|
||||
);
|
||||
let (launch, agent, _fs, pty, _bus, _sessions, _tr, _session) = launch_fixture_with_profile(
|
||||
profile_with_mcp(pid(9), McpConfigStrategy::flag("--mcp-config").unwrap()),
|
||||
Some(ContextInjectionPlan::File {
|
||||
target: "CLAUDE.md".to_owned(),
|
||||
}),
|
||||
);
|
||||
|
||||
launch.execute(launch_input(agent.id)).await.unwrap();
|
||||
|
||||
@ -1704,13 +1696,12 @@ async fn launch_mcp_flag_appends_flag_and_path_to_args() {
|
||||
#[tokio::test]
|
||||
async fn launch_mcp_env_appends_var_and_path_to_env() {
|
||||
// Test 6 — Env { var: "IDEA_MCP" } ⇒ spec.env carries (IDEA_MCP, <run_dir>).
|
||||
let (launch, agent, _fs, pty, _bus, _sessions, _tr, _session) =
|
||||
launch_fixture_with_profile(
|
||||
profile_with_mcp(pid(9), McpConfigStrategy::env("IDEA_MCP").unwrap()),
|
||||
Some(ContextInjectionPlan::File {
|
||||
target: "CLAUDE.md".to_owned(),
|
||||
}),
|
||||
);
|
||||
let (launch, agent, _fs, pty, _bus, _sessions, _tr, _session) = launch_fixture_with_profile(
|
||||
profile_with_mcp(pid(9), McpConfigStrategy::env("IDEA_MCP").unwrap()),
|
||||
Some(ContextInjectionPlan::File {
|
||||
target: "CLAUDE.md".to_owned(),
|
||||
}),
|
||||
);
|
||||
|
||||
launch.execute(launch_input(agent.id)).await.unwrap();
|
||||
|
||||
@ -1761,13 +1752,12 @@ async fn launch_mcp_runtime_writes_real_declaration_with_ordered_args() {
|
||||
// declaration is the REAL one: command == exe, and args == the ordered
|
||||
// ["mcp-server","--endpoint",endpoint,"--project",project_id,"--requester",
|
||||
// requester]. Structure-asserted via parsed JSON (not a fragile string match).
|
||||
let (launch, agent, fs, _pty, _bus, _sessions, _tr, _session) =
|
||||
launch_fixture_with_profile(
|
||||
profile_with_mcp(pid(9), McpConfigStrategy::config_file(".mcp.json").unwrap()),
|
||||
Some(ContextInjectionPlan::File {
|
||||
target: "CLAUDE.md".to_owned(),
|
||||
}),
|
||||
);
|
||||
let (launch, agent, fs, _pty, _bus, _sessions, _tr, _session) = launch_fixture_with_profile(
|
||||
profile_with_mcp(pid(9), McpConfigStrategy::config_file(".mcp.json").unwrap()),
|
||||
Some(ContextInjectionPlan::File {
|
||||
target: "CLAUDE.md".to_owned(),
|
||||
}),
|
||||
);
|
||||
|
||||
let exe = "/abs/idea";
|
||||
let endpoint = "/run/idea-mcp/abc.sock";
|
||||
@ -1820,13 +1810,12 @@ async fn launch_mcp_runtime_special_chars_stay_valid_json() {
|
||||
// M5d-2 — an exe/endpoint with a space and a backslash ⇒ the .mcp.json stays
|
||||
// valid JSON (parse OK, asserted by read_single_mcp_json) and the values are
|
||||
// faithfully preserved (correct escaping, no corruption).
|
||||
let (launch, agent, fs, _pty, _bus, _sessions, _tr, _session) =
|
||||
launch_fixture_with_profile(
|
||||
profile_with_mcp(pid(9), McpConfigStrategy::config_file(".mcp.json").unwrap()),
|
||||
Some(ContextInjectionPlan::File {
|
||||
target: "CLAUDE.md".to_owned(),
|
||||
}),
|
||||
);
|
||||
let (launch, agent, fs, _pty, _bus, _sessions, _tr, _session) = launch_fixture_with_profile(
|
||||
profile_with_mcp(pid(9), McpConfigStrategy::config_file(".mcp.json").unwrap()),
|
||||
Some(ContextInjectionPlan::File {
|
||||
target: "CLAUDE.md".to_owned(),
|
||||
}),
|
||||
);
|
||||
|
||||
let exe = r"C:\Program Files\IdeA\idea.exe";
|
||||
let endpoint = r"/tmp/idea mcp/a b\c.sock";
|
||||
@ -1867,13 +1856,12 @@ async fn launch_mcp_runtime_special_chars_stay_valid_json() {
|
||||
async fn launch_mcp_runtime_none_writes_minimal_declaration() {
|
||||
// M5d-3 — mcp_runtime = None + an MCP profile ⇒ the minimal declaration is
|
||||
// written: command == "idea", args == ["mcp-server"]. Still valid JSON.
|
||||
let (launch, agent, fs, _pty, _bus, _sessions, _tr, _session) =
|
||||
launch_fixture_with_profile(
|
||||
profile_with_mcp(pid(9), McpConfigStrategy::config_file(".mcp.json").unwrap()),
|
||||
Some(ContextInjectionPlan::File {
|
||||
target: "CLAUDE.md".to_owned(),
|
||||
}),
|
||||
);
|
||||
let (launch, agent, fs, _pty, _bus, _sessions, _tr, _session) = launch_fixture_with_profile(
|
||||
profile_with_mcp(pid(9), McpConfigStrategy::config_file(".mcp.json").unwrap()),
|
||||
Some(ContextInjectionPlan::File {
|
||||
target: "CLAUDE.md".to_owned(),
|
||||
}),
|
||||
);
|
||||
|
||||
// launch_input has mcp_runtime: None.
|
||||
launch.execute(launch_input(agent.id)).await.unwrap();
|
||||
@ -1928,22 +1916,28 @@ async fn launch_mcp_runtime_with_no_mcp_profile_writes_nothing() {
|
||||
}
|
||||
|
||||
#[tokio::test]
|
||||
async fn launch_mcp_runtime_is_non_clobbering() {
|
||||
// M5d-5 — a pre-existing .mcp.json is NOT overwritten, even when a real runtime
|
||||
// is injected (unchanged non-clobbering contract from M1).
|
||||
let (launch, agent, fs, _pty, _bus, _sessions, _tr, _session) =
|
||||
launch_fixture_with_profile(
|
||||
profile_with_mcp(pid(9), McpConfigStrategy::config_file(".mcp.json").unwrap()),
|
||||
Some(ContextInjectionPlan::File {
|
||||
target: "CLAUDE.md".to_owned(),
|
||||
}),
|
||||
);
|
||||
async fn launch_mcp_runtime_clobbers_stale_config_with_fresh_declaration() {
|
||||
// M5d-5 — with a real injected runtime, a pre-existing .mcp.json MUST be
|
||||
// regenerated and CLOBBERED on every (re)launch: its `command` (the IdeA exe
|
||||
// path) and endpoint drift between runs (the AppImage internal mount path
|
||||
// changes at each remount/reboot), so a stale declaration would point the bridge
|
||||
// at a dead binary/socket. `.mcp.json` is IdeA-managed, so clobbering is safe.
|
||||
let (launch, agent, fs, _pty, _bus, _sessions, _tr, _session) = launch_fixture_with_profile(
|
||||
profile_with_mcp(pid(9), McpConfigStrategy::config_file(".mcp.json").unwrap()),
|
||||
Some(ContextInjectionPlan::File {
|
||||
target: "CLAUDE.md".to_owned(),
|
||||
}),
|
||||
);
|
||||
let run_dir = format!("/home/me/proj/.ideai/run/{}", agent.id);
|
||||
// A stale file already on disk (e.g. written by a previous run pointing at a
|
||||
// now-dead AppImage mount).
|
||||
fs.mark_existing(&format!("{run_dir}/.mcp.json"));
|
||||
|
||||
let exe = "/abs/idea";
|
||||
let endpoint = "/run/idea-mcp/abc.sock";
|
||||
let runtime = application::McpRuntime {
|
||||
exe: "/abs/idea".to_owned(),
|
||||
endpoint: "/run/idea-mcp/abc.sock".to_owned(),
|
||||
exe: exe.to_owned(),
|
||||
endpoint: endpoint.to_owned(),
|
||||
project_id: "0123456789abcdef0123456789abcdef".to_owned(),
|
||||
requester: "agent-7".to_owned(),
|
||||
};
|
||||
@ -1953,9 +1947,118 @@ async fn launch_mcp_runtime_is_non_clobbering() {
|
||||
.await
|
||||
.unwrap();
|
||||
|
||||
// The fresh declaration is written despite the pre-existing file, and it carries
|
||||
// the current exe path / endpoint.
|
||||
let doc = read_single_mcp_json(&fs);
|
||||
let server = &doc["mcpServers"]["idea"];
|
||||
assert_eq!(
|
||||
server["command"].as_str(),
|
||||
Some(exe),
|
||||
"a stale .mcp.json must be clobbered with the fresh exe path, got: {doc}"
|
||||
);
|
||||
let args: Vec<&str> = server["args"]
|
||||
.as_array()
|
||||
.expect("args must be a JSON array")
|
||||
.iter()
|
||||
.map(|v| v.as_str().unwrap())
|
||||
.collect();
|
||||
assert_eq!(
|
||||
args.get(2).copied(),
|
||||
Some(endpoint),
|
||||
"the clobbered declaration must carry the fresh endpoint, got: {args:?}"
|
||||
);
|
||||
}
|
||||
|
||||
#[tokio::test]
|
||||
async fn launch_mcp_runtime_none_is_non_clobbering() {
|
||||
// M5d-5b — WITHOUT a runtime (orchestrator / hot-swap / tests) only the degraded
|
||||
// minimal declaration is available, so a pre-existing .mcp.json must NOT be
|
||||
// overwritten: we never replace a real declaration with the minimal one.
|
||||
let (launch, agent, fs, _pty, _bus, _sessions, _tr, _session) = launch_fixture_with_profile(
|
||||
profile_with_mcp(pid(9), McpConfigStrategy::config_file(".mcp.json").unwrap()),
|
||||
Some(ContextInjectionPlan::File {
|
||||
target: "CLAUDE.md".to_owned(),
|
||||
}),
|
||||
);
|
||||
let run_dir = format!("/home/me/proj/.ideai/run/{}", agent.id);
|
||||
fs.mark_existing(&format!("{run_dir}/.mcp.json"));
|
||||
|
||||
// launch_input has mcp_runtime: None.
|
||||
launch.execute(launch_input(agent.id)).await.unwrap();
|
||||
|
||||
assert!(
|
||||
writes_ending_with(&fs, "/.mcp.json").is_empty(),
|
||||
"an existing .mcp.json must not be clobbered even with an injected runtime"
|
||||
"without a runtime, an existing .mcp.json must not be clobbered with the minimal decl"
|
||||
);
|
||||
}
|
||||
|
||||
#[tokio::test]
|
||||
async fn launch_mcp_runtime_remount_clobbers_with_run_specific_facts() {
|
||||
// M5d-5c (QA) — direct reproduction of the AppImage-remount bug: across two
|
||||
// independent app sessions (= two reboots, each with a *different* mount path
|
||||
// and a *different* loopback socket), each launch must clobber a pre-existing
|
||||
// stale `.mcp.json` with the facts of THAT run. This proves the file is rebuilt
|
||||
// per-launch from the live runtime — never frozen on a previous run's dead
|
||||
// binary/socket. Two fixtures are used on purpose: relaunching the *same* live
|
||||
// agent would short-circuit through the reattach guard (no respawn), so it
|
||||
// would not exercise a second `apply_mcp_config`.
|
||||
let launch_once = |exe: &'static str, endpoint: &'static str| async move {
|
||||
let (launch, agent, fs, _pty, _bus, _sessions, _tr, _session) = launch_fixture_with_profile(
|
||||
profile_with_mcp(pid(9), McpConfigStrategy::config_file(".mcp.json").unwrap()),
|
||||
Some(ContextInjectionPlan::File {
|
||||
target: "CLAUDE.md".to_owned(),
|
||||
}),
|
||||
);
|
||||
let run_dir = format!("/home/me/proj/.ideai/run/{}", agent.id);
|
||||
// A stale declaration left by the previous boot is already on disk.
|
||||
fs.mark_existing(&format!("{run_dir}/.mcp.json"));
|
||||
|
||||
let runtime = application::McpRuntime {
|
||||
exe: exe.to_owned(),
|
||||
endpoint: endpoint.to_owned(),
|
||||
project_id: "0123456789abcdef0123456789abcdef".to_owned(),
|
||||
requester: "agent-7".to_owned(),
|
||||
};
|
||||
launch
|
||||
.execute(launch_input_with_runtime(agent.id, runtime))
|
||||
.await
|
||||
.unwrap();
|
||||
|
||||
let doc = read_single_mcp_json(&fs);
|
||||
let server = &doc["mcpServers"]["idea"];
|
||||
let command = server["command"].as_str().unwrap().to_owned();
|
||||
let args: Vec<String> = server["args"]
|
||||
.as_array()
|
||||
.expect("args must be a JSON array")
|
||||
.iter()
|
||||
.map(|v| v.as_str().unwrap().to_owned())
|
||||
.collect();
|
||||
(command, args)
|
||||
};
|
||||
|
||||
// Boot #1: mount path A + socket A.
|
||||
let exe_a = "/tmp/.mount_IdeA_AAA/idea";
|
||||
let endpoint_a = "/run/idea-mcp/boot-a.sock";
|
||||
let (command_a, args_a) = launch_once(exe_a, endpoint_a).await;
|
||||
assert_eq!(command_a, exe_a, "boot #1 must carry mount path A");
|
||||
assert_eq!(args_a.get(2).map(String::as_str), Some(endpoint_a));
|
||||
|
||||
// Boot #2: a *new* mount path + a *new* socket (the very drift that caused the
|
||||
// bug). The declaration must follow the live facts, not the previous boot's.
|
||||
let exe_b = "/tmp/.mount_IdeA_BBB/idea";
|
||||
let endpoint_b = "/run/idea-mcp/boot-b.sock";
|
||||
let (command_b, args_b) = launch_once(exe_b, endpoint_b).await;
|
||||
assert_eq!(command_b, exe_b, "boot #2 must carry the NEW mount path");
|
||||
assert_eq!(args_b.get(2).map(String::as_str), Some(endpoint_b));
|
||||
|
||||
// And nothing from the stale boot #1 leaks into the boot #2 declaration.
|
||||
assert_ne!(
|
||||
command_b, exe_a,
|
||||
"boot #2 must not reuse boot #1's dead exe"
|
||||
);
|
||||
assert!(
|
||||
!args_b.iter().any(|a| a == endpoint_a),
|
||||
"boot #2 must not reuse boot #1's dead endpoint, got: {args_b:?}"
|
||||
);
|
||||
}
|
||||
|
||||
@ -2122,11 +2225,7 @@ impl HandoffStore for FakeHandoffStore {
|
||||
async fn load(&self, conversation: ConversationId) -> Result<Option<Handoff>, StoreError> {
|
||||
Ok(self.0.lock().unwrap().get(&conversation).cloned())
|
||||
}
|
||||
async fn save(
|
||||
&self,
|
||||
conversation: ConversationId,
|
||||
handoff: Handoff,
|
||||
) -> Result<(), StoreError> {
|
||||
async fn save(&self, conversation: ConversationId, handoff: Handoff) -> Result<(), StoreError> {
|
||||
self.0.lock().unwrap().insert(conversation, handoff);
|
||||
Ok(())
|
||||
}
|
||||
@ -2338,8 +2437,8 @@ async fn launch_with_store_without_handoff_omits_resume_section() {
|
||||
#[tokio::test]
|
||||
async fn reopened_cell_loads_handoff_saved_under_resolve_key_end_to_end() {
|
||||
let agent_party = ConversationParty::agent(aid(1)); // l'agent du harnais.
|
||||
// (1) Clé de SAUVEGARDE = resolve_conversation(None, agent) avec registre câblé
|
||||
// == for_pair(User, agent) (égalité scellée Bloc 1, côté infrastructure).
|
||||
// (1) Clé de SAUVEGARDE = resolve_conversation(None, agent) avec registre câblé
|
||||
// == for_pair(User, agent) (égalité scellée Bloc 1, côté infrastructure).
|
||||
let save_key = ConversationId::for_pair(ConversationParty::User, agent_party);
|
||||
// (2) Clé que P8a persiste sur la cellule neuve, donc portée à la réouverture.
|
||||
let persisted_on_leaf = ConversationId::for_pair(ConversationParty::User, agent_party);
|
||||
@ -2383,14 +2482,15 @@ async fn reopened_cell_loads_handoff_saved_under_resolve_key_end_to_end() {
|
||||
#[tokio::test]
|
||||
async fn reopened_cell_does_not_load_handoff_saved_under_a_different_pair_key() {
|
||||
let agent_party = ConversationParty::agent(aid(1)); // agent du harnais.
|
||||
// La cellule porte SA clé de paire (celle que P8a aurait persistée)…
|
||||
// La cellule porte SA clé de paire (celle que P8a aurait persistée)…
|
||||
let leaf_key = ConversationId::for_pair(ConversationParty::User, agent_party);
|
||||
// …mais le handoff est rangé sous la clé d'une AUTRE paire (autre agent).
|
||||
let other_key = ConversationId::for_pair(
|
||||
ConversationParty::User,
|
||||
ConversationParty::agent(aid(2)),
|
||||
let other_key =
|
||||
ConversationId::for_pair(ConversationParty::User, ConversationParty::agent(aid(2)));
|
||||
assert_ne!(
|
||||
leaf_key, other_key,
|
||||
"préalable : deux clés de paire distinctes"
|
||||
);
|
||||
assert_ne!(leaf_key, other_key, "préalable : deux clés de paire distinctes");
|
||||
|
||||
let store = FakeHandoffStore::with(other_key, handoff_for("ne doit pas fuiter", Some("X")));
|
||||
let provider = Arc::new(FakeHandoffProvider(Arc::new(store))) as Arc<dyn HandoffProvider>;
|
||||
|
||||
Reference in New Issue
Block a user