阶段4验证埋点持久化与后台读取
Project CI / Repository checks (pull_request) Has been cancelled
Project CI / Frontend tests (pull_request) Has been cancelled
Project CI / Backend tests (pull_request) Has been cancelled
Project CI / Native shell tests (pull_request) Has been cancelled

补充 tracking draft、runtime input 和 outbox round-trip 验证

覆盖 SpacetimeDB mapper 与 metadata object 校验

补充账号态和 External API Key 后台 tracking readback 回归
This commit is contained in:
2026-09-02 10:17:59 +00:00
parent 01159a2698
commit c75c61521d
5 changed files with 372 additions and 5 deletions
+161 -2
View File
@@ -4394,7 +4394,7 @@ mod tests {
http::{Method, StatusCode},
response::IntoResponse,
};
use serde_json::json;
use serde_json::{Value, json};
use shared_contracts::admin::{
AdminConfirmEditorShowcaseCampaignImageUploadRequest, AdminDashboardQuery,
AdminDatabaseTableRowsQuery, AdminTrackingEventListQuery,
@@ -4407,7 +4407,11 @@ mod tests {
SpacetimeClientStage,
};
use crate::{config::AppConfig, state::AppState};
use crate::{
config::AppConfig,
state::AppState,
tracking::{TrackingEventDraft, build_tracking_event_input},
};
fn test_admin_editor_showcase_asset_record(owner_user_id: &str) -> EditorShowcaseAssetRecord {
EditorShowcaseAssetRecord {
@@ -5976,6 +5980,161 @@ mod tests {
assert_eq!(entries[0].module_key.as_deref(), Some("profile"));
}
#[test]
fn parse_admin_tracking_events_sql_response_preserves_agc_metadata_and_owner() {
let payload = json!([
{
"rows": [
[
"account-event-225",
"editor_projects_view",
"user",
"user-225",
20600,
{"some": "user-225"},
{"some": "user-225"},
null,
"editor",
"{\"route\":\"/api/editor/projects\",\"method\":\"GET\",\"status\":200,\"operation\":\"listEditorProjects\",\"client\":\"agc\"}",
"2026-09-02T00:00:00Z"
],
[
"external-event-225",
"editor_image_generation_submit",
"user",
"owner-225",
20600,
null,
{"some": "owner-225"},
null,
"editor",
"{\"route\":\"/api/external/v1/editor/images/generations\",\"method\":\"POST\",\"status\":202,\"operation\":\"generateExternalEditorImage\",\"client\":\"agc\"}",
"2026-09-02T00:00:01Z"
]
]
}
]);
let entries = parse_admin_tracking_events_sql_response(payload)
.expect("AGC tracking rows should be readable by the admin parser");
assert_eq!(entries.len(), 2);
let account = entries
.iter()
.find(|entry| entry.event_id == "account-event-225")
.expect("account event should be present");
let account_metadata = serde_json::from_str::<serde_json::Value>(&account.metadata_json)
.expect("account metadata should remain valid JSON");
assert_eq!(account_metadata["client"], "agc");
assert_eq!(account_metadata["route"], "/api/editor/projects");
assert_eq!(account.user_id.as_deref(), Some("user-225"));
assert_eq!(account.owner_user_id.as_deref(), Some("user-225"));
let external = entries
.iter()
.find(|entry| entry.event_id == "external-event-225")
.expect("External API Key event should be present");
let external_metadata = serde_json::from_str::<serde_json::Value>(&external.metadata_json)
.expect("External API Key metadata should remain valid JSON");
assert_eq!(external_metadata["client"], "agc");
assert_eq!(
external_metadata["route"],
"/api/external/v1/editor/images/generations"
);
assert_eq!(external.user_id, None);
assert_eq!(external.owner_user_id.as_deref(), Some("owner-225"));
}
#[test]
fn tracking_inputs_round_trip_to_admin_readback_for_both_subjects() {
let mut account_draft = TrackingEventDraft::new("editor_projects_view", "editor");
account_draft.scope_kind = module_runtime::RuntimeTrackingScopeKind::User;
account_draft.scope_id = "user-225".to_string();
account_draft.user_id = Some("user-225".to_string());
account_draft.owner_user_id = Some("user-225".to_string());
account_draft.metadata = json!({
"route": "/api/editor/projects",
"method": "GET",
"status": 200,
"operation": "listEditorProjects",
"client": "agc",
});
let mut external_draft =
TrackingEventDraft::new("editor_image_generation_submit", "editor");
external_draft.scope_kind = module_runtime::RuntimeTrackingScopeKind::User;
external_draft.scope_id = "owner-225".to_string();
external_draft.owner_user_id = Some("owner-225".to_string());
external_draft.metadata = json!({
"route": "/api/external/v1/editor/images/generations",
"method": "POST",
"status": 202,
"operation": "generateExternalEditorImage",
"client": "agc",
});
let inputs = [
build_tracking_event_input(account_draft),
build_tracking_event_input(external_draft),
];
let row_for_input = |input: &module_runtime::RuntimeTrackingEventInput,
occurred_at: &str| {
let optional_column = |value: &Option<String>| {
value
.as_ref()
.map(|value| json!({"some": value}))
.unwrap_or(Value::Null)
};
json!([
input.event_id.clone(),
input.event_key.clone(),
"user",
input.scope_id.clone(),
20600,
optional_column(&input.user_id),
optional_column(&input.owner_user_id),
optional_column(&input.profile_id),
optional_column(&input.module_key),
input.metadata_json.clone(),
occurred_at,
])
};
let payload = json!([{
"rows": [
row_for_input(&inputs[0], "2026-09-02T00:00:00Z"),
row_for_input(&inputs[1], "2026-09-02T00:00:01Z"),
]
}]);
let entries = parse_admin_tracking_events_sql_response(payload)
.expect("tracking inputs should remain readable by admin readback");
assert_eq!(entries.len(), 2);
let account = entries
.iter()
.find(|entry| entry.event_key == "editor_projects_view")
.expect("account input should be readable");
let account_metadata = serde_json::from_str::<Value>(&account.metadata_json)
.expect("account metadata should remain valid JSON");
assert_eq!(account_metadata["client"], "agc");
assert_eq!(account.user_id.as_deref(), Some("user-225"));
assert_eq!(account.owner_user_id.as_deref(), Some("user-225"));
let external = entries
.iter()
.find(|entry| entry.event_key == "editor_image_generation_submit")
.expect("External API Key input should be readable");
let external_metadata = serde_json::from_str::<Value>(&external.metadata_json)
.expect("External API Key metadata should remain valid JSON");
assert_eq!(external_metadata["client"], "agc");
assert_eq!(
external_metadata["route"],
"/api/external/v1/editor/images/generations"
);
assert_eq!(external.user_id, None);
assert_eq!(external.owner_user_id.as_deref(), Some("owner-225"));
}
#[test]
fn parse_admin_tracking_events_sql_response_normalizes_sats_values() {
let payload = json!([