Sync public tree to 4a860d7
This commit is contained in:
parent
1dad0df5c4
commit
4f5df7da20
4 changed files with 236 additions and 5 deletions
|
|
@ -1,7 +1,7 @@
|
||||||
{
|
{
|
||||||
"kind": "disasmer-filtered-public-tree",
|
"kind": "disasmer-filtered-public-tree",
|
||||||
"source_commit": "13dc821c5a4ff2b093b777374ebf510197ab5500",
|
"source_commit": "4a860d72efe34b376c4b08762555bee3fe3105f9",
|
||||||
"release_name": "dryrun-13dc821c5a4f",
|
"release_name": "dryrun-4a860d72efe3",
|
||||||
"filtered_out": [
|
"filtered_out": [
|
||||||
"private/**",
|
"private/**",
|
||||||
"experiments/**",
|
"experiments/**",
|
||||||
|
|
|
||||||
|
|
@ -1,4 +1,4 @@
|
||||||
use std::collections::BTreeSet;
|
use std::collections::{BTreeMap, BTreeSet};
|
||||||
use std::io::{BufRead, BufReader, Write};
|
use std::io::{BufRead, BufReader, Write};
|
||||||
use std::net::{TcpListener, TcpStream, ToSocketAddrs};
|
use std::net::{TcpListener, TcpStream, ToSocketAddrs};
|
||||||
use std::path::{Path, PathBuf};
|
use std::path::{Path, PathBuf};
|
||||||
|
|
@ -10,8 +10,9 @@ use base64::{engine::general_purpose::STANDARD as BASE64_STANDARD, Engine as _};
|
||||||
use clap::{Args, Parser, Subcommand};
|
use clap::{Args, Parser, Subcommand};
|
||||||
use disasmer_core::{
|
use disasmer_core::{
|
||||||
diagnose_environment_references, BrowserLoginFlow, BundleIdentityInputs, BundleMetadata,
|
diagnose_environment_references, BrowserLoginFlow, BundleIdentityInputs, BundleMetadata,
|
||||||
Capability, CliLoginFlow, Digest, EnvironmentReference, LimitKind, NodeCapabilities,
|
Capability, CliLoginFlow, Digest, EnvironmentBackend, EnvironmentReference, LimitKind,
|
||||||
ProjectModel, ResourceLimits, SelectedInput, SourceProviderKind, SourceProviderManifest,
|
NodeCapabilities, ProjectModel, ResourceLimits, SelectedInput, SourceProviderKind,
|
||||||
|
SourceProviderManifest,
|
||||||
};
|
};
|
||||||
use serde::{Deserialize, Serialize};
|
use serde::{Deserialize, Serialize};
|
||||||
use serde_json::{json, Value};
|
use serde_json::{json, Value};
|
||||||
|
|
@ -678,6 +679,7 @@ struct NodeAttachPlan {
|
||||||
node: String,
|
node: String,
|
||||||
coordinator: Option<String>,
|
coordinator: Option<String>,
|
||||||
capabilities: NodeCapabilities,
|
capabilities: NodeCapabilities,
|
||||||
|
detection: NodeAttachDetectionEvidence,
|
||||||
grant_disclosures: Vec<CapabilityGrantDisclosure>,
|
grant_disclosures: Vec<CapabilityGrantDisclosure>,
|
||||||
enrollment: Option<NodeEnrollmentPlan>,
|
enrollment: Option<NodeEnrollmentPlan>,
|
||||||
}
|
}
|
||||||
|
|
@ -711,6 +713,32 @@ struct CapabilityGrantDisclosure {
|
||||||
coordinator_policy_limited: bool,
|
coordinator_policy_limited: bool,
|
||||||
}
|
}
|
||||||
|
|
||||||
|
#[derive(Clone, Debug, PartialEq, Eq, Serialize)]
|
||||||
|
struct NodeAttachDetectionEvidence {
|
||||||
|
auto_detected: bool,
|
||||||
|
os: disasmer_core::Os,
|
||||||
|
arch: String,
|
||||||
|
command_backend: String,
|
||||||
|
command_backend_available: bool,
|
||||||
|
container_backend: Option<String>,
|
||||||
|
container_backend_reported: bool,
|
||||||
|
container_backend_available: bool,
|
||||||
|
source_provider_backends: Vec<SourceProviderBackendStatus>,
|
||||||
|
manual_capability_overrides_allowed: bool,
|
||||||
|
manual_capability_overrides: Vec<String>,
|
||||||
|
recognized_capability_overrides: Vec<Capability>,
|
||||||
|
unrecognized_capability_overrides: Vec<String>,
|
||||||
|
os_arch_capabilities_require_manual_flags: bool,
|
||||||
|
}
|
||||||
|
|
||||||
|
#[derive(Clone, Debug, PartialEq, Eq, Serialize)]
|
||||||
|
struct SourceProviderBackendStatus {
|
||||||
|
provider: String,
|
||||||
|
detected: bool,
|
||||||
|
available: bool,
|
||||||
|
reason: String,
|
||||||
|
}
|
||||||
|
|
||||||
#[derive(Clone, Debug, PartialEq, Eq, Serialize)]
|
#[derive(Clone, Debug, PartialEq, Eq, Serialize)]
|
||||||
struct NodeEnrollmentPlan {
|
struct NodeEnrollmentPlan {
|
||||||
grant: String,
|
grant: String,
|
||||||
|
|
@ -2794,6 +2822,12 @@ fn human_report(value: &Value) -> String {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
if let Some(detection) = value
|
||||||
|
.pointer("/plan/detection")
|
||||||
|
.or_else(|| value.get("detection"))
|
||||||
|
{
|
||||||
|
push_node_attach_detection(&mut lines, detection);
|
||||||
|
}
|
||||||
if let Some(disclosures) = value
|
if let Some(disclosures) = value
|
||||||
.get("grant_disclosures")
|
.get("grant_disclosures")
|
||||||
.and_then(Value::as_array)
|
.and_then(Value::as_array)
|
||||||
|
|
@ -2825,6 +2859,64 @@ fn human_report(value: &Value) -> String {
|
||||||
lines.join("\n")
|
lines.join("\n")
|
||||||
}
|
}
|
||||||
|
|
||||||
|
fn push_node_attach_detection(lines: &mut Vec<String>, detection: &Value) {
|
||||||
|
push_nested_string_field(lines, detection, "os", "node os");
|
||||||
|
push_nested_string_field(lines, detection, "arch", "node arch");
|
||||||
|
push_nested_string_field(lines, detection, "command_backend", "command backend");
|
||||||
|
if let Some(backend) = detection.get("container_backend").and_then(Value::as_str) {
|
||||||
|
let available = detection
|
||||||
|
.get("container_backend_available")
|
||||||
|
.and_then(Value::as_bool)
|
||||||
|
.unwrap_or(false);
|
||||||
|
lines.push(format!(
|
||||||
|
"container backend: {backend} ({})",
|
||||||
|
if available { "available" } else { "reported" }
|
||||||
|
));
|
||||||
|
} else if detection
|
||||||
|
.get("container_backend_reported")
|
||||||
|
.and_then(Value::as_bool)
|
||||||
|
.unwrap_or(false)
|
||||||
|
{
|
||||||
|
lines.push("container backend: reported".to_owned());
|
||||||
|
}
|
||||||
|
if let Some(providers) = detection
|
||||||
|
.get("source_provider_backends")
|
||||||
|
.and_then(Value::as_array)
|
||||||
|
{
|
||||||
|
let entries = providers
|
||||||
|
.iter()
|
||||||
|
.filter_map(|provider| {
|
||||||
|
let name = provider.get("provider").and_then(Value::as_str)?;
|
||||||
|
let state = if provider
|
||||||
|
.get("available")
|
||||||
|
.and_then(Value::as_bool)
|
||||||
|
.unwrap_or(false)
|
||||||
|
{
|
||||||
|
"available"
|
||||||
|
} else {
|
||||||
|
"detected"
|
||||||
|
};
|
||||||
|
Some(format!("{name}={state}"))
|
||||||
|
})
|
||||||
|
.collect::<Vec<_>>();
|
||||||
|
if !entries.is_empty() {
|
||||||
|
lines.push(format!("source providers: {}", entries.join(", ")));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
if let Some(overrides) = detection
|
||||||
|
.get("manual_capability_overrides")
|
||||||
|
.and_then(Value::as_array)
|
||||||
|
{
|
||||||
|
let overrides = overrides
|
||||||
|
.iter()
|
||||||
|
.filter_map(Value::as_str)
|
||||||
|
.collect::<Vec<_>>();
|
||||||
|
if !overrides.is_empty() {
|
||||||
|
lines.push(format!("capability overrides: {}", overrides.join(", ")));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
fn push_string_field(lines: &mut Vec<String>, value: &Value, key: &str, label: &str) {
|
fn push_string_field(lines: &mut Vec<String>, value: &Value, key: &str, label: &str) {
|
||||||
if let Some(text) = value.get(key).and_then(Value::as_str) {
|
if let Some(text) = value.get(key).and_then(Value::as_str) {
|
||||||
lines.push(format!("{label}: {text}"));
|
lines.push(format!("{label}: {text}"));
|
||||||
|
|
@ -5748,11 +5840,18 @@ fn add_workflow_actor_fields(request: &mut Value, session: &CliSession, fallback
|
||||||
|
|
||||||
fn attach_plan(args: AttachArgs) -> NodeAttachPlan {
|
fn attach_plan(args: AttachArgs) -> NodeAttachPlan {
|
||||||
let mut capabilities = NodeCapabilities::detect_current();
|
let mut capabilities = NodeCapabilities::detect_current();
|
||||||
|
let mut recognized_capability_overrides = Vec::new();
|
||||||
|
let mut unrecognized_capability_overrides = Vec::new();
|
||||||
for cap in &args.caps {
|
for cap in &args.caps {
|
||||||
if let Some(parsed) = parse_capability(&cap) {
|
if let Some(parsed) = parse_capability(&cap) {
|
||||||
|
recognized_capability_overrides.push(parsed.clone());
|
||||||
capabilities.capabilities.insert(parsed);
|
capabilities.capabilities.insert(parsed);
|
||||||
|
} else {
|
||||||
|
unrecognized_capability_overrides.push(cap.clone());
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
recognized_capability_overrides.sort();
|
||||||
|
recognized_capability_overrides.dedup();
|
||||||
let node = args.node.unwrap_or_else(default_node_id);
|
let node = args.node.unwrap_or_else(default_node_id);
|
||||||
let public_key = args
|
let public_key = args
|
||||||
.public_key
|
.public_key
|
||||||
|
|
@ -5762,17 +5861,94 @@ fn attach_plan(args: AttachArgs) -> NodeAttachPlan {
|
||||||
public_key_fingerprint: Digest::sha256(public_key),
|
public_key_fingerprint: Digest::sha256(public_key),
|
||||||
exchanges_short_lived_grant_for_long_lived_node_identity: true,
|
exchanges_short_lived_grant_for_long_lived_node_identity: true,
|
||||||
});
|
});
|
||||||
|
let detection = node_attach_detection_evidence(
|
||||||
|
&capabilities,
|
||||||
|
args.caps,
|
||||||
|
recognized_capability_overrides,
|
||||||
|
unrecognized_capability_overrides,
|
||||||
|
);
|
||||||
let grant_disclosures = capability_grant_disclosures(&capabilities);
|
let grant_disclosures = capability_grant_disclosures(&capabilities);
|
||||||
|
|
||||||
NodeAttachPlan {
|
NodeAttachPlan {
|
||||||
node,
|
node,
|
||||||
coordinator: args.coordinator,
|
coordinator: args.coordinator,
|
||||||
capabilities,
|
capabilities,
|
||||||
|
detection,
|
||||||
grant_disclosures,
|
grant_disclosures,
|
||||||
enrollment,
|
enrollment,
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
fn node_attach_detection_evidence(
|
||||||
|
capabilities: &NodeCapabilities,
|
||||||
|
manual_capability_overrides: Vec<String>,
|
||||||
|
recognized_capability_overrides: Vec<Capability>,
|
||||||
|
unrecognized_capability_overrides: Vec<String>,
|
||||||
|
) -> NodeAttachDetectionEvidence {
|
||||||
|
let command_backend_available = capabilities.capabilities.contains(&Capability::Command);
|
||||||
|
let container_backend_reported = capabilities
|
||||||
|
.environment_backends
|
||||||
|
.contains(&EnvironmentBackend::Container);
|
||||||
|
let container_backend = container_backend_reported.then(|| "rootless-podman".to_owned());
|
||||||
|
let container_backend_available = container_backend_reported
|
||||||
|
&& capabilities
|
||||||
|
.capabilities
|
||||||
|
.contains(&Capability::RootlessPodman)
|
||||||
|
&& command_available("podman");
|
||||||
|
|
||||||
|
NodeAttachDetectionEvidence {
|
||||||
|
auto_detected: true,
|
||||||
|
os: capabilities.os.clone(),
|
||||||
|
arch: capabilities.arch.clone(),
|
||||||
|
command_backend: if command_backend_available {
|
||||||
|
"native-command".to_owned()
|
||||||
|
} else {
|
||||||
|
"unavailable".to_owned()
|
||||||
|
},
|
||||||
|
command_backend_available,
|
||||||
|
container_backend,
|
||||||
|
container_backend_reported,
|
||||||
|
container_backend_available,
|
||||||
|
source_provider_backends: source_provider_backend_statuses(capabilities),
|
||||||
|
manual_capability_overrides_allowed: true,
|
||||||
|
manual_capability_overrides,
|
||||||
|
recognized_capability_overrides,
|
||||||
|
unrecognized_capability_overrides,
|
||||||
|
os_arch_capabilities_require_manual_flags: false,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
fn source_provider_backend_statuses(
|
||||||
|
capabilities: &NodeCapabilities,
|
||||||
|
) -> Vec<SourceProviderBackendStatus> {
|
||||||
|
let mut statuses = BTreeMap::new();
|
||||||
|
for provider in &capabilities.source_providers {
|
||||||
|
let available = match provider.as_str() {
|
||||||
|
"filesystem" => capabilities
|
||||||
|
.capabilities
|
||||||
|
.contains(&Capability::SourceFilesystem),
|
||||||
|
"git" => command_available("git"),
|
||||||
|
_ => true,
|
||||||
|
};
|
||||||
|
statuses.insert(
|
||||||
|
provider.clone(),
|
||||||
|
SourceProviderBackendStatus {
|
||||||
|
provider: provider.clone(),
|
||||||
|
detected: true,
|
||||||
|
available,
|
||||||
|
reason: if available {
|
||||||
|
"detected by local node capability probe".to_owned()
|
||||||
|
} else {
|
||||||
|
format!(
|
||||||
|
"source provider `{provider}` was detected but its local helper is missing"
|
||||||
|
)
|
||||||
|
},
|
||||||
|
},
|
||||||
|
);
|
||||||
|
}
|
||||||
|
statuses.into_values().collect()
|
||||||
|
}
|
||||||
|
|
||||||
fn capability_grant_disclosures(capabilities: &NodeCapabilities) -> Vec<CapabilityGrantDisclosure> {
|
fn capability_grant_disclosures(capabilities: &NodeCapabilities) -> Vec<CapabilityGrantDisclosure> {
|
||||||
let mut disclosures = Vec::new();
|
let mut disclosures = Vec::new();
|
||||||
let mut push = |capability: Capability, grant: &str, description: &str, risk: &str| {
|
let mut push = |capability: Capability, grant: &str, description: &str, risk: &str| {
|
||||||
|
|
@ -6225,6 +6401,27 @@ mod tests {
|
||||||
.capabilities
|
.capabilities
|
||||||
.contains(&Capability::QuicDirect));
|
.contains(&Capability::QuicDirect));
|
||||||
assert!(!plan.capabilities.arch.is_empty());
|
assert!(!plan.capabilities.arch.is_empty());
|
||||||
|
assert!(plan.detection.auto_detected);
|
||||||
|
assert_eq!(plan.detection.os, plan.capabilities.os);
|
||||||
|
assert_eq!(plan.detection.arch, plan.capabilities.arch);
|
||||||
|
assert_eq!(plan.detection.command_backend, "native-command");
|
||||||
|
assert!(plan.detection.command_backend_available);
|
||||||
|
assert!(plan.detection.manual_capability_overrides_allowed);
|
||||||
|
assert_eq!(
|
||||||
|
plan.detection.manual_capability_overrides,
|
||||||
|
vec!["quic-direct".to_owned()]
|
||||||
|
);
|
||||||
|
assert!(plan
|
||||||
|
.detection
|
||||||
|
.recognized_capability_overrides
|
||||||
|
.contains(&Capability::QuicDirect));
|
||||||
|
assert!(plan.detection.unrecognized_capability_overrides.is_empty());
|
||||||
|
assert!(!plan.detection.os_arch_capabilities_require_manual_flags);
|
||||||
|
assert!(plan
|
||||||
|
.detection
|
||||||
|
.source_provider_backends
|
||||||
|
.iter()
|
||||||
|
.any(|provider| provider.provider == "filesystem" && provider.detected));
|
||||||
}
|
}
|
||||||
|
|
||||||
#[test]
|
#[test]
|
||||||
|
|
|
||||||
|
|
@ -37,6 +37,7 @@ const cliFirstAcceptance = read("scripts/acceptance-cli-first.sh");
|
||||||
const outputModeSmoke = read("scripts/cli-output-mode-smoke.js");
|
const outputModeSmoke = read("scripts/cli-output-mode-smoke.js");
|
||||||
const errorExitSmoke = read("scripts/cli-error-exit-smoke.js");
|
const errorExitSmoke = read("scripts/cli-error-exit-smoke.js");
|
||||||
const browserLoginFlowSmoke = read("scripts/cli-browser-login-flow-smoke.js");
|
const browserLoginFlowSmoke = read("scripts/cli-browser-login-flow-smoke.js");
|
||||||
|
const nodeAttachSmoke = read("scripts/node-attach-smoke.js");
|
||||||
|
|
||||||
expect(criteria, "header", /^# Disasmer CLI-First MVP Acceptance Criteria/m);
|
expect(criteria, "header", /^# Disasmer CLI-First MVP Acceptance Criteria/m);
|
||||||
expect(
|
expect(
|
||||||
|
|
@ -250,6 +251,7 @@ for (const [name, pattern] of [
|
||||||
["CLI locality failure classifier", /fn classify_cli_error_message\(message: &str\)[\s\S]*message_mentions_locality_failure\(&message\)[\s\S]*return "connectivity"/],
|
["CLI locality failure classifier", /fn classify_cli_error_message\(message: &str\)[\s\S]*message_mentions_locality_failure\(&message\)[\s\S]*return "connectivity"/],
|
||||||
["CLI locality failure report helper", /fn task_locality_failure_from_reason\(reason: &Value\) -> Value[\s\S]*coordinator_bulk_relay_used[\s\S]*safe_next_actions/],
|
["CLI locality failure report helper", /fn task_locality_failure_from_reason\(reason: &Value\) -> Value[\s\S]*coordinator_bulk_relay_used[\s\S]*safe_next_actions/],
|
||||||
["CLI locality failure human output", /fn push_task_locality_failures\(lines: &mut Vec<String>, tasks: &\[Value\]\)[\s\S]*locality \{task_name\}/],
|
["CLI locality failure human output", /fn push_task_locality_failures\(lines: &mut Vec<String>, tasks: &\[Value\]\)[\s\S]*locality \{task_name\}/],
|
||||||
|
["node attach auto-detection coverage", /fn node_attach_detects_and_accepts_capability_overrides\(\)[\s\S]*detection\.auto_detected[\s\S]*command_backend[\s\S]*source_provider_backends/],
|
||||||
["node attach grant disclosure coverage", /fn node_attach_discloses_dangerous_capability_grants\(\)/],
|
["node attach grant disclosure coverage", /fn node_attach_discloses_dangerous_capability_grants\(\)/],
|
||||||
["node enroll public API grant coverage", /fn node_enroll_reports_short_lived_public_api_grant\(\)/],
|
["node enroll public API grant coverage", /fn node_enroll_reports_short_lived_public_api_grant\(\)/],
|
||||||
["quota local status coverage", /fn quota_status_uses_project_config_and_generic_public_limits\(\)/],
|
["quota local status coverage", /fn quota_status_uses_project_config_and_generic_public_limits\(\)/],
|
||||||
|
|
@ -280,6 +282,11 @@ expect(
|
||||||
"browser login smoke rejects provider token persistence",
|
"browser login smoke rejects provider token persistence",
|
||||||
/assert\.doesNotMatch\([\s\S]*access_token\|refresh_token\|id_token/
|
/assert\.doesNotMatch\([\s\S]*access_token\|refresh_token\|id_token/
|
||||||
);
|
);
|
||||||
|
expect(
|
||||||
|
nodeAttachSmoke,
|
||||||
|
"node attach smoke verifies auto-detection evidence",
|
||||||
|
/plan\.detection\.auto_detected[\s\S]*plan\.detection\.command_backend[\s\S]*source_provider_backends/
|
||||||
|
);
|
||||||
|
|
||||||
expect(
|
expect(
|
||||||
coordinator,
|
coordinator,
|
||||||
|
|
@ -356,6 +363,16 @@ expect(
|
||||||
"CLI sends agent workflow actor fields",
|
"CLI sends agent workflow actor fields",
|
||||||
/fn add_workflow_actor_fields[\s\S]*actor_agent[\s\S]*agent_public_key_fingerprint/
|
/fn add_workflow_actor_fields[\s\S]*actor_agent[\s\S]*agent_public_key_fingerprint/
|
||||||
);
|
);
|
||||||
|
expect(
|
||||||
|
cli,
|
||||||
|
"CLI exposes node attach detection evidence",
|
||||||
|
/struct NodeAttachDetectionEvidence[\s\S]*command_backend[\s\S]*container_backend[\s\S]*source_provider_backends[\s\S]*manual_capability_overrides[\s\S]*os_arch_capabilities_require_manual_flags/
|
||||||
|
);
|
||||||
|
expect(
|
||||||
|
cli,
|
||||||
|
"CLI renders node attach detection evidence",
|
||||||
|
/fn push_node_attach_detection[\s\S]*command backend[\s\S]*container backend[\s\S]*source providers[\s\S]*capability overrides/
|
||||||
|
);
|
||||||
expect(
|
expect(
|
||||||
cli,
|
cli,
|
||||||
"CLI exposes node attach grant disclosures",
|
"CLI exposes node attach grant disclosures",
|
||||||
|
|
|
||||||
|
|
@ -212,6 +212,23 @@ function runAttachedNodeWork(addr) {
|
||||||
assert.ok(report.plan.capabilities.arch.length > 0);
|
assert.ok(report.plan.capabilities.arch.length > 0);
|
||||||
assert.ok(report.plan.capabilities.source_providers.includes("filesystem"));
|
assert.ok(report.plan.capabilities.source_providers.includes("filesystem"));
|
||||||
assert.ok(report.plan.capabilities.capabilities.includes("QuicDirect"));
|
assert.ok(report.plan.capabilities.capabilities.includes("QuicDirect"));
|
||||||
|
assert.strictEqual(report.plan.detection.auto_detected, true);
|
||||||
|
assert.strictEqual(report.plan.detection.arch, report.plan.capabilities.arch);
|
||||||
|
assert.deepStrictEqual(report.plan.detection.manual_capability_overrides, ["quic-direct"]);
|
||||||
|
assert(
|
||||||
|
report.plan.detection.recognized_capability_overrides.includes("QuicDirect")
|
||||||
|
);
|
||||||
|
assert.strictEqual(
|
||||||
|
report.plan.detection.os_arch_capabilities_require_manual_flags,
|
||||||
|
false
|
||||||
|
);
|
||||||
|
assert.strictEqual(report.plan.detection.command_backend, "native-command");
|
||||||
|
assert.strictEqual(report.plan.detection.command_backend_available, true);
|
||||||
|
assert(
|
||||||
|
report.plan.detection.source_provider_backends.some(
|
||||||
|
(provider) => provider.provider === "filesystem" && provider.detected
|
||||||
|
)
|
||||||
|
);
|
||||||
assert.strictEqual(report.boundary.cli_contacted_coordinator, true);
|
assert.strictEqual(report.boundary.cli_contacted_coordinator, true);
|
||||||
assert.strictEqual(report.boundary.used_enrollment_exchange, true);
|
assert.strictEqual(report.boundary.used_enrollment_exchange, true);
|
||||||
assert.strictEqual(report.boundary.coordinator_session_requests, 3);
|
assert.strictEqual(report.boundary.coordinator_session_requests, 3);
|
||||||
|
|
|
||||||
Loading…
Add table
Add a link
Reference in a new issue