Add explicit non-interactive public CLI mode
This commit is contained in:
parent
e6c2a1a981
commit
afd7456423
6 changed files with 226 additions and 5 deletions
|
|
@ -1,7 +1,7 @@
|
||||||
{
|
{
|
||||||
"kind": "disasmer-filtered-public-tree",
|
"kind": "disasmer-filtered-public-tree",
|
||||||
"source_commit": "133d7b466c2f96c240f559722bc4b557aa94c091",
|
"source_commit": "09993cb75187f1e8798d6429946085e167059e9f",
|
||||||
"release_name": "dryrun-133d7b466c2f",
|
"release_name": "dryrun-09993cb75187",
|
||||||
"filtered_out": [
|
"filtered_out": [
|
||||||
"private/**",
|
"private/**",
|
||||||
"experiments/**",
|
"experiments/**",
|
||||||
|
|
|
||||||
|
|
@ -117,6 +117,8 @@ struct LoginArgs {
|
||||||
#[arg(long)]
|
#[arg(long)]
|
||||||
browser: bool,
|
browser: bool,
|
||||||
#[arg(long)]
|
#[arg(long)]
|
||||||
|
non_interactive: bool,
|
||||||
|
#[arg(long)]
|
||||||
plan: bool,
|
plan: bool,
|
||||||
#[arg(long)]
|
#[arg(long)]
|
||||||
json: bool,
|
json: bool,
|
||||||
|
|
@ -285,6 +287,8 @@ struct RunArgs {
|
||||||
#[arg(long)]
|
#[arg(long)]
|
||||||
local: bool,
|
local: bool,
|
||||||
#[arg(long)]
|
#[arg(long)]
|
||||||
|
non_interactive: bool,
|
||||||
|
#[arg(long)]
|
||||||
json: bool,
|
json: bool,
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
@ -837,6 +841,37 @@ fn auth_logout_report(args: AuthLogoutArgs, cwd: PathBuf) -> Result<Value> {
|
||||||
logout_report(args, cwd, "auth logout")
|
logout_report(args, cwd, "auth logout")
|
||||||
}
|
}
|
||||||
|
|
||||||
|
fn non_interactive_auth_machine_error(message: &str, next_actions: Vec<&'static str>) -> Value {
|
||||||
|
let mut machine_error = cli_error_summary_for_category("authentication", message);
|
||||||
|
if let Some(object) = machine_error.as_object_mut() {
|
||||||
|
object.insert("next_actions".to_owned(), json!(next_actions));
|
||||||
|
object.insert("browser_opened".to_owned(), json!(false));
|
||||||
|
}
|
||||||
|
machine_error
|
||||||
|
}
|
||||||
|
|
||||||
|
fn non_interactive_browser_login_report(args: &LoginArgs) -> Value {
|
||||||
|
let message =
|
||||||
|
"browser login requires an interactive browser, but non-interactive mode is enabled";
|
||||||
|
let next_actions = vec![
|
||||||
|
"rerun without --non-interactive to open the browser",
|
||||||
|
"disasmer login --browser --plan",
|
||||||
|
"use DISASMER_AGENT_PUBLIC_KEY for automation",
|
||||||
|
];
|
||||||
|
json!({
|
||||||
|
"command": "login",
|
||||||
|
"status": "authentication_required",
|
||||||
|
"coordinator": args.coordinator,
|
||||||
|
"non_interactive": true,
|
||||||
|
"browser_requested": true,
|
||||||
|
"browser_opened": false,
|
||||||
|
"safe_failure": true,
|
||||||
|
"message": message,
|
||||||
|
"next_actions": next_actions,
|
||||||
|
"machine_error": non_interactive_auth_machine_error(message, next_actions),
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
fn logout_report(args: AuthLogoutArgs, cwd: PathBuf, command: &str) -> Result<Value> {
|
fn logout_report(args: AuthLogoutArgs, cwd: PathBuf, command: &str) -> Result<Value> {
|
||||||
let session_file = session_config_file(&cwd);
|
let session_file = session_config_file(&cwd);
|
||||||
let existed = session_file.exists();
|
let existed = session_file.exists();
|
||||||
|
|
@ -2593,7 +2628,14 @@ fn run_cli() -> Result<()> {
|
||||||
}
|
}
|
||||||
Commands::Login(args) => {
|
Commands::Login(args) => {
|
||||||
let json_output = args.json;
|
let json_output = args.json;
|
||||||
if args.complete_browser_code.is_some() {
|
if args.non_interactive
|
||||||
|
&& args.browser
|
||||||
|
&& !args.plan
|
||||||
|
&& args.complete_browser_code.is_none()
|
||||||
|
{
|
||||||
|
let report = non_interactive_browser_login_report(&args);
|
||||||
|
emit_report(&report, json_output)?;
|
||||||
|
} else if args.complete_browser_code.is_some() {
|
||||||
let report = execute_browser_login_completion(args)?;
|
let report = execute_browser_login_completion(args)?;
|
||||||
emit_report(&report, json_output)?;
|
emit_report(&report, json_output)?;
|
||||||
} else if args.browser && !args.plan {
|
} else if args.browser && !args.plan {
|
||||||
|
|
@ -4297,7 +4339,39 @@ fn run_plan(args: RunArgs, cwd: PathBuf, session: CliSession) -> Result<RunPlan>
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
|
||||||
|
fn non_interactive_run_requires_auth_report(args: RunArgs, cwd: PathBuf) -> Value {
|
||||||
|
let project = args.project.unwrap_or(cwd);
|
||||||
|
let entry = args.entry.unwrap_or_else(|| "build".to_owned());
|
||||||
|
let message = "non-interactive run requires an authenticated human or agent session unless --local or --coordinator is explicit";
|
||||||
|
let next_actions = vec![
|
||||||
|
"disasmer login --browser",
|
||||||
|
"set DISASMER_AGENT_PUBLIC_KEY for automation",
|
||||||
|
"pass --local to run against local services",
|
||||||
|
"pass --coordinator for an explicit self-hosted coordinator",
|
||||||
|
];
|
||||||
|
json!({
|
||||||
|
"command": "run",
|
||||||
|
"status": "authentication_required",
|
||||||
|
"project_root": project,
|
||||||
|
"entry": entry,
|
||||||
|
"non_interactive": true,
|
||||||
|
"browser_opened": false,
|
||||||
|
"safe_failure": true,
|
||||||
|
"message": message,
|
||||||
|
"next_actions": next_actions,
|
||||||
|
"private_website_required": false,
|
||||||
|
"machine_error": non_interactive_auth_machine_error(message, next_actions),
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
fn run_report(args: RunArgs, cwd: PathBuf, session: CliSession) -> Result<Value> {
|
fn run_report(args: RunArgs, cwd: PathBuf, session: CliSession) -> Result<Value> {
|
||||||
|
if args.non_interactive
|
||||||
|
&& !args.local
|
||||||
|
&& args.coordinator.is_none()
|
||||||
|
&& !session.is_authenticated()
|
||||||
|
{
|
||||||
|
return Ok(non_interactive_run_requires_auth_report(args, cwd));
|
||||||
|
}
|
||||||
let plan = run_plan(args, cwd, session)?;
|
let plan = run_plan(args, cwd, session)?;
|
||||||
if should_execute_local_node(&plan) {
|
if should_execute_local_node(&plan) {
|
||||||
return Ok(serde_json::to_value(execute_local_node_run(plan)?)?);
|
return Ok(serde_json::to_value(execute_local_node_run(plan)?)?);
|
||||||
|
|
@ -5062,6 +5136,35 @@ mod tests {
|
||||||
assert_eq!(plan.session, CliSession::Anonymous);
|
assert_eq!(plan.session, CliSession::Anonymous);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn non_interactive_run_without_session_requires_explicit_auth_or_local() {
|
||||||
|
let Cli {
|
||||||
|
command: Commands::Run(args),
|
||||||
|
} = parse(&["disasmer", "run", "--non-interactive", "--json"])
|
||||||
|
else {
|
||||||
|
panic!("wrong command");
|
||||||
|
};
|
||||||
|
let report = run_report(args, PathBuf::from("/repo"), CliSession::Anonymous).unwrap();
|
||||||
|
|
||||||
|
assert_eq!(report["command"], "run");
|
||||||
|
assert_eq!(report["status"], "authentication_required");
|
||||||
|
assert_eq!(report["non_interactive"], true);
|
||||||
|
assert_eq!(report["browser_opened"], false);
|
||||||
|
assert_eq!(report["private_website_required"], false);
|
||||||
|
assert_eq!(report["machine_error"]["category"], "authentication");
|
||||||
|
assert_eq!(report["machine_error"]["stable_exit_code"], 20);
|
||||||
|
assert_eq!(report["machine_error"]["browser_opened"], false);
|
||||||
|
let next_actions = report["machine_error"]["next_actions"]
|
||||||
|
.as_array()
|
||||||
|
.unwrap()
|
||||||
|
.iter()
|
||||||
|
.filter_map(Value::as_str)
|
||||||
|
.collect::<Vec<_>>();
|
||||||
|
assert!(next_actions.contains(&"disasmer login --browser"));
|
||||||
|
assert!(next_actions.contains(&"set DISASMER_AGENT_PUBLIC_KEY for automation"));
|
||||||
|
assert!(next_actions.contains(&"pass --local to run against local services"));
|
||||||
|
}
|
||||||
|
|
||||||
#[test]
|
#[test]
|
||||||
fn run_project_and_named_entry_are_respected() {
|
fn run_project_and_named_entry_are_respected() {
|
||||||
let Cli {
|
let Cli {
|
||||||
|
|
@ -5157,6 +5260,7 @@ mod tests {
|
||||||
project: None,
|
project: None,
|
||||||
coordinator: None,
|
coordinator: None,
|
||||||
local: false,
|
local: false,
|
||||||
|
non_interactive: true,
|
||||||
json: false,
|
json: false,
|
||||||
};
|
};
|
||||||
let plan = run_plan(
|
let plan = run_plan(
|
||||||
|
|
@ -5233,6 +5337,7 @@ mod tests {
|
||||||
project: Some(temp.path().to_path_buf()),
|
project: Some(temp.path().to_path_buf()),
|
||||||
coordinator: Some(format!("http://{addr}")),
|
coordinator: Some(format!("http://{addr}")),
|
||||||
local: false,
|
local: false,
|
||||||
|
non_interactive: true,
|
||||||
json: false,
|
json: false,
|
||||||
},
|
},
|
||||||
PathBuf::from("/unused"),
|
PathBuf::from("/unused"),
|
||||||
|
|
@ -5310,6 +5415,7 @@ mod tests {
|
||||||
project: Some(temp.path().to_path_buf()),
|
project: Some(temp.path().to_path_buf()),
|
||||||
coordinator: Some(format!("http://{addr}")),
|
coordinator: Some(format!("http://{addr}")),
|
||||||
local: false,
|
local: false,
|
||||||
|
non_interactive: false,
|
||||||
json: false,
|
json: false,
|
||||||
},
|
},
|
||||||
PathBuf::from("/unused"),
|
PathBuf::from("/unused"),
|
||||||
|
|
@ -5322,6 +5428,7 @@ mod tests {
|
||||||
project: Some(temp.path().to_path_buf()),
|
project: Some(temp.path().to_path_buf()),
|
||||||
coordinator: Some(format!("http://{addr}")),
|
coordinator: Some(format!("http://{addr}")),
|
||||||
local: false,
|
local: false,
|
||||||
|
non_interactive: false,
|
||||||
json: false,
|
json: false,
|
||||||
},
|
},
|
||||||
PathBuf::from("/unused"),
|
PathBuf::from("/unused"),
|
||||||
|
|
@ -5433,6 +5540,34 @@ mod tests {
|
||||||
assert!(flow.state.starts_with("sha256:"));
|
assert!(flow.state.starts_with("sha256:"));
|
||||||
}
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn browser_login_non_interactive_fails_before_opening_browser() {
|
||||||
|
let Cli {
|
||||||
|
command: Commands::Login(args),
|
||||||
|
} = parse(&["disasmer", "login", "--browser", "--non-interactive"])
|
||||||
|
else {
|
||||||
|
panic!("wrong command");
|
||||||
|
};
|
||||||
|
let report = non_interactive_browser_login_report(&args);
|
||||||
|
|
||||||
|
assert_eq!(report["command"], "login");
|
||||||
|
assert_eq!(report["status"], "authentication_required");
|
||||||
|
assert_eq!(report["non_interactive"], true);
|
||||||
|
assert_eq!(report["browser_requested"], true);
|
||||||
|
assert_eq!(report["browser_opened"], false);
|
||||||
|
assert_eq!(report["machine_error"]["category"], "authentication");
|
||||||
|
assert_eq!(report["machine_error"]["stable_exit_code"], 20);
|
||||||
|
assert_eq!(report["machine_error"]["browser_opened"], false);
|
||||||
|
let next_actions = report["machine_error"]["next_actions"]
|
||||||
|
.as_array()
|
||||||
|
.unwrap()
|
||||||
|
.iter()
|
||||||
|
.filter_map(Value::as_str)
|
||||||
|
.collect::<Vec<_>>();
|
||||||
|
assert!(next_actions.contains(&"rerun without --non-interactive to open the browser"));
|
||||||
|
assert!(next_actions.contains(&"use DISASMER_AGENT_PUBLIC_KEY for automation"));
|
||||||
|
}
|
||||||
|
|
||||||
#[test]
|
#[test]
|
||||||
fn browser_login_completion_detects_raw_provider_token_fields() {
|
fn browser_login_completion_detects_raw_provider_token_fields() {
|
||||||
assert!(contains_provider_token_field(&json!({
|
assert!(contains_provider_token_field(&json!({
|
||||||
|
|
@ -5849,6 +5984,7 @@ mod tests {
|
||||||
&["disasmer", "auth", "status"],
|
&["disasmer", "auth", "status"],
|
||||||
&["disasmer", "logout", "--yes"],
|
&["disasmer", "logout", "--yes"],
|
||||||
&["disasmer", "auth", "logout", "--yes"],
|
&["disasmer", "auth", "logout", "--yes"],
|
||||||
|
&["disasmer", "login", "--browser", "--non-interactive"],
|
||||||
&[
|
&[
|
||||||
"disasmer",
|
"disasmer",
|
||||||
"key",
|
"key",
|
||||||
|
|
@ -5866,6 +6002,7 @@ mod tests {
|
||||||
&["disasmer", "project", "select", "project"],
|
&["disasmer", "project", "select", "project"],
|
||||||
&["disasmer", "inspect"],
|
&["disasmer", "inspect"],
|
||||||
&["disasmer", "build"],
|
&["disasmer", "build"],
|
||||||
|
&["disasmer", "run", "--non-interactive"],
|
||||||
&["disasmer", "node", "enroll"],
|
&["disasmer", "node", "enroll"],
|
||||||
&["disasmer", "node", "list"],
|
&["disasmer", "node", "list"],
|
||||||
&["disasmer", "node", "status"],
|
&["disasmer", "node", "status"],
|
||||||
|
|
@ -5961,6 +6098,13 @@ mod tests {
|
||||||
for args in [
|
for args in [
|
||||||
&["disasmer", "doctor", "--json"][..],
|
&["disasmer", "doctor", "--json"][..],
|
||||||
&["disasmer", "login", "--json"],
|
&["disasmer", "login", "--json"],
|
||||||
|
&[
|
||||||
|
"disasmer",
|
||||||
|
"login",
|
||||||
|
"--browser",
|
||||||
|
"--non-interactive",
|
||||||
|
"--json",
|
||||||
|
],
|
||||||
&["disasmer", "logout", "--yes", "--json"],
|
&["disasmer", "logout", "--yes", "--json"],
|
||||||
&["disasmer", "auth", "status", "--json"],
|
&["disasmer", "auth", "status", "--json"],
|
||||||
&[
|
&[
|
||||||
|
|
@ -5986,6 +6130,7 @@ mod tests {
|
||||||
&["disasmer", "build", "--json"],
|
&["disasmer", "build", "--json"],
|
||||||
&["disasmer", "bundle", "inspect", "--json"],
|
&["disasmer", "bundle", "inspect", "--json"],
|
||||||
&["disasmer", "run", "--json"],
|
&["disasmer", "run", "--json"],
|
||||||
|
&["disasmer", "run", "--non-interactive", "--json"],
|
||||||
&["disasmer", "node", "attach", "--json"],
|
&["disasmer", "node", "attach", "--json"],
|
||||||
&["disasmer", "node", "enroll", "--json"],
|
&["disasmer", "node", "enroll", "--json"],
|
||||||
&["disasmer", "process", "status", "--json"],
|
&["disasmer", "process", "status", "--json"],
|
||||||
|
|
|
||||||
|
|
@ -35,6 +35,33 @@ function runDisasmer(args) {
|
||||||
}
|
}
|
||||||
|
|
||||||
async function main() {
|
async function main() {
|
||||||
|
const nonInteractive = await runDisasmer([
|
||||||
|
"run",
|
||||||
|
"build",
|
||||||
|
"--project",
|
||||||
|
project,
|
||||||
|
"--non-interactive",
|
||||||
|
"--json",
|
||||||
|
]);
|
||||||
|
assert.strictEqual(nonInteractive.signal, null, nonInteractive.stderr);
|
||||||
|
assert.strictEqual(nonInteractive.code, 20, nonInteractive.stderr);
|
||||||
|
assert.doesNotMatch(nonInteractive.stderr, /Opening Disasmer browser login/);
|
||||||
|
const nonInteractiveReport = JSON.parse(nonInteractive.stdout);
|
||||||
|
assert.strictEqual(nonInteractiveReport.status, "authentication_required");
|
||||||
|
assert.strictEqual(nonInteractiveReport.non_interactive, true);
|
||||||
|
assert.strictEqual(nonInteractiveReport.browser_opened, false);
|
||||||
|
assert.strictEqual(nonInteractiveReport.machine_error.category, "authentication");
|
||||||
|
assert.strictEqual(nonInteractiveReport.machine_error.stable_exit_code, 20);
|
||||||
|
assert.strictEqual(
|
||||||
|
nonInteractiveReport.machine_error.process_exit_code_applied,
|
||||||
|
true
|
||||||
|
);
|
||||||
|
assert(
|
||||||
|
nonInteractiveReport.machine_error.next_actions.includes(
|
||||||
|
"pass --local to run against local services"
|
||||||
|
)
|
||||||
|
);
|
||||||
|
|
||||||
let request = "";
|
let request = "";
|
||||||
const server = net.createServer((socket) => {
|
const server = net.createServer((socket) => {
|
||||||
socket.setEncoding("utf8");
|
socket.setEncoding("utf8");
|
||||||
|
|
|
||||||
|
|
@ -141,6 +141,9 @@ for (const [name, pattern] of [
|
||||||
["top-level version metadata", /#\[command\([\s\S]*name = "disasmer"[\s\S]*version[\s\S]*arg_required_else_help = true[\s\S]*\)\]/],
|
["top-level version metadata", /#\[command\([\s\S]*name = "disasmer"[\s\S]*version[\s\S]*arg_required_else_help = true[\s\S]*\)\]/],
|
||||||
["top-level primary workflow help", /after_help = "Primary workflow:[\s\S]*disasmer login --browser[\s\S]*disasmer node attach --worker[\s\S]*Disasmer: Launch Virtual Process[\s\S]*Hosted account creation happens in the browser login flow\."/],
|
["top-level primary workflow help", /after_help = "Primary workflow:[\s\S]*disasmer login --browser[\s\S]*disasmer node attach --worker[\s\S]*Disasmer: Launch Virtual Process[\s\S]*Hosted account creation happens in the browser login flow\."/],
|
||||||
["top-level logout command", /enum Commands[\s\S]*Logout\(AuthLogoutArgs\)[\s\S]*Auth \{/],
|
["top-level logout command", /enum Commands[\s\S]*Logout\(AuthLogoutArgs\)[\s\S]*Auth \{/],
|
||||||
|
["login non-interactive flag", /struct LoginArgs[\s\S]*non_interactive: bool/],
|
||||||
|
["run non-interactive flag", /struct RunArgs[\s\S]*non_interactive: bool/],
|
||||||
|
["non-interactive auth report", /fn non_interactive_auth_machine_error[\s\S]*browser_opened[\s\S]*false/],
|
||||||
["human report renderer", /fn human_report\(value: &Value\) -> String/],
|
["human report renderer", /fn human_report\(value: &Value\) -> String/],
|
||||||
["shared report emitter", /fn emit_report<T: Serialize>\(report: &T, json_output: bool\) -> Result<\(\)>/],
|
["shared report emitter", /fn emit_report<T: Serialize>\(report: &T, json_output: bool\) -> Result<\(\)>/],
|
||||||
["doctor command", /Doctor\(DoctorArgs\)/],
|
["doctor command", /Doctor\(DoctorArgs\)/],
|
||||||
|
|
@ -166,6 +169,8 @@ for (const [name, pattern] of [
|
||||||
for (const [name, pattern] of [
|
for (const [name, pattern] of [
|
||||||
["CLI parse coverage", /fn cli_first_mvp_command_surface_parses\(\)/],
|
["CLI parse coverage", /fn cli_first_mvp_command_surface_parses\(\)/],
|
||||||
["CLI primary workflow help coverage", /fn top_level_help_exposes_primary_workflow_without_auth\(\)/],
|
["CLI primary workflow help coverage", /fn top_level_help_exposes_primary_workflow_without_auth\(\)/],
|
||||||
|
["CLI non-interactive run auth coverage", /fn non_interactive_run_without_session_requires_explicit_auth_or_local\(\)/],
|
||||||
|
["CLI non-interactive browser login coverage", /fn browser_login_non_interactive_fails_before_opening_browser\(\)/],
|
||||||
["CLI version coverage", /fn top_level_version_is_available\(\)/],
|
["CLI version coverage", /fn top_level_version_is_available\(\)/],
|
||||||
["CLI JSON parse coverage", /fn cli_first_json_mode_parses_for_primary_commands\(\)/],
|
["CLI JSON parse coverage", /fn cli_first_json_mode_parses_for_primary_commands\(\)/],
|
||||||
["CLI human output coverage", /fn human_report_is_text_not_json\(\)/],
|
["CLI human output coverage", /fn human_report_is_text_not_json\(\)/],
|
||||||
|
|
|
||||||
|
|
@ -18,6 +18,21 @@ function disasmer(args) {
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
function disasmerRaw(args, env = {}) {
|
||||||
|
return cp.spawnSync(
|
||||||
|
"cargo",
|
||||||
|
["run", "-q", "-p", "disasmer-cli", "--bin", "disasmer", "--", ...args],
|
||||||
|
{
|
||||||
|
cwd: repo,
|
||||||
|
encoding: "utf8",
|
||||||
|
env: {
|
||||||
|
...process.env,
|
||||||
|
...env,
|
||||||
|
},
|
||||||
|
}
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
const device = disasmer(["login", "--coordinator", coordinator, "--json"]);
|
const device = disasmer(["login", "--coordinator", coordinator, "--json"]);
|
||||||
assert.strictEqual(device.coordinator, coordinator);
|
assert.strictEqual(device.coordinator, coordinator);
|
||||||
assert(device.human_flow.Device, "default human login should use device flow");
|
assert(device.human_flow.Device, "default human login should use device flow");
|
||||||
|
|
@ -44,4 +59,25 @@ assert.match(
|
||||||
assert.match(browser.human_flow.Browser.callback_path, /^http:\/\/127\.0\.0\.1:\d+\/callback$/);
|
assert.match(browser.human_flow.Browser.callback_path, /^http:\/\/127\.0\.0\.1:\d+\/callback$/);
|
||||||
assert.match(browser.human_flow.Browser.state, /^sha256:[a-f0-9]{64}$/);
|
assert.match(browser.human_flow.Browser.state, /^sha256:[a-f0-9]{64}$/);
|
||||||
|
|
||||||
|
const nonInteractiveBrowser = disasmerRaw(
|
||||||
|
["login", "--browser", "--non-interactive", "--coordinator", coordinator, "--json"],
|
||||||
|
{
|
||||||
|
DISASMER_BROWSER_OPEN_COMMAND:
|
||||||
|
"node -e 'require(\"fs\").writeFileSync(\"/tmp/disasmer-browser-should-not-open\", \"opened\")'",
|
||||||
|
}
|
||||||
|
);
|
||||||
|
assert.strictEqual(nonInteractiveBrowser.status, 20, nonInteractiveBrowser.stderr);
|
||||||
|
assert.doesNotMatch(nonInteractiveBrowser.stderr, /Opening Disasmer browser login/);
|
||||||
|
const nonInteractiveReport = JSON.parse(nonInteractiveBrowser.stdout);
|
||||||
|
assert.strictEqual(nonInteractiveReport.status, "authentication_required");
|
||||||
|
assert.strictEqual(nonInteractiveReport.non_interactive, true);
|
||||||
|
assert.strictEqual(nonInteractiveReport.browser_opened, false);
|
||||||
|
assert.strictEqual(nonInteractiveReport.machine_error.category, "authentication");
|
||||||
|
assert.strictEqual(nonInteractiveReport.machine_error.stable_exit_code, 20);
|
||||||
|
assert(
|
||||||
|
nonInteractiveReport.machine_error.next_actions.includes(
|
||||||
|
"rerun without --non-interactive to open the browser"
|
||||||
|
)
|
||||||
|
);
|
||||||
|
|
||||||
console.log("CLI login smoke passed");
|
console.log("CLI login smoke passed");
|
||||||
|
|
|
||||||
|
|
@ -5,7 +5,15 @@ const fs = require("fs");
|
||||||
const path = require("path");
|
const path = require("path");
|
||||||
|
|
||||||
const repo = path.resolve(__dirname, "..");
|
const repo = path.resolve(__dirname, "..");
|
||||||
const readme = fs.readFileSync(path.join(repo, "README.md"), "utf8");
|
const readmePath = path.join(repo, "README.md");
|
||||||
|
if (!fs.existsSync(readmePath)) {
|
||||||
|
if (fs.existsSync(path.join(repo, "DISASMER_PUBLIC_TREE.json"))) {
|
||||||
|
console.log("Docs smoke skipped: root markdown is filtered from this public tree");
|
||||||
|
process.exit(0);
|
||||||
|
}
|
||||||
|
throw new Error("README.md is missing");
|
||||||
|
}
|
||||||
|
const readme = fs.readFileSync(readmePath, "utf8");
|
||||||
const userFacingDocs = [
|
const userFacingDocs = [
|
||||||
"README.md",
|
"README.md",
|
||||||
"MVP.md",
|
"MVP.md",
|
||||||
|
|
@ -73,7 +81,7 @@ const requiredReadmePatterns = [
|
||||||
["hosted community limit", /community tier does not provide arbitrary hosted native commands or hosted containers/],
|
["hosted community limit", /community tier does not provide arbitrary hosted native commands or hosted containers/],
|
||||||
["browser login", /disasmer login --browser/],
|
["browser login", /disasmer login --browser/],
|
||||||
["public-key agents", /disasmer agent enroll --public-key/],
|
["public-key agents", /disasmer agent enroll --public-key/],
|
||||||
["noninteractive agent CLI", /DISASMER_AGENT_PUBLIC_KEY=<agent-public-key> disasmer run build/],
|
["noninteractive agent CLI", /DISASMER_AGENT_PUBLIC_KEY=<agent-public-key> disasmer run --non-interactive build/],
|
||||||
["agent key lifecycle", /register, list, rotate, and revoke an agent key/],
|
["agent key lifecycle", /register, list, rotate, and revoke an agent key/],
|
||||||
["capability auto-detect", /auto-detects OS, architecture/],
|
["capability auto-detect", /auto-detects OS, architecture/],
|
||||||
["capability override", /--cap <name>/],
|
["capability override", /--cap <name>/],
|
||||||
|
|
|
||||||
Loading…
Add table
Add a link
Reference in a new issue