amduat-api/scripts/test_graph_queries.sh
2026-02-08 08:13:10 +01:00

782 lines
28 KiB
Bash
Executable file

#!/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}/build/vendor/amduat/amduat-asl"
if [[ ! -x "${ASL_BIN}" ]]; then
ASL_BIN="${ROOT_DIR}/vendor/amduat/build/amduat-asl"
fi
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"