#!/usr/bin/env bash set -euo pipefail ROOT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")/.." && pwd)" HTTP_HELPER="${ROOT_DIR}/build/amduatd_http_unix" USE_HTTP_HELPER=0 TMPDIR="${TMPDIR:-/tmp}" mkdir -p "${TMPDIR}" if ! command -v grep >/dev/null 2>&1; then echo "skip: grep not found" >&2 exit 77 fi if ! command -v awk >/dev/null 2>&1; then echo "skip: awk not found" >&2 exit 77 fi if command -v curl >/dev/null 2>&1; then if curl --help 2>/dev/null | grep -q -- '--unix-socket'; then USE_HTTP_HELPER=0 else USE_HTTP_HELPER=1 fi else USE_HTTP_HELPER=1 fi if [[ "${USE_HTTP_HELPER}" -eq 1 && ! -x "${HTTP_HELPER}" ]]; then echo "skip: curl lacks --unix-socket support and helper missing" >&2 exit 77 fi AMDUATD_BIN="${ROOT_DIR}/build/amduatd" ASL_BIN="${ROOT_DIR}/vendor/amduat/build/amduat-asl" if [[ ! -x "${AMDUATD_BIN}" || ! -x "${ASL_BIN}" ]]; then echo "missing binaries; build amduatd and amduat-asl first" >&2 exit 1 fi tmp_root="$(mktemp -d -p "${TMPDIR}" amduatd-graph-queries-XXXXXX)" root="${tmp_root}/root" sock="${tmp_root}/amduatd.sock" space_id="graphq" log_file="${tmp_root}/amduatd.log" cleanup() { if [[ -n "${pid:-}" ]]; then kill "${pid}" >/dev/null 2>&1 || true fi rm -rf "${tmp_root}" } trap cleanup EXIT http_get() { local path="$1" shift if [[ "${USE_HTTP_HELPER}" -eq 1 ]]; then "${HTTP_HELPER}" --sock "${sock}" --method GET --path "${path}" "$@" else curl --silent --show-error --fail \ --unix-socket "${sock}" \ "$@" \ "http://localhost${path}" fi } http_post() { local path="$1" local data="$2" shift 2 if [[ "${USE_HTTP_HELPER}" -eq 1 ]]; then "${HTTP_HELPER}" --sock "${sock}" --method POST --path "${path}" \ --data "${data}" \ "$@" else curl --silent --show-error --fail \ --unix-socket "${sock}" \ "$@" \ --data-binary "${data}" \ "http://localhost${path}" fi } extract_json_string() { local key="$1" awk -v k="\"${key}\":\"" ' { pos = index($0, k); if (pos > 0) { rest = substr($0, pos + length(k)); end = index(rest, "\""); if (end > 0) { print substr(rest, 1, end - 1); exit 0; } } } ' } cursor_plus_one() { local token="$1" local n if [[ -z "${token}" ]]; then return 1 fi if [[ "${token}" == g1_* ]]; then n="${token#g1_}" printf 'g1_%s' "$((n + 1))" return 0 fi printf '%s' "$((token + 1))" } wait_for_ready() { local i for i in $(seq 1 100); do if ! kill -0 "${pid}" >/dev/null 2>&1; then if [[ -f "${log_file}" ]] && grep -q "bind: Operation not permitted" "${log_file}"; then echo "skip: bind not permitted for unix socket" >&2 exit 77 fi [[ -f "${log_file}" ]] && cat "${log_file}" >&2 return 1 fi if [[ -S "${sock}" ]] && http_get "/v1/meta" >/dev/null 2>&1; then return 0 fi sleep 0.1 done return 1 } create_artifact() { local payload="$1" local resp local ref resp="$( http_post "/v1/artifacts" "${payload}" \ --header "Content-Type: application/octet-stream" \ --header "X-Amduat-Space: ${space_id}" )" ref="$(printf '%s\n' "${resp}" | extract_json_string "ref")" if [[ -z "${ref}" ]]; then echo "failed to parse artifact ref: ${resp}" >&2 exit 1 fi printf '%s' "${ref}" } create_node() { local name="$1" local ref="$2" http_post "/v2/graph/nodes" "{\"name\":\"${name}\",\"ref\":\"${ref}\"}" \ --header "Content-Type: application/json" \ --header "X-Amduat-Space: ${space_id}" >/dev/null } create_edge() { local subject="$1" local predicate="$2" local object="$3" http_post "/v2/graph/edges" \ "{\"subject\":\"${subject}\",\"predicate\":\"${predicate}\",\"object\":\"${object}\"}" \ --header "Content-Type: application/json" \ --header "X-Amduat-Space: ${space_id}" >/dev/null } create_edge_with_metadata() { local subject="$1" local predicate="$2" local object="$3" local metadata_ref="$4" http_post "/v2/graph/edges" \ "{\"subject\":\"${subject}\",\"predicate\":\"${predicate}\",\"object\":\"${object}\",\"metadata_ref\":\"${metadata_ref}\"}" \ --header "Content-Type: application/json" \ --header "X-Amduat-Space: ${space_id}" >/dev/null } start_daemon() { "${AMDUATD_BIN}" --root "${root}" --sock "${sock}" --store-backend index --space "${space_id}" \ >"${log_file}" 2>&1 & pid=$! if ! wait_for_ready; then echo "daemon not ready" >&2 exit 1 fi } restart_daemon() { if [[ -n "${pid:-}" ]]; then kill "${pid}" >/dev/null 2>&1 || true wait "${pid}" >/dev/null 2>&1 || true fi start_daemon } mkdir -p "${root}" "${ASL_BIN}" init --root "${root}" start_daemon ref_a="$(create_artifact "payload-a")" ref_b="$(create_artifact "payload-b")" ref_c="$(create_artifact "payload-c")" ref_d="$(create_artifact "payload-d")" ref_e="$(create_artifact "payload-e")" ref_e2="$(create_artifact "payload-e2")" ref_v1="$(create_artifact "payload-v1")" ref_v2="$(create_artifact "payload-v2")" ref_p1="$(create_artifact "payload-provenance-1")" ref_p2="$(create_artifact "payload-provenance-2")" create_node "gq-a" "${ref_a}" create_node "gq-b" "${ref_b}" create_node "gq-c" "${ref_c}" create_node "gq-d" "${ref_d}" create_node "gq-e" "${ref_e}" create_node "gq-v" "${ref_v1}" create_node "gq-prov1" "${ref_p1}" create_node "gq-prov2" "${ref_p2}" # Seed path and neighbor data in a controlled edge order. create_edge "gq-a" "ms.within_domain" "gq-b" create_edge "gq-b" "ms.within_domain" "gq-c" create_edge "gq-a" "ms.computed_by" "gq-b" cutoff_resp="$( http_get "/v2/graph/changes?event_types[]=edge_appended&limit=100" \ --header "X-Amduat-Space: ${space_id}" )" cutoff_cursor="$(printf '%s\n' "${cutoff_resp}" | extract_json_string "next_cursor")" if [[ -z "${cutoff_cursor}" ]]; then echo "failed to parse cutoff cursor: ${cutoff_resp}" >&2 exit 1 fi cutoff_cursor="$(cursor_plus_one "${cutoff_cursor}")" create_edge "gq-a" "ms.computed_by" "gq-d" create_edge "gq-a" "ms.within_domain" "gq-c" search_page1="$( http_get "/v2/graph/search?name_prefix=gq-&limit=2" \ --header "X-Amduat-Space: ${space_id}" )" search_cursor="$(printf '%s\n' "${search_page1}" | extract_json_string "next_cursor")" if [[ -z "${search_cursor}" ]]; then echo "missing search cursor in page1: ${search_page1}" >&2 exit 1 fi search_page2="$( http_get "/v2/graph/search?name_prefix=gq-&limit=10&cursor=${search_cursor}" \ --header "X-Amduat-Space: ${space_id}" )" search_joined="${search_page1} ${search_page2}" echo "${search_joined}" | grep -q '"name":"gq-a"' || { echo "search missing gq-a" >&2; exit 1; } echo "${search_joined}" | grep -q '"name":"gq-b"' || { echo "search missing gq-b" >&2; exit 1; } echo "${search_joined}" | grep -q '"name":"gq-c"' || { echo "search missing gq-c" >&2; exit 1; } echo "${search_joined}" | grep -q '"name":"gq-d"' || { echo "search missing gq-d" >&2; exit 1; } neighbors_page1="$( http_get "/v2/graph/nodes/gq-a/neighbors?dir=outgoing&predicate=ms.computed_by&limit=1&expand_names=true" \ --header "X-Amduat-Space: ${space_id}" )" neighbors_cursor="$(printf '%s\n' "${neighbors_page1}" | extract_json_string "next_cursor")" if [[ -z "${neighbors_cursor}" ]]; then echo "missing neighbors cursor in page1: ${neighbors_page1}" >&2 exit 1 fi neighbors_page2="$( http_get "/v2/graph/nodes/gq-a/neighbors?dir=outgoing&predicate=ms.computed_by&limit=10&cursor=${neighbors_cursor}&expand_names=true" \ --header "X-Amduat-Space: ${space_id}" )" neighbors_joined="${neighbors_page1} ${neighbors_page2}" echo "${neighbors_joined}" | grep -q '"neighbor_name":"gq-b"' || { echo "neighbors missing gq-b" >&2; exit 1; } echo "${neighbors_joined}" | grep -q '"neighbor_name":"gq-d"' || { echo "neighbors missing gq-d" >&2; exit 1; } neighbors_asof="$( http_get "/v2/graph/nodes/gq-a/neighbors?dir=outgoing&predicate=ms.computed_by&limit=10&as_of=${cutoff_cursor}&expand_names=true" \ --header "X-Amduat-Space: ${space_id}" )" echo "${neighbors_asof}" | grep -q '"neighbor_name":"gq-b"' || { echo "neighbors as_of missing gq-b" >&2; exit 1; } if echo "${neighbors_asof}" | grep -q '"neighbor_name":"gq-d"'; then echo "neighbors as_of unexpectedly includes gq-d" >&2 exit 1 fi paths_latest="$( http_get "/v2/graph/paths?from=gq-a&to=gq-c&predicate=ms.within_domain&max_depth=4&expand_names=true" \ --header "X-Amduat-Space: ${space_id}" )" echo "${paths_latest}" | grep -q '"depth":1' || { echo "paths latest expected direct depth 1: ${paths_latest}" >&2 exit 1 } paths_asof="$( http_get "/v2/graph/paths?from=gq-a&to=gq-c&predicate=ms.within_domain&max_depth=4&as_of=${cutoff_cursor}&expand_names=true" \ --header "X-Amduat-Space: ${space_id}" )" echo "${paths_asof}" | grep -q '"depth":2' || { echo "paths as_of expected historical depth 2: ${paths_asof}" >&2 exit 1 } subgraph_page1="$( http_get "/v2/graph/subgraph?roots[]=gq-a&max_depth=2&predicates[]=ms.computed_by&dir=outgoing&limit_edges=1&limit_nodes=10&include_versions=true" \ --header "X-Amduat-Space: ${space_id}" )" subgraph_cursor="$(printf '%s\n' "${subgraph_page1}" | extract_json_string "next_cursor")" if [[ -z "${subgraph_cursor}" ]]; then echo "missing subgraph cursor in page1: ${subgraph_page1}" >&2 exit 1 fi subgraph_page2="$( http_get "/v2/graph/subgraph?roots[]=gq-a&max_depth=2&predicates[]=ms.computed_by&dir=outgoing&limit_edges=10&limit_nodes=10&cursor=${subgraph_cursor}" \ --header "X-Amduat-Space: ${space_id}" )" subgraph_joined="${subgraph_page1} ${subgraph_page2}" echo "${subgraph_joined}" | grep -q '"name":"gq-a"' || { echo "subgraph missing root node gq-a" >&2; exit 1; } echo "${subgraph_joined}" | grep -q '"name":"gq-b"' || { echo "subgraph missing gq-b" >&2; exit 1; } echo "${subgraph_joined}" | grep -q '"name":"gq-d"' || { echo "subgraph missing gq-d" >&2; exit 1; } echo "${subgraph_joined}" | grep -q '"versions":\[' || { echo "subgraph include_versions missing versions" >&2; exit 1; } subgraph_asof="$( http_get "/v2/graph/subgraph?roots[]=gq-a&max_depth=2&predicates[]=ms.computed_by&dir=outgoing&as_of=${cutoff_cursor}&limit_edges=10&limit_nodes=10" \ --header "X-Amduat-Space: ${space_id}" )" echo "${subgraph_asof}" | grep -q '"name":"gq-b"' || { echo "subgraph as_of missing gq-b" >&2; exit 1; } if echo "${subgraph_asof}" | grep -q '"name":"gq-d"'; then echo "subgraph as_of unexpectedly includes gq-d" >&2 exit 1 fi gqd_edge_resp="$( http_get "/v2/graph/edges?subject=gq-a&predicate=ms.computed_by&object=gq-d&dir=outgoing&limit=1" \ --header "X-Amduat-Space: ${space_id}" )" gqd_edge_ref="$(printf '%s\n' "${gqd_edge_resp}" | extract_json_string "edge_ref")" if [[ -z "${gqd_edge_ref}" ]]; then echo "failed to parse gq-a->gq-d edge ref: ${gqd_edge_resp}" >&2 exit 1 fi http_post "/v2/graph/edges/tombstone" \ "{\"edge_ref\":\"${gqd_edge_ref}\"}" \ --header "Content-Type: application/json" \ --header "X-Amduat-Space: ${space_id}" >/dev/null subgraph_after_tombstone="$( http_get "/v2/graph/subgraph?roots[]=gq-a&max_depth=2&predicates[]=ms.computed_by&dir=outgoing&limit_edges=10&limit_nodes=10" \ --header "X-Amduat-Space: ${space_id}" )" if echo "${subgraph_after_tombstone}" | grep -q '"name":"gq-d"'; then echo "subgraph after tombstone unexpectedly includes gq-d: ${subgraph_after_tombstone}" >&2 exit 1 fi subgraph_include_tombstoned="$( http_get "/v2/graph/subgraph?roots[]=gq-a&max_depth=2&predicates[]=ms.computed_by&dir=outgoing&include_tombstoned=true&limit_edges=10&limit_nodes=10" \ --header "X-Amduat-Space: ${space_id}" )" echo "${subgraph_include_tombstoned}" | grep -q '"name":"gq-d"' || { echo "subgraph include_tombstoned missing gq-d: ${subgraph_include_tombstoned}" >&2 exit 1 } edges_include_tombstoned="$( http_get "/v2/graph/edges?subject=gq-a&predicate=ms.computed_by&dir=outgoing&include_tombstoned=true&limit=10&expand_names=true" \ --header "X-Amduat-Space: ${space_id}" )" echo "${edges_include_tombstoned}" | grep -q '"object_name":"gq-d"' || { echo "edges include_tombstoned missing gq-d: ${edges_include_tombstoned}" >&2 exit 1 } neighbors_after_tombstone="$( http_get "/v2/graph/nodes/gq-a/neighbors?dir=outgoing&predicate=ms.computed_by&limit=10&expand_names=true" \ --header "X-Amduat-Space: ${space_id}" )" if echo "${neighbors_after_tombstone}" | grep -q '"neighbor_name":"gq-d"'; then echo "neighbors default should exclude tombstoned gq-d edge: ${neighbors_after_tombstone}" >&2 exit 1 fi neighbors_include_tombstoned="$( http_get "/v2/graph/nodes/gq-a/neighbors?dir=outgoing&predicate=ms.computed_by&include_tombstoned=true&limit=10&expand_names=true" \ --header "X-Amduat-Space: ${space_id}" )" echo "${neighbors_include_tombstoned}" | grep -q '"neighbor_name":"gq-d"' || { echo "neighbors include_tombstoned missing gq-d: ${neighbors_include_tombstoned}" >&2 exit 1 } paths_excluding_tombstoned="$( http_get "/v2/graph/paths?from=gq-a&to=gq-d&predicate=ms.computed_by&max_depth=2" \ --header "X-Amduat-Space: ${space_id}" )" echo "${paths_excluding_tombstoned}" | grep -q '"paths":\[\]' || { echo "paths default should exclude tombstoned edge: ${paths_excluding_tombstoned}" >&2 exit 1 } paths_include_tombstoned="$( http_get "/v2/graph/paths?from=gq-a&to=gq-d&predicate=ms.computed_by&max_depth=2&include_tombstoned=true" \ --header "X-Amduat-Space: ${space_id}" )" echo "${paths_include_tombstoned}" | grep -q '"depth":1' || { echo "paths include_tombstoned expected depth 1 path: ${paths_include_tombstoned}" >&2 exit 1 } create_edge_with_metadata "gq-b" "ms.computed_by" "gq-d" "gq-prov1" create_edge_with_metadata "gq-b" "ms.computed_by" "gq-a" "gq-prov2" neighbors_provenance="$( http_get "/v2/graph/nodes/gq-b/neighbors?dir=outgoing&predicate=ms.computed_by&provenance_ref=gq-prov1&limit=10&expand_names=true" \ --header "X-Amduat-Space: ${space_id}" )" echo "${neighbors_provenance}" | grep -q '"neighbor_name":"gq-d"' || { echo "neighbors provenance filter missing gq-d: ${neighbors_provenance}" >&2 exit 1 } neighbors_provenance_missing="$( http_get "/v2/graph/nodes/gq-b/neighbors?dir=outgoing&predicate=ms.computed_by&provenance_ref=gq-prov-missing&limit=10" \ --header "X-Amduat-Space: ${space_id}" )" echo "${neighbors_provenance_missing}" | grep -q '"neighbors":\[\]' || { echo "neighbors unresolved provenance expected empty result: ${neighbors_provenance_missing}" >&2 exit 1 } paths_provenance_match="$( http_get "/v2/graph/paths?from=gq-b&to=gq-d&predicate=ms.computed_by&provenance_ref=gq-prov1&max_depth=2&expand_names=true" \ --header "X-Amduat-Space: ${space_id}" )" echo "${paths_provenance_match}" | grep -q '"depth":1' || { echo "paths provenance filter expected depth 1 path: ${paths_provenance_match}" >&2 exit 1 } echo "${paths_provenance_match}" | grep -q '"object_name":"gq-d"' || { echo "paths provenance filter missing gq-d: ${paths_provenance_match}" >&2 exit 1 } paths_provenance_excluded="$( http_get "/v2/graph/paths?from=gq-b&to=gq-a&predicate=ms.computed_by&provenance_ref=gq-prov1&max_depth=2" \ --header "X-Amduat-Space: ${space_id}" )" echo "${paths_provenance_excluded}" | grep -q '"paths":\[\]' || { echo "paths provenance filter unexpectedly includes gq-b->gq-a path: ${paths_provenance_excluded}" >&2 exit 1 } batch_resp="$( http_post "/v2/graph/batch" \ "{\"edges\":[{\"subject\":\"gq-c\",\"predicate\":\"ms.computed_by\",\"object\":\"gq-d\",\"metadata_ref\":\"gq-prov1\"}]}" \ --header "Content-Type: application/json" \ --header "X-Amduat-Space: ${space_id}" )" echo "${batch_resp}" | grep -q '"ok":true' || { echo "batch edge with metadata_ref failed: ${batch_resp}" >&2 exit 1 } echo "${batch_resp}" | grep -q '"results":\[' || { echo "batch response missing results array: ${batch_resp}" >&2 exit 1 } echo "${batch_resp}" | grep -q '"status":"applied"' || { echo "batch response missing applied item status: ${batch_resp}" >&2 exit 1 } batch_version_provenance="$( http_post "/v2/graph/batch" \ "{\"versions\":[{\"name\":\"gq-e\",\"ref\":\"${ref_e2}\",\"provenance\":{\"source_uri\":\"urn:test:gq-e-v2\",\"extractor\":\"graph-test\",\"confidence\":\"0.91\",\"observed_at\":1730000000000,\"ingested_at\":1730000000100,\"license\":\"test-only\",\"trace_id\":\"trace-gq-e-v2\"}}]}" \ --header "Content-Type: application/json" \ --header "X-Amduat-Space: ${space_id}" )" echo "${batch_version_provenance}" | grep -q '"ok":true' || { echo "batch version with provenance failed: ${batch_version_provenance}" >&2 exit 1 } echo "${batch_version_provenance}" | grep -q '"status":"applied"' || { echo "batch version with provenance missing applied status: ${batch_version_provenance}" >&2 exit 1 } batch_edge_provenance="$( http_post "/v2/graph/batch" \ "{\"edges\":[{\"subject\":\"gq-c\",\"predicate\":\"ms.computed_by\",\"object\":\"gq-a\",\"provenance\":{\"source_uri\":\"urn:test:gq-c-a\",\"extractor\":\"graph-test\",\"confidence\":\"0.87\",\"observed_at\":1730000000200,\"ingested_at\":1730000000300,\"trace_id\":\"trace-gq-c-a\"}}]}" \ --header "Content-Type: application/json" \ --header "X-Amduat-Space: ${space_id}" )" echo "${batch_edge_provenance}" | grep -q '"ok":true' || { echo "batch edge with provenance failed: ${batch_edge_provenance}" >&2 exit 1 } echo "${batch_edge_provenance}" | grep -q '"status":"applied"' || { echo "batch edge with provenance missing applied status: ${batch_edge_provenance}" >&2 exit 1 } http_post "/v2/graph/nodes/gq-v/versions" \ "{\"ref\":\"${ref_v2}\"}" \ --header "Content-Type: application/json" \ --header "X-Amduat-Space: ${space_id}" >/dev/null http_post "/v2/graph/nodes/gq-v/versions/tombstone" \ "{\"ref\":\"${ref_v2}\"}" \ --header "Content-Type: application/json" \ --header "X-Amduat-Space: ${space_id}" >/dev/null gqv_after_tombstone="$( http_get "/v2/graph/nodes/gq-v" \ --header "X-Amduat-Space: ${space_id}" )" gqv_latest="$(printf '%s\n' "${gqv_after_tombstone}" | extract_json_string "latest_ref")" if [[ "${gqv_latest}" != "${ref_v1}" ]]; then echo "version tombstone expected latest_ref=${ref_v1}, got ${gqv_latest}: ${gqv_after_tombstone}" >&2 exit 1 fi if echo "${gqv_after_tombstone}" | grep -q "\"ref\":\"${ref_v2}\""; then echo "version tombstone expected ${ref_v2} hidden by default: ${gqv_after_tombstone}" >&2 exit 1 fi gqv_include_tombstoned="$( http_get "/v2/graph/nodes/gq-v?include_tombstoned=true" \ --header "X-Amduat-Space: ${space_id}" )" gqv_latest_all="$(printf '%s\n' "${gqv_include_tombstoned}" | extract_json_string "latest_ref")" if [[ "${gqv_latest_all}" != "${ref_v2}" ]]; then echo "include_tombstoned expected latest_ref=${ref_v2}, got ${gqv_latest_all}: ${gqv_include_tombstoned}" >&2 exit 1 fi echo "${gqv_include_tombstoned}" | grep -q "\"ref\":\"${ref_v2}\"" || { echo "include_tombstoned expected historical version ${ref_v2}: ${gqv_include_tombstoned}" >&2 exit 1 } history_default="$( http_get "/v2/graph/history/gq-v" \ --header "X-Amduat-Space: ${space_id}" )" history_default_latest="$(printf '%s\n' "${history_default}" | extract_json_string "latest_ref")" if [[ "${history_default_latest}" != "${ref_v1}" ]]; then echo "history default expected latest_ref=${ref_v1}, got ${history_default_latest}: ${history_default}" >&2 exit 1 fi if echo "${history_default}" | grep -q "\"ref\":\"${ref_v2}\""; then echo "history default expected tombstoned version ${ref_v2} hidden: ${history_default}" >&2 exit 1 fi history_include_tombstoned="$( http_get "/v2/graph/history/gq-v?include_tombstoned=true" \ --header "X-Amduat-Space: ${space_id}" )" history_all_latest="$(printf '%s\n' "${history_include_tombstoned}" | extract_json_string "latest_ref")" if [[ "${history_all_latest}" != "${ref_v2}" ]]; then echo "history include_tombstoned expected latest_ref=${ref_v2}, got ${history_all_latest}: ${history_include_tombstoned}" >&2 exit 1 fi echo "${history_include_tombstoned}" | grep -q "\"ref\":\"${ref_v2}\"" || { echo "history include_tombstoned expected tombstoned version ${ref_v2}: ${history_include_tombstoned}" >&2 exit 1 } batch_continue="$( http_post "/v2/graph/batch" \ "{\"mode\":\"continue_on_error\",\"edges\":[{\"subject\":\"gq-a\",\"predicate\":\"ms.computed_by\",\"object\":\"gq-b\"},{\"subject\":\"gq-missing\",\"predicate\":\"ms.computed_by\",\"object\":\"gq-b\"}]}" \ --header "Content-Type: application/json" \ --header "X-Amduat-Space: ${space_id}" )" echo "${batch_continue}" | grep -q '"ok":false' || { echo "batch continue_on_error expected ok=false: ${batch_continue}" >&2 exit 1 } echo "${batch_continue}" | grep -q '"mode":"continue_on_error"' || { echo "batch continue_on_error mode echo missing: ${batch_continue}" >&2 exit 1 } echo "${batch_continue}" | grep -q '"status":"applied"' || { echo "batch continue_on_error missing applied result: ${batch_continue}" >&2 exit 1 } echo "${batch_continue}" | grep -q '"status":"error"' || { echo "batch continue_on_error missing error result: ${batch_continue}" >&2 exit 1 } idem_payload='{"idempotency_key":"gq-idem-1","mode":"continue_on_error","edges":[{"subject":"gq-b","predicate":"ms.computed_by","object":"gq-c"},{"subject":"gq-nope","predicate":"ms.computed_by","object":"gq-c"}]}' idem_first="$( http_post "/v2/graph/batch" \ "${idem_payload}" \ --header "Content-Type: application/json" \ --header "X-Amduat-Space: ${space_id}" )" idem_second="$( http_post "/v2/graph/batch" \ "${idem_payload}" \ --header "Content-Type: application/json" \ --header "X-Amduat-Space: ${space_id}" )" if [[ "${idem_first}" != "${idem_second}" ]]; then echo "idempotency replay response mismatch" >&2 echo "first=${idem_first}" >&2 echo "second=${idem_second}" >&2 exit 1 fi restart_daemon idem_third="$( http_post "/v2/graph/batch" \ "${idem_payload}" \ --header "Content-Type: application/json" \ --header "X-Amduat-Space: ${space_id}" )" if [[ "${idem_first}" != "${idem_third}" ]]; then echo "idempotency replay after restart mismatch" >&2 echo "first=${idem_first}" >&2 echo "third=${idem_third}" >&2 exit 1 fi query_include_versions="$( http_post "/v2/graph/query" \ "{\"where\":{\"subject\":\"gq-a\"},\"predicates\":[\"ms.within_domain\"],\"direction\":\"outgoing\",\"include_versions\":true,\"limit\":10}" \ --header "Content-Type: application/json" \ --header "X-Amduat-Space: ${space_id}" )" echo "${query_include_versions}" | grep -q '"versions":\[' || { echo "graph query include_versions missing versions: ${query_include_versions}" >&2 exit 1 } query_with_stats="$( http_post "/v2/graph/query" \ "{\"where\":{\"subject\":\"gq-a\"},\"predicates\":[\"ms.within_domain\"],\"direction\":\"outgoing\",\"include_stats\":true,\"max_result_bytes\":1048576,\"limit\":10}" \ --header "Content-Type: application/json" \ --header "X-Amduat-Space: ${space_id}" )" echo "${query_with_stats}" | grep -q '"stats":{' || { echo "graph query include_stats missing stats block: ${query_with_stats}" >&2 exit 1 } echo "${query_with_stats}" | grep -q '"plan":"' || { echo "graph query include_stats missing plan: ${query_with_stats}" >&2 exit 1 } query_provenance="$( http_post "/v2/graph/query" \ "{\"where\":{\"subject\":\"gq-b\",\"provenance_ref\":\"gq-prov1\"},\"predicates\":[\"ms.computed_by\"],\"direction\":\"outgoing\",\"limit\":10}" \ --header "Content-Type: application/json" \ --header "X-Amduat-Space: ${space_id}" )" echo "${query_provenance}" | grep -q '"name":"gq-d"' || { echo "graph query provenance filter missing expected node gq-d: ${query_provenance}" >&2 exit 1 } if echo "${query_provenance}" | grep -q '"name":"gq-a"'; then echo "graph query provenance filter unexpectedly includes gq-a: ${query_provenance}" >&2 exit 1 fi query_provenance_count="$(printf '%s\n' "${query_provenance}" | grep -o '"edge_ref":"' | wc -l | awk '{print $1}')" if [[ "${query_provenance_count}" != "1" ]]; then echo "graph query provenance expected exactly one edge, got ${query_provenance_count}: ${query_provenance}" >&2 exit 1 fi edges_provenance="$( http_get "/v2/graph/edges?subject=gq-b&predicate=ms.computed_by&dir=outgoing&provenance_ref=gq-prov1&limit=10&expand_names=true" \ --header "X-Amduat-Space: ${space_id}" )" echo "${edges_provenance}" | grep -q '"object_name":"gq-d"' || { echo "graph edges provenance filter missing gq-d: ${edges_provenance}" >&2 exit 1 } if echo "${edges_provenance}" | grep -q '"object_name":"gq-a"'; then echo "graph edges provenance filter unexpectedly includes gq-a: ${edges_provenance}" >&2 exit 1 fi edges_with_stats="$( http_get "/v2/graph/edges?subject=gq-b&predicate=ms.computed_by&dir=outgoing&include_stats=true&max_result_bytes=1048576&limit=10" \ --header "X-Amduat-Space: ${space_id}" )" echo "${edges_with_stats}" | grep -q '"stats":{' || { echo "graph edges include_stats missing stats block: ${edges_with_stats}" >&2 exit 1 } echo "${edges_with_stats}" | grep -q '"plan":"' || { echo "graph edges include_stats missing plan: ${edges_with_stats}" >&2 exit 1 } subgraph_provenance="$( http_get "/v2/graph/subgraph?roots[]=gq-b&max_depth=1&predicates[]=ms.computed_by&dir=outgoing&provenance_ref=gq-prov1&limit_edges=10&limit_nodes=10" \ --header "X-Amduat-Space: ${space_id}" )" echo "${subgraph_provenance}" | grep -q '"name":"gq-d"' || { echo "subgraph provenance filter missing gq-d: ${subgraph_provenance}" >&2 exit 1 } if echo "${subgraph_provenance}" | grep -q '"name":"gq-a"'; then echo "subgraph provenance filter unexpectedly includes gq-a: ${subgraph_provenance}" >&2 exit 1 fi subgraph_with_stats="$( http_get "/v2/graph/subgraph?roots[]=gq-a&max_depth=2&max_fanout=4096&include_stats=true&max_result_bytes=1048576&limit_edges=10&limit_nodes=10" \ --header "X-Amduat-Space: ${space_id}" )" echo "${subgraph_with_stats}" | grep -q '"stats":{' || { echo "subgraph include_stats missing stats block: ${subgraph_with_stats}" >&2 exit 1 } echo "${subgraph_with_stats}" | grep -q '"plan":"' || { echo "subgraph include_stats missing plan: ${subgraph_with_stats}" >&2 exit 1 } paths_with_stats="$( http_get "/v2/graph/paths?from=gq-a&to=gq-c&predicate=ms.within_domain&max_depth=4&include_stats=true&max_fanout=4096&max_result_bytes=1048576" \ --header "X-Amduat-Space: ${space_id}" )" echo "${paths_with_stats}" | grep -q '"stats":{' || { echo "paths include_stats missing stats block: ${paths_with_stats}" >&2 exit 1 } echo "${paths_with_stats}" | grep -q '"plan":"' || { echo "paths include_stats missing plan: ${paths_with_stats}" >&2 exit 1 } gqb_node="$( http_get "/v2/graph/nodes/gq-b" \ --header "X-Amduat-Space: ${space_id}" )" gqb_ref="$(printf '%s\n' "${gqb_node}" | extract_json_string "concept_ref")" if [[ -z "${gqb_ref}" ]]; then echo "failed to resolve gq-b concept ref: ${gqb_node}" >&2 exit 1 fi changes_tombstone="$( http_get "/v2/graph/changes?event_types[]=tombstone_applied&limit=100" \ --header "X-Amduat-Space: ${space_id}" )" echo "${changes_tombstone}" | grep -q '"event":"tombstone_applied"' || { echo "changes tombstone filter missing tombstone event: ${changes_tombstone}" >&2 exit 1 } changes_filtered="$( http_get "/v2/graph/changes?since_as_of=${cutoff_cursor}&predicates[]=ms.computed_by&roots[]=gq-b&limit=100" \ --header "X-Amduat-Space: ${space_id}" )" echo "${changes_filtered}" | grep -q "\"${gqb_ref}\"" || { echo "changes root/predicate filter missing gq-b involvement: ${changes_filtered}" >&2 exit 1 } if echo "${changes_filtered}" | grep -q '"event":"version_published"'; then echo "changes predicate filter unexpectedly includes version events: ${changes_filtered}" >&2 exit 1 fi changes_wait_empty="$( http_get "/v2/graph/changes?since_cursor=g1_999999&wait_ms=1&limit=1" \ --header "X-Amduat-Space: ${space_id}" )" echo "${changes_wait_empty}" | grep -q '"events":\[\]' || { echo "changes wait_ms empty poll expected no events: ${changes_wait_empty}" >&2 exit 1 } echo "ok: v2 graph query tests passed"