diff --git a/cli-e2e/spec/non_sync_cases.edn b/cli-e2e/spec/non_sync_cases.edn index e47e8974c6..fb99ec2734 100644 --- a/cli-e2e/spec/non_sync_cases.edn +++ b/cli-e2e/spec/non_sync_cases.edn @@ -1,912 +1,1071 @@ -[ - {:id "global-help" - :cmds ["{{cli}} --help"] - :expect {:exit 0 - :stdout-contains ["Usage: logseq [options]" - "graph create" - "completion"]} - :covers {:options {:global ["--help"]}} - :tags [:global :smoke]} - - {:id "global-version" - :cmds ["{{cli}} --version"] - :expect {:exit 0 - :stdout-contains ["Build time:" "Revision:"]} - :covers {:options {:global ["--version"]}} - :tags [:global :smoke]} - - {:id "verbose-graph-list-json" - :cmds ["{{cli}} --data-dir {{data-dir-arg}} --config {{config-path-arg}} --output json --verbose graph list"] - :expect {:exit 0 - :stderr-contains [":cli/parsed-options"] - :stdout-json-paths {[:status] "ok"}} - :covers {:commands ["graph list"] - :options {:global ["--config" "--data-dir" "--output" "--verbose"]}} - :tags [:global :graph]} - - {:id "graph-create-and-info-json" - :cmds ["{{cli}} --data-dir {{data-dir-arg}} --config {{config-path-arg}} --output json graph create --graph {{graph-arg}} >/dev/null" - "{{cli}} --data-dir {{data-dir-arg}} --config {{config-path-arg}} --output json graph info --graph {{graph-arg}}"] - :expect {:exit 0 - :stdout-json-paths {[:status] "ok" - [:data :graph] "{{graph}}"}} - :covers {:commands ["graph create" "graph info"] - :options {:global ["--config" "--graph" "--data-dir" "--output"]}} - :cleanup ["{{cli}} --data-dir {{data-dir-arg}} --config {{config-path-arg}} --output json server stop --graph {{graph-arg}}"] - :tags [:graph :smoke]} - - {:id "graph-list-human" - :setup ["{{cli}} --data-dir {{data-dir-arg}} --config {{config-path-arg}} --output json graph create --graph {{graph-arg}} >/dev/null"] - :cmds ["{{cli}} --data-dir {{data-dir-arg}} --config {{config-path-arg}} --output human graph list"] - :expect {:exit 0 - :stdout-contains ["{{graph}}"]} - :covers {:commands ["graph list"] - :options {:global ["--config" "--data-dir" "--output"]}} - :cleanup ["{{cli}} --data-dir {{data-dir-arg}} --config {{config-path-arg}} --output json server stop --graph {{graph-arg}}"] - :tags [:graph]} - - {:id "graph-list-edn" - :setup ["{{cli}} --data-dir {{data-dir-arg}} --config {{config-path-arg}} --output json graph create --graph {{graph-arg}} >/dev/null"] - :cmds ["{{cli}} --data-dir {{data-dir-arg}} --config {{config-path-arg}} --output edn graph list"] - :expect {:exit 0 - :stdout-edn-paths {[:status] :ok - [:data :graphs] ["{{graph}}"]}} - :covers {:commands ["graph list"] - :options {:global ["--config" "--data-dir" "--output"]}} - :cleanup ["{{cli}} --data-dir {{data-dir-arg}} --config {{config-path-arg}} --output json server stop --graph {{graph-arg}}"] - :tags [:graph]} - - {:id "graph-switch-json" - :vars {:other-graph "graph-two" - :other-graph-arg "graph-two"} - :setup ["{{cli}} --data-dir {{data-dir-arg}} --config {{config-path-arg}} --output json graph create --graph {{graph-arg}} >/dev/null" - "{{cli}} --data-dir {{data-dir-arg}} --config {{config-path-arg}} --output json graph create --graph {{other-graph-arg}} >/dev/null"] - :cmds ["{{cli}} --data-dir {{data-dir-arg}} --config {{config-path-arg}} --output json graph switch --graph {{other-graph-arg}}"] - :expect {:exit 0 - :stdout-json-paths {[:status] "ok" - [:data :message] "switched to graph-two"}} - :covers {:commands ["graph switch"] - :options {:global ["--config" "--graph" "--data-dir" "--output"]}} - :cleanup ["{{cli}} --data-dir {{data-dir-arg}} --config {{config-path-arg}} --output json server stop --graph {{graph-arg}}" - "{{cli}} --data-dir {{data-dir-arg}} --config {{config-path-arg}} --output json server stop --graph {{other-graph-arg}}"] - :tags [:graph]} - - {:id "graph-validate-fix-json" - :setup ["{{cli}} --data-dir {{data-dir-arg}} --config {{config-path-arg}} --output json graph create --graph {{graph-arg}} >/dev/null"] - :cmds ["{{cli}} --data-dir {{data-dir-arg}} --config {{config-path-arg}} --output json graph validate --graph {{graph-arg}} --fix"] - :expect {:exit 0 - :stdout-json-paths {[:status] "ok" - [:data :result :errors] nil}} - :covers {:commands ["graph validate"] - :options {:global ["--config" "--graph" "--data-dir" "--output"] - :graph ["--fix"]}} - :cleanup ["{{cli}} --data-dir {{data-dir-arg}} --config {{config-path-arg}} --output json server stop --graph {{graph-arg}}"] - :tags [:graph]} - - {:id "graph-export-json" - :setup ["{{cli}} --data-dir {{data-dir-arg}} --config {{config-path-arg}} --output json graph create --graph {{graph-arg}} >/dev/null" - "{{cli}} --data-dir {{data-dir-arg}} --config {{config-path-arg}} --output json upsert page --graph {{graph-arg}} --page ExportHome >/dev/null"] - :cmds ["{{cli}} --data-dir {{data-dir-arg}} --config {{config-path-arg}} --output json graph export --graph {{graph-arg}} --type edn --file {{export-path-arg}}"] - :expect {:exit 0 - :stdout-json-paths {[:status] "ok"} - :stdout-contains ["{{export-path}}"]} - :covers {:commands ["graph export"] - :options {:global ["--config" "--graph" "--data-dir" "--output"] - :graph ["--type" "--file"]}} - :cleanup ["{{cli}} --data-dir {{data-dir-arg}} --config {{config-path-arg}} --output json server stop --graph {{graph-arg}}"] - :tags [:graph]} - - {:id "graph-import-json" - :vars {:source-graph "source-graph" - :source-graph-arg "source-graph"} - :setup ["{{cli}} --data-dir {{data-dir-arg}} --config {{config-path-arg}} --output json graph create --graph {{source-graph-arg}} >/dev/null" - "{{cli}} --data-dir {{data-dir-arg}} --config {{config-path-arg}} --output json upsert page --graph {{source-graph-arg}} --page ImportSeed >/dev/null" - "{{cli}} --data-dir {{data-dir-arg}} --config {{config-path-arg}} --output json graph export --graph {{source-graph-arg}} --type edn --file {{export-path-arg}} >/dev/null"] - :cmds ["{{cli}} --data-dir {{data-dir-arg}} --config {{config-path-arg}} --output json graph import --graph {{graph-arg}} --type edn --input {{export-path-arg}}"] - :expect {:exit 0 - :stdout-json-paths {[:status] "ok"} - :stdout-contains ["Imported edn from" "{{export-path}}"]} - :covers {:commands ["graph import"] - :options {:global ["--config" "--graph" "--data-dir" "--output"] - :graph ["--type" "--input"]}} - :cleanup ["{{cli}} --data-dir {{data-dir-arg}} --config {{config-path-arg}} --output json server stop --graph {{source-graph-arg}}" - "{{cli}} --data-dir {{data-dir-arg}} --config {{config-path-arg}} --output json server stop --graph {{graph-arg}}"] - :tags [:graph]} - - {:id "graph-backup-lifecycle-json" - :vars {:restore-graph "backup-restore" - :restore-graph-arg "backup-restore"} - :setup ["{{cli}} --data-dir {{data-dir-arg}} --config {{config-path-arg}} --output json graph create --graph {{graph-arg}} >/dev/null" - "{{cli}} --data-dir {{data-dir-arg}} --config {{config-path-arg}} --output json upsert page --graph {{graph-arg}} --page BackupSeed >/dev/null"] - :cmds ["{{cli}} --data-dir {{data-dir-arg}} --config {{config-path-arg}} --output json graph backup create --graph {{graph-arg}} --name nightly >/dev/null" - "{{cli}} --data-dir {{data-dir-arg}} --config {{config-path-arg}} --output json graph backup list" - "{{cli}} --data-dir {{data-dir-arg}} --config {{config-path-arg}} --output json graph backup restore --src \"$({{cli}} --data-dir {{data-dir-arg}} --config {{config-path-arg}} --output json graph backup list | python3 -c 'import sys,json; print(json.load(sys.stdin)[\"data\"][\"backups\"][0][\"name\"])')\" --dst {{restore-graph-arg}} >/dev/null" - "{{cli}} --data-dir {{data-dir-arg}} --config {{config-path-arg}} --output json graph backup remove --src \"$({{cli}} --data-dir {{data-dir-arg}} --config {{config-path-arg}} --output json graph backup list | python3 -c 'import sys,json; print(json.load(sys.stdin)[\"data\"][\"backups\"][0][\"name\"])')\""] - :expect {:exit 0 - :stdout-json-paths {[:status] "ok"}} - :covers {:commands ["graph backup create" - "graph backup list" - "graph backup restore" - "graph backup remove"] - :options {:global ["--config" "--graph" "--data-dir" "--output"] - :graph ["--name" "--src" "--dst"]}} - :cleanup ["{{cli}} --data-dir {{data-dir-arg}} --config {{config-path-arg}} --output json server stop --graph {{graph-arg}}" - "{{cli}} --data-dir {{data-dir-arg}} --config {{config-path-arg}} --output json server stop --graph {{restore-graph-arg}}"] - :tags [:graph]} - - {:id "page-upsert-and-list-json" - :setup ["{{cli}} --data-dir {{data-dir-arg}} --config {{config-path-arg}} --output json graph create --graph {{graph-arg}} >/dev/null"] - :cmds ["{{cli}} --data-dir {{data-dir-arg}} --config {{config-path-arg}} --output json upsert page --graph {{graph-arg}} --page Home >/dev/null" - "{{cli}} --data-dir {{data-dir-arg}} --config {{config-path-arg}} --output json list page --graph {{graph-arg}} --fields title,id --sort updated-at --order desc --limit 1"] - :expect {:exit 0 - :stdout-json-paths {[:status] "ok" - [:data :items 0 :block/title] "Home"}} - :covers {:commands ["upsert page" "list page"] - :options {:global ["--config" "--graph" "--data-dir" "--output"] - :upsert ["--page"] - :list ["--fields" "--limit" "--sort" "--order"]}} - :cleanup ["{{cli}} --data-dir {{data-dir-arg}} --config {{config-path-arg}} --output json server stop --graph {{graph-arg}}"] - :tags [:upsert :list :smoke]} - - {:id "task-upsert-and-list-json" - :setup ["{{cli}} --data-dir {{data-dir-arg}} --config {{config-path-arg}} --output json graph create --graph {{graph-arg}} >/dev/null"] - :cmds ["{{cli}} --data-dir {{data-dir-arg}} --config {{config-path-arg}} --output json upsert task --graph {{graph-arg}} --page TaskHome --status todo --priority high --scheduled '2026-02-10T08:00:00.000Z' --deadline '2026-02-12T18:00:00.000Z' >/dev/null" - "{{cli}} --data-dir {{data-dir-arg}} --config {{config-path-arg}} --output json list task --graph {{graph-arg}} --status todo --priority high --content task --fields id,title,status,priority,scheduled,deadline --sort updated-at --order desc --limit 1"] - :expect {:exit 0 - :stdout-json-paths {[:status] "ok" - [:data :items 0 :block/title] "TaskHome" - [:data :items 0 :logseq.property/status] "logseq.property/status.todo" - [:data :items 0 :logseq.property/priority] "logseq.property/priority.high" - [:data :items 0 :logseq.property/scheduled] 1770710400000 - [:data :items 0 :logseq.property/deadline] 1770919200000}} - :covers {:commands ["upsert task" "list task"] - :options {:global ["--config" "--graph" "--data-dir" "--output"] - :upsert ["--page" "--status" "--priority" "--scheduled" "--deadline"] - :list ["--status" "--priority" "--content" "--fields" "--limit" "--sort" "--order"]}} - :cleanup ["{{cli}} --data-dir {{data-dir-arg}} --config {{config-path-arg}} --output json server stop --graph {{graph-arg}}"] - :tags [:upsert :list :smoke]} - - {:id "task-upsert-clear-properties-json" - :setup ["{{cli}} --data-dir {{data-dir-arg}} --config {{config-path-arg}} --output json graph create --graph {{graph-arg}} >/dev/null" - "{{cli}} --data-dir {{data-dir-arg}} --config {{config-path-arg}} --output json upsert task --graph {{graph-arg}} --page TaskHome --status todo --priority high --scheduled '2026-02-10T08:00:00.000Z' --deadline '2026-02-12T18:00:00.000Z' >/dev/null"] - :cmds ["{{cli}} --data-dir {{data-dir-arg}} --config {{config-path-arg}} --output json upsert task --graph {{graph-arg}} --page TaskHome --no-status --no-priority --no-scheduled --no-deadline >/dev/null" - "{{cli}} --data-dir {{data-dir-arg}} --config {{config-path-arg}} --output json list task --graph {{graph-arg}} --content taskhome --fields id,title,status,priority,scheduled,deadline --sort updated-at --order desc --limit 1"] - :expect {:exit 0 - :stdout-json-paths {[:status] "ok" - [:data :items 0 :block/title] "TaskHome"} - :stdout-not-contains ["logseq.property/priority.high" - "2026-02-10T08:00:00.000Z" - "2026-02-12T18:00:00.000Z"]} - :covers {:commands ["upsert task" "list task"] - :options {:global ["--config" "--graph" "--data-dir" "--output"] - :upsert ["--page" "--no-status" "--no-priority" "--no-scheduled" "--no-deadline"] - :list ["--content" "--fields" "--limit" "--sort" "--order"]}} - :cleanup ["{{cli}} --data-dir {{data-dir-arg}} --config {{config-path-arg}} --output json server stop --graph {{graph-arg}}"] - :tags [:upsert :list]} - - {:id "task-upsert-set-clear-conflict-json" - :setup ["{{cli}} --data-dir {{data-dir-arg}} --config {{config-path-arg}} --output json graph create --graph {{graph-arg}} >/dev/null"] - :cmds ["{{cli}} --data-dir {{data-dir-arg}} --config {{config-path-arg}} --output json upsert task --graph {{graph-arg}} --page TaskHome --status todo --no-status"] - :expect {:exit 1 - :stdout-contains ["invalid-options" - "--status and --no-status are mutually exclusive"]} - :covers {:commands ["upsert task"] - :options {:global ["--config" "--graph" "--data-dir" "--output"] - :upsert ["--page" "--status" "--no-status"]}} - :cleanup ["{{cli}} --data-dir {{data-dir-arg}} --config {{config-path-arg}} --output json server stop --graph {{graph-arg}}"] - :tags [:upsert]} - - {:id "node-list-by-tags-properties-json" - :setup ["{{cli}} --data-dir {{data-dir-arg}} --config {{config-path-arg}} --output json graph create --graph {{graph-arg}} >/dev/null"] - :cmds ["{{cli}} --data-dir {{data-dir-arg}} --config {{config-path-arg}} --output json upsert tag --graph {{graph-arg}} --name NodeTag >/dev/null" - "{{cli}} --data-dir {{data-dir-arg}} --config {{config-path-arg}} --output json upsert property --graph {{graph-arg}} --name node-prop >/dev/null" - "{{cli}} --data-dir {{data-dir-arg}} --config {{config-path-arg}} --output json upsert page --graph {{graph-arg}} --page NodeHome --update-tags '[\"NodeTag\"]' --update-properties '{:node-prop \"v\"}' >/dev/null" - "{{cli}} --data-dir {{data-dir-arg}} --config {{config-path-arg}} --output json list node --graph {{graph-arg}} --tags NodeTag --properties node-prop --fields id,title,type --sort updated-at --order desc --limit 20"] - :expect {:exit 0 - :stdout-json-paths {[:status] "ok" - [:data :items 0 :block/title] "NodeHome" - [:data :items 0 :node/type] "page"}} - :covers {:commands ["upsert tag" "upsert property" "upsert page" "list node"] - :options {:global ["--config" "--graph" "--data-dir" "--output"] - :upsert ["--name" "--page" "--update-tags" "--update-properties"] - :list ["--tags" "--properties" "--fields" "--limit" "--sort" "--order"]}} - :cleanup ["{{cli}} --data-dir {{data-dir-arg}} --config {{config-path-arg}} --output json server stop --graph {{graph-arg}}"] - :tags [:upsert :list]} - - {:id "asset-upsert-and-list-json" - :setup ["{{cli}} --data-dir {{data-dir-arg}} --config {{config-path-arg}} --output json graph create --graph {{graph-arg}} >/dev/null" - "printf 'asset-content' > {{export-path-arg}}.png"] - :cmds ["{{cli}} --data-dir {{data-dir-arg}} --config {{config-path-arg}} --output json upsert asset --graph {{graph-arg}} --path {{export-path-arg}}.png --content 'Asset One' --target-page Home >/dev/null" - "{{cli}} --data-dir {{data-dir-arg}} --config {{config-path-arg}} --output json list asset --graph {{graph-arg}} --fields id,title,asset-type,size --sort updated-at --order desc --limit 1"] - :expect {:exit 0 - :stdout-json-paths {[:status] "ok" - [:data :items 0 :block/title] "Asset One" - [:data :items 0 :logseq.property.asset/type] "png" - [:data :items 0 :logseq.property.asset/size] 13}} - :covers {:commands ["upsert asset" "list asset"] - :options {:global ["--config" "--graph" "--data-dir" "--output"] - :upsert ["--path" "--content" "--target-page"] - :list ["--fields" "--limit" "--sort" "--order"]}} - :cleanup ["{{cli}} --data-dir {{data-dir-arg}} --config {{config-path-arg}} --output json server stop --graph {{graph-arg}}"] - :tags [:upsert :list]} - - {:id "asset-upsert-update-json" - :setup ["{{cli}} --data-dir {{data-dir-arg}} --config {{config-path-arg}} --output json graph create --graph {{graph-arg}} >/dev/null" - "printf 'asset-content' > {{export-path-arg}}.png" - "{{cli}} --data-dir {{data-dir-arg}} --config {{config-path-arg}} --output json upsert asset --graph {{graph-arg}} --path {{export-path-arg}}.png --content 'Asset Original' --target-page Home >/dev/null"] - :cmds ["{{cli}} --data-dir {{data-dir-arg}} --config {{config-path-arg}} --output json upsert asset --graph {{graph-arg}} --id \"$({{cli}} --data-dir {{data-dir-arg}} --config {{config-path-arg}} --output json query --graph {{graph-arg}} --query '[:find ?e . :where [?e :block/title \"Asset Original\"]]' | python3 -c 'import sys,json; print(json.load(sys.stdin)[\"data\"][\"result\"])')\" --content 'Asset Updated' >/dev/null" - "{{cli}} --data-dir {{data-dir-arg}} --config {{config-path-arg}} --output json list asset --graph {{graph-arg}} --fields id,title --sort updated-at --order desc --limit 1"] - :expect {:exit 0 - :stdout-json-paths {[:status] "ok" - [:data :items 0 :block/title] "Asset Updated"}} - :covers {:commands ["upsert asset" "list asset"] - :options {:global ["--config" "--graph" "--data-dir" "--output"] - :upsert ["--id" "--content"] - :list ["--fields" "--limit" "--sort" "--order"]}} - :cleanup ["{{cli}} --data-dir {{data-dir-arg}} --config {{config-path-arg}} --output json server stop --graph {{graph-arg}}"] - :tags [:upsert :list]} - - {:id "block-upsert-and-show-json" - :setup ["{{cli}} --data-dir {{data-dir-arg}} --config {{config-path-arg}} --output json graph create --graph {{graph-arg}} >/dev/null" - "{{cli}} --data-dir {{data-dir-arg}} --config {{config-path-arg}} --output json upsert page --graph {{graph-arg}} --page Home >/dev/null"] - :cmds ["{{cli}} --data-dir {{data-dir-arg}} --config {{config-path-arg}} --output json upsert block --graph {{graph-arg}} --target-page Home --content 'Alpha block' >/dev/null" - "{{cli}} --data-dir {{data-dir-arg}} --config {{config-path-arg}} --output json show --graph {{graph-arg}} --page Home --level 2 --linked-references false"] - :expect {:exit 0 - :stdout-json-paths {[:status] "ok" - [:data :root :block/children 0 :block/title] "Alpha block"}} - :covers {:commands ["upsert block" "show"] - :options {:global ["--config" "--graph" "--data-dir" "--output"] - :upsert ["--target-page" "--content"] - :show ["--page" "--level" "--linked-references"]}} - :cleanup ["{{cli}} --data-dir {{data-dir-arg}} --config {{config-path-arg}} --output json server stop --graph {{graph-arg}}"] - :tags [:upsert :show :smoke]} - - {:id "tag-upsert-and-list-json" - :setup ["{{cli}} --data-dir {{data-dir-arg}} --config {{config-path-arg}} --output json graph create --graph {{graph-arg}} >/dev/null"] - :cmds ["{{cli}} --data-dir {{data-dir-arg}} --config {{config-path-arg}} --output json upsert tag --graph {{graph-arg}} --name TagOne >/dev/null" - "{{cli}} --data-dir {{data-dir-arg}} --config {{config-path-arg}} --output json list tag --graph {{graph-arg}} --with-properties --sort updated-at --order desc --limit 1"] - :expect {:exit 0 - :stdout-json-paths {[:status] "ok" - [:data :items 0 :block/title] "TagOne"}} - :covers {:commands ["upsert tag" "list tag"] - :options {:global ["--config" "--graph" "--data-dir" "--output"] - :upsert ["--name"] - :list ["--with-properties" "--sort" "--order" "--limit"]}} - :cleanup ["{{cli}} --data-dir {{data-dir-arg}} --config {{config-path-arg}} --output json server stop --graph {{graph-arg}}"] - :tags [:upsert :list]} - - {:id "property-upsert-and-list-json" - :setup ["{{cli}} --data-dir {{data-dir-arg}} --config {{config-path-arg}} --output json graph create --graph {{graph-arg}} >/dev/null"] - :cmds ["{{cli}} --data-dir {{data-dir-arg}} --config {{config-path-arg}} --output json upsert property --graph {{graph-arg}} --name score --type number --cardinality one >/dev/null" - "{{cli}} --data-dir {{data-dir-arg}} --config {{config-path-arg}} --output json list property --graph {{graph-arg}} --with-type --sort updated-at --order desc --limit 1"] - :expect {:exit 0 - :stdout-json-paths {[:status] "ok" - [:data :items 0 :block/title] "score" - [:data :items 0 :logseq.property/type] "number" - [:data :items 0 :db/cardinality] "db.cardinality/one"}} - :covers {:commands ["upsert property" "list property"] - :options {:global ["--config" "--graph" "--data-dir" "--output"] - :upsert ["--name" "--type" "--cardinality"] - :list ["--with-type" "--sort" "--order" "--limit"]}} - :cleanup ["{{cli}} --data-dir {{data-dir-arg}} --config {{config-path-arg}} --output json server stop --graph {{graph-arg}}"] - :tags [:upsert :list]} - - {:id "list-tag-expand-offset-json" - :setup ["{{cli}} --data-dir {{data-dir-arg}} --config {{config-path-arg}} --output json graph create --graph {{graph-arg}} >/dev/null" - "{{cli}} --data-dir {{data-dir-arg}} --config {{config-path-arg}} --output json upsert tag --graph {{graph-arg}} --name TagA >/dev/null" - "{{cli}} --data-dir {{data-dir-arg}} --config {{config-path-arg}} --output json upsert tag --graph {{graph-arg}} --name TagB >/dev/null"] - :cmds ["{{cli}} --data-dir {{data-dir-arg}} --config {{config-path-arg}} --output json list tag --graph {{graph-arg}} --expand --with-extends --sort updated-at --order desc --offset 0 --limit 20"] - :expect {:exit 0 - :stdout-json-paths {[:status] "ok"} - :stdout-contains ["TagA" "TagB"]} - :covers {:commands ["list tag"] - :options {:global ["--config" "--graph" "--data-dir" "--output"] - :list ["--expand" "--with-extends" "--offset" "--sort" "--order" "--limit"]}} - :cleanup ["{{cli}} --data-dir {{data-dir-arg}} --config {{config-path-arg}} --output json server stop --graph {{graph-arg}}"] - :tags [:list]} - - {:id "list-property-classes-json" - :setup ["{{cli}} --data-dir {{data-dir-arg}} --config {{config-path-arg}} --output json graph create --graph {{graph-arg}} >/dev/null" - "{{cli}} --data-dir {{data-dir-arg}} --config {{config-path-arg}} --output json upsert property --graph {{graph-arg}} --name alpha --type number --cardinality one >/dev/null" - "{{cli}} --data-dir {{data-dir-arg}} --config {{config-path-arg}} --output json upsert property --graph {{graph-arg}} --name beta --type number --cardinality one >/dev/null"] - :cmds ["{{cli}} --data-dir {{data-dir-arg}} --config {{config-path-arg}} --output json list property --graph {{graph-arg}} --with-classes --sort updated-at --order desc --offset 0 --limit 20"] - :expect {:exit 0 - :stdout-json-paths {[:status] "ok"} - :stdout-contains ["alpha" "beta"]} - :covers {:commands ["list property"] - :options {:global ["--config" "--graph" "--data-dir" "--output"] - :list ["--with-classes" "--sort" "--order" "--offset" "--limit"]}} - :cleanup ["{{cli}} --data-dir {{data-dir-arg}} --config {{config-path-arg}} --output json server stop --graph {{graph-arg}}"] - :tags [:list]} - - {:id "list-page-journal-only-json" - :setup ["{{cli}} --data-dir {{data-dir-arg}} --config {{config-path-arg}} --output json graph create --graph {{graph-arg}} >/dev/null"] - :cmds ["{{cli}} --data-dir {{data-dir-arg}} --config {{config-path-arg}} --output json list page --graph {{graph-arg}} --journal-only"] - :expect {:exit 0 - :stdout-json-paths {[:status] "ok" - [:data :items] []}} - :covers {:commands ["list page"] - :options {:global ["--config" "--graph" "--data-dir" "--output"] - :list ["--journal-only"]}} - :cleanup ["{{cli}} --data-dir {{data-dir-arg}} --config {{config-path-arg}} --output json server stop --graph {{graph-arg}}"] - :tags [:list]} - - {:id "search-block-json" - :setup ["{{cli}} --data-dir {{data-dir-arg}} --config {{config-path-arg}} --output json graph create --graph {{graph-arg}} >/dev/null" - "{{cli}} --data-dir {{data-dir-arg}} --config {{config-path-arg}} --output json upsert page --graph {{graph-arg}} --page SearchBlockTarget >/dev/null"] - :cmds ["{{cli}} --data-dir {{data-dir-arg}} --config {{config-path-arg}} search block --content blocktarget --output json --graph {{graph-arg}}"] - :expect {:exit 0 - :stdout-json-paths {[:status] "ok"} - :stdout-contains ["SearchBlockTarget"]} - :covers {:commands ["search block"] - :options {:global ["--config" "--graph" "--data-dir" "--output"] - :search ["--content"]}} - :cleanup ["{{cli}} --data-dir {{data-dir-arg}} --config {{config-path-arg}} --output json server stop --graph {{graph-arg}}"] - :tags [:search :smoke]} - - {:id "search-page-json" - :setup ["{{cli}} --data-dir {{data-dir-arg}} --config {{config-path-arg}} --output json graph create --graph {{graph-arg}} >/dev/null" - "{{cli}} --data-dir {{data-dir-arg}} --config {{config-path-arg}} --output json upsert page --graph {{graph-arg}} --page SearchPageTarget >/dev/null"] - :cmds ["{{cli}} --data-dir {{data-dir-arg}} --config {{config-path-arg}} --output json search page --content target --graph {{graph-arg}}"] - :expect {:exit 0 - :stdout-json-paths {[:status] "ok"} - :stdout-contains ["SearchPageTarget"]} - :covers {:commands ["search page"] - :options {:global ["--config" "--graph" "--data-dir" "--output"] - :search ["--content"]}} - :cleanup ["{{cli}} --data-dir {{data-dir-arg}} --config {{config-path-arg}} --output json server stop --graph {{graph-arg}}"] - :tags [:search]} - - {:id "search-property-json" - :setup ["{{cli}} --data-dir {{data-dir-arg}} --config {{config-path-arg}} --output json graph create --graph {{graph-arg}} >/dev/null" - "{{cli}} --data-dir {{data-dir-arg}} --config {{config-path-arg}} --output json upsert property --graph {{graph-arg}} --name SearchOwner --type default >/dev/null"] - :cmds ["{{cli}} --data-dir {{data-dir-arg}} --config {{config-path-arg}} --output json search property --content owner --graph {{graph-arg}}"] - :expect {:exit 0 - :stdout-json-paths {[:status] "ok"} - :stdout-contains ["SearchOwner"]} - :covers {:commands ["search property"] - :options {:global ["--config" "--graph" "--data-dir" "--output"] - :search ["--content"]}} - :cleanup ["{{cli}} --data-dir {{data-dir-arg}} --config {{config-path-arg}} --output json server stop --graph {{graph-arg}}"] - :tags [:search]} - - {:id "search-tag-json" - :setup ["{{cli}} --data-dir {{data-dir-arg}} --config {{config-path-arg}} --output json graph create --graph {{graph-arg}} >/dev/null" - "{{cli}} --data-dir {{data-dir-arg}} --config {{config-path-arg}} --output json upsert tag --graph {{graph-arg}} --name SearchTagTarget >/dev/null"] - :cmds ["{{cli}} --data-dir {{data-dir-arg}} --config {{config-path-arg}} --output json search tag --content target --graph {{graph-arg}}"] - :expect {:exit 0 - :stdout-json-paths {[:status] "ok"} - :stdout-contains ["SearchTagTarget"]} - :covers {:commands ["search tag"] - :options {:global ["--config" "--graph" "--data-dir" "--output"] - :search ["--content"]}} - :cleanup ["{{cli}} --data-dir {{data-dir-arg}} --config {{config-path-arg}} --output json server stop --graph {{graph-arg}}"] - :tags [:search]} - - {:id "block-upsert-blocks-file-json" - :setup ["{{cli}} --data-dir {{data-dir-arg}} --config {{config-path-arg}} --output json graph create --graph {{graph-arg}} >/dev/null" - "{{cli}} --data-dir {{data-dir-arg}} --config {{config-path-arg}} --output json upsert page --graph {{graph-arg}} --page Home >/dev/null" - "{{cli}} --data-dir {{data-dir-arg}} --config {{config-path-arg}} --output json upsert block --graph {{graph-arg}} --target-page Home --content 'Anchor block' >/dev/null" - "printf '[{:block/title \"Inserted from file\"}]' > {{export-path-arg}}"] - :cmds ["{{cli}} --data-dir {{data-dir-arg}} --config {{config-path-arg}} --output human upsert block --graph {{graph-arg}} --target-id \"$({{cli}} --data-dir {{data-dir-arg}} --config {{config-path-arg}} --output json query --graph {{graph-arg}} --query '[:find ?e . :where [?e :block/title \"Anchor block\"]]' | python3 -c 'import sys,json; print(json.load(sys.stdin)[\"data\"][\"result\"])')\" --blocks-file {{export-path-arg}} --pos sibling >/dev/null" - "{{cli}} --data-dir {{data-dir-arg}} --config {{config-path-arg}} --output human show --graph {{graph-arg}} --page Home"] - :expect {:exit 0 - :stdout-contains ["Inserted from file"]} - :covers {:commands ["upsert block"] - :options {:global ["--config" "--graph" "--data-dir" "--output"] - :upsert ["--blocks-file" "--target-id" "--pos"]}} - :cleanup ["{{cli}} --data-dir {{data-dir-arg}} --config {{config-path-arg}} --output json server stop --graph {{graph-arg}}"] - :tags [:upsert]} - - {:id "block-upsert-target-uuid-json" - :setup ["{{cli}} --data-dir {{data-dir-arg}} --config {{config-path-arg}} --output json graph create --graph {{graph-arg}} >/dev/null" - "{{cli}} --data-dir {{data-dir-arg}} --config {{config-path-arg}} --output json upsert page --graph {{graph-arg}} --page Home >/dev/null" - "{{cli}} --data-dir {{data-dir-arg}} --config {{config-path-arg}} --output json upsert block --graph {{graph-arg}} --target-page Home --content 'Anchor block' >/dev/null"] - :cmds ["{{cli}} --data-dir {{data-dir-arg}} --config {{config-path-arg}} --output human upsert block --graph {{graph-arg}} --target-uuid \"$({{cli}} --data-dir {{data-dir-arg}} --config {{config-path-arg}} --output json query --graph {{graph-arg}} --query '[:find ?uuid . :where [?e :block/title \"Anchor block\"] [?e :block/uuid ?uuid]]' | python3 -c 'import sys,json; print(json.load(sys.stdin)[\"data\"][\"result\"])')\" --content 'Inserted by uuid' >/dev/null" - "{{cli}} --data-dir {{data-dir-arg}} --config {{config-path-arg}} --output human show --graph {{graph-arg}} --page Home"] - :expect {:exit 0 - :stdout-contains ["Inserted by uuid"]} - :covers {:commands ["upsert block"] - :options {:global ["--config" "--graph" "--data-dir" "--output"] - :upsert ["--target-uuid"]}} - :cleanup ["{{cli}} --data-dir {{data-dir-arg}} --config {{config-path-arg}} --output json server stop --graph {{graph-arg}}"] - :tags [:upsert]} - - {:id "block-upsert-update-id-json" - :setup ["{{cli}} --data-dir {{data-dir-arg}} --config {{config-path-arg}} --output json graph create --graph {{graph-arg}} >/dev/null" - "{{cli}} --data-dir {{data-dir-arg}} --config {{config-path-arg}} --output json upsert page --graph {{graph-arg}} --page Home >/dev/null" - "{{cli}} --data-dir {{data-dir-arg}} --config {{config-path-arg}} --output json upsert block --graph {{graph-arg}} --target-page Home --content 'Alpha block' >/dev/null"] - :cmds ["{{cli}} --data-dir {{data-dir-arg}} --config {{config-path-arg}} --output human upsert block --graph {{graph-arg}} --id \"$({{cli}} --data-dir {{data-dir-arg}} --config {{config-path-arg}} --output json query --graph {{graph-arg}} --query '[:find ?e . :where [?e :block/title \"Alpha block\"]]' | python3 -c 'import sys,json; print(json.load(sys.stdin)[\"data\"][\"result\"])')\" --content 'Updated by id' --update-tags '[:logseq.class/Quote-block]' --update-properties '{:logseq.property/publishing-public? true}' >/dev/null" - "{{cli}} --data-dir {{data-dir-arg}} --config {{config-path-arg}} --output human show --graph {{graph-arg}} --page Home"] - :expect {:exit 0 - :stdout-contains ["Updated by id"]} - :covers {:commands ["upsert block"] - :options {:global ["--config" "--graph" "--data-dir" "--output"] - :upsert ["--id" "--update-tags" "--update-properties"]}} - :cleanup ["{{cli}} --data-dir {{data-dir-arg}} --config {{config-path-arg}} --output json server stop --graph {{graph-arg}}"] - :tags [:upsert]} - - {:id "block-upsert-update-id-custom-many-property-json" - :vars {:prop-name "Reproducible steps"} - :setup ["{{cli}} --data-dir {{data-dir-arg}} --config {{config-path-arg}} --output json graph create --graph {{graph-arg}} >/dev/null" - "{{cli}} --data-dir {{data-dir-arg}} --config {{config-path-arg}} --output json upsert page --graph {{graph-arg}} --page Home >/dev/null" - "{{cli}} --data-dir {{data-dir-arg}} --config {{config-path-arg}} --output json upsert property --graph {{graph-arg}} --name '{{prop-name}}' --type default --cardinality many --public true >/dev/null"] - :cmds ["{{cli}} --data-dir {{data-dir-arg}} --config {{config-path-arg}} --output json upsert block --graph {{graph-arg}} --target-page Home --content 'Alpha block' >/dev/null" - "{{cli}} --data-dir {{data-dir-arg}} --config {{config-path-arg}} --output json upsert block --graph {{graph-arg}} --id \"$({{cli}} --data-dir {{data-dir-arg}} --config {{config-path-arg}} --output json query --graph {{graph-arg}} --query '[:find ?e . :where [?e :block/title \"Alpha block\"]]' | python3 -c 'import sys,json; print(json.load(sys.stdin)[\"data\"][\"result\"])')\" --update-properties '{\"{{prop-name}}\" [\"Step 1\" \"Step 2\" \"Step 3\"]}' >/dev/null" - "{{cli}} --data-dir {{data-dir-arg}} --config {{config-path-arg}} --output human show --graph {{graph-arg}} --id \"$({{cli}} --data-dir {{data-dir-arg}} --config {{config-path-arg}} --output json query --graph {{graph-arg}} --query '[:find ?e . :where [?e :block/title \"Alpha block\"]]' | python3 -c 'import sys,json; print(json.load(sys.stdin)[\"data\"][\"result\"])')\""] - :expect {:exit 0 - :stdout-contains ["{{prop-name}}" "Step 1" "Step 2" "Step 3"]} - :covers {:commands ["upsert block" "show"] - :options {:global ["--config" "--graph" "--data-dir" "--output"] - :upsert ["--id" "--target-page" "--content" "--update-properties"] - :show ["--id"]}} - :tags [:upsert :show]} - - {:id "block-upsert-update-uuid-json" - :setup ["{{cli}} --data-dir {{data-dir-arg}} --config {{config-path-arg}} --output json graph create --graph {{graph-arg}} >/dev/null" - "{{cli}} --data-dir {{data-dir-arg}} --config {{config-path-arg}} --output json upsert page --graph {{graph-arg}} --page Home >/dev/null" - "{{cli}} --data-dir {{data-dir-arg}} --config {{config-path-arg}} --output json upsert block --graph {{graph-arg}} --target-page Home --content 'Alpha block' --update-tags '[:logseq.class/Quote-block]' --update-properties '{:logseq.property/publishing-public? true}' >/dev/null"] - :cmds ["{{cli}} --data-dir {{data-dir-arg}} --config {{config-path-arg}} --output human upsert block --graph {{graph-arg}} --uuid \"$({{cli}} --data-dir {{data-dir-arg}} --config {{config-path-arg}} --output json query --graph {{graph-arg}} --query '[:find ?uuid . :where [?e :block/title \"Alpha block\"] [?e :block/uuid ?uuid]]' | python3 -c 'import sys,json; print(json.load(sys.stdin)[\"data\"][\"result\"])')\" --content 'Updated by uuid' --remove-tags '[:logseq.class/Quote-block]' --remove-properties '[:logseq.property/publishing-public?]' >/dev/null" - "{{cli}} --data-dir {{data-dir-arg}} --config {{config-path-arg}} --output human show --graph {{graph-arg}} --page Home"] - :expect {:exit 0 - :stdout-contains ["Updated by uuid"]} - :covers {:commands ["upsert block"] - :options {:global ["--config" "--graph" "--data-dir" "--output"] - :upsert ["--uuid" "--remove-tags" "--remove-properties"]}} - :cleanup ["{{cli}} --data-dir {{data-dir-arg}} --config {{config-path-arg}} --output json server stop --graph {{graph-arg}}"] - :tags [:upsert]} - - {:id "property-upsert-update-id-json" - :setup ["{{cli}} --data-dir {{data-dir-arg}} --config {{config-path-arg}} --output json graph create --graph {{graph-arg}} >/dev/null" - "{{cli}} --data-dir {{data-dir-arg}} --config {{config-path-arg}} --output json upsert property --graph {{graph-arg}} --name score --type number --cardinality one >/dev/null"] - :cmds ["{{cli}} --data-dir {{data-dir-arg}} --config {{config-path-arg}} --output json upsert property --graph {{graph-arg}} --id \"$({{cli}} --data-dir {{data-dir-arg}} --config {{config-path-arg}} --output json list property --graph {{graph-arg}} --fields title,id --limit 200 --sort updated-at --order desc | python3 -c 'import sys,json; print(next(item[\"db/id\"] for item in json.load(sys.stdin)[\"data\"][\"items\"] if item[\"block/title\"] == \"score\"))')\" --hide true --public false"] - :expect {:exit 0 - :stdout-json-paths {[:status] "ok"}} - :covers {:commands ["upsert property"] - :options {:global ["--config" "--graph" "--data-dir" "--output"] - :upsert ["--id" "--hide" "--public"]}} - :cleanup ["{{cli}} --data-dir {{data-dir-arg}} --config {{config-path-arg}} --output json server stop --graph {{graph-arg}}"] - :tags [:upsert]} - - {:id "query-custom-json" - :setup ["{{cli}} --data-dir {{data-dir-arg}} --config {{config-path-arg}} --output json graph create --graph {{graph-arg}} >/dev/null" - "{{cli}} --data-dir {{data-dir-arg}} --config {{config-path-arg}} --output json upsert page --graph {{graph-arg}} --page Home >/dev/null" - "{{cli}} --data-dir {{data-dir-arg}} --config {{config-path-arg}} --output json upsert block --graph {{graph-arg}} --target-page Home --content 'Alpha block' >/dev/null"] - :cmds ["{{cli}} --data-dir {{data-dir-arg}} --config {{config-path-arg}} --output json query --graph {{graph-arg}} --query '[:find ?title . :where [?b :block/title ?title] [(= ?title \"Alpha block\")]]'"] - :expect {:exit 0 - :stdout-json-paths {[:status] "ok" - [:data :result] "Alpha block"}} - :covers {:commands ["query"] - :options {:global ["--config" "--graph" "--data-dir" "--output"] - :query ["--query"]}} - :cleanup ["{{cli}} --data-dir {{data-dir-arg}} --config {{config-path-arg}} --output json server stop --graph {{graph-arg}}"] - :tags [:query]} - - {:id "query-named-json" - :setup ["{{cli}} --data-dir {{data-dir-arg}} --config {{config-path-arg}} --output json graph create --graph {{graph-arg}} >/dev/null"] - :cmds ["{{cli}} --data-dir {{data-dir-arg}} --config {{config-path-arg}} --output json query --graph {{graph-arg}} --name recent-updated --inputs '[1]'"] - :expect {:exit 0 - :stdout-json-paths {[:status] "ok"}} - :covers {:commands ["query"] - :options {:global ["--config" "--graph" "--data-dir" "--output"] - :query ["--name" "--inputs"]}} - :cleanup ["{{cli}} --data-dir {{data-dir-arg}} --config {{config-path-arg}} --output json server stop --graph {{graph-arg}}"] - :tags [:query]} - - {:id "query-list-json" - :setup ["{{cli}} --data-dir {{data-dir-arg}} --config {{config-path-arg}} --output json graph create --graph {{graph-arg}} >/dev/null"] - :cmds ["{{cli}} --data-dir {{data-dir-arg}} --config {{config-path-arg}} --output json query list --graph {{graph-arg}}"] - :expect {:exit 0 - :stdout-json-paths {[:status] "ok" - [:data :queries 0 :name] "list-priority"}} - :covers {:commands ["query list"] - :options {:global ["--config" "--graph" "--data-dir" "--output"]}} - :cleanup ["{{cli}} --data-dir {{data-dir-arg}} --config {{config-path-arg}} --output json server stop --graph {{graph-arg}}"] - :tags [:query]} - - {:id "example-show-human" - :cmds ["{{cli}} --output human example show"] - :expect {:exit 0 - :stdout-contains ["Selector: show" - "Matched commands:" - "show" - "Examples:"]} - :covers {:commands ["example show"] - :options {:global ["--output"]}} - :tags [:example]} - - {:id "example-upsert-page-json" - :cmds ["{{cli}} --output json example upsert page"] - :expect {:exit 0 - :stdout-json-paths {[:status] "ok" - [:data :selector] "upsert page" - [:data :matched-commands 0] "upsert page"}} - :covers {:commands ["example upsert page"] - :options {:global ["--output"]}} - :tags [:example]} - - {:id "example-upsert-prefix-json" - :cmds ["{{cli}} --output json example upsert"] - :expect {:exit 0 - :stdout-json-paths {[:status] "ok" - [:data :selector] "upsert" - [:data :matched-commands 0] "upsert block"}} - :covers {:commands ["example upsert"] - :options {:global ["--output"]}} - :tags [:example]} - - {:id "example-list-prefix-json" - :cmds ["{{cli}} --output json example list"] - :expect {:exit 0 - :stdout-json-paths {[:status] "ok" - [:data :selector] "list" - [:data :matched-commands 0] "list page"}} - :covers {:commands ["example list"] - :options {:global ["--output"]}} - :tags [:example]} - - {:id "example-list-page-json" - :cmds ["{{cli}} --output json example list page"] - :expect {:exit 0 - :stdout-json-paths {[:status] "ok" - [:data :selector] "list page" - [:data :matched-commands] ["list page"]}} - :covers {:commands ["example list page"] - :options {:global ["--output"]}} - :tags [:example]} - - {:id "example-query-prefix-json" - :cmds ["{{cli}} --output json example query"] - :expect {:exit 0 - :stdout-json-paths {[:status] "ok" - [:data :selector] "query" - [:data :matched-commands 0] "query"}} - :covers {:commands ["example query"] - :options {:global ["--output"]}} - :tags [:example]} - - {:id "example-query-list-json" - :cmds ["{{cli}} --output json example query list"] - :expect {:exit 0 - :stdout-json-paths {[:status] "ok" - [:data :selector] "query list" - [:data :matched-commands] ["query list"]}} - :covers {:commands ["example query list"] - :options {:global ["--output"]}} - :tags [:example]} - - {:id "example-remove-prefix-json" - :cmds ["{{cli}} --output json example remove"] - :expect {:exit 0 - :stdout-json-paths {[:status] "ok" - [:data :selector] "remove" - [:data :matched-commands 0] "remove block"}} - :covers {:commands ["example remove"] - :options {:global ["--output"]}} - :tags [:example]} - - {:id "example-remove-page-json" - :cmds ["{{cli}} --output json example remove page"] - :expect {:exit 0 - :stdout-json-paths {[:status] "ok" - [:data :selector] "remove page" - [:data :matched-commands] ["remove page"]}} - :covers {:commands ["example remove page"] - :options {:global ["--output"]}} - :tags [:example]} - - {:id "example-search-prefix-json" - :cmds ["{{cli}} --output json example search"] - :expect {:exit 0 - :stdout-json-paths {[:status] "ok" - [:data :selector] "search" - [:data :matched-commands 0] "search block"}} - :covers {:commands ["example search"] - :options {:global ["--output"]}} - :tags [:example]} - - {:id "example-search-block-edn" - :cmds ["{{cli}} --output edn example search block"] - :expect {:exit 0 - :stdout-edn-paths {[:status] :ok - [:data :selector] "search block" - [:data :matched-commands] ["search block"]}} - :covers {:commands ["example search block"] - :options {:global ["--output"]}} - :tags [:example]} - - {:id "show-id-human" - :setup ["{{cli}} --data-dir {{data-dir-arg}} --config {{config-path-arg}} --output json graph create --graph {{graph-arg}} >/dev/null" - "{{cli}} --data-dir {{data-dir-arg}} --config {{config-path-arg}} --output json upsert page --graph {{graph-arg}} --page Home >/dev/null" - "{{cli}} --data-dir {{data-dir-arg}} --config {{config-path-arg}} --output json upsert block --graph {{graph-arg}} --target-page Home --content 'Alpha block' >/dev/null"] - :cmds ["{{cli}} --data-dir {{data-dir-arg}} --config {{config-path-arg}} --graph {{graph-arg}} --output human show --id \"$({{cli}} --data-dir {{data-dir-arg}} --config {{config-path-arg}} --graph {{graph-arg}} --output human query --query '[:find ?e . :where [?e :block/title \"Alpha block\"]]')\""] - :expect {:exit 0 - :stdout-contains ["Alpha block"]} - :covers {:commands ["show"] - :options {:global ["--config" "--graph" "--data-dir" "--output"] - :show ["--id"]}} - :cleanup ["{{cli}} --data-dir {{data-dir-arg}} --config {{config-path-arg}} --output json server stop --graph {{graph-arg}}"] - :tags [:show]} - - {:id "show-uuid-human" - :setup ["{{cli}} --data-dir {{data-dir-arg}} --config {{config-path-arg}} --output json graph create --graph {{graph-arg}} >/dev/null" - "{{cli}} --data-dir {{data-dir-arg}} --config {{config-path-arg}} --output json upsert page --graph {{graph-arg}} --page Home >/dev/null" - "{{cli}} --data-dir {{data-dir-arg}} --config {{config-path-arg}} --output json upsert block --graph {{graph-arg}} --target-page Home --content 'Alpha block' >/dev/null"] - :cmds ["{{cli}} --data-dir {{data-dir-arg}} --config {{config-path-arg}} --graph {{graph-arg}} --output human show --uuid \"$({{cli}} --data-dir {{data-dir-arg}} --config {{config-path-arg}} --graph {{graph-arg}} --output json query --query '[:find ?uuid . :where [?e :block/title \"Alpha block\"] [?e :block/uuid ?uuid]]' | python3 -c 'import sys,json; print(json.load(sys.stdin)[\"data\"][\"result\"])')\""] - :expect {:exit 0 - :stdout-contains ["Alpha block"]} - :covers {:commands ["show"] - :options {:global ["--config" "--graph" "--data-dir" "--output"] - :show ["--uuid"]}} - :cleanup ["{{cli}} --data-dir {{data-dir-arg}} --config {{config-path-arg}} --output json server stop --graph {{graph-arg}}"] - :tags [:show]} - - {:id "show-stdin-id-human" - :setup ["{{cli}} --data-dir {{data-dir-arg}} --config {{config-path-arg}} --output json graph create --graph {{graph-arg}} >/dev/null" - "{{cli}} --data-dir {{data-dir-arg}} --config {{config-path-arg}} --output json upsert page --graph {{graph-arg}} --page Home >/dev/null" - "{{cli}} --data-dir {{data-dir-arg}} --config {{config-path-arg}} --output json upsert block --graph {{graph-arg}} --target-page Home --content 'Alpha block' >/dev/null"] - :cmds ["{{cli}} --data-dir {{data-dir-arg}} --config {{config-path-arg}} --graph {{graph-arg}} --output human query --query '[:find [?e ...] :in $ ?q :where [?e :block/title ?title] [(clojure.string/includes? ?title ?q)]]' --inputs '[\"Alpha\"]' | {{cli}} --data-dir {{data-dir-arg}} --config {{config-path-arg}} --graph {{graph-arg}} --output human show --id"] - :expect {:exit 0 - :stdout-contains ["Alpha block"]} - :covers {:commands ["show"] - :options {:global ["--config" "--graph" "--data-dir" "--output"] - :show ["stdin:--id"]}} - :cleanup ["{{cli}} --data-dir {{data-dir-arg}} --config {{config-path-arg}} --output json server stop --graph {{graph-arg}}"] - :tags [:show :pipe]} - - {:id "debug-pull-id-json" - :setup ["{{cli}} --data-dir {{data-dir-arg}} --config {{config-path-arg}} --output json graph create --graph {{graph-arg}} >/dev/null" - "{{cli}} --data-dir {{data-dir-arg}} --config {{config-path-arg}} --output json upsert page --graph {{graph-arg}} --page Home >/dev/null"] - :cmds ["{{cli}} --data-dir {{data-dir-arg}} --config {{config-path-arg}} --output json debug pull --graph {{graph-arg}} --id \"$({{cli}} --data-dir {{data-dir-arg}} --config {{config-path-arg}} --output json query --graph {{graph-arg}} --query '[:find ?e . :where [?e :block/title \"Home\"]]' | python3 -c 'import sys,json; print(json.load(sys.stdin)[\"data\"][\"result\"])')\""] - :expect {:exit 0 - :stdout-json-paths {[:status] "ok" - [:data :entity :block/title] "Home"}} - :covers {:commands ["debug pull"] - :options {:global ["--config" "--graph" "--data-dir" "--output"] - :debug ["--id"]}} - :cleanup ["{{cli}} --data-dir {{data-dir-arg}} --config {{config-path-arg}} --output json server stop --graph {{graph-arg}}"] - :tags [:debug]} - - {:id "debug-pull-uuid-json" - :setup ["{{cli}} --data-dir {{data-dir-arg}} --config {{config-path-arg}} --output json graph create --graph {{graph-arg}} >/dev/null" - "{{cli}} --data-dir {{data-dir-arg}} --config {{config-path-arg}} --output json upsert page --graph {{graph-arg}} --page Home >/dev/null"] - :cmds ["{{cli}} --data-dir {{data-dir-arg}} --config {{config-path-arg}} --output json debug pull --graph {{graph-arg}} --uuid \"$({{cli}} --data-dir {{data-dir-arg}} --config {{config-path-arg}} --output json query --graph {{graph-arg}} --query '[:find ?uuid . :where [?e :block/title \"Home\"] [?e :block/uuid ?uuid]]' | python3 -c 'import sys,json; print(json.load(sys.stdin)[\"data\"][\"result\"])')\""] - :expect {:exit 0 - :stdout-json-paths {[:status] "ok" - [:data :entity :block/title] "Home"}} - :covers {:commands ["debug pull"] - :options {:global ["--config" "--graph" "--data-dir" "--output"] - :debug ["--uuid"]}} - :cleanup ["{{cli}} --data-dir {{data-dir-arg}} --config {{config-path-arg}} --output json server stop --graph {{graph-arg}}"] - :tags [:debug]} - - {:id "debug-pull-ident-json" - :setup ["{{cli}} --data-dir {{data-dir-arg}} --config {{config-path-arg}} --output json graph create --graph {{graph-arg}} >/dev/null"] - :cmds ["{{cli}} --data-dir {{data-dir-arg}} --config {{config-path-arg}} --output json debug pull --graph {{graph-arg}} --ident :logseq.class/Tag"] - :expect {:exit 0 - :stdout-json-paths {[:status] "ok" - [:data :entity :db/ident] "logseq.class/Tag"}} - :covers {:commands ["debug pull"] - :options {:global ["--config" "--graph" "--data-dir" "--output"] - :debug ["--ident"]}} - :cleanup ["{{cli}} --data-dir {{data-dir-arg}} --config {{config-path-arg}} --output json server stop --graph {{graph-arg}}"] - :tags [:debug]} - - {:id "debug-pull-current-graph-fallback-json" - :setup ["{{cli}} --data-dir {{data-dir-arg}} --config {{config-path-arg}} --output json graph create --graph {{graph-arg}} >/dev/null" - "{{cli}} --data-dir {{data-dir-arg}} --config {{config-path-arg}} --output json upsert page --graph {{graph-arg}} --page Home >/dev/null"] - :cmds ["{{cli}} --data-dir {{data-dir-arg}} --config {{config-path-arg}} --output json debug pull --id \"$({{cli}} --data-dir {{data-dir-arg}} --config {{config-path-arg}} --output json query --graph {{graph-arg}} --query '[:find ?e . :where [?e :block/title \"Home\"]]' | python3 -c 'import sys,json; print(json.load(sys.stdin)[\"data\"][\"result\"])')\""] - :expect {:exit 0 - :stdout-json-paths {[:status] "ok" - [:data :entity :block/title] "Home"}} - :covers {:commands ["debug pull"] - :options {:global ["--config" "--data-dir" "--output"] - :debug ["--id"]}} - :cleanup ["{{cli}} --data-dir {{data-dir-arg}} --config {{config-path-arg}} --output json server stop --graph {{graph-arg}}"] - :tags [:debug]} - - {:id "remove-page-json" - :setup ["{{cli}} --data-dir {{data-dir-arg}} --config {{config-path-arg}} --output json graph create --graph {{graph-arg}} >/dev/null" - "{{cli}} --data-dir {{data-dir-arg}} --config {{config-path-arg}} --output json upsert page --graph {{graph-arg}} --page Home >/dev/null"] - :cmds ["{{cli}} --data-dir {{data-dir-arg}} --config {{config-path-arg}} --output json remove page --graph {{graph-arg}} --page Home"] - :expect {:exit 0 - :stdout-json-paths {[:status] "ok" - [:data :result] true}} - :covers {:commands ["remove page"] - :options {:global ["--config" "--graph" "--data-dir" "--output"] - :remove ["--page"]}} - :cleanup ["{{cli}} --data-dir {{data-dir-arg}} --config {{config-path-arg}} --output json server stop --graph {{graph-arg}}"] - :tags [:remove]} - - {:id "graph-remove-json" - :setup ["{{cli}} --data-dir {{data-dir-arg}} --config {{config-path-arg}} --output json graph create --graph {{graph-arg}} >/dev/null" - "{{cli}} --data-dir {{data-dir-arg}} --config {{config-path-arg}} --output json server stop --graph {{graph-arg}} >/dev/null"] - :cmds ["{{cli}} --data-dir {{data-dir-arg}} --config {{config-path-arg}} --output json graph remove --graph {{graph-arg}}"] - :expect {:exit 1 - :stdout-contains ["graph-not-removed"]} - :covers {:commands ["graph remove"] - :options {:global ["--config" "--graph" "--data-dir" "--output"]}} - :tags [:graph :remove]} - - {:id "remove-block-uuid-human" - :setup ["{{cli}} --data-dir {{data-dir-arg}} --config {{config-path-arg}} --output json graph create --graph {{graph-arg}} >/dev/null" - "{{cli}} --data-dir {{data-dir-arg}} --config {{config-path-arg}} --output json upsert page --graph {{graph-arg}} --page Home >/dev/null" - "{{cli}} --data-dir {{data-dir-arg}} --config {{config-path-arg}} --output json upsert block --graph {{graph-arg}} --target-page Home --content 'Alpha block' >/dev/null"] - :cmds ["{{cli}} --data-dir {{data-dir-arg}} --config {{config-path-arg}} --output human remove block --graph {{graph-arg}} --uuid \"$({{cli}} --data-dir {{data-dir-arg}} --config {{config-path-arg}} --output json query --graph {{graph-arg}} --query '[:find ?uuid . :where [?e :block/title \"Alpha block\"] [?e :block/uuid ?uuid]]' | python3 -c 'import sys,json; print(json.load(sys.stdin)[\"data\"][\"result\"])')\" >/dev/null" - "{{cli}} --data-dir {{data-dir-arg}} --config {{config-path-arg}} --output human show --graph {{graph-arg}} --page Home --linked-references false"] - :expect {:exit 0 - :stdout-not-contains ["Alpha block"]} - :covers {:commands ["remove block"] - :options {:global ["--config" "--graph" "--data-dir" "--output"] - :remove ["--uuid"]}} - :cleanup ["{{cli}} --data-dir {{data-dir-arg}} --config {{config-path-arg}} --output json server stop --graph {{graph-arg}}"] - :tags [:remove]} - - {:id "remove-tag-name-json" - :setup ["{{cli}} --data-dir {{data-dir-arg}} --config {{config-path-arg}} --output json graph create --graph {{graph-arg}} >/dev/null" - "{{cli}} --data-dir {{data-dir-arg}} --config {{config-path-arg}} --output json upsert tag --graph {{graph-arg}} --name TagOne >/dev/null"] - :cmds ["{{cli}} --data-dir {{data-dir-arg}} --config {{config-path-arg}} --output json remove tag --graph {{graph-arg}} --name TagOne >/dev/null" - "{{cli}} --data-dir {{data-dir-arg}} --config {{config-path-arg}} --output json list tag --graph {{graph-arg}} --fields title,id"] - :expect {:exit 0 - :stdout-not-contains ["TagOne"]} - :covers {:commands ["remove tag"] - :options {:global ["--config" "--graph" "--data-dir" "--output"] - :remove ["--name"]}} - :cleanup ["{{cli}} --data-dir {{data-dir-arg}} --config {{config-path-arg}} --output json server stop --graph {{graph-arg}}"] - :tags [:remove]} - - {:id "remove-property-id-json" - :setup ["{{cli}} --data-dir {{data-dir-arg}} --config {{config-path-arg}} --output json graph create --graph {{graph-arg}} >/dev/null" - "{{cli}} --data-dir {{data-dir-arg}} --config {{config-path-arg}} --output json upsert property --graph {{graph-arg}} --name score --type number --cardinality one >/dev/null"] - :cmds ["{{cli}} --data-dir {{data-dir-arg}} --config {{config-path-arg}} --output json remove property --graph {{graph-arg}} --id \"$({{cli}} --data-dir {{data-dir-arg}} --config {{config-path-arg}} --output json list property --graph {{graph-arg}} --fields title,id --limit 200 --sort updated-at --order desc | python3 -c 'import sys,json; print(next(item[\"db/id\"] for item in json.load(sys.stdin)[\"data\"][\"items\"] if item[\"block/title\"] == \"score\"))')\" >/dev/null" - "{{cli}} --data-dir {{data-dir-arg}} --config {{config-path-arg}} --output json list property --graph {{graph-arg}} --fields title,id"] - :expect {:exit 0 - :stdout-not-contains ["score"]} - :covers {:commands ["remove property"] - :options {:global ["--config" "--graph" "--data-dir" "--output"] - :remove ["--id"]}} - :cleanup ["{{cli}} --data-dir {{data-dir-arg}} --config {{config-path-arg}} --output json server stop --graph {{graph-arg}}"] - :tags [:remove]} - - {:id "server-cleanup-json" - :setup ["{{cli}} --data-dir {{data-dir-arg}} --config {{config-path-arg}} --output json graph create --graph {{graph-arg}} >/dev/null"] - :cmds ["{{cli}} --data-dir {{data-dir-arg}} --config {{config-path-arg}} --output json server cleanup"] - :expect {:exit 0 - :stdout-json-paths {[:status] "ok" - [:data :mismatched] 0 - [:data :eligible] 0 - [:data :skipped-owner] 0}} - :covers {:commands ["server cleanup"] - :options {:global ["--config" "--graph" "--data-dir" "--output"] - :server []}} - :cleanup ["{{cli}} --data-dir {{data-dir-arg}} --config {{config-path-arg}} --output json server stop --graph {{graph-arg}}"] - :tags [:server]} - - {:id "server-list-json" - :setup ["{{cli}} --data-dir {{data-dir-arg}} --config {{config-path-arg}} --output json graph create --graph {{graph-arg}} >/dev/null"] - :cmds ["{{cli}} --data-dir {{data-dir-arg}} --config {{config-path-arg}} --output json server list"] - :expect {:exit 0 - :stdout-json-paths {[:status] "ok" - [:data :servers 0 :repo] "{{graph}}"}} - :covers {:commands ["server list"] - :options {:global ["--config" "--data-dir" "--output"]}} - :cleanup ["{{cli}} --data-dir {{data-dir-arg}} --config {{config-path-arg}} --output json server stop --graph {{graph-arg}}"] - :tags [:server]} - - {:id "server-restart-json" - :setup ["{{cli}} --data-dir {{data-dir-arg}} --config {{config-path-arg}} --output json graph create --graph {{graph-arg}} >/dev/null"] - :cmds ["{{cli}} --data-dir {{data-dir-arg}} --config {{config-path-arg}} --output json server restart --graph {{graph-arg}}"] - :expect {:exit 0 - :stdout-json-paths {[:status] "ok" - [:data :repo] "{{graph}}" - [:data :owned?] true}} - :covers {:commands ["server restart"] - :options {:global ["--config" "--graph" "--data-dir" "--output"] - :server ["--graph"]}} - :cleanup ["{{cli}} --data-dir {{data-dir-arg}} --config {{config-path-arg}} --output json server stop --graph {{graph-arg}}"] - :tags [:server]} - - {:id "server-stop-json" - :setup ["{{cli}} --data-dir {{data-dir-arg}} --config {{config-path-arg}} --output json graph create --graph {{graph-arg}} >/dev/null"] - :cmds ["{{cli}} --data-dir {{data-dir-arg}} --config {{config-path-arg}} --output json server stop --graph {{graph-arg}}"] - :expect {:exit 0 - :stdout-json-paths {[:status] "ok" - [:data :repo] "{{graph}}"}} - :covers {:commands ["server stop"] - :options {:global ["--config" "--graph" "--data-dir" "--output"] - :server ["--graph"]}} - :tags [:server]} - - {:id "server-start-json" - :setup ["{{cli}} --data-dir {{data-dir-arg}} --config {{config-path-arg}} --output json graph create --graph {{graph-arg}} >/dev/null" - "{{cli}} --data-dir {{data-dir-arg}} --config {{config-path-arg}} --output json server stop --graph {{graph-arg}} >/dev/null"] - :cmds ["{{cli}} --data-dir {{data-dir-arg}} --config {{config-path-arg}} --output json server start --graph {{graph-arg}}"] - :expect {:exit 0 - :stdout-json-paths {[:status] "ok" - [:data :repo] "{{graph}}" - [:data :owned?] true}} - :covers {:commands ["server start"] - :options {:global ["--config" "--graph" "--data-dir" "--output"] - :server ["--graph"]}} - :cleanup ["{{cli}} --data-dir {{data-dir-arg}} --config {{config-path-arg}} --output json server stop --graph {{graph-arg}}"] - :tags [:server]} - - {:id "skill-show-human" - :cmds ["{{cli}} skill show"] - :expect {:exit 0 - :stdout-contains ["name: logseq-cli" - "# Logseq CLI"]} - :covers {:commands ["skill show"] - :options {:skill []}} - :tags [:skill :smoke]} - - {:id "skill-show-json-still-raw-markdown" - :cmds ["{{cli}} --output json skill show"] - :expect {:exit 0 - :stdout-contains ["name: logseq-cli" - "# Logseq CLI"] - :stdout-not-contains ["\"status\"" "\"data\""]} - :covers {:commands ["skill show"] - :options {:global ["--output"]}} - :tags [:skill]} - - {:id "skill-install-local" - :cmds ["{{cli}} skill install >/dev/null" - "python3 -c 'import pathlib; p=pathlib.Path(\".agents/skills/logseq-cli/SKILL.md\"); print(\"installed\" if p.exists() else \"missing\")'"] - :expect {:exit 0 - :stdout-contains ["installed"]} - :covers {:commands ["skill install"] - :options {:skill []}} - :cleanup ["rm -rf ./.agents/skills/logseq-cli"] - :tags [:skill]} - - {:id "skill-install-global-preserves-other-skills" - :setup ["rm -rf ./tmp/cli-e2e-skill-home" - "mkdir -p ./tmp/cli-e2e-skill-home/.agents/skills/existing-skill" - "python3 -c 'import pathlib; pathlib.Path(\"./tmp/cli-e2e-skill-home/.agents/skills/existing-skill/SKILL.md\").write_text(\"keep\", encoding=\"utf8\")'"] - :cmds ["HOME=\"$(pwd)/tmp/cli-e2e-skill-home\" {{cli}} skill install --global >/dev/null" - "python3 -c 'import pathlib; home=pathlib.Path(\"./tmp/cli-e2e-skill-home\"); existing=(home / \".agents/skills/existing-skill/SKILL.md\").read_text(encoding=\"utf8\"); installed=(home / \".agents/skills/logseq-cli/SKILL.md\").exists(); print(\"ok\" if existing==\"keep\" and installed else \"bad\")'"] - :expect {:exit 0 - :stdout-contains ["ok"]} - :covers {:commands ["skill install"] - :options {:skill ["--global"]}} - :cleanup ["rm -rf ./tmp/cli-e2e-skill-home"] - :tags [:skill]} - - {:id "completion-zsh" - :cmds ["{{cli}} completion zsh"] - :expect {:exit 0 - :stdout-contains ["#compdef logseq" - "Auto-generated by `logseq completion zsh`"]} - :covers {:commands ["completion"] - :options {:completion ["zsh"]}} - :tags [:completion :smoke]} - - {:id "completion-bash-flag" - :cmds ["{{cli}} completion --shell bash"] - :expect {:exit 0 - :stdout-contains ["Auto-generated by `logseq completion bash`" - "_logseq_json_names_bash"]} - :covers {:commands ["completion"] - :options {:completion ["--shell" "bash"]}} - :tags [:completion]} - - {:id "completion-invalid-shell" - :cmds ["{{cli}} completion fish"] - :expect {:exit 1 - :stdout-contains ["unsupported shell: fish"]} - :covers {:commands ["completion"]} - :tags [:completion]} - - {:id "doctor-dev-script-json" - :cmds ["{{cli}} --data-dir {{data-dir-arg}} --config {{config-path-arg}} --output json doctor --dev-script"] - :expect {:exit 0 - :stdout-json-paths {[:status] "ok" - [:data :status] "ok" - [:data :checks 0 :id] "db-worker-script"} - :stdout-contains ["static/db-worker-node.js"]} - :covers {:commands ["doctor"] - :options {:global ["--config" "--data-dir" "--output"] - :doctor ["--dev-script"]}} - :tags [:doctor :smoke]} -] +{:templates + {:non-sync/graph-json-env + {:setup + ["{{cli}} --data-dir {{data-dir-arg}} --config {{config-path-arg}} --output json graph create --graph {{graph-arg}} >/dev/null"], + :cleanup + ["{{cli}} --data-dir {{data-dir-arg}} --config {{config-path-arg}} --output json server stop --graph {{graph-arg}}"]}, + :non-sync/graph-json-secondary-env + {:extends :non-sync/graph-json-env, + :vars + {:secondary-graph "graph-two", :secondary-graph-arg "graph-two"}, + :setup + ["{{cli}} --data-dir {{data-dir-arg}} --config {{config-path-arg}} --output json graph create --graph {{secondary-graph-arg}} >/dev/null"], + :cleanup + ["{{cli}} --data-dir {{data-dir-arg}} --config {{config-path-arg}} --output json server stop --graph {{secondary-graph-arg}}"]}, + :non-sync/source-graph-json-env + {:extends :non-sync/graph-json-secondary-env, + :vars + {:secondary-graph "source-graph", + :secondary-graph-arg "source-graph"}}, + :non-sync/example-command + {:tags [:example], + :covers {:options {:global ["--output"]}}, + :expect {:exit 0}, + :vars {:example-output "json"}, + :cmds + ["{{cli}} --output {{example-output}} example {{example-selector}}"]}, + :non-sync/example-human-command + {:extends :non-sync/example-command, + :vars {:example-output "human"}}, + :non-sync/example-json-command + {:extends :non-sync/example-command, + :expect {:stdout-json-paths {[:status] "ok"}}}, + :non-sync/example-edn-command + {:extends :non-sync/example-command, + :vars {:example-output "edn"}, + :expect {:stdout-edn-paths {[:status] :ok}}}, + :non-sync/completion-command + {:tags [:completion], + :covers {:commands ["completion"]}, + :expect {:exit 0}, + :cmds ["{{cli}} completion {{completion-selector}}"]}}, + :cases + [{:id "global-help", + :cmds ["{{cli}} --help"], + :expect + {:exit 0, + :stdout-contains + ["Usage: logseq [options]" "graph create" "completion"]}, + :covers {:options {:global ["--help"]}}, + :tags [:global :smoke]} + {:id "global-version", + :cmds ["{{cli}} --version"], + :expect {:exit 0, :stdout-contains ["Build time:" "Revision:"]}, + :covers {:options {:global ["--version"]}}, + :tags [:global :smoke]} + {:id "verbose-graph-list-json", + :cmds + ["{{cli}} --data-dir {{data-dir-arg}} --config {{config-path-arg}} --output json --verbose graph list"], + :expect + {:exit 0, + :stderr-contains [":cli/parsed-options"], + :stdout-json-paths {[:status] "ok"}}, + :covers + {:commands ["graph list"], + :options + {:global ["--config" "--data-dir" "--output" "--verbose"]}}, + :tags [:global :graph]} + {:id "graph-create-and-info-json", + :cmds + ["{{cli}} --data-dir {{data-dir-arg}} --config {{config-path-arg}} --output json graph create --graph {{graph-arg}} >/dev/null" + "{{cli}} --data-dir {{data-dir-arg}} --config {{config-path-arg}} --output json graph info --graph {{graph-arg}}"], + :expect + {:exit 0, + :stdout-json-paths {[:status] "ok", [:data :graph] "{{graph}}"}}, + :covers + {:commands ["graph create" "graph info"], + :options {:global ["--config" "--graph" "--data-dir" "--output"]}}, + :cleanup + ["{{cli}} --data-dir {{data-dir-arg}} --config {{config-path-arg}} --output json server stop --graph {{graph-arg}}"], + :tags [:graph :smoke]} + {:id "graph-list-human", + :cmds + ["{{cli}} --data-dir {{data-dir-arg}} --config {{config-path-arg}} --output human graph list"], + :expect {:exit 0, :stdout-contains ["{{graph}}"]}, + :covers + {:commands ["graph list"], + :options {:global ["--config" "--data-dir" "--output"]}}, + :tags [:graph], + :extends :non-sync/graph-json-env} + {:id "graph-list-edn", + :cmds + ["{{cli}} --data-dir {{data-dir-arg}} --config {{config-path-arg}} --output edn graph list"], + :expect + {:exit 0, + :stdout-edn-paths {[:status] :ok, [:data :graphs] ["{{graph}}"]}}, + :covers + {:commands ["graph list"], + :options {:global ["--config" "--data-dir" "--output"]}}, + :tags [:graph], + :extends :non-sync/graph-json-env} + {:tags [:graph], + :extends :non-sync/graph-json-secondary-env, + :expect + {:exit 0, + :stdout-json-paths + {[:status] "ok", + [:data :message] "switched to {{secondary-graph}}"}}, + :cmds + ["{{cli}} --data-dir {{data-dir-arg}} --config {{config-path-arg}} --output json graph switch --graph {{secondary-graph-arg}}"], + :id "graph-switch-json", + :covers + {:commands ["graph switch"], + :options {:global ["--config" "--graph" "--data-dir" "--output"]}}} + {:id "graph-validate-fix-json", + :cmds + ["{{cli}} --data-dir {{data-dir-arg}} --config {{config-path-arg}} --output json graph validate --graph {{graph-arg}} --fix"], + :expect + {:exit 0, + :stdout-json-paths {[:status] "ok", [:data :result :errors] nil}}, + :covers + {:commands ["graph validate"], + :options + {:global ["--config" "--graph" "--data-dir" "--output"], + :graph ["--fix"]}}, + :tags [:graph], + :extends :non-sync/graph-json-env} + {:id "graph-export-json", + :setup + ["{{cli}} --data-dir {{data-dir-arg}} --config {{config-path-arg}} --output json upsert page --graph {{graph-arg}} --page ExportHome >/dev/null"], + :cmds + ["{{cli}} --data-dir {{data-dir-arg}} --config {{config-path-arg}} --output json graph export --graph {{graph-arg}} --type edn --file {{export-path-arg}}"], + :expect + {:exit 0, + :stdout-json-paths {[:status] "ok"}, + :stdout-contains ["{{export-path}}"]}, + :covers + {:commands ["graph export"], + :options + {:global ["--config" "--graph" "--data-dir" "--output"], + :graph ["--type" "--file"]}}, + :tags [:graph], + :extends :non-sync/graph-json-env} + {:tags [:graph], + :extends :non-sync/source-graph-json-env, + :expect + {:exit 0, + :stdout-json-paths {[:status] "ok"}, + :stdout-contains ["Imported edn from" "{{export-path}}"]}, + :setup + ["{{cli}} --data-dir {{data-dir-arg}} --config {{config-path-arg}} --output json upsert page --graph {{secondary-graph-arg}} --page ImportSeed >/dev/null" + "{{cli}} --data-dir {{data-dir-arg}} --config {{config-path-arg}} --output json graph export --graph {{secondary-graph-arg}} --type edn --file {{export-path-arg}} >/dev/null"], + :cmds + ["{{cli}} --data-dir {{data-dir-arg}} --config {{config-path-arg}} --output json graph import --graph {{graph-arg}} --type edn --input {{export-path-arg}}"], + :id "graph-import-json", + :covers + {:commands ["graph import"], + :options + {:global ["--config" "--graph" "--data-dir" "--output"], + :graph ["--type" "--input"]}}} + {:tags [:graph], + :extends :non-sync/graph-json-env, + :expect {:exit 0, :stdout-json-paths {[:status] "ok"}}, + :setup + ["{{cli}} --data-dir {{data-dir-arg}} --config {{config-path-arg}} --output json upsert page --graph {{graph-arg}} --page BackupSeed >/dev/null"], + :cmds + ["{{cli}} --data-dir {{data-dir-arg}} --config {{config-path-arg}} --output json graph backup create --graph {{graph-arg}} --name nightly >/dev/null" + "{{cli}} --data-dir {{data-dir-arg}} --config {{config-path-arg}} --output json graph backup list" + "{{cli}} --data-dir {{data-dir-arg}} --config {{config-path-arg}} --output json graph backup restore --src \"$({{cli}} --data-dir {{data-dir-arg}} --config {{config-path-arg}} --output json graph backup list | python3 -c 'import sys,json; print(json.load(sys.stdin)[\"data\"][\"backups\"][0][\"name\"])')\" --dst {{restore-graph-arg}} >/dev/null" + "{{cli}} --data-dir {{data-dir-arg}} --config {{config-path-arg}} --output json graph backup remove --src \"$({{cli}} --data-dir {{data-dir-arg}} --config {{config-path-arg}} --output json graph backup list | python3 -c 'import sys,json; print(json.load(sys.stdin)[\"data\"][\"backups\"][0][\"name\"])')\""], + :id "graph-backup-lifecycle-json", + :covers + {:commands + ["graph backup create" + "graph backup list" + "graph backup restore" + "graph backup remove"], + :options + {:global ["--config" "--graph" "--data-dir" "--output"], + :graph ["--name" "--src" "--dst"]}}, + :vars + {:restore-graph "backup-restore", + :restore-graph-arg "backup-restore"}, + :cleanup + ["{{cli}} --data-dir {{data-dir-arg}} --config {{config-path-arg}} --output json server stop --graph {{restore-graph-arg}}"]} + {:id "page-upsert-and-list-json", + :cmds + ["{{cli}} --data-dir {{data-dir-arg}} --config {{config-path-arg}} --output json upsert page --graph {{graph-arg}} --page Home >/dev/null" + "{{cli}} --data-dir {{data-dir-arg}} --config {{config-path-arg}} --output json list page --graph {{graph-arg}} --fields title,id --sort updated-at --order desc --limit 1"], + :expect + {:exit 0, + :stdout-json-paths + {[:status] "ok", [:data :items 0 :block/title] "Home"}}, + :covers + {:commands ["upsert page" "list page"], + :options + {:global ["--config" "--graph" "--data-dir" "--output"], + :upsert ["--page"], + :list ["--fields" "--limit" "--sort" "--order"]}}, + :tags [:upsert :list :smoke], + :extends :non-sync/graph-json-env} + {:id "task-upsert-and-list-json", + :cmds + ["{{cli}} --data-dir {{data-dir-arg}} --config {{config-path-arg}} --output json upsert task --graph {{graph-arg}} --page TaskHome --status todo --priority high --scheduled '2026-02-10T08:00:00.000Z' --deadline '2026-02-12T18:00:00.000Z' >/dev/null" + "{{cli}} --data-dir {{data-dir-arg}} --config {{config-path-arg}} --output json list task --graph {{graph-arg}} --status todo --priority high --content task --fields id,title,status,priority,scheduled,deadline --sort updated-at --order desc --limit 1"], + :expect + {:exit 0, + :stdout-json-paths + {[:status] "ok", + [:data :items 0 :block/title] "TaskHome", + [:data :items 0 :logseq.property/status] + "logseq.property/status.todo", + [:data :items 0 :logseq.property/priority] + "logseq.property/priority.high", + [:data :items 0 :logseq.property/scheduled] 1770710400000, + [:data :items 0 :logseq.property/deadline] 1770919200000}}, + :covers + {:commands ["upsert task" "list task"], + :options + {:global ["--config" "--graph" "--data-dir" "--output"], + :upsert + ["--page" "--status" "--priority" "--scheduled" "--deadline"], + :list + ["--status" + "--priority" + "--content" + "--fields" + "--limit" + "--sort" + "--order"]}}, + :tags [:upsert :list :smoke], + :extends :non-sync/graph-json-env} + {:id "task-upsert-clear-properties-json", + :setup + ["{{cli}} --data-dir {{data-dir-arg}} --config {{config-path-arg}} --output json upsert task --graph {{graph-arg}} --page TaskHome --status todo --priority high --scheduled '2026-02-10T08:00:00.000Z' --deadline '2026-02-12T18:00:00.000Z' >/dev/null"], + :cmds + ["{{cli}} --data-dir {{data-dir-arg}} --config {{config-path-arg}} --output json upsert task --graph {{graph-arg}} --page TaskHome --no-status --no-priority --no-scheduled --no-deadline >/dev/null" + "{{cli}} --data-dir {{data-dir-arg}} --config {{config-path-arg}} --output json list task --graph {{graph-arg}} --content taskhome --fields id,title,status,priority,scheduled,deadline --sort updated-at --order desc --limit 1"], + :expect + {:exit 0, + :stdout-json-paths + {[:status] "ok", [:data :items 0 :block/title] "TaskHome"}, + :stdout-not-contains + ["logseq.property/priority.high" + "2026-02-10T08:00:00.000Z" + "2026-02-12T18:00:00.000Z"]}, + :covers + {:commands ["upsert task" "list task"], + :options + {:global ["--config" "--graph" "--data-dir" "--output"], + :upsert + ["--page" + "--no-status" + "--no-priority" + "--no-scheduled" + "--no-deadline"], + :list ["--content" "--fields" "--limit" "--sort" "--order"]}}, + :tags [:upsert :list], + :extends :non-sync/graph-json-env} + {:id "task-upsert-set-clear-conflict-json", + :cmds + ["{{cli}} --data-dir {{data-dir-arg}} --config {{config-path-arg}} --output json upsert task --graph {{graph-arg}} --page TaskHome --status todo --no-status"], + :expect + {:exit 1, + :stdout-contains + ["invalid-options" + "--status and --no-status are mutually exclusive"]}, + :covers + {:commands ["upsert task"], + :options + {:global ["--config" "--graph" "--data-dir" "--output"], + :upsert ["--page" "--status" "--no-status"]}}, + :tags [:upsert], + :extends :non-sync/graph-json-env} + {:id "node-list-by-tags-properties-json", + :cmds + ["{{cli}} --data-dir {{data-dir-arg}} --config {{config-path-arg}} --output json upsert tag --graph {{graph-arg}} --name NodeTag >/dev/null" + "{{cli}} --data-dir {{data-dir-arg}} --config {{config-path-arg}} --output json upsert property --graph {{graph-arg}} --name node-prop >/dev/null" + "{{cli}} --data-dir {{data-dir-arg}} --config {{config-path-arg}} --output json upsert page --graph {{graph-arg}} --page NodeHome --update-tags '[\"NodeTag\"]' --update-properties '{:node-prop \"v\"}' >/dev/null" + "{{cli}} --data-dir {{data-dir-arg}} --config {{config-path-arg}} --output json list node --graph {{graph-arg}} --tags NodeTag --properties node-prop --fields id,title,type --sort updated-at --order desc --limit 20"], + :expect + {:exit 0, + :stdout-json-paths + {[:status] "ok", + [:data :items 0 :block/title] "NodeHome", + [:data :items 0 :node/type] "page"}}, + :covers + {:commands + ["upsert tag" "upsert property" "upsert page" "list node"], + :options + {:global ["--config" "--graph" "--data-dir" "--output"], + :upsert ["--name" "--page" "--update-tags" "--update-properties"], + :list + ["--tags" + "--properties" + "--fields" + "--limit" + "--sort" + "--order"]}}, + :tags [:upsert :list], + :extends :non-sync/graph-json-env} + {:id "asset-upsert-and-list-json", + :setup ["printf 'asset-content' > {{export-path-arg}}.png"], + :cmds + ["{{cli}} --data-dir {{data-dir-arg}} --config {{config-path-arg}} --output json upsert asset --graph {{graph-arg}} --path {{export-path-arg}}.png --content 'Asset One' --target-page Home >/dev/null" + "{{cli}} --data-dir {{data-dir-arg}} --config {{config-path-arg}} --output json list asset --graph {{graph-arg}} --fields id,title,asset-type,size --sort updated-at --order desc --limit 1"], + :expect + {:exit 0, + :stdout-json-paths + {[:status] "ok", + [:data :items 0 :block/title] "Asset One", + [:data :items 0 :logseq.property.asset/type] "png", + [:data :items 0 :logseq.property.asset/size] 13}}, + :covers + {:commands ["upsert asset" "list asset"], + :options + {:global ["--config" "--graph" "--data-dir" "--output"], + :upsert ["--path" "--content" "--target-page"], + :list ["--fields" "--limit" "--sort" "--order"]}}, + :tags [:upsert :list], + :extends :non-sync/graph-json-env} + {:id "asset-upsert-update-json", + :setup + ["printf 'asset-content' > {{export-path-arg}}.png" + "{{cli}} --data-dir {{data-dir-arg}} --config {{config-path-arg}} --output json upsert asset --graph {{graph-arg}} --path {{export-path-arg}}.png --content 'Asset Original' --target-page Home >/dev/null"], + :cmds + ["{{cli}} --data-dir {{data-dir-arg}} --config {{config-path-arg}} --output json upsert asset --graph {{graph-arg}} --id \"$({{cli}} --data-dir {{data-dir-arg}} --config {{config-path-arg}} --output json query --graph {{graph-arg}} --query '[:find ?e . :where [?e :block/title \"Asset Original\"]]' | python3 -c 'import sys,json; print(json.load(sys.stdin)[\"data\"][\"result\"])')\" --content 'Asset Updated' >/dev/null" + "{{cli}} --data-dir {{data-dir-arg}} --config {{config-path-arg}} --output json list asset --graph {{graph-arg}} --fields id,title --sort updated-at --order desc --limit 1"], + :expect + {:exit 0, + :stdout-json-paths + {[:status] "ok", [:data :items 0 :block/title] "Asset Updated"}}, + :covers + {:commands ["upsert asset" "list asset"], + :options + {:global ["--config" "--graph" "--data-dir" "--output"], + :upsert ["--id" "--content"], + :list ["--fields" "--limit" "--sort" "--order"]}}, + :tags [:upsert :list], + :extends :non-sync/graph-json-env} + {:id "block-upsert-and-show-json", + :setup + ["{{cli}} --data-dir {{data-dir-arg}} --config {{config-path-arg}} --output json upsert page --graph {{graph-arg}} --page Home >/dev/null"], + :cmds + ["{{cli}} --data-dir {{data-dir-arg}} --config {{config-path-arg}} --output json upsert block --graph {{graph-arg}} --target-page Home --content 'Alpha block' >/dev/null" + "{{cli}} --data-dir {{data-dir-arg}} --config {{config-path-arg}} --output json show --graph {{graph-arg}} --page Home --level 2 --linked-references false"], + :expect + {:exit 0, + :stdout-json-paths + {[:status] "ok", + [:data :root :block/children 0 :block/title] "Alpha block"}}, + :covers + {:commands ["upsert block" "show"], + :options + {:global ["--config" "--graph" "--data-dir" "--output"], + :upsert ["--target-page" "--content"], + :show ["--page" "--level" "--linked-references"]}}, + :tags [:upsert :show :smoke], + :extends :non-sync/graph-json-env} + {:id "tag-upsert-and-list-json", + :cmds + ["{{cli}} --data-dir {{data-dir-arg}} --config {{config-path-arg}} --output json upsert tag --graph {{graph-arg}} --name TagOne >/dev/null" + "{{cli}} --data-dir {{data-dir-arg}} --config {{config-path-arg}} --output json list tag --graph {{graph-arg}} --with-properties --sort updated-at --order desc --limit 1"], + :expect + {:exit 0, + :stdout-json-paths + {[:status] "ok", [:data :items 0 :block/title] "TagOne"}}, + :covers + {:commands ["upsert tag" "list tag"], + :options + {:global ["--config" "--graph" "--data-dir" "--output"], + :upsert ["--name"], + :list ["--with-properties" "--sort" "--order" "--limit"]}}, + :tags [:upsert :list], + :extends :non-sync/graph-json-env} + {:id "property-upsert-and-list-json", + :cmds + ["{{cli}} --data-dir {{data-dir-arg}} --config {{config-path-arg}} --output json upsert property --graph {{graph-arg}} --name score --type number --cardinality one >/dev/null" + "{{cli}} --data-dir {{data-dir-arg}} --config {{config-path-arg}} --output json list property --graph {{graph-arg}} --with-type --sort updated-at --order desc --limit 1"], + :expect + {:exit 0, + :stdout-json-paths + {[:status] "ok", + [:data :items 0 :block/title] "score", + [:data :items 0 :logseq.property/type] "number", + [:data :items 0 :db/cardinality] "db.cardinality/one"}}, + :covers + {:commands ["upsert property" "list property"], + :options + {:global ["--config" "--graph" "--data-dir" "--output"], + :upsert ["--name" "--type" "--cardinality"], + :list ["--with-type" "--sort" "--order" "--limit"]}}, + :tags [:upsert :list], + :extends :non-sync/graph-json-env} + {:id "list-tag-expand-offset-json", + :setup + ["{{cli}} --data-dir {{data-dir-arg}} --config {{config-path-arg}} --output json upsert tag --graph {{graph-arg}} --name TagA >/dev/null" + "{{cli}} --data-dir {{data-dir-arg}} --config {{config-path-arg}} --output json upsert tag --graph {{graph-arg}} --name TagB >/dev/null"], + :cmds + ["{{cli}} --data-dir {{data-dir-arg}} --config {{config-path-arg}} --output json list tag --graph {{graph-arg}} --expand --with-extends --sort updated-at --order desc --offset 0 --limit 20"], + :expect + {:exit 0, + :stdout-json-paths {[:status] "ok"}, + :stdout-contains ["TagA" "TagB"]}, + :covers + {:commands ["list tag"], + :options + {:global ["--config" "--graph" "--data-dir" "--output"], + :list + ["--expand" + "--with-extends" + "--offset" + "--sort" + "--order" + "--limit"]}}, + :tags [:list], + :extends :non-sync/graph-json-env} + {:id "list-property-classes-json", + :setup + ["{{cli}} --data-dir {{data-dir-arg}} --config {{config-path-arg}} --output json upsert property --graph {{graph-arg}} --name alpha --type number --cardinality one >/dev/null" + "{{cli}} --data-dir {{data-dir-arg}} --config {{config-path-arg}} --output json upsert property --graph {{graph-arg}} --name beta --type number --cardinality one >/dev/null"], + :cmds + ["{{cli}} --data-dir {{data-dir-arg}} --config {{config-path-arg}} --output json list property --graph {{graph-arg}} --with-classes --sort updated-at --order desc --offset 0 --limit 20"], + :expect + {:exit 0, + :stdout-json-paths {[:status] "ok"}, + :stdout-contains ["alpha" "beta"]}, + :covers + {:commands ["list property"], + :options + {:global ["--config" "--graph" "--data-dir" "--output"], + :list + ["--with-classes" "--sort" "--order" "--offset" "--limit"]}}, + :tags [:list], + :extends :non-sync/graph-json-env} + {:id "list-page-journal-only-json", + :cmds + ["{{cli}} --data-dir {{data-dir-arg}} --config {{config-path-arg}} --output json list page --graph {{graph-arg}} --journal-only"], + :expect + {:exit 0, :stdout-json-paths {[:status] "ok", [:data :items] []}}, + :covers + {:commands ["list page"], + :options + {:global ["--config" "--graph" "--data-dir" "--output"], + :list ["--journal-only"]}}, + :tags [:list], + :extends :non-sync/graph-json-env} + {:id "search-block-json", + :setup + ["{{cli}} --data-dir {{data-dir-arg}} --config {{config-path-arg}} --output json upsert page --graph {{graph-arg}} --page SearchBlockTarget >/dev/null"], + :cmds + ["{{cli}} --data-dir {{data-dir-arg}} --config {{config-path-arg}} search block --content blocktarget --output json --graph {{graph-arg}}"], + :expect + {:exit 0, + :stdout-json-paths {[:status] "ok"}, + :stdout-contains ["SearchBlockTarget"]}, + :covers + {:commands ["search block"], + :options + {:global ["--config" "--graph" "--data-dir" "--output"], + :search ["--content"]}}, + :tags [:search :smoke], + :extends :non-sync/graph-json-env} + {:id "search-page-json", + :setup + ["{{cli}} --data-dir {{data-dir-arg}} --config {{config-path-arg}} --output json upsert page --graph {{graph-arg}} --page SearchPageTarget >/dev/null"], + :cmds + ["{{cli}} --data-dir {{data-dir-arg}} --config {{config-path-arg}} --output json search page --content target --graph {{graph-arg}}"], + :expect + {:exit 0, + :stdout-json-paths {[:status] "ok"}, + :stdout-contains ["SearchPageTarget"]}, + :covers + {:commands ["search page"], + :options + {:global ["--config" "--graph" "--data-dir" "--output"], + :search ["--content"]}}, + :tags [:search], + :extends :non-sync/graph-json-env} + {:id "search-property-json", + :setup + ["{{cli}} --data-dir {{data-dir-arg}} --config {{config-path-arg}} --output json upsert property --graph {{graph-arg}} --name SearchOwner --type default >/dev/null"], + :cmds + ["{{cli}} --data-dir {{data-dir-arg}} --config {{config-path-arg}} --output json search property --content owner --graph {{graph-arg}}"], + :expect + {:exit 0, + :stdout-json-paths {[:status] "ok"}, + :stdout-contains ["SearchOwner"]}, + :covers + {:commands ["search property"], + :options + {:global ["--config" "--graph" "--data-dir" "--output"], + :search ["--content"]}}, + :tags [:search], + :extends :non-sync/graph-json-env} + {:id "search-tag-json", + :setup + ["{{cli}} --data-dir {{data-dir-arg}} --config {{config-path-arg}} --output json upsert tag --graph {{graph-arg}} --name SearchTagTarget >/dev/null"], + :cmds + ["{{cli}} --data-dir {{data-dir-arg}} --config {{config-path-arg}} --output json search tag --content target --graph {{graph-arg}}"], + :expect + {:exit 0, + :stdout-json-paths {[:status] "ok"}, + :stdout-contains ["SearchTagTarget"]}, + :covers + {:commands ["search tag"], + :options + {:global ["--config" "--graph" "--data-dir" "--output"], + :search ["--content"]}}, + :tags [:search], + :extends :non-sync/graph-json-env} + {:id "block-upsert-blocks-file-json", + :setup + ["{{cli}} --data-dir {{data-dir-arg}} --config {{config-path-arg}} --output json upsert page --graph {{graph-arg}} --page Home >/dev/null" + "{{cli}} --data-dir {{data-dir-arg}} --config {{config-path-arg}} --output json upsert block --graph {{graph-arg}} --target-page Home --content 'Anchor block' >/dev/null" + "printf '[{:block/title \"Inserted from file\"}]' > {{export-path-arg}}"], + :cmds + ["{{cli}} --data-dir {{data-dir-arg}} --config {{config-path-arg}} --output human upsert block --graph {{graph-arg}} --target-id \"$({{cli}} --data-dir {{data-dir-arg}} --config {{config-path-arg}} --output json query --graph {{graph-arg}} --query '[:find ?e . :where [?e :block/title \"Anchor block\"]]' | python3 -c 'import sys,json; print(json.load(sys.stdin)[\"data\"][\"result\"])')\" --blocks-file {{export-path-arg}} --pos sibling >/dev/null" + "{{cli}} --data-dir {{data-dir-arg}} --config {{config-path-arg}} --output human show --graph {{graph-arg}} --page Home"], + :expect {:exit 0, :stdout-contains ["Inserted from file"]}, + :covers + {:commands ["upsert block"], + :options + {:global ["--config" "--graph" "--data-dir" "--output"], + :upsert ["--blocks-file" "--target-id" "--pos"]}}, + :tags [:upsert], + :extends :non-sync/graph-json-env} + {:id "block-upsert-target-uuid-json", + :setup + ["{{cli}} --data-dir {{data-dir-arg}} --config {{config-path-arg}} --output json upsert page --graph {{graph-arg}} --page Home >/dev/null" + "{{cli}} --data-dir {{data-dir-arg}} --config {{config-path-arg}} --output json upsert block --graph {{graph-arg}} --target-page Home --content 'Anchor block' >/dev/null"], + :cmds + ["{{cli}} --data-dir {{data-dir-arg}} --config {{config-path-arg}} --output human upsert block --graph {{graph-arg}} --target-uuid \"$({{cli}} --data-dir {{data-dir-arg}} --config {{config-path-arg}} --output json query --graph {{graph-arg}} --query '[:find ?uuid . :where [?e :block/title \"Anchor block\"] [?e :block/uuid ?uuid]]' | python3 -c 'import sys,json; print(json.load(sys.stdin)[\"data\"][\"result\"])')\" --content 'Inserted by uuid' >/dev/null" + "{{cli}} --data-dir {{data-dir-arg}} --config {{config-path-arg}} --output human show --graph {{graph-arg}} --page Home"], + :expect {:exit 0, :stdout-contains ["Inserted by uuid"]}, + :covers + {:commands ["upsert block"], + :options + {:global ["--config" "--graph" "--data-dir" "--output"], + :upsert ["--target-uuid"]}}, + :tags [:upsert], + :extends :non-sync/graph-json-env} + {:id "block-upsert-update-id-json", + :setup + ["{{cli}} --data-dir {{data-dir-arg}} --config {{config-path-arg}} --output json upsert page --graph {{graph-arg}} --page Home >/dev/null" + "{{cli}} --data-dir {{data-dir-arg}} --config {{config-path-arg}} --output json upsert block --graph {{graph-arg}} --target-page Home --content 'Alpha block' >/dev/null"], + :cmds + ["{{cli}} --data-dir {{data-dir-arg}} --config {{config-path-arg}} --output human upsert block --graph {{graph-arg}} --id \"$({{cli}} --data-dir {{data-dir-arg}} --config {{config-path-arg}} --output json query --graph {{graph-arg}} --query '[:find ?e . :where [?e :block/title \"Alpha block\"]]' | python3 -c 'import sys,json; print(json.load(sys.stdin)[\"data\"][\"result\"])')\" --content 'Updated by id' --update-tags '[:logseq.class/Quote-block]' --update-properties '{:logseq.property/publishing-public? true}' >/dev/null" + "{{cli}} --data-dir {{data-dir-arg}} --config {{config-path-arg}} --output human show --graph {{graph-arg}} --page Home"], + :expect {:exit 0, :stdout-contains ["Updated by id"]}, + :covers + {:commands ["upsert block"], + :options + {:global ["--config" "--graph" "--data-dir" "--output"], + :upsert ["--id" "--update-tags" "--update-properties"]}}, + :tags [:upsert], + :extends :non-sync/graph-json-env} + {:id "block-upsert-update-id-custom-many-property-json", + :vars {:prop-name "Reproducible steps"}, + :setup + ["{{cli}} --data-dir {{data-dir-arg}} --config {{config-path-arg}} --output json graph create --graph {{graph-arg}} >/dev/null" + "{{cli}} --data-dir {{data-dir-arg}} --config {{config-path-arg}} --output json upsert page --graph {{graph-arg}} --page Home >/dev/null" + "{{cli}} --data-dir {{data-dir-arg}} --config {{config-path-arg}} --output json upsert property --graph {{graph-arg}} --name '{{prop-name}}' --type default --cardinality many --public true >/dev/null"], + :cmds + ["{{cli}} --data-dir {{data-dir-arg}} --config {{config-path-arg}} --output json upsert block --graph {{graph-arg}} --target-page Home --content 'Alpha block' >/dev/null" + "{{cli}} --data-dir {{data-dir-arg}} --config {{config-path-arg}} --output json upsert block --graph {{graph-arg}} --id \"$({{cli}} --data-dir {{data-dir-arg}} --config {{config-path-arg}} --output json query --graph {{graph-arg}} --query '[:find ?e . :where [?e :block/title \"Alpha block\"]]' | python3 -c 'import sys,json; print(json.load(sys.stdin)[\"data\"][\"result\"])')\" --update-properties '{\"{{prop-name}}\" [\"Step 1\" \"Step 2\" \"Step 3\"]}' >/dev/null" + "{{cli}} --data-dir {{data-dir-arg}} --config {{config-path-arg}} --output human show --graph {{graph-arg}} --id \"$({{cli}} --data-dir {{data-dir-arg}} --config {{config-path-arg}} --output json query --graph {{graph-arg}} --query '[:find ?e . :where [?e :block/title \"Alpha block\"]]' | python3 -c 'import sys,json; print(json.load(sys.stdin)[\"data\"][\"result\"])')\""], + :expect + {:exit 0, + :stdout-contains ["{{prop-name}}" "Step 1" "Step 2" "Step 3"]}, + :covers + {:commands ["upsert block" "show"], + :options + {:global ["--config" "--graph" "--data-dir" "--output"], + :upsert + ["--id" "--target-page" "--content" "--update-properties"], + :show ["--id"]}}, + :tags [:upsert :show]} + {:id "block-upsert-update-uuid-json", + :setup + ["{{cli}} --data-dir {{data-dir-arg}} --config {{config-path-arg}} --output json upsert page --graph {{graph-arg}} --page Home >/dev/null" + "{{cli}} --data-dir {{data-dir-arg}} --config {{config-path-arg}} --output json upsert block --graph {{graph-arg}} --target-page Home --content 'Alpha block' --update-tags '[:logseq.class/Quote-block]' --update-properties '{:logseq.property/publishing-public? true}' >/dev/null"], + :cmds + ["{{cli}} --data-dir {{data-dir-arg}} --config {{config-path-arg}} --output human upsert block --graph {{graph-arg}} --uuid \"$({{cli}} --data-dir {{data-dir-arg}} --config {{config-path-arg}} --output json query --graph {{graph-arg}} --query '[:find ?uuid . :where [?e :block/title \"Alpha block\"] [?e :block/uuid ?uuid]]' | python3 -c 'import sys,json; print(json.load(sys.stdin)[\"data\"][\"result\"])')\" --content 'Updated by uuid' --remove-tags '[:logseq.class/Quote-block]' --remove-properties '[:logseq.property/publishing-public?]' >/dev/null" + "{{cli}} --data-dir {{data-dir-arg}} --config {{config-path-arg}} --output human show --graph {{graph-arg}} --page Home"], + :expect {:exit 0, :stdout-contains ["Updated by uuid"]}, + :covers + {:commands ["upsert block"], + :options + {:global ["--config" "--graph" "--data-dir" "--output"], + :upsert ["--uuid" "--remove-tags" "--remove-properties"]}}, + :tags [:upsert], + :extends :non-sync/graph-json-env} + {:id "property-upsert-update-id-json", + :setup + ["{{cli}} --data-dir {{data-dir-arg}} --config {{config-path-arg}} --output json upsert property --graph {{graph-arg}} --name score --type number --cardinality one >/dev/null"], + :cmds + ["{{cli}} --data-dir {{data-dir-arg}} --config {{config-path-arg}} --output json upsert property --graph {{graph-arg}} --id \"$({{cli}} --data-dir {{data-dir-arg}} --config {{config-path-arg}} --output json list property --graph {{graph-arg}} --fields title,id --limit 200 --sort updated-at --order desc | python3 -c 'import sys,json; print(next(item[\"db/id\"] for item in json.load(sys.stdin)[\"data\"][\"items\"] if item[\"block/title\"] == \"score\"))')\" --hide true --public false"], + :expect {:exit 0, :stdout-json-paths {[:status] "ok"}}, + :covers + {:commands ["upsert property"], + :options + {:global ["--config" "--graph" "--data-dir" "--output"], + :upsert ["--id" "--hide" "--public"]}}, + :tags [:upsert], + :extends :non-sync/graph-json-env} + {:id "query-custom-json", + :setup + ["{{cli}} --data-dir {{data-dir-arg}} --config {{config-path-arg}} --output json upsert page --graph {{graph-arg}} --page Home >/dev/null" + "{{cli}} --data-dir {{data-dir-arg}} --config {{config-path-arg}} --output json upsert block --graph {{graph-arg}} --target-page Home --content 'Alpha block' >/dev/null"], + :cmds + ["{{cli}} --data-dir {{data-dir-arg}} --config {{config-path-arg}} --output json query --graph {{graph-arg}} --query '[:find ?title . :where [?b :block/title ?title] [(= ?title \"Alpha block\")]]'"], + :expect + {:exit 0, + :stdout-json-paths + {[:status] "ok", [:data :result] "Alpha block"}}, + :covers + {:commands ["query"], + :options + {:global ["--config" "--graph" "--data-dir" "--output"], + :query ["--query"]}}, + :tags [:query], + :extends :non-sync/graph-json-env} + {:id "query-named-json", + :cmds + ["{{cli}} --data-dir {{data-dir-arg}} --config {{config-path-arg}} --output json query --graph {{graph-arg}} --name recent-updated --inputs '[1]'"], + :expect {:exit 0, :stdout-json-paths {[:status] "ok"}}, + :covers + {:commands ["query"], + :options + {:global ["--config" "--graph" "--data-dir" "--output"], + :query ["--name" "--inputs"]}}, + :tags [:query], + :extends :non-sync/graph-json-env} + {:id "query-list-json", + :cmds + ["{{cli}} --data-dir {{data-dir-arg}} --config {{config-path-arg}} --output json query list --graph {{graph-arg}}"], + :expect + {:exit 0, + :stdout-json-paths + {[:status] "ok", [:data :queries 0 :name] "list-priority"}}, + :covers + {:commands ["query list"], + :options {:global ["--config" "--graph" "--data-dir" "--output"]}}, + :tags [:query], + :extends :non-sync/graph-json-env} + {:id "example-show-human", + :expect + {:exit 0, + :stdout-contains + ["Selector: show" "Matched commands:" "show" "Examples:"]}, + :covers {:commands ["example show"]}, + :tags [:example], + :extends :non-sync/example-human-command, + :vars {:example-selector "show"}} + {:id "example-upsert-page-json", + :expect + {:exit 0, + :stdout-json-paths + {[:data :selector] "upsert page", + [:data :matched-commands 0] "upsert page"}}, + :covers {:commands ["example upsert page"]}, + :tags [:example], + :extends :non-sync/example-json-command, + :vars {:example-selector "upsert page"}} + {:id "example-upsert-prefix-json", + :expect + {:exit 0, + :stdout-json-paths + {[:data :selector] "upsert", + [:data :matched-commands 0] "upsert block"}}, + :covers {:commands ["example upsert"]}, + :tags [:example], + :extends :non-sync/example-json-command, + :vars {:example-selector "upsert"}} + {:id "example-list-prefix-json", + :expect + {:exit 0, + :stdout-json-paths + {[:data :selector] "list", + [:data :matched-commands 0] "list page"}}, + :covers {:commands ["example list"]}, + :tags [:example], + :extends :non-sync/example-json-command, + :vars {:example-selector "list"}} + {:id "example-list-page-json", + :expect + {:exit 0, + :stdout-json-paths + {[:data :selector] "list page", + [:data :matched-commands] ["list page"]}}, + :covers {:commands ["example list page"]}, + :tags [:example], + :extends :non-sync/example-json-command, + :vars {:example-selector "list page"}} + {:id "example-query-prefix-json", + :expect + {:exit 0, + :stdout-json-paths + {[:data :selector] "query", [:data :matched-commands 0] "query"}}, + :covers {:commands ["example query"]}, + :tags [:example], + :extends :non-sync/example-json-command, + :vars {:example-selector "query"}} + {:id "example-query-list-json", + :expect + {:exit 0, + :stdout-json-paths + {[:data :selector] "query list", + [:data :matched-commands] ["query list"]}}, + :covers {:commands ["example query list"]}, + :tags [:example], + :extends :non-sync/example-json-command, + :vars {:example-selector "query list"}} + {:id "example-remove-prefix-json", + :expect + {:exit 0, + :stdout-json-paths + {[:data :selector] "remove", + [:data :matched-commands 0] "remove block"}}, + :covers {:commands ["example remove"]}, + :tags [:example], + :extends :non-sync/example-json-command, + :vars {:example-selector "remove"}} + {:id "example-remove-page-json", + :expect + {:exit 0, + :stdout-json-paths + {[:data :selector] "remove page", + [:data :matched-commands] ["remove page"]}}, + :covers {:commands ["example remove page"]}, + :tags [:example], + :extends :non-sync/example-json-command, + :vars {:example-selector "remove page"}} + {:id "example-search-prefix-json", + :expect + {:exit 0, + :stdout-json-paths + {[:data :selector] "search", + [:data :matched-commands 0] "search block"}}, + :covers {:commands ["example search"]}, + :tags [:example], + :extends :non-sync/example-json-command, + :vars {:example-selector "search"}} + {:id "example-search-block-edn", + :expect + {:exit 0, + :stdout-edn-paths + {[:data :selector] "search block", + [:data :matched-commands] ["search block"]}}, + :covers {:commands ["example search block"]}, + :tags [:example], + :extends :non-sync/example-edn-command, + :vars {:example-selector "search block"}} + {:id "show-id-human", + :setup + ["{{cli}} --data-dir {{data-dir-arg}} --config {{config-path-arg}} --output json upsert page --graph {{graph-arg}} --page Home >/dev/null" + "{{cli}} --data-dir {{data-dir-arg}} --config {{config-path-arg}} --output json upsert block --graph {{graph-arg}} --target-page Home --content 'Alpha block' >/dev/null"], + :cmds + ["{{cli}} --data-dir {{data-dir-arg}} --config {{config-path-arg}} --graph {{graph-arg}} --output human show --id \"$({{cli}} --data-dir {{data-dir-arg}} --config {{config-path-arg}} --graph {{graph-arg}} --output human query --query '[:find ?e . :where [?e :block/title \"Alpha block\"]]')\""], + :expect {:exit 0, :stdout-contains ["Alpha block"]}, + :covers + {:commands ["show"], + :options + {:global ["--config" "--graph" "--data-dir" "--output"], + :show ["--id"]}}, + :tags [:show], + :extends :non-sync/graph-json-env} + {:id "show-uuid-human", + :setup + ["{{cli}} --data-dir {{data-dir-arg}} --config {{config-path-arg}} --output json upsert page --graph {{graph-arg}} --page Home >/dev/null" + "{{cli}} --data-dir {{data-dir-arg}} --config {{config-path-arg}} --output json upsert block --graph {{graph-arg}} --target-page Home --content 'Alpha block' >/dev/null"], + :cmds + ["{{cli}} --data-dir {{data-dir-arg}} --config {{config-path-arg}} --graph {{graph-arg}} --output human show --uuid \"$({{cli}} --data-dir {{data-dir-arg}} --config {{config-path-arg}} --graph {{graph-arg}} --output json query --query '[:find ?uuid . :where [?e :block/title \"Alpha block\"] [?e :block/uuid ?uuid]]' | python3 -c 'import sys,json; print(json.load(sys.stdin)[\"data\"][\"result\"])')\""], + :expect {:exit 0, :stdout-contains ["Alpha block"]}, + :covers + {:commands ["show"], + :options + {:global ["--config" "--graph" "--data-dir" "--output"], + :show ["--uuid"]}}, + :tags [:show], + :extends :non-sync/graph-json-env} + {:id "show-stdin-id-human", + :setup + ["{{cli}} --data-dir {{data-dir-arg}} --config {{config-path-arg}} --output json upsert page --graph {{graph-arg}} --page Home >/dev/null" + "{{cli}} --data-dir {{data-dir-arg}} --config {{config-path-arg}} --output json upsert block --graph {{graph-arg}} --target-page Home --content 'Alpha block' >/dev/null"], + :cmds + ["{{cli}} --data-dir {{data-dir-arg}} --config {{config-path-arg}} --graph {{graph-arg}} --output human query --query '[:find [?e ...] :in $ ?q :where [?e :block/title ?title] [(clojure.string/includes? ?title ?q)]]' --inputs '[\"Alpha\"]' | {{cli}} --data-dir {{data-dir-arg}} --config {{config-path-arg}} --graph {{graph-arg}} --output human show --id"], + :expect {:exit 0, :stdout-contains ["Alpha block"]}, + :covers + {:commands ["show"], + :options + {:global ["--config" "--graph" "--data-dir" "--output"], + :show ["stdin:--id"]}}, + :tags [:show :pipe], + :extends :non-sync/graph-json-env} + {:id "debug-pull-id-json", + :setup + ["{{cli}} --data-dir {{data-dir-arg}} --config {{config-path-arg}} --output json upsert page --graph {{graph-arg}} --page Home >/dev/null"], + :cmds + ["{{cli}} --data-dir {{data-dir-arg}} --config {{config-path-arg}} --output json debug pull --graph {{graph-arg}} --id \"$({{cli}} --data-dir {{data-dir-arg}} --config {{config-path-arg}} --output json query --graph {{graph-arg}} --query '[:find ?e . :where [?e :block/title \"Home\"]]' | python3 -c 'import sys,json; print(json.load(sys.stdin)[\"data\"][\"result\"])')\""], + :expect + {:exit 0, + :stdout-json-paths + {[:status] "ok", [:data :entity :block/title] "Home"}}, + :covers + {:commands ["debug pull"], + :options + {:global ["--config" "--graph" "--data-dir" "--output"], + :debug ["--id"]}}, + :tags [:debug], + :extends :non-sync/graph-json-env} + {:id "debug-pull-uuid-json", + :setup + ["{{cli}} --data-dir {{data-dir-arg}} --config {{config-path-arg}} --output json upsert page --graph {{graph-arg}} --page Home >/dev/null"], + :cmds + ["{{cli}} --data-dir {{data-dir-arg}} --config {{config-path-arg}} --output json debug pull --graph {{graph-arg}} --uuid \"$({{cli}} --data-dir {{data-dir-arg}} --config {{config-path-arg}} --output json query --graph {{graph-arg}} --query '[:find ?uuid . :where [?e :block/title \"Home\"] [?e :block/uuid ?uuid]]' | python3 -c 'import sys,json; print(json.load(sys.stdin)[\"data\"][\"result\"])')\""], + :expect + {:exit 0, + :stdout-json-paths + {[:status] "ok", [:data :entity :block/title] "Home"}}, + :covers + {:commands ["debug pull"], + :options + {:global ["--config" "--graph" "--data-dir" "--output"], + :debug ["--uuid"]}}, + :tags [:debug], + :extends :non-sync/graph-json-env} + {:id "debug-pull-ident-json", + :cmds + ["{{cli}} --data-dir {{data-dir-arg}} --config {{config-path-arg}} --output json debug pull --graph {{graph-arg}} --ident :logseq.class/Tag"], + :expect + {:exit 0, + :stdout-json-paths + {[:status] "ok", [:data :entity :db/ident] "logseq.class/Tag"}}, + :covers + {:commands ["debug pull"], + :options + {:global ["--config" "--graph" "--data-dir" "--output"], + :debug ["--ident"]}}, + :tags [:debug], + :extends :non-sync/graph-json-env} + {:id "debug-pull-current-graph-fallback-json", + :setup + ["{{cli}} --data-dir {{data-dir-arg}} --config {{config-path-arg}} --output json upsert page --graph {{graph-arg}} --page Home >/dev/null"], + :cmds + ["{{cli}} --data-dir {{data-dir-arg}} --config {{config-path-arg}} --output json debug pull --id \"$({{cli}} --data-dir {{data-dir-arg}} --config {{config-path-arg}} --output json query --graph {{graph-arg}} --query '[:find ?e . :where [?e :block/title \"Home\"]]' | python3 -c 'import sys,json; print(json.load(sys.stdin)[\"data\"][\"result\"])')\""], + :expect + {:exit 0, + :stdout-json-paths + {[:status] "ok", [:data :entity :block/title] "Home"}}, + :covers + {:commands ["debug pull"], + :options + {:global ["--config" "--data-dir" "--output"], :debug ["--id"]}}, + :tags [:debug], + :extends :non-sync/graph-json-env} + {:id "remove-page-json", + :setup + ["{{cli}} --data-dir {{data-dir-arg}} --config {{config-path-arg}} --output json upsert page --graph {{graph-arg}} --page Home >/dev/null"], + :cmds + ["{{cli}} --data-dir {{data-dir-arg}} --config {{config-path-arg}} --output json remove page --graph {{graph-arg}} --page Home"], + :expect + {:exit 0, + :stdout-json-paths {[:status] "ok", [:data :result] true}}, + :covers + {:commands ["remove page"], + :options + {:global ["--config" "--graph" "--data-dir" "--output"], + :remove ["--page"]}}, + :tags [:remove], + :extends :non-sync/graph-json-env} + {:id "graph-remove-json", + :setup + ["{{cli}} --data-dir {{data-dir-arg}} --config {{config-path-arg}} --output json graph create --graph {{graph-arg}} >/dev/null" + "{{cli}} --data-dir {{data-dir-arg}} --config {{config-path-arg}} --output json server stop --graph {{graph-arg}} >/dev/null"], + :cmds + ["{{cli}} --data-dir {{data-dir-arg}} --config {{config-path-arg}} --output json graph remove --graph {{graph-arg}}"], + :expect {:exit 1, :stdout-contains ["graph-not-removed"]}, + :covers + {:commands ["graph remove"], + :options {:global ["--config" "--graph" "--data-dir" "--output"]}}, + :tags [:graph :remove]} + {:id "remove-block-uuid-human", + :setup + ["{{cli}} --data-dir {{data-dir-arg}} --config {{config-path-arg}} --output json upsert page --graph {{graph-arg}} --page Home >/dev/null" + "{{cli}} --data-dir {{data-dir-arg}} --config {{config-path-arg}} --output json upsert block --graph {{graph-arg}} --target-page Home --content 'Alpha block' >/dev/null"], + :cmds + ["{{cli}} --data-dir {{data-dir-arg}} --config {{config-path-arg}} --output human remove block --graph {{graph-arg}} --uuid \"$({{cli}} --data-dir {{data-dir-arg}} --config {{config-path-arg}} --output json query --graph {{graph-arg}} --query '[:find ?uuid . :where [?e :block/title \"Alpha block\"] [?e :block/uuid ?uuid]]' | python3 -c 'import sys,json; print(json.load(sys.stdin)[\"data\"][\"result\"])')\" >/dev/null" + "{{cli}} --data-dir {{data-dir-arg}} --config {{config-path-arg}} --output human show --graph {{graph-arg}} --page Home --linked-references false"], + :expect {:exit 0, :stdout-not-contains ["Alpha block"]}, + :covers + {:commands ["remove block"], + :options + {:global ["--config" "--graph" "--data-dir" "--output"], + :remove ["--uuid"]}}, + :tags [:remove], + :extends :non-sync/graph-json-env} + {:id "remove-tag-name-json", + :setup + ["{{cli}} --data-dir {{data-dir-arg}} --config {{config-path-arg}} --output json upsert tag --graph {{graph-arg}} --name TagOne >/dev/null"], + :cmds + ["{{cli}} --data-dir {{data-dir-arg}} --config {{config-path-arg}} --output json remove tag --graph {{graph-arg}} --name TagOne >/dev/null" + "{{cli}} --data-dir {{data-dir-arg}} --config {{config-path-arg}} --output json list tag --graph {{graph-arg}} --fields title,id"], + :expect {:exit 0, :stdout-not-contains ["TagOne"]}, + :covers + {:commands ["remove tag"], + :options + {:global ["--config" "--graph" "--data-dir" "--output"], + :remove ["--name"]}}, + :tags [:remove], + :extends :non-sync/graph-json-env} + {:id "remove-property-id-json", + :setup + ["{{cli}} --data-dir {{data-dir-arg}} --config {{config-path-arg}} --output json upsert property --graph {{graph-arg}} --name score --type number --cardinality one >/dev/null"], + :cmds + ["{{cli}} --data-dir {{data-dir-arg}} --config {{config-path-arg}} --output json remove property --graph {{graph-arg}} --id \"$({{cli}} --data-dir {{data-dir-arg}} --config {{config-path-arg}} --output json list property --graph {{graph-arg}} --fields title,id --limit 200 --sort updated-at --order desc | python3 -c 'import sys,json; print(next(item[\"db/id\"] for item in json.load(sys.stdin)[\"data\"][\"items\"] if item[\"block/title\"] == \"score\"))')\" >/dev/null" + "{{cli}} --data-dir {{data-dir-arg}} --config {{config-path-arg}} --output json list property --graph {{graph-arg}} --fields title,id"], + :expect {:exit 0, :stdout-not-contains ["score"]}, + :covers + {:commands ["remove property"], + :options + {:global ["--config" "--graph" "--data-dir" "--output"], + :remove ["--id"]}}, + :tags [:remove], + :extends :non-sync/graph-json-env} + {:id "server-cleanup-json", + :cmds + ["{{cli}} --data-dir {{data-dir-arg}} --config {{config-path-arg}} --output json server cleanup"], + :expect + {:exit 0, + :stdout-json-paths + {[:status] "ok", + [:data :mismatched] 0, + [:data :eligible] 0, + [:data :skipped-owner] 0}}, + :covers + {:commands ["server cleanup"], + :options + {:global ["--config" "--graph" "--data-dir" "--output"], + :server []}}, + :tags [:server], + :extends :non-sync/graph-json-env} + {:id "server-list-json", + :cmds + ["{{cli}} --data-dir {{data-dir-arg}} --config {{config-path-arg}} --output json server list"], + :expect + {:exit 0, + :stdout-json-paths + {[:status] "ok", [:data :servers 0 :repo] "{{graph}}"}}, + :covers + {:commands ["server list"], + :options {:global ["--config" "--data-dir" "--output"]}}, + :tags [:server], + :extends :non-sync/graph-json-env} + {:id "server-restart-json", + :cmds + ["{{cli}} --data-dir {{data-dir-arg}} --config {{config-path-arg}} --output json server restart --graph {{graph-arg}}"], + :expect + {:exit 0, + :stdout-json-paths + {[:status] "ok", [:data :repo] "{{graph}}", [:data :owned?] true}}, + :covers + {:commands ["server restart"], + :options + {:global ["--config" "--graph" "--data-dir" "--output"], + :server ["--graph"]}}, + :tags [:server], + :extends :non-sync/graph-json-env} + {:id "server-stop-json", + :setup + ["{{cli}} --data-dir {{data-dir-arg}} --config {{config-path-arg}} --output json graph create --graph {{graph-arg}} >/dev/null"], + :cmds + ["{{cli}} --data-dir {{data-dir-arg}} --config {{config-path-arg}} --output json server stop --graph {{graph-arg}}"], + :expect + {:exit 0, + :stdout-json-paths {[:status] "ok", [:data :repo] "{{graph}}"}}, + :covers + {:commands ["server stop"], + :options + {:global ["--config" "--graph" "--data-dir" "--output"], + :server ["--graph"]}}, + :tags [:server]} + {:id "server-start-json", + :setup + ["{{cli}} --data-dir {{data-dir-arg}} --config {{config-path-arg}} --output json server stop --graph {{graph-arg}} >/dev/null"], + :cmds + ["{{cli}} --data-dir {{data-dir-arg}} --config {{config-path-arg}} --output json server start --graph {{graph-arg}}"], + :expect + {:exit 0, + :stdout-json-paths + {[:status] "ok", [:data :repo] "{{graph}}", [:data :owned?] true}}, + :covers + {:commands ["server start"], + :options + {:global ["--config" "--graph" "--data-dir" "--output"], + :server ["--graph"]}}, + :tags [:server], + :extends :non-sync/graph-json-env} + {:id "skill-show-human", + :cmds ["{{cli}} skill show"], + :expect + {:exit 0, :stdout-contains ["name: logseq-cli" "# Logseq CLI"]}, + :covers {:commands ["skill show"], :options {:skill []}}, + :tags [:skill :smoke]} + {:id "skill-show-json-still-raw-markdown", + :cmds ["{{cli}} --output json skill show"], + :expect + {:exit 0, + :stdout-contains ["name: logseq-cli" "# Logseq CLI"], + :stdout-not-contains ["\"status\"" "\"data\""]}, + :covers {:commands ["skill show"], :options {:global ["--output"]}}, + :tags [:skill]} + {:id "skill-install-local", + :cmds + ["{{cli}} skill install >/dev/null" + "python3 -c 'import pathlib; p=pathlib.Path(\".agents/skills/logseq-cli/SKILL.md\"); print(\"installed\" if p.exists() else \"missing\")'"], + :expect {:exit 0, :stdout-contains ["installed"]}, + :covers {:commands ["skill install"], :options {:skill []}}, + :cleanup ["rm -rf ./.agents/skills/logseq-cli"], + :tags [:skill]} + {:id "skill-install-global-preserves-other-skills", + :setup + ["rm -rf ./tmp/cli-e2e-skill-home" + "mkdir -p ./tmp/cli-e2e-skill-home/.agents/skills/existing-skill" + "python3 -c 'import pathlib; pathlib.Path(\"./tmp/cli-e2e-skill-home/.agents/skills/existing-skill/SKILL.md\").write_text(\"keep\", encoding=\"utf8\")'"], + :cmds + ["HOME=\"$(pwd)/tmp/cli-e2e-skill-home\" {{cli}} skill install --global >/dev/null" + "python3 -c 'import pathlib; home=pathlib.Path(\"./tmp/cli-e2e-skill-home\"); existing=(home / \".agents/skills/existing-skill/SKILL.md\").read_text(encoding=\"utf8\"); installed=(home / \".agents/skills/logseq-cli/SKILL.md\").exists(); print(\"ok\" if existing==\"keep\" and installed else \"bad\")'"], + :expect {:exit 0, :stdout-contains ["ok"]}, + :covers + {:commands ["skill install"], :options {:skill ["--global"]}}, + :cleanup ["rm -rf ./tmp/cli-e2e-skill-home"], + :tags [:skill]} + {:id "completion-zsh", + :expect + {:exit 0, + :stdout-contains + ["#compdef logseq" "Auto-generated by `logseq completion zsh`"]}, + :covers {:options {:completion ["zsh"]}}, + :tags [:smoke], + :extends :non-sync/completion-command, + :vars {:completion-selector "zsh"}} + {:id "completion-bash-flag", + :expect + {:exit 0, + :stdout-contains + ["Auto-generated by `logseq completion bash`" + "_logseq_json_names_bash"]}, + :covers {:options {:completion ["--shell" "bash"]}}, + :extends :non-sync/completion-command, + :vars {:completion-selector "--shell bash"}} + {:id "completion-invalid-shell", + :expect {:exit 1, :stdout-contains ["unsupported shell: fish"]}, + :extends :non-sync/completion-command, + :vars {:completion-selector "fish"}} + {:id "doctor-dev-script-json", + :cmds + ["{{cli}} --data-dir {{data-dir-arg}} --config {{config-path-arg}} --output json doctor --dev-script"], + :expect + {:exit 0, + :stdout-json-paths + {[:status] "ok", + [:data :status] "ok", + [:data :checks 0 :id] "db-worker-script"}, + :stdout-contains ["static/db-worker-node.js"]}, + :covers + {:commands ["doctor"], + :options + {:global ["--config" "--data-dir" "--output"], + :doctor ["--dev-script"]}}, + :tags [:doctor :smoke]}]} diff --git a/cli-e2e/spec/sync_cases.edn b/cli-e2e/spec/sync_cases.edn index 02cbb449ea..2f22031354 100644 --- a/cli-e2e/spec/sync_cases.edn +++ b/cli-e2e/spec/sync_cases.edn @@ -1,57 +1,30 @@ {:templates - #:sync{:default - {:expect - {:exit 0, - :stdout-json-paths - {[:status] "ok", - [:data :pending-local] 0, - [:data :pending-asset] 0, - [:data :pending-server] 0, - [:data :last-error] nil}}, - :covers - {:commands ["sync upload" "sync download" "sync status"], - :options - {:global ["--config" "--graph" "--data-dir" "--output"]}}, - :cleanup - ["{{cli}} --data-dir {{data-dir-arg}} --config {{config-path-arg}} --output json server stop --graph {{graph-arg}}" - "{{cli}} --data-dir '{{tmp-dir}}/graphs-b' --config '{{tmp-dir}}/cli-b.edn' --output json server stop --graph {{graph-arg}}" - "python3 '{{repo-root}}/cli-e2e/scripts/db_sync_server.py' stop --pid-file '{{tmp-dir}}/db-sync-server.pid'"], - :tags [:sync]}}, - :cases - [{:tags [:happy-path :bootstrap :a-to-b], - :extends :sync/default, - :setup - ["mkdir -p '{{tmp-dir}}/graphs-b'" - "mkdir -p '{{tmp-dir}}/home/logseq'" - "cp ~/logseq/auth.json '{{tmp-dir}}/home/logseq/auth.json'" - "python3 '{{repo-root}}/cli-e2e/scripts/prepare_sync_config.py' --output '{{config-path}}' --auth-path '{{auth-path}}' --http-base '{{sync-http-base}}' --ws-url '{{sync-ws-url}}'" - "python3 '{{repo-root}}/cli-e2e/scripts/prepare_sync_config.py' --output '{{tmp-dir}}/cli-b.edn' --auth-path '{{auth-path}}' --http-base '{{sync-http-base}}' --ws-url '{{sync-ws-url}}'" - "python3 '{{repo-root}}/cli-e2e/scripts/db_sync_server.py' start --repo-root '{{repo-root}}' --pid-file '{{tmp-dir}}/db-sync-server.pid' --log-file '{{tmp-dir}}/db-sync-server.log' --data-dir '{{tmp-dir}}/db-sync-server-data' --port {{sync-port}} --auth-path '{{auth-path}}'" - "{{cli-home}} --data-dir {{data-dir-arg}} --config {{config-path-arg}} --output json graph create --graph {{graph-arg}} >/dev/null" - "{{cli-home}} --data-dir {{data-dir-arg}} --config {{config-path-arg}} --output json sync ensure-keys --graph {{graph-arg}} --e2ee-password '11111' --upload-keys" - "{{cli-home}} --data-dir {{data-dir-arg}} --config {{config-path-arg}} --output json upsert page --graph {{graph-arg}} --page SyncBootstrapHome >/dev/null" - "{{cli-home}} --data-dir {{data-dir-arg}} --config {{config-path-arg}} --output json upsert block --graph {{graph-arg}} --target-page SyncBootstrapHome --content '{{marker-content}}' >/dev/null"], - :cmds - ["{{cli-home}} --data-dir {{data-dir-arg}} --config {{config-path-arg}} --output json sync upload --graph {{graph-arg}}" - "{{cli-home}} --data-dir {{data-dir-arg}} --config {{config-path-arg}} --output json sync start --graph {{graph-arg}} --e2ee-password '11111'" - "python3 '{{repo-root}}/cli-e2e/scripts/wait_sync_status.py' --cli '{{repo-root}}/static/logseq-cli.js' --data-dir '{{data-dir}}' --config '{{config-path}}' --graph '{{graph}}' --timeout-s 120 --interval-s 1" - "{{cli-home}} --data-dir '{{tmp-dir}}/graphs-b' --config '{{tmp-dir}}/cli-b.edn' --output json sync download --graph {{graph-arg}} --e2ee-password '11111'" - "{{cli-home}} --data-dir '{{tmp-dir}}/graphs-b' --config '{{tmp-dir}}/cli-b.edn' --output json sync start --graph {{graph-arg}} --e2ee-password '11111'" - "python3 '{{repo-root}}/cli-e2e/scripts/wait_sync_status.py' --cli '{{repo-root}}/static/logseq-cli.js' --data-dir '{{tmp-dir}}/graphs-b' --config '{{tmp-dir}}/cli-b.edn' --graph '{{graph}}' --timeout-s 120 --interval-s 1" - "python3 '{{repo-root}}/cli-e2e/scripts/compare_graph_queries.py' --cli '{{repo-root}}/static/logseq-cli.js' --graph '{{graph}}' --config-a '{{config-path}}' --data-dir-a '{{data-dir}}' --config-b '{{tmp-dir}}/cli-b.edn' --data-dir-b '{{tmp-dir}}/graphs-b' --query '[:find ?title :where [?b :block/title ?title] [(= ?title \"{{marker-content}}\")] ]' --require-result" - "{{cli-home}} --data-dir '{{tmp-dir}}/graphs-b' --config '{{tmp-dir}}/cli-b.edn' --output json sync status --graph {{graph-arg}}"], - :id "sync-bootstrap-upload-download-a-to-b", - :graph "sync-e2e-bootstrap-a-to-b", + {:sync/default + {:expect + {:exit 0, + :stdout-json-paths + {[:status] "ok", + [:data :pending-local] 0, + [:data :pending-asset] 0, + [:data :pending-server] 0, + [:data :last-error] nil}}, + :covers + {:commands ["sync upload" "sync download" "sync status"], + :options {:global ["--config" "--graph" "--data-dir" "--output"]}}, + :cleanup + ["{{cli}} --data-dir {{data-dir-arg}} --config {{config-path-arg}} --output json server stop --graph {{graph-arg}}" + "{{cli}} --data-dir '{{tmp-dir}}/graphs-b' --config '{{tmp-dir}}/cli-b.edn' --output json server stop --graph {{graph-arg}}" + "python3 '{{repo-root}}/cli-e2e/scripts/db_sync_server.py' stop --pid-file '{{tmp-dir}}/db-sync-server.pid'"], + :tags [:sync]}, + :sync/common + {:extends :sync/default, :vars {:sync-port "18080", :sync-http-base "http://127.0.0.1:18080", :sync-ws-url "ws://127.0.0.1:18080/sync/%s", - :marker-content "sync-happy-bootstrap-a-to-b-marker", :home-dir "{{tmp-dir}}/home", :auth-path "{{tmp-dir}}/home/logseq/auth.json", - :cli-home "HOME='{{tmp-dir}}/home' {{cli}}"}} - {:tags [:happy-path :incremental :a-to-b], - :extends :sync/default, + :cli-home "HOME='{{tmp-dir}}/home' {{cli}}"}, :setup ["mkdir -p '{{tmp-dir}}/graphs-b'" "mkdir -p '{{tmp-dir}}/home/logseq'" @@ -60,17 +33,33 @@ "python3 '{{repo-root}}/cli-e2e/scripts/prepare_sync_config.py' --output '{{tmp-dir}}/cli-b.edn' --auth-path '{{auth-path}}' --http-base '{{sync-http-base}}' --ws-url '{{sync-ws-url}}'" "python3 '{{repo-root}}/cli-e2e/scripts/db_sync_server.py' start --repo-root '{{repo-root}}' --pid-file '{{tmp-dir}}/db-sync-server.pid' --log-file '{{tmp-dir}}/db-sync-server.log' --data-dir '{{tmp-dir}}/db-sync-server-data' --port {{sync-port}} --auth-path '{{auth-path}}'" "{{cli-home}} --data-dir {{data-dir-arg}} --config {{config-path-arg}} --output json graph create --graph {{graph-arg}} >/dev/null" - "{{cli-home}} --data-dir {{data-dir-arg}} --config {{config-path-arg}} --output json sync ensure-keys --graph {{graph-arg}} --e2ee-password '11111' --upload-keys" - "{{cli-home}} --data-dir {{data-dir-arg}} --config {{config-path-arg}} --output json upsert page --graph {{graph-arg}} --page SyncIncrementalHome >/dev/null" - "{{cli-home}} --data-dir {{data-dir-arg}} --config {{config-path-arg}} --output json upsert block --graph {{graph-arg}} --target-page SyncIncrementalHome --content '{{seed-marker}}' >/dev/null"], + "{{cli-home}} --data-dir {{data-dir-arg}} --config {{config-path-arg}} --output json sync ensure-keys --graph {{graph-arg}} --e2ee-password '11111' --upload-keys"], :cmds ["{{cli-home}} --data-dir {{data-dir-arg}} --config {{config-path-arg}} --output json sync upload --graph {{graph-arg}}" "{{cli-home}} --data-dir {{data-dir-arg}} --config {{config-path-arg}} --output json sync start --graph {{graph-arg}} --e2ee-password '11111'" "python3 '{{repo-root}}/cli-e2e/scripts/wait_sync_status.py' --cli '{{repo-root}}/static/logseq-cli.js' --data-dir '{{data-dir}}' --config '{{config-path}}' --graph '{{graph}}' --timeout-s 120 --interval-s 1" "{{cli-home}} --data-dir '{{tmp-dir}}/graphs-b' --config '{{tmp-dir}}/cli-b.edn' --output json sync download --graph {{graph-arg}} --e2ee-password '11111'" "{{cli-home}} --data-dir '{{tmp-dir}}/graphs-b' --config '{{tmp-dir}}/cli-b.edn' --output json sync start --graph {{graph-arg}} --e2ee-password '11111'" - "python3 '{{repo-root}}/cli-e2e/scripts/wait_sync_status.py' --cli '{{repo-root}}/static/logseq-cli.js' --data-dir '{{tmp-dir}}/graphs-b' --config '{{tmp-dir}}/cli-b.edn' --graph '{{graph}}' --timeout-s 120 --interval-s 1" - "{{cli-home}} --data-dir {{data-dir-arg}} --config {{config-path-arg}} --output json upsert block --graph {{graph-arg}} --target-page SyncIncrementalHome --content '{{incremental-marker}}' >/dev/null" + "python3 '{{repo-root}}/cli-e2e/scripts/wait_sync_status.py' --cli '{{repo-root}}/static/logseq-cli.js' --data-dir '{{tmp-dir}}/graphs-b' --config '{{tmp-dir}}/cli-b.edn' --graph '{{graph}}' --timeout-s 120 --interval-s 1"]}}, + :cases + [{:tags [:happy-path :bootstrap :a-to-b], + :extends :sync/common, + :setup + ["{{cli-home}} --data-dir {{data-dir-arg}} --config {{config-path-arg}} --output json upsert page --graph {{graph-arg}} --page SyncBootstrapHome >/dev/null" + "{{cli-home}} --data-dir {{data-dir-arg}} --config {{config-path-arg}} --output json upsert block --graph {{graph-arg}} --target-page SyncBootstrapHome --content '{{marker-content}}' >/dev/null"], + :cmds + ["python3 '{{repo-root}}/cli-e2e/scripts/compare_graph_queries.py' --cli '{{repo-root}}/static/logseq-cli.js' --graph '{{graph}}' --config-a '{{config-path}}' --data-dir-a '{{data-dir}}' --config-b '{{tmp-dir}}/cli-b.edn' --data-dir-b '{{tmp-dir}}/graphs-b' --query '[:find ?title :where [?b :block/title ?title] [(= ?title \"{{marker-content}}\")] ]' --require-result" + "{{cli-home}} --data-dir '{{tmp-dir}}/graphs-b' --config '{{tmp-dir}}/cli-b.edn' --output json sync status --graph {{graph-arg}}"], + :id "sync-bootstrap-upload-download-a-to-b", + :graph "sync-e2e-bootstrap-a-to-b", + :vars {:marker-content "sync-happy-bootstrap-a-to-b-marker"}} + {:tags [:happy-path :incremental :a-to-b], + :extends :sync/common, + :setup + ["{{cli-home}} --data-dir {{data-dir-arg}} --config {{config-path-arg}} --output json upsert page --graph {{graph-arg}} --page SyncIncrementalHome >/dev/null" + "{{cli-home}} --data-dir {{data-dir-arg}} --config {{config-path-arg}} --output json upsert block --graph {{graph-arg}} --target-page SyncIncrementalHome --content '{{seed-marker}}' >/dev/null"], + :cmds + ["{{cli-home}} --data-dir {{data-dir-arg}} --config {{config-path-arg}} --output json upsert block --graph {{graph-arg}} --target-page SyncIncrementalHome --content '{{incremental-marker}}' >/dev/null" "{{cli-home}} --data-dir {{data-dir-arg}} --config {{config-path-arg}} --output json sync upload --graph {{graph-arg}}" "python3 '{{repo-root}}/cli-e2e/scripts/wait_sync_status.py' --cli '{{repo-root}}/static/logseq-cli.js' --data-dir '{{tmp-dir}}/graphs-b' --config '{{tmp-dir}}/cli-b.edn' --graph '{{graph}}' --timeout-s 120 --interval-s 1" "python3 '{{repo-root}}/cli-e2e/scripts/compare_graph_queries.py' --cli '{{repo-root}}/static/logseq-cli.js' --graph '{{graph}}' --config-a '{{config-path}}' --data-dir-a '{{data-dir}}' --config-b '{{tmp-dir}}/cli-b.edn' --data-dir-b '{{tmp-dir}}/graphs-b' --query '[:find ?title :where [?b :block/title ?title] [(= ?title \"{{incremental-marker}}\")] ]' --require-result" @@ -78,35 +67,15 @@ :id "sync-incremental-update-a-to-b", :graph "sync-e2e-incremental-a-to-b", :vars - {:sync-port "18080", - :sync-http-base "http://127.0.0.1:18080", - :sync-ws-url "ws://127.0.0.1:18080/sync/%s", - :seed-marker "sync-happy-incremental-seed-marker", - :incremental-marker "sync-happy-incremental-update-marker", - :home-dir "{{tmp-dir}}/home", - :auth-path "{{tmp-dir}}/home/logseq/auth.json", - :cli-home "HOME='{{tmp-dir}}/home' {{cli}}"}} + {:seed-marker "sync-happy-incremental-seed-marker", + :incremental-marker "sync-happy-incremental-update-marker"}} {:tags [:happy-path :bidirectional :roundtrip], - :extends :sync/default, + :extends :sync/common, :setup - ["mkdir -p '{{tmp-dir}}/graphs-b'" - "mkdir -p '{{tmp-dir}}/home/logseq'" - "cp ~/logseq/auth.json '{{tmp-dir}}/home/logseq/auth.json'" - "python3 '{{repo-root}}/cli-e2e/scripts/prepare_sync_config.py' --output '{{config-path}}' --auth-path '{{auth-path}}' --http-base '{{sync-http-base}}' --ws-url '{{sync-ws-url}}'" - "python3 '{{repo-root}}/cli-e2e/scripts/prepare_sync_config.py' --output '{{tmp-dir}}/cli-b.edn' --auth-path '{{auth-path}}' --http-base '{{sync-http-base}}' --ws-url '{{sync-ws-url}}'" - "python3 '{{repo-root}}/cli-e2e/scripts/db_sync_server.py' start --repo-root '{{repo-root}}' --pid-file '{{tmp-dir}}/db-sync-server.pid' --log-file '{{tmp-dir}}/db-sync-server.log' --data-dir '{{tmp-dir}}/db-sync-server-data' --port {{sync-port}} --auth-path '{{auth-path}}'" - "{{cli-home}} --data-dir {{data-dir-arg}} --config {{config-path-arg}} --output json graph create --graph {{graph-arg}} >/dev/null" - "{{cli-home}} --data-dir {{data-dir-arg}} --config {{config-path-arg}} --output json sync ensure-keys --graph {{graph-arg}} --e2ee-password '11111' --upload-keys" - "{{cli-home}} --data-dir {{data-dir-arg}} --config {{config-path-arg}} --output json upsert page --graph {{graph-arg}} --page SyncRoundtripAHome >/dev/null" + ["{{cli-home}} --data-dir {{data-dir-arg}} --config {{config-path-arg}} --output json upsert page --graph {{graph-arg}} --page SyncRoundtripAHome >/dev/null" "{{cli-home}} --data-dir {{data-dir-arg}} --config {{config-path-arg}} --output json upsert block --graph {{graph-arg}} --target-page SyncRoundtripAHome --content '{{marker-a}}' >/dev/null"], :cmds - ["{{cli-home}} --data-dir {{data-dir-arg}} --config {{config-path-arg}} --output json sync upload --graph {{graph-arg}}" - "{{cli-home}} --data-dir {{data-dir-arg}} --config {{config-path-arg}} --output json sync start --graph {{graph-arg}} --e2ee-password '11111'" - "python3 '{{repo-root}}/cli-e2e/scripts/wait_sync_status.py' --cli '{{repo-root}}/static/logseq-cli.js' --data-dir '{{data-dir}}' --config '{{config-path}}' --graph '{{graph}}' --timeout-s 120 --interval-s 1" - "{{cli-home}} --data-dir '{{tmp-dir}}/graphs-b' --config '{{tmp-dir}}/cli-b.edn' --output json sync download --graph {{graph-arg}} --e2ee-password '11111'" - "{{cli-home}} --data-dir '{{tmp-dir}}/graphs-b' --config '{{tmp-dir}}/cli-b.edn' --output json sync start --graph {{graph-arg}} --e2ee-password '11111'" - "python3 '{{repo-root}}/cli-e2e/scripts/wait_sync_status.py' --cli '{{repo-root}}/static/logseq-cli.js' --data-dir '{{tmp-dir}}/graphs-b' --config '{{tmp-dir}}/cli-b.edn' --graph '{{graph}}' --timeout-s 120 --interval-s 1" - "{{cli-home}} --data-dir '{{tmp-dir}}/graphs-b' --config '{{tmp-dir}}/cli-b.edn' --output json upsert page --graph {{graph-arg}} --page SyncRoundtripBHome >/dev/null" + ["{{cli-home}} --data-dir '{{tmp-dir}}/graphs-b' --config '{{tmp-dir}}/cli-b.edn' --output json upsert page --graph {{graph-arg}} --page SyncRoundtripBHome >/dev/null" "{{cli-home}} --data-dir '{{tmp-dir}}/graphs-b' --config '{{tmp-dir}}/cli-b.edn' --output json upsert block --graph {{graph-arg}} --target-page SyncRoundtripBHome --content '{{marker-b}}' >/dev/null" "{{cli-home}} --data-dir '{{tmp-dir}}/graphs-b' --config '{{tmp-dir}}/cli-b.edn' --output json sync upload --graph {{graph-arg}}" "python3 '{{repo-root}}/cli-e2e/scripts/wait_sync_status.py' --cli '{{repo-root}}/static/logseq-cli.js' --data-dir '{{data-dir}}' --config '{{config-path}}' --graph '{{graph}}' --timeout-s 120 --interval-s 1" @@ -115,35 +84,15 @@ :id "sync-bidirectional-roundtrip", :graph "sync-e2e-bidirectional-roundtrip", :vars - {:sync-port "18080", - :sync-http-base "http://127.0.0.1:18080", - :sync-ws-url "ws://127.0.0.1:18080/sync/%s", - :marker-a "sync-happy-roundtrip-a-seed-marker", - :marker-b "sync-happy-roundtrip-b-origin-marker", - :home-dir "{{tmp-dir}}/home", - :auth-path "{{tmp-dir}}/home/logseq/auth.json", - :cli-home "HOME='{{tmp-dir}}/home' {{cli}}"}} + {:marker-a "sync-happy-roundtrip-a-seed-marker", + :marker-b "sync-happy-roundtrip-b-origin-marker"}} {:tags [:happy-path :multi-batch :a-to-b], - :extends :sync/default, + :extends :sync/common, :setup - ["mkdir -p '{{tmp-dir}}/graphs-b'" - "mkdir -p '{{tmp-dir}}/home/logseq'" - "cp ~/logseq/auth.json '{{tmp-dir}}/home/logseq/auth.json'" - "python3 '{{repo-root}}/cli-e2e/scripts/prepare_sync_config.py' --output '{{config-path}}' --auth-path '{{auth-path}}' --http-base '{{sync-http-base}}' --ws-url '{{sync-ws-url}}'" - "python3 '{{repo-root}}/cli-e2e/scripts/prepare_sync_config.py' --output '{{tmp-dir}}/cli-b.edn' --auth-path '{{auth-path}}' --http-base '{{sync-http-base}}' --ws-url '{{sync-ws-url}}'" - "python3 '{{repo-root}}/cli-e2e/scripts/db_sync_server.py' start --repo-root '{{repo-root}}' --pid-file '{{tmp-dir}}/db-sync-server.pid' --log-file '{{tmp-dir}}/db-sync-server.log' --data-dir '{{tmp-dir}}/db-sync-server-data' --port {{sync-port}} --auth-path '{{auth-path}}'" - "{{cli-home}} --data-dir {{data-dir-arg}} --config {{config-path-arg}} --output json graph create --graph {{graph-arg}} >/dev/null" - "{{cli-home}} --data-dir {{data-dir-arg}} --config {{config-path-arg}} --output json sync ensure-keys --graph {{graph-arg}} --e2ee-password '11111' --upload-keys" - "{{cli-home}} --data-dir {{data-dir-arg}} --config {{config-path-arg}} --output json upsert page --graph {{graph-arg}} --page SyncMultiBatchHome >/dev/null" + ["{{cli-home}} --data-dir {{data-dir-arg}} --config {{config-path-arg}} --output json upsert page --graph {{graph-arg}} --page SyncMultiBatchHome >/dev/null" "{{cli-home}} --data-dir {{data-dir-arg}} --config {{config-path-arg}} --output json upsert block --graph {{graph-arg}} --target-page SyncMultiBatchHome --content '{{seed-marker}}' >/dev/null"], :cmds - ["{{cli-home}} --data-dir {{data-dir-arg}} --config {{config-path-arg}} --output json sync upload --graph {{graph-arg}}" - "{{cli-home}} --data-dir {{data-dir-arg}} --config {{config-path-arg}} --output json sync start --graph {{graph-arg}} --e2ee-password '11111'" - "python3 '{{repo-root}}/cli-e2e/scripts/wait_sync_status.py' --cli '{{repo-root}}/static/logseq-cli.js' --data-dir '{{data-dir}}' --config '{{config-path}}' --graph '{{graph}}' --timeout-s 120 --interval-s 1" - "{{cli-home}} --data-dir '{{tmp-dir}}/graphs-b' --config '{{tmp-dir}}/cli-b.edn' --output json sync download --graph {{graph-arg}} --e2ee-password '11111'" - "{{cli-home}} --data-dir '{{tmp-dir}}/graphs-b' --config '{{tmp-dir}}/cli-b.edn' --output json sync start --graph {{graph-arg}} --e2ee-password '11111'" - "python3 '{{repo-root}}/cli-e2e/scripts/wait_sync_status.py' --cli '{{repo-root}}/static/logseq-cli.js' --data-dir '{{tmp-dir}}/graphs-b' --config '{{tmp-dir}}/cli-b.edn' --graph '{{graph}}' --timeout-s 120 --interval-s 1" - "{{cli-home}} --data-dir {{data-dir-arg}} --config {{config-path-arg}} --output json upsert page --graph {{graph-arg}} --page SyncMultiBatchOne >/dev/null" + ["{{cli-home}} --data-dir {{data-dir-arg}} --config {{config-path-arg}} --output json upsert page --graph {{graph-arg}} --page SyncMultiBatchOne >/dev/null" "{{cli-home}} --data-dir {{data-dir-arg}} --config {{config-path-arg}} --output json upsert block --graph {{graph-arg}} --target-page SyncMultiBatchOne --content '{{batch-marker-1}}' >/dev/null" "sleep 1" "{{cli}} --data-dir {{data-dir-arg}} --config {{config-path-arg}} --output json server stop --graph {{graph-arg}}" @@ -173,70 +122,29 @@ :id "sync-multi-batch-operations", :graph "sync-e2e-multi-batch-operations", :vars - {:sync-port "18080", - :seed-marker "sync-happy-multi-batch-seed-marker", - :sync-http-base "http://127.0.0.1:18080", + {:seed-marker "sync-happy-multi-batch-seed-marker", :batch-marker-3 "sync-happy-multi-batch-marker-3", - :auth-path "{{tmp-dir}}/home/logseq/auth.json", :batch-marker-1 "sync-happy-multi-batch-marker-1", - :home-dir "{{tmp-dir}}/home", - :sync-ws-url "ws://127.0.0.1:18080/sync/%s", - :batch-marker-2 "sync-happy-multi-batch-marker-2", - :cli-home "HOME='{{tmp-dir}}/home' {{cli}}"}} + :batch-marker-2 "sync-happy-multi-batch-marker-2"}} {:tags [:happy-path :steady-state :status], - :extends :sync/default, + :extends :sync/common, :setup - ["mkdir -p '{{tmp-dir}}/graphs-b'" - "mkdir -p '{{tmp-dir}}/home/logseq'" - "cp ~/logseq/auth.json '{{tmp-dir}}/home/logseq/auth.json'" - "python3 '{{repo-root}}/cli-e2e/scripts/prepare_sync_config.py' --output '{{config-path}}' --auth-path '{{auth-path}}' --http-base '{{sync-http-base}}' --ws-url '{{sync-ws-url}}'" - "python3 '{{repo-root}}/cli-e2e/scripts/prepare_sync_config.py' --output '{{tmp-dir}}/cli-b.edn' --auth-path '{{auth-path}}' --http-base '{{sync-http-base}}' --ws-url '{{sync-ws-url}}'" - "python3 '{{repo-root}}/cli-e2e/scripts/db_sync_server.py' start --repo-root '{{repo-root}}' --pid-file '{{tmp-dir}}/db-sync-server.pid' --log-file '{{tmp-dir}}/db-sync-server.log' --data-dir '{{tmp-dir}}/db-sync-server-data' --port {{sync-port}} --auth-path '{{auth-path}}'" - "{{cli-home}} --data-dir {{data-dir-arg}} --config {{config-path-arg}} --output json graph create --graph {{graph-arg}} >/dev/null" - "{{cli-home}} --data-dir {{data-dir-arg}} --config {{config-path-arg}} --output json sync ensure-keys --graph {{graph-arg}} --e2ee-password '11111' --upload-keys" - "{{cli-home}} --data-dir {{data-dir-arg}} --config {{config-path-arg}} --output json upsert page --graph {{graph-arg}} --page SyncSteadyStateHome >/dev/null" + ["{{cli-home}} --data-dir {{data-dir-arg}} --config {{config-path-arg}} --output json upsert page --graph {{graph-arg}} --page SyncSteadyStateHome >/dev/null" "{{cli-home}} --data-dir {{data-dir-arg}} --config {{config-path-arg}} --output json upsert block --graph {{graph-arg}} --target-page SyncSteadyStateHome --content '{{marker-content}}' >/dev/null"], :cmds - ["{{cli-home}} --data-dir {{data-dir-arg}} --config {{config-path-arg}} --output json sync upload --graph {{graph-arg}}" - "{{cli-home}} --data-dir {{data-dir-arg}} --config {{config-path-arg}} --output json sync start --graph {{graph-arg}} --e2ee-password '11111'" - "python3 '{{repo-root}}/cli-e2e/scripts/wait_sync_status.py' --cli '{{repo-root}}/static/logseq-cli.js' --data-dir '{{data-dir}}' --config '{{config-path}}' --graph '{{graph}}' --timeout-s 120 --interval-s 1" - "{{cli-home}} --data-dir '{{tmp-dir}}/graphs-b' --config '{{tmp-dir}}/cli-b.edn' --output json sync download --graph {{graph-arg}} --e2ee-password '11111'" - "{{cli-home}} --data-dir '{{tmp-dir}}/graphs-b' --config '{{tmp-dir}}/cli-b.edn' --output json sync start --graph {{graph-arg}} --e2ee-password '11111'" - "python3 '{{repo-root}}/cli-e2e/scripts/wait_sync_status.py' --cli '{{repo-root}}/static/logseq-cli.js' --data-dir '{{tmp-dir}}/graphs-b' --config '{{tmp-dir}}/cli-b.edn' --graph '{{graph}}' --timeout-s 120 --interval-s 1" - "python3 '{{repo-root}}/cli-e2e/scripts/compare_graph_queries.py' --cli '{{repo-root}}/static/logseq-cli.js' --graph '{{graph}}' --config-a '{{config-path}}' --data-dir-a '{{data-dir}}' --config-b '{{tmp-dir}}/cli-b.edn' --data-dir-b '{{tmp-dir}}/graphs-b' --query '[:find ?title :where [?b :block/title ?title] [(= ?title \"{{marker-content}}\")] ]' --require-result" + ["python3 '{{repo-root}}/cli-e2e/scripts/compare_graph_queries.py' --cli '{{repo-root}}/static/logseq-cli.js' --graph '{{graph}}' --config-a '{{config-path}}' --data-dir-a '{{data-dir}}' --config-b '{{tmp-dir}}/cli-b.edn' --data-dir-b '{{tmp-dir}}/graphs-b' --query '[:find ?title :where [?b :block/title ?title] [(= ?title \"{{marker-content}}\")] ]' --require-result" "python3 '{{repo-root}}/cli-e2e/scripts/wait_sync_status.py' --cli '{{repo-root}}/static/logseq-cli.js' --data-dir '{{tmp-dir}}/graphs-b' --config '{{tmp-dir}}/cli-b.edn' --graph '{{graph}}' --timeout-s 30 --interval-s 1" "python3 '{{repo-root}}/cli-e2e/scripts/wait_sync_status.py' --cli '{{repo-root}}/static/logseq-cli.js' --data-dir '{{tmp-dir}}/graphs-b' --config '{{tmp-dir}}/cli-b.edn' --graph '{{graph}}' --timeout-s 30 --interval-s 1" "{{cli-home}} --data-dir '{{tmp-dir}}/graphs-b' --config '{{tmp-dir}}/cli-b.edn' --output json sync status --graph {{graph-arg}}"], :id "sync-status-steady-state", :graph "sync-e2e-status-steady-state", - :vars - {:sync-port "18080", - :sync-http-base "http://127.0.0.1:18080", - :sync-ws-url "ws://127.0.0.1:18080/sync/%s", - :marker-content "sync-happy-steady-state-marker", - :home-dir "{{tmp-dir}}/home", - :auth-path "{{tmp-dir}}/home/logseq/auth.json", - :cli-home "HOME='{{tmp-dir}}/home' {{cli}}"}} + :vars {:marker-content "sync-happy-steady-state-marker"}} {:tags [:stress :bidirectional :random :block-ops], - :extends :sync/default, + :extends :sync/common, :setup - ["mkdir -p '{{tmp-dir}}/graphs-b'" - "mkdir -p '{{tmp-dir}}/home/logseq'" - "cp ~/logseq/auth.json '{{tmp-dir}}/home/logseq/auth.json'" - "python3 '{{repo-root}}/cli-e2e/scripts/prepare_sync_config.py' --output '{{config-path}}' --auth-path '{{auth-path}}' --http-base '{{sync-http-base}}' --ws-url '{{sync-ws-url}}'" - "python3 '{{repo-root}}/cli-e2e/scripts/prepare_sync_config.py' --output '{{tmp-dir}}/cli-b.edn' --auth-path '{{auth-path}}' --http-base '{{sync-http-base}}' --ws-url '{{sync-ws-url}}'" - "python3 '{{repo-root}}/cli-e2e/scripts/db_sync_server.py' start --repo-root '{{repo-root}}' --pid-file '{{tmp-dir}}/db-sync-server.pid' --log-file '{{tmp-dir}}/db-sync-server.log' --data-dir '{{tmp-dir}}/db-sync-server-data' --port {{sync-port}} --auth-path '{{auth-path}}'" - "{{cli-home}} --data-dir {{data-dir-arg}} --config {{config-path-arg}} --output json graph create --graph {{graph-arg}} >/dev/null" - "{{cli-home}} --data-dir {{data-dir-arg}} --config {{config-path-arg}} --output json sync ensure-keys --graph {{graph-arg}} --e2ee-password '11111' --upload-keys" - "{{cli-home}} --data-dir {{data-dir-arg}} --config {{config-path-arg}} --output json upsert page --graph {{graph-arg}} --page '{{random-page}}' >/dev/null"], + ["{{cli-home}} --data-dir {{data-dir-arg}} --config {{config-path-arg}} --output json upsert page --graph {{graph-arg}} --page '{{random-page}}' >/dev/null"], :cmds - ["{{cli-home}} --data-dir {{data-dir-arg}} --config {{config-path-arg}} --output json sync upload --graph {{graph-arg}}" - "{{cli-home}} --data-dir {{data-dir-arg}} --config {{config-path-arg}} --output json sync start --graph {{graph-arg}} --e2ee-password '11111'" - "python3 '{{repo-root}}/cli-e2e/scripts/wait_sync_status.py' --cli '{{repo-root}}/static/logseq-cli.js' --data-dir '{{data-dir}}' --config '{{config-path}}' --graph '{{graph}}' --timeout-s 120 --interval-s 1" - "{{cli-home}} --data-dir '{{tmp-dir}}/graphs-b' --config '{{tmp-dir}}/cli-b.edn' --output json sync download --graph {{graph-arg}} --e2ee-password '11111'" - "{{cli-home}} --data-dir '{{tmp-dir}}/graphs-b' --config '{{tmp-dir}}/cli-b.edn' --output json sync start --graph {{graph-arg}} --e2ee-password '11111'" - "python3 '{{repo-root}}/cli-e2e/scripts/wait_sync_status.py' --cli '{{repo-root}}/static/logseq-cli.js' --data-dir '{{tmp-dir}}/graphs-b' --config '{{tmp-dir}}/cli-b.edn' --graph '{{graph}}' --timeout-s 120 --interval-s 1" - "python3 '{{repo-root}}/cli-e2e/scripts/random_bidirectional_block_ops.py' --cli '{{repo-root}}/static/logseq-cli.js' --graph '{{graph}}' --config-a '{{config-path}}' --data-dir-a '{{data-dir}}' --config-b '{{tmp-dir}}/cli-b.edn' --data-dir-b '{{tmp-dir}}/graphs-b' --page '{{random-page}}' --rounds-per-client {{rounds-per-client}} --seed {{random-seed}}" + ["python3 '{{repo-root}}/cli-e2e/scripts/random_bidirectional_block_ops.py' --cli '{{repo-root}}/static/logseq-cli.js' --graph '{{graph}}' --config-a '{{config-path}}' --data-dir-a '{{data-dir}}' --config-b '{{tmp-dir}}/cli-b.edn' --data-dir-b '{{tmp-dir}}/graphs-b' --page '{{random-page}}' --rounds-per-client {{rounds-per-client}} --seed {{random-seed}}" "python3 '{{repo-root}}/cli-e2e/scripts/wait_sync_status.py' --cli '{{repo-root}}/static/logseq-cli.js' --data-dir '{{data-dir}}' --config '{{config-path}}' --graph '{{graph}}' --timeout-s 180 --interval-s 1" "python3 '{{repo-root}}/cli-e2e/scripts/wait_sync_status.py' --cli '{{repo-root}}/static/logseq-cli.js' --data-dir '{{tmp-dir}}/graphs-b' --config '{{tmp-dir}}/cli-b.edn' --graph '{{graph}}' --timeout-s 180 --interval-s 1" "python3 '{{repo-root}}/cli-e2e/scripts/compare_graph_queries.py' --cli '{{repo-root}}/static/logseq-cli.js' --graph '{{graph}}' --config-a '{{config-path}}' --data-dir-a '{{data-dir}}' --config-b '{{tmp-dir}}/cli-b.edn' --data-dir-b '{{tmp-dir}}/graphs-b' --query '[:find (pull ?b [:block/uuid :block/title :block/order {:block/parent [:block/uuid]}]) :where [?p :block/title \"{{random-page}}\"] [?b :block/page ?p] [?b :block/uuid]]' --require-result" @@ -244,36 +152,16 @@ :id "sync-random-bidirectional-block-ops", :graph "sync-e2e-random-bidirectional-block-ops", :vars - {:sync-port "18080", - :sync-http-base "http://127.0.0.1:18080", - :random-seed "424242", + {:random-seed "424242", :random-page "SyncRandomOpsHome", - :auth-path "{{tmp-dir}}/home/logseq/auth.json", - :home-dir "{{tmp-dir}}/home", - :sync-ws-url "ws://127.0.0.1:18080/sync/%s", - :cli-home "HOME='{{tmp-dir}}/home' {{cli}}", :rounds-per-client "100"}} {:tags [:stress :offline :bidirectional :random :block-ops], - :extends :sync/default, + :extends :sync/common, :setup - ["mkdir -p '{{tmp-dir}}/graphs-b'" - "mkdir -p '{{tmp-dir}}/home/logseq'" - "cp ~/logseq/auth.json '{{tmp-dir}}/home/logseq/auth.json'" - "python3 '{{repo-root}}/cli-e2e/scripts/prepare_sync_config.py' --output '{{config-path}}' --auth-path '{{auth-path}}' --http-base '{{sync-http-base}}' --ws-url '{{sync-ws-url}}'" - "python3 '{{repo-root}}/cli-e2e/scripts/prepare_sync_config.py' --output '{{tmp-dir}}/cli-b.edn' --auth-path '{{auth-path}}' --http-base '{{sync-http-base}}' --ws-url '{{sync-ws-url}}'" - "python3 '{{repo-root}}/cli-e2e/scripts/db_sync_server.py' start --repo-root '{{repo-root}}' --pid-file '{{tmp-dir}}/db-sync-server.pid' --log-file '{{tmp-dir}}/db-sync-server.log' --data-dir '{{tmp-dir}}/db-sync-server-data' --port {{sync-port}} --auth-path '{{auth-path}}'" - "{{cli-home}} --data-dir {{data-dir-arg}} --config {{config-path-arg}} --output json graph create --graph {{graph-arg}} >/dev/null" - "{{cli-home}} --data-dir {{data-dir-arg}} --config {{config-path-arg}} --output json sync ensure-keys --graph {{graph-arg}} --e2ee-password '11111' --upload-keys" - "{{cli-home}} --data-dir {{data-dir-arg}} --config {{config-path-arg}} --output json upsert page --graph {{graph-arg}} --page '{{random-page}}' >/dev/null" + ["{{cli-home}} --data-dir {{data-dir-arg}} --config {{config-path-arg}} --output json upsert page --graph {{graph-arg}} --page '{{random-page}}' >/dev/null" "{{cli-home}} --data-dir {{data-dir-arg}} --config {{config-path-arg}} --output json upsert block --graph {{graph-arg}} --target-page '{{random-page}}' --content '{{seed-marker}}' >/dev/null"], :cmds - ["{{cli-home}} --data-dir {{data-dir-arg}} --config {{config-path-arg}} --output json sync upload --graph {{graph-arg}}" - "{{cli-home}} --data-dir {{data-dir-arg}} --config {{config-path-arg}} --output json sync start --graph {{graph-arg}} --e2ee-password '11111'" - "python3 '{{repo-root}}/cli-e2e/scripts/wait_sync_status.py' --cli '{{repo-root}}/static/logseq-cli.js' --data-dir '{{data-dir}}' --config '{{config-path}}' --graph '{{graph}}' --timeout-s 120 --interval-s 1" - "{{cli-home}} --data-dir '{{tmp-dir}}/graphs-b' --config '{{tmp-dir}}/cli-b.edn' --output json sync download --graph {{graph-arg}} --e2ee-password '11111'" - "{{cli-home}} --data-dir '{{tmp-dir}}/graphs-b' --config '{{tmp-dir}}/cli-b.edn' --output json sync start --graph {{graph-arg}} --e2ee-password '11111'" - "python3 '{{repo-root}}/cli-e2e/scripts/wait_sync_status.py' --cli '{{repo-root}}/static/logseq-cli.js' --data-dir '{{tmp-dir}}/graphs-b' --config '{{tmp-dir}}/cli-b.edn' --graph '{{graph}}' --timeout-s 120 --interval-s 1" - "python3 '{{repo-root}}/cli-e2e/scripts/compare_graph_queries.py' --cli '{{repo-root}}/static/logseq-cli.js' --graph '{{graph}}' --config-a '{{config-path}}' --data-dir-a '{{data-dir}}' --config-b '{{tmp-dir}}/cli-b.edn' --data-dir-b '{{tmp-dir}}/graphs-b' --query '[:find ?title :where [?b :block/title ?title] [(= ?title \"{{seed-marker}}\")] ]' --require-result" + ["python3 '{{repo-root}}/cli-e2e/scripts/compare_graph_queries.py' --cli '{{repo-root}}/static/logseq-cli.js' --graph '{{graph}}' --config-a '{{config-path}}' --data-dir-a '{{data-dir}}' --config-b '{{tmp-dir}}/cli-b.edn' --data-dir-b '{{tmp-dir}}/graphs-b' --query '[:find ?title :where [?b :block/title ?title] [(= ?title \"{{seed-marker}}\")] ]' --require-result" "{{cli-home}} --data-dir {{data-dir-arg}} --config {{config-path-arg}} --output json sync stop --graph {{graph-arg}}" "{{cli-home}} --data-dir '{{tmp-dir}}/graphs-b' --config '{{tmp-dir}}/cli-b.edn' --output json sync stop --graph {{graph-arg}}" "python3 '{{repo-root}}/cli-e2e/scripts/random_bidirectional_block_ops.py' --cli '{{repo-root}}/static/logseq-cli.js' --graph '{{graph}}' --config-a '{{config-path}}' --data-dir-a '{{data-dir}}' --config-b '{{tmp-dir}}/cli-b.edn' --data-dir-b '{{tmp-dir}}/graphs-b' --page '{{random-page}}' --rounds-per-client {{rounds-per-client}} --seed {{random-seed}}" @@ -286,13 +174,7 @@ :id "sync-offline-random-bidirectional-block-ops", :graph "sync-e2e-offline-random-bidirectional-block-ops", :vars - {:sync-port "18080", - :seed-marker "sync-offline-random-seed-marker", - :sync-http-base "http://127.0.0.1:18080", + {:seed-marker "sync-offline-random-seed-marker", :random-seed "989898", :random-page "SyncOfflineRandomOpsHome", - :auth-path "{{tmp-dir}}/home/logseq/auth.json", - :home-dir "{{tmp-dir}}/home", - :sync-ws-url "ws://127.0.0.1:18080/sync/%s", - :cli-home "HOME='{{tmp-dir}}/home' {{cli}}", :rounds-per-client "100"}}]} diff --git a/cli-e2e/src/logseq/cli/e2e/manifests.clj b/cli-e2e/src/logseq/cli/e2e/manifests.clj index 456bb284f7..3032052f5d 100644 --- a/cli-e2e/src/logseq/cli/e2e/manifests.clj +++ b/cli-e2e/src/logseq/cli/e2e/manifests.clj @@ -45,14 +45,109 @@ (defn- normalize-extends [extends] - (cond - (nil? extends) [] - (keyword? extends) [extends] - (vector? extends) extends - :else - (throw (ex-info "Invalid :extends value in cli-e2e manifest" - {:extends extends - :expected "keyword | vector | nil"})))) + (let [extends' (cond + (nil? extends) [] + (keyword? extends) [extends] + (vector? extends) extends + :else + (throw (ex-info "Invalid :extends value in cli-e2e manifest" + {:extends extends + :expected "keyword | vector | nil"}))) + invalid-entries (remove keyword? extends')] + (when (seq invalid-entries) + (throw (ex-info "Invalid :extends entries in cli-e2e manifest" + {:extends extends + :invalid-entries (vec invalid-entries) + :expected "keyword | vector | nil"}))) + extends')) + +(defn- parse-manifest + [manifest-data] + (if-not (map? manifest-data) + (throw (ex-info "Invalid cli-e2e manifest format" + {:manifest-type (type manifest-data) + :expected "{:templates {...} :cases [...]}"})) + (let [templates (or (:templates manifest-data) {}) + cases (:cases manifest-data)] + (when-not (map? templates) + (throw (ex-info "Invalid cli-e2e manifest :templates format" + {:templates templates + :expected "map"}))) + (when-not (vector? cases) + (throw (ex-info "Invalid cli-e2e manifest :cases format" + {:cases cases + :expected "vector"}))) + (doseq [case cases] + (when-not (map? case) + (throw (ex-info "Invalid cli-e2e case format" + {:case case + :expected "map"})))) + {:templates templates + :cases cases}))) + +(defn- reachable-template-ids + [templates roots] + (loop [stack (vec roots) + visited #{}] + (if-let [template-id (peek stack)] + (if (contains? visited template-id) + (recur (pop stack) visited) + (let [template (get templates template-id) + parent-ids (if template + (normalize-extends (:extends template)) + [])] + (recur (into (pop stack) parent-ids) + (conj visited template-id)))) + visited))) + +(defn- lint-manifest! + [{:keys [templates cases]}] + (let [template-ids (set (keys templates)) + template-refs (mapcat (fn [[template-id template]] + (map (fn [target] + {:type :invalid-extends + :source-type :template + :source template-id + :target target}) + (normalize-extends (:extends template)))) + templates) + case-refs (mapcat (fn [[index case]] + (map (fn [target] + {:type :invalid-extends + :source-type :case + :source (or (:id case) + (str "case#" (inc index))) + :target target}) + (normalize-extends (:extends case)))) + (map-indexed vector cases)) + invalid-extends-issues (->> (concat template-refs case-refs) + (filter (fn [{:keys [target]}] + (not (contains? template-ids target))))) + duplicate-id-issues (->> cases + (keep :id) + frequencies + (filter (fn [[_ count]] (> count 1))) + (sort-by first) + (map (fn [[case-id count]] + {:type :duplicate-case-id + :id case-id + :count count}))) + roots (->> cases + (mapcat #(normalize-extends (:extends %))) + distinct) + used-template-ids (reachable-template-ids templates roots) + unused-template-issues (->> (keys templates) + (remove used-template-ids) + sort + (map (fn [template-id] + {:type :unused-template + :template template-id}))) + issues (vec (concat invalid-extends-issues + duplicate-id-issues + unused-template-issues))] + (when (seq issues) + (throw (ex-info "cli-e2e manifest lint failed" + {:issues issues}))))) (defn- as-seq [value] @@ -119,38 +214,24 @@ [(dissoc template :extends)]))))) (defn- expand-manifest-cases - [manifest-data] - (cond - (vector? manifest-data) - (vec manifest-data) - - (map? manifest-data) - (let [templates (:templates manifest-data {}) - cases (:cases manifest-data)] - (when-not (vector? cases) - (throw (ex-info "Invalid cli-e2e manifest format" - {:manifest-type :map - :expected "{:templates {...} :cases [...] }"}))) - (mapv (fn [case] - (let [parent-ids (normalize-extends (:extends case)) - parent-values (map #(resolve-template templates % []) - parent-ids)] - (reduce merge-entry - {} - (concat parent-values - [(dissoc case :extends)])))) - cases)) - - :else - (throw (ex-info "Invalid cli-e2e manifest format" - {:manifest-type (type manifest-data) - :expected "vector | {:templates ... :cases ...}"})))) + [{:keys [templates cases]}] + (mapv (fn [case] + (let [parent-ids (normalize-extends (:extends case)) + parent-values (map #(resolve-template templates % []) + parent-ids)] + (reduce merge-entry + {} + (concat parent-values + [(dissoc case :extends)])))) + cases)) (defn load-cases ([] (load-cases nil)) ([suite] - (-> (manifest-file suite :cases) - paths/spec-path - read-edn-file - expand-manifest-cases))) + (let [manifest-data (-> (manifest-file suite :cases) + paths/spec-path + read-edn-file + parse-manifest)] + (lint-manifest! manifest-data) + (expand-manifest-cases manifest-data)))) diff --git a/cli-e2e/test/logseq/cli/e2e/main_test.clj b/cli-e2e/test/logseq/cli/e2e/main_test.clj index 298a35d3ed..7f3cb02815 100644 --- a/cli-e2e/test/logseq/cli/e2e/main_test.clj +++ b/cli-e2e/test/logseq/cli/e2e/main_test.clj @@ -295,6 +295,72 @@ (is (string/includes? output "Slow steps (top 10):")) (is (string/includes? output "main-graph-list")))) +(deftest test-without-timings-keeps-output-concise + (let [output (with-out-str + (main/test! {:inventory complete-inventory + :cases sample-cases + :include ["smoke"] + :skip-build true + :run-command (fn [_] + {:exit 0 + :out "" + :err ""}) + :run-case (fn [case _opts] + {:id (:id case) + :status :ok + :timings [{:phase :main + :step-index 1 + :step-total 1 + :elapsed-ms 88 + :status :ok + :cmd "hidden-step"}]})}))] + (is (not (string/includes? output "==> Step timing enabled (--timings)"))) + (is (not (string/includes? output "step timings:"))) + (is (not (string/includes? output "Slow steps (top 10):"))))) + +(deftest test-sync-timings-prints-step-details-and-slow-summary + (let [sync-inventory {:excluded-command-prefixes ["login" "logout"] + :scopes {:sync {:commands ["sync status"] + :options []}}} + sync-cases [{:id "sync-status-case" + :cmds ["node static/logseq-cli.js sync status"] + :covers {:commands ["sync status"]}}] + output (with-out-str + (main/test-sync! {:inventory sync-inventory + :cases sync-cases + :skip-build true + :timings true + :run-command (fn [_] + {:exit 0 + :out "" + :err ""}) + :run-case (fn [case _opts] + {:id (:id case) + :status :ok + :timings [{:phase :setup + :step-index 1 + :step-total 1 + :elapsed-ms 10 + :status :ok + :cmd "sync-setup"} + {:phase :main + :step-index 1 + :step-total 1 + :elapsed-ms 150 + :status :ok + :cmd "sync-main"} + {:phase :cleanup + :step-index 1 + :step-total 1 + :elapsed-ms 20 + :status :ok + :cmd "sync-cleanup"}]})}))] + (is (string/includes? output "==> Running cli-e2e cases")) + (is (string/includes? output "==> Step timing enabled (--timings)")) + (is (string/includes? output "step timings:")) + (is (string/includes? output "Slow steps (top 10):")) + (is (string/includes? output "sync-main")))) + (deftest test-help-prints-usage-and-skips-execution (let [ran? (atom false) result (atom nil) diff --git a/cli-e2e/test/logseq/cli/e2e/manifests_test.clj b/cli-e2e/test/logseq/cli/e2e/manifests_test.clj index 4c2d5d3291..71fc6c3357 100644 --- a/cli-e2e/test/logseq/cli/e2e/manifests_test.clj +++ b/cli-e2e/test/logseq/cli/e2e/manifests_test.clj @@ -2,69 +2,89 @@ (:require [clojure.test :refer [deftest is testing]] [logseq.cli.e2e.manifests :as manifests])) -(deftest load-cases-supports-legacy-vector-format +(deftest load-cases-requires-new-map-format (with-redefs [manifests/read-edn-file (fn [_] [{:id "legacy-a"} {:id "legacy-b"}])] - (is (= ["legacy-a" "legacy-b"] - (mapv :id (manifests/load-cases :non-sync)))))) + (let [error (try + (manifests/load-cases :non-sync) + nil + (catch clojure.lang.ExceptionInfo error + error))] + (is (some? error)) + (is (re-find #"Invalid cli-e2e manifest format" (.getMessage error))) + (is (= "{:templates {...} :cases [...]}" + (:expected (ex-data error))))))) -(deftest load-cases-supports-templates-and-inheritance - (with-redefs [manifests/read-edn-file (fn [_] - {:templates - {:base {:setup ["setup-a"] - :cmds ["cmd-a"] - :cleanup ["cleanup-a"] - :tags [:base] - :vars {:nested {:left 1} - :only-base true} - :covers {:commands ["base-command"] - :options {:global ["--base"]}} - :expect {:stdout-json-paths {[:status] "ok"}} - :graph "base-graph"} - :addon {:setup ["setup-b"] - :cmds ["cmd-b"] - :cleanup ["cleanup-b"] - :tags [:addon] - :vars {:nested {:right 2}} - :covers {:options {:graph ["--addon"]}} - :expect {:stdout-json-paths {[:data :x] 1}} - :graph "addon-graph"}} - :cases - [{:id "templated" - :extends [:base :addon] - :setup ["setup-case"] - :cmds ["cmd-case"] - :cleanup ["cleanup-case"] - :tags [:case] - :vars {:nested {:leaf 3} - :only-case true} - :covers {:options {:graph ["--case"]}} - :expect {:stdout-json-paths {[:data :y] 2}} - :graph "case-graph"}]})] - (let [case (first (manifests/load-cases :sync))] +(deftest load-cases-merges-templates-and-cases-by-rules + (with-redefs [manifests/read-edn-file + (fn [_] + {:templates + {:base {:setup ["setup-a"] + :cmds ["cmd-a"] + :cleanup ["cleanup-a"] + :tags [:base] + :vars {:nested {:left 1} + :only-base true} + :covers {:commands ["base-command"] + :options {:global ["--base"]}} + :expect {:stdout-json-paths {[:status] "ok" + [:data :base] 1}} + :graph "base-graph"} + :addon {:setup ["setup-b"] + :cmds ["cmd-b"] + :cleanup ["cleanup-b"] + :tags [:addon] + :vars {:nested {:right 2}} + :covers {:options {:graph ["--addon"]}} + :expect {:stdout-json-paths {[:data :addon] 2}} + :graph "addon-graph"}} + :cases + [{:id "single-parent" + :extends :base + :cmds ["cmd-case"] + :expect {:stdout-json-paths {[:data :case] 3}} + :graph "single-parent-graph"} + {:id "multi-parent" + :extends [:base :addon] + :setup ["setup-case"] + :cmds ["cmd-case"] + :cleanup ["cleanup-case"] + :tags [:case] + :vars {:nested {:leaf 3} + :only-case true} + :covers {:commands ["case-command"] + :options {:graph ["--case"]}} + :expect {:stdout-json-paths {[:data :case] 3}} + :graph "case-graph"}]})] + (let [[single-parent multi-parent] (manifests/load-cases :sync)] + (testing "supports :extends keyword" + (is (= "single-parent" (:id single-parent))) + (is (= ["cmd-a" "cmd-case"] (:cmds single-parent))) + (is (= "single-parent-graph" (:graph single-parent)))) (testing "append merge keys" - (is (= ["setup-a" "setup-b" "setup-case"] (:setup case))) - (is (= ["cmd-a" "cmd-b" "cmd-case"] (:cmds case))) - (is (= ["cleanup-a" "cleanup-b" "cleanup-case"] (:cleanup case))) - (is (= [:base :addon :case] (:tags case)))) + (is (= ["setup-a" "setup-b" "setup-case"] (:setup multi-parent))) + (is (= ["cmd-a" "cmd-b" "cmd-case"] (:cmds multi-parent))) + (is (= ["cleanup-a" "cleanup-b" "cleanup-case"] (:cleanup multi-parent))) + (is (= [:base :addon :case] (:tags multi-parent)))) (testing "deep merge keys" (is (= {:nested {:left 1 :right 2 :leaf 3} :only-base true :only-case true} - (:vars case))) - (is (= {:commands ["base-command"] + (:vars multi-parent))) + (is (= {:commands ["case-command"] :options {:global ["--base"] :graph ["--case"]}} - (:covers case))) + (:covers multi-parent))) (is (= {[:status] "ok" - [:data :x] 1 - [:data :y] 2} - (get-in case [:expect :stdout-json-paths])))) - (testing "scalar child override" - (is (= "case-graph" (:graph case))))))) + [:data :base] 1 + [:data :addon] 2 + [:data :case] 3} + (get-in multi-parent [:expect :stdout-json-paths])))) + (testing "scalar keys are overridden by child" + (is (= "case-graph" (:graph multi-parent))))))) -(deftest load-cases-detects-circular-template-inheritance +(deftest load-cases-detects-circular-template-inheritance-with-cycle-path (with-redefs [manifests/read-edn-file (fn [_] {:templates {:a {:extends :b @@ -72,7 +92,76 @@ :b {:extends :a :setup ["b"]}} :cases [{:id "cycle" :extends :a}]})] - (is (thrown-with-msg? - clojure.lang.ExceptionInfo - #"Circular template inheritance" - (manifests/load-cases :sync))))) + (let [error (try + (manifests/load-cases :sync) + nil + (catch clojure.lang.ExceptionInfo error + error))] + (is (some? error)) + (is (re-find #"Circular template inheritance" (.getMessage error))) + (is (= [:a :b :a] (:cycle (ex-data error))))))) + +(deftest load-cases-validates-extends-entries + (with-redefs [manifests/read-edn-file (fn [_] + {:templates {:base {:cmds ["base"]}} + :cases [{:id "invalid-extends" + :extends [:base "bad"] + :cmds ["case"]}]})] + (let [error (try + (manifests/load-cases :non-sync) + nil + (catch clojure.lang.ExceptionInfo error + error))] + (is (some? error)) + (is (re-find #"Invalid :extends entries" (.getMessage error))) + (is (= ["bad"] (:invalid-entries (ex-data error))))))) + +(deftest load-cases-lint-detects-invalid-extends-references + (with-redefs [manifests/read-edn-file (fn [_] + {:templates {:base {:cmds ["base"]} + :unused {:extends :missing-template + :cmds ["unused"]}} + :cases [{:id "valid" :extends :base :cmds ["case"]} + {:id "invalid" :extends :also-missing :cmds ["case"]}]})] + (let [error (try + (manifests/load-cases :sync) + nil + (catch clojure.lang.ExceptionInfo error + error)) + issues (:issues (ex-data error))] + (is (some? error)) + (is (re-find #"manifest lint failed" (.getMessage error))) + (is (= #{:also-missing :missing-template} + (set (map :target (filter #(= :invalid-extends (:type %)) issues)))))))) + +(deftest load-cases-lint-detects-duplicate-case-ids + (with-redefs [manifests/read-edn-file (fn [_] + {:templates {:base {:cmds ["base"]}} + :cases [{:id "dup" :extends :base :cmds ["case-a"]} + {:id "dup" :extends :base :cmds ["case-b"]}]})] + (let [error (try + (manifests/load-cases :non-sync) + nil + (catch clojure.lang.ExceptionInfo error + error)) + duplicate-issues (filter #(= :duplicate-case-id (:type %)) + (:issues (ex-data error)))] + (is (some? error)) + (is (= [{:type :duplicate-case-id :id "dup" :count 2}] + (vec duplicate-issues)))))) + +(deftest load-cases-lint-detects-unused-templates + (with-redefs [manifests/read-edn-file (fn [_] + {:templates {:base {:cmds ["base"]} + :unused {:cmds ["unused"]}} + :cases [{:id "only" :extends :base :cmds ["case"]}]})] + (let [error (try + (manifests/load-cases :sync) + nil + (catch clojure.lang.ExceptionInfo error + error)) + unused-issues (filter #(= :unused-template (:type %)) + (:issues (ex-data error)))] + (is (some? error)) + (is (= [{:type :unused-template :template :unused}] + (vec unused-issues))))))