Establishes version control for the personal Logseq DB fork before starting the server-client architecture migration and domain features described in docs/agent-guide and the approved implementation plan. Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
2918 lines
119 KiB
OCaml
2918 lines
119 KiB
OCaml
open Test_support
|
|
|
|
let () = ignore Cli_parity_test_cases.touch
|
|
let tree_pipe = decode_uri_component "%E2%94%82"
|
|
let tree_corner = decode_uri_component "%E2%94%94%E2%94%80%E2%94%80"
|
|
let tree_middle = decode_uri_component "%E2%94%9C%E2%94%80%E2%94%80"
|
|
|
|
let unicode_text codepoints =
|
|
Vec.string_concat ""
|
|
(Vec.map Js.String.fromCodePoint (Vec.of_array codepoints))
|
|
|
|
let () =
|
|
test "json output renders transit quoted uuid as string" (fun () ->
|
|
let uuid = "11111111-1111-4111-8111-111111111111" in
|
|
let value =
|
|
Edn_util.any
|
|
(Melange_edn_melange.tagged "transit/quote" (Edn_util.uuid uuid))
|
|
in
|
|
let output =
|
|
Format_types.to_json
|
|
(Cli_result.ok Output.Mode.Json (Cli_result.Query_result value))
|
|
in
|
|
expect_named_contains "quoted uuid string" output uuid;
|
|
expect_named_not_contains "quoted uuid tag" output "transit/quote";
|
|
expect_named_not_contains "uuid tag object" output "\"tag\"");
|
|
|
|
test "json error output includes hint" (fun () ->
|
|
let err =
|
|
Error.make ~hint:"Run logseq sync start --graph demo."
|
|
Error.Sync_not_started "sync is not started for this graph"
|
|
in
|
|
let output =
|
|
Format_types.to_json (Cli_result.error Output.Mode.Json err)
|
|
in
|
|
expect_named_contains "json error code" output
|
|
"\"code\":\"sync-not-started\"";
|
|
expect_named_contains "json error hint" output
|
|
"\"hint\":\"Run logseq sync start --graph demo.\"")
|
|
|
|
let () =
|
|
test "completion accepts --shell bash" (fun () ->
|
|
let output = run_cli [| "completion"; "--shell"; "bash" |] in
|
|
expect_named_contains "bash completion" output
|
|
"Auto-generated by `logseq completion bash`";
|
|
expect_named_contains "bash completion function" output
|
|
"_logseq_json_names_bash");
|
|
|
|
test "skill show ignores structured output mode" (fun () ->
|
|
mkdir_p ".agents/skills/logseq-cli";
|
|
write_file ".agents/skills/logseq-cli/SKILL.md"
|
|
"---\nname: logseq-cli\n---\n\n# Logseq CLI\n";
|
|
let output = run_cli [| "--output"; "json"; "skill"; "show" |] in
|
|
expect_named_contains "skill markdown name" output "name: logseq-cli";
|
|
expect_named_contains "skill markdown title" output "# Logseq CLI";
|
|
expect_named_not_contains "skill markdown status wrapper" output
|
|
"\"status\"";
|
|
expect_named_not_contains "skill markdown data wrapper" output "\"data\"")
|
|
|
|
let write_cli_wrapper path =
|
|
write_file path
|
|
("require("
|
|
^ Js.Json.stringify (Js.Json.string (Node.Path.resolve "." entrypoint))
|
|
^ ");\n")
|
|
|
|
let spawn_cli_from entrypoint ?(env = [||]) args =
|
|
spawn_sync Node.Process.argv.(0)
|
|
(Array.append [| entrypoint |] args)
|
|
[%obj { encoding = "utf8"; env = clone_env env }]
|
|
|
|
let doctor_args root = [| "--root-dir"; root; "--output"; "json"; "doctor" |]
|
|
|
|
let () =
|
|
test "doctor resolves db-worker-node beside CLI entrypoint" (fun () ->
|
|
let root = temp_dir "logseq-cli-doctor-entrypoint-root-" in
|
|
let app_dir = temp_dir "logseq-cli-doctor-app-" in
|
|
let cli_dir = Node.Path.join [| app_dir; "bin" |] in
|
|
let cli_path = Node.Path.join [| cli_dir; "logseq-cli.js" |] in
|
|
let worker_path = Node.Path.join [| cli_dir; "db-worker-node.js" |] in
|
|
try
|
|
mkdir_p cli_dir;
|
|
write_cli_wrapper cli_path;
|
|
write_file worker_path "console.log('db-worker');\n";
|
|
let result = spawn_cli_from cli_path (doctor_args root) in
|
|
remove_tree root;
|
|
remove_tree app_dir;
|
|
ignore (expect_exit_zero "doctor entrypoint worker" result);
|
|
expect_named_contains "doctor entrypoint path" result##stdout
|
|
worker_path
|
|
with exn ->
|
|
remove_tree root;
|
|
remove_tree app_dir;
|
|
fail_test (Printexc.to_string exn));
|
|
|
|
test "doctor resolves db-worker-node beside CLI inside app.asar" (fun () ->
|
|
let root = temp_dir "logseq-cli-doctor-asar-root-" in
|
|
let app_dir = temp_dir "logseq-cli-doctor-app-" in
|
|
let asar_dir = Node.Path.join [| app_dir; "Logseq.app.asar" |] in
|
|
let cli_path = Node.Path.join [| asar_dir; "logseq-cli.js" |] in
|
|
let worker_path = Node.Path.join [| asar_dir; "db-worker-node.js" |] in
|
|
try
|
|
mkdir_p asar_dir;
|
|
write_cli_wrapper cli_path;
|
|
write_file worker_path "console.log('db-worker');\n";
|
|
let result = spawn_cli_from cli_path (doctor_args root) in
|
|
remove_tree root;
|
|
remove_tree app_dir;
|
|
ignore (expect_exit_zero "doctor asar worker" result);
|
|
expect_named_contains "doctor asar path" result##stdout worker_path
|
|
with exn ->
|
|
remove_tree root;
|
|
remove_tree app_dir;
|
|
fail_test (Printexc.to_string exn));
|
|
|
|
test "doctor resolves db-worker-node under js beside CLI entrypoint"
|
|
(fun () ->
|
|
let root = temp_dir "logseq-cli-doctor-entrypoint-js-root-" in
|
|
let app_dir = temp_dir "logseq-cli-doctor-app-" in
|
|
let cli_dir = Node.Path.join [| app_dir; "bin" |] in
|
|
let cli_js_dir = Node.Path.join [| cli_dir; "js" |] in
|
|
let cli_path = Node.Path.join [| cli_dir; "logseq-cli.js" |] in
|
|
let worker_path = Node.Path.join [| cli_js_dir; "db-worker-node.js" |] in
|
|
try
|
|
mkdir_p cli_js_dir;
|
|
write_cli_wrapper cli_path;
|
|
write_file worker_path "console.log('db-worker');\n";
|
|
let result = spawn_cli_from cli_path (doctor_args root) in
|
|
remove_tree root;
|
|
remove_tree app_dir;
|
|
ignore (expect_exit_zero "doctor entrypoint js worker" result);
|
|
expect_named_contains "doctor entrypoint js path" result##stdout
|
|
worker_path
|
|
with exn ->
|
|
remove_tree root;
|
|
remove_tree app_dir;
|
|
fail_test (Printexc.to_string exn));
|
|
|
|
test "doctor keeps LOGSEQ_DB_WORKER_NODE_SCRIPT before app.asar worker"
|
|
(fun () ->
|
|
let root = temp_dir "logseq-cli-doctor-asar-env-root-" in
|
|
let app_dir = temp_dir "logseq-cli-doctor-app-" in
|
|
let asar_js_dir = Node.Path.join [| app_dir; "Logseq.app.asar"; "js" |] in
|
|
let cli_path = Node.Path.join [| asar_js_dir; "logseq-cli.js" |] in
|
|
let asar_worker_path =
|
|
Node.Path.join [| asar_js_dir; "db-worker-node.js" |]
|
|
in
|
|
let env_worker_path = Node.Path.join [| app_dir; "env-worker.js" |] in
|
|
try
|
|
mkdir_p asar_js_dir;
|
|
write_cli_wrapper cli_path;
|
|
write_file asar_worker_path "console.log('asar db-worker');\n";
|
|
write_file env_worker_path "console.log('env db-worker');\n";
|
|
let result =
|
|
spawn_cli_from cli_path
|
|
~env:[| ("LOGSEQ_DB_WORKER_NODE_SCRIPT", env_worker_path) |]
|
|
(doctor_args root)
|
|
in
|
|
remove_tree root;
|
|
remove_tree app_dir;
|
|
ignore (expect_exit_zero "doctor env worker" result);
|
|
expect_named_contains "doctor env path" result##stdout env_worker_path;
|
|
expect_named_not_contains "doctor env excludes asar path" result##stdout
|
|
asar_worker_path
|
|
with exn ->
|
|
remove_tree root;
|
|
remove_tree app_dir;
|
|
fail_test (Printexc.to_string exn))
|
|
|
|
let () =
|
|
test "sync download requires an explicit graph" (fun () ->
|
|
let root = temp_dir "logseq-cli-sync-download-graph-" in
|
|
try
|
|
write_file
|
|
(Node.Path.join [| root; "cli.edn" |])
|
|
"{:graph \"fallback-file\"}\n";
|
|
let result =
|
|
spawn_cli
|
|
~env:[| ("LOGSEQ_CLI_GRAPH", "fallback-env") |]
|
|
[| "--root-dir"; root; "sync"; "download" |]
|
|
in
|
|
remove_tree root;
|
|
ignore (expect_exit_non_zero "sync download without --graph" result);
|
|
expect_named_contains "sync download missing graph" result##stdout
|
|
"graph name is required"
|
|
with exn ->
|
|
remove_tree root;
|
|
fail_test (Printexc.to_string exn));
|
|
|
|
test "graph commands require explicit unicode graph names" (fun () ->
|
|
let root = temp_dir "logseq-cli-graph-unicode-" in
|
|
let graph = decode_uri_component "%E5%95%8A%E5%95%8A%E5%95%8A%E5%95%8A" in
|
|
let fallback_graph = "fallback-env" in
|
|
let encoded_graph_dir = "~E5~95~8A~E5~95~8A~E5~95~8A~E5~95~8A" in
|
|
let run ?(env = [||]) args =
|
|
spawn_cli ~env (Array.append [| "--root-dir"; root |] args)
|
|
in
|
|
try
|
|
mkdir_p root;
|
|
write_file
|
|
(Node.Path.join [| root; "cli.edn" |])
|
|
"{:graph \"fallback-file\"}\n";
|
|
let create_without_graph =
|
|
run
|
|
~env:[| ("LOGSEQ_CLI_GRAPH", fallback_graph) |]
|
|
[| "graph"; "create" |]
|
|
in
|
|
ignore
|
|
(expect_exit_non_zero "graph create without -g" create_without_graph);
|
|
ignore
|
|
(expect_named_contains "graph create without -g"
|
|
create_without_graph##stdout
|
|
"graph name is required");
|
|
if
|
|
Node.Fs.existsSync
|
|
(Node.Path.join [| root; "graphs"; fallback_graph |])
|
|
then fail_test "graph create without -g created fallback graph";
|
|
let create_fallback =
|
|
run [| "graph"; "create"; "-g"; fallback_graph |]
|
|
in
|
|
ignore (expect_exit_zero "fallback graph create" create_fallback);
|
|
if Node.Fs.existsSync (Node.Path.join [| root; "current-graph" |]) then
|
|
fail_test "graph create wrote current-graph";
|
|
let switch_without_graph =
|
|
run
|
|
~env:[| ("LOGSEQ_CLI_GRAPH", fallback_graph) |]
|
|
[| "graph"; "switch" |]
|
|
in
|
|
ignore
|
|
(expect_exit_non_zero "graph switch without -g" switch_without_graph);
|
|
ignore
|
|
(expect_named_contains "graph switch without -g"
|
|
switch_without_graph##stdout
|
|
"graph name is required");
|
|
let remove_without_graph =
|
|
run
|
|
~env:[| ("LOGSEQ_CLI_GRAPH", fallback_graph) |]
|
|
[| "graph"; "remove" |]
|
|
in
|
|
ignore
|
|
(expect_exit_non_zero "graph remove without -g" remove_without_graph);
|
|
ignore
|
|
(expect_named_contains "graph remove without -g"
|
|
remove_without_graph##stdout
|
|
"graph name is required");
|
|
if
|
|
not
|
|
(Node.Fs.existsSync
|
|
(Node.Path.join [| root; "graphs"; fallback_graph |]))
|
|
then fail_test "graph remove without -g removed fallback graph";
|
|
let create = run [| "graph"; "create"; "-g"; graph |] in
|
|
ignore (expect_exit_zero "graph create" create);
|
|
if Node.Fs.existsSync (Node.Path.join [| root; "current-graph" |]) then
|
|
fail_test "graph create wrote current-graph";
|
|
ignore (expect_named_contains "graph create" create##stdout graph);
|
|
let graph_dirs =
|
|
Node.Fs.readdirSync (Node.Path.join [| root; "graphs" |])
|
|
in
|
|
if not (Array.exists (( = ) encoded_graph_dir) graph_dirs) then
|
|
fail_test
|
|
(Printf.sprintf "graph dir: expected %s, got [%s]" encoded_graph_dir
|
|
(graph_dirs |> Vec.of_array |> Vec.string_concat ", "));
|
|
let human_list = run [| "graph"; "list" |] in
|
|
ignore (expect_exit_zero "human graph list" human_list);
|
|
ignore
|
|
(expect_named_contains "human graph list" human_list##stdout graph);
|
|
let json_list = run [| "--output"; "json"; "graph"; "list" |] in
|
|
ignore (expect_exit_zero "json graph list" json_list);
|
|
remove_tree root;
|
|
expect_graph_in_json_list json_list##stdout graph
|
|
with exn ->
|
|
remove_tree root;
|
|
fail_test (Printexc.to_string exn));
|
|
|
|
test "graph list ignores current-graph file" (fun () ->
|
|
let root = temp_dir "logseq-cli-graph-list-current-file-" in
|
|
let run args = spawn_cli (Array.append [| "--root-dir"; root |] args) in
|
|
try
|
|
mkdir_p root;
|
|
ignore
|
|
(expect_exit_zero "create alpha"
|
|
(run [| "graph"; "create"; "-g"; "alpha" |]));
|
|
ignore
|
|
(expect_exit_zero "create beta"
|
|
(run [| "graph"; "create"; "-g"; "beta" |]));
|
|
write_file (Node.Path.join [| root; "current-graph" |]) "alpha";
|
|
let result = run [| "graph"; "list" |] in
|
|
remove_tree root;
|
|
ignore (expect_exit_zero "graph list" result);
|
|
expect_named_not_contains "graph list current file marker"
|
|
result##stdout "* alpha"
|
|
with exn ->
|
|
remove_tree root;
|
|
fail_test (Printexc.to_string exn));
|
|
|
|
test "graph switch updates cli.edn graph" (fun () ->
|
|
let root = temp_dir "logseq-cli-graph-switch-config-" in
|
|
let run args = spawn_cli (Array.append [| "--root-dir"; root |] args) in
|
|
try
|
|
mkdir_p root;
|
|
ignore
|
|
(expect_exit_zero "create alpha"
|
|
(run [| "graph"; "create"; "-g"; "alpha" |]));
|
|
ignore
|
|
(expect_exit_zero "create beta"
|
|
(run [| "graph"; "create"; "-g"; "beta" |]));
|
|
write_file
|
|
(Node.Path.join [| root; "cli.edn" |])
|
|
"{:http-base \"http://127.0.0.1:65535\"}\n";
|
|
let switch =
|
|
spawn_cli
|
|
~env:[| ("LOGSEQ_CLI_BASE_URL", "http://127.0.0.1:65535") |]
|
|
[|
|
|
"--root-dir";
|
|
root;
|
|
"--output";
|
|
"json";
|
|
"--graph";
|
|
"beta";
|
|
"graph";
|
|
"switch";
|
|
|]
|
|
in
|
|
ignore (expect_exit_zero "graph switch" switch);
|
|
ignore
|
|
(expect_named_contains "graph switch message" switch##stdout
|
|
"\"message\":\"switched to beta\"");
|
|
let config = read_file (Node.Path.join [| root; "cli.edn" |]) in
|
|
ignore
|
|
(expect_named_contains "graph switch config graph" config
|
|
":graph \"beta\"");
|
|
ignore
|
|
(expect_named_contains "graph switch config preserves http-base"
|
|
config ":http-base \"http://127.0.0.1:65535\"");
|
|
let list = run [| "graph"; "list" |] in
|
|
remove_tree root;
|
|
ignore (expect_exit_zero "graph list" list);
|
|
expect_named_contains "graph list selected graph" list##stdout "* beta"
|
|
with exn ->
|
|
remove_tree root;
|
|
fail_test (Printexc.to_string exn));
|
|
|
|
test "json output escapes control characters" (fun () ->
|
|
let root = temp_dir "logseq-cli-json-output-" in
|
|
let graph = "json" ^ control_char ^ "graph" in
|
|
try
|
|
let result =
|
|
spawn_cli
|
|
[|
|
|
"--root-dir";
|
|
root;
|
|
"--output";
|
|
"json";
|
|
"graph";
|
|
"create";
|
|
"--graph";
|
|
graph;
|
|
|]
|
|
in
|
|
remove_tree root;
|
|
ignore (assert_exit_zero "json output graph create" result);
|
|
expect_valid_json "json output graph create" result##stdout
|
|
with exn ->
|
|
remove_tree root;
|
|
fail_test (Printexc.to_string exn));
|
|
|
|
test "verbose parsed options line is valid edn" (fun () ->
|
|
let root = temp_dir "logseq-cli verbose root-" in
|
|
try
|
|
let result =
|
|
spawn_cli
|
|
[|
|
|
"--verbose";
|
|
"--root-dir";
|
|
root;
|
|
"--graph";
|
|
"alpha";
|
|
"graph";
|
|
"list";
|
|
|]
|
|
in
|
|
remove_tree root;
|
|
ignore (assert_exit_zero "verbose graph list" result);
|
|
match Vec.peek_front_opt (Vec.split_on_char '\n' result##stderr) with
|
|
| Some line when String.trim line <> "" ->
|
|
expect_valid_edn "verbose parsed options line" line
|
|
| _ -> fail_test ("missing verbose stderr line: " ^ result##stderr)
|
|
with exn ->
|
|
remove_tree root;
|
|
fail_test (Printexc.to_string exn));
|
|
|
|
test "login times out without socket fallback" (fun () ->
|
|
let root = temp_dir "logseq-cli-login-" in
|
|
let config_path = Node.Path.join [| root; "cli.edn" |] in
|
|
try
|
|
write_file config_path "{:open-browser false :login-timeout-ms 1}\n";
|
|
let result =
|
|
spawn_cli [| "--root-dir"; root; "--config"; config_path; "login" |]
|
|
in
|
|
remove_tree root;
|
|
ignore (expect_exit_non_zero "login without callback" result);
|
|
ignore
|
|
(expect_named_not_contains "login stdout socket fallback"
|
|
result##stdout "sockets are unavailable");
|
|
ignore
|
|
(expect_named_not_contains "login stderr socket fallback"
|
|
result##stderr "sockets are unavailable");
|
|
if
|
|
String.contains result##stdout 'l'
|
|
&& Js.String.includes ~search:"login callback timed out"
|
|
result##stdout
|
|
|| String.contains result##stderr 'l'
|
|
&& Js.String.includes ~search:"login callback timed out"
|
|
result##stderr
|
|
then pass
|
|
else
|
|
fail_test
|
|
(Printf.sprintf "expected login callback timeout\n%s\n%s"
|
|
result##stdout result##stderr)
|
|
with exn ->
|
|
remove_tree root;
|
|
fail_test (Printexc.to_string exn));
|
|
|
|
test_promise "login generates fresh PKCE challenge across CLI processes"
|
|
(fun () ->
|
|
let login_once base_url index =
|
|
let root =
|
|
temp_dir ("logseq-cli-login-pkce-" ^ string_of_int index ^ "-")
|
|
in
|
|
let config_path = Node.Path.join [| root; "cli.edn" |] in
|
|
let auth_path = Node.Path.join [| root; "auth.json" |] in
|
|
let state, encoded_state =
|
|
if index = 1 then (unicode_text [| 0x4e2d |], "%E4%B8%AD")
|
|
else ("fixed-test-state", "fixed-test-state")
|
|
in
|
|
let code = "auth-code-" ^ string_of_int index in
|
|
write_file config_path
|
|
("{:open-browser false\n :login-timeout-ms 5000\n :auth-path "
|
|
^ Printf.sprintf "%S" auth_path
|
|
^ "\n :oauth-authorize-endpoint "
|
|
^ Printf.sprintf "%S" (base_url ^ "/authorize")
|
|
^ "\n :oauth-token-endpoint "
|
|
^ Printf.sprintf "%S" (base_url ^ "/oauth2/token")
|
|
^ "\n :oauth-client-id \"test-client\"\n :oauth-state \"" ^ state
|
|
^ "\"}\n");
|
|
let login =
|
|
run_cli_p
|
|
[|
|
|
"--root-dir";
|
|
root;
|
|
"--config";
|
|
config_path;
|
|
"--output";
|
|
"json";
|
|
"login";
|
|
|]
|
|
in
|
|
let callback_url =
|
|
Printf.sprintf "http://localhost:8765/auth/callback?state=%s&code=%s"
|
|
encoded_state code
|
|
in
|
|
let* () = fetch_with_retry 100 callback_url in
|
|
let* result = login in
|
|
remove_tree root;
|
|
ignore (expect_cli_exit_zero "login pkce" result);
|
|
let authorize_url = json_data_string result.stdout "authorize-url" in
|
|
if query_param authorize_url "state" <> Some encoded_state then
|
|
fail_promise ("unexpected state: " ^ authorize_url)
|
|
else
|
|
match query_param authorize_url "code_challenge" with
|
|
| Some challenge -> Js.Promise.resolve challenge
|
|
| None -> fail_promise ("missing code_challenge: " ^ authorize_url)
|
|
in
|
|
with_server (oauth_server ()) (fun base_url ->
|
|
let* first = login_once base_url 1 in
|
|
let* second = login_once base_url 2 in
|
|
if first <> second then Js.Promise.resolve pass
|
|
else
|
|
fail_promise
|
|
("expected fresh PKCE challenge across logins, got " ^ first)));
|
|
|
|
test_promise "logout deletes auth and exits without callback" (fun () ->
|
|
let root = temp_dir "logseq-cli-logout-flow-" in
|
|
let config_path = Node.Path.join [| root; "cli.edn" |] in
|
|
let auth_path = Node.Path.join [| root; "auth.json" |] in
|
|
let state = "fixed-logout-state" in
|
|
let write_auth () =
|
|
write_file auth_path
|
|
"{\"provider\":\"cognito\",\"refresh-token\":\"refresh-token\"}\n"
|
|
in
|
|
let logout_once base_url =
|
|
write_auth ();
|
|
write_file config_path
|
|
(Printf.sprintf
|
|
"{:open-browser false\n\
|
|
\ :logout-timeout-ms 5000\n\
|
|
\ :auth-path %S\n\
|
|
\ :http-base %S\n\
|
|
\ :oauth-client-id \"test-client\"\n\
|
|
\ :oauth-logout-state %S}\n"
|
|
auth_path base_url state);
|
|
let logout =
|
|
run_cli_p
|
|
[|
|
|
"--root-dir";
|
|
root;
|
|
"--config";
|
|
config_path;
|
|
"--output";
|
|
"json";
|
|
"logout";
|
|
|]
|
|
in
|
|
let* result = logout in
|
|
ignore (expect_cli_exit_zero "logout" result);
|
|
if Node.Fs.existsSync auth_path then
|
|
fail_promise "logout did not delete auth file"
|
|
else
|
|
let logout_url = json_data_string result.stdout "logout-url" in
|
|
let deleted = json_data_bool result.stdout "deleted" in
|
|
let opened = json_data_bool result.stdout "opened" in
|
|
let completed = json_data_bool result.stdout "logout-completed" in
|
|
if not deleted then
|
|
fail_promise ("expected deleted=true: " ^ result.stdout)
|
|
else if opened then
|
|
fail_promise ("expected opened=false: " ^ result.stdout)
|
|
else if not completed then
|
|
fail_promise ("expected logout-completed=true: " ^ result.stdout)
|
|
else if
|
|
not
|
|
(Js.String.startsWith ~prefix:(base_url ^ "/logout?") logout_url)
|
|
then fail_promise ("unexpected logout-url: " ^ logout_url)
|
|
else if query_param logout_url "client_id" <> Some "test-client" then
|
|
fail_promise ("missing client_id: " ^ logout_url)
|
|
else if query_param logout_url "response_type" <> Some "code" then
|
|
fail_promise ("missing response_type: " ^ logout_url)
|
|
else if
|
|
query_param logout_url "scope" <> Some "email%20openid%20phone"
|
|
then fail_promise ("missing scope: " ^ logout_url)
|
|
else if query_param logout_url "state" <> Some state then
|
|
fail_promise ("missing state: " ^ logout_url)
|
|
else
|
|
match query_param logout_url "redirect_uri" with
|
|
| Some uri
|
|
when uri = "http%3A%2F%2Flocalhost%3A8765%2Fauth%2Fcallback" ->
|
|
Js.Promise.resolve pass
|
|
| _ -> fail_promise ("missing redirect_uri: " ^ logout_url)
|
|
in
|
|
with_server (oauth_server ()) (fun base_url ->
|
|
let* result = logout_once base_url in
|
|
remove_tree root;
|
|
Js.Promise.resolve result));
|
|
|
|
test "example without selector returns all examples as json" (fun () ->
|
|
let result = spawn_cli [| "--output"; "json"; "example" |] in
|
|
ignore (expect_exit_zero "example all json" result);
|
|
ignore
|
|
(expect_named_contains "example all json status" result##stdout
|
|
"\"status\":\"ok\"");
|
|
ignore
|
|
(expect_named_contains "example all json selector" result##stdout
|
|
"\"selector\":\"all\"");
|
|
ignore
|
|
(expect_named_contains "example all json graph example" result##stdout
|
|
"logseq graph list");
|
|
ignore
|
|
(expect_named_contains "example all json sync example" result##stdout
|
|
"logseq sync config unset sync-enabled");
|
|
ignore
|
|
(expect_named_not_contains "example all json old sync flag"
|
|
result##stdout "--key sync-enabled");
|
|
pass);
|
|
|
|
test "example human output uses readable command and example lists" (fun () ->
|
|
let result = spawn_cli [| "example" |] in
|
|
ignore (expect_exit_zero "example all human" result);
|
|
ignore
|
|
(expect_named_contains "example human selector" result##stdout
|
|
"Selector: all");
|
|
ignore
|
|
(expect_named_contains "example human commands heading" result##stdout
|
|
"Matched commands:");
|
|
ignore
|
|
(expect_named_contains "example human command bullet" result##stdout
|
|
" - graph list");
|
|
ignore
|
|
(expect_named_contains "example human sync command bullet"
|
|
result##stdout " - sync config unset");
|
|
ignore
|
|
(expect_named_contains "example human examples heading" result##stdout
|
|
"Examples:");
|
|
ignore
|
|
(expect_named_contains "example human example bullet" result##stdout
|
|
" - logseq graph list");
|
|
expect_named_contains "example human sync example bullet" result##stdout
|
|
" - logseq sync config unset sync-enabled";
|
|
expect_named_not_contains "example human old sync flag" result##stdout
|
|
"--key sync-enabled")
|
|
|
|
let () =
|
|
test_promise "graph info formats kv metadata" (fun () ->
|
|
let root = temp_dir "logseq-cli-graph-info-" in
|
|
let request_count = ref 0 in
|
|
let result_transit =
|
|
"[\"~#set\",[[\"~:logseq.kv/graph-created-at\",1700000000],[\"~:logseq.kv/schema-version\",77],[\"~:logseq.kv/custom\",\"demo\"]]]"
|
|
in
|
|
let server =
|
|
invoke_server (fun body ->
|
|
incr request_count;
|
|
if not (Js.String.includes ~search:"thread-api/q" body) then
|
|
fail_test ("unexpected invoke method: " ^ body);
|
|
if not (Js.String.includes ~search:"logseq_db_alpha" body) then
|
|
fail_test ("missing repo in request: " ^ body);
|
|
if not (Js.String.includes ~search:"logseq.kv" body) then
|
|
fail_test ("missing graph info query namespace: " ^ body);
|
|
result_transit)
|
|
in
|
|
with_server server (fun base_url ->
|
|
let env = [| ("LOGSEQ_CLI_BASE_URL", base_url) |] in
|
|
let* human =
|
|
run_cli_p ~env
|
|
[| "--root-dir"; root; "--graph"; "alpha"; "graph"; "info" |]
|
|
in
|
|
ignore (expect_cli_exit_zero "human graph info" human);
|
|
ignore (expect_named_contains "human graph" human.stdout "graph");
|
|
ignore
|
|
(expect_named_contains "human schema" human.stdout
|
|
"logseq.kv/schema-version 77");
|
|
ignore (expect_named_contains "human created" human.stdout "ago");
|
|
ignore
|
|
(expect_line_starts_with "human custom kv row" human.stdout
|
|
"logseq.kv/custom");
|
|
ignore
|
|
(expect_named_not_contains "human created fallback" human.stdout
|
|
"ms ago");
|
|
ignore
|
|
(expect_named_not_contains "human unreasonable created seconds"
|
|
human.stdout "56 years");
|
|
ignore
|
|
(expect_named_not_contains "human no field header" human.stdout
|
|
"Field");
|
|
ignore
|
|
(expect_named_not_contains "human no value header" human.stdout
|
|
"Value");
|
|
ignore
|
|
(expect_named_not_contains "human no kv aggregate" human.stdout
|
|
"kv {");
|
|
ignore
|
|
(expect_named_not_contains "human missing created" human.stdout
|
|
"logseq.kv/graph-created-at -");
|
|
ignore
|
|
(expect_named_not_contains "human empty kv" human.stdout "kv {}");
|
|
let* json =
|
|
run_cli_p ~env
|
|
[|
|
|
"--root-dir";
|
|
root;
|
|
"--graph";
|
|
"alpha";
|
|
"--output";
|
|
"json";
|
|
"graph";
|
|
"info";
|
|
|]
|
|
in
|
|
ignore (expect_cli_exit_zero "json graph info" json);
|
|
ignore
|
|
(expect_named_contains "json created" json.stdout
|
|
"\"logseq.kv/graph-created-at\":1700000000");
|
|
ignore
|
|
(expect_named_contains "json schema" json.stdout
|
|
"\"logseq.kv/schema-version\":77");
|
|
ignore
|
|
(expect_named_contains "json kv" json.stdout
|
|
"\"logseq.kv/custom\":\"demo\"");
|
|
remove_tree root;
|
|
if !request_count = 2 then Js.Promise.resolve pass
|
|
else
|
|
fail_promise
|
|
(Printf.sprintf "expected two graph info requests, got %d"
|
|
!request_count)));
|
|
|
|
test_promise "graph info reuses an existing db-worker from health json"
|
|
(fun () ->
|
|
let root = temp_dir "logseq-cli-graph-info-existing-server-" in
|
|
let request_count = ref 0 in
|
|
let result_transit =
|
|
"[\"~#set\",[[\"~:logseq.kv/schema-version\",77]]]"
|
|
in
|
|
let port_of_base_url base_url =
|
|
let colon = String.rindex base_url ':' in
|
|
String.sub base_url (colon + 1) (String.length base_url - colon - 1)
|
|
in
|
|
let server =
|
|
create_server (fun[@u] req res ->
|
|
let body = ref "" in
|
|
req_set_encoding req "utf8";
|
|
req_on_data req "data" (fun[@u] chunk -> body := !body ^ chunk);
|
|
req_on_end req "end" (fun[@u] () ->
|
|
match (req_method req, req_url req) with
|
|
| "GET", "/healthz" ->
|
|
write_json res 200
|
|
"{\"repo\":\"logseq_db_alpha\",\"status\":\"ready\",\"host\":\"127.0.0.1\",\"pid\":1,\"owner-source\":\"cli\",\"root-dir\":\"/tmp/logseq-cli\",\"revision\":\"test-revision\"}"
|
|
| "POST", "/v1/invoke" ->
|
|
incr request_count;
|
|
if not (Js.String.includes ~search:"thread-api/q" !body)
|
|
then fail_test ("unexpected invoke method: " ^ !body);
|
|
if not (Js.String.includes ~search:"logseq_db_alpha" !body)
|
|
then fail_test ("missing repo in request: " ^ !body);
|
|
write_json res 200 (json_response result_transit)
|
|
| _ -> write_json res 404 (error_response "not found")))
|
|
in
|
|
with_server server (fun base_url ->
|
|
let port = port_of_base_url base_url in
|
|
write_file
|
|
(Node.Path.join [| root; "server-list" |])
|
|
(string_of_int process_pid ^ " " ^ port ^ "\n");
|
|
let* result =
|
|
run_cli_p
|
|
~env:
|
|
[|
|
|
( "LOGSEQ_DB_WORKER_NODE_SCRIPT",
|
|
Node.Path.join [| root; "missing-db-worker-node.js" |] );
|
|
|]
|
|
[| "--root-dir"; root; "--graph"; "alpha"; "graph"; "info" |]
|
|
in
|
|
remove_tree root;
|
|
ignore (expect_cli_exit_zero "graph info existing server" result);
|
|
ignore
|
|
(expect_named_contains "existing server schema" result.stdout
|
|
"logseq.kv/schema-version 77");
|
|
if !request_count = 1 then Js.Promise.resolve pass
|
|
else
|
|
fail_promise
|
|
(Printf.sprintf "expected one graph info request, got %d"
|
|
!request_count)));
|
|
|
|
test_promise "list page human output formats timestamps for large result sets"
|
|
(fun () ->
|
|
let request_count = ref 0 in
|
|
let row index =
|
|
Printf.sprintf
|
|
"{\"~:db/id\":%d,\"~:block/title\":%s,\"~:db/ident\":%s,\"~:logseq.property/type\":\"node\",\"~:block/created-at\":%d,\"~:block/updated-at\":%d}"
|
|
(index + 1)
|
|
(Js.Json.stringify
|
|
(Js.Json.string
|
|
(Printf.sprintf "Page %d with enough text for table formatting"
|
|
(index + 1))))
|
|
(Js.Json.stringify
|
|
(Js.Json.string
|
|
(Printf.sprintf "~:user.property/Page-%d-performance-check"
|
|
(index + 1))))
|
|
(1700000000000 + index) (1780411164893 + index)
|
|
in
|
|
let result_transit =
|
|
"[" ^ Vec.string_concat "," (Vec.init 700 row) ^ "]"
|
|
in
|
|
let server =
|
|
invoke_server (fun body ->
|
|
incr request_count;
|
|
if not (Js.String.includes ~search:"thread-api/cli-list-pages" body)
|
|
then fail_test ("unexpected invoke method: " ^ body);
|
|
result_transit)
|
|
in
|
|
with_server server (fun base_url ->
|
|
let* output =
|
|
run_cli_p
|
|
~env:[| ("LOGSEQ_CLI_BASE_URL", base_url) |]
|
|
[| "--graph"; "alpha"; "list"; "page" |]
|
|
in
|
|
ignore (expect_cli_exit_zero "list page human" output);
|
|
if !request_count <> 1 then
|
|
fail_promise
|
|
(Printf.sprintf "expected one request, got %d" !request_count)
|
|
else (
|
|
ignore
|
|
(expect_named_not_contains "created timestamp raw" output.stdout
|
|
"1700000000000");
|
|
ignore
|
|
(expect_named_not_contains "updated timestamp raw" output.stdout
|
|
"1780411164893");
|
|
ignore
|
|
(expect_named_not_contains "zero years output" output.stdout
|
|
"zero years");
|
|
ignore
|
|
(expect_named_contains "count footer" output.stdout "Count: 700");
|
|
Js.Promise.resolve pass)));
|
|
|
|
test_promise "list asset human output keeps asset columns stable" (fun () ->
|
|
let request_count = ref 0 in
|
|
let result_transit =
|
|
"[{\"~:block/updated-at\":1779715760691,\"~:logseq.property.asset/checksum\":\"abc123\",\"~:logseq.property.asset/size\":4096,\"~:block/title\":\"logo.png\",\"~:block/created-at\":1777063533827,\"~:db/id\":10,\"~:logseq.property.asset/type\":\"png\"}]"
|
|
in
|
|
let server =
|
|
invoke_server (fun body ->
|
|
incr request_count;
|
|
match !request_count with
|
|
| 1
|
|
when Js.String.includes ~search:"thread-api/pull" body
|
|
&& Js.String.includes ~search:"logseq.class/Asset" body ->
|
|
"[\"^ \",\"~:db/id\",77]"
|
|
| 2 -> result_transit
|
|
| _ ->
|
|
fail_test
|
|
(Printf.sprintf "unexpected request %d: %s" !request_count
|
|
body);
|
|
"")
|
|
in
|
|
let root = temp_dir "logseq-cli-list-asset-" in
|
|
with_server server (fun base_url ->
|
|
let* output =
|
|
run_cli_p
|
|
~env:[| ("LOGSEQ_CLI_BASE_URL", base_url) |]
|
|
[| "--root-dir"; root; "--graph"; "alpha"; "list"; "asset" |]
|
|
in
|
|
remove_tree root;
|
|
ignore (expect_cli_exit_zero "list asset" output);
|
|
if !request_count <> 2 then
|
|
fail_promise
|
|
(Printf.sprintf "expected two requests, got %d" !request_count)
|
|
else
|
|
let headers = headers_from output.stdout in
|
|
let expected_prefix = [| "id"; "title"; "size"; "type" |] in
|
|
let expected_suffix = [| "created-at"; "updated-at" |] in
|
|
if
|
|
not
|
|
(string_array_equal
|
|
(array_prefix headers (Array.length expected_prefix))
|
|
expected_prefix)
|
|
then
|
|
fail_promise
|
|
("unexpected asset header prefix: "
|
|
^ (headers |> Vec.of_array |> Vec.string_concat ","))
|
|
else if
|
|
not
|
|
(string_array_equal
|
|
(array_suffix headers (Array.length expected_suffix))
|
|
expected_suffix)
|
|
then
|
|
fail_promise
|
|
("unexpected asset header suffix: "
|
|
^ (headers |> Vec.of_array |> Vec.string_concat ","))
|
|
else if not (Array.exists (( = ) "checksum") headers) then
|
|
fail_promise ("missing middle asset column:\n" ^ output.stdout)
|
|
else if Js.String.includes ~search:"node/type" output.stdout then
|
|
fail_promise
|
|
("asset output must not render node/type:\n" ^ output.stdout)
|
|
else Js.Promise.resolve pass));
|
|
|
|
test_promise "list task human output uses registry header order" (fun () ->
|
|
let result_transit =
|
|
"[{\"~:logseq.property/priority\":\"~:logseq.property/priority.high\",\"~:block/title\":\"Ship\",\"~:logseq.property/status\":\"~:logseq.property/status.todo\",\"~:block/created-at\":40000,\"~:block/updated-at\":90000,\"~:db/id\":12,\"~:block/uuid\":\"u1\"}]"
|
|
in
|
|
let server =
|
|
invoke_server (fun body ->
|
|
if Js.String.includes ~search:"thread-api/cli-list-tasks" body then
|
|
result_transit
|
|
else (
|
|
fail_test ("unexpected request: " ^ body);
|
|
"null"))
|
|
in
|
|
let root = temp_dir "logseq-cli-list-task-order-" in
|
|
with_server server (fun base_url ->
|
|
let* output =
|
|
run_cli_p
|
|
~env:[| ("LOGSEQ_CLI_BASE_URL", base_url) |]
|
|
[| "--root-dir"; root; "--graph"; "alpha"; "list"; "task" |]
|
|
in
|
|
remove_tree root;
|
|
ignore (expect_cli_exit_zero "list task" output);
|
|
let headers =
|
|
headers_from output.stdout |> Vec.of_array |> Vec.string_concat ","
|
|
in
|
|
if headers <> "id,title,status,priority,created-at,updated-at" then
|
|
fail_promise ("unexpected task headers: " ^ headers)
|
|
else if
|
|
Js.String.includes ~search:"logseq.property/status.todo"
|
|
output.stdout
|
|
then
|
|
fail_promise ("task status value kept namespace:\n" ^ output.stdout)
|
|
else if
|
|
Js.String.includes ~search:"logseq.property/priority.high"
|
|
output.stdout
|
|
then
|
|
fail_promise
|
|
("task priority value kept namespace:\n" ^ output.stdout)
|
|
else (
|
|
ignore
|
|
(expect_named_contains "task status value" output.stdout
|
|
"status.todo");
|
|
ignore
|
|
(expect_named_contains "task priority value" output.stdout
|
|
"priority.high");
|
|
Js.Promise.resolve pass)));
|
|
|
|
test "CLI exits quietly on stdout EPIPE" (fun () ->
|
|
let root = temp_dir "logseq-cli-stdout-epipe-" in
|
|
let script_path = Node.Path.join [| root; "emit-stdout-epipe.js" |] in
|
|
let entrypoint_path = Node.Path.resolve "." entrypoint in
|
|
try
|
|
write_file script_path
|
|
("process.argv = [process.argv[0], "
|
|
^ Js.Json.stringify (Js.Json.string entrypoint_path)
|
|
^ ", '--version'];\nrequire("
|
|
^ Js.Json.stringify (Js.Json.string entrypoint_path)
|
|
^ ");\n\
|
|
const err = new Error('write EPIPE');\n\
|
|
err.code = 'EPIPE';\n\
|
|
process.stdout.emit('error', err);\n");
|
|
let output = spawn_cli_from script_path [||] in
|
|
remove_tree root;
|
|
ignore (expect_exit_zero "stdout EPIPE" output);
|
|
expect_named_not_contains "stdout EPIPE stderr" output##stderr "EPIPE";
|
|
expect_named_not_contains "stdout EPIPE unhandled" output##stderr
|
|
"Unhandled 'error' event"
|
|
with exn ->
|
|
remove_tree root;
|
|
fail_test (Printexc.to_string exn));
|
|
|
|
test_promise "list page human output aligns ragged property rows" (fun () ->
|
|
let request_count = ref 0 in
|
|
let title =
|
|
decode_uri_component "%E4%B8%AD%E6%96%87%E5%9B%BE%E8%B0%B1Alpha"
|
|
in
|
|
let truncated_title =
|
|
decode_uri_component "%E4%B8%AD%E6%96%87%E2%80%A6"
|
|
in
|
|
let result_transit =
|
|
"["
|
|
^ String.concat ","
|
|
[
|
|
Printf.sprintf
|
|
"{\"~:db/id\":1,\"~:block/title\":\"Plain\",\"~:block/created-at\":1777063533827,\"~:block/updated-at\":1779715760691}";
|
|
"{\"~:db/id\":2,\"~:block/title\":\"Property\",\"~:block/created-at\":1733130109047,\"~:block/updated-at\":1779543495725,\"~:db/ident\":\"~:user.property/Assignee-Xf-emnkZ\",\"~:logseq.property/type\":\"node\"}";
|
|
Printf.sprintf
|
|
"{\"~:db/id\":3,\"~:block/title\":%s,\"~:block/created-at\":1700000000000,\"~:block/updated-at\":1700000000000}"
|
|
(Js.Json.stringify (Js.Json.string title));
|
|
]
|
|
^ "]"
|
|
in
|
|
let server =
|
|
invoke_server (fun _body ->
|
|
incr request_count;
|
|
result_transit)
|
|
in
|
|
let root = temp_dir "logseq-cli-ragged-" in
|
|
write_file
|
|
(Node.Path.join [| root; "cli.edn" |])
|
|
"{:list-title-max-display-width 6}";
|
|
with_server server (fun base_url ->
|
|
let* output =
|
|
run_cli_p
|
|
~env:[| ("LOGSEQ_CLI_BASE_URL", base_url) |]
|
|
[| "--root-dir"; root; "--graph"; "alpha"; "list"; "page" |]
|
|
in
|
|
remove_tree root;
|
|
ignore (expect_cli_exit_zero "list page ragged" output);
|
|
if !request_count <> 1 then
|
|
fail_promise
|
|
(Printf.sprintf "expected one request, got %d" !request_count)
|
|
else (
|
|
ignore
|
|
(expect_named_contains "db ident" output.stdout
|
|
"user.property/Assignee-Xf-emnkZ");
|
|
ignore
|
|
(expect_named_not_contains "property type column" output.stdout
|
|
"logseq.property/type");
|
|
ignore
|
|
(expect_named_not_contains "node type value" output.stdout "node");
|
|
ignore
|
|
(expect_named_contains "display-width truncation" output.stdout
|
|
truncated_title);
|
|
ignore (expect_named_not_contains "full title" output.stdout title);
|
|
ignore (expect_created_at_column_aligned output.stdout);
|
|
ignore
|
|
(expect_named_contains "ragged placeholder" output.stdout "-");
|
|
Js.Promise.resolve pass)));
|
|
|
|
test_promise "upsert task sends scheduled and deadline as epoch millis"
|
|
(fun () ->
|
|
let step = ref 0 in
|
|
let server =
|
|
invoke_server (fun body ->
|
|
incr step;
|
|
match !step with
|
|
| 1 when Js.String.includes ~search:"thread-api/q" body ->
|
|
"[\"~#list\",[\"~:logseq.property/status.todo\"]]"
|
|
| 2
|
|
when Js.String.includes ~search:"thread-api/pull" body
|
|
&& Js.String.includes ~search:"logseq.class/Task" body ->
|
|
"[\"^ \",\"~:db/id\",77]"
|
|
| 3
|
|
when Js.String.includes ~search:"thread-api/q" body
|
|
&& Js.String.includes ~search:"taskhome" body ->
|
|
"[[\"^ \
|
|
\",\"~:db/id\",42,\"~:block/uuid\",\"11111111-1111-1111-1111-111111111111\"]]"
|
|
| 4
|
|
when Js.String.includes ~search:"thread-api/apply-outliner-ops"
|
|
body ->
|
|
if
|
|
Js.String.includes ~search:"2026-02-10T08:00:00.000Z" body
|
|
|| Js.String.includes ~search:"2026-02-12T18:00:00.000Z" body
|
|
then
|
|
fail_test
|
|
("datetime task values must not be ISO strings: " ^ body);
|
|
if
|
|
Js.String.includes ~search:"\\\"1770710400000\\\"" body
|
|
|| Js.String.includes ~search:"\\\"1770919200000\\\"" body
|
|
then
|
|
fail_test
|
|
("datetime task values must be JSON numbers: " ^ body);
|
|
if
|
|
not
|
|
(Js.String.includes ~search:"1770710400000" body
|
|
&& Js.String.includes ~search:"1770919200000" body)
|
|
then fail_test ("missing epoch millis task values: " ^ body);
|
|
"[]"
|
|
| _ ->
|
|
fail_test
|
|
(Printf.sprintf "unexpected request at step %d: %s" !step body);
|
|
"")
|
|
in
|
|
with_server server (fun base_url ->
|
|
let* output =
|
|
run_cli_p
|
|
~env:[| ("LOGSEQ_CLI_BASE_URL", base_url) |]
|
|
[|
|
|
"--graph";
|
|
"alpha";
|
|
"--output";
|
|
"json";
|
|
"upsert";
|
|
"task";
|
|
"--page";
|
|
"TaskHome";
|
|
"--status";
|
|
"todo";
|
|
"--priority";
|
|
"high";
|
|
"--scheduled";
|
|
"2026-02-10T08:00:00.000Z";
|
|
"--deadline";
|
|
"2026-02-12T18:00:00.000Z";
|
|
|]
|
|
in
|
|
ignore (expect_cli_exit_zero "upsert task datetime" output);
|
|
if !step = 4 then Js.Promise.resolve pass
|
|
else
|
|
fail_promise
|
|
(Printf.sprintf "expected four invoke requests, got %d" !step)));
|
|
|
|
test_promise "upsert page reports missing update property name" (fun () ->
|
|
let step = ref 0 in
|
|
let server =
|
|
invoke_server (fun body ->
|
|
incr step;
|
|
match !step with
|
|
| 1
|
|
when Js.String.includes ~search:"thread-api/pull" body
|
|
&& not (Js.String.includes ~search:"missing-prop" body) ->
|
|
"[\"^ \
|
|
\",\"~:db/id\",42,\"~:block/uuid\",\"11111111-1111-1111-1111-111111111111\",\"~:block/name\",\"atomicpage\"]"
|
|
| 2
|
|
when Js.String.includes ~search:"thread-api/pull" body
|
|
&& Js.String.includes ~search:"missing-prop" body ->
|
|
"null"
|
|
| 3
|
|
when Js.String.includes ~search:"thread-api/q" body
|
|
&& Js.String.includes ~search:"missing-prop" body ->
|
|
"[]"
|
|
| _ ->
|
|
fail_test
|
|
(Printf.sprintf "unexpected request at step %d: %s" !step body);
|
|
"")
|
|
in
|
|
with_server server (fun base_url ->
|
|
let* output =
|
|
run_cli_p
|
|
~env:[| ("LOGSEQ_CLI_BASE_URL", base_url) |]
|
|
[|
|
|
"--graph";
|
|
"alpha";
|
|
"--output";
|
|
"json";
|
|
"upsert";
|
|
"page";
|
|
"--id";
|
|
"42";
|
|
"--update-properties";
|
|
"{:missing-prop \"value\"}";
|
|
|]
|
|
in
|
|
if output.code = 0 then
|
|
fail_promise
|
|
("expected missing update property to fail\n" ^ output.stdout)
|
|
else if
|
|
not (Js.String.includes ~search:"property-not-found" output.stdout)
|
|
then
|
|
fail_promise
|
|
("missing property code\nstdout:\n" ^ output.stdout
|
|
^ "\nstderr:\n" ^ output.stderr)
|
|
else if not (Js.String.includes ~search:"missing-prop" output.stdout)
|
|
then
|
|
fail_promise
|
|
("missing property name\nstdout:\n" ^ output.stdout
|
|
^ "\nstderr:\n" ^ output.stderr)
|
|
else if
|
|
not
|
|
(Js.String.includes ~search:"\"option\":\"--update-properties\""
|
|
output.stdout)
|
|
then
|
|
fail_promise
|
|
("missing property option context\nstdout:\n" ^ output.stdout
|
|
^ "\nstderr:\n" ^ output.stderr)
|
|
else if
|
|
not
|
|
(Js.String.includes ~search:"\"phase\":\"resolve-options\""
|
|
output.stdout)
|
|
then
|
|
fail_promise
|
|
("missing property phase context\nstdout:\n" ^ output.stdout
|
|
^ "\nstderr:\n" ^ output.stderr)
|
|
else Js.Promise.resolve pass));
|
|
|
|
test_promise "upsert tag applies schema property outliner ops" (fun () ->
|
|
let step = ref 0 in
|
|
let tag_uuid = "22222222-2222-4222-8222-222222222222" in
|
|
let add_ident = "user.property/status" in
|
|
let remove_ident = "user.property/owner" in
|
|
let server =
|
|
invoke_server (fun body ->
|
|
incr step;
|
|
match !step with
|
|
| 1
|
|
when Js.String.includes ~search:"thread-api/q" body
|
|
&& Js.String.includes ~search:"project" body ->
|
|
"[]"
|
|
| 2
|
|
when Js.String.includes ~search:"thread-api/apply-outliner-ops"
|
|
body ->
|
|
if not (Js.String.includes ~search:"create-page" body) then
|
|
fail_test ("missing tag create op: " ^ body);
|
|
"[]"
|
|
| 3
|
|
when Js.String.includes ~search:"thread-api/q" body
|
|
&& Js.String.includes ~search:"project" body ->
|
|
"[[\"^ \
|
|
\",\"~:db/id\",42,\"~:block/uuid\",\"~u22222222-2222-4222-8222-222222222222\",\"~:block/name\",\"project\",\"~:block/tags\",[[\"^ \
|
|
\",\"~:db/ident\",\"~:logseq.class/Tag\"]]]]"
|
|
| 4
|
|
when Js.String.includes ~search:"thread-api/q" body
|
|
&& Js.String.includes ~search:"status" body ->
|
|
"[[\"^ \
|
|
\",\"~:db/id\",77,\"~:db/ident\",\"~:user.property/status\",\"~:logseq.property/type\",\"default\"]]"
|
|
| 5
|
|
when Js.String.includes ~search:"thread-api/q" body
|
|
&& Js.String.includes ~search:"owner" body ->
|
|
"[[\"^ \
|
|
\",\"~:db/id\",78,\"~:db/ident\",\"~:user.property/owner\",\"~:logseq.property/type\",\"default\"]]"
|
|
| 6
|
|
when Js.String.includes ~search:"thread-api/apply-outliner-ops"
|
|
body ->
|
|
if not (Js.String.includes ~search:"class-add-property" body)
|
|
then fail_test ("missing class-add-property op: " ^ body);
|
|
if not (Js.String.includes ~search:"class-remove-property" body)
|
|
then fail_test ("missing class-remove-property op: " ^ body);
|
|
if not (Js.String.includes ~search:tag_uuid body) then
|
|
fail_test ("missing tag uuid: " ^ body);
|
|
if not (Js.String.includes ~search:add_ident body) then
|
|
fail_test ("missing add property ident: " ^ body);
|
|
if not (Js.String.includes ~search:remove_ident body) then
|
|
fail_test ("missing remove property ident: " ^ body);
|
|
"[]"
|
|
| _ ->
|
|
fail_test
|
|
(Printf.sprintf "unexpected request at step %d: %s" !step body);
|
|
"")
|
|
in
|
|
with_server server (fun base_url ->
|
|
let* output =
|
|
run_cli_p
|
|
~env:[| ("LOGSEQ_CLI_BASE_URL", base_url) |]
|
|
[|
|
|
"--graph";
|
|
"alpha";
|
|
"--output";
|
|
"json";
|
|
"upsert";
|
|
"tag";
|
|
"--name";
|
|
"Project";
|
|
"--add-properties";
|
|
"[\"status\"]";
|
|
"--remove-properties";
|
|
"[\"owner\"]";
|
|
|]
|
|
in
|
|
ignore (expect_cli_exit_zero "upsert tag schema properties" output);
|
|
if !step = 6 then Js.Promise.resolve pass
|
|
else
|
|
fail_promise
|
|
(Printf.sprintf "expected six invoke requests, got %d" !step)));
|
|
|
|
test_promise "upsert tag reports missing schema property name" (fun () ->
|
|
let step = ref 0 in
|
|
let server =
|
|
invoke_server (fun body ->
|
|
incr step;
|
|
match !step with
|
|
| 1
|
|
when Js.String.includes ~search:"thread-api/q" body
|
|
&& Js.String.includes ~search:"project" body ->
|
|
"[[\"^ \
|
|
\",\"~:db/id\",42,\"~:block/uuid\",\"~u22222222-2222-4222-8222-222222222222\",\"~:block/name\",\"project\",\"~:block/tags\",[[\"^ \
|
|
\",\"~:db/ident\",\"~:logseq.class/Tag\"]]]]"
|
|
| 2
|
|
when Js.String.includes ~search:"thread-api/q" body
|
|
&& Js.String.includes ~search:"missing-prop" body ->
|
|
"[]"
|
|
| 3
|
|
when Js.String.includes ~search:"thread-api/pull" body
|
|
&& Js.String.includes ~search:"missing-prop" body ->
|
|
"null"
|
|
| _ ->
|
|
fail_test
|
|
(Printf.sprintf "unexpected request at step %d: %s" !step body);
|
|
"")
|
|
in
|
|
with_server server (fun base_url ->
|
|
let* output =
|
|
run_cli_p
|
|
~env:[| ("LOGSEQ_CLI_BASE_URL", base_url) |]
|
|
[|
|
|
"--graph";
|
|
"alpha";
|
|
"--output";
|
|
"json";
|
|
"upsert";
|
|
"tag";
|
|
"--name";
|
|
"Project";
|
|
"--add-properties";
|
|
"[\"missing-prop\"]";
|
|
|]
|
|
in
|
|
if output.code = 0 then
|
|
fail_promise
|
|
("expected missing tag schema property to fail\n" ^ output.stdout)
|
|
else if
|
|
not (Js.String.includes ~search:"property-not-found" output.stdout)
|
|
then fail_promise ("missing property code\nstdout:\n" ^ output.stdout)
|
|
else if not (Js.String.includes ~search:"missing-prop" output.stdout)
|
|
then fail_promise ("missing property name\nstdout:\n" ^ output.stdout)
|
|
else if
|
|
not
|
|
(Js.String.includes ~search:"\"option\":\"--add-properties\""
|
|
output.stdout)
|
|
then
|
|
fail_promise
|
|
("missing add-properties context\nstdout:\n" ^ output.stdout)
|
|
else if !step = 3 then Js.Promise.resolve pass
|
|
else
|
|
fail_promise
|
|
(Printf.sprintf "expected three invoke requests, got %d" !step)));
|
|
|
|
test_promise "upsert tag reports missing schema property ident" (fun () ->
|
|
let step = ref 0 in
|
|
let server =
|
|
invoke_server (fun body ->
|
|
incr step;
|
|
match !step with
|
|
| 1
|
|
when Js.String.includes ~search:"thread-api/q" body
|
|
&& Js.String.includes ~search:"project" body ->
|
|
"[[\"^ \
|
|
\",\"~:db/id\",42,\"~:block/uuid\",\"~u22222222-2222-4222-8222-222222222222\",\"~:block/name\",\"project\",\"~:block/tags\",[[\"^ \
|
|
\",\"~:db/ident\",\"~:logseq.class/Tag\"]]]]"
|
|
| 2
|
|
when Js.String.includes ~search:"thread-api/pull" body
|
|
&& Js.String.includes ~search:"user.property/missing-prop"
|
|
body ->
|
|
"null"
|
|
| _ ->
|
|
fail_test
|
|
(Printf.sprintf "unexpected request at step %d: %s" !step body);
|
|
"")
|
|
in
|
|
with_server server (fun base_url ->
|
|
let* output =
|
|
run_cli_p
|
|
~env:[| ("LOGSEQ_CLI_BASE_URL", base_url) |]
|
|
[|
|
|
"--graph";
|
|
"alpha";
|
|
"--output";
|
|
"json";
|
|
"upsert";
|
|
"tag";
|
|
"--name";
|
|
"Project";
|
|
"--add-properties";
|
|
"[:user.property/missing-prop]";
|
|
|]
|
|
in
|
|
if output.code = 0 then
|
|
fail_promise
|
|
("expected missing tag schema property to fail\n" ^ output.stdout)
|
|
else if
|
|
not (Js.String.includes ~search:"property-not-found" output.stdout)
|
|
then fail_promise ("missing property code\nstdout:\n" ^ output.stdout)
|
|
else if not (Js.String.includes ~search:"missing-prop" output.stdout)
|
|
then fail_promise ("missing property ident\nstdout:\n" ^ output.stdout)
|
|
else if
|
|
not
|
|
(Js.String.includes ~search:"\"option\":\"--add-properties\""
|
|
output.stdout)
|
|
then
|
|
fail_promise
|
|
("missing add-properties context\nstdout:\n" ^ output.stdout)
|
|
else if !step = 2 then Js.Promise.resolve pass
|
|
else
|
|
fail_promise
|
|
(Printf.sprintf "expected two invoke requests, got %d" !step)));
|
|
|
|
test_promise
|
|
"upsert page resolves unqualified update property keywords by name"
|
|
(fun () ->
|
|
let step = ref 0 in
|
|
let property_ident = "user.property/node-prop" in
|
|
let server =
|
|
invoke_server (fun body ->
|
|
incr step;
|
|
match !step with
|
|
| 1
|
|
when Js.String.includes ~search:"thread-api/pull" body
|
|
&& not (Js.String.includes ~search:"node-prop" body) ->
|
|
"[\"^ \
|
|
\",\"~:db/id\",42,\"~:block/uuid\",\"11111111-1111-1111-1111-111111111111\",\"~:block/name\",\"nodehome\"]"
|
|
| 2
|
|
when Js.String.includes ~search:"thread-api/pull" body
|
|
&& Js.String.includes ~search:"node-prop" body ->
|
|
"null"
|
|
| 3
|
|
when Js.String.includes ~search:"thread-api/q" body
|
|
&& Js.String.includes ~search:"node-prop" body ->
|
|
"[[\"^ \
|
|
\",\"~:db/id\",77,\"~:db/ident\",\"~:user.property/node-prop\",\"~:logseq.property/type\",\"default\"]]"
|
|
| 4
|
|
when Js.String.includes ~search:"thread-api/apply-outliner-ops"
|
|
body ->
|
|
if not (Js.String.includes ~search:property_ident body) then
|
|
fail_test ("missing resolved property ident: " ^ body);
|
|
"[]"
|
|
| _ ->
|
|
fail_test
|
|
(Printf.sprintf "unexpected request at step %d: %s" !step body);
|
|
"")
|
|
in
|
|
with_server server (fun base_url ->
|
|
let* output =
|
|
run_cli_p
|
|
~env:[| ("LOGSEQ_CLI_BASE_URL", base_url) |]
|
|
[|
|
|
"--graph";
|
|
"alpha";
|
|
"--output";
|
|
"json";
|
|
"upsert";
|
|
"page";
|
|
"--id";
|
|
"42";
|
|
"--update-properties";
|
|
"{:node-prop \"v\"}";
|
|
|]
|
|
in
|
|
ignore (expect_cli_exit_zero "upsert page property by name" output);
|
|
if !step = 4 then Js.Promise.resolve pass
|
|
else
|
|
fail_promise
|
|
(Printf.sprintf "expected four invoke requests, got %d" !step)));
|
|
|
|
test_promise "upsert block create resolves inline property names per block"
|
|
(fun () ->
|
|
let step = ref 0 in
|
|
let property_ident = "user.property/inline-note" in
|
|
let server =
|
|
invoke_server (fun body ->
|
|
incr step;
|
|
match !step with
|
|
| 1
|
|
when Js.String.includes ~search:"thread-api/q" body
|
|
&& Js.String.includes ~search:"inline note" body ->
|
|
"[[\"^ \
|
|
\",\"~:db/id\",77,\"~:db/ident\",\"~:user.property/inline-note\",\"~:logseq.property/type\",\"default\"]]"
|
|
| 2
|
|
when Js.String.includes ~search:"thread-api/q" body
|
|
&& Js.String.includes ~search:"home" body ->
|
|
"[[\"^ \
|
|
\",\"~:db/id\",42,\"~:block/uuid\",\"11111111-1111-1111-1111-111111111111\",\"~:block/name\",\"home\"]]"
|
|
| 3
|
|
when Js.String.includes ~search:"thread-api/apply-outliner-ops"
|
|
body ->
|
|
if Js.String.includes ~search:property_ident body then
|
|
fail_test
|
|
("inline property should not be inserted with block: "
|
|
^ body);
|
|
"[]"
|
|
| 4 when Js.String.includes ~search:"thread-api/pull" body ->
|
|
"[\"^ \",\"~:db/id\",10]"
|
|
| 5
|
|
when Js.String.includes ~search:"thread-api/apply-outliner-ops"
|
|
body ->
|
|
if not (Js.String.includes ~search:property_ident body) then
|
|
fail_test ("missing inline property ident: " ^ body);
|
|
if not (Js.String.includes ~search:"First note" body) then
|
|
fail_test ("missing inline property value: " ^ body);
|
|
"[]"
|
|
| _ ->
|
|
fail_test
|
|
(Printf.sprintf "unexpected request at step %d: %s" !step body);
|
|
"")
|
|
in
|
|
with_server server (fun base_url ->
|
|
let* output =
|
|
run_cli_p
|
|
~env:[| ("LOGSEQ_CLI_BASE_URL", base_url) |]
|
|
[|
|
|
"--graph";
|
|
"alpha";
|
|
"--output";
|
|
"json";
|
|
"upsert";
|
|
"block";
|
|
"--target-page";
|
|
"Home";
|
|
"--blocks";
|
|
"[{:block/title \"First inline\" \"Inline Note\" \"First \
|
|
note\"}]";
|
|
|]
|
|
in
|
|
ignore (expect_cli_exit_zero "upsert block inline property" output);
|
|
if !step = 5 then Js.Promise.resolve pass
|
|
else
|
|
fail_promise
|
|
(Printf.sprintf "expected five invoke requests, got %d" !step)));
|
|
|
|
test_promise "upsert block create sends page refs from block title" (fun () ->
|
|
let step = ref 0 in
|
|
let ref_uuid = "22222222-2222-4222-8222-222222222222" in
|
|
let captured_apply_body = ref None in
|
|
let server =
|
|
invoke_server (fun body ->
|
|
incr step;
|
|
match !step with
|
|
| 1
|
|
when Js.String.includes ~search:"thread-api/q" body
|
|
&& Js.String.includes ~search:"home" body ->
|
|
"[[\"^ \
|
|
\",\"~:db/id\",42,\"~:block/uuid\",\"11111111-1111-1111-1111-111111111111\",\"~:block/name\",\"home\",\"~:block/title\",\"Home\"]]"
|
|
| 2
|
|
when Js.String.includes ~search:"thread-api/q" body
|
|
&& Js.String.includes ~search:"bar_page" body ->
|
|
"[[\"^ \",\"~:db/id\",88,\"~:block/uuid\",\"" ^ ref_uuid
|
|
^ "\",\"~:block/name\",\"bar_page\",\"~:block/title\",\"bar_page\"]]"
|
|
| 3
|
|
when Js.String.includes ~search:"thread-api/apply-outliner-ops"
|
|
body ->
|
|
captured_apply_body := Some body;
|
|
"[]"
|
|
| 4 when Js.String.includes ~search:"thread-api/pull" body ->
|
|
"[\"^ \",\"~:db/id\",10]"
|
|
| _ ->
|
|
fail_test
|
|
(Printf.sprintf "unexpected request at step %d: %s" !step body);
|
|
"")
|
|
in
|
|
with_server server (fun base_url ->
|
|
let* output =
|
|
run_cli_p
|
|
~env:[| ("LOGSEQ_CLI_BASE_URL", base_url) |]
|
|
[|
|
|
"--graph";
|
|
"alpha";
|
|
"--output";
|
|
"json";
|
|
"upsert";
|
|
"block";
|
|
"--target-page";
|
|
"Home";
|
|
"--content";
|
|
"Foo links to [[bar_page]]";
|
|
|]
|
|
in
|
|
ignore (expect_cli_exit_zero "upsert block title page refs" output);
|
|
let body =
|
|
match !captured_apply_body with
|
|
| Some body -> body
|
|
| None ->
|
|
fail_test "missing captured apply body";
|
|
failwith "missing captured apply body"
|
|
in
|
|
expect_named_contains "block refs attr" body "block/refs";
|
|
expect_named_contains "resolved page ref uuid" body ref_uuid;
|
|
expect_named_contains "resolved page ref title" body "bar_page";
|
|
if !step = 4 then Js.Promise.resolve pass
|
|
else
|
|
fail_promise
|
|
(Printf.sprintf "expected four invoke requests, got %d" !step)));
|
|
|
|
test_promise "upsert block update resolves tag names from tag list" (fun () ->
|
|
let step = ref 0 in
|
|
let captured_apply_body = ref None in
|
|
let server =
|
|
invoke_server (fun body ->
|
|
incr step;
|
|
match !step with
|
|
| 1
|
|
when Js.String.includes ~search:"thread-api/pull" body
|
|
&& Js.String.includes ~search:"233" body ->
|
|
"[\"^ \
|
|
\",\"~:db/id\",233,\"~:block/uuid\",\"~u00000000-0000-4000-8000-000000000233\"]"
|
|
| 2 when Js.String.includes ~search:"thread-api/cli-list-tags" body
|
|
->
|
|
"[[\"^ \",\"~:db/id\",200,\"~:block/title\",\"Tag A\"]]"
|
|
| 3
|
|
when Js.String.includes ~search:"thread-api/apply-outliner-ops"
|
|
body ->
|
|
captured_apply_body := Some body;
|
|
"[]"
|
|
| _ ->
|
|
fail_test
|
|
(Printf.sprintf "unexpected request at step %d: %s" !step body);
|
|
"")
|
|
in
|
|
with_server server (fun base_url ->
|
|
let* output =
|
|
run_cli_p
|
|
~env:[| ("LOGSEQ_CLI_BASE_URL", base_url) |]
|
|
[|
|
|
"--graph";
|
|
"alpha";
|
|
"--output";
|
|
"json";
|
|
"upsert";
|
|
"block";
|
|
"--id";
|
|
"233";
|
|
"--update-tags";
|
|
"[\"Tag A\"]";
|
|
|]
|
|
in
|
|
ignore (expect_cli_exit_zero "upsert block tag name" output);
|
|
let body =
|
|
match !captured_apply_body with
|
|
| Some body -> body
|
|
| None ->
|
|
fail_test "missing captured apply body";
|
|
failwith "missing captured apply body"
|
|
in
|
|
expect_named_contains "batch tag set op" body "batch-set-property";
|
|
expect_named_contains "resolved tag id" body "200";
|
|
if !step = 3 then Js.Promise.resolve pass
|
|
else
|
|
fail_promise
|
|
(Printf.sprintf "expected three invoke requests, got %d" !step)));
|
|
|
|
test_promise "upsert block create resolves inline block uuid property refs"
|
|
(fun () ->
|
|
let step = ref 0 in
|
|
let ref_uuid = "22222222-2222-4222-8222-222222222222" in
|
|
let property_ident = "logseq.property.comments/blocks" in
|
|
let server =
|
|
invoke_server (fun body ->
|
|
incr step;
|
|
match !step with
|
|
| 1
|
|
when Js.String.includes ~search:"thread-api/pull" body
|
|
&& Js.String.includes ~search:ref_uuid body ->
|
|
"[\"^ \
|
|
\",\"~:db/id\",88,\"~:block/uuid\",\"22222222-2222-4222-8222-222222222222\"]"
|
|
| 2
|
|
when Js.String.includes ~search:"thread-api/q" body
|
|
&& Js.String.includes ~search:"home" body ->
|
|
"[[\"^ \
|
|
\",\"~:db/id\",42,\"~:block/uuid\",\"11111111-1111-1111-1111-111111111111\",\"~:block/name\",\"home\"]]"
|
|
| 3
|
|
when Js.String.includes ~search:"thread-api/apply-outliner-ops"
|
|
body ->
|
|
if Js.String.includes ~search:property_ident body then
|
|
fail_test
|
|
("inline ref property should not be inserted with block: "
|
|
^ body);
|
|
"[]"
|
|
| 4 when Js.String.includes ~search:"thread-api/pull" body ->
|
|
"[\"^ \",\"~:db/id\",10]"
|
|
| 5
|
|
when Js.String.includes ~search:"thread-api/apply-outliner-ops"
|
|
body ->
|
|
if not (Js.String.includes ~search:property_ident body) then
|
|
fail_test ("missing comments blocks property ident: " ^ body);
|
|
if not (Js.String.includes ~search:"88" body) then
|
|
fail_test ("missing resolved block ref id: " ^ body);
|
|
if Js.String.includes ~search:ref_uuid body then
|
|
fail_test
|
|
("unresolved block uuid leaked into property op: " ^ body);
|
|
"[]"
|
|
| _ ->
|
|
fail_test
|
|
(Printf.sprintf "unexpected request at step %d: %s" !step body);
|
|
"")
|
|
in
|
|
with_server server (fun base_url ->
|
|
let* output =
|
|
run_cli_p
|
|
~env:[| ("LOGSEQ_CLI_BASE_URL", base_url) |]
|
|
[|
|
|
"--graph";
|
|
"alpha";
|
|
"--output";
|
|
"json";
|
|
"upsert";
|
|
"block";
|
|
"--target-page";
|
|
"Home";
|
|
"--blocks";
|
|
"[{:block/title \"Comment area\" \
|
|
:logseq.property.comments/blocks [[:block/uuid #uuid \
|
|
\"22222222-2222-4222-8222-222222222222\"]]}]";
|
|
|]
|
|
in
|
|
ignore
|
|
(expect_cli_exit_zero "upsert block inline ref property" output);
|
|
if !step = 5 then Js.Promise.resolve pass
|
|
else
|
|
fail_promise
|
|
(Printf.sprintf "expected five invoke requests, got %d" !step)));
|
|
|
|
test_promise
|
|
"upsert block create keeps inline tags out of property ops while resolving \
|
|
refs" (fun () ->
|
|
let step = ref 0 in
|
|
let ref_uuid = "22222222-2222-4222-8222-222222222222" in
|
|
let property_ident = "logseq.property.comments/blocks" in
|
|
let tag_ident = "logseq.class/Comments" in
|
|
let server =
|
|
invoke_server (fun body ->
|
|
incr step;
|
|
match !step with
|
|
| 1
|
|
when Js.String.includes ~search:"thread-api/pull" body
|
|
&& Js.String.includes ~search:ref_uuid body ->
|
|
"[\"^ \
|
|
\",\"~:db/id\",88,\"~:block/uuid\",\"22222222-2222-4222-8222-222222222222\"]"
|
|
| 2
|
|
when Js.String.includes ~search:"thread-api/q" body
|
|
&& Js.String.includes ~search:"home" body ->
|
|
"[[\"^ \
|
|
\",\"~:db/id\",42,\"~:block/uuid\",\"11111111-1111-1111-1111-111111111111\",\"~:block/name\",\"home\"]]"
|
|
| 3
|
|
when Js.String.includes ~search:"thread-api/apply-outliner-ops"
|
|
body ->
|
|
if Js.String.includes ~search:property_ident body then
|
|
fail_test
|
|
("inline ref property should not be inserted with block: "
|
|
^ body);
|
|
if not (Js.String.includes ~search:tag_ident body) then
|
|
fail_test ("missing inline tag in inserted block: " ^ body);
|
|
"[]"
|
|
| 4 when Js.String.includes ~search:"thread-api/pull" body ->
|
|
"[\"^ \",\"~:db/id\",10]"
|
|
| 5
|
|
when Js.String.includes ~search:"thread-api/apply-outliner-ops"
|
|
body ->
|
|
if not (Js.String.includes ~search:property_ident body) then
|
|
fail_test ("missing comments blocks property ident: " ^ body);
|
|
if Js.String.includes ~search:tag_ident body then
|
|
fail_test
|
|
("block/tags leaked into inline property op: " ^ body);
|
|
if not (Js.String.includes ~search:"88" body) then
|
|
fail_test ("missing resolved block ref id: " ^ body);
|
|
if Js.String.includes ~search:ref_uuid body then
|
|
fail_test
|
|
("unresolved block uuid leaked into property op: " ^ body);
|
|
"[]"
|
|
| _ ->
|
|
fail_test
|
|
(Printf.sprintf "unexpected request at step %d: %s" !step body);
|
|
"")
|
|
in
|
|
with_server server (fun base_url ->
|
|
let* output =
|
|
run_cli_p
|
|
~env:[| ("LOGSEQ_CLI_BASE_URL", base_url) |]
|
|
[|
|
|
"--graph";
|
|
"alpha";
|
|
"--output";
|
|
"json";
|
|
"upsert";
|
|
"block";
|
|
"--target-page";
|
|
"Home";
|
|
"--blocks";
|
|
"[{:block/title \"Comment area\" :block/tags \
|
|
[:logseq.class/Comments] :logseq.property.comments/blocks \
|
|
[[:block/uuid #uuid \
|
|
\"22222222-2222-4222-8222-222222222222\"]]}]";
|
|
|]
|
|
in
|
|
ignore (expect_cli_exit_zero "upsert block inline ref tags" output);
|
|
if !step = 5 then Js.Promise.resolve pass
|
|
else
|
|
fail_promise
|
|
(Printf.sprintf "expected five invoke requests, got %d" !step)));
|
|
|
|
test_promise "update block resolves string update property names by title"
|
|
(fun () ->
|
|
let step = ref 0 in
|
|
let property_ident = "user.property/reproducible-steps" in
|
|
let server =
|
|
invoke_server (fun body ->
|
|
incr step;
|
|
match !step with
|
|
| 1
|
|
when Js.String.includes ~search:"thread-api/pull" body
|
|
&& not (Js.String.includes ~search:"reproducible" body) ->
|
|
"[\"^ \
|
|
\",\"~:db/id\",42,\"~:block/uuid\",\"11111111-1111-1111-1111-111111111111\",\"~:block/title\",\"Alpha \
|
|
block\"]"
|
|
| 2
|
|
when Js.String.includes ~search:"thread-api/q" body
|
|
&& Js.String.includes ~search:"reproducible steps" body ->
|
|
"[[\"^ \
|
|
\",\"~:db/id\",77,\"~:db/ident\",\"~:user.property/reproducible-steps\",\"~:logseq.property/type\",\"default\"]]"
|
|
| 3
|
|
when Js.String.includes ~search:"thread-api/apply-outliner-ops"
|
|
body ->
|
|
if not (Js.String.includes ~search:property_ident body) then
|
|
fail_test ("missing update property ident: " ^ body);
|
|
if not (Js.String.includes ~search:"Step 1" body) then
|
|
fail_test ("missing update property value: " ^ body);
|
|
"[]"
|
|
| _ ->
|
|
fail_test
|
|
(Printf.sprintf "unexpected request at step %d: %s" !step body);
|
|
"")
|
|
in
|
|
with_server server (fun base_url ->
|
|
let* output =
|
|
run_cli_p
|
|
~env:[| ("LOGSEQ_CLI_BASE_URL", base_url) |]
|
|
[|
|
|
"--graph";
|
|
"alpha";
|
|
"--output";
|
|
"json";
|
|
"upsert";
|
|
"block";
|
|
"--id";
|
|
"42";
|
|
"--update-properties";
|
|
"{\"Reproducible steps\" [\"Step 1\"]}";
|
|
|]
|
|
in
|
|
ignore (expect_cli_exit_zero "update block property by title" output);
|
|
if !step = 3 then Js.Promise.resolve pass
|
|
else
|
|
fail_promise
|
|
(Printf.sprintf "expected three invoke requests, got %d" !step)));
|
|
|
|
test_promise "list node resolves unqualified property filters by name"
|
|
(fun () ->
|
|
let step = ref 0 in
|
|
let property_ident = "user.property/node-prop" in
|
|
let server =
|
|
invoke_server (fun body ->
|
|
incr step;
|
|
match !step with
|
|
| 1
|
|
when Js.String.includes ~search:"thread-api/q" body
|
|
&& Js.String.includes ~search:"node-prop" body ->
|
|
"[[\"^ \
|
|
\",\"~:db/id\",77,\"~:db/ident\",\"~:user.property/node-prop\",\"~:logseq.property/type\",\"default\"]]"
|
|
| 2 when Js.String.includes ~search:"thread-api/cli-list-nodes" body
|
|
->
|
|
if not (Js.String.includes ~search:property_ident body) then
|
|
fail_test ("missing list property ident: " ^ body);
|
|
"[]"
|
|
| _ ->
|
|
fail_test
|
|
(Printf.sprintf "unexpected request at step %d: %s" !step body);
|
|
"")
|
|
in
|
|
with_server server (fun base_url ->
|
|
let* output =
|
|
run_cli_p
|
|
~env:[| ("LOGSEQ_CLI_BASE_URL", base_url) |]
|
|
[|
|
|
"--graph";
|
|
"alpha";
|
|
"--output";
|
|
"json";
|
|
"list";
|
|
"node";
|
|
"--properties";
|
|
"node-prop";
|
|
|]
|
|
in
|
|
ignore (expect_cli_exit_zero "list node property by name" output);
|
|
if !step = 2 then Js.Promise.resolve pass
|
|
else
|
|
fail_promise
|
|
(Printf.sprintf "expected two invoke requests, got %d" !step)));
|
|
|
|
test_promise "upsert asset copies file and sends asset metadata" (fun () ->
|
|
let root = temp_dir "logseq-cli-upsert-asset-" in
|
|
let source_path = Node.Path.join [| root; "logo.png" |] in
|
|
let expected_checksum =
|
|
"d59386e0ae435e292fbe0ebcdb954b75ed5fb3922091277cb19f798fc5d50718"
|
|
in
|
|
write_file source_path "asset";
|
|
let step = ref 0 in
|
|
let server =
|
|
invoke_server (fun body ->
|
|
incr step;
|
|
match !step with
|
|
| 1
|
|
when Js.String.includes ~search:"thread-api/pull" body
|
|
&& Js.String.includes ~search:"logseq.class/Asset" body ->
|
|
"[\"^ \",\"~:db/id\",77]"
|
|
| 2
|
|
when Js.String.includes ~search:"thread-api/q" body
|
|
&& Js.String.includes ~search:"home" body ->
|
|
"[\"~#list\",[[\"^ \
|
|
\",\"~:db/id\",1,\"~:block/uuid\",\"~u11111111-1111-1111-1111-111111111111\",\"~:block/name\",\"home\"]]]"
|
|
| 3
|
|
when Js.String.includes ~search:"thread-api/apply-outliner-ops"
|
|
body ->
|
|
if not (Js.String.includes ~search:expected_checksum body) then
|
|
fail_test "missing asset checksum";
|
|
if
|
|
not
|
|
(Js.String.includes ~search:"logseq.property.asset/type"
|
|
body
|
|
&& Js.String.includes ~search:"png" body)
|
|
then fail_test "missing asset type metadata";
|
|
"[]"
|
|
| 4 when Js.String.includes ~search:"thread-api/pull" body ->
|
|
"[\"^ \",\"~:db/id\",10]"
|
|
| _ ->
|
|
fail_test
|
|
(Printf.sprintf "unexpected request at step %d: %s" !step body);
|
|
"")
|
|
in
|
|
with_server server (fun base_url ->
|
|
let* output =
|
|
run_cli_p
|
|
~env:[| ("LOGSEQ_CLI_BASE_URL", base_url) |]
|
|
[|
|
|
"--root-dir";
|
|
root;
|
|
"--graph";
|
|
"alpha";
|
|
"--output";
|
|
"json";
|
|
"upsert";
|
|
"asset";
|
|
"--path";
|
|
source_path;
|
|
"--target-page";
|
|
"Home";
|
|
"--content";
|
|
"Logo";
|
|
|]
|
|
in
|
|
let finish_with_error message =
|
|
remove_tree root;
|
|
fail_promise message
|
|
in
|
|
if output.code <> 0 then
|
|
finish_with_error
|
|
(Printf.sprintf
|
|
"expected CLI upsert asset to exit 0, got %d\n%s\n%s"
|
|
output.code output.stdout output.stderr)
|
|
else if !step <> 4 then
|
|
finish_with_error
|
|
(Printf.sprintf "expected four invoke requests, got %d" !step)
|
|
else if
|
|
not
|
|
(Js.String.includes
|
|
~search:"{\"status\":\"ok\",\"data\":{\"result\":[10]}}"
|
|
output.stdout)
|
|
then
|
|
finish_with_error ("expected success json output\n" ^ output.stdout)
|
|
else
|
|
let assets_dir =
|
|
Node.Path.join [| root; "graphs"; "alpha"; "assets" |]
|
|
in
|
|
let copied =
|
|
if Node.Fs.existsSync assets_dir then
|
|
Node.Fs.readdirSync assets_dir
|
|
else [||]
|
|
in
|
|
if Array.length copied <> 1 then
|
|
finish_with_error
|
|
(Printf.sprintf "expected one copied asset, got %d"
|
|
(Array.length copied))
|
|
else if
|
|
read_file (Node.Path.join [| assets_dir; copied.(0) |]) <> "asset"
|
|
then finish_with_error "expected copied asset content"
|
|
else (
|
|
remove_tree root;
|
|
Js.Promise.resolve pass)));
|
|
|
|
test_promise
|
|
"show human tree preserves multiline guides and linked references"
|
|
(fun () ->
|
|
let responses =
|
|
[|
|
|
( "thread-api/pull",
|
|
"[\"^ \",\"~:db/id\",16764,\"~:block/title\",\"type system to \
|
|
strengthen codebase examples\",\"~:block/name\",\"type system to \
|
|
strengthen codebase examples\"]" );
|
|
( "thread-api/q",
|
|
"[[[\"^ \",\"~:db/id\",16766,\"~:block/title\",\"use type system \
|
|
to ensure block validated, graph-name validated, \
|
|
etc..\",\"~:block/order\",\"a\",\"~:block/parent\",[\"^ \
|
|
\",\"~:db/id\",16764]]],[[\"^ \
|
|
\",\"~:db/id\",16767,\"~:block/title\",\"module type Block : \
|
|
sig\\ntype raw\\ntype validated\\n\\ntype _ t\\n\\nval of_edn : \
|
|
Edn.t -> raw t\\nval validate : raw t -> (validated t, string \
|
|
list) result\\n\\nval uuid : validated t -> Uuidm.t\\nval db_id : \
|
|
validated t -> Db_id.t\\nval title : validated t -> string \
|
|
option\\nval journal_day : validated t -> Journal_day.t \
|
|
option\\nend \
|
|
#Code\",\"~:block/order\",\"b\",\"~:block/parent\",[\"^ \
|
|
\",\"~:db/id\",16766]]],[[\"^ \
|
|
\",\"~:db/id\",16768,\"~:block/title\",\"split block into raw & \
|
|
validated 2 types, all fns need to operate on blocks only accept \
|
|
`validated \
|
|
block`\",\"~:block/order\",\"c\",\"~:block/parent\",[\"^ \
|
|
\",\"~:db/id\",16764]]],[[\"^ \
|
|
\",\"~:db/id\",16769,\"~:block/title\",\"-\",\"~:block/order\",\"d\",\"~:block/parent\",[\"^ \
|
|
\",\"~:db/id\",16764]]]]" );
|
|
("thread-api/get-block-refs", "[[\"^ \",\"~:db/id\",16763]]");
|
|
( "thread-api/pull",
|
|
"[\"^ \",\"~:db/id\",16763,\"~:block/title\",\"[[type system to \
|
|
strengthen codebase examples]]\",\"~:block/page\",[\"^ \
|
|
\",\"~:db/id\",16760,\"~:block/title\",\"May 31st, \
|
|
2026\",\"~:block/name\",\"may 31st, 2026\"]]" );
|
|
|]
|
|
in
|
|
let index = ref 0 in
|
|
let server =
|
|
invoke_server (fun body ->
|
|
if !index >= Array.length responses then
|
|
fail_test ("unexpected extra request: " ^ body);
|
|
let method_, transit = responses.(!index) in
|
|
incr index;
|
|
if not (Js.String.includes ~search:method_ body) then
|
|
fail_test (Printf.sprintf "expected %s, got %s" method_ body);
|
|
transit)
|
|
in
|
|
with_server server (fun base_url ->
|
|
let* output =
|
|
run_cli_p
|
|
~env:[| ("LOGSEQ_CLI_BASE_URL", base_url) |]
|
|
[| "--graph"; "alpha"; "show"; "--id"; "16764" |]
|
|
in
|
|
ignore (expect_cli_exit_zero "show human tree" output);
|
|
if !index <> Array.length responses then
|
|
fail_promise
|
|
(Printf.sprintf "handled %d requests, expected %d" !index
|
|
(Array.length responses))
|
|
else (
|
|
ignore
|
|
(expect_named_not_contains "multiline raw prefix" output.stdout
|
|
"\ntype raw\n");
|
|
ignore
|
|
(expect_named_contains "multiline tree context" output.stdout
|
|
("\n " ^ tree_pipe ^ " type raw\n"));
|
|
ignore
|
|
(expect_named_contains "blank multiline tree guide" output.stdout
|
|
("\n " ^ tree_pipe ^ "\n " ^ tree_pipe
|
|
^ " type _ t\n"));
|
|
ignore
|
|
(expect_named_contains "linked reference page context"
|
|
output.stdout
|
|
("Linked References (1)\n16760 May 31st, 2026\n16763 "
|
|
^ tree_corner
|
|
^ " [[type system to strengthen codebase examples]]"));
|
|
Js.Promise.resolve pass)));
|
|
|
|
test_promise "show human tree aligns guides when ids have different widths"
|
|
(fun () ->
|
|
let responses =
|
|
[|
|
|
( "thread-api/pull",
|
|
"[\"^ \",\"~:db/id\",1,\"~:block/title\",\"Root\"]" );
|
|
( "thread-api/q",
|
|
"[[[\"^ \
|
|
\",\"~:db/id\",9,\"~:block/title\",\"Short\",\"~:block/order\",\"a\",\"~:block/parent\",[\"^ \
|
|
\",\"~:db/id\",1]]],[[\"^ \
|
|
\",\"~:db/id\",12345,\"~:block/title\",\"Wide\",\"~:block/order\",\"b\",\"~:block/parent\",[\"^ \
|
|
\",\"~:db/id\",1]]],[[\"^ \
|
|
\",\"~:db/id\",4395,\"~:block/title\",\"Nested\",\"~:block/order\",\"a\",\"~:block/parent\",[\"^ \
|
|
\",\"~:db/id\",9]]]]" );
|
|
|]
|
|
in
|
|
let index = ref 0 in
|
|
let server =
|
|
invoke_server (fun body ->
|
|
if !index >= Array.length responses then
|
|
fail_test ("unexpected extra request: " ^ body);
|
|
let method_, transit = responses.(!index) in
|
|
incr index;
|
|
if not (Js.String.includes ~search:method_ body) then
|
|
fail_test (Printf.sprintf "expected %s, got %s" method_ body);
|
|
transit)
|
|
in
|
|
with_server server (fun base_url ->
|
|
let* output =
|
|
run_cli_p
|
|
~env:[| ("LOGSEQ_CLI_BASE_URL", base_url) |]
|
|
[|
|
|
"--graph";
|
|
"alpha";
|
|
"show";
|
|
"--id";
|
|
"1";
|
|
"--linked-references=false";
|
|
|]
|
|
in
|
|
ignore (expect_cli_exit_zero "show human tree aligned ids" output);
|
|
if !index <> Array.length responses then
|
|
fail_promise
|
|
(Printf.sprintf "handled %d requests, expected %d" !index
|
|
(Array.length responses))
|
|
else (
|
|
ignore
|
|
(expect_named_contains "aligned root" output.stdout "1 Root");
|
|
ignore
|
|
(expect_named_contains "aligned short id branch" output.stdout
|
|
("9 " ^ tree_middle ^ " Short"));
|
|
ignore
|
|
(expect_named_contains "aligned nested guide" output.stdout
|
|
("\n4395 " ^ tree_pipe ^ " " ^ tree_corner ^ " Nested\n"));
|
|
ignore
|
|
(expect_named_contains "aligned wide id branch" output.stdout
|
|
("12345 " ^ tree_corner ^ " Wide"));
|
|
Js.Promise.resolve pass)));
|
|
|
|
test_promise "show human output writes unicode text from worker strings"
|
|
(fun () ->
|
|
let responses =
|
|
[|
|
|
( "thread-api/pull",
|
|
"[\"^ \",\"~:db/id\",1,\"~:block/title\",\"\\u4e2d\\u6587\"]" );
|
|
("thread-api/q", "[]");
|
|
|]
|
|
in
|
|
let index = ref 0 in
|
|
let server =
|
|
invoke_server (fun body ->
|
|
if !index >= Array.length responses then
|
|
fail_test ("unexpected extra request: " ^ body);
|
|
let method_, transit = responses.(!index) in
|
|
incr index;
|
|
if not (Js.String.includes ~search:method_ body) then
|
|
fail_test (Printf.sprintf "expected %s, got %s" method_ body);
|
|
transit)
|
|
in
|
|
with_server server (fun base_url ->
|
|
let* output =
|
|
run_cli_p
|
|
~env:[| ("LOGSEQ_CLI_BASE_URL", base_url) |]
|
|
[|
|
|
"--graph";
|
|
"alpha";
|
|
"show";
|
|
"--id";
|
|
"1";
|
|
"--linked-references=false";
|
|
|]
|
|
in
|
|
ignore (expect_cli_exit_zero "show human unicode output" output);
|
|
if !index <> Array.length responses then
|
|
fail_promise
|
|
(Printf.sprintf "handled %d requests, expected %d" !index
|
|
(Array.length responses))
|
|
else (
|
|
ignore
|
|
(expect_named_contains "unicode stdout" output.stdout
|
|
(unicode_text [| 0x4e2d; 0x6587 |]));
|
|
ignore
|
|
(expect_named_not_contains "mojibake stdout" output.stdout
|
|
"\195\164\194\184\194\173");
|
|
Js.Promise.resolve pass)));
|
|
|
|
test_promise "show human properties renders task status and property values"
|
|
(fun () ->
|
|
let saw_status_selector = ref false in
|
|
let saw_wildcard_selector = ref false in
|
|
let index = ref 0 in
|
|
let static_transits =
|
|
[|
|
|
"[\"^ \
|
|
\",\"~:db/id\",1,\"~:block/title\",\"Home\",\"~:block/name\",\"home\"]";
|
|
"";
|
|
"[\"^ \
|
|
\",\"~:db/id\",7,\"~:db/ident\",\"~:user.property/owner\",\"~:block/title\",\"Owner\",\"~:logseq.property/type\",\"~:default\"]";
|
|
"[\"^ \
|
|
\",\"~:db/id\",8,\"~:db/ident\",\"~:user.property/agent-skills\",\"~:block/title\",\"agent-skills\",\"~:logseq.property/type\",\"~:default\"]";
|
|
"[\"^ \
|
|
\",\"~:db/id\",16,\"~:db/ident\",\"~:user.property/reproducible-steps\",\"~:block/title\",\"Reproducible \
|
|
steps\",\"~:logseq.property/type\",\"~:default\"]";
|
|
"[\"^ \
|
|
\",\"~:db/id\",17,\"~:db/ident\",\"~:user.property/empty-list\",\"~:block/title\",\"empty-list\",\"~:logseq.property/type\",\"~:default\"]";
|
|
"[\"^ \
|
|
\",\"~:db/id\",15,\"~:db/ident\",\"~:logseq.property/assignee\",\"~:block/title\",\"Assignee\",\"~:logseq.property/type\",\"~:default\"]";
|
|
"[\"^ \
|
|
\",\"~:db/id\",19,\"~:db/ident\",\"~:logseq.property/reviewer\",\"~:block/title\",\"Reviewer\",\"~:logseq.property/type\",\"~:default\"]";
|
|
"[\"^ \",\"~:db/id\",9,\"~:block/title\",\"Alice\"]";
|
|
"[\"^ \",\"~:db/id\",10,\"~:block/title\",\"logseq-answer-machine\"]";
|
|
"[\"^ \",\"~:db/id\",11,\"~:logseq.property/value\",\"Type `[[assig` \
|
|
and only see [[Assignee(legacy)]]\"]";
|
|
"[\"^ \",\"~:db/id\",12,\"~:logseq.property/value\",\"Expect to see \
|
|
two entries\"]";
|
|
"[\"^ \",\"~:db/id\",13,\"~:logseq.property/value\",\"Expected to \
|
|
see the built-in property\"]";
|
|
"[\"^ \",\"~:db/id\",14,\"~:block/title\",\"czys-Mac-Studio.local\"]";
|
|
"[\"^ \",\"~:db/id\",18,\"~:block/title\",\"general-reviewer.local\"]";
|
|
|]
|
|
in
|
|
let expected_methods =
|
|
[|
|
|
"thread-api/pull";
|
|
"thread-api/q";
|
|
"thread-api/pull";
|
|
"thread-api/pull";
|
|
"thread-api/pull";
|
|
"thread-api/pull";
|
|
"thread-api/pull";
|
|
"thread-api/pull";
|
|
"thread-api/pull";
|
|
"thread-api/pull";
|
|
"thread-api/pull";
|
|
"thread-api/pull";
|
|
"thread-api/pull";
|
|
"thread-api/pull";
|
|
"thread-api/pull";
|
|
|]
|
|
in
|
|
let q_transit body =
|
|
saw_status_selector :=
|
|
Js.String.includes ~search:"~:logseq.property/status" body;
|
|
saw_wildcard_selector := Js.String.includes ~search:"~$*" body;
|
|
let child_fields =
|
|
ref
|
|
(Vec.of_array
|
|
[|
|
|
"\"~:db/id\",2";
|
|
"\"~:block/title\",\"Ship CLI\"";
|
|
"\"~:block/order\",\"a\"";
|
|
"\"~:block/parent\",[\"^ \",\"~:db/id\",1]";
|
|
"\"~:block/tags\",[[\"^ \
|
|
\",\"~:db/ident\",\"~:logseq.class/Task\",\"~:block/title\",\"Task\"]]";
|
|
|])
|
|
in
|
|
if !saw_status_selector then
|
|
child_fields :=
|
|
Vec.append !child_fields
|
|
(Vec.of_array
|
|
[|
|
|
"\"~:logseq.property/status\",[\"^ \
|
|
\",\"~:db/ident\",\"~:logseq.property/status.todo\",\"~:block/title\",\"Todo\"]";
|
|
|]);
|
|
if !saw_wildcard_selector then
|
|
child_fields :=
|
|
Vec.append !child_fields
|
|
(Vec.of_array
|
|
[|
|
|
"\"~:user.property/owner\",[\"~:db/id\",9]";
|
|
"\"~:user.property/agent-skills\",[[ \"~:db/id\",10 ]]";
|
|
"\"~:user.property/reproducible-steps\",[\"~:db/id\",11,[\"~:db/id\",12],[\"~:db/id\",13]]";
|
|
"\"~:user.property/empty-list\",[]";
|
|
"\"~:logseq.property/assignee\",[[\"~:db/id\",14]]";
|
|
"\"~:logseq.property/reviewer\",[[\"~:db/id\",18]]";
|
|
|]);
|
|
"[[[\"^ \"," ^ Vec.string_concat "," !child_fields ^ "]]]"
|
|
in
|
|
let server =
|
|
invoke_server (fun body ->
|
|
if !index >= Array.length expected_methods then
|
|
fail_test ("unexpected extra request: " ^ body);
|
|
let expected_method = expected_methods.(!index) in
|
|
if not (Js.String.includes ~search:expected_method body) then
|
|
fail_test
|
|
(Printf.sprintf "expected %s, got %s" expected_method body);
|
|
let current = !index in
|
|
incr index;
|
|
if current = 1 then q_transit body else static_transits.(current))
|
|
in
|
|
with_server server (fun base_url ->
|
|
let* output =
|
|
run_cli_p
|
|
~env:[| ("LOGSEQ_CLI_BASE_URL", base_url) |]
|
|
[|
|
|
"--graph";
|
|
"alpha";
|
|
"show";
|
|
"--page";
|
|
"home";
|
|
"--linked-references=false";
|
|
|]
|
|
in
|
|
ignore (expect_cli_exit_zero "show human properties" output);
|
|
if not !saw_status_selector then
|
|
fail_promise "show tree selector did not request task status"
|
|
else if not !saw_wildcard_selector then
|
|
fail_promise "show tree selector did not request custom properties"
|
|
else if !index <> Array.length expected_methods then
|
|
fail_promise
|
|
(Printf.sprintf "handled %d requests, expected %d" !index
|
|
(Array.length expected_methods))
|
|
else (
|
|
ignore
|
|
(expect_named_contains "task status" output.stdout
|
|
("2 " ^ tree_corner ^ " Todo Ship CLI #Task"));
|
|
ignore
|
|
(expect_named_contains "owner property" output.stdout
|
|
" Owner: Alice");
|
|
ignore
|
|
(expect_named_contains "single cardinality-many property"
|
|
output.stdout "agent-skills: logseq-answer-machine");
|
|
ignore
|
|
(expect_named_contains "multi-value property bullets"
|
|
output.stdout
|
|
"Reproducible steps:\n\
|
|
\ - Type `[[assig` and only see [[Assignee(legacy)]]\n\
|
|
\ - Expect to see two entries\n\
|
|
\ - Expected to see the built-in property");
|
|
ignore
|
|
(expect_named_not_contains "empty list property" output.stdout
|
|
"empty-list");
|
|
ignore
|
|
(expect_named_contains "assignee property" output.stdout
|
|
"Assignee: czys-Mac-Studio.local");
|
|
ignore
|
|
(expect_named_contains "reviewer property" output.stdout
|
|
"Reviewer: general-reviewer.local");
|
|
ignore
|
|
(expect_named_not_contains "db id leak" output.stdout ":db/id");
|
|
Js.Promise.resolve pass)));
|
|
|
|
let sync_graph_id = "11111111-1111-1111-1111-111111111111" in
|
|
let sync_remote_graph_transit =
|
|
"[[\"^ \",\"~:graph-name\",\"alpha\",\"~:graph-id\",\"~u" ^ sync_graph_id
|
|
^ "\",\"~:graph-e2ee?\",false]]"
|
|
in
|
|
let sync_invoke_response download_requests body =
|
|
if Js.String.includes ~search:"thread-api/set-db-sync-config" body then
|
|
Some "true"
|
|
else if
|
|
Js.String.includes ~search:"thread-api/db-sync-list-remote-graphs" body
|
|
then Some sync_remote_graph_transit
|
|
else if Js.String.includes ~search:"thread-api/q" body then Some "0"
|
|
else if
|
|
Js.String.includes ~search:"thread-api/db-sync-download-graph-by-id" body
|
|
then (
|
|
incr download_requests;
|
|
Some "true")
|
|
else (
|
|
fail_test ("unexpected invoke body: " ^ body);
|
|
None)
|
|
in
|
|
test_promise "invoke response decodes JSON escaped resultTransit" (fun () ->
|
|
let server =
|
|
create_server (fun[@u] req res ->
|
|
let body = ref "" in
|
|
req_set_encoding req "utf8";
|
|
req_on_data req "data" (fun[@u] chunk -> body := !body ^ chunk);
|
|
req_on_end req "end" (fun[@u] () ->
|
|
if req_method req <> "POST" || req_url req <> "/v1/invoke" then
|
|
write_json res 404 (error_response "not found")
|
|
else if
|
|
not
|
|
(Js.String.includes ~search:"thread-api/cli-list-pages"
|
|
!body)
|
|
then write_json res 400 (error_response "unexpected request")
|
|
else write_json res 200 "{\"resultTransit\":\"\\u005b\\u005d\"}"))
|
|
in
|
|
with_server server (fun base_url ->
|
|
let* output =
|
|
run_cli_p
|
|
~env:[| ("LOGSEQ_CLI_BASE_URL", base_url) |]
|
|
[| "--graph"; "alpha"; "--output"; "json"; "list"; "page" |]
|
|
in
|
|
ignore (expect_cli_exit_zero "json escaped resultTransit" output);
|
|
if
|
|
Js.String.includes
|
|
~search:"{\"status\":\"ok\",\"data\":{\"items\":[]}}"
|
|
output.stdout
|
|
then Js.Promise.resolve pass
|
|
else fail_promise ("expected empty list result\n" ^ output.stdout)));
|
|
|
|
let run_sync_progress_case name root_prefix handle_events
|
|
?(delay_download_response = false) ?(delay_events_response = false)
|
|
assert_progress =
|
|
test_promise name (fun () ->
|
|
let root = temp_dir root_prefix in
|
|
let events_requests = ref 0 in
|
|
let download_requests = ref 0 in
|
|
let server =
|
|
create_server (fun[@u] req res ->
|
|
let body = ref "" in
|
|
req_set_encoding req "utf8";
|
|
req_on_data req "data" (fun[@u] chunk -> body := !body ^ chunk);
|
|
req_on_end req "end" (fun[@u] () ->
|
|
if req_method req = "GET" && req_url req = "/v1/events" then (
|
|
incr events_requests;
|
|
if delay_events_response then
|
|
set_timeout (fun () -> handle_events res) 100
|
|
else handle_events res)
|
|
else if req_method req = "POST" && req_url req = "/v1/invoke"
|
|
then
|
|
try
|
|
match sync_invoke_response download_requests !body with
|
|
| Some result_transit
|
|
when delay_download_response
|
|
&& Js.String.includes
|
|
~search:
|
|
"thread-api/db-sync-download-graph-by-id"
|
|
!body ->
|
|
set_timeout
|
|
(fun () ->
|
|
write_json res 200 (json_response result_transit))
|
|
100
|
|
| Some result_transit ->
|
|
write_json res 200 (json_response result_transit)
|
|
| None -> ()
|
|
with exn ->
|
|
write_json res 400
|
|
(error_response (Printexc.to_string exn))
|
|
else write_json res 404 (error_response "not found")))
|
|
in
|
|
with_server server (fun base_url ->
|
|
let* output =
|
|
run_cli_p
|
|
~env:[| ("LOGSEQ_CLI_BASE_URL", base_url) |]
|
|
[|
|
|
"--root-dir";
|
|
root;
|
|
"--graph";
|
|
"alpha";
|
|
"--output";
|
|
"json";
|
|
"sync";
|
|
"download";
|
|
"--progress";
|
|
|]
|
|
in
|
|
remove_tree root;
|
|
ignore (expect_cli_exit_zero name output);
|
|
if !events_requests <> 1 then
|
|
fail_promise
|
|
(Printf.sprintf "expected one events request, got %d"
|
|
!events_requests)
|
|
else if !download_requests <> 1 then
|
|
fail_promise
|
|
(Printf.sprintf "expected one download request, got %d"
|
|
!download_requests)
|
|
else if
|
|
not
|
|
(Js.String.includes
|
|
~search:"{\"status\":\"ok\",\"data\":{\"result\":true}}"
|
|
output.stdout)
|
|
then fail_promise ("expected success json output\n" ^ output.stdout)
|
|
else (
|
|
assert_progress output.stdout;
|
|
Js.Promise.resolve pass)))
|
|
in
|
|
run_sync_progress_case
|
|
"sync download prints progress from closed event stream"
|
|
"logseq-cli-sync-progress-" ~delay_download_response:true
|
|
(fun res ->
|
|
res_write_head res 200
|
|
(Js.Dict.fromArray [| ("Content-Type", "text/event-stream") |]);
|
|
res_end res
|
|
("data: {\"payload\":\"[\\\"~:rtc-log\\\",[\\\"^ \
|
|
\\\",\\\"~:type\\\",\\\"~:rtc.log/download\\\",\\\"~:graph-uuid\\\",\\\"~u"
|
|
^ sync_graph_id
|
|
^ "\\\",\\\"~:message\\\",\\\"downloaded 1 block\\\"]]\"}\n\n"))
|
|
(fun stdout ->
|
|
ignore
|
|
(expect_named_contains "progress output" stdout "downloaded 1 block"));
|
|
|
|
run_sync_progress_case "sync download decodes JSON escaped progress payload"
|
|
"logseq-cli-sync-progress-escaped-" ~delay_events_response:true
|
|
(fun res ->
|
|
let payload =
|
|
"[\"~:rtc-log\",[\"^ \
|
|
\",\"~:type\",\"~:rtc.log/download\",\"~:graph-uuid\",\"~u"
|
|
^ sync_graph_id ^ "\",\"~:message\",\"downloaded escaped payload\"]]"
|
|
in
|
|
let payload_json = Js.Json.stringify (Js.Json.string payload) in
|
|
let escaped_payload_json =
|
|
"\"\\u005b" ^ String.sub payload_json 2 (String.length payload_json - 2)
|
|
in
|
|
res_write_head res 200
|
|
(Js.Dict.fromArray [| ("Content-Type", "text/event-stream") |]);
|
|
res_end res ("data: {\"payload\":" ^ escaped_payload_json ^ "}\n\n"))
|
|
(fun stdout ->
|
|
ignore
|
|
(expect_named_contains "escaped progress output" stdout
|
|
"downloaded escaped payload"));
|
|
|
|
run_sync_progress_case "sync download finishes while event stream stays open"
|
|
"logseq-cli-sync-progress-stream-" ~delay_download_response:true
|
|
(fun res ->
|
|
res_write_head res 200
|
|
(Js.Dict.fromArray [| ("Content-Type", "text/event-stream") |]);
|
|
let large_sync_payload =
|
|
"[\"~:sync-db-changes\",[\"^ \",\"~:pages\"," ^ "["
|
|
^ Vec.string_concat ","
|
|
(Vec.init 12000 (fun i ->
|
|
Printf.sprintf
|
|
"[\"^ \",\"~:db/id\",%d,\"~:block/title\",\"block %d\"]" i i))
|
|
^ "]]]"
|
|
in
|
|
res_write res
|
|
("data: " ^ "{\"type\":\"sync-db-changes\",\"payload\":"
|
|
^ Js.Json.stringify (Js.Json.string large_sync_payload)
|
|
^ "}\n\n");
|
|
res_write res
|
|
("data: {\"payload\":\"[\\\"~:rtc-log\\\",[\\\"^ \
|
|
\\\",\\\"~:type\\\",\\\"~:rtc.log/download\\\",\\\"~:graph-uuid\\\",\\\"~u"
|
|
^ sync_graph_id
|
|
^ "\\\",\\\"~:message\\\",\\\"downloaded streaming block\\\"]]\"}\n\n"))
|
|
(fun stdout ->
|
|
ignore
|
|
(expect_named_contains "streaming progress output" stdout
|
|
"downloaded streaming block"));
|
|
|
|
run_sync_progress_case "sync download survives event stream disconnect"
|
|
"logseq-cli-sync-progress-disconnect-" ~delay_download_response:true
|
|
(fun res ->
|
|
res_write_head res 200
|
|
(Js.Dict.fromArray [| ("Content-Type", "text/event-stream") |]);
|
|
res_write res
|
|
("data: {\"payload\":\"[\\\"~:rtc-log\\\",[\\\"^ \
|
|
\\\",\\\"~:type\\\",\\\"~:rtc.log/download\\\",\\\"~:graph-uuid\\\",\\\"~u"
|
|
^ sync_graph_id
|
|
^ "\\\",\\\"~:message\\\",\\\"download started\\\"]]\"}\n\n");
|
|
set_timeout (fun () -> res_destroy res) 20)
|
|
(fun _stdout -> ());
|
|
|
|
test_promise
|
|
"sync upload without password does not require stored e2ee password"
|
|
(fun () ->
|
|
let root = temp_dir "logseq-cli-sync-upload-no-password-" in
|
|
let invoke_calls = ref Vec.empty in
|
|
let server =
|
|
invoke_server (fun body ->
|
|
invoke_calls := Vec.push_front !invoke_calls body;
|
|
if Js.String.includes ~search:"thread-api/get-e2ee-password" body
|
|
then
|
|
fail_test ("sync upload unexpectedly requested password: " ^ body);
|
|
if Js.String.includes ~search:"thread-api/db-sync-upload-graph" body
|
|
then "{\"~:graph-id\":\"graph-1\"}"
|
|
else "null")
|
|
in
|
|
with_server server (fun base_url ->
|
|
let env = [| ("LOGSEQ_CLI_BASE_URL", base_url) |] in
|
|
let* result =
|
|
run_cli_p ~env
|
|
[|
|
|
"--root-dir";
|
|
root;
|
|
"--graph";
|
|
"alpha";
|
|
"--output";
|
|
"json";
|
|
"sync";
|
|
"upload";
|
|
|]
|
|
in
|
|
remove_tree root;
|
|
ignore (expect_cli_exit_zero "sync upload no password" result);
|
|
if
|
|
Vec.exists
|
|
(Js.String.includes ~search:"thread-api/db-sync-upload-graph")
|
|
!invoke_calls
|
|
then Js.Promise.resolve pass
|
|
else fail_promise ("missing db-sync upload call\n" ^ result.stdout)));
|
|
|
|
test_promise
|
|
"sync upload with password initializes e2ee keys before verifying password"
|
|
(fun () ->
|
|
let root = temp_dir "logseq-cli-sync-upload-password-keys-" in
|
|
let config_path = Node.Path.join [| root; "cli.edn" |] in
|
|
let invoke_calls = ref Vec.empty in
|
|
let method_names = ref Vec.empty in
|
|
let record_method body method_name =
|
|
if Js.String.includes ~search:method_name body then
|
|
method_names := Vec.push_back !method_names method_name
|
|
in
|
|
let server =
|
|
invoke_server (fun body ->
|
|
invoke_calls := Vec.push_front !invoke_calls body;
|
|
record_method body "thread-api/db-sync-ensure-user-rsa-keys";
|
|
record_method body "thread-api/verify-and-save-e2ee-password";
|
|
record_method body "thread-api/db-sync-upload-graph";
|
|
if Js.String.includes ~search:"thread-api/db-sync-upload-graph" body
|
|
then "{\"~:graph-id\":\"graph-1\"}"
|
|
else "null")
|
|
in
|
|
with_server server (fun base_url ->
|
|
write_file config_path
|
|
"{:refresh-token \"refresh-token\"\n :id-token \"id-token\"}\n";
|
|
let env = [| ("LOGSEQ_CLI_BASE_URL", base_url) |] in
|
|
let* result =
|
|
run_cli_p ~env
|
|
[|
|
|
"--root-dir";
|
|
root;
|
|
"--graph";
|
|
"alpha";
|
|
"--output";
|
|
"json";
|
|
"sync";
|
|
"upload";
|
|
"--e2ee-password";
|
|
"pw";
|
|
|]
|
|
in
|
|
remove_tree root;
|
|
ignore (expect_cli_exit_zero "sync upload password" result);
|
|
if
|
|
Vec.equal String.equal !method_names
|
|
(Vec.of_array
|
|
[|
|
|
"thread-api/db-sync-ensure-user-rsa-keys";
|
|
"thread-api/verify-and-save-e2ee-password";
|
|
"thread-api/db-sync-upload-graph";
|
|
|])
|
|
then
|
|
if
|
|
Vec.exists
|
|
(fun body ->
|
|
Js.String.includes
|
|
~search:"thread-api/db-sync-ensure-user-rsa-keys" body
|
|
&& Js.String.includes ~search:"ensure-server" body
|
|
&& Js.String.includes ~search:"pw" body)
|
|
!invoke_calls
|
|
then Js.Promise.resolve pass
|
|
else fail_promise "missing ensure-keys password options"
|
|
else
|
|
fail_promise
|
|
("unexpected sync upload method order: "
|
|
^ Vec.string_concat " -> " !method_names)));
|
|
|
|
test_promise
|
|
"sync ensure-keys with password verifies password before ensuring local \
|
|
keys" (fun () ->
|
|
let root = temp_dir "logseq-cli-sync-ensure-keys-password-" in
|
|
let config_path = Node.Path.join [| root; "cli.edn" |] in
|
|
let method_names = ref Vec.empty in
|
|
let record_method body method_name =
|
|
if Js.String.includes ~search:method_name body then
|
|
method_names := Vec.push_back !method_names method_name
|
|
in
|
|
let server =
|
|
invoke_server (fun body ->
|
|
record_method body "thread-api/verify-and-save-e2ee-password";
|
|
record_method body "thread-api/db-sync-ensure-user-rsa-keys";
|
|
"null")
|
|
in
|
|
with_server server (fun base_url ->
|
|
write_file config_path
|
|
"{:refresh-token \"refresh-token\"\n :id-token \"id-token\"}\n";
|
|
let env = [| ("LOGSEQ_CLI_BASE_URL", base_url) |] in
|
|
let* result =
|
|
run_cli_p ~env
|
|
[|
|
|
"--root-dir";
|
|
root;
|
|
"--output";
|
|
"json";
|
|
"sync";
|
|
"ensure-keys";
|
|
"--e2ee-password";
|
|
"pw";
|
|
|]
|
|
in
|
|
remove_tree root;
|
|
ignore (expect_cli_exit_zero "sync ensure-keys password" result);
|
|
if
|
|
Vec.equal String.equal !method_names
|
|
(Vec.of_array
|
|
[|
|
|
"thread-api/verify-and-save-e2ee-password";
|
|
"thread-api/db-sync-ensure-user-rsa-keys";
|
|
|])
|
|
then Js.Promise.resolve pass
|
|
else
|
|
fail_promise
|
|
("unexpected sync ensure-keys method order: "
|
|
^ Vec.string_concat " -> " !method_names)));
|
|
|
|
test_promise "sync ensure-keys resolves auth file before uploading keys"
|
|
(fun () ->
|
|
let root = temp_dir "logseq-cli-sync-ensure-keys-auth-" in
|
|
let auth_path = Node.Path.join [| root; "auth.json" |] in
|
|
let config_path = Node.Path.join [| root; "cli.edn" |] in
|
|
let invoke_calls = ref Vec.empty in
|
|
let server =
|
|
invoke_server (fun body ->
|
|
invoke_calls := Vec.push_front !invoke_calls body;
|
|
"null")
|
|
in
|
|
with_server server (fun base_url ->
|
|
write_file auth_path
|
|
"{\"provider\":\"cognito\",\"id-token\":\"id-token\",\"access-token\":\"access-token\",\"refresh-token\":\"refresh-token\",\"expires-at\":4102444800000}\n";
|
|
write_file config_path (Printf.sprintf "{:auth-path %S}\n" auth_path);
|
|
let env = [| ("LOGSEQ_CLI_BASE_URL", base_url) |] in
|
|
let* result =
|
|
run_cli_p ~env
|
|
[|
|
|
"--root-dir";
|
|
root;
|
|
"--output";
|
|
"json";
|
|
"sync";
|
|
"ensure-keys";
|
|
"--e2ee-password";
|
|
"pw";
|
|
"--upload-keys";
|
|
|]
|
|
in
|
|
remove_tree root;
|
|
ignore (expect_cli_exit_zero "sync ensure-keys auth" result);
|
|
if
|
|
Vec.exists
|
|
(fun body ->
|
|
Js.String.includes ~search:"thread-api/sync-app-state" body
|
|
&& Js.String.includes ~search:"auth/id-token" body
|
|
&& Js.String.includes ~search:"id-token" body)
|
|
!invoke_calls
|
|
then Js.Promise.resolve pass
|
|
else fail_promise "missing sync-app-state auth payload"));
|
|
|
|
test_promise "agent bridge keeps codex alive and forwards worker env"
|
|
(fun () ->
|
|
let tmp = temp_dir "logseq-cli-agent-" in
|
|
let config_path = Node.Path.join [| tmp; "cli.edn" |] in
|
|
let codex_bin = Node.Path.join [| tmp; "fake-codex" |] in
|
|
let marker_path = Node.Path.join [| tmp; "codex-marker.txt" |] in
|
|
let worker_script = "/tmp/logseq-cli-test-db-worker-node.js" in
|
|
let agent_name = "agent-a" ^ control_char ^ "lock" in
|
|
let fake_codex_js =
|
|
Node.Path.join [| current_dirname; "fake_codex.js" |]
|
|
in
|
|
write_file codex_bin
|
|
("#!/bin/sh\nexec " ^ Node.Process.argv.(0) ^ " " ^ fake_codex_js
|
|
^ " \"$@\"\n");
|
|
chmod_sync codex_bin 0o755;
|
|
write_file config_path
|
|
(edn_map
|
|
[
|
|
(edn_keyword "agent-name", edn_string agent_name);
|
|
(edn_keyword "codex-bin", edn_string codex_bin);
|
|
]);
|
|
let registry_page =
|
|
"{\"~:db/id\":100,\"~:block/uuid\":\"~uaaaaaaaa-aaaa-4aaa-8aaa-aaaaaaaaaaaa\",\"~:block/name\":\"agentbridge\",\"~:block/title\":\"AgentBridge\"}"
|
|
in
|
|
let agent_page =
|
|
"{\"~:db/id\":101,\"~:block/uuid\":\"~ubbbbbbbb-bbbb-4bbb-8bbb-bbbbbbbbbbbb\",\"~:block/name\":\"agent-a\",\"~:block/title\":\"agent-a\"}"
|
|
in
|
|
let master_block =
|
|
"{\"~:db/id\":110,\"~:block/uuid\":\"~ucccccccc-cccc-4ccc-8ccc-cccccccccccc\",\"~:block/title\":\"AgentBridge \
|
|
master \
|
|
prompt\",\"~:block/order\":1,\"~:block/_parent\":[{\"~:db/id\":111,\"~:block/title\":\"Graph \
|
|
master \
|
|
prompt\",\"~:block/order\":1,\"~:block/tags\":[{\"~:db/ident\":\"~:logseq.class/Code-block\"}]}]}"
|
|
in
|
|
let responses =
|
|
[|
|
|
registry_page;
|
|
agent_page;
|
|
agent_page;
|
|
master_block;
|
|
registry_page;
|
|
"";
|
|
"";
|
|
|]
|
|
in
|
|
let post_index = ref 0 in
|
|
let event_response = ref None in
|
|
let server =
|
|
create_server (fun[@u] req res ->
|
|
let body = ref "" in
|
|
req_set_encoding req "utf8";
|
|
req_on_data req "data" (fun[@u] chunk -> body := !body ^ chunk);
|
|
req_on_end req "end" (fun[@u] () ->
|
|
if req_method req = "GET" && req_url req = "/v1/events" then (
|
|
res_write_head res 200
|
|
(Js.Dict.fromArray
|
|
[| ("Content-Type", "text/event-stream") |]);
|
|
res_write res "\n";
|
|
event_response := Some res)
|
|
else if req_method req <> "POST" || req_url req <> "/v1/invoke"
|
|
then
|
|
write_json res 400
|
|
(error_response
|
|
("unexpected " ^ req_method req ^ " " ^ req_url req))
|
|
else if !post_index >= Array.length responses then
|
|
write_json res 400
|
|
(error_response ("unexpected extra request: " ^ !body))
|
|
else
|
|
let transit = responses.(!post_index) in
|
|
incr post_index;
|
|
let result_transit =
|
|
if transit = "" then "[]" else "[" ^ transit ^ "]"
|
|
in
|
|
write_json res 200 (json_response result_transit)))
|
|
in
|
|
with_server server (fun base_url ->
|
|
Js.Promise.make (fun ~resolve ~reject ->
|
|
let child =
|
|
spawn_cli_async
|
|
~env:
|
|
[|
|
|
("LOGSEQ_CLI_BASE_URL", base_url);
|
|
("LOGSEQ_DB_WORKER_NODE_SCRIPT", worker_script);
|
|
("AGENT_BRIDGE_TEST_MARKER", marker_path);
|
|
|]
|
|
[|
|
|
"--root-dir";
|
|
tmp;
|
|
"--config";
|
|
config_path;
|
|
"--graph";
|
|
"alpha";
|
|
"agent";
|
|
"bridge";
|
|
|]
|
|
in
|
|
let stdout = ref "" in
|
|
let stderr = ref "" in
|
|
let finished = ref false in
|
|
stream_set_encoding child##stdout "utf8";
|
|
stream_set_encoding child##stderr "utf8";
|
|
stream_on_data child##stdout "data" (fun[@u] chunk ->
|
|
stdout := !stdout ^ chunk);
|
|
stream_on_data child##stderr "data" (fun[@u] chunk ->
|
|
stderr := !stderr ^ chunk);
|
|
child_on_error child "error" (fun[@u] exn -> (reject exn [@u]));
|
|
let cleanup_and_resolve value =
|
|
if not !finished then (
|
|
finished := true;
|
|
child_kill child "SIGTERM";
|
|
Option.iter (fun res -> res_end res "") !event_response;
|
|
remove_tree tmp;
|
|
resolve value [@u])
|
|
in
|
|
let cleanup_and_fail message =
|
|
if not !finished then (
|
|
finished := true;
|
|
child_kill child "SIGTERM";
|
|
Option.iter (fun res -> res_end res "") !event_response;
|
|
remove_tree tmp;
|
|
reject_with_assertion reject message)
|
|
in
|
|
let remaining_attempts = ref 50 in
|
|
let interval = ref None in
|
|
let tick () =
|
|
if Node.Fs.existsSync marker_path then (
|
|
Option.iter clear_interval !interval;
|
|
let marker = read_file marker_path in
|
|
let owner_path =
|
|
Node.Path.join
|
|
[|
|
|
tmp;
|
|
"agent-bridge-locks";
|
|
encode_uri_component "alpha"
|
|
^ "--"
|
|
^ encode_uri_component agent_name
|
|
^ ".lock";
|
|
"owner.edn";
|
|
|]
|
|
in
|
|
if marker <> worker_script then
|
|
cleanup_and_fail
|
|
("worker script env was not forwarded: "
|
|
^ Js.Json.stringify (Js.Json.string marker))
|
|
else if not (Node.Fs.existsSync owner_path) then
|
|
cleanup_and_fail
|
|
("missing agent bridge owner: " ^ owner_path)
|
|
else (
|
|
ignore
|
|
(expect_valid_edn "agent bridge owner"
|
|
(read_file owner_path));
|
|
if
|
|
not
|
|
(Js.String.includes
|
|
~search:
|
|
"Codex master session started: cli-live-session"
|
|
!stdout)
|
|
then
|
|
cleanup_and_fail
|
|
(Printf.sprintf
|
|
"master session log missing:\n\
|
|
stdout:\n\
|
|
%s\n\
|
|
stderr:\n\
|
|
%s"
|
|
!stdout !stderr)
|
|
else cleanup_and_resolve pass))
|
|
else (
|
|
decr remaining_attempts;
|
|
if !remaining_attempts <= 0 then (
|
|
Option.iter clear_interval !interval;
|
|
cleanup_and_fail
|
|
(Printf.sprintf
|
|
"fake codex did not stay alive long enough to write \
|
|
marker\n\
|
|
stdout:\n\
|
|
%s\n\
|
|
stderr:\n\
|
|
%s"
|
|
!stdout !stderr)))
|
|
in
|
|
interval := Some (set_interval (fun () -> tick ()) 100);
|
|
child_on_exit child "exit" (fun[@u] code ->
|
|
if (not !finished) && not (Node.Fs.existsSync marker_path)
|
|
then (
|
|
Option.iter clear_interval !interval;
|
|
cleanup_and_fail
|
|
(Printf.sprintf
|
|
"agent bridge exited before fake codex marker; code=%d\n\
|
|
stdout:\n\
|
|
%s\n\
|
|
stderr:\n\
|
|
%s"
|
|
code !stdout !stderr))))));
|
|
|
|
test_promise "fetch timeout exits non-zero with timeout message" (fun () ->
|
|
Js.Promise.make (fun ~resolve ~reject ->
|
|
let request_count = ref 0 in
|
|
let server =
|
|
create_server (fun[@u] req res ->
|
|
let body = ref "" in
|
|
req_set_encoding req "utf8";
|
|
req_on_data req "data" (fun[@u] chunk -> body := !body ^ chunk);
|
|
req_on_end req "end" (fun[@u] () ->
|
|
incr request_count;
|
|
if
|
|
not
|
|
(Js.String.includes ~search:"thread-api/cli-list-pages"
|
|
!body)
|
|
then
|
|
write_json res 400 (error_response "unexpected request")
|
|
else
|
|
set_timeout
|
|
(fun () -> write_json res 200 (json_response "[]"))
|
|
500))
|
|
in
|
|
server_listen server 0 "127.0.0.1" (fun[@u] () ->
|
|
let port = (server_address server)##port in
|
|
let child =
|
|
spawn_cli_async
|
|
~env:
|
|
[|
|
|
( "LOGSEQ_CLI_BASE_URL",
|
|
Printf.sprintf "http://127.0.0.1:%d" port );
|
|
|]
|
|
[|
|
|
"--timeout-ms";
|
|
"100";
|
|
"--graph";
|
|
"alpha";
|
|
"--output";
|
|
"json";
|
|
"list";
|
|
"page";
|
|
|]
|
|
in
|
|
let stdout = ref "" in
|
|
let stderr = ref "" in
|
|
stream_set_encoding child##stdout "utf8";
|
|
stream_set_encoding child##stderr "utf8";
|
|
stream_on_data child##stdout "data" (fun[@u] chunk ->
|
|
stdout := !stdout ^ chunk);
|
|
stream_on_data child##stderr "data" (fun[@u] chunk ->
|
|
stderr := !stderr ^ chunk);
|
|
child_on_error child "error" (fun[@u] exn -> (reject exn [@u]));
|
|
child_on_exit child "exit" (fun[@u] code ->
|
|
server_close server (fun[@u] () ->
|
|
if !request_count <> 1 then
|
|
reject_with_assertion reject
|
|
(Printf.sprintf "expected one request, got %d\n%s\n%s"
|
|
!request_count !stdout !stderr)
|
|
else if code = 0 then
|
|
reject_with_assertion reject
|
|
("expected CLI list page to time out\n" ^ !stdout)
|
|
else if
|
|
String.contains !stdout 'r'
|
|
&& Js.String.includes ~search:"request timeout" !stdout
|
|
|| String.contains !stderr 'r'
|
|
&& Js.String.includes ~search:"request timeout"
|
|
!stderr
|
|
then resolve pass [@u]
|
|
else
|
|
reject_with_assertion reject
|
|
(Printf.sprintf "expected timeout message\n%s\n%s"
|
|
!stdout !stderr))))))
|
|
|
|
let registered = ()
|