From b84ab1d8bacfe38185dc7c76433920d9f76a2ec1 Mon Sep 17 00:00:00 2001 From: Gregory Schier Date: Mon, 17 Aug 2026 07:03:06 -0700 Subject: [PATCH] Fix cookie defaults on the bootstrapped workspace and tie order in ordered atoms `Workspace::default()` is derived, so the first workspace was created with `setting_send_cookies` and `setting_store_cookies` false, contradicting the model's `default_true` serde defaults and the migration's DEFAULT TRUE. `createOrderedModelAtom` returned -1 for equal keys, an inconsistent comparator, so same-millisecond `http_response_event` rows came out of V8's sort reversed. --- crates/yaak-models/guest-js/atoms.test.ts | 59 ++++++++++++++++++++ crates/yaak-models/guest-js/atoms.ts | 4 +- crates/yaak-models/src/queries/workspaces.rs | 29 ++++++++++ 3 files changed, 91 insertions(+), 1 deletion(-) create mode 100644 crates/yaak-models/guest-js/atoms.test.ts diff --git a/crates/yaak-models/guest-js/atoms.test.ts b/crates/yaak-models/guest-js/atoms.test.ts new file mode 100644 index 00000000..517c818c --- /dev/null +++ b/crates/yaak-models/guest-js/atoms.test.ts @@ -0,0 +1,59 @@ +import { createStore } from "jotai"; +import { expect, test } from "vitest"; +import type { HttpResponseEvent } from "../bindings/gen_models"; +import { httpResponseEventsAtom, modelStoreDataAtom } from "./atoms"; +import { newStoreData } from "./util"; + +// The five setting events that every send writes, all within the same millisecond +const SETTING_NAMES = [ + "validate_certificates", + "redirects", + "timeout", + "send_cookies", + "store_cookies", +]; + +function settingEvent(id: string, name: string, createdAt: string): HttpResponseEvent { + return { + model: "http_response_event", + id, + createdAt, + updatedAt: createdAt, + workspaceId: "wk_1", + responseId: "rs_1", + event: { type: "setting", name, value: "true" }, + }; +} + +test("events with equal createdAt keep store (DB) insertion order", () => { + const store = createStore(); + const data = newStoreData(); + SETTING_NAMES.forEach((name, i) => { + data.http_response_event[`hre_${i}`] = settingEvent( + `hre_${i}`, + name, + "2026-08-17T00:00:00.123", + ); + }); + store.set(modelStoreDataAtom, data); + + const names = store.get(httpResponseEventsAtom).map((e) => { + return e.event.type === "setting" ? e.event.name : e.event.type; + }); + expect(names).toEqual(SETTING_NAMES); +}); + +test("events with distinct createdAt sort ascending", () => { + const store = createStore(); + const data = newStoreData(); + for (const [id, createdAt] of [ + ["hre_b", "2026-08-17T00:00:00.456"], + ["hre_a", "2026-08-17T00:00:00.123"], + ["hre_c", "2026-08-17T00:00:00.789"], + ]) { + data.http_response_event[id!] = settingEvent(id!, "timeout", createdAt!); + } + store.set(modelStoreDataAtom, data); + + expect(store.get(httpResponseEventsAtom).map((e) => e.id)).toEqual(["hre_a", "hre_b", "hre_c"]); +}); diff --git a/crates/yaak-models/guest-js/atoms.ts b/crates/yaak-models/guest-js/atoms.ts index b9dc3e18..24c68234 100644 --- a/crates/yaak-models/guest-js/atoms.ts +++ b/crates/yaak-models/guest-js/atoms.ts @@ -61,7 +61,9 @@ export function createOrderedModelAtom( const modelData = data[modelType] ?? {}; return Object.values(modelData).sort( (a: ExtractModel, b: ExtractModel) => { - const n = a[field] > b[field] ? 1 : -1; + // NOTE: ties must return 0, or the comparator is inconsistent and V8 reorders + // equal-keyed rows. Sort is stable, so 0 preserves store (DB) insertion order. + const n = a[field] === b[field] ? 0 : a[field] > b[field] ? 1 : -1; return order === "desc" ? n * -1 : n; }, ); diff --git a/crates/yaak-models/src/queries/workspaces.rs b/crates/yaak-models/src/queries/workspaces.rs index b7874c35..f77dbbb8 100644 --- a/crates/yaak-models/src/queries/workspaces.rs +++ b/crates/yaak-models/src/queries/workspaces.rs @@ -25,10 +25,14 @@ impl<'a> ClientDb<'a> { if workspaces.is_empty() { workspaces.push(self.upsert_workspace( + // NOTE: `Workspace::default()` is derived, so every `default_true` setting has + // to be set explicitly here or the first workspace gets `false`. &Workspace { name: "Yaak".to_string(), setting_follow_redirects: true, setting_request_message_size: crate::models::DEFAULT_REQUEST_MESSAGE_SIZE, + setting_send_cookies: true, + setting_store_cookies: true, setting_validate_certificates: true, ..Default::default() }, @@ -207,3 +211,28 @@ pub fn default_headers() -> Vec { }, ] } + +#[cfg(test)] +mod tests { + use crate::init_in_memory; + + #[test] + fn bootstraps_first_workspace_with_default_true_settings() { + let (query_manager, _blob_manager, _rx) = init_in_memory().expect("Failed to init DB"); + let db = query_manager.connect(); + + let workspaces = db.list_workspaces().expect("Failed to list workspaces"); + let workspace = workspaces.first().expect("No workspace was bootstrapped"); + + // Every setting with a `default_true` serde default must be true, since + // this workspace is built in Rust and never goes through deserialization + assert!(workspace.setting_send_cookies, "setting_send_cookies"); + assert!(workspace.setting_store_cookies, "setting_store_cookies"); + assert!(workspace.setting_follow_redirects, "setting_follow_redirects"); + assert!(workspace.setting_validate_certificates, "setting_validate_certificates"); + assert_eq!( + workspace.setting_request_message_size, + crate::models::DEFAULT_REQUEST_MESSAGE_SIZE + ); + } +}