1
This commit is contained in:
@@ -115,7 +115,7 @@ pub async fn begin_story_runtime_session(
|
||||
story_session_payload_from_record(story_result.session),
|
||||
vec![story_event_payload_from_record(story_result.event)],
|
||||
&persisted,
|
||||
persisted.version,
|
||||
None,
|
||||
),
|
||||
},
|
||||
))
|
||||
@@ -257,7 +257,7 @@ pub async fn resolve_story_runtime_action(
|
||||
story_session_payload_from_record(story_result.session),
|
||||
vec![story_event_payload_from_record(story_result.event)],
|
||||
&persisted,
|
||||
resolved.server_version.max(persisted.version),
|
||||
Some(resolved.server_version),
|
||||
),
|
||||
},
|
||||
))
|
||||
@@ -395,7 +395,7 @@ fn build_story_runtime_projection_from_persisted(
|
||||
story_session: StorySessionPayload,
|
||||
story_events: Vec<StoryEventPayload>,
|
||||
record: &RuntimeSnapshotRecord,
|
||||
server_version: u32,
|
||||
resolved_version: Option<u32>,
|
||||
) -> shared_contracts::story::StoryRuntimeProjectionResponse {
|
||||
let snapshot = story_runtime_snapshot_payload_from_record(record);
|
||||
let current_story = snapshot.current_story.as_ref();
|
||||
@@ -405,6 +405,8 @@ fn build_story_runtime_projection_from_persisted(
|
||||
.or_else(|| Some(story_session.latest_narrative_text.clone()));
|
||||
let action_result_text = read_story_runtime_current_field(current_story, "resultText");
|
||||
let toast = read_story_runtime_current_field(current_story, "toast");
|
||||
let server_version =
|
||||
resolve_story_runtime_projection_version(&snapshot.game_state, resolved_version);
|
||||
|
||||
module_runtime_story::build_story_runtime_projection(
|
||||
module_runtime_story::StoryRuntimeProjectionSource {
|
||||
@@ -420,6 +422,15 @@ fn build_story_runtime_projection_from_persisted(
|
||||
)
|
||||
}
|
||||
|
||||
fn resolve_story_runtime_projection_version(
|
||||
game_state: &Value,
|
||||
resolved_version: Option<u32>,
|
||||
) -> u32 {
|
||||
module_runtime_story::read_u32_field(game_state, "runtimeActionVersion")
|
||||
.or(resolved_version)
|
||||
.unwrap_or(1)
|
||||
}
|
||||
|
||||
fn read_story_runtime_current_text(current_story: Option<&Value>) -> Option<String> {
|
||||
read_story_runtime_current_field(current_story, "text")
|
||||
.or_else(|| read_story_runtime_current_field(current_story, "storyText"))
|
||||
@@ -619,10 +630,12 @@ mod tests {
|
||||
use time::OffsetDateTime;
|
||||
use tower::ServiceExt;
|
||||
|
||||
use super::require_story_session_owner;
|
||||
use super::{build_story_runtime_projection_from_persisted, require_story_session_owner};
|
||||
use crate::{
|
||||
app::build_router, config::AppConfig, request_context::RequestContext, state::AppState,
|
||||
};
|
||||
use module_runtime::RuntimeSnapshotRecord;
|
||||
use shared_contracts::story::StorySessionPayload;
|
||||
|
||||
#[tokio::test]
|
||||
async fn begin_story_session_requires_authentication() {
|
||||
@@ -1028,6 +1041,56 @@ mod tests {
|
||||
);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn story_runtime_projection_version_prefers_runtime_action_version() {
|
||||
let projection = build_story_runtime_projection_from_persisted(
|
||||
StorySessionPayload {
|
||||
story_session_id: "storysess_001".to_string(),
|
||||
runtime_session_id: "runtime_001".to_string(),
|
||||
actor_user_id: "user_1".to_string(),
|
||||
world_profile_id: "profile_1".to_string(),
|
||||
initial_prompt: "进入营地".to_string(),
|
||||
opening_summary: Some("营地开场".to_string()),
|
||||
latest_narrative_text: "最新故事".to_string(),
|
||||
latest_choice_function_id: Some("npc_chat".to_string()),
|
||||
status: "active".to_string(),
|
||||
version: 9,
|
||||
created_at: "1.000000Z".to_string(),
|
||||
updated_at: "3.000000Z".to_string(),
|
||||
},
|
||||
vec![],
|
||||
&RuntimeSnapshotRecord {
|
||||
user_id: "user_1".to_string(),
|
||||
version: 2,
|
||||
saved_at: "3.000000Z".to_string(),
|
||||
saved_at_micros: 3,
|
||||
bottom_tab: "adventure".to_string(),
|
||||
game_state: json!({
|
||||
"runtimeSessionId": "runtime_001",
|
||||
"runtimeActionVersion": 7,
|
||||
"playerHp": 30,
|
||||
"playerMaxHp": 40,
|
||||
"playerMana": 10,
|
||||
"playerMaxMana": 20,
|
||||
"playerCurrency": 0,
|
||||
"playerInventory": [],
|
||||
"playerEquipment": { "weapon": null, "armor": null, "relic": null },
|
||||
"inBattle": false,
|
||||
"npcInteractionActive": false,
|
||||
"storyHistory": []
|
||||
}),
|
||||
current_story: None,
|
||||
game_state_json: "{}".to_string(),
|
||||
current_story_json: None,
|
||||
created_at_micros: 1,
|
||||
updated_at_micros: 3,
|
||||
},
|
||||
None,
|
||||
);
|
||||
|
||||
assert_eq!(projection.server_version, 7);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn story_session_owner_guard_rejects_mismatched_actor() {
|
||||
let context = RequestContext::new(
|
||||
|
||||
Reference in New Issue
Block a user