Files
Synapse/cli/test/test_cases.ml
Dasemu 40631362b3 Initial import of LogDarioSeq baseline
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>
2026-08-25 15:32:15 +02:00

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 = ()