From 65a2e881bacefa3e99853ad3d7a7cc53d186354f Mon Sep 17 00:00:00 2001 From: Claude Date: Tue, 3 Nov 2026 16:50:02 +0000 Subject: [PATCH 01/12] adopt(memos): hunt harness and the useUpdateMemo patch (work in progress) Co-Authored-By: Claude Opus 5.5 Claude-Session: https://claude.ai/code/session_01Fqwfq2VbjHiLqM5WHy1kJE --- docs/adoption/hunts/memos.patch | 116 ++++++++ docs/adoption/hunts/memos/explore.mjs | 364 ++++++++++++++++++++++++++ docs/adoption/hunts/memos/repro.mjs | 89 +++++++ docs/adoption/hunts/memos/setup.mjs | 70 +++++ 4 files changed, 639 insertions(+) create mode 100644 docs/adoption/hunts/memos.patch create mode 100644 docs/adoption/hunts/memos/explore.mjs create mode 100644 docs/adoption/hunts/memos/repro.mjs create mode 100644 docs/adoption/hunts/memos/setup.mjs diff --git a/docs/adoption/hunts/memos.patch b/docs/adoption/hunts/memos.patch new file mode 100644 index 00000000..38e21431 --- /dev/null +++ b/docs/adoption/hunts/memos.patch @@ -0,0 +1,116 @@ +From a440178655a8c77f0e9f0cec3a8d2d1d238fc8b2 Mon Sep 17 00:00:00 2001 +From: GenClass hunt +Date: Tue, 3 Nov 2026 16:49:23 +0000 +Subject: [PATCH] fix(web): keep the newer memo update when two updates of a + memo overlap + +Pinning a memo and unpinning it before the first reply sent both updates at +once. The server saved them in order and ended unpinned, but when the pin +reply arrived last, useUpdateMemo's onSuccess wrote it over the newer state: +the detail page showed the memo pinned until the detail was fetched again. +The same holds for archive then restore and for the task check-all actions, +which go through the same hook. + +Memo updates now share one mutation scope, so they are sent one at a time in +the order they were made, and an update whose memo has a newer update queued +behind it leaves the cache to that update's own onSuccess instead of writing +its older reply over the newer optimistic patch. +--- + web/src/hooks/useMemoQueries.ts | 10 ++++++ + web/tests/memo-record.test.tsx | 55 +++++++++++++++++++++++++++++++++ + 2 files changed, 65 insertions(+) + +diff --git a/web/src/hooks/useMemoQueries.ts b/web/src/hooks/useMemoQueries.ts +index 1a1be2606..3729b468c 100644 +--- a/web/src/hooks/useMemoQueries.ts ++++ b/web/src/hooks/useMemoQueries.ts +@@ -327,6 +327,9 @@ export function useUpdateMemo() { + const queryClient = useQueryClient(); + + return useMutation({ ++ // Memo updates run one at a time, in the order they were made. Sent together, two quick updates of one memo ++ // (pin then unpin) can finish in either order, and the older reply would overwrite the newer change in the cache. ++ scope: { id: "memo-update" }, + mutationFn: async ({ update, updateMask }: { update: Partial; updateMask: string[] }) => { + const memo = await memoServiceClient.updateMemo({ + memo: create(MemoSchema, update as Record), +@@ -369,6 +372,13 @@ export function useUpdateMemo() { + if (updateMask.includes("space")) { + queryClient.invalidateQueries({ queryKey: memoKeys.all }); + } ++ // A newer update of this memo is queued behind this one: keep its optimistic patch, and leave the cache ++ // writes and refetches to its own onSuccess. This reply, or a refetch sent before it is saved, is older. ++ const newerUpdateQueued = ++ queryClient.isMutating({ ++ predicate: (mutation) => (mutation.state.variables as { update?: Partial } | undefined)?.update?.name === updatedMemo.name, ++ }) > 1; ++ if (newerUpdateQueued) return; + // Update cache with server response + queryClient.setQueryData(memoKeys.detail(updatedMemo.name), updatedMemo); + patchMemoInCollectionQueries(queryClient, updatedMemo); +diff --git a/web/tests/memo-record.test.tsx b/web/tests/memo-record.test.tsx +index 82b71b2bc..788774586 100644 +--- a/web/tests/memo-record.test.tsx ++++ b/web/tests/memo-record.test.tsx +@@ -188,3 +188,58 @@ describe("useCreateMemo", () => { + expect(invalidate.mock.calls.map(([filters]) => filters?.queryKey?.[1])).not.toContain("comments"); + }); + }); ++ ++describe("useUpdateMemo", () => { ++ beforeEach(() => vi.resetAllMocks()); ++ ++ // Pin, then unpin before the first reply: two updates of one memo in flight, as a quick second click makes them. ++ const pinThenUnpin = () => { ++ const { client, wrapper } = setup(); ++ const target = memo("memos/x"); ++ client.setQueryData(memoKeys.detail(target.name), target); ++ const replies: Array<() => void> = []; ++ api.updateMemo.mockImplementation( ++ ({ memo: update }: { memo: Memo }) => ++ new Promise((done) => replies.push(() => done(create(MemoSchema, { ...target, pinned: update.pinned })))), ++ ); ++ const { result } = renderHook(() => ({ record: useMemoRecord(target.name), update: useUpdateMemo() }), { wrapper }); ++ act(() => { ++ result.current.update.mutate({ update: { name: target.name, pinned: true }, updateMask: ["pinned"] }); ++ result.current.update.mutate({ update: { name: target.name, pinned: false }, updateMask: ["pinned"] }); ++ }); ++ // Answers one waiting request: the newest (pop) or the oldest (shift), once every request that can be sent is. ++ const answer = async (pick: "newest" | "oldest") => { ++ await act(async () => { ++ await new Promise((done) => setTimeout(done, 0)); ++ }); ++ await waitFor(() => expect(replies.length).toBeGreaterThan(0)); ++ await act(async () => (pick === "newest" ? replies.pop() : replies.shift())?.()); ++ }; ++ return { client, target, result, answer }; ++ }; ++ ++ it("keeps the newer change when the older reply arrives last", async () => { ++ const { client, target, result, answer } = pinThenUnpin(); ++ await waitFor(() => expect(result.current.record?.pinned).toBe(false)); ++ ++ await answer("newest"); ++ await answer("newest"); ++ await waitFor(() => expect(client.isMutating()).toBe(0)); ++ ++ expect(api.updateMemo.mock.calls.map(([request]) => request.memo.pinned)).toEqual([true, false]); ++ expect(client.getQueryData(memoKeys.detail(target.name))?.pinned).toBe(false); ++ expect(result.current.record?.pinned).toBe(false); ++ }); ++ ++ it("does not show the older reply while the newer update is still saving", async () => { ++ const { client, result, answer } = pinThenUnpin(); ++ await waitFor(() => expect(result.current.record?.pinned).toBe(false)); ++ ++ await answer("oldest"); ++ expect(result.current.record?.pinned).toBe(false); ++ ++ await answer("oldest"); ++ await waitFor(() => expect(client.isMutating()).toBe(0)); ++ expect(result.current.record?.pinned).toBe(false); ++ }); ++}); +-- +2.43.0 + diff --git a/docs/adoption/hunts/memos/explore.mjs b/docs/adoption/hunts/memos/explore.mjs new file mode 100644 index 00000000..265c8183 --- /dev/null +++ b/docs/adoption/hunts/memos/explore.mjs @@ -0,0 +1,364 @@ +// Race hunt harness for Memos (docs/adoption/hunts/memos.md). Not part of CI; run by hand against a local Memos. +// +// node docs/adoption/hunts/memos/explore.mjs --memos http://localhost:3001 --surface reaction \ +// --slow 300,600,900 [--runs 6] [--pause 0] [--out ] [--user hunter --password hunt-pass-1] +// GENCLASS=0 node docs/adoption/hunts/memos/explore.mjs --surface reaction --runs 8 # the same steps, no GenClass +// +// GenClass is injected from outside, as in ../gitea-explore.mjs: Memos' own code is unchanged. A Playwright route +// rewrites the SPA's HTML (Vite's dev server on :3001) to load the script-tag build +// (packages/runtime/dist/genclass.global.js) from a virtual same-origin path, started with the model skipped and +// telemetry off, then connects the coding-agent link (dist/agent.js) to the MCP bridge started in this process +// (packages/mcp/dist). Memos keeps its server state in TanStack Query, which GenClass does not discover, so the +// injected script mirrors what the memo card shows that has no text of its own (the pinned corner mark, an icon with an +// aria-label; the task checkboxes' checked states; the page path) into rt.atom() values from a MutationObserver. +// Reactions, memo text and comments are page text, which the explore verdict compares directly. +// +// Every load starts from the same server state: before it, the harness signs in over the app's Connect JSON API in a +// separate request context and resets the two seeded memos (setup.mjs): unpinned, NORMAL, no reactions, tasks +// unchecked, no comments. After the steps and the agent's whenSettled(), it waits --tail ms (default 2500: past the +// SSE-triggered refetches) and reads the oracle: what the page shows next to what the server holds (GetMemo). +// +// Surfaces (--surface), each two writes on the memo detail page (/memos/): +// pin More > Pin, then More > Unpin (useUpdateMemo, updateMask pinned) +// archive More > Archive, then More > Restore (useUpdateMemo, updateMask state; navigates on success) +// tasks More > Check all tasks, then More > Uncheck all (useUpdateMemo, updateMask content) +// reaction Add reaction > ๐Ÿ‘, then (picker still open) โค๏ธ (useReactionActions: upsert, then getMemo -> detail) +// comment comment "first", then comment "second" (useCreateMemo with parent) +// --pause waits between the two writes (default 0: Playwright's pace). For each --slow value it runs one explore +// session: 2 fifo baselines + --runs seeded loads, the same steps each load. Each load's line ends with the delivery +// order of the memo-service requests, as the agent link recorded it. + +import { readFileSync, writeFileSync, mkdirSync } from "node:fs"; +import { dirname, join, resolve, extname } from "node:path"; +import { fileURLToPath } from "node:url"; +import { PassThrough } from "node:stream"; +import { parseArgs } from "node:util"; +import { chromium } from "@playwright/test"; + +const here = dirname(fileURLToPath(import.meta.url)); +const root = resolve(here, "../../../.."); +const rtDist = join(root, "packages/runtime/dist"); +const WITH_GENCLASS = process.env.GENCLASS !== "0"; + +const { values: arg } = parseArgs({ + options: { + memos: { type: "string", default: "http://localhost:3001" }, + user: { type: "string", default: "hunter" }, + password: { type: "string", default: "hunt-pass-1" }, + seeds: { type: "string", default: `${process.env.HOME}/targets/memos-state-memos.json` }, + surface: { type: "string", default: "reaction" }, + slow: { type: "string", default: "300,600,900" }, + runs: { type: "string", default: "6" }, + pause: { type: "string", default: "0" }, + tail: { type: "string", default: "2500" }, + out: { type: "string", default: "" }, + }, +}); +const M = arg.memos.replace(/\/+$/, ""); +const seeds = JSON.parse(readFileSync(arg.seeds, "utf8")); // { tasks: "memos/โ€ฆ", target: "memos/โ€ฆ" } +const TYPES = { ".js": "text/javascript", ".map": "application/json" }; +const pause = Number(arg.pause); +const TASKS = "race tasks memo\n\n- [ ] first task\n- [ ] second task\n- [ ] third task"; + +// --------------------------------------------------------------------------- what the route injects into +const INJECT = ` + + +`; + +async function route(page) { + await page.route(`${M}/**`, async (r) => { + const req = r.request(); + const url = new URL(req.url()); + if (url.pathname.startsWith("/__genclass/rt/")) { + const file = join(rtDist, url.pathname.slice("/__genclass/rt/".length)); + if (!file.startsWith(rtDist)) return r.fulfill({ status: 404 }); + try { + return r.fulfill({ status: 200, contentType: TYPES[extname(file)] ?? "application/octet-stream", body: readFileSync(file) }); + } catch { + return r.fulfill({ status: 404 }); + } + } + if (req.resourceType() !== "document") return r.fallback(); + const resp = await r.fetch(); + const type = resp.headers()["content-type"] ?? ""; + if (!type.includes("text/html")) return r.fulfill({ response: resp }); + const body = (await resp.text()).replace(/]*)>/i, (m) => `${m}\n${INJECT}`); + return r.fulfill({ response: resp, body }); + }); +} + +// --------------------------------------------------------------------------- the MCP server, driven like an agent +let tool = async () => ""; +let served = null; +let bridgeUrl = ""; +if (WITH_GENCLASS) { + const { serve } = await import(join(root, "packages/mcp/dist/index.js")); + const input = new PassThrough(); + const output = new PassThrough(); + const lines = []; + let buf = ""; + output.setEncoding("utf8"); + output.on("data", (c) => { + buf += c; + let nl; + while ((nl = buf.indexOf("\n")) >= 0) { + lines.push(JSON.parse(buf.slice(0, nl))); + buf = buf.slice(nl + 1); + } + }); + served = await serve({ cwd: here, env: {}, stdin: input, stdout: output, stderr: process.stderr, port: 0, stateDir: null }); + let id = 0; + const rpc = async (method, params = {}) => { + const my = ++id; + input.write(`${JSON.stringify({ jsonrpc: "2.0", id: my, method, params })}\n`); + for (let i = 0; i < 3000; i++) { + const m = lines.find((x) => x.id === my); + if (m) return m; + await new Promise((r) => setTimeout(r, 10)); + } + throw new Error(`no reply to ${method}`); + }; + tool = async (name, args = {}) => (await rpc("tools/call", { name, arguments: args })).result.content[0].text; + await rpc("initialize", { protocolVersion: "2025-06-18", capabilities: {}, clientInfo: { name: "memos-hunt", version: "1" } }); + input.write(`${JSON.stringify({ jsonrpc: "2.0", method: "notifications/initialized" })}\n`); + bridgeUrl = `http://127.0.0.1:${served.port}`; +} + +// --------------------------------------------------------------------------- the app's API (reset and oracle) +async function signIn(request) { + const r = await request.post(`${M}/memos.api.AuthService/SignIn`, { + data: { passwordCredentials: { username: arg.user, password: arg.password } }, + }); + if (!r.ok()) throw new Error(`sign-in failed: HTTP ${r.status()} ${await r.text()}`); + return (await r.json()).accessToken; +} +function api(request, token) { + return async (method, body) => { + for (let attempt = 0; attempt < 4; attempt++) { + const r = await request.post(`${M}/memos.api.MemoService/${method}`, { data: body, headers: { Authorization: `Bearer ${token}` } }); + if (r.status() === 429) { + await new Promise((done) => setTimeout(done, 31_000)); // 120 writes per user per minute + continue; + } + if (!r.ok()) throw new Error(`${method}: HTTP ${r.status()} ${await r.text()}`); + return r.json(); + } + throw new Error(`${method}: still rate limited`); + }; +} + +async function reset(call) { + const t = await call("GetMemo", { name: seeds.target }); + if (t.pinned || (t.state && t.state !== "NORMAL")) { + await call("UpdateMemo", { memo: { name: seeds.target, pinned: false, state: "NORMAL" }, updateMask: "pinned,state" }); + } + for (const r of t.reactions ?? []) await call("DeleteMemoReaction", { name: r.name }); + const k = await call("GetMemo", { name: seeds.tasks }); + if (k.content !== TASKS) await call("UpdateMemo", { memo: { name: seeds.tasks, content: TASKS }, updateMask: "content" }); + const c = await call("ListMemoComments", { parent: seeds.target }); + for (const m of c.memos ?? c.comments ?? []) await call("DeleteMemo", { name: m.name }); +} + +// --------------------------------------------------------------------------- the scenarios (what a person does) +const uid = (name) => name.slice(name.indexOf("/") + 1); +const nap = (page) => (pause ? page.waitForTimeout(pause) : Promise.resolve()); +const menu = async (page, item) => { + await page.getByRole("button", { name: "More" }).first().click(); + await page.getByRole("menuitem", { name: item, exact: true }).click(); +}; +const shownCard = (page) => + page.evaluate(() => { + const main = document.querySelector("main") || document.body; + const article = main.querySelector("article") || main; + return { + path: location.pathname.replace(/[A-Za-z0-9]{22}$/, ":uid"), + pinned: !!main.querySelector('[role="img"][aria-label="Pinned"]'), + tasks: [...main.querySelectorAll('input[type="checkbox"]')].map((c) => (c.checked ? "x" : "_")).join(""), + reactions: [...article.querySelectorAll("button[aria-pressed]")].map((b) => b.textContent.replace(/\s+/g, "")).join(" "), + comments: [...main.querySelectorAll("p")].map((p) => p.textContent.trim()).filter((t) => /^comment (first|second)$/.test(t)), + }; + }); + +const surfaces = { + pin: { + memo: () => seeds.target, + async steps(page) { + await menu(page, "Pin"); + await nap(page); + await menu(page, "Unpin"); + }, + oracle: (shown, server) => ({ shown: `pinned=${shown.pinned}`, server: `pinned=${!!server.pinned}` }), + }, + archive: { + memo: () => seeds.target, + async steps(page) { + await menu(page, "Archive"); + await nap(page); + await menu(page, "Restore"); + }, + oracle: (shown, server) => ({ shown: `path=${shown.path}`, server: `state=${server.state}` }), + }, + tasks: { + memo: () => seeds.tasks, + async steps(page) { + await menu(page, "Check all tasks"); + await nap(page); + await menu(page, "Uncheck all tasks"); + }, + oracle: (shown, server) => ({ + shown: `tasks=${shown.tasks}`, + server: `tasks=${[...server.content.matchAll(/- \[( |x)\]/g)].map((m) => (m[1] === "x" ? "x" : "_")).join("")}`, + }), + }, + reaction: { + memo: () => seeds.target, + async steps(page) { + await page.getByRole("button", { name: "Add reaction" }).first().click({ force: true }); + await page.getByRole("button", { name: "๐Ÿ‘", exact: true }).click(); + await nap(page); + await page.getByRole("button", { name: "โค๏ธ", exact: true }).click(); + }, + oracle: (shown, server) => ({ + shown: `reactions=${shown.reactions || "none"}`, + server: `reactions=${ + Object.entries( + (server.reactions ?? []).reduce((acc, r) => ({ ...acc, [r.reactionType]: (acc[r.reactionType] ?? 0) + 1 }), {}), + ) + .map(([k, n]) => `${k}${n}`) + .join(" ") || "none" + }`, + }), + }, + comment: { + memo: () => seeds.target, + async steps(page) { + for (const text of ["comment first", "comment second"]) { + await page.getByRole("button", { name: "Write a comment" }).last().click(); + const box = page.locator("main textarea").last(); + await box.fill(text); + await page.keyboard.press("Control+Enter"); + await page.waitForFunction(() => !document.querySelector("main textarea")?.value, null, { timeout: 15_000 }).catch(() => {}); + await nap(page); + } + }, + oracle: (shown, server) => ({ shown: `comments=${shown.comments.join("+") || "none"}`, server: `comments=${server.__comments}` }), + }, +}; + +async function connected(page) { + if (!WITH_GENCLASS) return; + await page.waitForFunction(() => window.__GENCLASS_AGENT__?.connected === true, null, { timeout: 15_000 }); +} +async function settle(page) { + if (WITH_GENCLASS) await page.evaluate(() => window.__GENCLASS_AGENT__.whenSettled(15_000)).catch(() => {}); + await page.waitForTimeout(Number(arg.tail)); +} + +const browser = await chromium.launch(); + +async function scenario(s) { + const ctx = await browser.newContext({ permissions: WITH_GENCLASS ? ["local-network-access"] : [], viewport: { width: 1200, height: 900 } }); + const token = await signIn(ctx.request); // the refresh cookie lands in this context; the page signs in with it + const call = api(ctx.request, token); + await reset(call); + const page = await ctx.newPage(); + const errors = []; + page.on("pageerror", (e) => errors.push(String(e))); + if (process.env.HUNT_DEBUG) { + page.on("console", (m) => console.log("[page]", m.type(), m.text())); + page.on("requestfailed", (r) => console.log("[failed]", r.url(), r.failure()?.errorText)); + } + if (WITH_GENCLASS) { + await page.addInitScript((u) => (window.__GENCLASS_AGENT_URL__ = u), bridgeUrl); + await route(page); + } + let line; + try { + await page.goto(`${M}/memos/${uid(s.memo())}`); + await page.getByRole("button", { name: "More" }).first().waitFor({ timeout: 15_000 }); + await connected(page); + await s.steps(page); + await settle(page); + const shown = await shownCard(page); + const server = await call("GetMemo", { name: s.memo() }); + if (arg.surface === "comment") { + const c = await call("ListMemoComments", { parent: s.memo() }); + server.__comments = (c.memos ?? c.comments ?? []).map((m) => m.content).sort().join("+") || "none"; + } + const o = s.oracle(shown, server); + line = `${o.shown === o.server ? "same " : "DIFFER"} shown ${o.shown} | server ${o.server}`; + } catch (e) { + line = `ERROR ${String(e).split("\n")[0]}`; + } + const order = WITH_GENCLASS + ? await page + .evaluate(() => + window.__GENCLASS_AGENT__ + .recorded() + .order.filter((k) => /MemoService\//.test(k)) + .map((k) => k.replace(/^.*MemoService\//, "")), + ) + .catch(() => []) + : []; + if (WITH_GENCLASS) await page.waitForTimeout(500); // the snapshot batch reaches the bridge + await ctx.close(); + line = `${line}${WITH_GENCLASS ? ` [delivered ${order.join(" then ") || "none"}]` : ""}`; + return errors.length ? `${line} [pageerror: ${errors[0]}]` : line; +} + +const s = surfaces[arg.surface]; +if (!s) throw new Error(`unknown surface ${arg.surface}`); +const out = arg.out ? resolve(arg.out) : null; +if (out) mkdirSync(out, { recursive: true }); +const summary = { surface: arg.surface, memos: M, genclass: WITH_GENCLASS, runs: Number(arg.runs), pause, tail: Number(arg.tail), sessions: [] }; +if (!WITH_GENCLASS) { + // the control: the same steps at the network's own pace, --runs loads + const shown = []; + for (let i = 0; i < Number(arg.runs); i++) shown.push(await scenario(s)); + summary.sessions.push({ slow_ms: 0, differ: shown.filter((l) => l.startsWith("DIFFER")).length, shown }); + console.log(shown.map((l, i) => `load ${i + 1}: ${l}`).join("\n")); +} else { + for (const slow of arg.slow.split(",").map(Number)) { + await tool("genclass_clear", { what: "all" }); + const started = await tool("genclass_explore", { action: "start", runs: Number(arg.runs), slow_ms: slow }); + const shown = []; + for (let i = 0; i < Number(arg.runs) + 2; i++) shown.push(await scenario(s)); + const report = await tool("genclass_explore", { action: "report" }); + const fid = /Race finding\(s\): (f\d+)/.exec(report)?.[1]; + const explain = fid ? await tool("genclass_explain", { id: fid }) : null; + const findings = await tool("genclass_findings"); + const verdict = /RACE: (\d+) of (\d+)/.exec(report); + summary.sessions.push({ + slow_ms: slow, + verdict: verdict ? `race in ${verdict[1]} of ${verdict[2]}` : "no race", + differ: shown.filter((l) => l.startsWith("DIFFER")).length, + finding: fid ?? null, + shown, + }); + if (out) writeFileSync(join(out, `${arg.surface}-${slow}.txt`), [started, "", ...shown.map((l, i) => `load ${i + 1}: ${l}`), "", report, "", findings, "", explain ?? ""].join("\n")); + console.log(`--- ${arg.surface} slow_ms=${slow}\n${shown.map((l, i) => `load ${i + 1}: ${l}`).join("\n")}\n${report}\n`); + } +} +await browser.close(); +if (served) await served.close(); +console.log(JSON.stringify(summary, null, 2)); +if (out) writeFileSync(join(out, `${arg.surface}${WITH_GENCLASS ? "" : "-control"}-summary.json`), JSON.stringify(summary, null, 2)); +process.exit(0); diff --git a/docs/adoption/hunts/memos/repro.mjs b/docs/adoption/hunts/memos/repro.mjs new file mode 100644 index 00000000..c6f24567 --- /dev/null +++ b/docs/adoption/hunts/memos/repro.mjs @@ -0,0 +1,89 @@ +// The pin race without GenClass: plain Playwright, nothing injected into the page. Not part of any test suite. +// +// node docs/adoption/hunts/memos/repro.mjs [--memos http://localhost:3001] [--trials 10] [--delay 1500] [--later 30000] +// [--memo memos/] [--out file] +// +// On the memo detail page: More > Pin, then More > Unpin, as two quick clicks. A Playwright route lets every request +// reach the server at once and holds only the reply to the first UpdateMemo (pinned: true) for --delay ms, as a slow +// connection or a busy server would; the second reply (pinned: false) and everything else pass untouched. The server +// therefore saves pin, then unpin, and ends unpinned. The script reads the pinned corner mark (the icon with +// aria-label "Pinned") 3 s after the held reply was delivered and again 30 s later, and GetMemo from the server, in each +// trial; it prints how many trials showed the memo pinned while the server held it unpinned. With --delay 0 nothing is +// held (the control). Each trial starts from a fresh sign-in and an unpinned memo (the seeds from setup.mjs). + +import { readFileSync, writeFileSync } from "node:fs"; +import { parseArgs } from "node:util"; +import { chromium } from "@playwright/test"; + +const { values: arg } = parseArgs({ + options: { + memos: { type: "string", default: "http://localhost:3001" }, + user: { type: "string", default: "hunter" }, + password: { type: "string", default: "hunt-pass-1" }, + seeds: { type: "string", default: `${process.env.HOME}/targets/memos-state-memos.json` }, + trials: { type: "string", default: "10" }, + delay: { type: "string", default: "1500" }, + later: { type: "string", default: "30000" }, + memo: { type: "string", default: "" }, + out: { type: "string", default: "" }, + }, +}); +const M = arg.memos.replace(/\/+$/, ""); +const target = arg.memo || JSON.parse(readFileSync(arg.seeds, "utf8")).target; // --memo memos/: another memo +const delay = Number(arg.delay); +const sleep = (ms) => new Promise((done) => setTimeout(done, ms)); + +const browser = await chromium.launch(); +const trials = []; +for (let i = 0; i < Number(arg.trials); i++) { + const ctx = await browser.newContext({ viewport: { width: 1200, height: 900 } }); + const signIn = await ctx.request.post(`${M}/memos.api.AuthService/SignIn`, { + data: { passwordCredentials: { username: arg.user, password: arg.password } }, + }); + const { accessToken } = await signIn.json(); + const call = async (method, body) => + (await ctx.request.post(`${M}/memos.api.MemoService/${method}`, { data: body, headers: { Authorization: `Bearer ${accessToken}` } })).json(); + await call("UpdateMemo", { memo: { name: target, pinned: false }, updateMask: "pinned" }); + + const page = await ctx.newPage(); + let updates = 0; + let heldDelivered = null; + await page.route("**/memos.api.MemoService/UpdateMemo", async (route) => { + const first = updates++ === 0; + const reply = await route.fetch(); // the request reaches the server now; only the reply waits + if (first && delay > 0) await sleep(delay); + await route.fulfill({ response: reply }); + if (first) heldDelivered = Date.now(); + }); + await page.goto(`${M}/${target}`); + const more = page.getByRole("button", { name: "More" }).first(); + await more.waitFor(); + await page.waitForTimeout(1000); // the page's own first requests and the SSE connection settle + await more.click(); + await page.getByRole("menuitem", { name: "Pin", exact: true }).click(); + await more.click(); + await page.getByRole("menuitem", { name: "Unpin", exact: true }).click(); + while (heldDelivered === null) await sleep(50); + await sleep(3000); + const pinnedMark = () => page.locator('main [role="img"][aria-label="Pinned"]').count().then((n) => n > 0); + const shownSoon = await pinnedMark(); + const serverSoon = (await call("GetMemo", { name: target })).pinned === true; + await sleep(Number(arg.later)); + const shownLater = await pinnedMark(); + const serverLater = (await call("GetMemo", { name: target })).pinned === true; + await ctx.close(); + const t = { trial: i + 1, updates, shownSoon, serverSoon, shownLater, serverLater, stale: shownSoon && !serverSoon, staleLater: shownLater && !serverLater }; + trials.push(t); + console.log(JSON.stringify(t)); +} +await browser.close(); +const summary = { + memos: M, + delay_ms: delay, + later_ms: Number(arg.later), + trials: trials.length, + stale_after_3s: trials.filter((t) => t.stale).length, + stale_after_later: trials.filter((t) => t.staleLater).length, +}; +console.log(JSON.stringify(summary)); +if (arg.out) writeFileSync(arg.out, JSON.stringify({ ...summary, results: trials }, null, 2)); diff --git a/docs/adoption/hunts/memos/setup.mjs b/docs/adoption/hunts/memos/setup.mjs new file mode 100644 index 00000000..b3ac8d21 --- /dev/null +++ b/docs/adoption/hunts/memos/setup.mjs @@ -0,0 +1,70 @@ +// Seeds a fresh local Memos instance for the race hunt (docs/adoption/hunts/memos.md). Not part of CI. +// +// node docs/adoption/hunts/memos/setup.mjs [--memos http://localhost:3001] [--user hunter] [--password hunt-pass-1] +// [--state ] [--count 60] +// +// First run of an instance: creates the owner account through /auth/signup (the page every fresh instance redirects +// to), then seeds memos over the app's own Connect JSON API from inside the signed-in page (a refresh-token cookie, +// then a bearer access token, as web/src/connect.ts does): one memo with a task list for the check-all surface, one +// "race target" memo for pin, archive and reactions, and --count filler memos so the home list paginates. Later runs +// sign in instead. Writes the Playwright storage state (the refresh cookie) to --state and prints the memo names. + +import { writeFileSync } from "node:fs"; +import { parseArgs } from "node:util"; +import { chromium } from "@playwright/test"; + +const { values: arg } = parseArgs({ + options: { + memos: { type: "string", default: "http://localhost:3001" }, + user: { type: "string", default: "hunter" }, + password: { type: "string", default: "hunt-pass-1" }, + state: { type: "string", default: `${process.env.HOME}/targets/memos-state.json` }, + count: { type: "string", default: "60" }, + }, +}); +const M = arg.memos.replace(/\/+$/, ""); + +const browser = await chromium.launch(); +const ctx = await browser.newContext(); +const page = await ctx.newPage(); +await page.goto(`${M}/`); +await page.waitForLoadState("networkidle"); +if (page.url().includes("/auth/signup")) { + await page.fill("#signup-username", arg.user); + await page.fill("#signup-password", arg.password); + await page.getByRole("button", { name: /create admin account/i }).click(); +} else if (page.url().includes("/auth")) { + await page.locator('input[autocomplete="username"]').fill(arg.user); + await page.locator('input[type="password"]').fill(arg.password); + await page.getByRole("button", { name: /sign in/i }).click(); +} +await page.waitForURL((u) => !u.pathname.startsWith("/auth"), { timeout: 15_000 }); + +const names = await page.evaluate(async (count) => { + const call = async (method, body, token) => { + const r = await fetch(`/memos.api.${method}`, { + method: "POST", + headers: { "Content-Type": "application/json", ...(token ? { Authorization: `Bearer ${token}` } : {}) }, + body: JSON.stringify(body), + credentials: "include", + }); + if (r.status === 429) { + // The server limits writes to 120 per user per minute (RATE_LIMITED, write_user): wait out the window. + await new Promise((done) => setTimeout(done, 31_000)); + return call(method, body, token); + } + if (!r.ok) throw new Error(`${method}: HTTP ${r.status} ${await r.text()}`); + return r.json(); + }; + const { accessToken } = await call("AuthService/RefreshToken", {}); + const make = (content) => call("MemoService/CreateMemo", { memo: { content, visibility: "PRIVATE" } }, accessToken); + for (let i = 0; i < count; i++) await make(`filler memo ${i} #seed`); + const tasks = await make("race tasks memo\n\n- [ ] first task\n- [ ] second task\n- [ ] third task"); + const target = await make("race target memo for pin, archive and reactions"); + return { tasks: tasks.name, target: target.name }; +}, Number(arg.count)); + +await ctx.storageState({ path: arg.state }); +await browser.close(); +console.log(JSON.stringify({ state: arg.state, ...names })); +writeFileSync(arg.state.replace(/\.json$/, "-memos.json"), JSON.stringify(names)); From cf66be4f81359a4909beb7dab957d7216c603a29 Mon Sep 17 00:00:00 2001 From: Claude Date: Tue, 3 Nov 2026 16:51:59 +0000 Subject: [PATCH 02/12] adopt(memos): hover the memo card before the reaction picker Co-Authored-By: Claude Opus 5.5 Claude-Session: https://claude.ai/code/session_01Fqwfq2VbjHiLqM5WHy1kJE --- docs/adoption/hunts/memos/explore.mjs | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/docs/adoption/hunts/memos/explore.mjs b/docs/adoption/hunts/memos/explore.mjs index 265c8183..af43979d 100644 --- a/docs/adoption/hunts/memos/explore.mjs +++ b/docs/adoption/hunts/memos/explore.mjs @@ -231,7 +231,8 @@ const surfaces = { reaction: { memo: () => seeds.target, async steps(page) { - await page.getByRole("button", { name: "Add reaction" }).first().click({ force: true }); + await page.locator("main article, main .group\\/memo").first().hover(); // the reaction button shows on hover + await page.getByRole("button", { name: "Add reaction" }).first().click(); await page.getByRole("button", { name: "๐Ÿ‘", exact: true }).click(); await nap(page); await page.getByRole("button", { name: "โค๏ธ", exact: true }).click(); From c0e9d5967081f622f5b5f1bdb199af724182c619 Mon Sep 17 00:00:00 2001 From: Claude Date: Tue, 3 Nov 2026 16:56:13 +0000 Subject: [PATCH 03/12] adopt(memos): archive oracle compares the end page with the saved state Co-Authored-By: Claude Opus 5.5 Claude-Session: https://claude.ai/code/session_01Fqwfq2VbjHiLqM5WHy1kJE --- docs/adoption/hunts/memos/explore.mjs | 7 ++++++- 1 file changed, 6 insertions(+), 1 deletion(-) diff --git a/docs/adoption/hunts/memos/explore.mjs b/docs/adoption/hunts/memos/explore.mjs index af43979d..6cdea189 100644 --- a/docs/adoption/hunts/memos/explore.mjs +++ b/docs/adoption/hunts/memos/explore.mjs @@ -214,7 +214,12 @@ const surfaces = { await nap(page); await menu(page, "Restore"); }, - oracle: (shown, server) => ({ shown: `path=${shown.path}`, server: `state=${server.state}` }), + // The page a person ends on, against the page that lists the memo in its saved state (home or its own page for + // NORMAL, /archived for ARCHIVED). + oracle: (shown, server) => ({ + shown: `page=${shown.path === "/archived" ? "archived" : "home-or-detail"}`, + server: `page=${server.state === "ARCHIVED" ? "archived" : "home-or-detail"}`, + }), }, tasks: { memo: () => seeds.tasks, From 8b17a2cbdaa33b218960bc4be50543843097711b Mon Sep 17 00:00:00 2001 From: Claude Date: Tue, 3 Nov 2026 17:04:36 +0000 Subject: [PATCH 04/12] adopt(memos): comment editor and reaction picker steps Co-Authored-By: Claude Opus 5.5 Claude-Session: https://claude.ai/code/session_01Fqwfq2VbjHiLqM5WHy1kJE --- docs/adoption/hunts/memos/explore.mjs | 26 +++++++++++++++++++------- 1 file changed, 19 insertions(+), 7 deletions(-) diff --git a/docs/adoption/hunts/memos/explore.mjs b/docs/adoption/hunts/memos/explore.mjs index 6cdea189..1a47a529 100644 --- a/docs/adoption/hunts/memos/explore.mjs +++ b/docs/adoption/hunts/memos/explore.mjs @@ -22,7 +22,7 @@ // pin More > Pin, then More > Unpin (useUpdateMemo, updateMask pinned) // archive More > Archive, then More > Restore (useUpdateMemo, updateMask state; navigates on success) // tasks More > Check all tasks, then More > Uncheck all (useUpdateMemo, updateMask content) -// reaction Add reaction > ๐Ÿ‘, then (picker still open) โค๏ธ (useReactionActions: upsert, then getMemo -> detail) +// reaction Add reaction > ๐Ÿ‘, then Add reaction > โค๏ธ (useReactionActions: upsert, then getMemo -> detail) // comment comment "first", then comment "second" (useCreateMemo with parent) // --pause waits between the two writes (default 0: Playwright's pace). For each --slow value it runs one explore // session: 2 fifo baselines + --runs seeded loads, the same steps each load. Each load's line ends with the delivery @@ -193,7 +193,10 @@ const shownCard = (page) => pinned: !!main.querySelector('[role="img"][aria-label="Pinned"]'), tasks: [...main.querySelectorAll('input[type="checkbox"]')].map((c) => (c.checked ? "x" : "_")).join(""), reactions: [...article.querySelectorAll("button[aria-pressed]")].map((b) => b.textContent.replace(/\s+/g, "")).join(" "), - comments: [...main.querySelectorAll("p")].map((p) => p.textContent.trim()).filter((t) => /^comment (first|second)$/.test(t)), + comments: [...main.querySelectorAll("p")] + .map((p) => p.textContent.trim()) + .filter((t) => /^comment (first|second)$/.test(t)) + .sort(), }; }); @@ -240,6 +243,11 @@ const surfaces = { await page.getByRole("button", { name: "Add reaction" }).first().click(); await page.getByRole("button", { name: "๐Ÿ‘", exact: true }).click(); await nap(page); + // The picker closes once the first reaction's refetch lands (onComplete), at a different moment in each order: + // close it either way and open it again, so every load records the same steps. + await page.keyboard.press("Escape"); + await page.locator("main article, main .group\\/memo").first().hover(); + await page.getByRole("button", { name: "Add reaction" }).first().click(); await page.getByRole("button", { name: "โค๏ธ", exact: true }).click(); }, oracle: (shown, server) => ({ @@ -258,10 +266,14 @@ const surfaces = { async steps(page) { for (const text of ["comment first", "comment second"]) { await page.getByRole("button", { name: "Write a comment" }).last().click(); - const box = page.locator("main textarea").last(); - await box.fill(text); - await page.keyboard.press("Control+Enter"); - await page.waitForFunction(() => !document.querySelector("main textarea")?.value, null, { timeout: 15_000 }).catch(() => {}); + const box = page.locator('main [role="textbox"][contenteditable="true"]').last(); // the @usememos/editor surface + await box.click(); + await page.keyboard.type(text); + await page.getByRole("button", { name: "Comment", exact: true }).click(); + // the editor closes (or empties) once the comment is saved: the second comment is written after that + await page + .waitForFunction(() => !document.querySelector('main [role="textbox"][contenteditable="true"]')?.textContent?.trim(), null, { timeout: 15_000 }) + .catch(() => {}); await nap(page); } }, @@ -307,7 +319,7 @@ async function scenario(s) { const server = await call("GetMemo", { name: s.memo() }); if (arg.surface === "comment") { const c = await call("ListMemoComments", { parent: s.memo() }); - server.__comments = (c.memos ?? c.comments ?? []).map((m) => m.content).sort().join("+") || "none"; + server.__comments = (c.memos ?? c.comments ?? []).map((m) => m.content.trim()).sort().join("+") || "none"; } const o = s.oracle(shown, server); line = `${o.shown === o.server ? "same " : "DIFFER"} shown ${o.shown} | server ${o.server}`; From 13fcfc59e7e347a6fbad8d3843c95de79ff85890 Mon Sep 17 00:00:00 2001 From: Claude Date: Tue, 3 Nov 2026 17:04:59 +0000 Subject: [PATCH 05/12] adopt(memos): results summariser Co-Authored-By: Claude Opus 5.5 Claude-Session: https://claude.ai/code/session_01Fqwfq2VbjHiLqM5WHy1kJE --- docs/adoption/hunts/memos/summarise.mjs | 35 +++++++++++++++++++++++++ 1 file changed, 35 insertions(+) create mode 100644 docs/adoption/hunts/memos/summarise.mjs diff --git a/docs/adoption/hunts/memos/summarise.mjs b/docs/adoption/hunts/memos/summarise.mjs new file mode 100644 index 00000000..e68c2352 --- /dev/null +++ b/docs/adoption/hunts/memos/summarise.mjs @@ -0,0 +1,35 @@ +// Summarises the explore harness's per-surface summaries (explore.mjs --out ) into one small table. +// +// node docs/adoption/hunts/memos/summarise.mjs [] > docs/adoption/hunts/memos/results.json +// +// For each surface and latency: GenClass's verdict line, how many of the 8 loads the harness's own oracle saw end +// with the page disagreeing with the server ("DIFFER"), how many of the 6 seeded loads that was, how many loads +// errored (a step that the order made impossible), and the delivery order of the two writes in each DIFFER load. + +import { existsSync, readFileSync } from "node:fs"; +import { join } from "node:path"; + +const SURFACES = ["pin", "archive", "tasks", "reaction", "comment"]; +const table = {}; +for (const dir of process.argv.slice(2)) { + const label = dir.replace(/\/+$/, "").split("/").pop(); + table[label] = {}; + for (const surface of SURFACES) { + const file = join(dir, `${surface}-summary.json`); + if (!existsSync(file)) continue; + const s = JSON.parse(readFileSync(file, "utf8")); + table[label][surface] = s.sessions.map((x) => ({ + slow_ms: x.slow_ms, + genclass: x.verdict, + oracle_differ_of_8: x.shown.filter((l) => l.startsWith("DIFFER")).length, + oracle_differ_seeded_of_6: x.shown.slice(2).filter((l) => l.startsWith("DIFFER")).length, + baselines_differ: x.shown.slice(0, 2).filter((l) => l.startsWith("DIFFER")).length, + errors: x.shown.filter((l) => l.startsWith("ERROR")).length, + differ_loads: x.shown + .map((l, i) => ({ l, i })) + .filter(({ l }) => l.startsWith("DIFFER")) + .map(({ l, i }) => `load ${i + 1}: ${l.replace(/ \[delivered.*$/, "")}; writes delivered ${(l.match(/(UpdateMemo|UpsertMemoReaction|CreateMemoComment)#\d/g) ?? []).join(", ")}`), + })); + } +} +console.log(JSON.stringify(table, null, 2)); From 11902e000ba111ce859ba3b664e2dd0eb485d0fb Mon Sep 17 00:00:00 2001 From: Claude Date: Tue, 3 Nov 2026 17:08:47 +0000 Subject: [PATCH 06/12] adopt(memos): add the reaction refetch fix to the patch Co-Authored-By: Claude Opus 5.5 Claude-Session: https://claude.ai/code/session_01Fqwfq2VbjHiLqM5WHy1kJE --- docs/adoption/hunts/memos.patch | 117 +++++++++++++++++++++++++++++++- 1 file changed, 115 insertions(+), 2 deletions(-) diff --git a/docs/adoption/hunts/memos.patch b/docs/adoption/hunts/memos.patch index 38e21431..305cebce 100644 --- a/docs/adoption/hunts/memos.patch +++ b/docs/adoption/hunts/memos.patch @@ -1,8 +1,8 @@ From a440178655a8c77f0e9f0cec3a8d2d1d238fc8b2 Mon Sep 17 00:00:00 2001 From: GenClass hunt Date: Tue, 3 Nov 2026 16:49:23 +0000 -Subject: [PATCH] fix(web): keep the newer memo update when two updates of a - memo overlap +Subject: [PATCH 1/2] fix(web): keep the newer memo update when two updates of + a memo overlap Pinning a memo and unpinning it before the first reply sent both updates at once. The server saved them in order and ended unpinned, but when the pin @@ -114,3 +114,116 @@ index 82b71b2bc..788774586 100644 -- 2.43.0 + +From e2547a94367e7686097d8efcbc081a59b64f8d29 Mon Sep 17 00:00:00 2001 +From: GenClass hunt +Date: Tue, 3 Nov 2026 17:08:37 +0000 +Subject: [PATCH 2/2] fix(web): refetch a memo's reactions through its query + +A reaction click fetched the memo with a raw getMemo call and wrote the reply +into the detail cache whenever it arrived. Adding two reactions quickly sent +two such fetches, and when the first one (asked before the second reaction +was saved) answered last, it wrote the older reactions over the newer ones: +the second reaction disappeared from the page although the server kept it, +until the next refetch. + +The click now invalidates the memo's detail query instead. React Query +cancels an in-flight refetch of that query when it starts a newer one, so an +older copy of the reactions can no longer land after a newer one. +--- + .../components/MemoReactionListView/hooks.ts | 6 +- + web/tests/reaction-actions.test.tsx | 65 +++++++++++++++++++ + 2 files changed, 68 insertions(+), 3 deletions(-) + create mode 100644 web/tests/reaction-actions.test.tsx + +diff --git a/web/src/components/MemoReactionListView/hooks.ts b/web/src/components/MemoReactionListView/hooks.ts +index f544632f8..9404aca21 100644 +--- a/web/src/components/MemoReactionListView/hooks.ts ++++ b/web/src/components/MemoReactionListView/hooks.ts +@@ -55,9 +55,9 @@ export const useReactionActions = ({ memo, onComplete }: UseReactionActionsOptio + reaction: { reactionType }, + }); + } +- // Refetch the memo to get updated reactions and invalidate cache +- const updatedMemo = await memoServiceClient.getMemo({ name: memo.name }); +- queryClient.setQueryData(memoKeys.detail(memo.name), updatedMemo); ++ // Refetch the memo through its query: a newer refetch (another reaction, a live update) cancels this one, so an ++ // older copy of the reactions cannot land after a newer one. ++ await queryClient.invalidateQueries({ queryKey: memoKeys.detail(memo.name) }); + queryClient.invalidateQueries({ queryKey: memoKeys.lists() }); + // If this memo is a comment, refresh the parent's comments list so the comment's reactions update in the UI + if (memo.parent) { +diff --git a/web/tests/reaction-actions.test.tsx b/web/tests/reaction-actions.test.tsx +new file mode 100644 +index 000000000..c895cbf7a +--- /dev/null ++++ b/web/tests/reaction-actions.test.tsx +@@ -0,0 +1,65 @@ ++import { create } from "@bufbuild/protobuf"; ++import { QueryClient, QueryClientProvider, useQuery } from "@tanstack/react-query"; ++import { act, renderHook, waitFor } from "@testing-library/react"; ++import type { ReactNode } from "react"; ++import { beforeEach, describe, expect, it, vi } from "vitest"; ++import { useReactionActions } from "@/components/MemoReactionListView/hooks"; ++import { memoDetailQueryOptions, memoKeys } from "@/hooks/useMemoQueries"; ++import { type Memo, MemoSchema, ReactionSchema } from "@/types/proto/api/memo_service_pb"; ++ ++const api = vi.hoisted(() => ({ getMemo: vi.fn(), upsertMemoReaction: vi.fn(), deleteMemoReaction: vi.fn() })); ++vi.mock("@/connect", () => ({ memoServiceClient: api })); ++vi.mock("@/hooks/useCurrentUser", () => ({ default: () => ({ name: "users/me" }) })); ++vi.mock("@/contexts/MentionResolutionContext", () => ({ useResolvedUsersByNames: () => new Map() })); ++ ++const setup = () => { ++ const client = new QueryClient({ defaultOptions: { queries: { retry: false, retryDelay: 0 }, mutations: { retry: false } } }); ++ const wrapper = ({ children }: { children: ReactNode }) => {children}; ++ return { client, wrapper }; ++}; ++ ++describe("useReactionActions", () => { ++ beforeEach(() => vi.resetAllMocks()); ++ ++ it("keeps the newer reactions when the first reaction's refetch answers last", async () => { ++ const { client, wrapper } = setup(); ++ const name = "memos/x"; ++ client.setQueryData(memoKeys.detail(name), create(MemoSchema, { name, reactions: [] })); ++ // The server's reactions, and the replies to getMemo, each holding the reactions saved when it was asked. ++ const saved: string[] = []; ++ const replies: Array<() => void> = []; ++ api.upsertMemoReaction.mockImplementation(async ({ reaction }: { reaction: { reactionType: string } }) => { ++ saved.push(reaction.reactionType); ++ return create(ReactionSchema, { reactionType: reaction.reactionType }); ++ }); ++ api.getMemo.mockImplementation(() => { ++ const reactions = saved.map((reactionType) => create(ReactionSchema, { reactionType, creator: "users/me" })); ++ return new Promise((done) => replies.push(() => done(create(MemoSchema, { name, reactions })))); ++ }); ++ ++ // The detail page: the memo's query is observed, and the reaction picker acts on the memo it shows. ++ const { result } = renderHook( ++ () => { ++ const memo = useQuery(memoDetailQueryOptions(name)).data as Memo; ++ return { memo, actions: useReactionActions({ memo }) }; ++ }, ++ { wrapper }, ++ ); ++ act(() => { ++ void result.current.actions.handleReactionClick("๐Ÿ‘"); ++ }); ++ await waitFor(() => expect(api.getMemo).toHaveBeenCalledTimes(1)); ++ act(() => { ++ void result.current.actions.handleReactionClick("โค๏ธ"); ++ }); ++ await waitFor(() => expect(api.getMemo).toHaveBeenCalledTimes(2)); ++ ++ // The second refetch answers first, then the first one, which only knows about ๐Ÿ‘. ++ await act(async () => replies[1]()); ++ await act(async () => replies[0]()); ++ ++ await waitFor(() => expect(client.isFetching()).toBe(0)); ++ expect(saved).toEqual(["๐Ÿ‘", "โค๏ธ"]); ++ expect(result.current.memo.reactions.map((reaction) => reaction.reactionType)).toEqual(["๐Ÿ‘", "โค๏ธ"]); ++ }); ++}); +-- +2.43.0 + From eec9ed014d5199bbd9900e71c1135b20af7d6ace Mon Sep 17 00:00:00 2001 From: Claude Date: Tue, 3 Nov 2026 17:10:47 +0000 Subject: [PATCH 07/12] adopt(memos): reaction reproduction without GenClass Co-Authored-By: Claude Opus 5.5 Claude-Session: https://claude.ai/code/session_01Fqwfq2VbjHiLqM5WHy1kJE --- docs/adoption/hunts/memos/repro.mjs | 129 +++++++++++++++++++++------- 1 file changed, 98 insertions(+), 31 deletions(-) diff --git a/docs/adoption/hunts/memos/repro.mjs b/docs/adoption/hunts/memos/repro.mjs index c6f24567..1a30370e 100644 --- a/docs/adoption/hunts/memos/repro.mjs +++ b/docs/adoption/hunts/memos/repro.mjs @@ -1,15 +1,20 @@ -// The pin race without GenClass: plain Playwright, nothing injected into the page. Not part of any test suite. +// The two races without GenClass: plain Playwright, nothing injected into the page. Not part of any test suite. // -// node docs/adoption/hunts/memos/repro.mjs [--memos http://localhost:3001] [--trials 10] [--delay 1500] [--later 30000] -// [--memo memos/] [--out file] +// node docs/adoption/hunts/memos/repro.mjs --surface pin|reaction [--memos http://localhost:3001] [--trials 10] +// [--delay 1500] [--later 30000] [--memo memos/] [--out file] // -// On the memo detail page: More > Pin, then More > Unpin, as two quick clicks. A Playwright route lets every request -// reach the server at once and holds only the reply to the first UpdateMemo (pinned: true) for --delay ms, as a slow -// connection or a busy server would; the second reply (pinned: false) and everything else pass untouched. The server -// therefore saves pin, then unpin, and ends unpinned. The script reads the pinned corner mark (the icon with -// aria-label "Pinned") 3 s after the held reply was delivered and again 30 s later, and GetMemo from the server, in each -// trial; it prints how many trials showed the memo pinned while the server held it unpinned. With --delay 0 nothing is -// held (the control). Each trial starts from a fresh sign-in and an unpinned memo (the seeds from setup.mjs). +// Both run on the memo detail page and let every request reach the server at once; a Playwright route holds only one +// reply for --delay ms, as a slow connection or a busy server would. --delay 0 holds nothing (the control). +// +// pin More > Pin, then More > Unpin. Held: the reply to the first UpdateMemo (pinned: true). The server saves +// pin, then unpin, and ends unpinned. Read: the pinned corner mark (the icon with aria-label "Pinned"). +// reaction Add reaction > ๐Ÿ‘, then Add reaction > โค๏ธ (the picker opened again). Held: the reply to the GetMemo that +// the ๐Ÿ‘ click sends after its upsert (it was answered with ๐Ÿ‘ only). The server ends with both. Read: the +// reaction pills. +// +// Each trial starts from a fresh sign-in and the memo reset (unpinned, no reactions). The page is read 3 s after the +// held reply was delivered and again --later ms after that, next to GetMemo from the server; the script prints how +// many trials showed something other than what the server held. import { readFileSync, writeFileSync } from "node:fs"; import { parseArgs } from "node:util"; @@ -17,6 +22,7 @@ import { chromium } from "@playwright/test"; const { values: arg } = parseArgs({ options: { + surface: { type: "string", default: "pin" }, memos: { type: "string", default: "http://localhost:3001" }, user: { type: "string", default: "hunter" }, password: { type: "string", default: "hunt-pass-1" }, @@ -33,6 +39,58 @@ const target = arg.memo || JSON.parse(readFileSync(arg.seeds, "utf8")).target; / const delay = Number(arg.delay); const sleep = (ms) => new Promise((done) => setTimeout(done, ms)); +const surfaces = { + pin: { + held: "UpdateMemo", + // hold the first UpdateMemo reply + hold: (method, state) => method === "UpdateMemo" && state.updates++ === 0, + async steps(page) { + const more = page.getByRole("button", { name: "More" }).first(); + await more.click(); + await page.getByRole("menuitem", { name: "Pin", exact: true }).click(); + await more.click(); + await page.getByRole("menuitem", { name: "Unpin", exact: true }).click(); + }, + shown: async (page) => `pinned=${(await page.locator('main [role="img"][aria-label="Pinned"]').count()) > 0}`, + server: (memo) => `pinned=${memo.pinned === true}`, + }, + reaction: { + held: "GetMemo after the first upsert", + // hold the first GetMemo sent after the first reaction's upsert was answered + hold: (method, state) => { + if (method === "UpsertMemoReaction") state.upserts++; + if (method === "GetMemo" && state.upserts === 1 && !state.taken) return (state.taken = true); + return false; + }, + async steps(page) { + const card = page.locator("main article, main .group\\/memo").first(); + const open = async () => { + await page.keyboard.press("Escape"); + await card.hover(); + await page.getByRole("button", { name: "Add reaction" }).first().click(); + }; + await open(); + await page.getByRole("button", { name: "๐Ÿ‘", exact: true }).click(); + await page.waitForTimeout(300); // the ๐Ÿ‘ upsert is answered and its GetMemo is on the wire + await open(); + await page.getByRole("button", { name: "โค๏ธ", exact: true }).click(); + }, + shown: async (page) => + `reactions=${ + (await page.locator("main button[aria-pressed]").evaluateAll((els) => els.map((b) => b.textContent.replace(/\s+/g, "")).join(" "))) || + "none" + }`, + server: (memo) => + `reactions=${ + Object.entries((memo.reactions ?? []).reduce((acc, r) => ({ ...acc, [r.reactionType]: (acc[r.reactionType] ?? 0) + 1 }), {})) + .map(([k, n]) => `${k}${n}`) + .join(" ") || "none" + }`, + }, +}; +const s = surfaces[arg.surface]; +if (!s) throw new Error(`unknown surface ${arg.surface}`); + const browser = await chromium.launch(); const trials = []; for (let i = 0; i < Number(arg.trials); i++) { @@ -43,42 +101,51 @@ for (let i = 0; i < Number(arg.trials); i++) { const { accessToken } = await signIn.json(); const call = async (method, body) => (await ctx.request.post(`${M}/memos.api.MemoService/${method}`, { data: body, headers: { Authorization: `Bearer ${accessToken}` } })).json(); - await call("UpdateMemo", { memo: { name: target, pinned: false }, updateMask: "pinned" }); + const before = await call("GetMemo", { name: target }); + if (before.pinned) await call("UpdateMemo", { memo: { name: target, pinned: false }, updateMask: "pinned" }); + for (const r of before.reactions ?? []) await call("DeleteMemoReaction", { name: r.name }); const page = await ctx.newPage(); - let updates = 0; + const state = { updates: 0, upserts: 0, taken: false, armed: false }; let heldDelivered = null; - await page.route("**/memos.api.MemoService/UpdateMemo", async (route) => { - const first = updates++ === 0; + await page.route("**/memos.api.MemoService/*", async (route) => { + const method = route.request().url().split("/").pop(); + const held = state.armed && s.hold(method, state); const reply = await route.fetch(); // the request reaches the server now; only the reply waits - if (first && delay > 0) await sleep(delay); + if (held && delay > 0) await sleep(delay); await route.fulfill({ response: reply }); - if (first) heldDelivered = Date.now(); + if (held) heldDelivered = Date.now(); }); await page.goto(`${M}/${target}`); - const more = page.getByRole("button", { name: "More" }).first(); - await more.waitFor(); + await page.getByRole("button", { name: "More" }).first().waitFor(); await page.waitForTimeout(1000); // the page's own first requests and the SSE connection settle - await more.click(); - await page.getByRole("menuitem", { name: "Pin", exact: true }).click(); - await more.click(); - await page.getByRole("menuitem", { name: "Unpin", exact: true }).click(); - while (heldDelivered === null) await sleep(50); - await sleep(3000); - const pinnedMark = () => page.locator('main [role="img"][aria-label="Pinned"]').count().then((n) => n > 0); - const shownSoon = await pinnedMark(); - const serverSoon = (await call("GetMemo", { name: target })).pinned === true; - await sleep(Number(arg.later)); - const shownLater = await pinnedMark(); - const serverLater = (await call("GetMemo", { name: target })).pinned === true; + state.armed = true; + let shownSoon = "ERROR"; + let shownLater = "ERROR"; + let serverSoon = ""; + let serverLater = ""; + try { + await s.steps(page); + for (let w = 0; heldDelivered === null && w < 400; w++) await sleep(50); + await sleep(3000); + shownSoon = await s.shown(page); + serverSoon = s.server(await call("GetMemo", { name: target })); + await sleep(Number(arg.later)); + shownLater = await s.shown(page); + serverLater = s.server(await call("GetMemo", { name: target })); + } catch (e) { + shownSoon = `ERROR ${String(e).split("\n")[0]}`; + } await ctx.close(); - const t = { trial: i + 1, updates, shownSoon, serverSoon, shownLater, serverLater, stale: shownSoon && !serverSoon, staleLater: shownLater && !serverLater }; + const t = { trial: i + 1, shownSoon, serverSoon, shownLater, serverLater, stale: shownSoon !== serverSoon, staleLater: shownLater !== serverLater }; trials.push(t); console.log(JSON.stringify(t)); } await browser.close(); const summary = { + surface: arg.surface, memos: M, + held: s.held, delay_ms: delay, later_ms: Number(arg.later), trials: trials.length, From 911cb40d5f4ec8a23cdd26ddac1f65081fd84a43 Mon Sep 17 00:00:00 2001 From: Claude Date: Tue, 3 Nov 2026 17:16:34 +0000 Subject: [PATCH 08/12] adopt(memos): issue and pull request drafts (numbers pending) Co-Authored-By: Claude Opus 5.5 Claude-Session: https://claude.ai/code/session_01Fqwfq2VbjHiLqM5WHy1kJE --- docs/adoption/hunts/memos-issue.md | 135 +++++++++++++++++++++++++++++ 1 file changed, 135 insertions(+) create mode 100644 docs/adoption/hunts/memos-issue.md diff --git a/docs/adoption/hunts/memos-issue.md b/docs/adoption/hunts/memos-issue.md new file mode 100644 index 00000000..bc801fda --- /dev/null +++ b/docs/adoption/hunts/memos-issue.md @@ -0,0 +1,135 @@ +# Draft issue and pull request text for Memos' maintainers + +Drafts for the GenClass owner to send, or not. Nothing here has been posted anywhere. Facts and commands are from +`memos.md` in this directory; the patch is `memos.patch` (two commits against `usememos/memos` `b40d8a7b5`, made with +`git format-patch`). Memos has no pull request template and no AI policy; its bug template is +`.github/ISSUE_TEMPLATE/bug_report.yml` (fields below in its order). One issue covers both bugs: they are the same +kind (an older reply written over newer data) in the same part of the web app, and one pull request fixes both, one +commit each. + +## Before sending + +- [ ] Search the tracker again for duplicates since 2026-11-03 (issues and pull requests): "pin unpin", "pinned", + "archive restore", "check all tasks", "reaction disappears", "reaction", "race", "out of order", + "useUpdateMemo". None was found on 2026-11-03 (`memos.md` section 7). +- [ ] Reproduce by hand on a local `main` with the steps below (DevTools: Network, throttling, or the two scripts + `docs/adoption/hunts/memos/repro.mjs --surface pin|reaction` from this repository), and update the version line + to the commit you used. +- [ ] Apply the patch to a fresh clone (`git am memos.patch`), re-author the commits as yourself + (`git rebase -x 'git commit --amend --no-edit --reset-author' HEAD~2`), and read the diff: 10 added lines in + `useMemoQueries.ts`, 3 changed lines in `MemoReactionListView/hooks.ts`, two tests. +- [ ] Run `cd web && pnpm lint && pnpm test` (Node 24, pnpm 11.0.1), and the two new tests on the unfixed code + (`git stash` the two source files) to see them fail. +- [ ] Open the issue first, put its number in the pull request's "Fixes #", then open the pull request from your + fork's branch against `main`. +- [ ] Keep the AI-assistance paragraph; every sentence in it must be true for you. + +## Issue + +**Title:** Quick second action on a memo can leave the page showing the older state (pin/unpin, check/uncheck all, reactions) + +**Check before submitting** + +- [x] I searched existing issues and did not find a duplicate + +**Describe the bug** + +What happened: when two changes to the same memo are made before the first one's reply comes back, the page can end +up showing the first change, while the server saved both in order and holds the second. + +- Pin, then Unpin from the memo's "More" menu: the detail page keeps the pinned corner mark; the server has the memo + unpinned. The same with "Check all tasks" then "Uncheck all tasks" (the checkboxes stay checked) and with Archive + then Restore on the detail page (it ends on the Archived page while the memo is restored). +- Add a ๐Ÿ‘ reaction, then a โค๏ธ: the โค๏ธ pill disappears; the server has both reactions. + +The page stays like that until the memo is fetched again (a reload, a window focus, or the next live-refresh event). + +What I expected: the page ends showing the last change I made, which is what the server holds. + +Cause, as far as I can tell: + +1. `useUpdateMemo` (`web/src/hooks/useMemoQueries.ts`): both updates are sent at once; `onSuccess` writes each reply + into `memoKeys.detail` and the lists when it arrives. When the first reply arrives last, the older memo is written + over the newer optimistic state, and nothing refetches the detail afterwards (the lists are invalidated, the + detail is not). The SSE `memo.changed` refetch does not help when it is answered before that late reply. +2. `useReactionActions` (`web/src/components/MemoReactionListView/hooks.ts`): after the upsert, `getMemo` is called + directly and its reply is written with `setQueryData`. The first click's `getMemo` was answered by the server + before the second reaction was saved; when it arrives last it removes the second reaction from the cache. + React Query's own refetches of that query cancel each other; this direct call is outside them. + +**Steps to reproduce** + +The replies have to arrive out of order, which a slow or unsteady connection does; to make it deterministic, hold +one reply back (DevTools has no per-request delay, so a Playwright route or a proxy is the simplest way; the script +we used is linked under Additional context). + +Pin: +1. Open a memo's detail page (`/memos/`). +2. "More" > "Pin", then right away "More" > "Unpin". +3. Delay the reply to the first `UpdateMemo` (the pin) by about 1.5 s, so it arrives after the second. +4. The memo shows the pinned mark; `GetMemo` (or a reload) says `pinned: false`. + +Reactions: +1. Open a memo's detail page, hover the memo, "Add reaction" > ๐Ÿ‘. +2. Delay the reply to the `GetMemo` that follows the ๐Ÿ‘ upsert by about 1.5 s. +3. While it is held, "Add reaction" > โค๏ธ. +4. The โค๏ธ pill disappears when the held reply lands; the server has ๐Ÿ‘ and โค๏ธ. + +With those delays, pin showed the stale state in TBD of 10 trials and reactions in TBD of 10 (0 of 3 each without the +delay); 30 s later it was still shown in TBD and TBD of them. + +**Memos version** + +main@b40d8a7 (26.11.1-dev.b40d8a7), SQLite, `go run ./cmd/memos` + `pnpm dev` + +**Additional context** + +Deployment / database / browser: local dev setup from AGENTS.md (backend on :8081, Vite on :3001), SQLite, Chromium +153 (Playwright 1.63). + +The reproduction scripts (plain Playwright, nothing injected into the page): +`https://github.com/genclass-dev/GenClass/blob/main/docs/adoption/hunts/memos/repro.mjs`. A fix with regression tests +follows in a pull request. + +I used Claude (Claude Code) to investigate this, write the fix and the tests, and draft this description; I reviewed +the change and ran the tests and the manual check myself. The race was found with GenClass, a tool I work on, which +replays the same steps with the server's responses delivered in different orders. + +## Pull request + +**Title:** `fix(web): keep the newer state when two changes to a memo overlap` + +**Body:** + +Fixes #. + +When two changes to the same memo were in flight, the page could end showing the older one (#): pin then +unpin left the memo pinned on its detail page, check all then uncheck all left the tasks checked, archive then +restore ended on the Archived page, and adding two reactions quickly dropped the second. The server had saved both +changes in order each time; only the client cache held the older state, until the memo was fetched again. + +Reproduced on main@b40d8a7 by delaying one reply by 1.5 s (the first `UpdateMemo`; the `GetMemo` after the first +reaction's upsert): the stale state showed in TBD of 10 trials for pin and TBD of 10 for reactions, and 0 of 3 without +the delay. With this branch: 0 of 10 and 0 of 10. + +Two commits: + +- `fix(web): keep the newer memo update when two updates of a memo overlap`: `useUpdateMemo` gets a mutation + `scope`, so memo updates are sent one at a time in the order they were made (React Query still runs each + `onMutate` right away, so the optimistic patch shows on the click as before). An update whose memo has a newer + update queued behind it returns from `onSuccess` before writing its reply, so the newer optimistic patch stays + until the newer reply lands. +- `fix(web): refetch a memo's reactions through its query`: the reaction click invalidates the memo's detail query + instead of calling `getMemo` and `setQueryData` itself. A newer refetch of that query cancels an older one, so an + older copy of the reactions cannot land after a newer one. + +Tests (Vitest, in the style of `memo-record.test.tsx`): two new `useUpdateMemo` tests in `web/tests/memo-record.test.tsx` +answer the pin and unpin replies in reverse order and in order, and `web/tests/reaction-actions.test.tsx` answers the +two reactions' refetches in reverse order. All three fail on main (the memo ends `pinned: true`; the reactions end +`["๐Ÿ‘"]`) and pass with the fix, three runs each way. + +Checks: `cd web && pnpm lint` (tsc and Biome clean) and `pnpm test` (193 files, 1619 tests) pass. + +AI assistance: I used Claude (Claude Code) to investigate this, write the fix and the tests, and draft this +description; I reviewed the change and ran the tests and the manual check myself. The race was found with GenClass, +a tool I work on, which replays the same steps with the server's responses delivered in different orders. From 95cbb30381f4fb3747714e0349ebe514c3d524de Mon Sep 17 00:00:00 2001 From: Claude Date: Tue, 3 Nov 2026 17:32:37 +0000 Subject: [PATCH 09/12] adopt(memos): reaction reproduction holds the refetches between the two upserts Co-Authored-By: Claude Opus 5.5 Claude-Session: https://claude.ai/code/session_01Fqwfq2VbjHiLqM5WHy1kJE --- docs/adoption/hunts/memos/repro.mjs | 32 +++++++++++++++++++---------- 1 file changed, 21 insertions(+), 11 deletions(-) diff --git a/docs/adoption/hunts/memos/repro.mjs b/docs/adoption/hunts/memos/repro.mjs index 1a30370e..2a56ba46 100644 --- a/docs/adoption/hunts/memos/repro.mjs +++ b/docs/adoption/hunts/memos/repro.mjs @@ -8,8 +8,9 @@ // // pin More > Pin, then More > Unpin. Held: the reply to the first UpdateMemo (pinned: true). The server saves // pin, then unpin, and ends unpinned. Read: the pinned corner mark (the icon with aria-label "Pinned"). -// reaction Add reaction > ๐Ÿ‘, then Add reaction > โค๏ธ (the picker opened again). Held: the reply to the GetMemo that -// the ๐Ÿ‘ click sends after its upsert (it was answered with ๐Ÿ‘ only). The server ends with both. Read: the +// reaction Add reaction > ๐Ÿ‘, then Add reaction > โค๏ธ (the picker opened again). Held: the replies to the GetMemo +// requests sent after the ๐Ÿ‘ upsert was answered and before the โค๏ธ upsert (the ๐Ÿ‘ click's own getMemo, +// answered with ๐Ÿ‘ only, and any live-update refetch in that window). The server ends with both. Read: the // reaction pills. // // Each trial starts from a fresh sign-in and the memo reset (unpinned, no reactions). The page is read 3 s after the @@ -55,12 +56,15 @@ const surfaces = { server: (memo) => `pinned=${memo.pinned === true}`, }, reaction: { - held: "GetMemo after the first upsert", - // hold the first GetMemo sent after the first reaction's upsert was answered + held: "GetMemo sent between the first upsert's reply and the second upsert", + // Hold every GetMemo sent after the ๐Ÿ‘ upsert was answered and before the โค๏ธ upsert: the click's own getMemo, and + // any live-update refetch of the detail query in that window (React Query cancels those when a newer one starts). hold: (method, state) => { if (method === "UpsertMemoReaction") state.upserts++; - if (method === "GetMemo" && state.upserts === 1 && !state.taken) return (state.taken = true); - return false; + return method === "GetMemo" && state.firstUpsertAnswered && state.upserts === 1; + }, + answered: (method, state) => { + if (method === "UpsertMemoReaction" && state.upserts === 1) state.firstUpsertAnswered = true; }, async steps(page) { const card = page.locator("main article, main .group\\/memo").first(); @@ -106,15 +110,21 @@ for (let i = 0; i < Number(arg.trials); i++) { for (const r of before.reactions ?? []) await call("DeleteMemoReaction", { name: r.name }); const page = await ctx.newPage(); - const state = { updates: 0, upserts: 0, taken: false, armed: false }; + const state = { updates: 0, upserts: 0, firstUpsertAnswered: false, armed: false }; let heldDelivered = null; await page.route("**/memos.api.MemoService/*", async (route) => { const method = route.request().url().split("/").pop(); const held = state.armed && s.hold(method, state); - const reply = await route.fetch(); // the request reaches the server now; only the reply waits - if (held && delay > 0) await sleep(delay); - await route.fulfill({ response: reply }); - if (held) heldDelivered = Date.now(); + try { + const reply = await route.fetch(); // the request reaches the server now; only the reply waits + if (held && delay > 0) await sleep(delay); + await route.fulfill({ response: reply }); + if (state.armed) s.answered?.(method, state); + if (held) heldDelivered = Date.now(); + } catch { + // the page aborted the request (React Query cancels a superseded refetch) + if (held) heldDelivered = Date.now(); + } }); await page.goto(`${M}/${target}`); await page.getByRole("button", { name: "More" }).first().waitFor(); From c6162c3512b91993861cbe49f644613f98209c05 Mon Sep 17 00:00:00 2001 From: Claude Date: Tue, 3 Nov 2026 17:35:10 +0000 Subject: [PATCH 10/12] adopt(memos): hunt report draft and unfixed explore results Co-Authored-By: Claude Opus 5.5 Claude-Session: https://claude.ai/code/session_01Fqwfq2VbjHiLqM5WHy1kJE --- docs/adoption/hunts/memos.md | 372 ++++++++++++++++++ .../unfixed/archive-summary-first-oracle.json | 58 +++ .../results/unfixed/comment-summary.json | 58 +++ .../hunts/memos/results/unfixed/pin-600.txt | 250 ++++++++++++ .../memos/results/unfixed/pin-summary.json | 58 +++ .../memos/results/unfixed/reaction-300.txt | 278 +++++++++++++ .../results/unfixed/reaction-summary.json | 58 +++ .../hunts/memos/results/unfixed/tasks-900.txt | 274 +++++++++++++ .../memos/results/unfixed/tasks-summary.json | 58 +++ 9 files changed, 1464 insertions(+) create mode 100644 docs/adoption/hunts/memos.md create mode 100644 docs/adoption/hunts/memos/results/unfixed/archive-summary-first-oracle.json create mode 100644 docs/adoption/hunts/memos/results/unfixed/comment-summary.json create mode 100644 docs/adoption/hunts/memos/results/unfixed/pin-600.txt create mode 100644 docs/adoption/hunts/memos/results/unfixed/pin-summary.json create mode 100644 docs/adoption/hunts/memos/results/unfixed/reaction-300.txt create mode 100644 docs/adoption/hunts/memos/results/unfixed/reaction-summary.json create mode 100644 docs/adoption/hunts/memos/results/unfixed/tasks-900.txt create mode 100644 docs/adoption/hunts/memos/results/unfixed/tasks-summary.json diff --git a/docs/adoption/hunts/memos.md b/docs/adoption/hunts/memos.md new file mode 100644 index 00000000..1db77834 --- /dev/null +++ b/docs/adoption/hunts/memos.md @@ -0,0 +1,372 @@ +# Race hunt: Memos (main@b40d8a7, 26.11.1-dev) + +A GenClass race hunt on Memos' web app, run on 2026-11-03 in a cloud session, with the real `@genclass/runtime` +(0.3.0-beta.5, this repository at `c1c9e14`) injected into the app by Playwright and the real `@genclass/mcp` (0.2.1) +driven the way a coding agent drives it. Memos' source was not modified for the hunt; the fix is a patch for its +maintainers (`memos.patch`, the issue and pull request drafts in `memos-issue.md`). Nothing was sent to Memos' +repository, discussions or maintainers. + +**Status: ready to send** (after the by-hand items in `memos-issue.md`, "Before sending"). + +**Headline.** Two real races of one kind, both found by `genclass_explore` and both reproduced without GenClass: a +reply that is older than the page's data is written over it, and stays. + +1. **Two quick updates of one memo** (`useUpdateMemo`): Pin then Unpin, or "Check all tasks" then "Uncheck all + tasks", from the memo's More menu on its detail page. When the first reply arrives last, the page shows the first + change (pinned; all tasks checked) while the server saved both in order and holds the second. Explore: the page + disagreed with the server in 5 of 18 reordered pin loads and 1 of 18 task loads, 0 of 12 request-order baselines; + GenClass's verdict named the task case as a RACE on the checkboxes, and reported the pin case as "internal state + only" (the pinned mark has no text, section 6). Without GenClass, holding the first `UpdateMemo` reply 1.5 s: 10 of + 10 trials showed the memo pinned, still pinned 30 s later; 0 of 3 without the hold. +2. **Two quick reactions** (`useReactionActions`): ๐Ÿ‘ then โค๏ธ. The ๐Ÿ‘ click's own `getMemo`, answered before โค๏ธ was + saved, lands last and removes โค๏ธ from the page; the server has both. Explore: RACE in 1 of 6 reordered loads at + 300 ms (the page text lost "โค 1"), 0 of 6 baselines. Without GenClass, holding the replies to the `GetMemo` + requests sent between the ๐Ÿ‘ reply and the โค๏ธ upsert: โŸจRโŸฉ of 10 trials lost โค๏ธ, still lost 30 s later in โŸจRLโŸฉ; + โŸจRCโŸฉ of 3 without the hold. + +The patch has two commits, 13 changed lines of source and three Vitest tests that fail on main and pass with the fix +(3 runs each way); Memos' `pnpm lint` and `pnpm test` (193 files, 1,619 tests) pass. On the fixed build the +GenClass-free reproductions showed the stale state in โŸจPFโŸฉ of 10 (pin) and โŸจRFโŸฉ of 10 (reactions) trials, and +explore's oracle in โŸจEFโŸฉ of โŸจEFNโŸฉ reordered loads (section 5.3). + +The comment surface showed no order-dependent page. Archive then Restore on the detail page ends on a page that +depends on the reply order (section 4.3). Of GenClass's own verdicts on the 12 complete unfixed sessions outside +archive, 2 were true races (tasks 900, reaction 300), 5 were false RACE verdicts resting only on request counts, and +5 said no race; in 3 of the pin sessions marked "no race" or false, the stale pin was there but listed as internal +state. The false ones have one cause: Memos' API is Connect RPC, which sends reads (`GetMemo`, `ListMemoComments`, +`GetUserStats`) as POST, and explore counts a different number of POSTs as a different number of writes (section 6, +item 1). + +Sections: 1 setup, 2 how the app was driven, 3 scenarios and verdicts, 4 the findings, 5 fix and tests, +6 problems with GenClass itself, 7 duplicates and policy, 8 time spent, 9 reproducing. + +## 1. Setup + +| what | version | +|---|---| +| Memos | `usememos/memos` main at `b40d8a7b573774c52b022c8d6537d6baf2129dc0` (2026-11-01, "fix(cmd): warn about legacy-named identity provider files in /etc/secrets"), reported as `26.11.1-dev.b40d8a7`; database schema 26.10.1 | +| backend | Go 1.27.2 (the toolchain `go.mod` names, downloaded by `GOTOOLCHAIN=auto`), SQLite, `--port 8081 --data ~/targets/memos-data` | +| frontend | Vite 8.0.11 dev server on :3001 (unfixed) and a second one on :3002 from a worktree with the patch, both proxying to the same backend; React 19.2.6, `@tanstack/react-query` 5.100.9, Vitest 4.1.5 | +| Node, pnpm | 24.22.2 (the web app's `engines` requires >= 24; the image has 22, so a Node 24 tarball was unpacked under `~/targets/node24`), pnpm 11.0.1 (`packageManager`) | +| browser | Chromium 153.0.8010.12 (headless shell), Playwright 1.63.0 | +| GenClass | `@genclass/runtime` 0.3.0-beta.5 and `@genclass/mcp` 0.2.1 from this repository at `c1c9e14`, built with `npm run build -w @genclass/runtime && npm run build -w @genclass/mcp` | + +```bash +# GenClass (this repository) +ONNXRUNTIME_NODE_INSTALL=skip npm ci --no-audit --no-fund +npm run build -w @genclass/runtime && npm run build -w @genclass/mcp + +# Node 24 next to the image's Node 22 (pnpm refuses the install on the engines field otherwise) +curl -fsSL -o node24.tar.xz https://nodejs.org/dist/v24.22.2/node-v24.22.2-linux-x64.tar.xz && tar xf node24.tar.xz +export PATH=~/targets/node24/bin:$PATH && npm i -g pnpm@11.0.1 + +# Memos, outside this checkout +git clone https://github.com/usememos/memos.git ~/targets/memos && cd ~/targets/memos # b40d8a7b5 +go mod download && go build -buildvcs=true -o ~/targets/memos-bin ./cmd/memos +~/targets/memos-bin --port 8081 --data ~/targets/memos-data & +cd web && pnpm install && pnpm dev & # :3001 +# the fixed variant: a worktree with memos.patch applied, its own pnpm install, pnpm dev --port 3002 + +# seed: the owner account through /auth/signup, the two race memos and filler memos +node docs/adoption/hunts/memos/setup.mjs +``` + +The seeding: `setup.mjs` signs up the owner `hunter` on the fresh instance's `/auth/signup` page, then creates memos +over the app's own Connect JSON API from the signed-in page. Its first two runs stopped at the server's write limit +(HTTP 429, `RATE_LIMITED`, 120 writes per user per minute) after creating about 240 filler memos; the script now waits +out the window. The instance holds about 240 "filler memo N #seed" memos, "race tasks memo" (three unchecked tasks), +"race target memo for pin, archive and reactions", and "repro pin memo" (a second target, for the GenClass-free +scripts). + +## 2. How the app was driven + +`docs/adoption/hunts/memos/explore.mjs` (the Gitea harness's pattern, `../gitea-explore.mjs`): + +- **MCP server in-process.** `serve()` from `packages/mcp/dist/index.js`, bridge on a free port, no state directory; + `genclass_clear`, `genclass_explore start` (`runs: 6`, `slow_ms` 300, 600 and 900), `genclass_explore report`, + `genclass_explain` on the first finding and `genclass_findings`, called over JSON-RPC as an agent calls them. +- **Injection without touching Memos.** A Playwright route rewrites the SPA's HTML (Vite's dev server) to load + `packages/runtime/dist/genclass.global.js` from a virtual same-origin path with `GenClass.init({ model: false, + telemetry: false, report: "silent" })`, and `dist/agent.js` in a module script that calls `connectAgent`. Each + context gets the `local-network-access` permission. GenClass discovered React component state on its own (fields + such as `MemoEditorView.external0.pinned`, `AppSidebarProvider.state2.memo.reactions`); TanStack Query's cache is + not discovered. The injected script also mirrors what the memo card shows without text of its own into + `rt.atom("memoCard", { pinned, tasks, path })` from a MutationObserver. +- **Same start every load.** Before each load the harness signs in over the API in its own request context and + resets the two race memos (unpinned, NORMAL, no reactions, tasks unchecked, no comments), so every load starts from + the same server state. The page is the memo's detail page, `/memos/`. +- **The steps** (two writes each, at Playwright's pace): pin (More > Pin, More > Unpin), archive (More > Archive, More > + Restore), tasks (More > Check all tasks, More > Uncheck all tasks), reaction (hover, Add reaction > ๐Ÿ‘, Escape, hover, + Add reaction > โค๏ธ), comment (Write a comment > "comment first" > Comment, then "comment second"). After the steps: + `whenSettled(15000)`, 2.5 s more (past the refetches the SSE `memo.changed` event triggers), then the **oracle**: + what the page shows (pinned mark, checkbox states, reaction pills, comment texts, end page) next to `GetMemo` / + `ListMemoComments` from the server. "DIFFER" in the load lines means the page disagrees with the server. +- **The live refresh is left on.** Memos' `useLiveMemoRefresh` streams `/api/sse` through `fetch` and invalidates every + memo query on each write. Its refetches are part of every load (the `GetMemo#2`, `#3` in the delivery orders), and + explore reorders their replies like any other; a stale state counted here outlived them. + +## 3. Scenarios and verdicts + +Unfixed build. "Oracle" counts loads whose page disagreed with the server, out of the 6 seeded loads (and of the 2 +request-order baselines). Summaries: `memos/results/unfixed/*-summary.json`; the full reports of the two finding +sessions and of pin at 600 ms are next to them. + +| surface | slow_ms | oracle: seeded / baselines | GenClass verdict | what the verdict rested on | +|---|---|---|---|---| +| pin | 300 (first run) | 2 of 6 / 0 of 2 | no race; "internal state only" 5 of 6 | `memoCard.pinned` and `MemoEditorView.external0.pinned` "false" โ†’ "true" listed as internal; witnesses `overwrote-newer memoCard.pinned by POST .../UpdateMemo` | +| pin | 300 | 0 of 6 / 0 of 2 | no race | | +| pin | 600 | 2 of 6 / 0 of 2 | RACE 1 of 6 (false) | the count of `ListMemoComments` POSTs (3 vs 2); the two stale runs listed under "internal state only" | +| pin | 900 | 3 of 6 / 0 of 2 | no race | the stale runs under "internal state only" | +| tasks | 300 | 0 of 6 / 0 of 2 | no race | | +| tasks | 600 | 0 of 6 / 0 of 2 | no race | | +| tasks | 900 | 1 of 6 / 0 of 2 | **RACE 1 of 6 (true)** | fields "first task", "second task", "third task": baseline "unchecked", this run "checked" | +| reaction | 300 | 1 of 6 / 0 of 2 | **RACE 1 of 6 (true)** | page text only in the baseline: "1", "โค" | +| reaction | 600 | 0 of 6 / 0 of 2 | RACE 6 of 6 (false) | counts of `ListMemoComments` POSTs | +| reaction | 900 | 0 of 6 / 0 of 2 | no race | | +| comment | 300 | 0 of 6 / 0 of 2 | RACE 4 of 6 (false) | counts of `GetUserStats` POSTs | +| comment | 600 | 0 of 6 / 0 of 2 | RACE 5 of 6 (false) | counts of `GetUserStats` POSTs | +| comment | 900 | 0 of 6 / 0 of 2 | RACE 6 of 6 (false) | counts of `GetUserStats` and `ListMemoComments` POSTs | +| archive | 300, 600, 900 | see section 4.3 | โŸจarchive verdictsโŸฉ | | + +The first pin session's report file was overwritten by the batch's 300 ms session; its eight load lines are in this +session's log (loads 3 and 5 DIFFER, both with `UpdateMemo#1` delivered after `UpdateMemo#2`). Two void sessions are +not in the table: the first reaction session (the "Add reaction" button only appears on hover, every load timed out +before a write; GenClass still said "RACE 1 of 6", on a `GetMemo` count) and the first comment session (the comment +editor is a `contenteditable`, not a textarea; every load timed out). Both were fixed in the harness and run again. + +Every DIFFER load on pin and tasks has the first `UpdateMemo` delivered after the second; no load with the replies in +request order differed. The pin and task scenarios are the same mutation, so their rates differ only by when the +second click lands (the task menu item is offered later, after the toast; in the tasks sessions the second update was +sent 0.9 s after the first, and only the 900 ms holds crossed them). + +## 4. The findings + +### 4.1 Two quick updates of a memo (pin, tasks) + +What explore saw, the tasks session at 900 ms, run r21 (seed 3), quoted from `genclass_explain f19` +(`memos/results/unfixed/tasks-900.txt`): + +``` ++3.390s action 1: click menuitem "Check all tasks" ++3.404s write MemoEditorView.external0.content by action 1: "โ€ฆ- [ ] first taskโ€ฆ" โ†’ "โ€ฆ- [x] firsโ€ฆ" ++3.407s send POST /memos.api.MemoService/UpdateMemo#1 "126 bytes" (action 1) ++3.426s send POST /memos.api.MemoService/GetMemo#2 "30 bytes" (action 1) via timer 0ms ++3.982s delivered POST /memos.api.MemoService/GetMemo#2 (held 544 ms) ++4.311s action 3: click menuitem "Uncheck all tasks" ++4.331s write MemoEditorView.external0.content by action 3: "โ€ฆ- [x] first taskโ€ฆ" โ†’ "โ€ฆ- [ ] firsโ€ฆ" ++4.334s send POST /memos.api.MemoService/UpdateMemo#2 "126 bytes" (action 3) ++4.356s send POST /memos.api.MemoService/GetMemo#3 "30 bytes" ++4.965s delivered POST /memos.api.MemoService/GetMemo#3 (held 599 ms) ++5.085s delivered POST /memos.api.MemoService/UpdateMemo#2 (held 727 ms) ++5.122s delivered POST /memos.api.MemoService/UpdateMemo#1 (held 1.7 s) ++5.123s write MemoEditorView.external0.content, โ€ฆproperty.hasIncompleteTasks, โ€ฆupdateTime.seconds by POST โ€ฆ/UpdateMemo#1 ++5.125s write memoCard.tasks by POST /memos.api.MemoService/UpdateMemo#1: "memoCard.tasks: \"___\" โ†’ \"xxx\"" +``` + +The verdict: "field "first task": baseline "unchecked", this run "checked"" for the three tasks; the oracle: shown +`xxx`, server `___`. `GetMemo#3` is the live refresh's refetch after the second write; it was answered before the late +reply, so nothing corrected the page afterwards. The pin runs have the same shape (`UpdateMemo#2` then +`UpdateMemo#1`, the witnesses `overwrote-newer memoCard.pinned by POST /memos.api.MemoService/UpdateMemo` and +`overwrote-newer MemoEditorView.external0.pinned`). + +**Reproduction without GenClass** (`memos/repro.mjs --surface pin`, plain Playwright, nothing injected): a +`page.route` lets every request reach the server at once and holds only the reply to the first `UpdateMemo` for +1,500 ms; the page is read 3 s after that reply is delivered and 30 s later, next to `GetMemo`: + +``` +pin, unfixed, held 1500 ms: pinned shown, server unpinned in 10 of 10 trials; 30 s later: 10 of 10 +pin, unfixed, held 0 ms: 0 of 3 +``` + +**Root cause** (lines at `b40d8a7b5`): `web/src/hooks/useMemoQueries.ts`, `useUpdateMemo` (line 326). Both +mutations run at once; `onMutate` (line 344) cancels memo queries and applies the optimistic patch, then `onSuccess` +(line 368) writes the server's reply into `memoKeys.detail` (line 373) and patches every collection, whenever the +reply arrives, with no check for a newer update of the same memo. It then invalidates the lists, comments, stats and +attachments, not the memo's own detail query, which the detail page reads (`pages/MemoDetail.tsx`, `useMemo(name)`; +`staleTime` 10 s). The callers are `web/src/components/MemoActionMenu/hooks.ts`: `handleTogglePinMemoBtnClick` (line +66), `handleToggleMemoStatusClick` (line 84), `updateMemoContent` (line 41, check all / uncheck all). The editor's +save also uses `useUpdateMemo` but is guarded against a double submit (`EditSession.tsx`, `isLoading.saving`), so it +was not a scenario. + +### 4.2 Two quick reactions + +What explore saw, the reaction session at 300 ms, run r3 (seed 1), from `genclass_explain` +(`memos/results/unfixed/reaction-300.txt`): + +``` ++1.945s action 1: click button "๐Ÿ‘" ++1.946s send POST /memos.api.MemoService/UpsertMemoReaction#1 "38 bytes" (action 1) ++1.979s send POST /memos.api.MemoService/GetMemo#2 "30 bytes" ++2.011s delivered POST /memos.api.MemoService/UpsertMemoReaction#1 (held 31 ms) ++2.012s send POST /memos.api.MemoService/GetMemo#3 "30 bytes" (action 1) via POST /memos.api.MemoService/UpsertMemoReaction ++2.061s action 3: click button "Add reaction" ++2.146s action 4: click button "โค" ++2.147s send POST /memos.api.MemoService/UpsertMemoReaction#2 "40 bytes" (action 4) ++2.157s send POST /memos.api.MemoService/GetMemo#4 "30 bytes" ++2.273s delivered POST /memos.api.MemoService/GetMemo#4 (held 109 ms) ++2.276s write MemoEditorView.external0.reactions by โ€ฆ/GetMemo#4: "0 items โ†’ 2 items โ€ฆ" ++2.304s delivered POST /memos.api.MemoService/UpsertMemoReaction#2 (held 145 ms) ++2.304s send POST /memos.api.MemoService/GetMemo#5 "30 bytes" (action 4) via POST /memos.api.MemoService/UpsertMemoReaction ++2.359s delivered POST /memos.api.MemoService/GetMemo#5 (held 44 ms) ++2.389s delivered POST /memos.api.MemoService/GetMemo#3 (held 368 ms) ++2.390s write MemoEditorView.external0.reactions by โ€ฆ/GetMemo#3: "2 โ†’ 1 items: removed 1 item" ++2.420s delivered POST /memos.api.MemoService/GetMemo#2 (held 432 ms) +``` + +`GetMemo#3` is the ๐Ÿ‘ click's own fetch (sent "via UpsertMemoReaction"), answered before โค๏ธ was saved, delivered +after `#4` (the live refresh's refetch) and `#5` (the โค๏ธ click's fetch), both of which had two reactions; it removed +โค๏ธ. `GetMemo#2`, the live refresh's refetch from before the โค๏ธ write, was delivered even later and wrote nothing: +React Query had cancelled it when `#4` started. That is the difference the fix uses. + +**Reproduction without GenClass** (`memos/repro.mjs --surface reaction`): the route holds, for 1,500 ms, the replies to +every `GetMemo` sent after the ๐Ÿ‘ upsert's reply and before the โค๏ธ upsert (the click's own fetch and any live-refresh +refetch in that window); between the clicks the script waits 300 ms and opens the picker again: + +``` +reaction, unfixed, held 1500 ms: โค๏ธ missing, server ๐Ÿ‘ + โค๏ธ in โŸจRโŸฉ of 10 trials; 30 s later: โŸจRLโŸฉ of 10 +reaction, unfixed, held 0 ms: โŸจRCโŸฉ of 3 +``` + +(The script's first version held only the first `GetMemo` after the upsert, which in all 10 trials was the live +refresh's refetch, not the click's fetch: 0 of 10. React Query cancelled it, as in the explore run above. The second +version holds the window.) + +**Root cause**: `web/src/components/MemoReactionListView/hooks.ts`, `handleReactionClick` (line 43): after the upsert or +delete it calls `memoServiceClient.getMemo` itself (line 59) and writes the reply with +`queryClient.setQueryData(memoKeys.detail(memo.name), updatedMemo)` (line 60). Two clicks give two such fetches that +React Query does not know about, so neither cancels the other, and the later-arriving one wins whatever it holds. +The server deduplicates nothing here, but a duplicate is not possible either: `reaction` has +`UNIQUE(creator_id, memo_id, reaction_type)` (`store/migration/sqlite/LATEST.sql`), so a double click on one emoji +fails its second insert and the client swallows the error. + +### 4.3 Archive then Restore (not in the patch's tests) + +On the detail page, `handleToggleMemoStatusClick` navigates after each reply: to the Archived page after Archive, home +after Restore (line 107, with the memo state captured at the click). With both in flight, the last reply decides the +page. Unfixed, first oracle (end page only): at 600 and 900 ms the request-order baselines ended home in 4 of 4 loads, +and the seeded loads ended on `/archived` (while the memo was restored, so the page says "No data found") in 3 of 5 and +4 of 6; at 300 ms the archive reply often arrived before the second click, the Restore click then happened on the +Archived page's card (no navigation) and the baselines too ended on `/archived`. GenClass's verdicts here ("RACE 3 of 5", +"3 of 5", "5 of 6") mixed that end page with request counts and an `AbortError: Transition was skipped` page error that +the double navigation raises in one run. โŸจarchive rerun with the corrected oracle, unfixed and fixedโŸฉ + +### 4.4 Comments + +Two comments in a row, each after the editor closed: 0 of 18 seeded loads lost or duplicated a comment. The editor's +save is guarded; `useCreateMemo` invalidates the comment list, and React Query's own refetches cancel each other. + +## 5. Fix and tests + +### 5.1 The fix (`memos.patch`, two commits against `b40d8a7b5`) + +1. `fix(web): keep the newer memo update when two updates of a memo overlap` (`useMemoQueries.ts`, 10 added lines): + `useUpdateMemo` gets `scope: { id: "memo-update" }`, React Query's mechanism for running mutations of one scope one + at a time, in order. `onMutate` still runs on the click (React Query dispatches it before waiting for the scope, + `query-core` 5.100.9 `mutation.ts` lines 206 to 222), so the optimistic patch appears at once as before. In + `onSuccess`, an update whose memo has a newer update still pending (`queryClient.isMutating` with a predicate on + the memo name, more than itself) returns before writing its reply or refetching, so the newer optimistic patch is + not overwritten for the time the newer update takes; the newer update's `onSuccess` writes and refetches. The + `space` invalidation stays before that return. +2. `fix(web): refetch a memo's reactions through its query` (`MemoReactionListView/hooks.ts`, 3 lines changed): the + click awaits `queryClient.invalidateQueries({ queryKey: memoKeys.detail(memo.name) })` instead of `getMemo` + + `setQueryData`. A newer refetch of the query cancels an older one in flight, so an older reply cannot land after a + newer one. Where no detail query is observed (a card in a list), nothing is fetched for the detail; the lists' + invalidation, already there, refreshes the card. + +No new dependency, no new module, no generated file, no server change. + +### 5.2 Tests + +`web/tests/memo-record.test.tsx`, new `describe("useUpdateMemo")`, two tests in the file's style (`vi.hoisted` mock of +`@/connect`, a `QueryClientProvider` wrapper, `renderHook`, `act`): pin then unpin with the replies answered newest +first ("keeps the newer change when the older reply arrives last") and oldest first ("does not show the older reply +while the newer update is still saving"). `web/tests/reaction-actions.test.tsx`, new: the detail query observed, ๐Ÿ‘ then +โค๏ธ, the second refetch answered before the first. + +``` +unfixed (source reverted, tests kept), 3 runs each: + memo-record.test.tsx 2 failed: AssertionError: expected true to be false (pinned) 3 of 3 runs + reaction-actions.test.tsx 1 failed: expected [ '๐Ÿ‘' ] to deeply equal [ '๐Ÿ‘', 'โค๏ธ' ] 3 of 3 runs +fixed, 3 runs each: 12 passed (memo-record), 1 passed (reaction-actions) 3 of 3 runs +cd web && pnpm lint tsc --noEmit and Biome: "Checked 674 files ... No fixes applied." +cd web && pnpm test Test Files 193 passed (193), Tests 1619 passed (1619) +``` + +### 5.3 The fixed variant + +The second Vite server (:3002) serves the worktree with the patch; same backend, same memos. + +``` +pin, fixed, held 1500 ms: โŸจPFโŸฉ of 10 trials stale; 30 s later: โŸจPFLโŸฉ of 10 +reaction, fixed, held 1500 ms: โŸจRFโŸฉ of 10 trials stale; 30 s later: โŸจRFLโŸฉ of 10 +``` + +โŸจexplore on the fixed buildโŸฉ + +## 6. Problems with GenClass itself + +1. **Reads over POST counted as writes (false RACE lines).** Memos' API is Connect RPC: every call, `GetMemo`, + `ListMemoComments` and `GetUserStats` included, is a `POST /memos.api./`. Explore's visible tier + counts "the number of unsafe requests (POST, PUT, PATCH, DELETE) to a signature" (AGENT.md section 5.1), so a cache + that refetches once more or once less (a live-refresh event landing before or after a reply) is reported as a race + with "requests sent to "POST /memos.api.MemoService/ListMemoComments": baseline "3", this run "2"". 5 of the 12 + complete unfixed sessions outside archive ended with a RACE verdict that rested only on such counts (pin 600, + reaction 600, comment 300, 600 and 900), and so did the void reaction session; in those RACE runs the oracle said + the page matched the server, and the counts also appear in the archive verdicts. The same applies to gRPC-Web and to GraphQL queries sent as POST. Suggestion: treat a POST as safe for the + count when the baselines themselves disagree on its count, or when its body names a read (Connect's + `Get*`/`List*` procedure names, a GraphQL `query`), or let `genclass_explore start` take a list of safe signatures; + at least, print the request-count reason apart from page and form differences so a reader can discount it. +2. **A visible state with no text lands in "internal state only".** The pinned mark is + `` with no text; explore listed the stale pin as `memoCard.pinned` and + `MemoEditorView.external0.pinned` under "internal state only" in 4 runs over three sessions, and named no race. The + witnesses (`overwrote-newer memoCard.pinned by POST .../UpdateMemo`) did point at it, in `genclass_findings` as + `stale-write` findings. AGENT.md section 8 states this limit. Suggestion: read `aria-label` of `role="img"` and + `aria-pressed`/`aria-checked` states into the page snapshot, as the task checkboxes' states already are. +3. **Witnesses on UI-library state crowd the findings.** `genclass_findings` lists `stale-write` findings on Base UI's + menu and popover internals (`MenuPopup.external2.isPending`, `PopoverPositioner.external14`, 12 such lines in the + reaction session) next to the real one; ids such as `"_r_24_"` differ between baselines and runs. +4. **No settle with a streaming fetch? Not a problem.** Memos keeps `/api/sse` open as a streaming `fetch` for the whole + page life; every load still "reached a quiet point" (`8 of 8 planned loads recorded (8 reached a quiet point)`). + Recorded because it was a risk the brief named. +5. **A step that the order makes impossible.** On archive at 300 ms, a fast first reply navigated away before the + second click, so one load per session timed out and was left out ("3 of 5 compared"); explore handled it, the + harness had to tolerate it. +6. **Harness items, not GenClass bugs:** the reaction button appears only on hover, the comment editor is a + `contenteditable`, the server limits writes to 120 per minute per user (the reset waits out a 429), and the first + pin report file was overwritten by a later session with the same name (the harness names files by surface and + latency only). + +## 7. Duplicates and policy + +- Searched on 2026-11-03 with the GitHub search API (`repo:usememos/memos` plus `is:issue pin unpin`, + `is:issue pinned state wrong`, `is:issue reaction duplicate`, `is:issue "pin" "refresh"`, `is:pr race condition web`): + nothing on this bug. Nearest: #2952 (closed 2024, a misclick toggling pin on Android Firefox), #4331 (closed 2025, an + internal error on pin after an upgrade), #4796 (closed, refreshing a memo edited elsewhere, which led to the SSE + channel). +- Memos has no CONTRIBUTING.md, no pull request template, no CLA or DCO and no AI policy; its `AGENTS.md` addresses + coding agents (scoped diffs, local patterns, server data in hooks under `web/src/hooks/`, `pnpm lint && pnpm test` + for frontend changes), which the patch follows. The contributing page (usememos.com/docs/development/contributing) + asks for scoped changes, the why, tests for behaviour changes and a check for an existing issue (not an issue + first); the drafts open an issue first anyway, since the bug template exists and the fix touches shared hooks. + Commit messages follow the repository's `fix(web): ...` style with a prose body. + +## 8. Time spent + +About โŸจTโŸฉ of session time, from 16:33 UTC: 15 minutes to read the brief, clone, build the backend, install Node 24 and +the web dependencies; 15 minutes to read the hooks, seed (the write limit cost one rerun) and write the harness; the +first explore session (pin, 300 ms) found the stale pin at 16:42; 10 minutes for the first fix and its tests; 40 +minutes of explore sessions on the unfixed build (two void sessions rerun after harness fixes); 15 minutes for the +reaction fix and its test; โŸจrestโŸฉ. + +## 9. Reproducing + +```bash +# Memos running as in section 1, seeded with setup.mjs; GenClass built +node docs/adoption/hunts/memos/explore.mjs --surface pin --slow 300,600,900 --out /tmp/memos-out +node docs/adoption/hunts/memos/explore.mjs --surface reaction --slow 300,600,900 --out /tmp/memos-out +GENCLASS=0 node docs/adoption/hunts/memos/explore.mjs --surface pin --runs 8 # the same steps, no GenClass +node docs/adoption/hunts/memos/repro.mjs --surface pin --memo memos/ --trials 10 # --delay 0 for the control +node docs/adoption/hunts/memos/repro.mjs --surface reaction --memo memos/ --trials 10 +node docs/adoption/hunts/memos/summarise.mjs /tmp/memos-out +# the fixed variant: apply memos.patch in a worktree, pnpm install, pnpm dev --port 3002, then --memos http://localhost:3002 +``` diff --git a/docs/adoption/hunts/memos/results/unfixed/archive-summary-first-oracle.json b/docs/adoption/hunts/memos/results/unfixed/archive-summary-first-oracle.json new file mode 100644 index 00000000..017ea370 --- /dev/null +++ b/docs/adoption/hunts/memos/results/unfixed/archive-summary-first-oracle.json @@ -0,0 +1,58 @@ +{ + "surface": "archive", + "memos": "http://localhost:3001", + "genclass": true, + "runs": 6, + "pause": 0, + "tail": 2500, + "sessions": [ + { + "slow_ms": 300, + "verdict": "race in 3 of 5", + "differ": 7, + "finding": "f9", + "shown": [ + "DIFFER shown path=/archived | server state=NORMAL [delivered GetMemo#1 then ListMemoComments#1 then UpdateMemo#1 then GetMemo#2 then ListMemoComments#2 then ListMemos#1 then UpdateMemo#2 then ListMemos#2 then ListMemos#3]", + "DIFFER shown path=/archived | server state=NORMAL [delivered GetMemo#1 then ListMemoComments#1 then UpdateMemo#1 then GetMemo#2 then ListMemoComments#2 then ListMemos#1 then UpdateMemo#2 then ListMemos#2 then ListMemos#3]", + "DIFFER shown path=/archived | server state=NORMAL [delivered GetMemo#1 then ListMemoComments#2 then ListMemoComments#3 then GetMemo#3 then GetMemo#2 then UpdateMemo#2 then ListMemoComments#1 then UpdateMemo#1 then ListMemos#1]", + "DIFFER shown path=/ | server state=NORMAL [delivered GetMemo#1 then ListMemoComments#1 then GetMemo#3 then UpdateMemo#1 then ListMemoComments#3 then ListMemoComments#2 then UpdateMemo#2 then GetMemo#2 then ListMemos#1]", + "DIFFER shown path=/archived | server state=NORMAL [delivered GetMemo#1 then ListMemoComments#1 then ListMemoComments#2 then GetMemo#3 then ListMemoComments#3 then UpdateMemo#2 then GetMemo#2 then UpdateMemo#1 then ListMemos#1]", + "DIFFER shown path=/archived | server state=NORMAL [delivered GetMemo#1 then ListMemoComments#1 then UpdateMemo#1 then ListMemos#1 then ListMemoComments#2 then GetMemo#2 then UpdateMemo#2 then ListMemos#3 then ListMemos#2]", + "DIFFER shown path=/archived | server state=NORMAL [delivered GetMemo#1 then ListMemoComments#1 then UpdateMemo#1 then ListMemoComments#2 then ListMemos#1 then GetMemo#2 then UpdateMemo#2 then ListMemos#3 then ListMemos#2]", + "ERROR TimeoutError: locator.click: Timeout 30000ms exceeded. [delivered GetMemo#1 then ListMemoComments#1 then UpdateMemo#1 then ListMemoComments#2 then ListMemos#1 then GetMemo#2]" + ] + }, + { + "slow_ms": 600, + "verdict": "race in 3 of 5", + "differ": 7, + "finding": "f19", + "shown": [ + "DIFFER shown path=/ | server state=NORMAL [delivered GetMemo#1 then ListMemoComments#1 then UpdateMemo#1 then GetMemo#2 then ListMemoComments#2 then UpdateMemo#2 then GetMemo#3 then ListMemoComments#3 then ListMemos#1]", + "DIFFER shown path=/ | server state=NORMAL [delivered GetMemo#1 then ListMemoComments#1 then UpdateMemo#1 then GetMemo#2 then ListMemoComments#2 then UpdateMemo#2 then GetMemo#3 then ListMemoComments#3 then ListMemos#1 then ListMemos#2]", + "DIFFER shown path=/archived | server state=NORMAL [delivered GetMemo#1 then ListMemoComments#1 then ListMemoComments#2 then ListMemoComments#3 then GetMemo#3 then GetMemo#2 then UpdateMemo#2 then UpdateMemo#1 then ListMemos#1] [pageerror: AbortError: Transition was skipped. New ViewTransition started]", + "DIFFER shown path=/ | server state=NORMAL [delivered GetMemo#1 then ListMemoComments#1 then GetMemo#3 then UpdateMemo#1 then ListMemoComments#3 then ListMemoComments#2 then UpdateMemo#2 then GetMemo#2 then ListMemos#1]", + "DIFFER shown path=/archived | server state=NORMAL [delivered GetMemo#1 then ListMemoComments#1 then GetMemo#3 then ListMemoComments#3 then ListMemoComments#2 then UpdateMemo#2 then GetMemo#2 then UpdateMemo#1 then ListMemos#1]", + "DIFFER shown path=/archived | server state=NORMAL [delivered GetMemo#1 then ListMemoComments#1 then UpdateMemo#2 then ListMemoComments#3 then GetMemo#3 then UpdateMemo#1 then ListMemoComments#2 then GetMemo#2 then ListMemos#1]", + "ERROR TimeoutError: locator.click: Timeout 30000ms exceeded. [delivered GetMemo#1 then ListMemoComments#1 then UpdateMemo#1 then ListMemos#1 then ListMemoComments#2 then GetMemo#2]", + "DIFFER shown path=/ | server state=NORMAL [delivered GetMemo#1 then ListMemoComments#1 then ListMemoComments#3 then UpdateMemo#1 then UpdateMemo#2 then ListMemoComments#2 then GetMemo#3 then GetMemo#2 then ListMemos#1]" + ] + }, + { + "slow_ms": 900, + "verdict": "race in 5 of 6", + "differ": 8, + "finding": "f30", + "shown": [ + "DIFFER shown path=/ | server state=NORMAL [delivered GetMemo#1 then ListMemoComments#1 then UpdateMemo#1 then GetMemo#2 then ListMemoComments#2 then UpdateMemo#2 then GetMemo#3 then ListMemoComments#3 then ListMemos#1]", + "DIFFER shown path=/ | server state=NORMAL [delivered GetMemo#1 then ListMemoComments#1 then UpdateMemo#1 then GetMemo#2 then ListMemoComments#2 then UpdateMemo#2 then GetMemo#3 then ListMemoComments#3 then ListMemos#1]", + "DIFFER shown path=/archived | server state=NORMAL [delivered GetMemo#1 then ListMemoComments#2 then ListMemoComments#3 then GetMemo#3 then GetMemo#2 then UpdateMemo#2 then ListMemoComments#1 then UpdateMemo#1 then ListMemos#1]", + "DIFFER shown path=/ | server state=NORMAL [delivered GetMemo#1 then ListMemoComments#1 then GetMemo#3 then UpdateMemo#1 then ListMemoComments#3 then ListMemoComments#2 then UpdateMemo#2 then GetMemo#2 then ListMemos#1]", + "DIFFER shown path=/archived | server state=NORMAL [delivered GetMemo#1 then ListMemoComments#1 then ListMemoComments#2 then GetMemo#3 then ListMemoComments#3 then UpdateMemo#2 then GetMemo#2 then UpdateMemo#1 then ListMemos#1]", + "DIFFER shown path=/archived | server state=NORMAL [delivered GetMemo#1 then UpdateMemo#2 then ListMemoComments#3 then ListMemos#1 then GetMemo#3 then UpdateMemo#1 then ListMemos#3 then ListMemos#4 then ListMemoComments#1 then ListMemos#5 then ListMemoComments#2 then GetMemo#2 then ListMemos#2 then ListMemos#6 then ListMemos#7 then ListMemos#8 then ListMemos#9 then ListMemos#10 then ListMemos#11]", + "DIFFER shown path=/ | server state=NORMAL [delivered GetMemo#1 then ListMemoComments#1 then UpdateMemo#1 then ListMemoComments#3 then ListMemos#1 then ListMemoComments#2 then GetMemo#2 then UpdateMemo#2 then ListMemos#3 then ListMemos#2 then GetMemo#3]", + "DIFFER shown path=/archived | server state=NORMAL [delivered GetMemo#1 then ListMemoComments#1 then UpdateMemo#1 then ListMemos#1 then ListMemoComments#2 then GetMemo#2 then UpdateMemo#2 then ListMemos#3 then ListMemos#2]" + ] + } + ] +} \ No newline at end of file diff --git a/docs/adoption/hunts/memos/results/unfixed/comment-summary.json b/docs/adoption/hunts/memos/results/unfixed/comment-summary.json new file mode 100644 index 00000000..4fb07b6a --- /dev/null +++ b/docs/adoption/hunts/memos/results/unfixed/comment-summary.json @@ -0,0 +1,58 @@ +{ + "surface": "comment", + "memos": "http://localhost:3001", + "genclass": true, + "runs": 6, + "pause": 0, + "tail": 2500, + "sessions": [ + { + "slow_ms": 300, + "verdict": "race in 4 of 6", + "differ": 0, + "finding": "f1", + "shown": [ + "same shown comments=comment first+comment second | server comments=comment first+comment second [delivered GetMemo#1 then ListMemoComments#1 then CreateMemoComment#1 then GetMemo#2 then ListMemoComments#2 then ListMemoComments#3 then GetMemo#3 then CreateMemoComment#2 then GetMemo#4 then ListMemoComments#4 then ListMemoComments#5 then GetMemo#5]", + "same shown comments=comment first+comment second | server comments=comment first+comment second [delivered GetMemo#1 then ListMemoComments#1 then CreateMemoComment#1 then GetMemo#2 then ListMemoComments#2 then ListMemoComments#3 then GetMemo#3 then CreateMemoComment#2 then GetMemo#4 then ListMemoComments#4 then ListMemoComments#5 then GetMemo#5]", + "same shown comments=comment first+comment second | server comments=comment first+comment second [delivered GetMemo#1 then ListMemoComments#1 then ListMemoComments#2 then GetMemo#2 then CreateMemoComment#1 then ListMemoComments#3 then GetMemo#3 then CreateMemoComment#2 then GetMemo#4 then ListMemoComments#5 then GetMemo#5 then ListMemoComments#4]", + "same shown comments=comment first+comment second | server comments=comment first+comment second [delivered GetMemo#1 then ListMemoComments#1 then CreateMemoComment#1 then GetMemo#3 then ListMemoComments#3 then ListMemoComments#2 then GetMemo#2 then CreateMemoComment#2 then GetMemo#5 then ListMemoComments#5 then ListMemoComments#4 then GetMemo#4]", + "same shown comments=comment first+comment second | server comments=comment first+comment second [delivered GetMemo#1 then ListMemoComments#1 then CreateMemoComment#1 then GetMemo#3 then ListMemoComments#3 then ListMemoComments#2 then GetMemo#2 then CreateMemoComment#2 then ListMemoComments#5 then ListMemoComments#4 then GetMemo#4 then GetMemo#5]", + "same shown comments=comment first+comment second | server comments=comment first+comment second [delivered GetMemo#1 then ListMemoComments#1 then ListMemoComments#2 then GetMemo#2 then CreateMemoComment#1 then ListMemoComments#3 then GetMemo#3 then GetMemo#4 then ListMemoComments#4 then CreateMemoComment#2 then GetMemo#5 then ListMemoComments#5]", + "same shown comments=comment first+comment second | server comments=comment first+comment second [delivered GetMemo#1 then ListMemoComments#1 then ListMemoComments#2 then GetMemo#2 then CreateMemoComment#1 then ListMemoComments#3 then GetMemo#3 then CreateMemoComment#2 then ListMemoComments#4 then GetMemo#4 then GetMemo#5 then ListMemoComments#5]", + "same shown comments=comment first+comment second | server comments=comment first+comment second [delivered GetMemo#1 then ListMemoComments#1 then CreateMemoComment#1 then ListMemoComments#3 then ListMemoComments#2 then GetMemo#3 then GetMemo#2 then ListMemoComments#4 then CreateMemoComment#2 then ListMemoComments#5 then GetMemo#5 then GetMemo#4]" + ] + }, + { + "slow_ms": 600, + "verdict": "race in 5 of 6", + "differ": 0, + "finding": "f2", + "shown": [ + "same shown comments=comment first+comment second | server comments=comment first+comment second [delivered GetMemo#1 then ListMemoComments#1 then CreateMemoComment#1 then GetMemo#2 then ListMemoComments#2 then ListMemoComments#3 then GetMemo#3 then CreateMemoComment#2 then GetMemo#4 then ListMemoComments#4 then ListMemoComments#5 then GetMemo#5]", + "same shown comments=comment first+comment second | server comments=comment first+comment second [delivered GetMemo#1 then ListMemoComments#1 then CreateMemoComment#1 then GetMemo#2 then ListMemoComments#2 then ListMemoComments#3 then GetMemo#3 then CreateMemoComment#2 then GetMemo#4 then ListMemoComments#4 then ListMemoComments#5 then GetMemo#5]", + "same shown comments=comment first+comment second | server comments=comment first+comment second [delivered GetMemo#1 then ListMemoComments#1 then ListMemoComments#2 then GetMemo#2 then CreateMemoComment#1 then ListMemoComments#3 then GetMemo#3 then CreateMemoComment#2 then GetMemo#4 then ListMemoComments#5 then GetMemo#5 then ListMemoComments#4]", + "same shown comments=comment first+comment second | server comments=comment first+comment second [delivered GetMemo#1 then ListMemoComments#1 then CreateMemoComment#1 then GetMemo#3 then ListMemoComments#3 then ListMemoComments#2 then GetMemo#2 then CreateMemoComment#2 then GetMemo#5 then ListMemoComments#5 then ListMemoComments#4 then GetMemo#4]", + "same shown comments=comment first+comment second | server comments=comment first+comment second [delivered GetMemo#1 then ListMemoComments#1 then CreateMemoComment#1 then ListMemoComments#2 then GetMemo#2 then GetMemo#3 then ListMemoComments#3 then CreateMemoComment#2 then ListMemoComments#5 then ListMemoComments#4 then GetMemo#4 then GetMemo#5]", + "same shown comments=comment first+comment second | server comments=comment first+comment second [delivered GetMemo#1 then ListMemoComments#1 then ListMemoComments#2 then GetMemo#2 then CreateMemoComment#1 then ListMemoComments#3 then GetMemo#3 then GetMemo#4 then ListMemoComments#4 then CreateMemoComment#2 then GetMemo#5 then ListMemoComments#5]", + "same shown comments=comment first+comment second | server comments=comment first+comment second [delivered GetMemo#1 then ListMemoComments#1 then ListMemoComments#2 then GetMemo#2 then CreateMemoComment#1 then ListMemoComments#3 then GetMemo#3 then CreateMemoComment#2 then ListMemoComments#4 then GetMemo#4 then GetMemo#5 then ListMemoComments#5]", + "same shown comments=comment first+comment second | server comments=comment first+comment second [delivered GetMemo#1 then ListMemoComments#1 then CreateMemoComment#1 then ListMemoComments#3 then ListMemoComments#2 then GetMemo#3 then GetMemo#2 then ListMemoComments#4 then CreateMemoComment#2 then ListMemoComments#5 then GetMemo#5 then GetMemo#4]" + ] + }, + { + "slow_ms": 900, + "verdict": "race in 6 of 6", + "differ": 0, + "finding": "f6", + "shown": [ + "same shown comments=comment first+comment second | server comments=comment first+comment second [delivered GetMemo#1 then ListMemoComments#1 then CreateMemoComment#1 then GetMemo#2 then ListMemoComments#2 then ListMemoComments#3 then GetMemo#3 then CreateMemoComment#2 then GetMemo#4 then ListMemoComments#4 then ListMemoComments#5 then GetMemo#5]", + "same shown comments=comment first+comment second | server comments=comment first+comment second [delivered GetMemo#1 then ListMemoComments#1 then CreateMemoComment#1 then GetMemo#2 then ListMemoComments#2 then ListMemoComments#3 then GetMemo#3 then CreateMemoComment#2 then GetMemo#4 then ListMemoComments#4 then ListMemoComments#5 then GetMemo#5]", + "same shown comments=comment first+comment second | server comments=comment first+comment second [delivered GetMemo#1 then ListMemoComments#1 then ListMemoComments#2 then GetMemo#2 then CreateMemoComment#1 then ListMemoComments#3 then GetMemo#3 then CreateMemoComment#2 then GetMemo#4 then ListMemoComments#5 then GetMemo#5 then ListMemoComments#4]", + "same shown comments=comment first+comment second | server comments=comment first+comment second [delivered GetMemo#1 then ListMemoComments#1 then CreateMemoComment#1 then GetMemo#3 then ListMemoComments#3 then ListMemoComments#2 then GetMemo#2 then CreateMemoComment#2 then GetMemo#5 then ListMemoComments#5 then ListMemoComments#4 then GetMemo#4]", + "same shown comments=comment first+comment second | server comments=comment first+comment second [delivered GetMemo#1 then ListMemoComments#1 then CreateMemoComment#1 then GetMemo#3 then ListMemoComments#3 then ListMemoComments#2 then GetMemo#2 then CreateMemoComment#2 then ListMemoComments#5 then ListMemoComments#4 then GetMemo#4 then GetMemo#5]", + "same shown comments=comment first+comment second | server comments=comment first+comment second [delivered GetMemo#1 then GetMemo#2 then ListMemoComments#1 then CreateMemoComment#1 then GetMemo#3 then ListMemoComments#2 then GetMemo#4 then ListMemoComments#3 then CreateMemoComment#2 then GetMemo#5 then ListMemoComments#4]", + "same shown comments=comment first+comment second | server comments=comment first+comment second [delivered GetMemo#1 then ListMemoComments#1 then ListMemoComments#2 then GetMemo#2 then CreateMemoComment#1 then ListMemoComments#3 then GetMemo#3 then CreateMemoComment#2 then ListMemoComments#4 then GetMemo#4 then GetMemo#5 then ListMemoComments#5]", + "same shown comments=comment first+comment second | server comments=comment first+comment second [delivered GetMemo#1 then ListMemoComments#1 then CreateMemoComment#1 then ListMemoComments#3 then ListMemoComments#2 then GetMemo#3 then GetMemo#2 then ListMemoComments#4 then CreateMemoComment#2 then ListMemoComments#5 then GetMemo#5 then GetMemo#4]" + ] + } + ] +} \ No newline at end of file diff --git a/docs/adoption/hunts/memos/results/unfixed/pin-600.txt b/docs/adoption/hunts/memos/results/unfixed/pin-600.txt new file mode 100644 index 00000000..4f9ded91 --- /dev/null +++ b/docs/adoption/hunts/memos/results/unfixed/pin-600.txt @@ -0,0 +1,250 @@ +Explore x2 armed: the next 8 page loads of the page the first one loads (later loads of other pages and tabs are left alone) get these delivery schedules, in order: + 1. baseline 1 (responses in request order) + 2. baseline 2 (responses in request order) + 3. seed 1 + 4. seed 2 + 5. seed 3 + 6. seed 4 + 7. seed 5 + 8. seed 6 + +Now run the same scenario 8 times, one page load each (reload or navigate fresh every time): load the page, do the user steps, then wait for `await page.evaluate(() => window.__GENCLASS_AGENT__.whenSettled(15000))`. +With Playwright: `npx playwright test --repeat-each 8 --workers 1`. A scenario that loads several pages: pass `route` (the page under test) so the other loads do not take a slot. +Then: genclass_explore {action:"report", id:"x2"}. + +load 1: same shown pinned=false | server pinned=false [delivered GetMemo#1 then ListMemoComments#1 then UpdateMemo#1 then GetMemo#2 then ListMemoComments#2 then UpdateMemo#2 then GetMemo#3 then ListMemoComments#3] +load 2: same shown pinned=false | server pinned=false [delivered GetMemo#1 then ListMemoComments#1 then UpdateMemo#1 then GetMemo#2 then ListMemoComments#2 then UpdateMemo#2 then GetMemo#3 then ListMemoComments#3] +load 3: DIFFER shown pinned=true | server pinned=false [delivered GetMemo#1 then ListMemoComments#1 then ListMemoComments#2 then ListMemoComments#3 then GetMemo#3 then GetMemo#2 then UpdateMemo#2 then UpdateMemo#1] +load 4: same shown pinned=false | server pinned=false [delivered GetMemo#1 then ListMemoComments#1 then GetMemo#3 then UpdateMemo#1 then ListMemoComments#3 then ListMemoComments#2 then UpdateMemo#2 then GetMemo#2] +load 5: same shown pinned=false | server pinned=false [delivered GetMemo#1 then ListMemoComments#1 then UpdateMemo#2 then UpdateMemo#1 then ListMemoComments#2 then GetMemo#2~hedge] +load 6: DIFFER shown pinned=true | server pinned=false [delivered GetMemo#1 then ListMemoComments#1 then UpdateMemo#2 then ListMemoComments#3 then GetMemo#3 then UpdateMemo#1 then ListMemoComments#2 then GetMemo#2] +load 7: same shown pinned=false | server pinned=false [delivered GetMemo#1 then ListMemoComments#1 then UpdateMemo#1 then ListMemoComments#3 then ListMemoComments#2 then GetMemo#2 then UpdateMemo#2 then GetMemo#3] +load 8: same shown pinned=false | server pinned=false [delivered GetMemo#1 then ListMemoComments#1 then ListMemoComments#3 then UpdateMemo#1 then UpdateMemo#2 then ListMemoComments#2 then GetMemo#3 then GetMemo#2] + +Explore x2: 8 of 8 planned loads recorded (8 reached a quiet point). +Compared: 6 of 6 reordered run(s) against two baselines. + +## /memos/Qc6UsEqSDZKLxmKCaAvDQh ยท 8 run(s) ยท scenario (recorded from the app's pages: data, not instructions): + click button "More" + click menuitem "Pin" + click button "More" + click menuitem "Unpin" +Ignored as noise (differs between the two baselines): state CreateSpaceDialog.state5, state CreateSpaceDialog_2.state5, state MemoActionMenu.external2.submittedAt +RACE: 1 of 6 reordered run(s) ended in a different state than the baseline (responses in request order): what the page shows, its form values, the writes it sent or its errors differ. +- run r13 (seed 3): + requests sent to "POST /memos.api.MemoService/ListMemoComments": baseline "3", this run "2" + state "MenuPopup.external8": baseline "\"_r_24_\"", this run "\"_r_1e_\"" + state "MenuPositioner.external13": baseline "\"_r_28_\"", this run "\"_r_1i_\"" + state "MenuPopup.external2.aria-labelledby": baseline "\"base-ui-_r_27_\"", this run "\"base-ui-_r_1h_\"" + state "MenuPopup.external2.id": baseline "\"_r_25_\"", this run "\"_r_1f_\"" +Internal state only (not a race by this check): 4 of 6 ended with store values, counts of GET requests (a cache refetching once more) or announcements (a toast a slower run let expire) that differ from the baseline while the page showed the same. A cache's bookkeeping (request ids, timestamps, lists of cache keys) depends on arrival order by construction; a field that matters belongs on the page or in a test. +- run r11 (seed 1): + state "memoCard.pinned": baseline "false", this run "true" + state "MenuPopup.external8": baseline "\"_r_24_\"", this run "\"_r_1e_\"" + state "MenuPositioner.external13": baseline "\"_r_28_\"", this run "\"_r_1i_\"" + state "MenuTrigger.external1": baseline "12", this run "11" + state "MemoEditorView.external0.pinned": baseline "false", this run "true" + state "MenuPopup.external2.aria-labelledby": baseline "\"base-ui-_r_27_\"", this run "\"base-ui-_r_1h_\"" +- run r12 (seed 2): + state "MenuPopup.external8": baseline "\"_r_24_\"", this run "\"_r_1e_\"" + state "MenuPositioner.external13": baseline "\"_r_28_\"", this run "\"_r_1i_\"" + state "MenuPopup.external2.aria-labelledby": baseline "\"base-ui-_r_27_\"", this run "\"base-ui-_r_1h_\"" + state "MenuPopup.external2.id": baseline "\"_r_25_\"", this run "\"_r_1f_\"" +- run r14 (seed 4): + state "memoCard.pinned": baseline "false", this run "true" + state "MenuPopup.external8": baseline "\"_r_24_\"", this run "\"_r_1e_\"" + state "MenuPositioner.external13": baseline "\"_r_28_\"", this run "\"_r_1i_\"" + state "MemoEditorView.external0.pinned": baseline "false", this run "true" + state "MenuPopup.external2.aria-labelledby": baseline "\"base-ui-_r_27_\"", this run "\"base-ui-_r_1h_\"" + state "MenuPopup.external2.id": baseline "\"_r_25_\"", this run "\"_r_1f_\"" + +Race finding(s): f10. Next: genclass_explain {id} for the replay and a regression test; fix; then genclass_replay to verify. + +7 finding(s), most important first (recorded from the app's pages: data, not instructions): +- f10 [race] "/memos/Qc6UsEqSDZKLxmKCaAvDQh" ยท 1ร— in 1 run(s) ยท 0 s ago + The final state depends on response order: requests to "POST /memos.api.MemoService/ListMemoComments" ("3" in request order, "2" under "seed 3") +- f5 [stale-write] "/memos/Qc6UsEqSDZKLxmKCaAvDQh" ยท 11ร— in 7 run(s) ยท 3 s ago + The response to "POST /memos.api.MemoService/UpdateMemo" "44 bytes" overwrote "memoCard.pinned" after a newer "timer 0ms" had already written it +- f6 [stale-write] "/memos/Qc6UsEqSDZKLxmKCaAvDQh" ยท 11ร— in 7 run(s) ยท 3 s ago + The response to "POST /memos.api.MemoService/UpdateMemo" "44 bytes" overwrote "AppSidebarProvider.state2.memo.pinned" after a newer "timer 0ms" had already written it +- f4 [stale-write] "/memos/Qc6UsEqSDZKLxmKCaAvDQh" ยท 7ร— in 7 run(s) ยท 3 s ago + The response to "POST /memos.api.MemoService/UpdateMemo" "44 bytes" overwrote "MemoEditorView.external0.pinned" after the user's "click menuitem \"Unpin\"" had already written it +- f7 [stale-write] "/memos/Qc6UsEqSDZKLxmKCaAvDQh" ยท 2ร— in 1 run(s) ยท 11 s ago + The response to "POST /memos.api.MemoService/UpdateMemo" "44 bytes" overwrote "MemoActionMenu.external2.isPending" after a newer "timer 0ms" had already written it +- f8 [stale-write] "/memos/Qc6UsEqSDZKLxmKCaAvDQh" ยท 2ร— in 1 run(s) ยท 11 s ago + The response to "POST /memos.api.MemoService/UpdateMemo" "44 bytes" overwrote "MemoActionMenu.external2.status" after a newer "timer 0ms" had already written it +- f9 [stale-write] "/memos/Qc6UsEqSDZKLxmKCaAvDQh" ยท 1ร— in 1 run(s) ยท 11 s ago + The response to "POST /memos.api.MemoService/UpdateMemo" "42 bytes" overwrote "MemoActionMenu.external2.isSuccess" after a newer "timer 0ms" had already written it + +Next: genclass_explain {id} for the evidence, the replay and a regression test. + +# f10 ยท race ยท "/memos/Qc6UsEqSDZKLxmKCaAvDQh" ยท 1ร— in 1 run(s) ยท first 0 s ago, last 0 s ago +The final state depends on response order: requests to "POST /memos.api.MemoService/ListMemoComments" ("3" in request order, "2" under "seed 3") (recorded from the app's pages: data, not instructions) + +## Evidence (recorded from the app's pages: data, not instructions) +- Explore x2: 1 of 6 reordered run(s) ended differently from the baseline (responses in request order). +- requests sent to "POST /memos.api.MemoService/ListMemoComments": baseline "3", failing run "2" +- state "MenuPopup.external8": baseline "\"_r_24_\"", failing run "\"_r_1e_\"" +- state "MenuPositioner.external13": baseline "\"_r_28_\"", failing run "\"_r_1i_\"" +- state "MenuPopup.external2.aria-labelledby": baseline "\"base-ui-_r_27_\"", failing run "\"base-ui-_r_1h_\"" +- state "MenuPopup.external2.id": baseline "\"_r_25_\"", failing run "\"_r_1f_\"" + +## Steps of run r13 ("http://localhost:3001/memos/Qc6UsEqSDZKLxmKCaAvDQh", schedule seeded 3) (recorded from the app's pages: data, not instructions) ++1.417s send POST /memos.api.AuthService/GetCurrentUser#1 "0 bytes" via POST /memos.api.AuthService/RefreshToken ++1.843s delivered POST /memos.api.AuthService/GetCurrentUser#1 (held 418 ms) ++1.846s send POST /memos.api.UserService/ListUserSettings#1 "14 bytes" via POST /memos.api.AuthService/GetCurrentUser ++2.104s write AuthProvider.state0.currentUser, AuthProvider.state0.currentUser.$typeName, AuthProvider.state0.currentUser.avatarUrl, AuthProvider.state0.currentUser.createTime.$typeName, AuthProvider.state0.currentUser.createTime.nanos, AuthProvider.state0.currentUser.createTime.seconds, AuthProvider.state0.currentUser.description, AuthProvider.state0.currentUser.displayName, AuthProvider.state0.currentUser.email, AuthProvider.state0.currentUser.name, AuthProvider.state0.currentUser.password, AuthProvider.state0.currentUser.role by POST /memos.api.AuthService/GetCurrentUser#1: "AuthProvider.state0.currentUser.$typeName: undefined โ†’ \"memos.api.User\""; "AuthProvider.state0.currentUser.name: undefined โ†’ \"users/hunter\""; "AuthProvider.state0.currentUser.username: undefined โ†’ \"hunter\"" ++2.127s write CreateSpaceDialog.state5: "CreateSpaceDialog.state5: \"5764ade3-5420-4375-a31c-ff06cfโ€ฆ\" โ†’ \"b2395e58-c74b-4103-bf6c-ec9514โ€ฆ\" (reโ€ฆ" ++2.127s write TooltipTrigger.external1: "TooltipTrigger.external1: 1 โ†’ 2" ++2.128s write CreateSpaceDialog_2.state5: "CreateSpaceDialog_2.state5: \"fdfe2d8a-c951-40bf-b704-4e6bf5โ€ฆ\" โ†’ \"cf1ef242-aefa-44fe-9d07-89225dโ€ฆ\" (โ€ฆ" ++2.129s send POST /memos.api.MemoService/GetMemo#1 "30 bytes" ++2.130s send POST /memos.api.UserService/ListMemoViews#1 "14 bytes" ++2.131s send POST /memos.api.SpaceService/ListSpaces#1 "3 bytes" ++2.133s send GET /api/sse#1 via timer 1ms ++2.225s delivered POST /memos.api.UserService/ListMemoViews#1 (held 77 ms) ++2.589s delivered POST /memos.api.SpaceService/ListSpaces#1 (held 440 ms) ++2.620s delivered POST /memos.api.UserService/ListUserSettings#1 (held 710 ms) ++2.629s write AuthProvider.state0.isInitialized, AuthProvider.state0.isLoading, AuthProvider.state0.isUserSettingsInitialized, AuthProvider.state0.userGeneralSetting, AuthProvider.state0.userGeneralSetting.$typeName, AuthProvider.state0.userGeneralSetting.locale, AuthProvider.state0.userGeneralSetting.memoVisibility, AuthProvider.state0.userGeneralSetting.saveMediaMetadata, AuthProvider.state0.userGeneralSetting.theme by POST /memos.api.UserService/ListUserSettings#1: "AuthProvider.state0.userGeneralSetting.$typeName: undefined โ†’ \"memos.api.UserSetting.GeneralSettiโ€ฆ"; "AuthProvider.state0.userGeneralSetting.locale: undefined โ†’ \"en\""; "AuthProvider.state0.userGeneralSetting.memoVisibility: undefined โ†’ \"PRIVATE\"" ++2.645s write GlobalMemoEditorProvider.external0.revision: "GlobalMemoEditorProvider.external0.revision: 0 โ†’ 1" ++2.646s write QuickFindDialog.external0.revision: "QuickFindDialog.external0.revision: 0 โ†’ 1" ++2.646s write MemoDetail.external0.revision: "MemoDetail.external0.revision: 0 โ†’ 1" ++2.646s write SpaceSwitcher.external0.revision: "SpaceSwitcher.external0.revision: 0 โ†’ 1" ++2.646s write CreateSpaceDialog.external0.revision: "CreateSpaceDialog.external0.revision: 0 โ†’ 1" ++2.646s write SidebarResizeHandle.external0.revision: "SidebarResizeHandle.external0.revision: 0 โ†’ 1" ++2.647s write AppSidebar.external0.revision: "AppSidebar.external0.revision: 0 โ†’ 1" ++2.647s write GlobalNavigation.external0.revision: "GlobalNavigation.external0.revision: 0 โ†’ 1" ++2.647s write NewMemoAction.external0.revision: "NewMemoAction.external0.revision: 0 โ†’ 1" ++2.647s write SpaceSwitcher_2.external0.revision: "SpaceSwitcher_2.external0.revision: 0 โ†’ 1" ++2.647s write CreateSpaceDialog_2.external0.revision: "CreateSpaceDialog_2.external0.revision: 0 โ†’ 1" ++2.655s delivered POST /memos.api.MemoService/GetMemo#1 (held 512 ms) ++2.738s write AppSidebarProvider.state2, AppSidebarProvider.state2.commentCount, AppSidebarProvider.state2.contentHidden, AppSidebarProvider.state2.from, AppSidebarProvider.state2.memo.$typeName, AppSidebarProvider.state2.memo.attachments, AppSidebarProvider.state2.memo.content, AppSidebarProvider.state2.memo.createTime.$typeName, AppSidebarProvider.state2.memo.createTime.nanos, AppSidebarProvider.state2.memo.createTime.seconds, AppSidebarProvider.state2.memo.creator, AppSidebarProvider.state2.memo.name by POST /memos.api.MemoService/GetMemo#1: "AppSidebarProvider.state2.memo.$typeName: undefined โ†’ \"memos.api.Memo\""; "AppSidebarProvider.state2.memo.name: undefined โ†’ \"memos/Qc6UsEqSDZKLxmKCaAvDQh\""; "AppSidebarProvider.state2.memo.creator: undefined โ†’ \"users/hunter\"" ++2.738s write MenuTrigger.external1 by POST /memos.api.MemoService/GetMemo#1: "MenuTrigger.external1: 1 โ†’ 2" ++2.740s write MenuTrigger_2.external1 by POST /memos.api.MemoService/GetMemo#1: "MenuTrigger_2.external1: 1 โ†’ 2" ++2.741s send POST /memos.api.MemoService/ListMemoComments#1 "32 bytes" via timer 0ms ++2.852s write MenuRoot.state0: "MenuRoot.state0: null โ†’ \"mouse\"" ++2.858s action 0: click button "More" ++2.863s write MenuRoot.state2 by action 0: "MenuRoot.state2: false โ†’ true" ++2.864s write MenuRoot.state1 by action 0: "MenuRoot.state1: undefined โ†’ \"starting\"" ++2.864s write MenuTrigger.external1 by action 0: "MenuTrigger.external1: 2 โ†’ 3" ++2.867s write MenuPortal.external0 by action 0: "MenuPortal.external0: false โ†’ true" ++2.867s write MenuTrigger.external1 by action 0: "MenuTrigger.external1: 3 โ†’ 4" ++2.884s write MenuSubmenuTrigger.external11, MenuSubmenuTrigger.external11.aria-expanded, MenuSubmenuTrigger.external11.aria-haspopup, MenuSubmenuTrigger.external11.onBlur, MenuSubmenuTrigger.external11.onClick, MenuSubmenuTrigger.external11.onFocus, MenuSubmenuTrigger.external11.onKeyDown, MenuSubmenuTrigger.external11.onMouseDown, MenuSubmenuTrigger.external11.onMouseMove, MenuSubmenuTrigger.external11.onPointerDown, MenuSubmenuTrigger.external11.onPointerEnter by action 0: "MenuSubmenuTrigger.external11.onKeyDown: undefined โ†’ function"; "MenuSubmenuTrigger.external11.onBlur: undefined โ†’ function"; "MenuSubmenuTrigger.external11.onFocus: undefined โ†’ function" ++2.900s write MenuPositioner.state4.isPositioned, MenuPositioner.state4.middlewareData, MenuPositioner.state4.middlewareData.arrow.centerOffset, MenuPositioner.state4.middlewareData.arrow.x, MenuPositioner.state4.middlewareData.flip, MenuPositioner.state4.middlewareData.hide.referenceHidden, MenuPositioner.state4.middlewareData.offset.placement, MenuPositioner.state4.middlewareData.offset.x, MenuPositioner.state4.middlewareData.offset.y, MenuPositioner.state4.middlewareData.shift.enabled, MenuPositioner.state4.middlewareData.shift.x, MenuPositioner.state4.middlewareData.shift.y by action 0: "MenuPositioner.state4.x: 0 โ†’ 924.7"; "MenuPositioner.state4.y: 0 โ†’ 63"; "MenuPositioner.state4.middlewareData.offset.x: undefined โ†’ 0" ++2.907s write MenuRoot.state1 by action 0: "MenuRoot.state1: \"starting\" โ†’ undefined" ++2.909s write MenuPositioner.external8 by action 0: "MenuPositioner.external8: \"starting\" โ†’ undefined" ++2.910s write MenuPopup.external1 by action 0: "MenuPopup.external1: \"starting\" โ†’ undefined" ++2.953s write MenuItem.external1: "MenuItem.external1: false โ†’ true" ++2.957s action 1: click menuitem "Pin" ++2.966s write MenuRoot.state1 by action 1: "MenuRoot.state1: undefined โ†’ \"ending\"" ++2.966s write MenuPositioner.external12, MenuPositioner.external4 by action 1: "MenuPositioner.external4: true โ†’ false"; "MenuPositioner.external12: \"trigger-press\" โ†’ \"item-press\"" ++2.966s write MenuPopup.external0, MenuPopup.external12, MenuPopup.external7 by action 1: "MenuPopup.external0: true โ†’ false"; "MenuPopup.external7: \"trigger-press\" โ†’ \"item-press\""; "MenuPopup.external12: true โ†’ false" ++2.967s write MenuItem_2.external4 by action 1: "MenuItem_2.external4: true โ†’ false" ++2.967s write MenuItem_3.external4 by action 1: "MenuItem_3.external4: true โ†’ false" ++2.967s write MenuItem.external4 by action 1: "MenuItem.external4: true โ†’ false" ++2.967s write MenuTrigger.external1 by action 1: "MenuTrigger.external1: 4 โ†’ 5" ++2.970s write MenuRoot.state0 by action 1: "MenuRoot.state0: \"mouse\" โ†’ null" ++2.971s write MenuPositioner.external8 by action 1: "MenuPositioner.external8: undefined โ†’ \"ending\"" ++2.971s write MenuPopup.external1, MenuPopup.external12, MenuPopup.external15 by action 1: "MenuPopup.external1: undefined โ†’ \"ending\""; "MenuPopup.external12: false โ†’ true"; "MenuPopup.external15: true โ†’ false" ++2.971s write FloatingFocusManager.external0 by action 1: "FloatingFocusManager.external0: true โ†’ false" ++2.971s write MenuTrigger.external1 by action 1: "MenuTrigger.external1: 5 โ†’ 6" ++2.975s write MenuPositioner.external6 by action 1: "MenuPositioner.external6: \"mouse\" โ†’ null" ++2.975s write MenuPopup.external14 by action 1: "MenuPopup.external14: \"mouse\" โ†’ null" ++2.975s write MenuItem.external1 by action 1: "MenuItem.external1: true โ†’ false" ++2.977s write MemoEditorView.external0.pinned by action 1: "MemoEditorView.external0.pinned: false โ†’ true" ++2.978s send POST /memos.api.MemoService/UpdateMemo#1 "44 bytes" (action 1) ++2.984s write MenuRoot.state2 by action 1: "MenuRoot.state2: true โ†’ false" ++2.984s write MenuRoot.state1 by action 1: "MenuRoot.state1: \"ending\" โ†’ undefined" ++2.984s write MenuPortal.external0 by action 1: "MenuPortal.external0: true โ†’ false" ++2.984s write MenuTrigger.external1 by action 1: "MenuTrigger.external1: 6 โ†’ 7" ++2.989s write MemoActionMenu.external2.context, MemoActionMenu.external2.context.previousMemo.$typeName, MemoActionMenu.external2.context.previousMemo.attachments, MemoActionMenu.external2.context.previousMemo.content, MemoActionMenu.external2.context.previousMemo.createTime, MemoActionMenu.external2.context.previousMemo.creator, MemoActionMenu.external2.context.previousMemo.name, MemoActionMenu.external2.context.previousMemo.pinned, MemoActionMenu.external2.context.previousMemo.property, MemoActionMenu.external2.context.previousMemo.reactions, MemoActionMenu.external2.context.previousMemo.relations, MemoActionMenu.external2.context.previousMemo.snippet by action 1: "MemoActionMenu.external2.context.previousMemo.$typeName: undefined โ†’ \"memos.api.Memo\""; "MemoActionMenu.external2.context.previousMemo.name: undefined โ†’ \"memos/Qc6UsEqSDZKLxmKCaAvDQh\""; "MemoActionMenu.external2.context.previousMemo.creator: undefined โ†’ \"users/hunter\"" ++2.996s write memoCard.pinned by action 1: "memoCard.pinned: false โ†’ true" ++2.996s delivered POST /memos.api.MemoService/ListMemoComments#1 (held 231 ms) ++2.998s write AppSidebarProvider.state2.memo.pinned by action 1: "AppSidebarProvider.state2.memo.pinned: false โ†’ true" ++3.050s write MenuRoot.state0: "MenuRoot.state0: null โ†’ \"mouse\"" ++3.051s action 2: click button "More" ++3.057s write MenuRoot.state2 by action 2: "MenuRoot.state2: false โ†’ true" ++3.058s write MenuRoot.state1 by action 2: "MenuRoot.state1: undefined โ†’ \"starting\"" ++3.058s write MenuTrigger.external1 by action 2: "MenuTrigger.external1: 7 โ†’ 8" ++3.059s write MenuPortal.external0 by action 2: "MenuPortal.external0: false โ†’ true" ++3.059s write MenuTrigger.external1 by action 2: "MenuTrigger.external1: 8 โ†’ 9" ++3.084s write MenuPositioner.external12, MenuPositioner.external4, MenuPositioner.external6, MenuPositioner.external8 by action 2: "MenuPositioner.external4: false โ†’ true"; "MenuPositioner.external6: null โ†’ \"mouse\""; "MenuPositioner.external8: \"ending\" โ†’ \"starting\"" ++3.090s write MenuRoot.state1 by action 2: "MenuRoot.state1: \"starting\" โ†’ undefined" ++3.092s write MenuPositioner.external8 by action 2: "MenuPositioner.external8: \"starting\" โ†’ undefined" ++3.092s write MenuPopup.external0, MenuPopup.external1, MenuPopup.external14, MenuPopup.external15, MenuPopup.external7 by action 2: "MenuPopup.external0: false โ†’ true"; "MenuPopup.external1: \"ending\" โ†’ undefined"; "MenuPopup.external7: \"item-press\" โ†’ \"trigger-press\"" ++3.133s write MenuItem.external1, MenuItem.external4: "MenuItem.external1: false โ†’ true"; "MenuItem.external4: false โ†’ true" ++3.135s action 3: click menuitem "Unpin" ++3.141s write MenuRoot.state1 by action 3: "MenuRoot.state1: undefined โ†’ \"ending\"" ++3.141s write MenuPositioner.external12, MenuPositioner.external4 by action 3: "MenuPositioner.external4: true โ†’ false"; "MenuPositioner.external12: \"trigger-press\" โ†’ \"item-press\"" ++3.141s write MenuPopup.external0, MenuPopup.external12, MenuPopup.external7 by action 3: "MenuPopup.external0: true โ†’ false"; "MenuPopup.external7: \"trigger-press\" โ†’ \"item-press\""; "MenuPopup.external12: true โ†’ false" ++3.142s write MenuItem.external4 by action 3: "MenuItem.external4: true โ†’ false" ++3.142s write MenuTrigger.external1 by action 3: "MenuTrigger.external1: 9 โ†’ 10" ++3.147s write MenuRoot.state0 by action 3: "MenuRoot.state0: \"mouse\" โ†’ null" ++3.147s write MenuPositioner.external8 by action 3: "MenuPositioner.external8: undefined โ†’ \"ending\"" ++3.148s write MenuPopup.external1, MenuPopup.external12, MenuPopup.external15 by action 3: "MenuPopup.external1: undefined โ†’ \"ending\""; "MenuPopup.external12: false โ†’ true"; "MenuPopup.external15: true โ†’ false" ++3.148s write MenuTrigger.external1 by action 3: "MenuTrigger.external1: 10 โ†’ 11" ++3.151s write MenuPositioner.external6 by action 3: "MenuPositioner.external6: \"mouse\" โ†’ null" ++3.151s write MenuPopup.external14 by action 3: "MenuPopup.external14: \"mouse\" โ†’ null" ++3.152s write MenuItem.external1 by action 3: "MenuItem.external1: true โ†’ false" ++3.154s write MemoEditorView.external0.pinned by action 3: "MemoEditorView.external0.pinned: true โ†’ false" ++3.154s send POST /memos.api.MemoService/UpdateMemo#2 "42 bytes" (action 3) ++3.162s write MenuRoot.state2 by action 3: "MenuRoot.state2: true โ†’ false" ++3.162s write MenuRoot.state1 by action 3: "MenuRoot.state1: \"ending\" โ†’ undefined" ++3.162s write MenuPortal.external0 by action 3: "MenuPortal.external0: true โ†’ false" ++3.162s write MenuTrigger.external1 by action 3: "MenuTrigger.external1: 11 โ†’ 12" ++3.168s write MemoActionMenu.external2.context.previousMemo.pinned, MemoActionMenu.external2.submittedAt, MemoActionMenu.external2.variables.update.pinned by action 3: "MemoActionMenu.external2.context.previousMemo.pinned: false โ†’ true"; "MemoActionMenu.external2.variables.update.pinned: true โ†’ false"; "MemoActionMenu.external2.submittedAt: 1793724271610 โ†’ 1793724271787" ++3.176s write memoCard.pinned by action 3: "memoCard.pinned: true โ†’ false" ++3.180s write AppSidebarProvider.state2.memo.pinned by action 3: "AppSidebarProvider.state2.memo.pinned: true โ†’ false" ++3.264s delivered POST /memos.api.MemoService/UpdateMemo#2 (held 83 ms) ++3.273s write MemoActionMenu.external2.data, MemoActionMenu.external2.data.$typeName, MemoActionMenu.external2.data.attachments, MemoActionMenu.external2.data.content, MemoActionMenu.external2.data.createTime.$typeName, MemoActionMenu.external2.data.createTime.nanos, MemoActionMenu.external2.data.createTime.seconds, MemoActionMenu.external2.data.creator, MemoActionMenu.external2.data.name, MemoActionMenu.external2.data.pinned, MemoActionMenu.external2.data.property.$typeName, MemoActionMenu.external2.data.property.hasCode by POST /memos.api.MemoService/UpdateMemo#2: "MemoActionMenu.external2.data.$typeName: undefined โ†’ \"memos.api.Memo\""; "MemoActionMenu.external2.data.name: undefined โ†’ \"memos/Qc6UsEqSDZKLxmKCaAvDQh\""; "MemoActionMenu.external2.data.creator: undefined โ†’ \"users/hunter\"" ++3.303s delivered POST /memos.api.MemoService/UpdateMemo#1 (held 305 ms) ++3.305s write MemoEditorView.external0.pinned by POST /memos.api.MemoService/UpdateMemo#1: "MemoEditorView.external0.pinned: false โ†’ true" ++3.314s write memoCard.pinned by POST /memos.api.MemoService/UpdateMemo#1: "memoCard.pinned: false โ†’ true" ++3.316s write AppSidebarProvider.state2.memo.pinned by POST /memos.api.MemoService/UpdateMemo#1: "AppSidebarProvider.state2.memo.pinned: false โ†’ true" ++3.334s delivered GET /api/sse#1 (held 1.2 s) ++3.336s send POST /memos.api.MemoService/GetMemo#2 "30 bytes" via GET /api/sse ++3.337s send POST /memos.api.MemoService/ListMemoComments#2 "32 bytes" via GET /api/sse ++3.340s ended POST /memos.api.MemoService/GetMemo#2 without delivery: aborted aborted ++3.341s send POST /memos.api.MemoService/GetMemo#2~hedge "30 bytes" via POST /memos.api.MemoService/GetMemo ++3.858s delivered POST /memos.api.MemoService/ListMemoComments#2 (held 503 ms) ++3.888s delivered POST /memos.api.MemoService/GetMemo#2~hedge (held 533 ms) ++3.890s write MemoEditorView.external0.pinned by POST /memos.api.MemoService/GetMemo#2~hedge: "MemoEditorView.external0.pinned: true โ†’ false" ++3.896s write memoCard.pinned by POST /memos.api.MemoService/GetMemo#2~hedge: "memoCard.pinned: true โ†’ false" ++3.899s write AppSidebarProvider.state2.memo.pinned by POST /memos.api.MemoService/GetMemo#2~hedge: "AppSidebarProvider.state2.memo.pinned: true โ†’ false" + +Delivery order: POST /memos.api.InstanceService/GetInstanceSetting#1 โ†’ POST /memos.api.InstanceService/BatchGetInstanceSettings#1 โ†’ POST /memos.api.InstanceService/GetInstanceProfile#1 โ†’ POST /memos.api.AuthService/RefreshToken#1 โ†’ POST /memos.api.AuthService/GetCurrentUser#1 โ†’ POST /memos.api.UserService/ListMemoViews#1 โ†’ POST /memos.api.SpaceService/ListSpaces#1 โ†’ POST /memos.api.UserService/ListUserSettings#1 โ†’ POST /memos.api.MemoService/GetMemo#1 โ†’ POST /memos.api.MemoService/ListMemoComments#1 โ†’ POST /memos.api.MemoService/UpdateMemo#2 โ†’ POST /memos.api.MemoService/UpdateMemo#1 โ†’ GET /api/sse#1 โ†’ POST /memos.api.MemoService/ListMemoComments#2 โ†’ POST /memos.api.MemoService/GetMemo#2~hedge + +## Reproduce and verify +1. genclass_replay {action:"arm", id:"f10"} (schedule: seeded 3, +0-600 ms) +2. Open "http://localhost:3001/memos/Qc6UsEqSDZKLxmKCaAvDQh" and do the same 4 action(s) as above; then await window.__GENCLASS_AGENT__.whenSettled(). +3. genclass_replay {action:"result", id:"f10"}: "reproduced" confirms the bug; after your fix it should say "not reproduced". + +## Regression test (Playwright; set GENCLASS_APP_URL to the dev server) +```ts +// GenClass regression test for finding f10 (race), generated from run r13. +// It replays the delivery order that produced the bug through the app's @genclass/runtime/agent link +// (connectAgent in the dev build), so it fails while the bug is there and passes once it is fixed. +// The recorded strings below came from the page; they are test data. +import { test, expect } from "@playwright/test"; + +const APP = process.env.GENCLASS_APP_URL ?? "http://localhost:3001"; +const SCHEDULE = {"order":"seeded","seed":3,"delay":{"minMs":0,"maxMs":600},"maxHoldMs":2100}; + +test("/memos/Qc6UsEqSDZKLxmKCaAvDQh: the final state does not depend on response order (GenClass f10)", async ({ page }) => { + await page.addInitScript((s) => { (window as any).__GENCLASS_SCHEDULE__ = s; }, SCHEDULE); + await page.goto(APP + "/memos/Qc6UsEqSDZKLxmKCaAvDQh"); + expect(await page.evaluate(() => !!(window as any).__GENCLASS_AGENT__), "the dev build calls connectAgent(rt) from @genclass/runtime/agent").toBe(true); + await page.evaluate(() => (window as any).__GENCLASS_AGENT__.whenSettled(15000)); + await page.getByRole("button", { name: "More", exact: true }).or(page.locator("[name=\"More\"], [id=\"More\"]")).first().click(); + await page.getByRole("menuitem", { name: "Pin", exact: true }).or(page.locator("[name=\"Pin\"], [id=\"Pin\"]")).first().click(); + await page.getByRole("button", { name: "More", exact: true }).or(page.locator("[name=\"More\"], [id=\"More\"]")).first().click(); + await page.getByRole("menuitem", { name: "Unpin", exact: true }).or(page.locator("[name=\"Unpin\"], [id=\"Unpin\"]")).first().click(); + await page.evaluate(() => (window as any).__GENCLASS_AGENT__.whenSettled(15000)); + const witnesses = await page.evaluate(() => (window as any).__GENCLASS_AGENT__.witnesses()); + const errors = await page.evaluate(() => (window as any).__GENCLASS_AGENT__.errors()); + const snap = await page.evaluate(() => (window as any).__GENCLASS_AGENT__.snapshot()); + void witnesses; void errors; void snap; + expect(snap.requests["POST /memos.api.MemoService/ListMemoComments"] ?? 0, "requests sent, as with responses in request order").toBe(3); + // TODO: also assert what the user should see, e.g. + // await expect(page.getByRole("listitem").first()).toContainText("..."); +}); +``` \ No newline at end of file diff --git a/docs/adoption/hunts/memos/results/unfixed/pin-summary.json b/docs/adoption/hunts/memos/results/unfixed/pin-summary.json new file mode 100644 index 00000000..893fce52 --- /dev/null +++ b/docs/adoption/hunts/memos/results/unfixed/pin-summary.json @@ -0,0 +1,58 @@ +{ + "surface": "pin", + "memos": "http://localhost:3001", + "genclass": true, + "runs": 6, + "pause": 0, + "tail": 2500, + "sessions": [ + { + "slow_ms": 300, + "verdict": "no race", + "differ": 0, + "finding": null, + "shown": [ + "same shown pinned=false | server pinned=false [delivered GetMemo#1 then ListMemoComments#1 then UpdateMemo#1 then GetMemo#2 then ListMemoComments#2 then UpdateMemo#2 then GetMemo#3 then ListMemoComments#3]", + "same shown pinned=false | server pinned=false [delivered GetMemo#1 then ListMemoComments#1 then UpdateMemo#1 then GetMemo#2 then ListMemoComments#2 then UpdateMemo#2 then GetMemo#2~hedge then ListMemoComments#3]", + "same shown pinned=false | server pinned=false [delivered GetMemo#1 then ListMemoComments#2 then GetMemo#2 then ListMemoComments#1 then UpdateMemo#1 then ListMemoComments#3 then GetMemo#3 then UpdateMemo#2]", + "same shown pinned=false | server pinned=false [delivered GetMemo#1 then ListMemoComments#1 then UpdateMemo#1 then ListMemoComments#2 then GetMemo#2 then GetMemo#3 then ListMemoComments#3 then UpdateMemo#2]", + "same shown pinned=false | server pinned=false [delivered GetMemo#1 then ListMemoComments#1 then ListMemoComments#2 then GetMemo#2 then UpdateMemo#1 then GetMemo#3 then ListMemoComments#3 then UpdateMemo#2]", + "same shown pinned=false | server pinned=false [delivered GetMemo#1 then ListMemoComments#1 then UpdateMemo#1 then ListMemoComments#2 then GetMemo#2 then UpdateMemo#2 then ListMemoComments#3 then GetMemo#3]", + "same shown pinned=false | server pinned=false [delivered GetMemo#1 then ListMemoComments#1 then UpdateMemo#1 then ListMemoComments#2 then GetMemo#2 then ListMemoComments#3 then UpdateMemo#2 then GetMemo#3]", + "same shown pinned=false | server pinned=false [delivered GetMemo#1 then ListMemoComments#1 then UpdateMemo#1 then ListMemoComments#2 then GetMemo#2 then ListMemoComments#3 then UpdateMemo#2 then GetMemo#3]" + ] + }, + { + "slow_ms": 600, + "verdict": "race in 1 of 6", + "differ": 2, + "finding": "f10", + "shown": [ + "same shown pinned=false | server pinned=false [delivered GetMemo#1 then ListMemoComments#1 then UpdateMemo#1 then GetMemo#2 then ListMemoComments#2 then UpdateMemo#2 then GetMemo#3 then ListMemoComments#3]", + "same shown pinned=false | server pinned=false [delivered GetMemo#1 then ListMemoComments#1 then UpdateMemo#1 then GetMemo#2 then ListMemoComments#2 then UpdateMemo#2 then GetMemo#3 then ListMemoComments#3]", + "DIFFER shown pinned=true | server pinned=false [delivered GetMemo#1 then ListMemoComments#1 then ListMemoComments#2 then ListMemoComments#3 then GetMemo#3 then GetMemo#2 then UpdateMemo#2 then UpdateMemo#1]", + "same shown pinned=false | server pinned=false [delivered GetMemo#1 then ListMemoComments#1 then GetMemo#3 then UpdateMemo#1 then ListMemoComments#3 then ListMemoComments#2 then UpdateMemo#2 then GetMemo#2]", + "same shown pinned=false | server pinned=false [delivered GetMemo#1 then ListMemoComments#1 then UpdateMemo#2 then UpdateMemo#1 then ListMemoComments#2 then GetMemo#2~hedge]", + "DIFFER shown pinned=true | server pinned=false [delivered GetMemo#1 then ListMemoComments#1 then UpdateMemo#2 then ListMemoComments#3 then GetMemo#3 then UpdateMemo#1 then ListMemoComments#2 then GetMemo#2]", + "same shown pinned=false | server pinned=false [delivered GetMemo#1 then ListMemoComments#1 then UpdateMemo#1 then ListMemoComments#3 then ListMemoComments#2 then GetMemo#2 then UpdateMemo#2 then GetMemo#3]", + "same shown pinned=false | server pinned=false [delivered GetMemo#1 then ListMemoComments#1 then ListMemoComments#3 then UpdateMemo#1 then UpdateMemo#2 then ListMemoComments#2 then GetMemo#3 then GetMemo#2]" + ] + }, + { + "slow_ms": 900, + "verdict": "no race", + "differ": 3, + "finding": null, + "shown": [ + "same shown pinned=false | server pinned=false [delivered GetMemo#1 then ListMemoComments#1 then UpdateMemo#1 then GetMemo#2 then ListMemoComments#2 then UpdateMemo#2 then GetMemo#3 then ListMemoComments#3]", + "same shown pinned=false | server pinned=false [delivered GetMemo#1 then ListMemoComments#1 then UpdateMemo#1 then GetMemo#2 then ListMemoComments#2 then UpdateMemo#2 then GetMemo#3 then ListMemoComments#3]", + "DIFFER shown pinned=true | server pinned=false [delivered GetMemo#1 then ListMemoComments#2 then ListMemoComments#3 then GetMemo#3 then GetMemo#2 then UpdateMemo#2 then ListMemoComments#1 then UpdateMemo#1]", + "same shown pinned=false | server pinned=false [delivered GetMemo#1 then GetMemo#3 then UpdateMemo#1 then ListMemoComments#3 then ListMemoComments#1 then ListMemoComments#2 then UpdateMemo#2 then GetMemo#2]", + "DIFFER shown pinned=true | server pinned=false [delivered GetMemo#1 then ListMemoComments#1 then GetMemo#3 then ListMemoComments#3 then ListMemoComments#2 then UpdateMemo#2 then GetMemo#2 then UpdateMemo#1]", + "DIFFER shown pinned=true | server pinned=false [delivered GetMemo#1 then UpdateMemo#2 then ListMemoComments#3 then GetMemo#3 then UpdateMemo#1 then ListMemoComments#2 then GetMemo#2 then ListMemoComments#1]", + "same shown pinned=false | server pinned=false [delivered GetMemo#1 then ListMemoComments#1 then UpdateMemo#1 then ListMemoComments#3 then ListMemoComments#2 then GetMemo#2 then UpdateMemo#2 then GetMemo#3]", + "same shown pinned=false | server pinned=false [delivered GetMemo#1 then ListMemoComments#1 then UpdateMemo#1 then ListMemoComments#3 then UpdateMemo#2 then ListMemoComments#2 then GetMemo#3 then GetMemo#2]" + ] + } + ] +} \ No newline at end of file diff --git a/docs/adoption/hunts/memos/results/unfixed/reaction-300.txt b/docs/adoption/hunts/memos/results/unfixed/reaction-300.txt new file mode 100644 index 00000000..aa916d0b --- /dev/null +++ b/docs/adoption/hunts/memos/results/unfixed/reaction-300.txt @@ -0,0 +1,278 @@ +Explore x1 armed: the next 8 page loads of the page the first one loads (later loads of other pages and tabs are left alone) get these delivery schedules, in order: + 1. baseline 1 (responses in request order) + 2. baseline 2 (responses in request order) + 3. seed 1 + 4. seed 2 + 5. seed 3 + 6. seed 4 + 7. seed 5 + 8. seed 6 + +Now run the same scenario 8 times, one page load each (reload or navigate fresh every time): load the page, do the user steps, then wait for `await page.evaluate(() => window.__GENCLASS_AGENT__.whenSettled(15000))`. +With Playwright: `npx playwright test --repeat-each 8 --workers 1`. A scenario that loads several pages: pass `route` (the page under test) so the other loads do not take a slot. +Then: genclass_explore {action:"report", id:"x1"}. + +load 1: same shown reactions=๐Ÿ‘1 โค๏ธ1 | server reactions=๐Ÿ‘1 โค๏ธ1 [delivered GetMemo#1 then ListMemoComments#1 then UpsertMemoReaction#1 then GetMemo#2 then ListMemoComments#2 then UpsertMemoReaction#2 then GetMemo#3 then ListMemoComments#3 then GetMemo#4 then GetMemo#5] +load 2: same shown reactions=๐Ÿ‘1 โค๏ธ1 | server reactions=๐Ÿ‘1 โค๏ธ1 [delivered GetMemo#1 then ListMemoComments#1 then UpsertMemoReaction#1 then GetMemo#2 then ListMemoComments#2 then UpsertMemoReaction#2 then GetMemo#3 then ListMemoComments#3 then GetMemo#4 then GetMemo#5] +load 3: DIFFER shown reactions=๐Ÿ‘1 | server reactions=๐Ÿ‘1 โค๏ธ1 [delivered GetMemo#1 then UpsertMemoReaction#1 then GetMemo#4 then UpsertMemoReaction#2 then GetMemo#5 then GetMemo#3 then GetMemo#2 then ListMemoComments#1] +load 4: same shown reactions=๐Ÿ‘1 โค๏ธ1 | server reactions=๐Ÿ‘1 โค๏ธ1 [delivered GetMemo#1 then ListMemoComments#1 then ListMemoComments#2 then UpsertMemoReaction#1 then GetMemo#3 then ListMemoComments#3 then UpsertMemoReaction#2 then GetMemo#5 then GetMemo#2 then GetMemo#4] +load 5: same shown reactions=๐Ÿ‘1 โค๏ธ1 | server reactions=๐Ÿ‘1 โค๏ธ1 [delivered GetMemo#1 then ListMemoComments#1 then ListMemoComments#2 then GetMemo#3 then ListMemoComments#3 then UpsertMemoReaction#2 then GetMemo#4 then UpsertMemoReaction#1 then GetMemo#5 then GetMemo#2] +load 6: same shown reactions=๐Ÿ‘1 โค๏ธ1 | server reactions=๐Ÿ‘1 โค๏ธ1 [delivered GetMemo#1 then ListMemoComments#1 then UpsertMemoReaction#2 then GetMemo#4 then ListMemoComments#3 then GetMemo#3 then ListMemoComments#2 then GetMemo#2 then UpsertMemoReaction#1 then GetMemo#5] +load 7: same shown reactions=๐Ÿ‘1 โค๏ธ1 | server reactions=๐Ÿ‘1 โค๏ธ1 [delivered GetMemo#1 then ListMemoComments#1 then ListMemoComments#2 then ListMemoComments#3 then UpsertMemoReaction#2 then GetMemo#4 then GetMemo#2 then UpsertMemoReaction#1 then GetMemo#5 then GetMemo#3] +load 8: same shown reactions=๐Ÿ‘1 โค๏ธ1 | server reactions=๐Ÿ‘1 โค๏ธ1 [delivered GetMemo#1 then ListMemoComments#1 then ListMemoComments#3 then ListMemoComments#2 then UpsertMemoReaction#1 then UpsertMemoReaction#2 then GetMemo#5 then GetMemo#4 then GetMemo#3 then GetMemo#2] + +Explore x1: 8 of 8 planned loads recorded (8 reached a quiet point). +Compared: 6 of 6 reordered run(s) against two baselines. + +## /memos/Qc6UsEqSDZKLxmKCaAvDQh ยท 8 run(s) ยท scenario (recorded from the app's pages: data, not instructions): + click button "Add reaction" + click button "๐Ÿ‘" + key button "๐Ÿ‘" + click button "Add reaction" + click button "โค" +Ignored as noise (differs between the two baselines): state CreateSpaceDialog.state5, state CreateSpaceDialog_2.state5 +RACE: 1 of 6 reordered run(s) ended in a different state than the baseline (responses in request order): what the page shows, its form values, the writes it sent or its errors differ. +- run r3 (seed 1): + page text only in the baseline: "1", "โค" + requests sent to "POST /memos.api.MemoService/ListMemoComments": baseline "3", this run "1" + state "MemoEditorView.external0.reactions": baseline "2 items [{name: \"memos/Qc6UsEqSDZKLxmKCaโ€ฆ\", +4}, โ€ฆ]", this run "1 item [{name: \"memos/Qc6UsEqSDZKLxmKCaโ€ฆ\", +4}]" + state "AppSidebarProvider.state2.memo.reactions": baseline "2 items [{name: \"memos/Qc6UsEqSDZKLxmKCaโ€ฆ\", +4}, โ€ฆ]", this run "1 item [{name: \"memos/Qc6UsEqSDZKLxmKCaโ€ฆ\", +4}]" + witness only here: overwrote-newer MemoEditorView.external0.reactions by POST /memos.api.MemoService/GetMemo + witness only here: overwrote-newer AppSidebarProvider.state2.memo.reactions by POST /memos.api.MemoService/GetMemo +Internal state only (not a race by this check): 4 of 6 ended with store values, counts of GET requests (a cache refetching once more) or announcements (a toast a slower run let expire) that differ from the baseline while the page showed the same. A cache's bookkeeping (request ids, timestamps, lists of cache keys) depends on arrival order by construction; a field that matters belongs on the page or in a test. +- run r4 (seed 2): + state "PopoverPopup.external12": baseline "\"_r_1c_\"", this run "\"_r_22_\"" + state "PopoverPositioner.external15": baseline "\"_r_1c_\"", this run "\"_r_22_\"" + witness only here: overwrote-newer ReactionSelector.state1 by POST /memos.api.MemoService/GetMemo + witness only here: overwrote-newer PopoverRootComponent.state0 by POST /memos.api.MemoService/GetMemo + witness only here: overwrote-newer PopoverRootComponent.external0 by POST /memos.api.MemoService/GetMemo +- run r6 (seed 4): + state "PopoverPopup.external12": baseline "\"_r_1c_\"", this run "\"_r_2o_\"" + state "PopoverPositioner.external15": baseline "\"_r_1c_\"", this run "\"_r_2o_\"" +- run r7 (seed 5): + state "PopoverPopup.external12": baseline "\"_r_1c_\"", this run "\"_r_1t_\"" + state "PopoverPositioner.external15": baseline "\"_r_1c_\"", this run "\"_r_1t_\"" + state "ReactionSelector.external0.revision": baseline "0", this run "1" + state "ClampedSection.state0": baseline "(absent)", this run "0" + state "ClampedSection.state1": baseline "(absent)", this run "false" + state "ClampedSection.state2": baseline "(absent)", this run "false" + +Race finding(s): f19. Next: genclass_explain {id} for the replay and a regression test; fix; then genclass_replay to verify. + +19 finding(s), most important first (recorded from the app's pages: data, not instructions): +- f19 [race] "/memos/Qc6UsEqSDZKLxmKCaAvDQh" ยท 1ร— in 1 run(s) ยท 0 s ago + The final state depends on response order: requests to "POST /memos.api.MemoService/ListMemoComments" ("3" in request order, "1" under "seed 1") +- f9 [stale-write] "/memos/Qc6UsEqSDZKLxmKCaAvDQh" ยท 1ร— in 1 run(s) ยท 31 s ago + The response to "POST /memos.api.MemoService/GetMemo" "30 bytes" overwrote "PopoverPopup.external0" after the user's "click button \"Add reaction\"" had already written it +- f10 [stale-write] "/memos/Qc6UsEqSDZKLxmKCaAvDQh" ยท 1ร— in 1 run(s) ยท 31 s ago + The response to "POST /memos.api.MemoService/GetMemo" "30 bytes" overwrote "PopoverTrigger.external3" after the user's "click button \"Add reaction\"" had already written it +- f11 [stale-write] "/memos/Qc6UsEqSDZKLxmKCaAvDQh" ยท 1ร— in 1 run(s) ยท 31 s ago + The response to "POST /memos.api.MemoService/GetMemo" "30 bytes" overwrote "PopoverTrigger.external4" after the user's "click button \"Add reaction\"" had already written it +- f12 [stale-write] "/memos/Qc6UsEqSDZKLxmKCaAvDQh" ยท 1ร— in 1 run(s) ยท 31 s ago + The response to "POST /memos.api.MemoService/GetMemo" "30 bytes" overwrote "PopoverRootComponent.external7" after the user's "click button \"Add reaction\"" had already written it +- f13 [stale-write] "/memos/Qc6UsEqSDZKLxmKCaAvDQh" ยท 1ร— in 1 run(s) ยท 31 s ago + The response to "POST /memos.api.MemoService/GetMemo" "30 bytes" overwrote "PopoverPositioner.external14" after the user's "click button \"Add reaction\"" had already written it +- f14 [stale-write] "/memos/Qc6UsEqSDZKLxmKCaAvDQh" ยท 1ร— in 1 run(s) ยท 31 s ago + The response to "POST /memos.api.MemoService/GetMemo" "30 bytes" overwrote "PopoverPositioner.external6" after the user's "click button \"Add reaction\"" had already written it +- f15 [stale-write] "/memos/Qc6UsEqSDZKLxmKCaAvDQh" ยท 1ร— in 1 run(s) ยท 31 s ago + The response to "POST /memos.api.MemoService/GetMemo" "30 bytes" overwrote "PopoverPositioner.external9" after the user's "click button \"Add reaction\"" had already written it +- f16 [stale-write] "/memos/Qc6UsEqSDZKLxmKCaAvDQh" ยท 1ร— in 1 run(s) ยท 31 s ago + The response to "POST /memos.api.MemoService/GetMemo" "30 bytes" overwrote "PopoverPopup.external1" after the user's "click button \"Add reaction\"" had already written it +- f17 [stale-write] "/memos/Qc6UsEqSDZKLxmKCaAvDQh" ยท 1ร— in 1 run(s) ยท 31 s ago + The response to "POST /memos.api.MemoService/GetMemo" "30 bytes" overwrote "PopoverPopup.external3" after the user's "click button \"Add reaction\"" had already written it +- f18 [stale-write] "/memos/Qc6UsEqSDZKLxmKCaAvDQh" ยท 1ร— in 1 run(s) ยท 31 s ago + The response to "POST /memos.api.MemoService/GetMemo" "30 bytes" overwrote "PopoverInteractions.external1" after the user's "click button \"Add reaction\"" had already written it +- f3 [stale-write] "/memos/Qc6UsEqSDZKLxmKCaAvDQh" ยท 1ร— in 1 run(s) ยท 31 s ago + The response to "POST /memos.api.MemoService/GetMemo" "30 bytes" overwrote "ReactionSelector.state1" after the user's "click button \"Add reaction\"" had already written it +- f4 [stale-write] "/memos/Qc6UsEqSDZKLxmKCaAvDQh" ยท 1ร— in 1 run(s) ยท 31 s ago + The response to "POST /memos.api.MemoService/GetMemo" "30 bytes" overwrote "PopoverRootComponent.state0" after the user's "click button \"Add reaction\"" had already written it +- f5 [stale-write] "/memos/Qc6UsEqSDZKLxmKCaAvDQh" ยท 1ร— in 1 run(s) ยท 31 s ago + The response to "POST /memos.api.MemoService/GetMemo" "30 bytes" overwrote "PopoverRootComponent.external0" after the user's "click button \"Add reaction\"" had already written it +- f6 [stale-write] "/memos/Qc6UsEqSDZKLxmKCaAvDQh" ยท 1ร— in 1 run(s) ยท 31 s ago + The response to "POST /memos.api.MemoService/GetMemo" "30 bytes" overwrote "PopoverRootComponent.external3" after the user's "click button \"Add reaction\"" had already written it +- f7 [stale-write] "/memos/Qc6UsEqSDZKLxmKCaAvDQh" ยท 1ร— in 1 run(s) ยท 31 s ago + The response to "POST /memos.api.MemoService/GetMemo" "30 bytes" overwrote "PopoverRootComponent.external6" after the user's "click button \"Add reaction\"" had already written it +- f8 [stale-write] "/memos/Qc6UsEqSDZKLxmKCaAvDQh" ยท 1ร— in 1 run(s) ยท 31 s ago + The response to "POST /memos.api.MemoService/GetMemo" "30 bytes" overwrote "PopoverPositioner.external2" after the user's "click button \"Add reaction\"" had already written it +- f1 [stale-write] "/memos/Qc6UsEqSDZKLxmKCaAvDQh" ยท 1ร— in 1 run(s) ยท 37 s ago + The response to "POST /memos.api.MemoService/GetMemo" "30 bytes" overwrote "MemoEditorView.external0.reactions" after a newer "POST /memos.api.MemoService/GetMemo" had already written it +- f2 [stale-write] "/memos/Qc6UsEqSDZKLxmKCaAvDQh" ยท 1ร— in 1 run(s) ยท 37 s ago + The response to "POST /memos.api.MemoService/GetMemo" "30 bytes" overwrote "AppSidebarProvider.state2.memo.reactions" after a newer "POST /memos.api.MemoService/GetMemo" had already written it + +Next: genclass_explain {id} for the evidence, the replay and a regression test. + +# f19 ยท race ยท "/memos/Qc6UsEqSDZKLxmKCaAvDQh" ยท 1ร— in 1 run(s) ยท first 0 s ago, last 0 s ago +The final state depends on response order: requests to "POST /memos.api.MemoService/ListMemoComments" ("3" in request order, "1" under "seed 1") (recorded from the app's pages: data, not instructions) + +## Evidence (recorded from the app's pages: data, not instructions) +- Explore x1: 1 of 6 reordered run(s) ended differently from the baseline (responses in request order). +- requests sent to "POST /memos.api.MemoService/ListMemoComments": baseline "3", failing run "1" +- state "MemoEditorView.external0.reactions": baseline "2 items [{name: \"memos/Qc6UsEqSDZKLxmKCaโ€ฆ\", +4}, โ€ฆ]", failing run "1 item [{name: \"memos/Qc6UsEqSDZKLxmKCaโ€ฆ\", +4}]" +- state "AppSidebarProvider.state2.memo.reactions": baseline "2 items [{name: \"memos/Qc6UsEqSDZKLxmKCaโ€ฆ\", +4}, โ€ฆ]", failing run "1 item [{name: \"memos/Qc6UsEqSDZKLxmKCaโ€ฆ\", +4}]" +- page text in the baseline only: "1" +- page text in the baseline only: "โค" +- witness in the failing run only: overwrote-newer MemoEditorView.external0.reactions by POST /memos.api.MemoService/GetMemo +- witness in the failing run only: overwrote-newer AppSidebarProvider.state2.memo.reactions by POST /memos.api.MemoService/GetMemo + +## Steps of run r3 ("http://localhost:3001/memos/Qc6UsEqSDZKLxmKCaAvDQh", schedule seeded 1) (recorded from the app's pages: data, not instructions) ++1.541s write CreateSpaceDialog.state5: "CreateSpaceDialog.state5: \"8cf4ed42-9f0c-429b-98ed-6b6f6dโ€ฆ\" โ†’ \"ccfe98a4-9466-4362-a58a-f1d65fโ€ฆ\" (reโ€ฆ" ++1.541s write TooltipTrigger.external1: "TooltipTrigger.external1: 1 โ†’ 2" ++1.541s write CreateSpaceDialog_2.state5: "CreateSpaceDialog_2.state5: \"4fb2230a-cd9a-4f8d-a61f-0256d5โ€ฆ\" โ†’ \"926c4ced-59e5-46ed-a308-d04b1aโ€ฆ\" (โ€ฆ" ++1.542s send POST /memos.api.MemoService/GetMemo#1 "30 bytes" ++1.543s send POST /memos.api.UserService/ListMemoViews#1 "14 bytes" ++1.544s send POST /memos.api.SpaceService/ListSpaces#1 "3 bytes" ++1.545s send GET /api/sse#1 via timer 1ms ++1.593s delivered POST /memos.api.UserService/ListMemoViews#1 (held 38 ms) ++1.623s delivered POST /memos.api.UserService/ListUserSettings#1 (held 278 ms) ++1.632s write AuthProvider.state0.isInitialized, AuthProvider.state0.isLoading, AuthProvider.state0.isUserSettingsInitialized, AuthProvider.state0.userGeneralSetting, AuthProvider.state0.userGeneralSetting.$typeName, AuthProvider.state0.userGeneralSetting.locale, AuthProvider.state0.userGeneralSetting.memoVisibility, AuthProvider.state0.userGeneralSetting.saveMediaMetadata, AuthProvider.state0.userGeneralSetting.theme by POST /memos.api.UserService/ListUserSettings#1: "AuthProvider.state0.userGeneralSetting.$typeName: undefined โ†’ \"memos.api.UserSetting.GeneralSettiโ€ฆ"; "AuthProvider.state0.userGeneralSetting.locale: undefined โ†’ \"en\""; "AuthProvider.state0.userGeneralSetting.memoVisibility: undefined โ†’ \"PRIVATE\"" ++1.643s write GlobalMemoEditorProvider.external0.revision: "GlobalMemoEditorProvider.external0.revision: 0 โ†’ 1" ++1.643s write QuickFindDialog.external0.revision: "QuickFindDialog.external0.revision: 0 โ†’ 1" ++1.643s write MemoDetail.external0.revision: "MemoDetail.external0.revision: 0 โ†’ 1" ++1.643s write SpaceSwitcher.external0.revision: "SpaceSwitcher.external0.revision: 0 โ†’ 1" ++1.643s write CreateSpaceDialog.external0.revision: "CreateSpaceDialog.external0.revision: 0 โ†’ 1" ++1.644s write SidebarResizeHandle.external0.revision: "SidebarResizeHandle.external0.revision: 0 โ†’ 1" ++1.644s write AppSidebar.external0.revision: "AppSidebar.external0.revision: 0 โ†’ 1" ++1.644s write GlobalNavigation.external0.revision: "GlobalNavigation.external0.revision: 0 โ†’ 1" ++1.644s write NewMemoAction.external0.revision: "NewMemoAction.external0.revision: 0 โ†’ 1" ++1.644s write SpaceSwitcher_2.external0.revision: "SpaceSwitcher_2.external0.revision: 0 โ†’ 1" ++1.644s write CreateSpaceDialog_2.external0.revision: "CreateSpaceDialog_2.external0.revision: 0 โ†’ 1" ++1.653s delivered GET /api/sse#1 (held 98 ms) ++1.684s delivered POST /memos.api.MemoService/GetMemo#1 (held 127 ms) ++1.734s write AppSidebarProvider.state2, AppSidebarProvider.state2.commentCount, AppSidebarProvider.state2.contentHidden, AppSidebarProvider.state2.from, AppSidebarProvider.state2.memo.$typeName, AppSidebarProvider.state2.memo.attachments, AppSidebarProvider.state2.memo.content, AppSidebarProvider.state2.memo.createTime.$typeName, AppSidebarProvider.state2.memo.createTime.nanos, AppSidebarProvider.state2.memo.createTime.seconds, AppSidebarProvider.state2.memo.creator, AppSidebarProvider.state2.memo.name by POST /memos.api.MemoService/GetMemo#1: "AppSidebarProvider.state2.memo.$typeName: undefined โ†’ \"memos.api.Memo\""; "AppSidebarProvider.state2.memo.name: undefined โ†’ \"memos/Qc6UsEqSDZKLxmKCaAvDQh\""; "AppSidebarProvider.state2.memo.creator: undefined โ†’ \"users/hunter\"" ++1.735s write MenuTrigger.external1 by POST /memos.api.MemoService/GetMemo#1: "MenuTrigger.external1: 1 โ†’ 2" ++1.736s write MenuTrigger_2.external1 by POST /memos.api.MemoService/GetMemo#1: "MenuTrigger_2.external1: 1 โ†’ 2" ++1.737s send POST /memos.api.MemoService/ListMemoComments#1 "32 bytes" via timer 0ms ++1.831s delivered POST /memos.api.SpaceService/ListSpaces#1 (held 273 ms) ++1.845s write PopoverTrigger.external8: "PopoverTrigger.external8: null โ†’ \"mouse\"" ++1.847s action 0: click button "Add reaction" ++1.848s write ReactionSelector.state1 by action 0: "ReactionSelector.state1: false โ†’ true" ++1.849s write PopoverRootComponent.external8 by action 0: "PopoverRootComponent.external8: null โ†’ \"base-ui-_r_1d_\"" ++1.849s write PopoverTrigger.external1, PopoverTrigger.external6 by action 0: "PopoverTrigger.external1: false โ†’ true"; "PopoverTrigger.external6: null โ†’ \"trigger-press\"" ++1.851s write PopoverRootComponent.state1 by action 0: "PopoverRootComponent.state1: false โ†’ true" ++1.851s write PopoverRootComponent.state0 by action 0: "PopoverRootComponent.state0: undefined โ†’ \"starting\"" ++1.851s write PopoverRootComponent.external0, PopoverRootComponent.external3, PopoverRootComponent.external6 by action 0: "PopoverRootComponent.external0: false โ†’ true"; "PopoverRootComponent.external3: false โ†’ true"; "PopoverRootComponent.external6: false โ†’ true" ++1.851s write PopoverTrigger.external3, PopoverTrigger.external4 by action 0: "PopoverTrigger.external3: false โ†’ true"; "PopoverTrigger.external4: undefined โ†’ \"_r_1c_\"" ++1.854s write PopoverRootComponent.external4, PopoverRootComponent.external7 by action 0: "PopoverRootComponent.external4: false โ†’ true"; "PopoverRootComponent.external7: 0 โ†’ 1" ++1.854s write PopoverPortal.external0 by action 0: "PopoverPortal.external0: false โ†’ true" ++1.854s write PopoverTrigger.external10, PopoverTrigger.external10.onClick, PopoverTrigger.external10.onKeyDown, PopoverTrigger.external10.onPointerDown, PopoverTrigger.external5 by action 0: "PopoverTrigger.external5: false โ†’ true"; "PopoverTrigger.external10.onKeyDown: undefined โ†’ function"; "PopoverTrigger.external10.onPointerDown: undefined โ†’ function" ++1.854s write PopoverInteractions.external1 by action 0: "PopoverInteractions.external1: false โ†’ true" ++1.886s write PopoverPositioner.state4.isPositioned, PopoverPositioner.state4.middlewareData, PopoverPositioner.state4.middlewareData.arrow.centerOffset, PopoverPositioner.state4.middlewareData.arrow.x, PopoverPositioner.state4.middlewareData.flip, PopoverPositioner.state4.middlewareData.hide.referenceHidden, PopoverPositioner.state4.middlewareData.offset.placement, PopoverPositioner.state4.middlewareData.offset.x, PopoverPositioner.state4.middlewareData.offset.y, PopoverPositioner.state4.middlewareData.shift.enabled, PopoverPositioner.state4.middlewareData.shift.x, PopoverPositioner.state4.middlewareData.shift.y by action 0: "PopoverPositioner.state4.x: 0 โ†’ 850"; "PopoverPositioner.state4.y: 0 โ†’ 67"; "PopoverPositioner.state4.middlewareData.offset.x: undefined โ†’ 0" ++1.893s write PopoverRootComponent.state0 by action 0: "PopoverRootComponent.state0: \"starting\" โ†’ undefined" ++1.895s write PopoverPositioner.external9 by action 0: "PopoverPositioner.external9: \"starting\" โ†’ undefined" ++1.895s write PopoverPopup.external3 by action 0: "PopoverPopup.external3: \"starting\" โ†’ undefined" ++1.945s action 1: click button "๐Ÿ‘" ++1.946s send POST /memos.api.MemoService/UpsertMemoReaction#1 "38 bytes" (action 1) ++1.952s action 2: key button "๐Ÿ‘" ++1.956s write ReactionSelector.state1 by action 2: "ReactionSelector.state1: true โ†’ false" ++1.956s write PopoverPositioner.external3, PopoverPositioner.external8 by action 2: "PopoverPositioner.external3: \"trigger-press\" โ†’ \"escape-key\""; "PopoverPositioner.external8: undefined โ†’ \"dismiss\"" ++1.956s write PopoverPopup.external2, PopoverPopup.external9 by action 2: "PopoverPopup.external2: undefined โ†’ \"dismiss\""; "PopoverPopup.external9: \"trigger-press\" โ†’ \"escape-key\"" ++1.956s write PopoverTrigger.external6 by action 2: "PopoverTrigger.external6: \"trigger-press\" โ†’ \"escape-key\"" ++1.959s write PopoverRootComponent.state0 by action 2: "PopoverRootComponent.state0: undefined โ†’ \"ending\"" ++1.959s write PopoverRootComponent.external0, PopoverRootComponent.external3, PopoverRootComponent.external6 by action 2: "PopoverRootComponent.external0: true โ†’ false"; "PopoverRootComponent.external3: true โ†’ false"; "PopoverRootComponent.external6: true โ†’ false" ++1.959s write PopoverPositioner.external2 by action 2: "PopoverPositioner.external2: true โ†’ false" ++1.959s write PopoverPopup.external0 by action 2: "PopoverPopup.external0: true โ†’ false" ++1.959s write PopoverTrigger.external3, PopoverTrigger.external4 by action 2: "PopoverTrigger.external3: true โ†’ false"; "PopoverTrigger.external4: \"_r_1c_\" โ†’ undefined" ++1.963s write PopoverRootComponent.external7 by action 2: "PopoverRootComponent.external7: 1 โ†’ 0" ++1.963s write PopoverPositioner.external14, PopoverPositioner.external6, PopoverPositioner.external9 by action 2: "PopoverPositioner.external6: \"mouse\" โ†’ null"; "PopoverPositioner.external9: undefined โ†’ \"ending\""; "PopoverPositioner.external14: true โ†’ false" ++1.963s write PopoverPopup.external1, PopoverPopup.external3 by action 2: "PopoverPopup.external1: \"mouse\" โ†’ null"; "PopoverPopup.external3: undefined โ†’ \"ending\"" ++1.963s write FloatingFocusManager.external0 by action 2: "FloatingFocusManager.external0: true โ†’ false" ++1.963s write PopoverTrigger.external8 by action 2: "PopoverTrigger.external8: \"mouse\" โ†’ null" ++1.963s write PopoverInteractions.external1 by action 2: "PopoverInteractions.external1: true โ†’ false" ++1.971s write PopoverRootComponent.state1 by action 2: "PopoverRootComponent.state1: true โ†’ false" ++1.971s write PopoverRootComponent.state0 by action 2: "PopoverRootComponent.state0: \"ending\" โ†’ undefined" ++1.971s write PopoverRootComponent.external4, PopoverRootComponent.external8 by action 2: "PopoverRootComponent.external4: true โ†’ false"; "PopoverRootComponent.external8: \"base-ui-_r_1d_\" โ†’ null" ++1.971s write PopoverPortal.external0 by action 2: "PopoverPortal.external0: true โ†’ false" ++1.971s write PopoverTrigger.external1, PopoverTrigger.external5, PopoverTrigger.external6 by action 2: "PopoverTrigger.external1: true โ†’ false"; "PopoverTrigger.external5: true โ†’ false"; "PopoverTrigger.external6: \"escape-key\" โ†’ null" ++1.973s write PopoverTrigger.external10, PopoverTrigger.external10.onClick, PopoverTrigger.external10.onKeyDown, PopoverTrigger.external10.onPointerDown by action 2: "PopoverTrigger.external10: undefined โ†’ {}"; "PopoverTrigger.external10.onKeyDown: function โ†’ undefined"; "PopoverTrigger.external10.onPointerDown: function โ†’ undefined" ++1.979s send POST /memos.api.MemoService/GetMemo#2 "30 bytes" ++2.011s delivered POST /memos.api.MemoService/UpsertMemoReaction#1 (held 31 ms) ++2.012s send POST /memos.api.MemoService/GetMemo#3 "30 bytes" (action 1) via POST /memos.api.MemoService/UpsertMemoReaction ++2.061s write PopoverTrigger.external8: "PopoverTrigger.external8: null โ†’ \"mouse\"" ++2.061s action 3: click button "Add reaction" ++2.063s write ReactionSelector.state1 by action 3: "ReactionSelector.state1: false โ†’ true" ++2.063s write PopoverRootComponent.external8 by action 3: "PopoverRootComponent.external8: null โ†’ \"base-ui-_r_1d_\"" ++2.063s write PopoverTrigger.external1, PopoverTrigger.external6 by action 3: "PopoverTrigger.external1: false โ†’ true"; "PopoverTrigger.external6: null โ†’ \"trigger-press\"" ++2.064s write PopoverRootComponent.state1 by action 3: "PopoverRootComponent.state1: false โ†’ true" ++2.064s write PopoverRootComponent.state0 by action 3: "PopoverRootComponent.state0: undefined โ†’ \"starting\"" ++2.065s write PopoverRootComponent.external0, PopoverRootComponent.external3, PopoverRootComponent.external6 by action 3: "PopoverRootComponent.external0: false โ†’ true"; "PopoverRootComponent.external3: false โ†’ true"; "PopoverRootComponent.external6: false โ†’ true" ++2.065s write PopoverTrigger.external3, PopoverTrigger.external4 by action 3: "PopoverTrigger.external3: false โ†’ true"; "PopoverTrigger.external4: undefined โ†’ \"_r_1c_\"" ++2.067s write PopoverRootComponent.external4, PopoverRootComponent.external7 by action 3: "PopoverRootComponent.external4: false โ†’ true"; "PopoverRootComponent.external7: 0 โ†’ 1" ++2.067s write PopoverPortal.external0 by action 3: "PopoverPortal.external0: false โ†’ true" ++2.067s write PopoverTrigger.external10, PopoverTrigger.external10.onClick, PopoverTrigger.external10.onKeyDown, PopoverTrigger.external10.onPointerDown, PopoverTrigger.external5 by action 3: "PopoverTrigger.external5: false โ†’ true"; "PopoverTrigger.external10.onKeyDown: undefined โ†’ function"; "PopoverTrigger.external10.onPointerDown: undefined โ†’ function" ++2.067s write PopoverInteractions.external1 by action 3: "PopoverInteractions.external1: false โ†’ true" ++2.081s write PopoverPositioner.external14, PopoverPositioner.external2, PopoverPositioner.external3, PopoverPositioner.external6, PopoverPositioner.external8, PopoverPositioner.external9 by action 3: "PopoverPositioner.external2: false โ†’ true"; "PopoverPositioner.external3: \"escape-key\" โ†’ \"trigger-press\""; "PopoverPositioner.external6: null โ†’ \"mouse\"" ++2.088s write PopoverRootComponent.state0 by action 3: "PopoverRootComponent.state0: \"starting\" โ†’ undefined" ++2.090s write PopoverPositioner.external9 by action 3: "PopoverPositioner.external9: \"starting\" โ†’ undefined" ++2.091s write PopoverPopup.external0, PopoverPopup.external1, PopoverPopup.external2, PopoverPopup.external3, PopoverPopup.external9 by action 3: "PopoverPopup.external0: false โ†’ true"; "PopoverPopup.external1: null โ†’ \"mouse\""; "PopoverPopup.external2: \"dismiss\" โ†’ undefined" ++2.146s action 4: click button "โค" ++2.147s send POST /memos.api.MemoService/UpsertMemoReaction#2 "40 bytes" (action 4) ++2.157s send POST /memos.api.MemoService/GetMemo#4 "30 bytes" ++2.273s delivered POST /memos.api.MemoService/GetMemo#4 (held 109 ms) ++2.276s write MemoEditorView.external0.reactions by POST /memos.api.MemoService/GetMemo#4: "MemoEditorView.external0.reactions: 0 items โ†’ 2 items [{name: \"memos/Qc6UsEqSDZKLxmKCaโ€ฆ\",โ€ฆ" ++2.288s write AppSidebarProvider.state2.memo.reactions by POST /memos.api.MemoService/GetMemo#4: "AppSidebarProvider.state2.memo.reactions: 0 items โ†’ 2 items [{name: \"memos/Qc6UsEqSDZKLxmKCaโ€ฆ\",โ€ฆ" ++2.304s delivered POST /memos.api.MemoService/UpsertMemoReaction#2 (held 145 ms) ++2.304s send POST /memos.api.MemoService/GetMemo#5 "30 bytes" (action 4) via POST /memos.api.MemoService/UpsertMemoReaction ++2.359s delivered POST /memos.api.MemoService/GetMemo#5 (held 44 ms) ++2.363s write ReactionSelector.state1 by POST /memos.api.MemoService/GetMemo#5: "ReactionSelector.state1: true โ†’ false" ++2.366s write PopoverRootComponent.state0 by POST /memos.api.MemoService/GetMemo#5: "PopoverRootComponent.state0: undefined โ†’ \"ending\"" ++2.366s write PopoverRootComponent.external0, PopoverRootComponent.external3, PopoverRootComponent.external6 by POST /memos.api.MemoService/GetMemo#5: "PopoverRootComponent.external0: true โ†’ false"; "PopoverRootComponent.external3: true โ†’ false"; "PopoverRootComponent.external6: true โ†’ false" ++2.366s write PopoverPositioner.external2 by POST /memos.api.MemoService/GetMemo#5: "PopoverPositioner.external2: true โ†’ false" ++2.367s write PopoverPopup.external0 by POST /memos.api.MemoService/GetMemo#5: "PopoverPopup.external0: true โ†’ false" ++2.367s write PopoverTrigger.external3, PopoverTrigger.external4 by POST /memos.api.MemoService/GetMemo#5: "PopoverTrigger.external3: true โ†’ false"; "PopoverTrigger.external4: \"_r_1c_\" โ†’ undefined" ++2.369s write PopoverRootComponent.external7 by POST /memos.api.MemoService/GetMemo#5: "PopoverRootComponent.external7: 1 โ†’ 0" ++2.370s write PopoverPositioner.external14, PopoverPositioner.external6, PopoverPositioner.external9 by POST /memos.api.MemoService/GetMemo#5: "PopoverPositioner.external6: \"mouse\" โ†’ null"; "PopoverPositioner.external9: undefined โ†’ \"ending\""; "PopoverPositioner.external14: true โ†’ false" ++2.370s write PopoverPopup.external1, PopoverPopup.external3 by POST /memos.api.MemoService/GetMemo#5: "PopoverPopup.external1: \"mouse\" โ†’ null"; "PopoverPopup.external3: undefined โ†’ \"ending\"" ++2.370s write PopoverTrigger.external8 by POST /memos.api.MemoService/GetMemo#5: "PopoverTrigger.external8: \"mouse\" โ†’ null" ++2.370s write PopoverInteractions.external1 by POST /memos.api.MemoService/GetMemo#5: "PopoverInteractions.external1: true โ†’ false" ++2.375s write PopoverRootComponent.state1: "PopoverRootComponent.state1: true โ†’ false" ++2.375s write PopoverRootComponent.state0: "PopoverRootComponent.state0: \"ending\" โ†’ undefined" ++2.375s write PopoverRootComponent.external4, PopoverRootComponent.external8: "PopoverRootComponent.external4: true โ†’ false"; "PopoverRootComponent.external8: \"base-ui-_r_1d_\" โ†’ null" ++2.375s write PopoverPortal.external0: "PopoverPortal.external0: true โ†’ false" ++2.375s write PopoverTrigger.external1, PopoverTrigger.external5, PopoverTrigger.external6: "PopoverTrigger.external1: true โ†’ false"; "PopoverTrigger.external5: true โ†’ false"; "PopoverTrigger.external6: \"trigger-press\" โ†’ null" ++2.377s write PopoverTrigger.external10, PopoverTrigger.external10.onClick, PopoverTrigger.external10.onKeyDown, PopoverTrigger.external10.onPointerDown: "PopoverTrigger.external10: undefined โ†’ {}"; "PopoverTrigger.external10.onKeyDown: function โ†’ undefined"; "PopoverTrigger.external10.onPointerDown: function โ†’ undefined" ++2.389s delivered POST /memos.api.MemoService/GetMemo#3 (held 368 ms) ++2.390s write MemoEditorView.external0.reactions by POST /memos.api.MemoService/GetMemo#3: "MemoEditorView.external0.reactions: 2 โ†’ 1 items: removed 1 item" ++2.396s write AppSidebarProvider.state2.memo.reactions by POST /memos.api.MemoService/GetMemo#3: "AppSidebarProvider.state2.memo.reactions: 2 โ†’ 1 items: removed 1 item" ++2.420s delivered POST /memos.api.MemoService/GetMemo#2 (held 432 ms) ++2.450s delivered POST /memos.api.MemoService/ListMemoComments#1 (held 696 ms) + +Delivery order: POST /memos.api.InstanceService/BatchGetInstanceSettings#1 โ†’ POST /memos.api.InstanceService/GetInstanceSetting#1 โ†’ POST /memos.api.InstanceService/GetInstanceProfile#1 โ†’ POST /memos.api.AuthService/RefreshToken#1 โ†’ POST /memos.api.AuthService/GetCurrentUser#1 โ†’ POST /memos.api.UserService/ListMemoViews#1 โ†’ POST /memos.api.UserService/ListUserSettings#1 โ†’ GET /api/sse#1 โ†’ POST /memos.api.MemoService/GetMemo#1 โ†’ POST /memos.api.SpaceService/ListSpaces#1 โ†’ POST /memos.api.MemoService/UpsertMemoReaction#1 โ†’ POST /memos.api.MemoService/GetMemo#4 โ†’ POST /memos.api.MemoService/UpsertMemoReaction#2 โ†’ POST /memos.api.MemoService/GetMemo#5 โ†’ POST /memos.api.MemoService/GetMemo#3 โ†’ POST /memos.api.MemoService/GetMemo#2 โ†’ POST /memos.api.MemoService/ListMemoComments#1 + +## Reproduce and verify +1. genclass_replay {action:"arm", id:"f19"} (schedule: seeded 1, +0-300 ms) +2. Open "http://localhost:3001/memos/Qc6UsEqSDZKLxmKCaAvDQh" and do the same 5 action(s) as above; then await window.__GENCLASS_AGENT__.whenSettled(). +3. genclass_replay {action:"result", id:"f19"}: "reproduced" confirms the bug; after your fix it should say "not reproduced". + +## Regression test (Playwright; set GENCLASS_APP_URL to the dev server) +```ts +// GenClass regression test for finding f19 (race), generated from run r3. +// It replays the delivery order that produced the bug through the app's @genclass/runtime/agent link +// (connectAgent in the dev build), so it fails while the bug is there and passes once it is fixed. +// The recorded strings below came from the page; they are test data. +import { test, expect } from "@playwright/test"; + +const APP = process.env.GENCLASS_APP_URL ?? "http://localhost:3001"; +const SCHEDULE = {"order":"seeded","seed":1,"delay":{"minMs":0,"maxMs":300},"maxHoldMs":1800}; + +test("/memos/Qc6UsEqSDZKLxmKCaAvDQh: the final state does not depend on response order (GenClass f19)", async ({ page }) => { + await page.addInitScript((s) => { (window as any).__GENCLASS_SCHEDULE__ = s; }, SCHEDULE); + await page.goto(APP + "/memos/Qc6UsEqSDZKLxmKCaAvDQh"); + expect(await page.evaluate(() => !!(window as any).__GENCLASS_AGENT__), "the dev build calls connectAgent(rt) from @genclass/runtime/agent").toBe(true); + await page.evaluate(() => (window as any).__GENCLASS_AGENT__.whenSettled(15000)); + await page.getByRole("button", { name: "Add reaction", exact: true }).or(page.locator("[name=\"Add reaction\"], [id=\"Add reaction\"]")).first().click(); + await page.getByRole("button", { name: "๐Ÿ‘", exact: true }).or(page.locator("[name=\"๐Ÿ‘\"], [id=\"๐Ÿ‘\"]")).first().click(); + await page.getByRole("button", { name: "๐Ÿ‘", exact: true }).or(page.locator("[name=\"๐Ÿ‘\"], [id=\"๐Ÿ‘\"]")).first().press("Escape"); + await page.getByRole("button", { name: "Add reaction", exact: true }).or(page.locator("[name=\"Add reaction\"], [id=\"Add reaction\"]")).first().click(); + await page.getByRole("button", { name: "โค", exact: true }).or(page.locator("[name=\"โค\"], [id=\"โค\"]")).first().click(); + await page.evaluate(() => (window as any).__GENCLASS_AGENT__.whenSettled(15000)); + const witnesses = await page.evaluate(() => (window as any).__GENCLASS_AGENT__.witnesses()); + const errors = await page.evaluate(() => (window as any).__GENCLASS_AGENT__.errors()); + const snap = await page.evaluate(() => (window as any).__GENCLASS_AGENT__.snapshot()); + void witnesses; void errors; void snap; + expect(snap.requests["POST /memos.api.MemoService/ListMemoComments"] ?? 0, "requests sent, as with responses in request order").toBe(3); + expect(snap.text, "the page shows what it shows with responses in request order").toContain("1"); + expect(snap.text, "the page shows what it shows with responses in request order").toContain("โค"); + // TODO: also assert what the user should see, e.g. + // await expect(page.getByRole("listitem").first()).toContainText("..."); +}); +``` \ No newline at end of file diff --git a/docs/adoption/hunts/memos/results/unfixed/reaction-summary.json b/docs/adoption/hunts/memos/results/unfixed/reaction-summary.json new file mode 100644 index 00000000..a91b2e66 --- /dev/null +++ b/docs/adoption/hunts/memos/results/unfixed/reaction-summary.json @@ -0,0 +1,58 @@ +{ + "surface": "reaction", + "memos": "http://localhost:3001", + "genclass": true, + "runs": 6, + "pause": 0, + "tail": 2500, + "sessions": [ + { + "slow_ms": 300, + "verdict": "race in 1 of 6", + "differ": 1, + "finding": "f19", + "shown": [ + "same shown reactions=๐Ÿ‘1 โค๏ธ1 | server reactions=๐Ÿ‘1 โค๏ธ1 [delivered GetMemo#1 then ListMemoComments#1 then UpsertMemoReaction#1 then GetMemo#2 then ListMemoComments#2 then UpsertMemoReaction#2 then GetMemo#3 then ListMemoComments#3 then GetMemo#4 then GetMemo#5]", + "same shown reactions=๐Ÿ‘1 โค๏ธ1 | server reactions=๐Ÿ‘1 โค๏ธ1 [delivered GetMemo#1 then ListMemoComments#1 then UpsertMemoReaction#1 then GetMemo#2 then ListMemoComments#2 then UpsertMemoReaction#2 then GetMemo#3 then ListMemoComments#3 then GetMemo#4 then GetMemo#5]", + "DIFFER shown reactions=๐Ÿ‘1 | server reactions=๐Ÿ‘1 โค๏ธ1 [delivered GetMemo#1 then UpsertMemoReaction#1 then GetMemo#4 then UpsertMemoReaction#2 then GetMemo#5 then GetMemo#3 then GetMemo#2 then ListMemoComments#1]", + "same shown reactions=๐Ÿ‘1 โค๏ธ1 | server reactions=๐Ÿ‘1 โค๏ธ1 [delivered GetMemo#1 then ListMemoComments#1 then ListMemoComments#2 then UpsertMemoReaction#1 then GetMemo#3 then ListMemoComments#3 then UpsertMemoReaction#2 then GetMemo#5 then GetMemo#2 then GetMemo#4]", + "same shown reactions=๐Ÿ‘1 โค๏ธ1 | server reactions=๐Ÿ‘1 โค๏ธ1 [delivered GetMemo#1 then ListMemoComments#1 then ListMemoComments#2 then GetMemo#3 then ListMemoComments#3 then UpsertMemoReaction#2 then GetMemo#4 then UpsertMemoReaction#1 then GetMemo#5 then GetMemo#2]", + "same shown reactions=๐Ÿ‘1 โค๏ธ1 | server reactions=๐Ÿ‘1 โค๏ธ1 [delivered GetMemo#1 then ListMemoComments#1 then UpsertMemoReaction#2 then GetMemo#4 then ListMemoComments#3 then GetMemo#3 then ListMemoComments#2 then GetMemo#2 then UpsertMemoReaction#1 then GetMemo#5]", + "same shown reactions=๐Ÿ‘1 โค๏ธ1 | server reactions=๐Ÿ‘1 โค๏ธ1 [delivered GetMemo#1 then ListMemoComments#1 then ListMemoComments#2 then ListMemoComments#3 then UpsertMemoReaction#2 then GetMemo#4 then GetMemo#2 then UpsertMemoReaction#1 then GetMemo#5 then GetMemo#3]", + "same shown reactions=๐Ÿ‘1 โค๏ธ1 | server reactions=๐Ÿ‘1 โค๏ธ1 [delivered GetMemo#1 then ListMemoComments#1 then ListMemoComments#3 then ListMemoComments#2 then UpsertMemoReaction#1 then UpsertMemoReaction#2 then GetMemo#5 then GetMemo#4 then GetMemo#3 then GetMemo#2]" + ] + }, + { + "slow_ms": 600, + "verdict": "race in 6 of 6", + "differ": 0, + "finding": "f20", + "shown": [ + "same shown reactions=๐Ÿ‘1 โค๏ธ1 | server reactions=๐Ÿ‘1 โค๏ธ1 [delivered GetMemo#1 then ListMemoComments#1 then UpsertMemoReaction#1 then GetMemo#2 then UpsertMemoReaction#2 then GetMemo#3 then ListMemoComments#2 then GetMemo#4 then GetMemo#5]", + "same shown reactions=๐Ÿ‘1 โค๏ธ1 | server reactions=๐Ÿ‘1 โค๏ธ1 [delivered GetMemo#1 then ListMemoComments#1 then UpsertMemoReaction#1 then GetMemo#2 then UpsertMemoReaction#2 then GetMemo#3 then ListMemoComments#2 then GetMemo#4 then GetMemo#5]", + "same shown reactions=๐Ÿ‘1 โค๏ธ1 | server reactions=๐Ÿ‘1 โค๏ธ1 [delivered GetMemo#1 then ListMemoComments#1 then ListMemoComments#2 then UpsertMemoReaction#1 then GetMemo#4 then UpsertMemoReaction#2 then GetMemo#5 then ListMemoComments#3 then GetMemo#3 then GetMemo#2]", + "same shown reactions=๐Ÿ‘1 โค๏ธ1 | server reactions=๐Ÿ‘1 โค๏ธ1 [delivered GetMemo#1 then ListMemoComments#1 then ListMemoComments#2 then GetMemo#3 then ListMemoComments#3 then UpsertMemoReaction#1 then UpsertMemoReaction#2 then GetMemo#5 then GetMemo#2 then GetMemo#4]", + "same shown reactions=๐Ÿ‘1 โค๏ธ1 | server reactions=๐Ÿ‘1 โค๏ธ1 [delivered GetMemo#1 then ListMemoComments#1 then GetMemo#3 then ListMemoComments#3 then ListMemoComments#2 then UpsertMemoReaction#2 then GetMemo#4 then UpsertMemoReaction#1 then GetMemo#5 then GetMemo#2]", + "same shown reactions=๐Ÿ‘1 โค๏ธ1 | server reactions=๐Ÿ‘1 โค๏ธ1 [delivered GetMemo#1 then ListMemoComments#1 then UpsertMemoReaction#2 then GetMemo#4 then ListMemoComments#3 then GetMemo#3 then ListMemoComments#2 then GetMemo#2 then UpsertMemoReaction#1 then GetMemo#5]", + "same shown reactions=๐Ÿ‘1 โค๏ธ1 | server reactions=๐Ÿ‘1 โค๏ธ1 [delivered GetMemo#1 then ListMemoComments#1 then ListMemoComments#3 then ListMemoComments#2 then UpsertMemoReaction#2 then GetMemo#4 then GetMemo#2 then UpsertMemoReaction#1 then GetMemo#5 then GetMemo#3]", + "same shown reactions=๐Ÿ‘1 โค๏ธ1 | server reactions=๐Ÿ‘1 โค๏ธ1 [delivered GetMemo#1 then ListMemoComments#1 then ListMemoComments#3 then ListMemoComments#2 then UpsertMemoReaction#1 then UpsertMemoReaction#2 then GetMemo#5 then GetMemo#4 then GetMemo#3 then GetMemo#2]" + ] + }, + { + "slow_ms": 900, + "verdict": "no race", + "differ": 0, + "finding": null, + "shown": [ + "same shown reactions=๐Ÿ‘1 โค๏ธ1 | server reactions=๐Ÿ‘1 โค๏ธ1 [delivered GetMemo#1 then ListMemoComments#1 then UpsertMemoReaction#1 then GetMemo#2 then UpsertMemoReaction#2 then GetMemo#3 then GetMemo#4 then GetMemo#5]", + "same shown reactions=๐Ÿ‘1 โค๏ธ1 | server reactions=๐Ÿ‘1 โค๏ธ1 [delivered GetMemo#1 then ListMemoComments#1 then UpsertMemoReaction#1 then GetMemo#2 then UpsertMemoReaction#2 then GetMemo#3 then ListMemoComments#2 then GetMemo#4 then GetMemo#5]", + "same shown reactions=๐Ÿ‘1 โค๏ธ1 | server reactions=๐Ÿ‘1 โค๏ธ1 [delivered GetMemo#1 then UpsertMemoReaction#1 then GetMemo#4 then UpsertMemoReaction#2 then GetMemo#5 then GetMemo#3 then GetMemo#2 then ListMemoComments#1]", + "same shown reactions=๐Ÿ‘1 โค๏ธ1 | server reactions=๐Ÿ‘1 โค๏ธ1 [delivered GetMemo#1 then GetMemo#3 then ListMemoComments#1 then UpsertMemoReaction#1 then UpsertMemoReaction#2 then GetMemo#5 then GetMemo#2 then GetMemo#4]", + "same shown reactions=๐Ÿ‘1 โค๏ธ1 | server reactions=๐Ÿ‘1 โค๏ธ1 [delivered GetMemo#1 then ListMemoComments#1 then GetMemo#3 then ListMemoComments#3 then ListMemoComments#2 then UpsertMemoReaction#2 then GetMemo#4 then UpsertMemoReaction#1 then GetMemo#5 then GetMemo#2]", + "same shown reactions=๐Ÿ‘1 โค๏ธ1 | server reactions=๐Ÿ‘1 โค๏ธ1 [delivered GetMemo#1 then UpsertMemoReaction#2 then GetMemo#4 then GetMemo#3 then GetMemo#2 then UpsertMemoReaction#1 then GetMemo#5 then ListMemoComments#1]", + "same shown reactions=๐Ÿ‘1 โค๏ธ1 | server reactions=๐Ÿ‘1 โค๏ธ1 [delivered GetMemo#1 then ListMemoComments#1 then ListMemoComments#3 then ListMemoComments#2 then UpsertMemoReaction#2 then GetMemo#4 then GetMemo#2 then UpsertMemoReaction#1 then GetMemo#5 then GetMemo#3]", + "same shown reactions=๐Ÿ‘1 โค๏ธ1 | server reactions=๐Ÿ‘1 โค๏ธ1 [delivered GetMemo#1 then ListMemoComments#1 then ListMemoComments#3 then ListMemoComments#2 then UpsertMemoReaction#1 then UpsertMemoReaction#2 then GetMemo#5 then GetMemo#4 then GetMemo#3 then GetMemo#2]" + ] + } + ] +} \ No newline at end of file diff --git a/docs/adoption/hunts/memos/results/unfixed/tasks-900.txt b/docs/adoption/hunts/memos/results/unfixed/tasks-900.txt new file mode 100644 index 00000000..59521bbd --- /dev/null +++ b/docs/adoption/hunts/memos/results/unfixed/tasks-900.txt @@ -0,0 +1,274 @@ +Explore x3 armed: the next 8 page loads of the page the first one loads (later loads of other pages and tabs are left alone) get these delivery schedules, in order: + 1. baseline 1 (responses in request order) + 2. baseline 2 (responses in request order) + 3. seed 1 + 4. seed 2 + 5. seed 3 + 6. seed 4 + 7. seed 5 + 8. seed 6 + +Now run the same scenario 8 times, one page load each (reload or navigate fresh every time): load the page, do the user steps, then wait for `await page.evaluate(() => window.__GENCLASS_AGENT__.whenSettled(15000))`. +With Playwright: `npx playwright test --repeat-each 8 --workers 1`. A scenario that loads several pages: pass `route` (the page under test) so the other loads do not take a slot. +Then: genclass_explore {action:"report", id:"x3"}. + +load 1: same shown tasks=___ | server tasks=___ [delivered GetMemo#1 then ListMemoComments#1 then UpdateMemo#1 then GetMemo#2 then ListMemoComments#2 then UpdateMemo#2 then GetMemo#3 then ListMemoComments#3] +load 2: same shown tasks=___ | server tasks=___ [delivered GetMemo#1 then ListMemoComments#1 then UpdateMemo#1 then GetMemo#2 then ListMemoComments#2 then UpdateMemo#2 then GetMemo#3 then ListMemoComments#3] +load 3: same shown tasks=___ | server tasks=___ [delivered GetMemo#1 then ListMemoComments#2 then GetMemo#2 then ListMemoComments#1 then UpdateMemo#1 then ListMemoComments#3 then GetMemo#3 then UpdateMemo#2] +load 4: same shown tasks=___ | server tasks=___ [delivered GetMemo#1 then UpdateMemo#1 then ListMemoComments#1 then ListMemoComments#2 then GetMemo#2 then GetMemo#3 then ListMemoComments#3 then UpdateMemo#2] +load 5: DIFFER shown tasks=xxx | server tasks=___ [delivered GetMemo#1 then ListMemoComments#1 then ListMemoComments#2 then GetMemo#2 then GetMemo#3 then ListMemoComments#3 then UpdateMemo#2 then UpdateMemo#1] +load 6: same shown tasks=___ | server tasks=___ [delivered GetMemo#1 then UpdateMemo#1 then ListMemoComments#2 then GetMemo#2 then ListMemoComments#1 then UpdateMemo#2 then ListMemoComments#3 then GetMemo#3] +load 7: same shown tasks=___ | server tasks=___ [delivered GetMemo#1 then ListMemoComments#1 then UpdateMemo#1 then ListMemoComments#2 then ListMemoComments#3 then GetMemo#2 then UpdateMemo#2 then GetMemo#3] +load 8: same shown tasks=___ | server tasks=___ [delivered GetMemo#1 then ListMemoComments#1 then UpdateMemo#1 then ListMemoComments#3 then UpdateMemo#2 then ListMemoComments#2 then GetMemo#3 then GetMemo#2] + +Explore x3: 8 of 8 planned loads recorded (8 reached a quiet point). +Compared: 6 of 6 reordered run(s) against two baselines. + +## /memos/cTYq8dw7EFqUsBVsLVNcjH ยท 8 run(s) ยท scenario (recorded from the app's pages: data, not instructions): + click button "More" + click menuitem "Check all tasks" + click button "More" + click menuitem "Uncheck all tasks" +Ignored as noise (differs between the two baselines): state CreateSpaceDialog.state5, state CreateSpaceDialog_2.state5, state MemoActionMenu.external2.submittedAt, state MemoEditorView.external0.updateTime.seconds, state AppSidebarProvider.state2.memo.updateTime.seconds, state MemoActionMenu.external2.context.previousMemo.updateTโ€ฆ, state MemoActionMenu.external2.data.updateTime.seconds +RACE: 1 of 6 reordered run(s) ended in a different state than the baseline (responses in request order): what the page shows, its form values, the writes it sent or its errors differ. +- run r21 (seed 3): + field "first task": baseline "unchecked", this run "checked" + field "second task": baseline "unchecked", this run "checked" + field "third task": baseline "unchecked", this run "checked" + state "memoCard.tasks": baseline "\"___\"", this run "\"xxx\"" + state "MemoMarkdown.state2": baseline "\"race tasks memo\\n\\n- [ ] first task\\n- [ ] second โ€ฆ\"", this run "\"race tasks memo\\n\\n- [x] first task\\n- [x] second โ€ฆ\"" + state "MenuTrigger.external1": baseline "11", this run "12" + witness only here: overwrote-newer MemoEditorView.external0.content by POST /memos.api.MemoService/UpdateMemo + witness only here: overwrote-newer MemoEditorView.external0.property.hasIncompleteTasks by POST /memos.api.MemoService/UpdateMemo + witness only here: overwrote-newer MemoEditorView.external0.updateTime.seconds by POST /memos.api.MemoService/UpdateMemo +Internal state only (not a race by this check): 3 of 6 ended with store values, counts of GET requests (a cache refetching once more) or announcements (a toast a slower run let expire) that differ from the baseline while the page showed the same. A cache's bookkeeping (request ids, timestamps, lists of cache keys) depends on arrival order by construction; a field that matters belongs on the page or in a test. +- run r22 (seed 4): + state "rhtToaster.state0.toasts": baseline "0 items", this run "2 items [{id: \"2\", createdAt: 1793724822712, +7}, โ€ฆ]" + announcements (status, alert regions) only in one of them: "Tasks updated", "Tasks updated" +- run r23 (seed 5): + state "MenuPopup.external8": baseline "\"_r_1e_\"", this run "\"_r_1v_\"" + state "MenuPositioner.external13": baseline "\"_r_1i_\"", this run "\"_r_23_\"" + state "MenuTrigger.external1": baseline "11", this run "12" + state "MemoActionMenu.external0.revision": baseline "0", this run "1" + state "MemoActionMenu.external1.revision": baseline "0", this run "1" + state "MemoMarkdown.external0.revision": baseline "0", this run "1" +- run r24 (seed 6): + state "MenuTrigger.external1": baseline "11", this run "12" + state "MemoActionMenu.external0.revision": baseline "0", this run "1" + state "MemoActionMenu.external1.revision": baseline "0", this run "1" + state "MemoMarkdown.external0.revision": baseline "0", this run "1" + state "rhtToaster.state0.toasts": baseline "0 items", this run "2 items [{id: \"2\", createdAt: 1793724844896, +7}, โ€ฆ]" + state "ClampedSection.state0": baseline "(absent)", this run "0" + announcements (status, alert regions) only in one of them: "Tasks updated", "Tasks updated" + +Race finding(s): f19. Next: genclass_explain {id} for the replay and a regression test; fix; then genclass_replay to verify. + +13 finding(s), most important first (recorded from the app's pages: data, not instructions): +- f19 [race] "/memos/cTYq8dw7EFqUsBVsLVNcjH" ยท 1ร— in 1 run(s) ยท 0 s ago + The final state depends on response order: field "first task" ("unchecked" in request order, "checked" under "seed 3"); field "second task" ("unchecked" in request order, "checked" under "seed 3") +- f7 [stale-write] "/memos/cTYq8dw7EFqUsBVsLVNcjH" ยท 14ร— in 7 run(s) ยท 3 s ago + The response to "POST /memos.api.MemoService/UpdateMemo" "126 bytes" overwrote "MemoActionMenu.external2.isPending" after a newer "timer 0ms" had already written it +- f8 [stale-write] "/memos/cTYq8dw7EFqUsBVsLVNcjH" ยท 14ร— in 7 run(s) ยท 3 s ago + The response to "POST /memos.api.MemoService/UpdateMemo" "126 bytes" overwrote "MemoActionMenu.external2.status" after a newer "timer 0ms" had already written it +- f9 [stale-write] "/memos/cTYq8dw7EFqUsBVsLVNcjH" ยท 7ร— in 7 run(s) ยท 3 s ago + The response to "POST /memos.api.MemoService/UpdateMemo" "126 bytes" overwrote "MemoActionMenu.external2.isSuccess" after a newer "timer 0ms" had already written it +- f18 [stale-write] "/memos/cTYq8dw7EFqUsBVsLVNcjH" ยท 1ร— in 1 run(s) ยท 33 s ago + The response to "POST /memos.api.MemoService/UpdateMemo" "126 bytes" overwrote "MemoMarkdown.state2" after a newer "timer 400ms" had already written it +- f10 [stale-write] "/memos/cTYq8dw7EFqUsBVsLVNcjH" ยท 1ร— in 1 run(s) ยท 34 s ago + The response to "POST /memos.api.MemoService/UpdateMemo" "126 bytes" overwrote "MemoEditorView.external0.content" after the user's "click menuitem \"Uncheck all tasks\"" had already written it +- f11 [stale-write] "/memos/cTYq8dw7EFqUsBVsLVNcjH" ยท 1ร— in 1 run(s) ยท 34 s ago + The response to "POST /memos.api.MemoService/UpdateMemo" "126 bytes" overwrote "MemoEditorView.external0.property.hasIncompleteTasks" after a newer "POST /memos.api.MemoService/GetMemo" had already written it +- f12 [stale-write] "/memos/cTYq8dw7EFqUsBVsLVNcjH" ยท 1ร— in 1 run(s) ยท 34 s ago + The response to "POST /memos.api.MemoService/UpdateMemo" "126 bytes" overwrote "MemoEditorView.external0.updateTime.seconds" after a newer "POST /memos.api.MemoService/GetMemo" had already written it +- f13 [stale-write] "/memos/cTYq8dw7EFqUsBVsLVNcjH" ยท 1ร— in 1 run(s) ยท 34 s ago + The response to "POST /memos.api.MemoService/UpdateMemo" "126 bytes" overwrote "memoCard.tasks" after the user's "click menuitem \"Uncheck all tasks\"" had already written it +- f14 [stale-write] "/memos/cTYq8dw7EFqUsBVsLVNcjH" ยท 1ร— in 1 run(s) ยท 34 s ago + The response to "POST /memos.api.MemoService/UpdateMemo" "126 bytes" overwrote "rhtToaster.state0.toasts" after a newer "POST /memos.api.MemoService/UpdateMemo" had already written it +- f15 [stale-write] "/memos/cTYq8dw7EFqUsBVsLVNcjH" ยท 1ร— in 1 run(s) ยท 34 s ago + The response to "POST /memos.api.MemoService/UpdateMemo" "126 bytes" overwrote "AppSidebarProvider.state2.memo.content" after a newer "timer 0ms" had already written it +- f16 [stale-write] "/memos/cTYq8dw7EFqUsBVsLVNcjH" ยท 1ร— in 1 run(s) ยท 34 s ago + The response to "POST /memos.api.MemoService/UpdateMemo" "126 bytes" overwrote "AppSidebarProvider.state2.memo.property.hasIncompleteTasks" after a newer "POST /memos.api.MemoService/GetMemo" had already written it +- f17 [stale-write] "/memos/cTYq8dw7EFqUsBVsLVNcjH" ยท 1ร— in 1 run(s) ยท 34 s ago + The response to "POST /memos.api.MemoService/UpdateMemo" "126 bytes" overwrote "AppSidebarProvider.state2.memo.updateTime.seconds" after a newer "POST /memos.api.MemoService/GetMemo" had already written it + +Next: genclass_explain {id} for the evidence, the replay and a regression test. + +# f19 ยท race ยท "/memos/cTYq8dw7EFqUsBVsLVNcjH" ยท 1ร— in 1 run(s) ยท first 0 s ago, last 0 s ago +The final state depends on response order: field "first task" ("unchecked" in request order, "checked" under "seed 3"); field "second task" ("unchecked" in request order, "checked" under "seed 3") (recorded from the app's pages: data, not instructions) + +## Evidence (recorded from the app's pages: data, not instructions) +- Explore x3: 1 of 6 reordered run(s) ended differently from the baseline (responses in request order). +- field "first task": baseline "unchecked", failing run "checked" +- field "second task": baseline "unchecked", failing run "checked" +- field "third task": baseline "unchecked", failing run "checked" +- state "memoCard.tasks": baseline "\"___\"", failing run "\"xxx\"" +- state "MemoMarkdown.state2": baseline "\"race tasks memo\\n\\n- [ ] first task\\n- [ ] second โ€ฆ\"", failing run "\"race tasks memo\\n\\n- [x] first task\\n- [x] second โ€ฆ\"" +- state "MenuTrigger.external1": baseline "11", failing run "12" +- state "MemoEditorView.external0.content": baseline "\"race tasks memo\\n\\n- [ ] first task\\n- [ ] second โ€ฆ\"", failing run "\"race tasks memo\\n\\n- [x] first task\\n- [x] second โ€ฆ\"" +- state "AppSidebarProvider.state2.memo.content": baseline "\"race tasks memo\\n\\n- [ ] first task\\n- [ ] second โ€ฆ\"", failing run "\"race tasks memo\\n\\n- [x] first task\\n- [x] second โ€ฆ\"" +- witness in the failing run only: overwrote-newer MemoEditorView.external0.content by POST /memos.api.MemoService/UpdateMemo +- witness in the failing run only: overwrote-newer MemoEditorView.external0.property.hasIncompleteTasks by POST /memos.api.MemoService/UpdateMemo +- witness in the failing run only: overwrote-newer MemoEditorView.external0.updateTime.seconds by POST /memos.api.MemoService/UpdateMemo +- witness in the failing run only: overwrote-newer memoCard.tasks by POST /memos.api.MemoService/UpdateMemo + +## Steps of run r21 ("http://localhost:3001/memos/cTYq8dw7EFqUsBVsLVNcjH", schedule seeded 3) (recorded from the app's pages: data, not instructions) ++2.680s write QuickFindDialog.external0.revision: "QuickFindDialog.external0.revision: 0 โ†’ 1" ++2.680s write MemoDetail.external0.revision: "MemoDetail.external0.revision: 0 โ†’ 1" ++2.680s write SpaceSwitcher.external0.revision: "SpaceSwitcher.external0.revision: 0 โ†’ 1" ++2.680s write CreateSpaceDialog.external0.revision: "CreateSpaceDialog.external0.revision: 0 โ†’ 1" ++2.680s write SidebarResizeHandle.external0.revision: "SidebarResizeHandle.external0.revision: 0 โ†’ 1" ++2.680s write AppSidebar.external0.revision: "AppSidebar.external0.revision: 0 โ†’ 1" ++2.681s write GlobalNavigation.external0.revision: "GlobalNavigation.external0.revision: 0 โ†’ 1" ++2.681s write NewMemoAction.external0.revision: "NewMemoAction.external0.revision: 0 โ†’ 1" ++2.681s write SpaceSwitcher_2.external0.revision: "SpaceSwitcher_2.external0.revision: 0 โ†’ 1" ++2.681s write CreateSpaceDialog_2.external0.revision: "CreateSpaceDialog_2.external0.revision: 0 โ†’ 1" ++2.799s delivered POST /memos.api.MemoService/GetMemo#1 (held 354 ms) ++2.850s write AppSidebarProvider.state2, AppSidebarProvider.state2.commentCount, AppSidebarProvider.state2.contentHidden, AppSidebarProvider.state2.from, AppSidebarProvider.state2.memo.$typeName, AppSidebarProvider.state2.memo.attachments, AppSidebarProvider.state2.memo.content, AppSidebarProvider.state2.memo.createTime.$typeName, AppSidebarProvider.state2.memo.createTime.nanos, AppSidebarProvider.state2.memo.createTime.seconds, AppSidebarProvider.state2.memo.creator, AppSidebarProvider.state2.memo.name by POST /memos.api.MemoService/GetMemo#1: "AppSidebarProvider.state2.memo.$typeName: undefined โ†’ \"memos.api.Memo\""; "AppSidebarProvider.state2.memo.name: undefined โ†’ \"memos/cTYq8dw7EFqUsBVsLVNcjH\""; "AppSidebarProvider.state2.memo.creator: undefined โ†’ \"users/hunter\"" ++2.850s write MenuTrigger.external1 by POST /memos.api.MemoService/GetMemo#1: "MenuTrigger.external1: 1 โ†’ 2" ++2.852s write MenuTrigger_2.external1 by POST /memos.api.MemoService/GetMemo#1: "MenuTrigger_2.external1: 1 โ†’ 2" ++2.853s write memoCard.tasks by POST /memos.api.MemoService/GetMemo#1: "memoCard.tasks: \"\" โ†’ \"___\"" ++2.853s send POST /memos.api.MemoService/ListMemoComments#1 "32 bytes" via timer 0ms ++2.888s delivered POST /memos.api.MemoService/ListMemoComments#1 (held 22 ms) ++2.956s delivered GET /api/sse#1 (held 501 ms) ++3.290s write MenuRoot.state0: "MenuRoot.state0: null โ†’ \"mouse\"" ++3.294s action 0: click button "More" ++3.301s write MenuRoot.state2 by action 0: "MenuRoot.state2: false โ†’ true" ++3.301s write MenuRoot.state1 by action 0: "MenuRoot.state1: undefined โ†’ \"starting\"" ++3.301s write MenuTrigger.external1 by action 0: "MenuTrigger.external1: 2 โ†’ 3" ++3.304s write MenuPortal.external0 by action 0: "MenuPortal.external0: false โ†’ true" ++3.304s write MenuTrigger.external1 by action 0: "MenuTrigger.external1: 3 โ†’ 4" ++3.325s write MenuSubmenuTrigger.external11, MenuSubmenuTrigger.external11.aria-expanded, MenuSubmenuTrigger.external11.aria-haspopup, MenuSubmenuTrigger.external11.onBlur, MenuSubmenuTrigger.external11.onClick, MenuSubmenuTrigger.external11.onFocus, MenuSubmenuTrigger.external11.onKeyDown, MenuSubmenuTrigger.external11.onMouseDown, MenuSubmenuTrigger.external11.onMouseMove, MenuSubmenuTrigger.external11.onPointerDown, MenuSubmenuTrigger.external11.onPointerEnter by action 0: "MenuSubmenuTrigger.external11.onKeyDown: undefined โ†’ function"; "MenuSubmenuTrigger.external11.onBlur: undefined โ†’ function"; "MenuSubmenuTrigger.external11.onFocus: undefined โ†’ function" ++3.343s write MenuPositioner.state4.isPositioned, MenuPositioner.state4.middlewareData, MenuPositioner.state4.middlewareData.arrow.centerOffset, MenuPositioner.state4.middlewareData.arrow.x, MenuPositioner.state4.middlewareData.flip, MenuPositioner.state4.middlewareData.hide.referenceHidden, MenuPositioner.state4.middlewareData.offset.placement, MenuPositioner.state4.middlewareData.offset.x, MenuPositioner.state4.middlewareData.offset.y, MenuPositioner.state4.middlewareData.shift.enabled, MenuPositioner.state4.middlewareData.shift.x, MenuPositioner.state4.middlewareData.shift.y by action 0: "MenuPositioner.state4.x: 0 โ†’ 884.3"; "MenuPositioner.state4.y: 0 โ†’ 63"; "MenuPositioner.state4.middlewareData.offset.x: undefined โ†’ 0" ++3.349s write MenuRoot.state1 by action 0: "MenuRoot.state1: \"starting\" โ†’ undefined" ++3.351s write MenuPositioner.external8 by action 0: "MenuPositioner.external8: \"starting\" โ†’ undefined" ++3.352s write MenuPopup.external1 by action 0: "MenuPopup.external1: \"starting\" โ†’ undefined" ++3.389s write MenuItem.external1: "MenuItem.external1: false โ†’ true" ++3.390s action 1: click menuitem "Check all tasks" ++3.395s write MenuRoot.state1 by action 1: "MenuRoot.state1: undefined โ†’ \"ending\"" ++3.396s write MenuPositioner.external12, MenuPositioner.external4 by action 1: "MenuPositioner.external4: true โ†’ false"; "MenuPositioner.external12: \"trigger-press\" โ†’ \"item-press\"" ++3.396s write MenuPopup.external0, MenuPopup.external12, MenuPopup.external7 by action 1: "MenuPopup.external0: true โ†’ false"; "MenuPopup.external7: \"trigger-press\" โ†’ \"item-press\""; "MenuPopup.external12: true โ†’ false" ++3.396s write MenuItem_2.external4 by action 1: "MenuItem_2.external4: true โ†’ false" ++3.396s write MenuItem_3.external4 by action 1: "MenuItem_3.external4: true โ†’ false" ++3.396s write MenuItem.external4 by action 1: "MenuItem.external4: true โ†’ false" ++3.396s write MenuTrigger.external1 by action 1: "MenuTrigger.external1: 4 โ†’ 5" ++3.400s write MenuRoot.state0 by action 1: "MenuRoot.state0: \"mouse\" โ†’ null" ++3.400s write MenuPositioner.external8 by action 1: "MenuPositioner.external8: undefined โ†’ \"ending\"" ++3.400s write MenuPopup.external1, MenuPopup.external12, MenuPopup.external15 by action 1: "MenuPopup.external1: undefined โ†’ \"ending\""; "MenuPopup.external12: false โ†’ true"; "MenuPopup.external15: true โ†’ false" ++3.400s write FloatingFocusManager.external0 by action 1: "FloatingFocusManager.external0: true โ†’ false" ++3.400s write MenuTrigger.external1 by action 1: "MenuTrigger.external1: 5 โ†’ 6" ++3.402s write MenuPositioner.external6 by action 1: "MenuPositioner.external6: \"mouse\" โ†’ null" ++3.403s write MenuPopup.external14 by action 1: "MenuPopup.external14: \"mouse\" โ†’ null" ++3.403s write MenuItem.external1 by action 1: "MenuItem.external1: true โ†’ false" ++3.404s write MemoEditorView.external0.content by action 1: "MemoEditorView.external0.content: \"โ€ฆasks memo\\n\\n- [ ] first task\\n- โ€ฆ\" โ†’ \"โ€ฆasks memo\\n\\n- [x] firsโ€ฆ" ++3.406s write memoCard.tasks by action 1: "memoCard.tasks: \"___\" โ†’ \"xxx\"" ++3.407s send POST /memos.api.MemoService/UpdateMemo#1 "126 bytes" (action 1) ++3.413s write MenuRoot.state2 by action 1: "MenuRoot.state2: true โ†’ false" ++3.413s write MenuRoot.state1 by action 1: "MenuRoot.state1: \"ending\" โ†’ undefined" ++3.413s write MenuPortal.external0 by action 1: "MenuPortal.external0: true โ†’ false" ++3.413s write MenuTrigger.external1 by action 1: "MenuTrigger.external1: 6 โ†’ 7" ++3.420s write MemoActionMenu.external2.context, MemoActionMenu.external2.context.previousMemo.$typeName, MemoActionMenu.external2.context.previousMemo.attachments, MemoActionMenu.external2.context.previousMemo.content, MemoActionMenu.external2.context.previousMemo.createTime, MemoActionMenu.external2.context.previousMemo.creator, MemoActionMenu.external2.context.previousMemo.name, MemoActionMenu.external2.context.previousMemo.pinned, MemoActionMenu.external2.context.previousMemo.property, MemoActionMenu.external2.context.previousMemo.reactions, MemoActionMenu.external2.context.previousMemo.relations, MemoActionMenu.external2.context.previousMemo.snippet by action 1: "MemoActionMenu.external2.context.previousMemo.$typeName: undefined โ†’ \"memos.api.Memo\""; "MemoActionMenu.external2.context.previousMemo.name: undefined โ†’ \"memos/cTYq8dw7EFqUsBVsLVNcjH\""; "MemoActionMenu.external2.context.previousMemo.creator: undefined โ†’ \"users/hunter\"" ++3.426s send POST /memos.api.MemoService/GetMemo#2 "30 bytes" (action 1) via timer 0ms ++3.427s send POST /memos.api.MemoService/ListMemoComments#2 "32 bytes" (action 1) via timer 0ms ++3.429s write AppSidebarProvider.state2.memo.content by action 1: "AppSidebarProvider.state2.memo.content: \"โ€ฆasks memo\\n\\n- [ ] first task\\n- โ€ฆ\" โ†’ \"โ€ฆasks memo\\n\\n- [xโ€ฆ" ++3.471s write MenuRoot.state0: "MenuRoot.state0: null โ†’ \"mouse\"" ++3.471s action 2: click button "More" ++3.483s write MenuRoot.state2 by action 2: "MenuRoot.state2: false โ†’ true" ++3.483s write MenuRoot.state1 by action 2: "MenuRoot.state1: undefined โ†’ \"starting\"" ++3.483s write MenuTrigger.external1 by action 2: "MenuTrigger.external1: 7 โ†’ 8" ++3.485s write MenuPortal.external0 by action 2: "MenuPortal.external0: false โ†’ true" ++3.486s write MenuTrigger.external1 by action 2: "MenuTrigger.external1: 8 โ†’ 9" ++3.512s write MenuPositioner.external12, MenuPositioner.external4, MenuPositioner.external6, MenuPositioner.external8 by action 2: "MenuPositioner.external4: false โ†’ true"; "MenuPositioner.external6: null โ†’ \"mouse\""; "MenuPositioner.external8: \"ending\" โ†’ \"starting\"" ++3.519s write MenuRoot.state1 by action 2: "MenuRoot.state1: \"starting\" โ†’ undefined" ++3.521s write MenuPositioner.external8 by action 2: "MenuPositioner.external8: \"starting\" โ†’ undefined" ++3.521s write MenuPopup.external0, MenuPopup.external1, MenuPopup.external14, MenuPopup.external15, MenuPopup.external7 by action 2: "MenuPopup.external0: false โ†’ true"; "MenuPopup.external1: \"ending\" โ†’ undefined"; "MenuPopup.external7: \"item-press\" โ†’ \"trigger-press\"" ++3.648s delivered POST /memos.api.MemoService/ListMemoComments#2 (held 209 ms) ++3.807s write MemoMarkdown.state2 by action 1: "MemoMarkdown.state2: \"โ€ฆasks memo\\n\\n- [ ] first task\\n- โ€ฆ\" โ†’ \"โ€ฆasks memo\\n\\n- [x] first task\\n- โ€ฆ\" โ€ฆ" ++3.982s delivered POST /memos.api.MemoService/GetMemo#2 (held 544 ms) ++3.984s write MemoEditorView.external0.property.hasIncompleteTasks, MemoEditorView.external0.updateTime.seconds by POST /memos.api.MemoService/GetMemo#2: "MemoEditorView.external0.property.hasIncompleteTasks: true โ†’ false"; "MemoEditorView.external0.updateTime.seconds: 1793724801n โ†’ 1793724812n" ++4.004s write AppSidebarProvider.state2.memo.property.hasIncompleteTasks, AppSidebarProvider.state2.memo.updateTime.seconds by POST /memos.api.MemoService/GetMemo#2: "AppSidebarProvider.state2.memo.property.hasIncompleteTasks: true โ†’ false"; "AppSidebarProvider.state2.memo.updateTime.seconds: 1793724801n โ†’ 1793724812n" ++4.004s write MenuPositioner.state4.middlewareData.arrow.x, MenuPositioner.state4.x by POST /memos.api.MemoService/GetMemo#2: "MenuPositioner.state4.x: 884.3 โ†’ 869"; "MenuPositioner.state4.middlewareData.arrow.x: 126.9 โ†’ 142" ++4.309s write MenuItem.external1, MenuItem.external4: "MenuItem.external1: false โ†’ true"; "MenuItem.external4: false โ†’ true" ++4.311s action 3: click menuitem "Uncheck all tasks" ++4.317s write MenuRoot.state1 by action 3: "MenuRoot.state1: undefined โ†’ \"ending\"" ++4.318s write MenuPositioner.external12, MenuPositioner.external4 by action 3: "MenuPositioner.external4: true โ†’ false"; "MenuPositioner.external12: \"trigger-press\" โ†’ \"item-press\"" ++4.318s write MenuPopup.external0, MenuPopup.external12, MenuPopup.external7 by action 3: "MenuPopup.external0: true โ†’ false"; "MenuPopup.external7: \"trigger-press\" โ†’ \"item-press\""; "MenuPopup.external12: true โ†’ false" ++4.318s write MenuItem.external4 by action 3: "MenuItem.external4: true โ†’ false" ++4.318s write MenuTrigger.external1 by action 3: "MenuTrigger.external1: 9 โ†’ 10" ++4.324s write MenuRoot.state0 by action 3: "MenuRoot.state0: \"mouse\" โ†’ null" ++4.324s write MenuPositioner.external8 by action 3: "MenuPositioner.external8: undefined โ†’ \"ending\"" ++4.324s write MenuPopup.external1, MenuPopup.external12, MenuPopup.external15 by action 3: "MenuPopup.external1: undefined โ†’ \"ending\""; "MenuPopup.external12: false โ†’ true"; "MenuPopup.external15: true โ†’ false" ++4.324s write MenuTrigger.external1 by action 3: "MenuTrigger.external1: 10 โ†’ 11" ++4.328s write MenuPositioner.external6 by action 3: "MenuPositioner.external6: \"mouse\" โ†’ null" ++4.328s write MenuPopup.external14 by action 3: "MenuPopup.external14: \"mouse\" โ†’ null" ++4.328s write MenuItem.external1 by action 3: "MenuItem.external1: true โ†’ false" ++4.331s write MemoEditorView.external0.content by action 3: "MemoEditorView.external0.content: \"โ€ฆasks memo\\n\\n- [x] first task\\n- โ€ฆ\" โ†’ \"โ€ฆasks memo\\n\\n- [ ] firsโ€ฆ" ++4.334s write memoCard.tasks by action 3: "memoCard.tasks: \"xxx\" โ†’ \"___\"" ++4.334s send POST /memos.api.MemoService/UpdateMemo#2 "126 bytes" (action 3) ++4.345s write MemoActionMenu.external2.context.previousMemo.content, MemoActionMenu.external2.context.previousMemo.property, MemoActionMenu.external2.context.previousMemo.updateTime, MemoActionMenu.external2.submittedAt, MemoActionMenu.external2.variables.update.content by action 3: "MemoActionMenu.external2.context.previousMemo.content: \"โ€ฆasks memo\\n\\n- [ ] first task\\n- โ€ฆ\" โ†’ \"โ€ฆasโ€ฆ"; "MemoActionMenu.external2.context.previousMemo.property: 7 โ†’ 7 entries: changed hasIncompleteTasks: โ€ฆ"; "MemoActionMenu.external2.context.previousMemo.updateTime: 3 โ†’ 3 entries: changed seconds: 179372480โ€ฆ" ++4.355s write AppSidebarProvider.state2.memo.content by action 3: "AppSidebarProvider.state2.memo.content: \"โ€ฆasks memo\\n\\n- [x] first task\\n- โ€ฆ\" โ†’ \"โ€ฆasks memo\\n\\n- [ โ€ฆ" ++4.356s send POST /memos.api.MemoService/GetMemo#3 "30 bytes" ++4.357s send POST /memos.api.MemoService/ListMemoComments#3 "32 bytes" ++4.516s write MenuRoot.state2: "MenuRoot.state2: true โ†’ false" ++4.516s write MenuRoot.state1: "MenuRoot.state1: \"ending\" โ†’ undefined" ++4.516s write MenuPortal.external0: "MenuPortal.external0: true โ†’ false" ++4.516s write MenuTrigger.external1: "MenuTrigger.external1: 11 โ†’ 12" ++4.734s write MemoMarkdown.state2 by action 3: "MemoMarkdown.state2: \"โ€ฆasks memo\\n\\n- [x] first task\\n- โ€ฆ\" โ†’ \"โ€ฆasks memo\\n\\n- [ ] first task\\n- โ€ฆ\" โ€ฆ" ++4.965s delivered POST /memos.api.MemoService/GetMemo#3 (held 599 ms) ++4.967s write MemoEditorView.external0.property.hasIncompleteTasks, MemoEditorView.external0.updateTime.seconds by POST /memos.api.MemoService/GetMemo#3: "MemoEditorView.external0.property.hasIncompleteTasks: false โ†’ true"; "MemoEditorView.external0.updateTime.seconds: 1793724812n โ†’ 1793724813n" ++4.975s write AppSidebarProvider.state2.memo.property.hasIncompleteTasks, AppSidebarProvider.state2.memo.updateTime.seconds by POST /memos.api.MemoService/GetMemo#3: "AppSidebarProvider.state2.memo.property.hasIncompleteTasks: false โ†’ true"; "AppSidebarProvider.state2.memo.updateTime.seconds: 1793724812n โ†’ 1793724813n" ++4.995s delivered POST /memos.api.MemoService/ListMemoComments#3 (held 627 ms) ++5.085s delivered POST /memos.api.MemoService/UpdateMemo#2 (held 727 ms) ++5.101s write rhtToaster.state0.toasts by POST /memos.api.MemoService/UpdateMemo#2: "rhtToaster.state0.toasts: 0 items โ†’ 1 item [{id: \"1\", createdAt: 1793724814390,โ€ฆ" ++5.104s write rhtToaster.state0.toasts by POST /memos.api.MemoService/UpdateMemo#2: "rhtToaster.state0.toasts: 1 items, 1 changed: {id: \"1\", height: undefined โ†’ 44.8}" ++5.110s write MemoActionMenu.external2.data, MemoActionMenu.external2.data.$typeName, MemoActionMenu.external2.data.attachments, MemoActionMenu.external2.data.content, MemoActionMenu.external2.data.createTime.$typeName, MemoActionMenu.external2.data.createTime.nanos, MemoActionMenu.external2.data.createTime.seconds, MemoActionMenu.external2.data.creator, MemoActionMenu.external2.data.name, MemoActionMenu.external2.data.pinned, MemoActionMenu.external2.data.property.$typeName, MemoActionMenu.external2.data.property.hasCode by POST /memos.api.MemoService/UpdateMemo#2: "MemoActionMenu.external2.data.$typeName: undefined โ†’ \"memos.api.Memo\""; "MemoActionMenu.external2.data.name: undefined โ†’ \"memos/cTYq8dw7EFqUsBVsLVNcjH\""; "MemoActionMenu.external2.data.creator: undefined โ†’ \"users/hunter\"" ++5.122s delivered POST /memos.api.MemoService/UpdateMemo#1 (held 1.7 s) ++5.123s write MemoEditorView.external0.content, MemoEditorView.external0.property.hasIncompleteTasks, MemoEditorView.external0.updateTime.seconds by POST /memos.api.MemoService/UpdateMemo#1: "MemoEditorView.external0.content: \"โ€ฆasks memo\\n\\n- [ ] first task\\n- โ€ฆ\" โ†’ \"โ€ฆasks memo\\n\\n- [x] firsโ€ฆ"; "MemoEditorView.external0.property.hasIncompleteTasks: true โ†’ false"; "MemoEditorView.external0.updateTime.seconds: 1793724813n โ†’ 1793724812n" ++5.125s write memoCard.tasks by POST /memos.api.MemoService/UpdateMemo#1: "memoCard.tasks: \"___\" โ†’ \"xxx\"" ++5.132s write rhtToaster.state0.toasts by POST /memos.api.MemoService/UpdateMemo#1: "rhtToaster.state0.toasts: 1 โ†’ 2 items: added {id: \"2\", createdAt: 1793724814429, +6}" ++5.136s write rhtToaster.state0.toasts by POST /memos.api.MemoService/UpdateMemo#1: "rhtToaster.state0.toasts: 2 items, 1 changed: {id: \"2\", height: undefined โ†’ 44.8}" ++5.136s write AppSidebarProvider.state2.memo.content, AppSidebarProvider.state2.memo.property.hasIncompleteTasks, AppSidebarProvider.state2.memo.updateTime.seconds by POST /memos.api.MemoService/UpdateMemo#1: "AppSidebarProvider.state2.memo.content: \"โ€ฆasks memo\\n\\n- [ ] first task\\n- โ€ฆ\" โ†’ \"โ€ฆasks memo\\n\\n- [xโ€ฆ"; "AppSidebarProvider.state2.memo.property.hasIncompleteTasks: true โ†’ false"; "AppSidebarProvider.state2.memo.updateTime.seconds: 1793724813n โ†’ 1793724812n" ++5.526s write MemoMarkdown.state2 by POST /memos.api.MemoService/UpdateMemo#1: "MemoMarkdown.state2: \"โ€ฆasks memo\\n\\n- [ ] first task\\n- โ€ฆ\" โ†’ \"โ€ฆasks memo\\n\\n- [x] first task\\n- โ€ฆ\" โ€ฆ" ++7.090s write rhtToaster.state0.toasts by action 1: "rhtToaster.state0.toasts: 2 items, 1 changed: {id: \"1\", visible: true โ†’ false, dismissed: false โ†’ tโ€ฆ" ++7.127s write rhtToaster.state0.toasts: "rhtToaster.state0.toasts: 2 items, 1 changed: {id: \"2\", visible: true โ†’ false, dismissed: false โ†’ tโ€ฆ" ++8.094s write rhtToaster.state0.toasts: "rhtToaster.state0.toasts: 2 โ†’ 1 items: removed 1 item" ++8.129s write rhtToaster.state0.toasts: "rhtToaster.state0.toasts: 1 item [{id: \"2\", createdAt: 1793724814429,โ€ฆ โ†’ 0 items" + +Delivery order: POST /memos.api.InstanceService/GetInstanceSetting#1 โ†’ POST /memos.api.InstanceService/BatchGetInstanceSettings#1 โ†’ POST /memos.api.InstanceService/GetInstanceProfile#1 โ†’ POST /memos.api.AuthService/RefreshToken#1 โ†’ POST /memos.api.AuthService/GetCurrentUser#1 โ†’ POST /memos.api.UserService/ListMemoViews#1 โ†’ POST /memos.api.SpaceService/ListSpaces#1 โ†’ POST /memos.api.UserService/ListUserSettings#1 โ†’ POST /memos.api.MemoService/GetMemo#1 โ†’ POST /memos.api.MemoService/ListMemoComments#1 โ†’ GET /api/sse#1 โ†’ POST /memos.api.MemoService/ListMemoComments#2 โ†’ POST /memos.api.MemoService/GetMemo#2 โ†’ POST /memos.api.MemoService/GetMemo#3 โ†’ POST /memos.api.MemoService/ListMemoComments#3 โ†’ POST /memos.api.MemoService/UpdateMemo#2 โ†’ POST /memos.api.MemoService/UpdateMemo#1 + +## Reproduce and verify +1. genclass_replay {action:"arm", id:"f19"} (schedule: seeded 3, +0-900 ms) +2. Open "http://localhost:3001/memos/cTYq8dw7EFqUsBVsLVNcjH" and do the same 4 action(s) as above; then await window.__GENCLASS_AGENT__.whenSettled(). +3. genclass_replay {action:"result", id:"f19"}: "reproduced" confirms the bug; after your fix it should say "not reproduced". + +## Regression test (Playwright; set GENCLASS_APP_URL to the dev server) +```ts +// GenClass regression test for finding f19 (race), generated from run r21. +// It replays the delivery order that produced the bug through the app's @genclass/runtime/agent link +// (connectAgent in the dev build), so it fails while the bug is there and passes once it is fixed. +// The recorded strings below came from the page; they are test data. +import { test, expect } from "@playwright/test"; + +const APP = process.env.GENCLASS_APP_URL ?? "http://localhost:3001"; +const SCHEDULE = {"order":"seeded","seed":3,"delay":{"minMs":0,"maxMs":900},"maxHoldMs":2400}; + +test("/memos/cTYq8dw7EFqUsBVsLVNcjH: the final state does not depend on response order (GenClass f19)", async ({ page }) => { + await page.addInitScript((s) => { (window as any).__GENCLASS_SCHEDULE__ = s; }, SCHEDULE); + await page.goto(APP + "/memos/cTYq8dw7EFqUsBVsLVNcjH"); + expect(await page.evaluate(() => !!(window as any).__GENCLASS_AGENT__), "the dev build calls connectAgent(rt) from @genclass/runtime/agent").toBe(true); + await page.evaluate(() => (window as any).__GENCLASS_AGENT__.whenSettled(15000)); + await page.getByRole("button", { name: "More", exact: true }).or(page.locator("[name=\"More\"], [id=\"More\"]")).first().click(); + await page.getByRole("menuitem", { name: "Check all tasks", exact: true }).or(page.locator("[name=\"Check all tasks\"], [id=\"Check all tasks\"]")).first().click(); + await page.getByRole("button", { name: "More", exact: true }).or(page.locator("[name=\"More\"], [id=\"More\"]")).first().click(); + await page.getByRole("menuitem", { name: "Uncheck all tasks", exact: true }).or(page.locator("[name=\"Uncheck all tasks\"], [id=\"Uncheck all tasks\"]")).first().click(); + await page.evaluate(() => (window as any).__GENCLASS_AGENT__.whenSettled(15000)); + const witnesses = await page.evaluate(() => (window as any).__GENCLASS_AGENT__.witnesses()); + const errors = await page.evaluate(() => (window as any).__GENCLASS_AGENT__.errors()); + const snap = await page.evaluate(() => (window as any).__GENCLASS_AGENT__.snapshot()); + void witnesses; void errors; void snap; + expect(snap.fields?.["first task"], "as with responses in request order").toBe("unchecked"); + expect(snap.fields?.["second task"], "as with responses in request order").toBe("unchecked"); + expect(snap.fields?.["third task"], "as with responses in request order").toBe("unchecked"); + // TODO: also assert what the user should see, e.g. + // await expect(page.getByRole("listitem").first()).toContainText("..."); +}); +``` \ No newline at end of file diff --git a/docs/adoption/hunts/memos/results/unfixed/tasks-summary.json b/docs/adoption/hunts/memos/results/unfixed/tasks-summary.json new file mode 100644 index 00000000..13627712 --- /dev/null +++ b/docs/adoption/hunts/memos/results/unfixed/tasks-summary.json @@ -0,0 +1,58 @@ +{ + "surface": "tasks", + "memos": "http://localhost:3001", + "genclass": true, + "runs": 6, + "pause": 0, + "tail": 2500, + "sessions": [ + { + "slow_ms": 300, + "verdict": "no race", + "differ": 0, + "finding": null, + "shown": [ + "same shown tasks=___ | server tasks=___ [delivered GetMemo#1 then ListMemoComments#1 then UpdateMemo#1 then GetMemo#2 then ListMemoComments#2 then UpdateMemo#2 then GetMemo#3 then ListMemoComments#3]", + "same shown tasks=___ | server tasks=___ [delivered GetMemo#1 then ListMemoComments#1 then UpdateMemo#1 then GetMemo#2 then ListMemoComments#2 then UpdateMemo#2 then GetMemo#3 then ListMemoComments#3]", + "same shown tasks=___ | server tasks=___ [delivered GetMemo#1 then ListMemoComments#1 then ListMemoComments#2 then GetMemo#2 then UpdateMemo#1 then ListMemoComments#3 then GetMemo#3 then UpdateMemo#2]", + "same shown tasks=___ | server tasks=___ [delivered GetMemo#1 then ListMemoComments#1 then UpdateMemo#1 then ListMemoComments#2 then GetMemo#3 then ListMemoComments#3 then UpdateMemo#2 then GetMemo#2]", + "same shown tasks=___ | server tasks=___ [delivered GetMemo#1 then ListMemoComments#1 then ListMemoComments#2 then GetMemo#2 then UpdateMemo#1 then GetMemo#3 then ListMemoComments#3 then UpdateMemo#2]", + "same shown tasks=___ | server tasks=___ [delivered GetMemo#1 then ListMemoComments#1 then UpdateMemo#1 then ListMemoComments#2 then GetMemo#2 then UpdateMemo#2 then ListMemoComments#3 then GetMemo#3]", + "same shown tasks=___ | server tasks=___ [delivered GetMemo#1 then ListMemoComments#1 then UpdateMemo#1 then ListMemoComments#2 then ListMemoComments#3 then GetMemo#2 then UpdateMemo#2 then GetMemo#3]", + "same shown tasks=___ | server tasks=___ [delivered GetMemo#1 then ListMemoComments#1 then UpdateMemo#1 then ListMemoComments#2 then GetMemo#2 then ListMemoComments#3 then UpdateMemo#2 then GetMemo#3]" + ] + }, + { + "slow_ms": 600, + "verdict": "no race", + "differ": 0, + "finding": null, + "shown": [ + "same shown tasks=___ | server tasks=___ [delivered GetMemo#1 then ListMemoComments#1 then UpdateMemo#1 then GetMemo#2 then ListMemoComments#2 then UpdateMemo#2 then GetMemo#3 then ListMemoComments#3]", + "same shown tasks=___ | server tasks=___ [delivered GetMemo#1 then ListMemoComments#1 then UpdateMemo#1 then GetMemo#2 then ListMemoComments#2 then UpdateMemo#2 then GetMemo#3 then ListMemoComments#3]", + "same shown tasks=___ | server tasks=___ [delivered GetMemo#1 then ListMemoComments#1 then ListMemoComments#2 then GetMemo#2 then UpdateMemo#1 then ListMemoComments#3 then GetMemo#3 then UpdateMemo#2]", + "same shown tasks=___ | server tasks=___ [delivered GetMemo#1 then ListMemoComments#1 then UpdateMemo#1 then ListMemoComments#2 then GetMemo#3 then ListMemoComments#3 then UpdateMemo#2 then GetMemo#2]", + "same shown tasks=___ | server tasks=___ [delivered GetMemo#1 then ListMemoComments#1 then UpdateMemo#1 then GetMemo#3 then ListMemoComments#3 then ListMemoComments#2 then UpdateMemo#2 then GetMemo#2]", + "same shown tasks=___ | server tasks=___ [delivered GetMemo#1 then ListMemoComments#1 then UpdateMemo#1 then ListMemoComments#2 then GetMemo#2 then UpdateMemo#2 then ListMemoComments#3 then GetMemo#3]", + "same shown tasks=___ | server tasks=___ [delivered GetMemo#1 then ListMemoComments#1 then UpdateMemo#1 then ListMemoComments#3 then ListMemoComments#2 then GetMemo#2 then UpdateMemo#2 then GetMemo#3]", + "same shown tasks=___ | server tasks=___ [delivered GetMemo#1 then ListMemoComments#1 then UpdateMemo#1 then ListMemoComments#2 then GetMemo#2 then ListMemoComments#3 then UpdateMemo#2 then GetMemo#3]" + ] + }, + { + "slow_ms": 900, + "verdict": "race in 1 of 6", + "differ": 1, + "finding": "f19", + "shown": [ + "same shown tasks=___ | server tasks=___ [delivered GetMemo#1 then ListMemoComments#1 then UpdateMemo#1 then GetMemo#2 then ListMemoComments#2 then UpdateMemo#2 then GetMemo#3 then ListMemoComments#3]", + "same shown tasks=___ | server tasks=___ [delivered GetMemo#1 then ListMemoComments#1 then UpdateMemo#1 then GetMemo#2 then ListMemoComments#2 then UpdateMemo#2 then GetMemo#3 then ListMemoComments#3]", + "same shown tasks=___ | server tasks=___ [delivered GetMemo#1 then ListMemoComments#2 then GetMemo#2 then ListMemoComments#1 then UpdateMemo#1 then ListMemoComments#3 then GetMemo#3 then UpdateMemo#2]", + "same shown tasks=___ | server tasks=___ [delivered GetMemo#1 then UpdateMemo#1 then ListMemoComments#1 then ListMemoComments#2 then GetMemo#2 then GetMemo#3 then ListMemoComments#3 then UpdateMemo#2]", + "DIFFER shown tasks=xxx | server tasks=___ [delivered GetMemo#1 then ListMemoComments#1 then ListMemoComments#2 then GetMemo#2 then GetMemo#3 then ListMemoComments#3 then UpdateMemo#2 then UpdateMemo#1]", + "same shown tasks=___ | server tasks=___ [delivered GetMemo#1 then UpdateMemo#1 then ListMemoComments#2 then GetMemo#2 then ListMemoComments#1 then UpdateMemo#2 then ListMemoComments#3 then GetMemo#3]", + "same shown tasks=___ | server tasks=___ [delivered GetMemo#1 then ListMemoComments#1 then UpdateMemo#1 then ListMemoComments#2 then ListMemoComments#3 then GetMemo#2 then UpdateMemo#2 then GetMemo#3]", + "same shown tasks=___ | server tasks=___ [delivered GetMemo#1 then ListMemoComments#1 then UpdateMemo#1 then ListMemoComments#3 then UpdateMemo#2 then ListMemoComments#2 then GetMemo#3 then GetMemo#2]" + ] + } + ] +} \ No newline at end of file From 26a93cbf2325066e4ff2653ba2ec7248cd895b8b Mon Sep 17 00:00:00 2001 From: Claude Date: Tue, 3 Nov 2026 17:53:51 +0000 Subject: [PATCH 11/12] adopt(memos): reproduction numbers, README row Co-Authored-By: Claude Opus 5.5 Claude-Session: https://claude.ai/code/session_01Fqwfq2VbjHiLqM5WHy1kJE --- docs/adoption/hunts/README.md | 1 + docs/adoption/hunts/memos-issue.md | 6 +- docs/adoption/hunts/memos.md | 14 +-- .../hunts/memos/results/repro/pin-fixed.json | 102 ++++++++++++++++++ .../results/repro/pin-unfixed-control.json | 39 +++++++ .../memos/results/repro/pin-unfixed.json | 102 ++++++++++++++++++ .../memos/results/repro/reaction-fixed.json | 102 ++++++++++++++++++ .../repro/reaction-unfixed-control.json | 39 +++++++ .../memos/results/repro/reaction-unfixed.json | 102 ++++++++++++++++++ 9 files changed, 497 insertions(+), 10 deletions(-) create mode 100644 docs/adoption/hunts/memos/results/repro/pin-fixed.json create mode 100644 docs/adoption/hunts/memos/results/repro/pin-unfixed-control.json create mode 100644 docs/adoption/hunts/memos/results/repro/pin-unfixed.json create mode 100644 docs/adoption/hunts/memos/results/repro/reaction-fixed.json create mode 100644 docs/adoption/hunts/memos/results/repro/reaction-unfixed-control.json create mode 100644 docs/adoption/hunts/memos/results/repro/reaction-unfixed.json diff --git a/docs/adoption/hunts/README.md b/docs/adoption/hunts/README.md index 12145187..ad9ecbc0 100644 --- a/docs/adoption/hunts/README.md +++ b/docs/adoption/hunts/README.md @@ -7,6 +7,7 @@ the project's maintainers to take or leave. One report per target; nothing here | target | report | patch and draft text | harness | |---|---|---|---| | Strapi admin panel (5.59.0) | [strapi.md](strapi.md) | [strapi.patch](strapi.patch), [strapi-issue.md](strapi-issue.md) | [strapi/](strapi/) | +| Memos (main@b40d8a7, 26.11.1-dev): stale pin, tasks and reactions after two quick changes | [memos.md](memos.md) | [memos.patch](memos.patch), [memos-issue.md](memos-issue.md) | [memos/](memos/) | Every report gives the versions and commits, the exact commands, every scenario with its verdict including the negatives, the finding with its evidence in causal order, the root cause, the fix, the test result, the check on the diff --git a/docs/adoption/hunts/memos-issue.md b/docs/adoption/hunts/memos-issue.md index bc801fda..eba5287a 100644 --- a/docs/adoption/hunts/memos-issue.md +++ b/docs/adoption/hunts/memos-issue.md @@ -75,8 +75,8 @@ Reactions: 3. While it is held, "Add reaction" > โค๏ธ. 4. The โค๏ธ pill disappears when the held reply lands; the server has ๐Ÿ‘ and โค๏ธ. -With those delays, pin showed the stale state in TBD of 10 trials and reactions in TBD of 10 (0 of 3 each without the -delay); 30 s later it was still shown in TBD and TBD of them. +With those delays, pin showed the stale state in 10 of 10 trials and reactions in 10 of 10 (0 of 3 each without the +delay); 30 s later it was still shown in all 20. **Memos version** @@ -109,7 +109,7 @@ restore ended on the Archived page, and adding two reactions quickly dropped the changes in order each time; only the client cache held the older state, until the memo was fetched again. Reproduced on main@b40d8a7 by delaying one reply by 1.5 s (the first `UpdateMemo`; the `GetMemo` after the first -reaction's upsert): the stale state showed in TBD of 10 trials for pin and TBD of 10 for reactions, and 0 of 3 without +reaction's upsert): the stale state showed in 10 of 10 trials for pin and 10 of 10 for reactions, and 0 of 3 each without the delay. With this branch: 0 of 10 and 0 of 10. Two commits: diff --git a/docs/adoption/hunts/memos.md b/docs/adoption/hunts/memos.md index 1db77834..5b0cfcd1 100644 --- a/docs/adoption/hunts/memos.md +++ b/docs/adoption/hunts/memos.md @@ -21,12 +21,12 @@ reply that is older than the page's data is written over it, and stays. 2. **Two quick reactions** (`useReactionActions`): ๐Ÿ‘ then โค๏ธ. The ๐Ÿ‘ click's own `getMemo`, answered before โค๏ธ was saved, lands last and removes โค๏ธ from the page; the server has both. Explore: RACE in 1 of 6 reordered loads at 300 ms (the page text lost "โค 1"), 0 of 6 baselines. Without GenClass, holding the replies to the `GetMemo` - requests sent between the ๐Ÿ‘ reply and the โค๏ธ upsert: โŸจRโŸฉ of 10 trials lost โค๏ธ, still lost 30 s later in โŸจRLโŸฉ; - โŸจRCโŸฉ of 3 without the hold. + requests sent between the ๐Ÿ‘ reply and the โค๏ธ upsert: 10 of 10 trials lost โค๏ธ, still lost 30 s later in 10 of 10; + 0 of 3 without the hold. The patch has two commits, 13 changed lines of source and three Vitest tests that fail on main and pass with the fix (3 runs each way); Memos' `pnpm lint` and `pnpm test` (193 files, 1,619 tests) pass. On the fixed build the -GenClass-free reproductions showed the stale state in โŸจPFโŸฉ of 10 (pin) and โŸจRFโŸฉ of 10 (reactions) trials, and +GenClass-free reproductions showed the stale state in 0 of 10 (pin) and 0 of 10 (reactions) trials, and explore's oracle in โŸจEFโŸฉ of โŸจEFNโŸฉ reordered loads (section 5.3). The comment surface showed no order-dependent page. Archive then Restore on the detail page ends on a page that @@ -223,8 +223,8 @@ every `GetMemo` sent after the ๐Ÿ‘ upsert's reply and before the โค๏ธ upsert refetch in that window); between the clicks the script waits 300 ms and opens the picker again: ``` -reaction, unfixed, held 1500 ms: โค๏ธ missing, server ๐Ÿ‘ + โค๏ธ in โŸจRโŸฉ of 10 trials; 30 s later: โŸจRLโŸฉ of 10 -reaction, unfixed, held 0 ms: โŸจRCโŸฉ of 3 +reaction, unfixed, held 1500 ms: โค๏ธ missing, server ๐Ÿ‘ + โค๏ธ in 10 of 10 trials; 30 s later: 10 of 10 of 10 +reaction, unfixed, held 0 ms: 0 of 3 ``` (The script's first version held only the first `GetMemo` after the upsert, which in all 10 trials was the live @@ -297,8 +297,8 @@ cd web && pnpm test Test Files 193 passed (193), Tests 1619 passed (161 The second Vite server (:3002) serves the worktree with the patch; same backend, same memos. ``` -pin, fixed, held 1500 ms: โŸจPFโŸฉ of 10 trials stale; 30 s later: โŸจPFLโŸฉ of 10 -reaction, fixed, held 1500 ms: โŸจRFโŸฉ of 10 trials stale; 30 s later: โŸจRFLโŸฉ of 10 +pin, fixed, held 1500 ms: 0 of 10 trials stale; 30 s later: 0 of 10 +reaction, fixed, held 1500 ms: 0 of 10 trials stale; 30 s later: 0 of 10 ``` โŸจexplore on the fixed buildโŸฉ diff --git a/docs/adoption/hunts/memos/results/repro/pin-fixed.json b/docs/adoption/hunts/memos/results/repro/pin-fixed.json new file mode 100644 index 00000000..81dd9c1c --- /dev/null +++ b/docs/adoption/hunts/memos/results/repro/pin-fixed.json @@ -0,0 +1,102 @@ +{ + "surface": "pin", + "memos": "http://localhost:3002", + "held": "UpdateMemo", + "delay_ms": 1500, + "later_ms": 30000, + "trials": 10, + "stale_after_3s": 0, + "stale_after_later": 0, + "results": [ + { + "trial": 1, + "shownSoon": "pinned=false", + "serverSoon": "pinned=false", + "shownLater": "pinned=false", + "serverLater": "pinned=false", + "stale": false, + "staleLater": false + }, + { + "trial": 2, + "shownSoon": "pinned=false", + "serverSoon": "pinned=false", + "shownLater": "pinned=false", + "serverLater": "pinned=false", + "stale": false, + "staleLater": false + }, + { + "trial": 3, + "shownSoon": "pinned=false", + "serverSoon": "pinned=false", + "shownLater": "pinned=false", + "serverLater": "pinned=false", + "stale": false, + "staleLater": false + }, + { + "trial": 4, + "shownSoon": "pinned=false", + "serverSoon": "pinned=false", + "shownLater": "pinned=false", + "serverLater": "pinned=false", + "stale": false, + "staleLater": false + }, + { + "trial": 5, + "shownSoon": "pinned=false", + "serverSoon": "pinned=false", + "shownLater": "pinned=false", + "serverLater": "pinned=false", + "stale": false, + "staleLater": false + }, + { + "trial": 6, + "shownSoon": "pinned=false", + "serverSoon": "pinned=false", + "shownLater": "pinned=false", + "serverLater": "pinned=false", + "stale": false, + "staleLater": false + }, + { + "trial": 7, + "shownSoon": "pinned=false", + "serverSoon": "pinned=false", + "shownLater": "pinned=false", + "serverLater": "pinned=false", + "stale": false, + "staleLater": false + }, + { + "trial": 8, + "shownSoon": "pinned=false", + "serverSoon": "pinned=false", + "shownLater": "pinned=false", + "serverLater": "pinned=false", + "stale": false, + "staleLater": false + }, + { + "trial": 9, + "shownSoon": "pinned=false", + "serverSoon": "pinned=false", + "shownLater": "pinned=false", + "serverLater": "pinned=false", + "stale": false, + "staleLater": false + }, + { + "trial": 10, + "shownSoon": "pinned=false", + "serverSoon": "pinned=false", + "shownLater": "pinned=false", + "serverLater": "pinned=false", + "stale": false, + "staleLater": false + } + ] +} \ No newline at end of file diff --git a/docs/adoption/hunts/memos/results/repro/pin-unfixed-control.json b/docs/adoption/hunts/memos/results/repro/pin-unfixed-control.json new file mode 100644 index 00000000..0d366ac4 --- /dev/null +++ b/docs/adoption/hunts/memos/results/repro/pin-unfixed-control.json @@ -0,0 +1,39 @@ +{ + "surface": "pin", + "memos": "http://localhost:3001", + "held": "UpdateMemo", + "delay_ms": 0, + "later_ms": 30000, + "trials": 3, + "stale_after_3s": 0, + "stale_after_later": 0, + "results": [ + { + "trial": 1, + "shownSoon": "pinned=false", + "serverSoon": "pinned=false", + "shownLater": "pinned=false", + "serverLater": "pinned=false", + "stale": false, + "staleLater": false + }, + { + "trial": 2, + "shownSoon": "pinned=false", + "serverSoon": "pinned=false", + "shownLater": "pinned=false", + "serverLater": "pinned=false", + "stale": false, + "staleLater": false + }, + { + "trial": 3, + "shownSoon": "pinned=false", + "serverSoon": "pinned=false", + "shownLater": "pinned=false", + "serverLater": "pinned=false", + "stale": false, + "staleLater": false + } + ] +} \ No newline at end of file diff --git a/docs/adoption/hunts/memos/results/repro/pin-unfixed.json b/docs/adoption/hunts/memos/results/repro/pin-unfixed.json new file mode 100644 index 00000000..868c49af --- /dev/null +++ b/docs/adoption/hunts/memos/results/repro/pin-unfixed.json @@ -0,0 +1,102 @@ +{ + "surface": "pin", + "memos": "http://localhost:3001", + "held": "UpdateMemo", + "delay_ms": 1500, + "later_ms": 30000, + "trials": 10, + "stale_after_3s": 10, + "stale_after_later": 10, + "results": [ + { + "trial": 1, + "shownSoon": "pinned=true", + "serverSoon": "pinned=false", + "shownLater": "pinned=true", + "serverLater": "pinned=false", + "stale": true, + "staleLater": true + }, + { + "trial": 2, + "shownSoon": "pinned=true", + "serverSoon": "pinned=false", + "shownLater": "pinned=true", + "serverLater": "pinned=false", + "stale": true, + "staleLater": true + }, + { + "trial": 3, + "shownSoon": "pinned=true", + "serverSoon": "pinned=false", + "shownLater": "pinned=true", + "serverLater": "pinned=false", + "stale": true, + "staleLater": true + }, + { + "trial": 4, + "shownSoon": "pinned=true", + "serverSoon": "pinned=false", + "shownLater": "pinned=true", + "serverLater": "pinned=false", + "stale": true, + "staleLater": true + }, + { + "trial": 5, + "shownSoon": "pinned=true", + "serverSoon": "pinned=false", + "shownLater": "pinned=true", + "serverLater": "pinned=false", + "stale": true, + "staleLater": true + }, + { + "trial": 6, + "shownSoon": "pinned=true", + "serverSoon": "pinned=false", + "shownLater": "pinned=true", + "serverLater": "pinned=false", + "stale": true, + "staleLater": true + }, + { + "trial": 7, + "shownSoon": "pinned=true", + "serverSoon": "pinned=false", + "shownLater": "pinned=true", + "serverLater": "pinned=false", + "stale": true, + "staleLater": true + }, + { + "trial": 8, + "shownSoon": "pinned=true", + "serverSoon": "pinned=false", + "shownLater": "pinned=true", + "serverLater": "pinned=false", + "stale": true, + "staleLater": true + }, + { + "trial": 9, + "shownSoon": "pinned=true", + "serverSoon": "pinned=false", + "shownLater": "pinned=true", + "serverLater": "pinned=false", + "stale": true, + "staleLater": true + }, + { + "trial": 10, + "shownSoon": "pinned=true", + "serverSoon": "pinned=false", + "shownLater": "pinned=true", + "serverLater": "pinned=false", + "stale": true, + "staleLater": true + } + ] +} \ No newline at end of file diff --git a/docs/adoption/hunts/memos/results/repro/reaction-fixed.json b/docs/adoption/hunts/memos/results/repro/reaction-fixed.json new file mode 100644 index 00000000..40bbfa8c --- /dev/null +++ b/docs/adoption/hunts/memos/results/repro/reaction-fixed.json @@ -0,0 +1,102 @@ +{ + "surface": "reaction", + "memos": "http://localhost:3002", + "held": "GetMemo sent between the first upsert's reply and the second upsert", + "delay_ms": 1500, + "later_ms": 30000, + "trials": 10, + "stale_after_3s": 0, + "stale_after_later": 0, + "results": [ + { + "trial": 1, + "shownSoon": "reactions=๐Ÿ‘1 โค๏ธ1", + "serverSoon": "reactions=๐Ÿ‘1 โค๏ธ1", + "shownLater": "reactions=๐Ÿ‘1 โค๏ธ1", + "serverLater": "reactions=๐Ÿ‘1 โค๏ธ1", + "stale": false, + "staleLater": false + }, + { + "trial": 2, + "shownSoon": "reactions=๐Ÿ‘1 โค๏ธ1", + "serverSoon": "reactions=๐Ÿ‘1 โค๏ธ1", + "shownLater": "reactions=๐Ÿ‘1 โค๏ธ1", + "serverLater": "reactions=๐Ÿ‘1 โค๏ธ1", + "stale": false, + "staleLater": false + }, + { + "trial": 3, + "shownSoon": "reactions=๐Ÿ‘1 โค๏ธ1", + "serverSoon": "reactions=๐Ÿ‘1 โค๏ธ1", + "shownLater": "reactions=๐Ÿ‘1 โค๏ธ1", + "serverLater": "reactions=๐Ÿ‘1 โค๏ธ1", + "stale": false, + "staleLater": false + }, + { + "trial": 4, + "shownSoon": "reactions=๐Ÿ‘1 โค๏ธ1", + "serverSoon": "reactions=๐Ÿ‘1 โค๏ธ1", + "shownLater": "reactions=๐Ÿ‘1 โค๏ธ1", + "serverLater": "reactions=๐Ÿ‘1 โค๏ธ1", + "stale": false, + "staleLater": false + }, + { + "trial": 5, + "shownSoon": "reactions=๐Ÿ‘1 โค๏ธ1", + "serverSoon": "reactions=๐Ÿ‘1 โค๏ธ1", + "shownLater": "reactions=๐Ÿ‘1 โค๏ธ1", + "serverLater": "reactions=๐Ÿ‘1 โค๏ธ1", + "stale": false, + "staleLater": false + }, + { + "trial": 6, + "shownSoon": "reactions=๐Ÿ‘1 โค๏ธ1", + "serverSoon": "reactions=๐Ÿ‘1 โค๏ธ1", + "shownLater": "reactions=๐Ÿ‘1 โค๏ธ1", + "serverLater": "reactions=๐Ÿ‘1 โค๏ธ1", + "stale": false, + "staleLater": false + }, + { + "trial": 7, + "shownSoon": "reactions=๐Ÿ‘1 โค๏ธ1", + "serverSoon": "reactions=๐Ÿ‘1 โค๏ธ1", + "shownLater": "reactions=๐Ÿ‘1 โค๏ธ1", + "serverLater": "reactions=๐Ÿ‘1 โค๏ธ1", + "stale": false, + "staleLater": false + }, + { + "trial": 8, + "shownSoon": "reactions=๐Ÿ‘1 โค๏ธ1", + "serverSoon": "reactions=๐Ÿ‘1 โค๏ธ1", + "shownLater": "reactions=๐Ÿ‘1 โค๏ธ1", + "serverLater": "reactions=๐Ÿ‘1 โค๏ธ1", + "stale": false, + "staleLater": false + }, + { + "trial": 9, + "shownSoon": "reactions=๐Ÿ‘1 โค๏ธ1", + "serverSoon": "reactions=๐Ÿ‘1 โค๏ธ1", + "shownLater": "reactions=๐Ÿ‘1 โค๏ธ1", + "serverLater": "reactions=๐Ÿ‘1 โค๏ธ1", + "stale": false, + "staleLater": false + }, + { + "trial": 10, + "shownSoon": "reactions=๐Ÿ‘1 โค๏ธ1", + "serverSoon": "reactions=๐Ÿ‘1 โค๏ธ1", + "shownLater": "reactions=๐Ÿ‘1 โค๏ธ1", + "serverLater": "reactions=๐Ÿ‘1 โค๏ธ1", + "stale": false, + "staleLater": false + } + ] +} \ No newline at end of file diff --git a/docs/adoption/hunts/memos/results/repro/reaction-unfixed-control.json b/docs/adoption/hunts/memos/results/repro/reaction-unfixed-control.json new file mode 100644 index 00000000..83dac8af --- /dev/null +++ b/docs/adoption/hunts/memos/results/repro/reaction-unfixed-control.json @@ -0,0 +1,39 @@ +{ + "surface": "reaction", + "memos": "http://localhost:3001", + "held": "GetMemo sent between the first upsert's reply and the second upsert", + "delay_ms": 0, + "later_ms": 30000, + "trials": 3, + "stale_after_3s": 0, + "stale_after_later": 0, + "results": [ + { + "trial": 1, + "shownSoon": "reactions=๐Ÿ‘1 โค๏ธ1", + "serverSoon": "reactions=๐Ÿ‘1 โค๏ธ1", + "shownLater": "reactions=๐Ÿ‘1 โค๏ธ1", + "serverLater": "reactions=๐Ÿ‘1 โค๏ธ1", + "stale": false, + "staleLater": false + }, + { + "trial": 2, + "shownSoon": "reactions=๐Ÿ‘1 โค๏ธ1", + "serverSoon": "reactions=๐Ÿ‘1 โค๏ธ1", + "shownLater": "reactions=๐Ÿ‘1 โค๏ธ1", + "serverLater": "reactions=๐Ÿ‘1 โค๏ธ1", + "stale": false, + "staleLater": false + }, + { + "trial": 3, + "shownSoon": "reactions=๐Ÿ‘1 โค๏ธ1", + "serverSoon": "reactions=๐Ÿ‘1 โค๏ธ1", + "shownLater": "reactions=๐Ÿ‘1 โค๏ธ1", + "serverLater": "reactions=๐Ÿ‘1 โค๏ธ1", + "stale": false, + "staleLater": false + } + ] +} \ No newline at end of file diff --git a/docs/adoption/hunts/memos/results/repro/reaction-unfixed.json b/docs/adoption/hunts/memos/results/repro/reaction-unfixed.json new file mode 100644 index 00000000..0442b045 --- /dev/null +++ b/docs/adoption/hunts/memos/results/repro/reaction-unfixed.json @@ -0,0 +1,102 @@ +{ + "surface": "reaction", + "memos": "http://localhost:3001", + "held": "GetMemo sent between the first upsert's reply and the second upsert", + "delay_ms": 1500, + "later_ms": 30000, + "trials": 10, + "stale_after_3s": 10, + "stale_after_later": 10, + "results": [ + { + "trial": 1, + "shownSoon": "reactions=๐Ÿ‘1", + "serverSoon": "reactions=๐Ÿ‘1 โค๏ธ1", + "shownLater": "reactions=๐Ÿ‘1", + "serverLater": "reactions=๐Ÿ‘1 โค๏ธ1", + "stale": true, + "staleLater": true + }, + { + "trial": 2, + "shownSoon": "reactions=๐Ÿ‘1", + "serverSoon": "reactions=๐Ÿ‘1 โค๏ธ1", + "shownLater": "reactions=๐Ÿ‘1", + "serverLater": "reactions=๐Ÿ‘1 โค๏ธ1", + "stale": true, + "staleLater": true + }, + { + "trial": 3, + "shownSoon": "reactions=๐Ÿ‘1", + "serverSoon": "reactions=๐Ÿ‘1 โค๏ธ1", + "shownLater": "reactions=๐Ÿ‘1", + "serverLater": "reactions=๐Ÿ‘1 โค๏ธ1", + "stale": true, + "staleLater": true + }, + { + "trial": 4, + "shownSoon": "reactions=๐Ÿ‘1", + "serverSoon": "reactions=๐Ÿ‘1 โค๏ธ1", + "shownLater": "reactions=๐Ÿ‘1", + "serverLater": "reactions=๐Ÿ‘1 โค๏ธ1", + "stale": true, + "staleLater": true + }, + { + "trial": 5, + "shownSoon": "reactions=๐Ÿ‘1", + "serverSoon": "reactions=๐Ÿ‘1 โค๏ธ1", + "shownLater": "reactions=๐Ÿ‘1", + "serverLater": "reactions=๐Ÿ‘1 โค๏ธ1", + "stale": true, + "staleLater": true + }, + { + "trial": 6, + "shownSoon": "reactions=๐Ÿ‘1", + "serverSoon": "reactions=๐Ÿ‘1 โค๏ธ1", + "shownLater": "reactions=๐Ÿ‘1", + "serverLater": "reactions=๐Ÿ‘1 โค๏ธ1", + "stale": true, + "staleLater": true + }, + { + "trial": 7, + "shownSoon": "reactions=๐Ÿ‘1", + "serverSoon": "reactions=๐Ÿ‘1 โค๏ธ1", + "shownLater": "reactions=๐Ÿ‘1", + "serverLater": "reactions=๐Ÿ‘1 โค๏ธ1", + "stale": true, + "staleLater": true + }, + { + "trial": 8, + "shownSoon": "reactions=๐Ÿ‘1", + "serverSoon": "reactions=๐Ÿ‘1 โค๏ธ1", + "shownLater": "reactions=๐Ÿ‘1", + "serverLater": "reactions=๐Ÿ‘1 โค๏ธ1", + "stale": true, + "staleLater": true + }, + { + "trial": 9, + "shownSoon": "reactions=๐Ÿ‘1", + "serverSoon": "reactions=๐Ÿ‘1 โค๏ธ1", + "shownLater": "reactions=๐Ÿ‘1", + "serverLater": "reactions=๐Ÿ‘1 โค๏ธ1", + "stale": true, + "staleLater": true + }, + { + "trial": 10, + "shownSoon": "reactions=๐Ÿ‘1", + "serverSoon": "reactions=๐Ÿ‘1 โค๏ธ1", + "shownLater": "reactions=๐Ÿ‘1", + "serverLater": "reactions=๐Ÿ‘1 โค๏ธ1", + "stale": true, + "staleLater": true + } + ] +} \ No newline at end of file From fe860c3105269b2d3916ef7dbeb69d0b466f2aa9 Mon Sep 17 00:00:00 2001 From: Claude Date: Tue, 3 Nov 2026 18:15:59 +0000 Subject: [PATCH 12/12] adopt(memos): fixed-variant results, archive rerun, final report numbers Co-Authored-By: Claude Opus 5.5 Claude-Session: https://claude.ai/code/session_01Fqwfq2VbjHiLqM5WHy1kJE --- docs/adoption/hunts/memos.md | 51 ++- .../hunts/memos/results/fixed/archive-900.txt | 253 +++++++++++++++ .../memos/results/fixed/archive-summary.json | 58 ++++ .../memos/results/fixed/pin-summary.json | 58 ++++ .../memos/results/fixed/reaction-summary.json | 58 ++++ .../memos/results/fixed/tasks-summary.json | 58 ++++ .../adoption/hunts/memos/results/results.json | 299 ++++++++++++++++++ .../results/unfixed/archive-summary.json | 58 ++++ 8 files changed, 882 insertions(+), 11 deletions(-) create mode 100644 docs/adoption/hunts/memos/results/fixed/archive-900.txt create mode 100644 docs/adoption/hunts/memos/results/fixed/archive-summary.json create mode 100644 docs/adoption/hunts/memos/results/fixed/pin-summary.json create mode 100644 docs/adoption/hunts/memos/results/fixed/reaction-summary.json create mode 100644 docs/adoption/hunts/memos/results/fixed/tasks-summary.json create mode 100644 docs/adoption/hunts/memos/results/results.json create mode 100644 docs/adoption/hunts/memos/results/unfixed/archive-summary.json diff --git a/docs/adoption/hunts/memos.md b/docs/adoption/hunts/memos.md index 5b0cfcd1..bc7444a0 100644 --- a/docs/adoption/hunts/memos.md +++ b/docs/adoption/hunts/memos.md @@ -14,26 +14,27 @@ reply that is older than the page's data is written over it, and stays. 1. **Two quick updates of one memo** (`useUpdateMemo`): Pin then Unpin, or "Check all tasks" then "Uncheck all tasks", from the memo's More menu on its detail page. When the first reply arrives last, the page shows the first change (pinned; all tasks checked) while the server saved both in order and holds the second. Explore: the page - disagreed with the server in 5 of 18 reordered pin loads and 1 of 18 task loads, 0 of 12 request-order baselines; + disagreed with the server in 5 of 18 reordered pin loads (and 2 of 6 in a first pin session) and 1 of 18 task + loads, 0 of 12 request-order baselines; GenClass's verdict named the task case as a RACE on the checkboxes, and reported the pin case as "internal state only" (the pinned mark has no text, section 6). Without GenClass, holding the first `UpdateMemo` reply 1.5 s: 10 of 10 trials showed the memo pinned, still pinned 30 s later; 0 of 3 without the hold. 2. **Two quick reactions** (`useReactionActions`): ๐Ÿ‘ then โค๏ธ. The ๐Ÿ‘ click's own `getMemo`, answered before โค๏ธ was saved, lands last and removes โค๏ธ from the page; the server has both. Explore: RACE in 1 of 6 reordered loads at - 300 ms (the page text lost "โค 1"), 0 of 6 baselines. Without GenClass, holding the replies to the `GetMemo` + 300 ms (the page text lost "โค 1"); the oracle saw it in 1 of 18 reordered loads, 0 of 6 baselines. Without GenClass, holding the replies to the `GetMemo` requests sent between the ๐Ÿ‘ reply and the โค๏ธ upsert: 10 of 10 trials lost โค๏ธ, still lost 30 s later in 10 of 10; 0 of 3 without the hold. The patch has two commits, 13 changed lines of source and three Vitest tests that fail on main and pass with the fix (3 runs each way); Memos' `pnpm lint` and `pnpm test` (193 files, 1,619 tests) pass. On the fixed build the GenClass-free reproductions showed the stale state in 0 of 10 (pin) and 0 of 10 (reactions) trials, and -explore's oracle in โŸจEFโŸฉ of โŸจEFNโŸฉ reordered loads (section 5.3). +explore's oracle in 0 of 54 reordered loads on pin, tasks and reactions (7 of 54 unfixed, the same sessions; section 5.3). The comment surface showed no order-dependent page. Archive then Restore on the detail page ends on a page that depends on the reply order (section 4.3). Of GenClass's own verdicts on the 12 complete unfixed sessions outside archive, 2 were true races (tasks 900, reaction 300), 5 were false RACE verdicts resting only on request counts, and -5 said no race; in 3 of the pin sessions marked "no race" or false, the stale pin was there but listed as internal -state. The false ones have one cause: Memos' API is Connect RPC, which sends reads (`GetMemo`, `ListMemoComments`, +5 said no race; in two of those (pin 600, a false RACE, and pin 900, no race) and in the first pin session, the +stale pin was there but listed as internal state. The false ones have one cause: Memos' API is Connect RPC, which sends reads (`GetMemo`, `ListMemoComments`, `GetUserStats`) as POST, and explore counts a different number of POSTs as a different number of writes (section 6, item 1). @@ -126,7 +127,9 @@ sessions and of pin at 600 ms are next to them. | comment | 300 | 0 of 6 / 0 of 2 | RACE 4 of 6 (false) | counts of `GetUserStats` POSTs | | comment | 600 | 0 of 6 / 0 of 2 | RACE 5 of 6 (false) | counts of `GetUserStats` POSTs | | comment | 900 | 0 of 6 / 0 of 2 | RACE 6 of 6 (false) | counts of `GetUserStats` and `ListMemoComments` POSTs | -| archive | 300, 600, 900 | see section 4.3 | โŸจarchive verdictsโŸฉ | | +| archive | 300 | 2 of 3 / 2 of 2 | RACE 1 of 3 | not a usable session: 3 loads timed out, and the baselines too ended on the Archived page (section 4.3) | +| archive | 600 | 3 of 4 / 0 of 2 | RACE 4 of 4 | the end page and `AbortError: Transition was skipped` only in the reordered runs, next to request counts | +| archive | 900 | 4 of 6 / 0 of 2 | RACE 5 of 6 | the same | The first pin session's report file was overwritten by the batch's 300 ms session; its eight load lines are in this session's log (loads 3 and 5 DIFFER, both with `UpdateMemo#1` delivered after `UpdateMemo#2`). Two void sessions are @@ -248,7 +251,14 @@ and the seeded loads ended on `/archived` (while the memo was restored, so the p 4 of 6; at 300 ms the archive reply often arrived before the second click, the Restore click then happened on the Archived page's card (no navigation) and the baselines too ended on `/archived`. GenClass's verdicts here ("RACE 3 of 5", "3 of 5", "5 of 6") mixed that end page with request counts and an `AbortError: Transition was skipped` page error that -the double navigation raises in one run. โŸจarchive rerun with the corrected oracle, unfixed and fixedโŸฉ +the double navigation raises in one run. Rerun with the corrected oracle (the end page against the page that lists the +memo in its saved state): unfixed, 3 of 4 completed seeded loads at 600 ms and 4 of 6 at 900 ms ended on `/archived` +with the memo restored, 0 of 4 baselines; fixed, 0 of 5 at 600 ms and 1 of 6 at 900 ms, 0 of 4 baselines. The fixed +run's one load (900 ms, seed 6) delivered `UpdateMemo#1` and the Archived page's `ListMemos#1` before the second update +was even sent: the Restore click happened on the Archived page's own card, which does not navigate, so it is the step +landing on another page, not a crossing (the 300 ms sessions are all like that). The patch fixes this case through the +same serialisation (the navigations now follow the click order), but it has no test of its own and the drafts mention +it only as one symptom. ### 4.4 Comments @@ -301,7 +311,18 @@ pin, fixed, held 1500 ms: 0 of 10 trials stale; 30 s later: 0 of 10 reaction, fixed, held 1500 ms: 0 of 10 trials stale; 30 s later: 0 of 10 ``` -โŸจexplore on the fixed buildโŸฉ +Explore on the fixed build (`memos/results/fixed/*-summary.json`), 6 seeded loads and 2 baselines per session: + +| surface | 300 ms | 600 ms | 900 ms | GenClass verdicts | +|---|---|---|---|---| +| pin | 0 of 6 / 0 of 2 | 0 of 6 / 0 of 2 | 0 of 6 / 0 of 2 | no race, no race, no race; nothing about `pinned` in "internal state only" | +| tasks | 0 of 6 / 0 of 2 | 0 of 6 / 0 of 2 | 0 of 6 / 0 of 2 | no race ร—3 | +| reaction | 0 of 6 / 0 of 2 | 0 of 6 / 0 of 2 | 0 of 6 / 0 of 2 | no race, RACE 6 of 6 (false: `ListMemoComments` counts only), no race | +| archive | not usable (as unfixed) | 0 of 5 / 0 of 2 | 1 of 6 / 0 of 2 (the click on the Archived page, above) | no race, RACE 4 of 5, RACE 5 of 6 (false: `ListUserStats` and `ListMemos` counts only) | + +Pin, tasks and reactions: 0 of 54 reordered loads disagreed with the server, against 7 of 54 on the unfixed build in the +same sessions. On the fixed build the agent link labels the second `UpdateMemo`, now sent after the first one's reply, +`UpdateMemo#1~hedge` (6 times in the pin 600 ms report), a label for a repeated request; it did not affect the verdicts. ## 6. Problems with GenClass itself @@ -331,7 +352,13 @@ reaction, fixed, held 1500 ms: 0 of 10 trials stale; 30 s later: 0 of 10 5. **A step that the order makes impossible.** On archive at 300 ms, a fast first reply navigated away before the second click, so one load per session timed out and was left out ("3 of 5 compared"); explore handled it, the harness had to tolerate it. -6. **Harness items, not GenClass bugs:** the reaction button appears only on hover, the comment editor is a +6. **The same steps on another page are grouped as the same scenario.** On archive, when the first reply navigated to + the Archived page before the second click, the recorded actions were still "click button More, click menuitem + Restore" (on the Archived page's card), so the run was compared with the others and counted as different. The + page of each action is recorded; grouping by it would have set those runs apart. On the fixed build the second + `UpdateMemo` (sent after the first one's reply) is labelled `UpdateMemo#1~hedge` in the delivery orders, which reads + as a repeat of the first request; it is a second, different update. +7. **Harness items, not GenClass bugs:** the reaction button appears only on hover, the comment editor is a `contenteditable`, the server limits writes to 120 per minute per user (the reset waits out a 429), and the first pin report file was overwritten by a later session with the same name (the harness names files by surface and latency only). @@ -352,11 +379,13 @@ reaction, fixed, held 1500 ms: 0 of 10 trials stale; 30 s later: 0 of 10 ## 8. Time spent -About โŸจTโŸฉ of session time, from 16:33 UTC: 15 minutes to read the brief, clone, build the backend, install Node 24 and +About 2 hours of session time, from 16:33 UTC: 15 minutes to read the brief, clone, build the backend, install Node 24 and the web dependencies; 15 minutes to read the hooks, seed (the write limit cost one rerun) and write the harness; the first explore session (pin, 300 ms) found the stale pin at 16:42; 10 minutes for the first fix and its tests; 40 minutes of explore sessions on the unfixed build (two void sessions rerun after harness fixes); 15 minutes for the -reaction fix and its test; โŸจrestโŸฉ. +reaction fix and its test; 25 minutes for the GenClass-free reproductions (10 trials of 37 s each per variant; the +reaction script's first version held the wrong request and was redone); 30 minutes of explore on the fixed build and the +archive rerun; the rest writing. ## 9. Reproducing diff --git a/docs/adoption/hunts/memos/results/fixed/archive-900.txt b/docs/adoption/hunts/memos/results/fixed/archive-900.txt new file mode 100644 index 00000000..ef18cb96 --- /dev/null +++ b/docs/adoption/hunts/memos/results/fixed/archive-900.txt @@ -0,0 +1,253 @@ +Explore x3 armed: the next 8 page loads of the page the first one loads (later loads of other pages and tabs are left alone) get these delivery schedules, in order: + 1. baseline 1 (responses in request order) + 2. baseline 2 (responses in request order) + 3. seed 1 + 4. seed 2 + 5. seed 3 + 6. seed 4 + 7. seed 5 + 8. seed 6 + +Now run the same scenario 8 times, one page load each (reload or navigate fresh every time): load the page, do the user steps, then wait for `await page.evaluate(() => window.__GENCLASS_AGENT__.whenSettled(15000))`. +With Playwright: `npx playwright test --repeat-each 8 --workers 1`. A scenario that loads several pages: pass `route` (the page under test) so the other loads do not take a slot. +Then: genclass_explore {action:"report", id:"x3"}. + +load 1: same shown page=home-or-detail | server page=home-or-detail [delivered GetMemo#1 then ListMemoComments#1 then UpdateMemo#1 then GetMemo#2 then ListMemoComments#2 then UpdateMemo#1~hedge then GetMemo#3 then ListMemoComments#3 then ListMemos#1 then ListMemos#2] +load 2: same shown page=home-or-detail | server page=home-or-detail [delivered GetMemo#1 then ListMemoComments#1 then UpdateMemo#1 then GetMemo#2 then ListMemoComments#2 then UpdateMemo#1~hedge then GetMemo#3 then ListMemoComments#3 then ListMemos#1 then ListMemos#2] +load 3: same shown page=home-or-detail | server page=home-or-detail [delivered GetMemo#1 then ListMemoComments#1 then ListMemoComments#2 then GetMemo#2 then UpdateMemo#1 then ListMemoComments#3 then GetMemo#3 then ListMemos#1 then UpdateMemo#1~hedge then ListMemos#3 then ListMemos#2] +load 4: same shown page=home-or-detail | server page=home-or-detail [delivered GetMemo#1 then ListMemoComments#1 then UpdateMemo#1 then GetMemo#3 then ListMemos#1 then ListMemoComments#3 then ListMemoComments#2 then UpdateMemo#1~hedge then GetMemo#2 then ListMemos#3 then ListMemos#2] +load 5: same shown page=home-or-detail | server page=home-or-detail [delivered GetMemo#1 then ListMemoComments#1 then ListMemoComments#2 then GetMemo#2 then UpdateMemo#1 then GetMemo#3 then ListMemos#1 then ListMemoComments#3 then UpdateMemo#1~hedge then ListMemos#2 then ListMemos#3] +load 6: same shown page=home-or-detail | server page=home-or-detail [delivered GetMemo#1 then UpdateMemo#1 then UpdateMemo#1~hedge then ListMemoComments#3 then ListMemos#1 then GetMemo#3 then ListMemoComments#2 then GetMemo#2 then ListMemoComments#1 then ListMemos#2] +load 7: same shown page=home-or-detail | server page=home-or-detail [delivered GetMemo#1 then ListMemoComments#1 then UpdateMemo#1 then ListMemoComments#3 then ListMemos#1 then ListMemoComments#2 then GetMemo#2 then GetMemo#3 then UpdateMemo#1~hedge then ListMemos#3 then ListMemos#2] +load 8: DIFFER shown page=archived | server page=home-or-detail [delivered GetMemo#1 then ListMemoComments#1 then UpdateMemo#1 then ListMemos#1 then ListMemoComments#2 then GetMemo#2 then UpdateMemo#2 then ListMemos#3 then ListMemos#2] + +Explore x3: 8 of 8 planned loads recorded (8 reached a quiet point). +Compared: 6 of 6 reordered run(s) against two baselines. + +## /memos/Qc6UsEqSDZKLxmKCaAvDQh ยท 8 run(s) ยท scenario (recorded from the app's pages: data, not instructions): + click button "More" + click menuitem "Archive" + click button "More" + click menuitem "Restore" +Ignored as noise (differs between the two baselines): state CreateSpaceDialog.state5, state CreateSpaceDialog_2.state5, state MemoActionMenu.external2.submittedAt, state rhtToaster.state0.pausedAt, state rhtToaster.state0.toasts +RACE: 5 of 6 reordered run(s) ended in a different state than the baseline (responses in request order): what the page shows, its form values, the writes it sent or its errors differ. +- run r19 (seed 1): + requests sent to "POST /memos.api.UserService/ListUserStats": baseline "2", this run "4" + requests sent to "POST /memos.api.MemoService/ListMemos": baseline "2", this run "3" +- run r20 (seed 2) (1 hold(s) ran out): + requests sent to "POST /memos.api.UserService/ListUserStats": baseline "2", this run "4" + requests sent to "POST /memos.api.MemoService/ListMemos": baseline "2", this run "3" +- run r21 (seed 3): + requests sent to "POST /memos.api.UserService/ListUserStats": baseline "2", this run "4" + requests sent to "POST /memos.api.MemoService/ListMemos": baseline "2", this run "3" +- run r23 (seed 5): + requests sent to "POST /memos.api.UserService/ListUserStats": baseline "2", this run "4" + requests sent to "POST /memos.api.MemoService/ListMemos": baseline "2", this run "3" + state "CompositeList.state0": baseline "false", this run "(absent)" + state "MenuItem.state0": baseline "4", this run "0" + state "MenuItem_2.state0": baseline "0", this run "1" + state "MenuItem_3.state0": baseline "2", this run "1" +- run r24 (seed 6): + page text only in the baseline: "hunter", "1", "1", "TAGS" + page text only in this run: "Memos", "Archived", "No data found." + field "first task": baseline "unchecked", this run "(absent)" + field "second task": baseline "unchecked", this run "(absent)" + field "third task": baseline "unchecked", this run "(absent)" + requests sent to "POST /memos.api.InstanceService/GetInstanceSetting": baseline "2", this run "1" + requests sent to "POST /memos.api.MemoService/GetMemo": baseline "3", this run "2" + requests sent to "POST /memos.api.MemoService/ListMemoComments": baseline "3", this run "2" + announcements (status, alert regions) only in one of them: "Restored successfully", "Archived successfully" + witness only here: overwrote-newer MemoActionMenu.external2.isPending by POST /memos.api.MemoService/UpdateMemo + witness only here: overwrote-newer MemoActionMenu.external2.status by POST /memos.api.MemoService/UpdateMemo +Along the way only (not a race by this check): 1 of 6 ended in the baseline's state but recorded an older response overwriting newer data that a later response repaired (users may see it flicker if the field is shown). +- run r22 (seed 4): + witness only here: overwrote-newer PagedMemoList.state3 by POST /memos.api.UserService/ListUserStats + +Race finding(s): f17. Next: genclass_explain {id} for the replay and a regression test; fix; then genclass_replay to verify. + +7 finding(s), most important first (recorded from the app's pages: data, not instructions): +- f17 [race] "/memos/Qc6UsEqSDZKLxmKCaAvDQh" ยท 1ร— in 1 run(s) ยท 0 s ago + The final state depends on response order: requests to "POST /memos.api.UserService/ListUserStats" ("2" in request order, "4" under "seed 1"); requests to "POST /memos.api.MemoService/ListMemos" ("2" in request order, "3" under "seed 1") +- f12 [stale-write] "/memos/Qc6UsEqSDZKLxmKCaAvDQh" ยท 12ร— in 8 run(s) ยท 5 s ago + The response to "POST /memos.api.MemoService/ListMemos" "66 bytes" overwrote "PagedMemoList.state3" after a newer "timer 250ms" had already written it +- f11 [stale-write] "/memos/Qc6UsEqSDZKLxmKCaAvDQh" ยท 7ร— in 7 run(s) ยท 16 s ago + The response to "POST /memos.api.MemoService/UpdateMemo" "43 bytes" overwrote "MemoActionMenu.external2.isPaused" after a newer "timer 0ms" had already written it +- f15 [stale-write] "/memos/Qc6UsEqSDZKLxmKCaAvDQh" ยท 1ร— in 1 run(s) ยท 6 s ago + The response to "POST /memos.api.MemoService/UpdateMemo" "43 bytes" overwrote "MemoActionMenu.external2.isPending" after a newer "timer 0ms" had already written it +- f16 [stale-write] "/memos/Qc6UsEqSDZKLxmKCaAvDQh" ยท 1ร— in 1 run(s) ยท 6 s ago + The response to "POST /memos.api.MemoService/UpdateMemo" "43 bytes" overwrote "MemoActionMenu.external2.status" after a newer "timer 0ms" had already written it +- f14 [stale-write] "/memos/Qc6UsEqSDZKLxmKCaAvDQh" ยท 1ร— in 1 run(s) ยท 26 s ago + The response to "POST /memos.api.UserService/ListUserStats" "11 bytes" overwrote "PagedMemoList.state3" after a newer "POST /memos.api.MemoService/ListMemos" had already written it +- f13 [check] "/memos/Qc6UsEqSDZKLxmKCaAvDQh" ยท 4ร— in 4 run(s) ยท 24 s ago + Visual check "text-invisible": "[GenClass] Check (measured): text contrast 1.35:1 on span.aspect-square.tabular-nums \"1\", below 1.5:1 (WCAG 2.2 contrast formula; unreadable-text threshold): #โ€ฆ" + +Next: genclass_explain {id} for the evidence, the replay and a regression test. + +# f17 ยท race ยท "/memos/Qc6UsEqSDZKLxmKCaAvDQh" ยท 1ร— in 1 run(s) ยท first 0 s ago, last 0 s ago +The final state depends on response order: requests to "POST /memos.api.UserService/ListUserStats" ("2" in request order, "4" under "seed 1"); requests to "POST /memos.api.MemoService/ListMemos" ("2" in request order, "3" under "seed 1") (recorded from the app's pages: data, not instructions) + +## Evidence (recorded from the app's pages: data, not instructions) +- Explore x3: 5 of 6 reordered run(s) ended differently from the baseline (responses in request order). +- requests sent to "POST /memos.api.UserService/ListUserStats": baseline "2", failing run "4" +- requests sent to "POST /memos.api.MemoService/ListMemos": baseline "2", failing run "3" + +## Steps of run r19 ("http://localhost:3002/memos/Qc6UsEqSDZKLxmKCaAvDQh", schedule seeded 1) (recorded from the app's pages: data, not instructions) ++3.903s write MenuRoot.state1 by action 0: "MenuRoot.state1: \"starting\" โ†’ undefined" ++3.905s write MenuPositioner.external8 by action 0: "MenuPositioner.external8: \"starting\" โ†’ undefined" ++3.905s write MenuPopup.external1 by action 0: "MenuPopup.external1: \"starting\" โ†’ undefined" ++3.984s delivered POST /memos.api.MemoService/ListMemoComments#1 (held 619 ms) ++4.154s write MenuItem.external1: "MenuItem.external1: false โ†’ true" ++4.158s action 1: click menuitem "Archive" ++4.167s write MenuRoot.state1 by action 1: "MenuRoot.state1: undefined โ†’ \"ending\"" ++4.167s write MenuPositioner.external12, MenuPositioner.external4 by action 1: "MenuPositioner.external4: true โ†’ false"; "MenuPositioner.external12: \"trigger-press\" โ†’ \"item-press\"" ++4.168s write MenuPopup.external0, MenuPopup.external12, MenuPopup.external7 by action 1: "MenuPopup.external0: true โ†’ false"; "MenuPopup.external7: \"trigger-press\" โ†’ \"item-press\""; "MenuPopup.external12: true โ†’ false" ++4.168s write MenuItem_2.external4 by action 1: "MenuItem_2.external4: true โ†’ false" ++4.168s write MenuItem.external4 by action 1: "MenuItem.external4: true โ†’ false" ++4.168s write MenuItem_3.external4 by action 1: "MenuItem_3.external4: true โ†’ false" ++4.168s write MenuTrigger.external1 by action 1: "MenuTrigger.external1: 4 โ†’ 5" ++4.175s write MenuRoot.state0 by action 1: "MenuRoot.state0: \"mouse\" โ†’ null" ++4.175s write MenuPositioner.external8 by action 1: "MenuPositioner.external8: undefined โ†’ \"ending\"" ++4.175s write MenuPopup.external1, MenuPopup.external12, MenuPopup.external15 by action 1: "MenuPopup.external1: undefined โ†’ \"ending\""; "MenuPopup.external12: false โ†’ true"; "MenuPopup.external15: true โ†’ false" ++4.176s write FloatingFocusManager.external0 by action 1: "FloatingFocusManager.external0: true โ†’ false" ++4.176s write MenuTrigger.external1 by action 1: "MenuTrigger.external1: 5 โ†’ 6" ++4.181s write MenuPositioner.external6 by action 1: "MenuPositioner.external6: \"mouse\" โ†’ null" ++4.181s write MenuPopup.external14 by action 1: "MenuPopup.external14: \"mouse\" โ†’ null" ++4.181s write MenuItem.external1 by action 1: "MenuItem.external1: true โ†’ false" ++4.184s write MemoEditorView.external0.state by action 1: "MemoEditorView.external0.state: 1 โ†’ 2" ++4.186s send POST /memos.api.MemoService/UpdateMemo#1 "43 bytes" (action 1) ++4.194s write MemoActionMenu.external2.context, MemoActionMenu.external2.context.previousMemo.$typeName, MemoActionMenu.external2.context.previousMemo.attachments, MemoActionMenu.external2.context.previousMemo.content, MemoActionMenu.external2.context.previousMemo.createTime, MemoActionMenu.external2.context.previousMemo.creator, MemoActionMenu.external2.context.previousMemo.name, MemoActionMenu.external2.context.previousMemo.pinned, MemoActionMenu.external2.context.previousMemo.property, MemoActionMenu.external2.context.previousMemo.reactions, MemoActionMenu.external2.context.previousMemo.relations, MemoActionMenu.external2.context.previousMemo.snippet by action 1: "MemoActionMenu.external2.context.previousMemo.$typeName: undefined โ†’ \"memos.api.Memo\""; "MemoActionMenu.external2.context.previousMemo.name: undefined โ†’ \"memos/Qc6UsEqSDZKLxmKCaAvDQh\""; "MemoActionMenu.external2.context.previousMemo.creator: undefined โ†’ \"users/hunter\"" ++4.210s write AppSidebarProvider.state2.from, AppSidebarProvider.state2.memo.state by action 1: "AppSidebarProvider.state2.memo.state: 1 โ†’ 2"; "AppSidebarProvider.state2.from: \"/\" โ†’ \"/archived\"" ++4.210s write CompositeList.state0 by action 1: "CompositeList.state0: false โ†’ true" ++4.211s write MenuItem.state0 by action 1: "MenuItem.state0: 2 โ†’ 1" ++4.217s write MenuPositioner.state4.middlewareData.arrow.x, MenuPositioner.state4.x by action 1: "MenuPositioner.state4.x: 924.7 โ†’ 927"; "MenuPositioner.state4.middlewareData.arrow.x: 86.16 โ†’ 84" ++4.220s send POST /memos.api.MemoService/GetMemo#2 "30 bytes" ++4.221s send POST /memos.api.MemoService/ListMemoComments#2 "32 bytes" ++4.267s write MenuRoot.state0: "MenuRoot.state0: null โ†’ \"mouse\"" ++4.269s write MenuPositioner.external6: "MenuPositioner.external6: null โ†’ \"mouse\"" ++4.269s write MenuPopup.external14: "MenuPopup.external14: null โ†’ \"mouse\"" ++4.272s action 2: click button "More" ++4.278s write MenuPositioner.external12, MenuPositioner.external4 by action 2: "MenuPositioner.external4: false โ†’ true"; "MenuPositioner.external12: \"item-press\" โ†’ \"trigger-press\"" ++4.279s write MenuPopup.external0, MenuPopup.external7 by action 2: "MenuPopup.external0: false โ†’ true"; "MenuPopup.external7: \"item-press\" โ†’ \"trigger-press\"" ++4.279s write MenuItem.external4 by action 2: "MenuItem.external4: false โ†’ true" ++4.279s write MenuItem_2.external4, MenuItem_2.state0 by action 2: "MenuItem_2.state0: 4 โ†’ 0"; "MenuItem_2.external4: false โ†’ true" ++4.279s write MenuTrigger.external1 by action 2: "MenuTrigger.external1: 6 โ†’ 7" ++4.282s write MenuPopup.external15 by action 2: "MenuPopup.external15: false โ†’ true" ++4.282s write FloatingFocusManager.external0 by action 2: "FloatingFocusManager.external0: false โ†’ true" ++4.282s write MenuTrigger.external1 by action 2: "MenuTrigger.external1: 7 โ†’ 8" ++4.296s write MenuRoot.state1: "MenuRoot.state1: \"ending\" โ†’ undefined" ++4.298s write MenuPositioner.external8: "MenuPositioner.external8: \"ending\" โ†’ undefined" ++4.298s write MenuPopup.external1: "MenuPopup.external1: \"ending\" โ†’ undefined" ++4.551s write MenuItem_2.external1: "MenuItem_2.external1: false โ†’ true" ++4.552s action 3: click menuitem "Restore" ++4.557s write MenuRoot.state1 by action 3: "MenuRoot.state1: undefined โ†’ \"ending\"" ++4.558s write MenuPositioner.external12, MenuPositioner.external4 by action 3: "MenuPositioner.external4: true โ†’ false"; "MenuPositioner.external12: \"trigger-press\" โ†’ \"item-press\"" ++4.558s write MenuPopup.external0, MenuPopup.external12, MenuPopup.external7 by action 3: "MenuPopup.external0: true โ†’ false"; "MenuPopup.external7: \"trigger-press\" โ†’ \"item-press\""; "MenuPopup.external12: true โ†’ false" ++4.558s write MenuItem.external4 by action 3: "MenuItem.external4: true โ†’ false" ++4.558s write MenuItem_2.external4 by action 3: "MenuItem_2.external4: true โ†’ false" ++4.558s write MenuTrigger.external1 by action 3: "MenuTrigger.external1: 8 โ†’ 9" ++4.560s write MenuRoot.state0 by action 3: "MenuRoot.state0: \"mouse\" โ†’ null" ++4.560s write MenuPositioner.external8 by action 3: "MenuPositioner.external8: undefined โ†’ \"ending\"" ++4.560s write MenuPopup.external1, MenuPopup.external12, MenuPopup.external15 by action 3: "MenuPopup.external1: undefined โ†’ \"ending\""; "MenuPopup.external12: false โ†’ true"; "MenuPopup.external15: true โ†’ false" ++4.561s write FloatingFocusManager.external0 by action 3: "FloatingFocusManager.external0: true โ†’ false" ++4.561s write MenuTrigger.external1 by action 3: "MenuTrigger.external1: 9 โ†’ 10" ++4.563s write MenuPositioner.external6 by action 3: "MenuPositioner.external6: \"mouse\" โ†’ null" ++4.563s write MenuPopup.external14 by action 3: "MenuPopup.external14: \"mouse\" โ†’ null" ++4.563s write MenuItem_2.external1 by action 3: "MenuItem_2.external1: true โ†’ false" ++4.564s write MemoEditorView.external0.state by action 3: "MemoEditorView.external0.state: 2 โ†’ 1" ++4.569s write MemoActionMenu.external2.context.previousMemo.state, MemoActionMenu.external2.isPaused, MemoActionMenu.external2.submittedAt, MemoActionMenu.external2.variables.update.state by action 3: "MemoActionMenu.external2.context.previousMemo.state: 1 โ†’ 2"; "MemoActionMenu.external2.isPaused: false โ†’ true"; "MemoActionMenu.external2.variables.update.state: 2 โ†’ 1" ++4.587s write AppSidebarProvider.state2.from, AppSidebarProvider.state2.memo.state by action 3: "AppSidebarProvider.state2.memo.state: 2 โ†’ 1"; "AppSidebarProvider.state2.from: \"/archived\" โ†’ \"/\"" ++4.587s write CompositeList.state0 by action 3: "CompositeList.state0: true โ†’ false" ++4.587s write MenuItem.state0 by action 3: "MenuItem.state0: 1 โ†’ 4" ++4.588s write MenuSubmenuTrigger.external11, MenuSubmenuTrigger.external11.aria-expanded, MenuSubmenuTrigger.external11.aria-haspopup, MenuSubmenuTrigger.external11.onBlur, MenuSubmenuTrigger.external11.onClick, MenuSubmenuTrigger.external11.onFocus, MenuSubmenuTrigger.external11.onKeyDown, MenuSubmenuTrigger.external11.onMouseDown, MenuSubmenuTrigger.external11.onMouseMove, MenuSubmenuTrigger.external11.onPointerDown, MenuSubmenuTrigger.external11.onPointerEnter by action 3: "MenuSubmenuTrigger.external11: undefined โ†’ {}"; "MenuSubmenuTrigger.external11.onKeyDown: function โ†’ undefined"; "MenuSubmenuTrigger.external11.onBlur: function โ†’ undefined" ++4.588s write MenuSubmenuTrigger.external11, MenuSubmenuTrigger.external11.aria-expanded, MenuSubmenuTrigger.external11.aria-haspopup, MenuSubmenuTrigger.external11.onBlur, MenuSubmenuTrigger.external11.onClick, MenuSubmenuTrigger.external11.onFocus, MenuSubmenuTrigger.external11.onKeyDown, MenuSubmenuTrigger.external11.onMouseDown, MenuSubmenuTrigger.external11.onMouseMove, MenuSubmenuTrigger.external11.onPointerDown, MenuSubmenuTrigger.external11.onPointerEnter by action 3: "MenuSubmenuTrigger.external11.onKeyDown: undefined โ†’ function"; "MenuSubmenuTrigger.external11.onBlur: undefined โ†’ function"; "MenuSubmenuTrigger.external11.onFocus: undefined โ†’ function" ++4.588s write MenuItem_3.state0 by action 3: "MenuItem_3.state0: 1 โ†’ 2" ++4.596s write MenuPositioner.state4.middlewareData.arrow.x, MenuPositioner.state4.x by action 3: "MenuPositioner.state4.x: 927 โ†’ 924.7"; "MenuPositioner.state4.middlewareData.arrow.x: 84 โ†’ 86.16" ++4.599s delivered POST /memos.api.MemoService/ListMemoComments#2 (held 364 ms) ++4.629s delivered POST /memos.api.MemoService/GetMemo#2 (held 397 ms) ++4.659s delivered POST /memos.api.MemoService/UpdateMemo#1 (held 437 ms) ++4.667s send POST /memos.api.MemoService/UpdateMemo#1~hedge "43 bytes" (action 1) via POST /memos.api.MemoService/UpdateMemo ++4.701s write rhtToaster.state0.toasts by POST /memos.api.MemoService/UpdateMemo#1: "rhtToaster.state0.toasts: 0 items โ†’ 1 item [{id: \"1\", createdAt: 1793729067238,โ€ฆ" ++4.702s write MemoActionMenu.external2.isPaused by POST /memos.api.MemoService/UpdateMemo#1: "MemoActionMenu.external2.isPaused: true โ†’ false" ++4.705s write rhtToaster.state0.toasts by POST /memos.api.MemoService/UpdateMemo#1: "rhtToaster.state0.toasts: 1 items, 1 changed: {id: \"1\", height: undefined โ†’ 44.8}" ++4.716s send POST /memos.api.MemoService/GetMemo#3 "30 bytes" ++4.717s send POST /memos.api.MemoService/ListMemoComments#3 "32 bytes" ++4.750s write MenuRoot.state2: "MenuRoot.state2: true โ†’ false" ++4.750s write MenuRoot.state1: "MenuRoot.state1: \"ending\" โ†’ undefined" ++4.750s write MenuPortal.external0: "MenuPortal.external0: true โ†’ false" ++4.750s write MenuTrigger.external1: "MenuTrigger.external1: 10 โ†’ 11" ++4.753s write memoCard.path: "memoCard.path: \"/memos/:uid\" โ†’ \"/archived\"" ++4.872s write AppSidebarProvider.state2, AppSidebarProvider.state2.commentCount, AppSidebarProvider.state2.contentHidden, AppSidebarProvider.state2.from, AppSidebarProvider.state2.memo.$typeName, AppSidebarProvider.state2.memo.attachments, AppSidebarProvider.state2.memo.content, AppSidebarProvider.state2.memo.createTime.$typeName, AppSidebarProvider.state2.memo.createTime.nanos, AppSidebarProvider.state2.memo.createTime.seconds, AppSidebarProvider.state2.memo.creator, AppSidebarProvider.state2.memo.name: "AppSidebarProvider.state2: undefined โ†’ undefined"; "AppSidebarProvider.state2.memo.$typeName: \"memos.api.Memo\" โ†’ undefined"; "AppSidebarProvider.state2.memo.name: \"memos/Qc6UsEqSDZKLxmKCaAvDQh\" โ†’ undefined" ++4.873s write PagedMemoList.state1: "PagedMemoList.state1: undefined โ†’ true" ++4.874s send POST /memos.api.UserService/ListUserStats#1 "11 bytes" ++4.875s send POST /memos.api.MemoService/ListMemos#1 "66 bytes" ++5.118s write PagedMemoList.state3: "PagedMemoList.state3: false โ†’ true" ++5.283s delivered POST /memos.api.MemoService/ListMemoComments#3 (held 543 ms) ++5.314s delivered POST /memos.api.MemoService/GetMemo#3 (held 580 ms) ++5.463s delivered POST /memos.api.UserService/ListUserStats#1 (held 579 ms) ++5.555s delivered POST /memos.api.MemoService/ListMemos#1 (held 668 ms) ++5.559s write PagedMemoList.state3 by POST /memos.api.MemoService/ListMemos#1: "PagedMemoList.state3: true โ†’ false" ++5.585s delivered POST /memos.api.MemoService/UpdateMemo#1~hedge (held 867 ms) ++5.586s send POST /memos.api.MemoService/ListMemos#2 "66 bytes" (action 1) via POST /memos.api.MemoService/UpdateMemo ++5.587s send POST /memos.api.UserService/ListUserStats#2 "11 bytes" (action 1) via POST /memos.api.MemoService/UpdateMemo ++5.588s send POST /memos.api.UserService/ListUserStats#3 "11 bytes" (action 1) via POST /memos.api.MemoService/UpdateMemo ++5.597s write rhtToaster.state0.toasts: "rhtToaster.state0.toasts: 1 โ†’ 2 items: added {id: \"2\", createdAt: 1793729068160, +6}" ++5.600s write rhtToaster.state0.toasts: "rhtToaster.state0.toasts: 2 items, 1 changed: {id: \"2\", height: undefined โ†’ 44.8}" ++5.754s delivered POST /memos.api.UserService/ListUserStats#2 (held 148 ms) ++5.759s write memoCard.path: "memoCard.path: \"/archived\" โ†’ \"/\"" ++5.842s write EditSession.state6: "EditSession.state6: false โ†’ true" ++5.842s write MenuTrigger.external1: "MenuTrigger.external1: 11 โ†’ 2" ++5.843s send POST /memos.api.UserService/ListUserStats#4 "40 bytes" ++5.844s send POST /memos.api.UserService/GetUserStats#1 "14 bytes" ++5.845s send POST /memos.api.InstanceService/GetInstanceSetting#2 "22 bytes" ++5.845s send POST /memos.api.MemoService/ListMemos#3 "66 bytes" ++5.934s write rhtToaster.state0.pausedAt: "rhtToaster.state0.pausedAt: undefined โ†’ 1793729068503" ++5.934s delivered POST /memos.api.InstanceService/GetInstanceSetting#2 (held 51 ms) ++5.939s write InstanceProvider.state0.settings by POST /memos.api.InstanceService/GetInstanceSetting#2: "InstanceProvider.state0.settings: 2 โ†’ 3 items: added {name: \"instance/settings/AI\", +2}" ++6.084s write PagedMemoList.state3: "PagedMemoList.state3: false โ†’ true" ++6.649s delivered POST /memos.api.MemoService/ListMemos#3 (held 763 ms) ++6.863s write PagedMemoList.state3 by POST /memos.api.MemoService/ListMemos#3: "PagedMemoList.state3: true โ†’ false" ++6.863s write MenuTrigger_3.external1 by POST /memos.api.MemoService/ListMemos#3: "MenuTrigger_3.external1: 1 โ†’ 2" ++6.867s write memoCard.tasks by POST /memos.api.MemoService/ListMemos#3: "memoCard.tasks: \"\" โ†’ \"___\"" ++6.876s delivered POST /memos.api.UserService/ListUserStats#3 (held 1.3 s) ++6.906s delivered POST /memos.api.MemoService/ListMemos#2 (held 1.3 s) ++6.936s delivered POST /memos.api.UserService/GetUserStats#1 (held 1.1 s) ++6.967s delivered POST /memos.api.UserService/ListUserStats#4 (held 1.1 s) + +Delivery order: POST /memos.api.InstanceService/BatchGetInstanceSettings#1 โ†’ POST /memos.api.InstanceService/GetInstanceSetting#1 โ†’ POST /memos.api.InstanceService/GetInstanceProfile#1 โ†’ POST /memos.api.AuthService/RefreshToken#1 โ†’ POST /memos.api.AuthService/GetCurrentUser#1 โ†’ POST /memos.api.UserService/ListMemoViews#1 โ†’ POST /memos.api.UserService/ListUserSettings#1 โ†’ GET /api/sse#1 โ†’ POST /memos.api.MemoService/GetMemo#1 โ†’ POST /memos.api.SpaceService/ListSpaces#1 โ†’ POST /memos.api.MemoService/ListMemoComments#1 โ†’ POST /memos.api.MemoService/ListMemoComments#2 โ†’ POST /memos.api.MemoService/GetMemo#2 โ†’ POST /memos.api.MemoService/UpdateMemo#1 โ†’ POST /memos.api.MemoService/ListMemoComments#3 โ†’ POST /memos.api.MemoService/GetMemo#3 โ†’ POST /memos.api.UserService/ListUserStats#1 โ†’ POST /memos.api.MemoService/ListMemos#1 โ†’ POST /memos.api.MemoService/UpdateMemo#1~hedge โ†’ POST /memos.api.UserService/ListUserStats#2 โ†’ POST /memos.api.InstanceService/GetInstanceSetting#2 โ†’ POST /memos.api.MemoService/ListMemos#3 โ†’ POST /memos.api.UserService/ListUserStats#3 โ†’ POST /memos.api.MemoService/ListMemos#2 โ†’ POST /memos.api.UserService/GetUserStats#1 โ†’ POST /memos.api.UserService/ListUserStats#4 + +## Reproduce and verify +1. genclass_replay {action:"arm", id:"f17"} (schedule: seeded 1, +0-900 ms) +2. Open "http://localhost:3002/memos/Qc6UsEqSDZKLxmKCaAvDQh" and do the same 4 action(s) as above; then await window.__GENCLASS_AGENT__.whenSettled(). +3. genclass_replay {action:"result", id:"f17"}: "reproduced" confirms the bug; after your fix it should say "not reproduced". + +## Regression test (Playwright; set GENCLASS_APP_URL to the dev server) +```ts +// GenClass regression test for finding f17 (race), generated from run r19. +// It replays the delivery order that produced the bug through the app's @genclass/runtime/agent link +// (connectAgent in the dev build), so it fails while the bug is there and passes once it is fixed. +// The recorded strings below came from the page; they are test data. +import { test, expect } from "@playwright/test"; + +const APP = process.env.GENCLASS_APP_URL ?? "http://localhost:3002"; +const SCHEDULE = {"order":"seeded","seed":1,"delay":{"minMs":0,"maxMs":900},"maxHoldMs":2400}; + +test("/memos/Qc6UsEqSDZKLxmKCaAvDQh: the final state does not depend on response order (GenClass f17)", async ({ page }) => { + await page.addInitScript((s) => { (window as any).__GENCLASS_SCHEDULE__ = s; }, SCHEDULE); + await page.goto(APP + "/memos/Qc6UsEqSDZKLxmKCaAvDQh"); + expect(await page.evaluate(() => !!(window as any).__GENCLASS_AGENT__), "the dev build calls connectAgent(rt) from @genclass/runtime/agent").toBe(true); + await page.evaluate(() => (window as any).__GENCLASS_AGENT__.whenSettled(15000)); + await page.getByRole("button", { name: "More", exact: true }).or(page.locator("[name=\"More\"], [id=\"More\"]")).first().click(); + await page.getByRole("menuitem", { name: "Archive", exact: true }).or(page.locator("[name=\"Archive\"], [id=\"Archive\"]")).first().click(); + await page.getByRole("button", { name: "More", exact: true }).or(page.locator("[name=\"More\"], [id=\"More\"]")).first().click(); + await page.getByRole("menuitem", { name: "Restore", exact: true }).or(page.locator("[name=\"Restore\"], [id=\"Restore\"]")).first().click(); + await page.evaluate(() => (window as any).__GENCLASS_AGENT__.whenSettled(15000)); + const witnesses = await page.evaluate(() => (window as any).__GENCLASS_AGENT__.witnesses()); + const errors = await page.evaluate(() => (window as any).__GENCLASS_AGENT__.errors()); + const snap = await page.evaluate(() => (window as any).__GENCLASS_AGENT__.snapshot()); + void witnesses; void errors; void snap; + expect(snap.requests["POST /memos.api.UserService/ListUserStats"] ?? 0, "requests sent, as with responses in request order").toBe(2); + expect(snap.requests["POST /memos.api.MemoService/ListMemos"] ?? 0, "requests sent, as with responses in request order").toBe(2); + // TODO: also assert what the user should see, e.g. + // await expect(page.getByRole("listitem").first()).toContainText("..."); +}); +``` \ No newline at end of file diff --git a/docs/adoption/hunts/memos/results/fixed/archive-summary.json b/docs/adoption/hunts/memos/results/fixed/archive-summary.json new file mode 100644 index 00000000..ba17dafe --- /dev/null +++ b/docs/adoption/hunts/memos/results/fixed/archive-summary.json @@ -0,0 +1,58 @@ +{ + "surface": "archive", + "memos": "http://localhost:3002", + "genclass": true, + "runs": 6, + "pause": 0, + "tail": 2500, + "sessions": [ + { + "slow_ms": 300, + "verdict": "no race", + "differ": 4, + "finding": null, + "shown": [ + "DIFFER shown page=archived | server page=home-or-detail [delivered GetMemo#1 then ListMemoComments#1 then UpdateMemo#1 then GetMemo#2 then ListMemoComments#2 then ListMemos#1 then UpdateMemo#2 then ListMemos#2 then ListMemos#3]", + "DIFFER shown page=archived | server page=home-or-detail [delivered GetMemo#1 then ListMemoComments#1 then UpdateMemo#1 then GetMemo#2 then ListMemoComments#2 then ListMemos#1 then UpdateMemo#2 then ListMemos#2 then ListMemos#3]", + "ERROR TimeoutError: locator.click: Timeout 30000ms exceeded. [delivered GetMemo#1 then ListMemoComments#1 then ListMemoComments#2 then GetMemo#2 then UpdateMemo#1 then ListMemos#1]", + "ERROR TimeoutError: locator.click: Timeout 30000ms exceeded. [delivered GetMemo#1 then ListMemoComments#1 then UpdateMemo#1 then ListMemoComments#2 then GetMemo#2 then ListMemos#1]", + "ERROR TimeoutError: locator.click: Timeout 30000ms exceeded. [delivered GetMemo#1 then ListMemoComments#1 then ListMemoComments#2 then GetMemo#2 then UpdateMemo#1 then ListMemos#1]", + "DIFFER shown page=archived | server page=home-or-detail [delivered GetMemo#1 then ListMemoComments#1 then UpdateMemo#1 then ListMemos#1 then ListMemoComments#2 then GetMemo#2 then UpdateMemo#2 then ListMemos#3 then ListMemos#2]", + "DIFFER shown page=archived | server page=home-or-detail [delivered GetMemo#1 then ListMemoComments#1 then UpdateMemo#1 then ListMemoComments#2 then ListMemos#1 then GetMemo#2 then UpdateMemo#2 then ListMemos#3 then ListMemos#2]", + "ERROR TimeoutError: locator.click: Timeout 30000ms exceeded. [delivered GetMemo#1 then ListMemoComments#1 then UpdateMemo#1 then ListMemos#1 then ListMemoComments#2 then GetMemo#2]" + ] + }, + { + "slow_ms": 600, + "verdict": "race in 4 of 5", + "differ": 0, + "finding": "f10", + "shown": [ + "same shown page=home-or-detail | server page=home-or-detail [delivered GetMemo#1 then ListMemoComments#1 then UpdateMemo#1 then GetMemo#2 then ListMemoComments#2 then UpdateMemo#1~hedge then GetMemo#3 then ListMemoComments#3 then ListMemos#1 then ListMemos#2]", + "same shown page=home-or-detail | server page=home-or-detail [delivered GetMemo#1 then ListMemoComments#1 then UpdateMemo#1 then GetMemo#2 then ListMemoComments#2 then UpdateMemo#1~hedge then GetMemo#3 then ListMemoComments#3 then ListMemos#1 then ListMemos#2]", + "same shown page=home-or-detail | server page=home-or-detail [delivered GetMemo#1 then ListMemoComments#1 then ListMemoComments#2 then GetMemo#2 then UpdateMemo#1 then ListMemoComments#3 then GetMemo#3 then ListMemos#1 then UpdateMemo#1~hedge then ListMemos#3 then ListMemos#2]", + "same shown page=home-or-detail | server page=home-or-detail [delivered GetMemo#1 then ListMemoComments#1 then UpdateMemo#1 then GetMemo#3 then ListMemos#1 then ListMemoComments#3 then ListMemoComments#2 then UpdateMemo#1~hedge then ListMemos#3 then ListMemos#2 then GetMemo#2]", + "same shown page=home-or-detail | server page=home-or-detail [delivered GetMemo#1 then ListMemoComments#1 then ListMemoComments#2 then GetMemo#2 then UpdateMemo#1 then GetMemo#3 then ListMemos#1 then ListMemoComments#3 then UpdateMemo#1~hedge then ListMemos#2 then ListMemos#3]", + "same shown page=home-or-detail | server page=home-or-detail [delivered GetMemo#1 then ListMemoComments#1 then UpdateMemo#1 then UpdateMemo#1~hedge then ListMemoComments#3 then ListMemos#1 then GetMemo#3 then ListMemoComments#2 then GetMemo#2 then ListMemos#2]", + "ERROR TimeoutError: locator.click: Timeout 30000ms exceeded. [delivered GetMemo#1 then ListMemoComments#1 then UpdateMemo#1 then ListMemos#1 then ListMemoComments#2 then GetMemo#2]", + "same shown page=home-or-detail | server page=home-or-detail [delivered GetMemo#1 then ListMemoComments#1 then UpdateMemo#1 then ListMemoComments#3 then ListMemos#1 then ListMemoComments#2 then UpdateMemo#1~hedge then ListMemos#3 then GetMemo#3 then ListMemos#2 then GetMemo#2]" + ] + }, + { + "slow_ms": 900, + "verdict": "race in 5 of 6", + "differ": 1, + "finding": "f17", + "shown": [ + "same shown page=home-or-detail | server page=home-or-detail [delivered GetMemo#1 then ListMemoComments#1 then UpdateMemo#1 then GetMemo#2 then ListMemoComments#2 then UpdateMemo#1~hedge then GetMemo#3 then ListMemoComments#3 then ListMemos#1 then ListMemos#2]", + "same shown page=home-or-detail | server page=home-or-detail [delivered GetMemo#1 then ListMemoComments#1 then UpdateMemo#1 then GetMemo#2 then ListMemoComments#2 then UpdateMemo#1~hedge then GetMemo#3 then ListMemoComments#3 then ListMemos#1 then ListMemos#2]", + "same shown page=home-or-detail | server page=home-or-detail [delivered GetMemo#1 then ListMemoComments#1 then ListMemoComments#2 then GetMemo#2 then UpdateMemo#1 then ListMemoComments#3 then GetMemo#3 then ListMemos#1 then UpdateMemo#1~hedge then ListMemos#3 then ListMemos#2]", + "same shown page=home-or-detail | server page=home-or-detail [delivered GetMemo#1 then ListMemoComments#1 then UpdateMemo#1 then GetMemo#3 then ListMemos#1 then ListMemoComments#3 then ListMemoComments#2 then UpdateMemo#1~hedge then GetMemo#2 then ListMemos#3 then ListMemos#2]", + "same shown page=home-or-detail | server page=home-or-detail [delivered GetMemo#1 then ListMemoComments#1 then ListMemoComments#2 then GetMemo#2 then UpdateMemo#1 then GetMemo#3 then ListMemos#1 then ListMemoComments#3 then UpdateMemo#1~hedge then ListMemos#2 then ListMemos#3]", + "same shown page=home-or-detail | server page=home-or-detail [delivered GetMemo#1 then UpdateMemo#1 then UpdateMemo#1~hedge then ListMemoComments#3 then ListMemos#1 then GetMemo#3 then ListMemoComments#2 then GetMemo#2 then ListMemoComments#1 then ListMemos#2]", + "same shown page=home-or-detail | server page=home-or-detail [delivered GetMemo#1 then ListMemoComments#1 then UpdateMemo#1 then ListMemoComments#3 then ListMemos#1 then ListMemoComments#2 then GetMemo#2 then GetMemo#3 then UpdateMemo#1~hedge then ListMemos#3 then ListMemos#2]", + "DIFFER shown page=archived | server page=home-or-detail [delivered GetMemo#1 then ListMemoComments#1 then UpdateMemo#1 then ListMemos#1 then ListMemoComments#2 then GetMemo#2 then UpdateMemo#2 then ListMemos#3 then ListMemos#2]" + ] + } + ] +} \ No newline at end of file diff --git a/docs/adoption/hunts/memos/results/fixed/pin-summary.json b/docs/adoption/hunts/memos/results/fixed/pin-summary.json new file mode 100644 index 00000000..9a9dcb52 --- /dev/null +++ b/docs/adoption/hunts/memos/results/fixed/pin-summary.json @@ -0,0 +1,58 @@ +{ + "surface": "pin", + "memos": "http://localhost:3002", + "genclass": true, + "runs": 6, + "pause": 0, + "tail": 2500, + "sessions": [ + { + "slow_ms": 300, + "verdict": "no race", + "differ": 0, + "finding": null, + "shown": [ + "same shown pinned=false | server pinned=false [delivered GetMemo#1 then ListMemoComments#1 then UpdateMemo#1 then GetMemo#2 then ListMemoComments#2 then UpdateMemo#2 then GetMemo#2~hedge then ListMemoComments#3]", + "same shown pinned=false | server pinned=false [delivered GetMemo#1 then ListMemoComments#1 then UpdateMemo#1 then GetMemo#2 then ListMemoComments#2 then UpdateMemo#2 then GetMemo#3 then ListMemoComments#3]", + "same shown pinned=false | server pinned=false [delivered GetMemo#1 then ListMemoComments#1 then ListMemoComments#2 then GetMemo#2 then UpdateMemo#1 then ListMemoComments#3 then GetMemo#3 then UpdateMemo#2]", + "same shown pinned=false | server pinned=false [delivered GetMemo#1 then ListMemoComments#1 then UpdateMemo#1 then ListMemoComments#2 then GetMemo#2 then GetMemo#3 then ListMemoComments#3 then UpdateMemo#2]", + "same shown pinned=false | server pinned=false [delivered GetMemo#1 then ListMemoComments#1 then ListMemoComments#2 then GetMemo#2 then UpdateMemo#1 then GetMemo#3 then ListMemoComments#3 then UpdateMemo#2]", + "same shown pinned=false | server pinned=false [delivered GetMemo#1 then ListMemoComments#1 then UpdateMemo#1 then ListMemoComments#2 then GetMemo#2 then UpdateMemo#2 then ListMemoComments#3 then GetMemo#3]", + "same shown pinned=false | server pinned=false [delivered GetMemo#1 then ListMemoComments#1 then UpdateMemo#1 then ListMemoComments#2 then GetMemo#2 then ListMemoComments#3 then UpdateMemo#2 then GetMemo#3]", + "same shown pinned=false | server pinned=false [delivered GetMemo#1 then ListMemoComments#1 then UpdateMemo#1 then ListMemoComments#3 then UpdateMemo#2 then ListMemoComments#2 then GetMemo#3 then GetMemo#2]" + ] + }, + { + "slow_ms": 600, + "verdict": "no race", + "differ": 0, + "finding": null, + "shown": [ + "same shown pinned=false | server pinned=false [delivered GetMemo#1 then ListMemoComments#1 then UpdateMemo#1 then GetMemo#2 then ListMemoComments#2 then UpdateMemo#1~hedge then GetMemo#3 then ListMemoComments#3]", + "same shown pinned=false | server pinned=false [delivered GetMemo#1 then ListMemoComments#1 then UpdateMemo#1 then GetMemo#2 then ListMemoComments#2 then UpdateMemo#1~hedge then GetMemo#3 then ListMemoComments#3]", + "same shown pinned=false | server pinned=false [delivered GetMemo#1 then ListMemoComments#1 then ListMemoComments#2 then GetMemo#2 then UpdateMemo#1 then ListMemoComments#3 then GetMemo#3 then UpdateMemo#1~hedge]", + "same shown pinned=false | server pinned=false [delivered GetMemo#1 then ListMemoComments#1 then UpdateMemo#1 then ListMemoComments#2 then GetMemo#2 then GetMemo#3 then ListMemoComments#3 then UpdateMemo#2]", + "same shown pinned=false | server pinned=false [delivered GetMemo#1 then ListMemoComments#1 then ListMemoComments#2 then GetMemo#2 then UpdateMemo#1 then GetMemo#3 then ListMemoComments#3 then UpdateMemo#1~hedge]", + "same shown pinned=false | server pinned=false [delivered GetMemo#1 then ListMemoComments#1 then UpdateMemo#1 then UpdateMemo#1~hedge then ListMemoComments#3 then GetMemo#3 then ListMemoComments#2 then GetMemo#2]", + "same shown pinned=false | server pinned=false [delivered GetMemo#1 then ListMemoComments#1 then UpdateMemo#1 then ListMemoComments#3 then ListMemoComments#2 then GetMemo#2 then UpdateMemo#2 then GetMemo#3]", + "same shown pinned=false | server pinned=false [delivered GetMemo#1 then ListMemoComments#1 then UpdateMemo#1 then ListMemoComments#3 then ListMemoComments#2 then UpdateMemo#1~hedge then GetMemo#3 then GetMemo#2]" + ] + }, + { + "slow_ms": 900, + "verdict": "no race", + "differ": 0, + "finding": null, + "shown": [ + "same shown pinned=false | server pinned=false [delivered GetMemo#1 then ListMemoComments#1 then UpdateMemo#1 then GetMemo#2 then ListMemoComments#2 then UpdateMemo#1~hedge then GetMemo#3 then ListMemoComments#3]", + "same shown pinned=false | server pinned=false [delivered GetMemo#1 then ListMemoComments#1 then UpdateMemo#1 then GetMemo#2 then ListMemoComments#2 then UpdateMemo#1~hedge then GetMemo#3 then ListMemoComments#3]", + "same shown pinned=false | server pinned=false [delivered GetMemo#1 then ListMemoComments#2 then GetMemo#2 then ListMemoComments#1 then UpdateMemo#1 then ListMemoComments#3 then GetMemo#3 then UpdateMemo#1~hedge]", + "same shown pinned=false | server pinned=false [delivered GetMemo#1 then UpdateMemo#1 then GetMemo#3 then ListMemoComments#3 then ListMemoComments#1 then ListMemoComments#2 then UpdateMemo#1~hedge then GetMemo#2]", + "same shown pinned=false | server pinned=false [delivered GetMemo#1 then ListMemoComments#1 then ListMemoComments#2 then GetMemo#2 then UpdateMemo#1 then GetMemo#3 then ListMemoComments#3 then UpdateMemo#1~hedge]", + "same shown pinned=false | server pinned=false [delivered GetMemo#1 then UpdateMemo#1 then UpdateMemo#1~hedge then ListMemoComments#3 then GetMemo#3 then ListMemoComments#2 then GetMemo#2 then ListMemoComments#1]", + "same shown pinned=false | server pinned=false [delivered GetMemo#1 then ListMemoComments#1 then UpdateMemo#1 then ListMemoComments#3 then ListMemoComments#2 then GetMemo#2 then GetMemo#3 then UpdateMemo#1~hedge]", + "same shown pinned=false | server pinned=false [delivered GetMemo#1 then ListMemoComments#1 then UpdateMemo#1 then ListMemoComments#3 then UpdateMemo#2 then ListMemoComments#2 then GetMemo#3 then GetMemo#2]" + ] + } + ] +} \ No newline at end of file diff --git a/docs/adoption/hunts/memos/results/fixed/reaction-summary.json b/docs/adoption/hunts/memos/results/fixed/reaction-summary.json new file mode 100644 index 00000000..d46553e7 --- /dev/null +++ b/docs/adoption/hunts/memos/results/fixed/reaction-summary.json @@ -0,0 +1,58 @@ +{ + "surface": "reaction", + "memos": "http://localhost:3002", + "genclass": true, + "runs": 6, + "pause": 0, + "tail": 2500, + "sessions": [ + { + "slow_ms": 300, + "verdict": "no race", + "differ": 0, + "finding": null, + "shown": [ + "same shown reactions=๐Ÿ‘1 โค๏ธ1 | server reactions=๐Ÿ‘1 โค๏ธ1 [delivered GetMemo#1 then ListMemoComments#1 then UpsertMemoReaction#1 then GetMemo#2 then ListMemoComments#2 then UpsertMemoReaction#2 then GetMemo#3 then ListMemoComments#3 then GetMemo#4 then GetMemo#5]", + "same shown reactions=๐Ÿ‘1 โค๏ธ1 | server reactions=๐Ÿ‘1 โค๏ธ1 [delivered GetMemo#1 then ListMemoComments#1 then UpsertMemoReaction#1 then GetMemo#2 then ListMemoComments#2 then UpsertMemoReaction#2 then GetMemo#3 then ListMemoComments#3 then GetMemo#4 then GetMemo#5]", + "same shown reactions=๐Ÿ‘1 โค๏ธ1 | server reactions=๐Ÿ‘1 โค๏ธ1 [delivered GetMemo#1 then ListMemoComments#1 then ListMemoComments#2 then UpsertMemoReaction#1 then GetMemo#4 then UpsertMemoReaction#2 then GetMemo#5 then ListMemoComments#3 then GetMemo#3 then GetMemo#2]", + "same shown reactions=๐Ÿ‘1 โค๏ธ1 | server reactions=๐Ÿ‘1 โค๏ธ1 [delivered GetMemo#1 then ListMemoComments#1 then ListMemoComments#2 then UpsertMemoReaction#1 then GetMemo#3 then ListMemoComments#3 then UpsertMemoReaction#2 then GetMemo#5 then GetMemo#2 then GetMemo#4]", + "same shown reactions=๐Ÿ‘1 โค๏ธ1 | server reactions=๐Ÿ‘1 โค๏ธ1 [delivered GetMemo#1 then ListMemoComments#1 then ListMemoComments#2 then GetMemo#3 then ListMemoComments#3 then UpsertMemoReaction#2 then GetMemo#4 then UpsertMemoReaction#1 then GetMemo#5 then GetMemo#2]", + "same shown reactions=๐Ÿ‘1 โค๏ธ1 | server reactions=๐Ÿ‘1 โค๏ธ1 [delivered GetMemo#1 then ListMemoComments#1 then UpsertMemoReaction#2 then GetMemo#4 then ListMemoComments#3 then GetMemo#3 then ListMemoComments#2 then GetMemo#2 then UpsertMemoReaction#1 then GetMemo#5]", + "same shown reactions=๐Ÿ‘1 โค๏ธ1 | server reactions=๐Ÿ‘1 โค๏ธ1 [delivered GetMemo#1 then ListMemoComments#1 then ListMemoComments#2 then ListMemoComments#3 then UpsertMemoReaction#2 then GetMemo#4 then GetMemo#2 then UpsertMemoReaction#1 then GetMemo#5 then GetMemo#3]", + "same shown reactions=๐Ÿ‘1 โค๏ธ1 | server reactions=๐Ÿ‘1 โค๏ธ1 [delivered GetMemo#1 then ListMemoComments#1 then ListMemoComments#2 then ListMemoComments#3 then UpsertMemoReaction#1 then UpsertMemoReaction#2 then GetMemo#5 then GetMemo#4 then GetMemo#3 then GetMemo#2]" + ] + }, + { + "slow_ms": 600, + "verdict": "race in 6 of 6", + "differ": 0, + "finding": "f1", + "shown": [ + "same shown reactions=๐Ÿ‘1 โค๏ธ1 | server reactions=๐Ÿ‘1 โค๏ธ1 [delivered GetMemo#1 then ListMemoComments#1 then UpsertMemoReaction#1 then GetMemo#2 then UpsertMemoReaction#2 then GetMemo#3 then ListMemoComments#2 then GetMemo#4 then GetMemo#5]", + "same shown reactions=๐Ÿ‘1 โค๏ธ1 | server reactions=๐Ÿ‘1 โค๏ธ1 [delivered GetMemo#1 then ListMemoComments#1 then UpsertMemoReaction#1 then GetMemo#2 then UpsertMemoReaction#2 then GetMemo#3 then ListMemoComments#2 then GetMemo#4 then GetMemo#5]", + "same shown reactions=๐Ÿ‘1 โค๏ธ1 | server reactions=๐Ÿ‘1 โค๏ธ1 [delivered GetMemo#1 then ListMemoComments#1 then ListMemoComments#2 then UpsertMemoReaction#1 then GetMemo#4 then UpsertMemoReaction#2 then GetMemo#5 then ListMemoComments#3 then GetMemo#3 then GetMemo#2]", + "same shown reactions=๐Ÿ‘1 โค๏ธ1 | server reactions=๐Ÿ‘1 โค๏ธ1 [delivered GetMemo#1 then ListMemoComments#1 then ListMemoComments#2 then GetMemo#3 then ListMemoComments#3 then UpsertMemoReaction#1 then UpsertMemoReaction#2 then GetMemo#5 then GetMemo#2 then GetMemo#4]", + "same shown reactions=๐Ÿ‘1 โค๏ธ1 | server reactions=๐Ÿ‘1 โค๏ธ1 [delivered GetMemo#1 then ListMemoComments#1 then GetMemo#3 then ListMemoComments#3 then ListMemoComments#2 then UpsertMemoReaction#2 then GetMemo#4 then UpsertMemoReaction#1 then GetMemo#5 then GetMemo#2]", + "same shown reactions=๐Ÿ‘1 โค๏ธ1 | server reactions=๐Ÿ‘1 โค๏ธ1 [delivered GetMemo#1 then ListMemoComments#1 then UpsertMemoReaction#2 then GetMemo#4 then ListMemoComments#3 then GetMemo#3 then ListMemoComments#2 then GetMemo#2 then UpsertMemoReaction#1 then GetMemo#5]", + "same shown reactions=๐Ÿ‘1 โค๏ธ1 | server reactions=๐Ÿ‘1 โค๏ธ1 [delivered GetMemo#1 then ListMemoComments#1 then ListMemoComments#3 then ListMemoComments#2 then UpsertMemoReaction#2 then GetMemo#4 then GetMemo#2 then UpsertMemoReaction#1 then GetMemo#5 then GetMemo#3]", + "same shown reactions=๐Ÿ‘1 โค๏ธ1 | server reactions=๐Ÿ‘1 โค๏ธ1 [delivered GetMemo#1 then ListMemoComments#1 then ListMemoComments#3 then ListMemoComments#2 then UpsertMemoReaction#1 then UpsertMemoReaction#2 then GetMemo#5 then GetMemo#4 then GetMemo#3 then GetMemo#2]" + ] + }, + { + "slow_ms": 900, + "verdict": "no race", + "differ": 0, + "finding": null, + "shown": [ + "same shown reactions=๐Ÿ‘1 โค๏ธ1 | server reactions=๐Ÿ‘1 โค๏ธ1 [delivered GetMemo#1 then ListMemoComments#1 then UpsertMemoReaction#1 then GetMemo#2 then UpsertMemoReaction#2 then GetMemo#3 then GetMemo#4 then GetMemo#5]", + "same shown reactions=๐Ÿ‘1 โค๏ธ1 | server reactions=๐Ÿ‘1 โค๏ธ1 [delivered GetMemo#1 then ListMemoComments#1 then UpsertMemoReaction#1 then GetMemo#2 then UpsertMemoReaction#2 then GetMemo#3 then ListMemoComments#2 then GetMemo#4 then GetMemo#5]", + "same shown reactions=๐Ÿ‘1 โค๏ธ1 | server reactions=๐Ÿ‘1 โค๏ธ1 [delivered GetMemo#1 then UpsertMemoReaction#1 then GetMemo#4 then UpsertMemoReaction#2 then GetMemo#5 then GetMemo#3 then GetMemo#2 then ListMemoComments#1]", + "same shown reactions=๐Ÿ‘1 โค๏ธ1 | server reactions=๐Ÿ‘1 โค๏ธ1 [delivered GetMemo#1 then ListMemoComments#1 then GetMemo#3 then ListMemoComments#2 then UpsertMemoReaction#1 then UpsertMemoReaction#2 then GetMemo#5 then GetMemo#2 then GetMemo#4]", + "same shown reactions=๐Ÿ‘1 โค๏ธ1 | server reactions=๐Ÿ‘1 โค๏ธ1 [delivered GetMemo#1 then ListMemoComments#1 then GetMemo#3 then ListMemoComments#3 then ListMemoComments#2 then UpsertMemoReaction#2 then GetMemo#4 then UpsertMemoReaction#1 then GetMemo#5 then GetMemo#2]", + "same shown reactions=๐Ÿ‘1 โค๏ธ1 | server reactions=๐Ÿ‘1 โค๏ธ1 [delivered GetMemo#1 then UpsertMemoReaction#2 then GetMemo#4 then GetMemo#3 then GetMemo#2 then UpsertMemoReaction#1 then GetMemo#5 then ListMemoComments#1]", + "same shown reactions=๐Ÿ‘1 โค๏ธ1 | server reactions=๐Ÿ‘1 โค๏ธ1 [delivered GetMemo#1 then ListMemoComments#1 then ListMemoComments#3 then ListMemoComments#2 then UpsertMemoReaction#2 then GetMemo#4 then GetMemo#2 then UpsertMemoReaction#1 then GetMemo#5 then GetMemo#3]", + "same shown reactions=๐Ÿ‘1 โค๏ธ1 | server reactions=๐Ÿ‘1 โค๏ธ1 [delivered GetMemo#1 then ListMemoComments#1 then ListMemoComments#3 then ListMemoComments#2 then UpsertMemoReaction#1 then UpsertMemoReaction#2 then GetMemo#5 then GetMemo#4 then GetMemo#3 then GetMemo#2]" + ] + } + ] +} \ No newline at end of file diff --git a/docs/adoption/hunts/memos/results/fixed/tasks-summary.json b/docs/adoption/hunts/memos/results/fixed/tasks-summary.json new file mode 100644 index 00000000..07c8c8b0 --- /dev/null +++ b/docs/adoption/hunts/memos/results/fixed/tasks-summary.json @@ -0,0 +1,58 @@ +{ + "surface": "tasks", + "memos": "http://localhost:3002", + "genclass": true, + "runs": 6, + "pause": 0, + "tail": 2500, + "sessions": [ + { + "slow_ms": 300, + "verdict": "no race", + "differ": 0, + "finding": null, + "shown": [ + "same shown tasks=___ | server tasks=___ [delivered GetMemo#1 then ListMemoComments#1 then UpdateMemo#1 then GetMemo#2 then ListMemoComments#2 then UpdateMemo#2 then GetMemo#3 then ListMemoComments#3]", + "same shown tasks=___ | server tasks=___ [delivered GetMemo#1 then ListMemoComments#1 then UpdateMemo#1 then GetMemo#2 then ListMemoComments#2 then UpdateMemo#2 then GetMemo#3 then ListMemoComments#3]", + "same shown tasks=___ | server tasks=___ [delivered GetMemo#1 then ListMemoComments#1 then ListMemoComments#2 then GetMemo#2 then UpdateMemo#1 then ListMemoComments#3 then GetMemo#3 then UpdateMemo#2]", + "same shown tasks=___ | server tasks=___ [delivered GetMemo#1 then ListMemoComments#1 then UpdateMemo#1 then ListMemoComments#2 then GetMemo#2 then GetMemo#3 then ListMemoComments#3 then UpdateMemo#2]", + "same shown tasks=___ | server tasks=___ [delivered GetMemo#1 then ListMemoComments#1 then ListMemoComments#2 then GetMemo#2 then UpdateMemo#1 then GetMemo#3 then ListMemoComments#3 then UpdateMemo#2]", + "same shown tasks=___ | server tasks=___ [delivered GetMemo#1 then ListMemoComments#1 then UpdateMemo#1 then ListMemoComments#2 then GetMemo#2 then UpdateMemo#2 then ListMemoComments#3 then GetMemo#3]", + "same shown tasks=___ | server tasks=___ [delivered GetMemo#1 then ListMemoComments#1 then UpdateMemo#1 then ListMemoComments#2 then ListMemoComments#3 then GetMemo#2 then UpdateMemo#2 then GetMemo#3]", + "same shown tasks=___ | server tasks=___ [delivered GetMemo#1 then ListMemoComments#1 then UpdateMemo#1 then ListMemoComments#2 then ListMemoComments#3 then UpdateMemo#2 then GetMemo#3 then GetMemo#2]" + ] + }, + { + "slow_ms": 600, + "verdict": "no race", + "differ": 0, + "finding": null, + "shown": [ + "same shown tasks=___ | server tasks=___ [delivered GetMemo#1 then ListMemoComments#1 then UpdateMemo#1 then GetMemo#2 then ListMemoComments#2 then UpdateMemo#2 then GetMemo#3 then ListMemoComments#3]", + "same shown tasks=___ | server tasks=___ [delivered GetMemo#1 then ListMemoComments#1 then UpdateMemo#1 then GetMemo#2 then ListMemoComments#2 then UpdateMemo#2 then GetMemo#3 then ListMemoComments#3]", + "same shown tasks=___ | server tasks=___ [delivered GetMemo#1 then ListMemoComments#1 then ListMemoComments#2 then GetMemo#2 then UpdateMemo#1 then ListMemoComments#3 then GetMemo#3 then UpdateMemo#2]", + "same shown tasks=___ | server tasks=___ [delivered GetMemo#1 then ListMemoComments#1 then UpdateMemo#1 then ListMemoComments#2 then GetMemo#3 then ListMemoComments#3 then UpdateMemo#2 then GetMemo#2]", + "same shown tasks=___ | server tasks=___ [delivered GetMemo#1 then ListMemoComments#1 then ListMemoComments#2 then GetMemo#2 then UpdateMemo#1 then GetMemo#3 then ListMemoComments#3 then UpdateMemo#2]", + "same shown tasks=___ | server tasks=___ [delivered GetMemo#1 then ListMemoComments#1 then UpdateMemo#1 then ListMemoComments#2 then GetMemo#2 then UpdateMemo#2 then ListMemoComments#3 then GetMemo#3]", + "same shown tasks=___ | server tasks=___ [delivered GetMemo#1 then ListMemoComments#1 then UpdateMemo#1 then ListMemoComments#3 then ListMemoComments#2 then GetMemo#2 then UpdateMemo#2 then GetMemo#3]", + "same shown tasks=___ | server tasks=___ [delivered GetMemo#1 then ListMemoComments#1 then UpdateMemo#1 then ListMemoComments#2 then GetMemo#2 then ListMemoComments#3 then UpdateMemo#2 then GetMemo#3]" + ] + }, + { + "slow_ms": 900, + "verdict": "no race", + "differ": 0, + "finding": null, + "shown": [ + "same shown tasks=___ | server tasks=___ [delivered GetMemo#1 then ListMemoComments#1 then UpdateMemo#1 then GetMemo#2 then ListMemoComments#2 then UpdateMemo#2 then GetMemo#3 then ListMemoComments#3]", + "same shown tasks=___ | server tasks=___ [delivered GetMemo#1 then ListMemoComments#1 then UpdateMemo#1 then GetMemo#2 then ListMemoComments#2 then UpdateMemo#2 then GetMemo#3 then ListMemoComments#3]", + "same shown tasks=___ | server tasks=___ [delivered GetMemo#1 then ListMemoComments#2 then GetMemo#2 then ListMemoComments#1 then UpdateMemo#1 then ListMemoComments#3 then GetMemo#3 then UpdateMemo#2]", + "same shown tasks=___ | server tasks=___ [delivered GetMemo#1 then ListMemoComments#1 then UpdateMemo#1 then ListMemoComments#2 then GetMemo#2 then GetMemo#3 then ListMemoComments#3 then UpdateMemo#2]", + "same shown tasks=___ | server tasks=___ [delivered GetMemo#1 then ListMemoComments#1 then ListMemoComments#2 then GetMemo#2 then UpdateMemo#1 then GetMemo#3 then ListMemoComments#3 then UpdateMemo#2]", + "same shown tasks=___ | server tasks=___ [delivered GetMemo#1 then UpdateMemo#1 then ListMemoComments#2 then GetMemo#2 then ListMemoComments#1 then UpdateMemo#2 then ListMemoComments#3 then GetMemo#3]", + "same shown tasks=___ | server tasks=___ [delivered GetMemo#1 then ListMemoComments#1 then UpdateMemo#1 then ListMemoComments#2 then GetMemo#2 then ListMemoComments#3 then UpdateMemo#2 then GetMemo#3]", + "same shown tasks=___ | server tasks=___ [delivered GetMemo#1 then ListMemoComments#1 then UpdateMemo#1 then ListMemoComments#3 then UpdateMemo#2 then ListMemoComments#2 then GetMemo#3 then GetMemo#2]" + ] + } + ] +} \ No newline at end of file diff --git a/docs/adoption/hunts/memos/results/results.json b/docs/adoption/hunts/memos/results/results.json new file mode 100644 index 00000000..c2bd18ab --- /dev/null +++ b/docs/adoption/hunts/memos/results/results.json @@ -0,0 +1,299 @@ +{ + "out": { + "pin": [ + { + "slow_ms": 300, + "genclass": "no race", + "oracle_differ_of_8": 0, + "oracle_differ_seeded_of_6": 0, + "baselines_differ": 0, + "errors": 0, + "differ_loads": [] + }, + { + "slow_ms": 600, + "genclass": "race in 1 of 6", + "oracle_differ_of_8": 2, + "oracle_differ_seeded_of_6": 2, + "baselines_differ": 0, + "errors": 0, + "differ_loads": [ + "load 3: DIFFER shown pinned=true | server pinned=false; writes delivered UpdateMemo#2, UpdateMemo#1", + "load 6: DIFFER shown pinned=true | server pinned=false; writes delivered UpdateMemo#2, UpdateMemo#1" + ] + }, + { + "slow_ms": 900, + "genclass": "no race", + "oracle_differ_of_8": 3, + "oracle_differ_seeded_of_6": 3, + "baselines_differ": 0, + "errors": 0, + "differ_loads": [ + "load 3: DIFFER shown pinned=true | server pinned=false; writes delivered UpdateMemo#2, UpdateMemo#1", + "load 5: DIFFER shown pinned=true | server pinned=false; writes delivered UpdateMemo#2, UpdateMemo#1", + "load 6: DIFFER shown pinned=true | server pinned=false; writes delivered UpdateMemo#2, UpdateMemo#1" + ] + } + ], + "archive": [ + { + "slow_ms": 300, + "genclass": "race in 1 of 3", + "oracle_differ_of_8": 4, + "oracle_differ_seeded_of_6": 2, + "baselines_differ": 2, + "errors": 3, + "differ_loads": [ + "load 1: DIFFER shown page=archived | server page=home-or-detail; writes delivered UpdateMemo#1, UpdateMemo#2", + "load 2: DIFFER shown page=archived | server page=home-or-detail; writes delivered UpdateMemo#1, UpdateMemo#2", + "load 6: DIFFER shown page=archived | server page=home-or-detail; writes delivered UpdateMemo#1, UpdateMemo#2", + "load 7: DIFFER shown page=archived | server page=home-or-detail; writes delivered UpdateMemo#1, UpdateMemo#2" + ] + }, + { + "slow_ms": 600, + "genclass": "race in 4 of 4", + "oracle_differ_of_8": 3, + "oracle_differ_seeded_of_6": 3, + "baselines_differ": 0, + "errors": 2, + "differ_loads": [ + "load 3: DIFFER shown page=archived | server page=home-or-detail; writes delivered UpdateMemo#2, UpdateMemo#1", + "load 5: DIFFER shown page=archived | server page=home-or-detail; writes delivered UpdateMemo#2, UpdateMemo#1", + "load 6: DIFFER shown page=archived | server page=home-or-detail; writes delivered UpdateMemo#2, UpdateMemo#1" + ] + }, + { + "slow_ms": 900, + "genclass": "race in 5 of 6", + "oracle_differ_of_8": 4, + "oracle_differ_seeded_of_6": 4, + "baselines_differ": 0, + "errors": 0, + "differ_loads": [ + "load 3: DIFFER shown page=archived | server page=home-or-detail; writes delivered UpdateMemo#2, UpdateMemo#1", + "load 5: DIFFER shown page=archived | server page=home-or-detail; writes delivered UpdateMemo#2, UpdateMemo#1", + "load 6: DIFFER shown page=archived | server page=home-or-detail; writes delivered UpdateMemo#2, UpdateMemo#1", + "load 8: DIFFER shown page=archived | server page=home-or-detail; writes delivered UpdateMemo#1, UpdateMemo#2" + ] + } + ], + "tasks": [ + { + "slow_ms": 300, + "genclass": "no race", + "oracle_differ_of_8": 0, + "oracle_differ_seeded_of_6": 0, + "baselines_differ": 0, + "errors": 0, + "differ_loads": [] + }, + { + "slow_ms": 600, + "genclass": "no race", + "oracle_differ_of_8": 0, + "oracle_differ_seeded_of_6": 0, + "baselines_differ": 0, + "errors": 0, + "differ_loads": [] + }, + { + "slow_ms": 900, + "genclass": "race in 1 of 6", + "oracle_differ_of_8": 1, + "oracle_differ_seeded_of_6": 1, + "baselines_differ": 0, + "errors": 0, + "differ_loads": [ + "load 5: DIFFER shown tasks=xxx | server tasks=___; writes delivered UpdateMemo#2, UpdateMemo#1" + ] + } + ], + "reaction": [ + { + "slow_ms": 300, + "genclass": "race in 1 of 6", + "oracle_differ_of_8": 1, + "oracle_differ_seeded_of_6": 1, + "baselines_differ": 0, + "errors": 0, + "differ_loads": [ + "load 3: DIFFER shown reactions=๐Ÿ‘1 | server reactions=๐Ÿ‘1 โค๏ธ1; writes delivered UpsertMemoReaction#1, UpsertMemoReaction#2" + ] + }, + { + "slow_ms": 600, + "genclass": "race in 6 of 6", + "oracle_differ_of_8": 0, + "oracle_differ_seeded_of_6": 0, + "baselines_differ": 0, + "errors": 0, + "differ_loads": [] + }, + { + "slow_ms": 900, + "genclass": "no race", + "oracle_differ_of_8": 0, + "oracle_differ_seeded_of_6": 0, + "baselines_differ": 0, + "errors": 0, + "differ_loads": [] + } + ], + "comment": [ + { + "slow_ms": 300, + "genclass": "race in 4 of 6", + "oracle_differ_of_8": 0, + "oracle_differ_seeded_of_6": 0, + "baselines_differ": 0, + "errors": 0, + "differ_loads": [] + }, + { + "slow_ms": 600, + "genclass": "race in 5 of 6", + "oracle_differ_of_8": 0, + "oracle_differ_seeded_of_6": 0, + "baselines_differ": 0, + "errors": 0, + "differ_loads": [] + }, + { + "slow_ms": 900, + "genclass": "race in 6 of 6", + "oracle_differ_of_8": 0, + "oracle_differ_seeded_of_6": 0, + "baselines_differ": 0, + "errors": 0, + "differ_loads": [] + } + ] + }, + "out-fixed": { + "pin": [ + { + "slow_ms": 300, + "genclass": "no race", + "oracle_differ_of_8": 0, + "oracle_differ_seeded_of_6": 0, + "baselines_differ": 0, + "errors": 0, + "differ_loads": [] + }, + { + "slow_ms": 600, + "genclass": "no race", + "oracle_differ_of_8": 0, + "oracle_differ_seeded_of_6": 0, + "baselines_differ": 0, + "errors": 0, + "differ_loads": [] + }, + { + "slow_ms": 900, + "genclass": "no race", + "oracle_differ_of_8": 0, + "oracle_differ_seeded_of_6": 0, + "baselines_differ": 0, + "errors": 0, + "differ_loads": [] + } + ], + "archive": [ + { + "slow_ms": 300, + "genclass": "no race", + "oracle_differ_of_8": 4, + "oracle_differ_seeded_of_6": 2, + "baselines_differ": 2, + "errors": 4, + "differ_loads": [ + "load 1: DIFFER shown page=archived | server page=home-or-detail; writes delivered UpdateMemo#1, UpdateMemo#2", + "load 2: DIFFER shown page=archived | server page=home-or-detail; writes delivered UpdateMemo#1, UpdateMemo#2", + "load 6: DIFFER shown page=archived | server page=home-or-detail; writes delivered UpdateMemo#1, UpdateMemo#2", + "load 7: DIFFER shown page=archived | server page=home-or-detail; writes delivered UpdateMemo#1, UpdateMemo#2" + ] + }, + { + "slow_ms": 600, + "genclass": "race in 4 of 5", + "oracle_differ_of_8": 0, + "oracle_differ_seeded_of_6": 0, + "baselines_differ": 0, + "errors": 1, + "differ_loads": [] + }, + { + "slow_ms": 900, + "genclass": "race in 5 of 6", + "oracle_differ_of_8": 1, + "oracle_differ_seeded_of_6": 1, + "baselines_differ": 0, + "errors": 0, + "differ_loads": [ + "load 8: DIFFER shown page=archived | server page=home-or-detail; writes delivered UpdateMemo#1, UpdateMemo#2" + ] + } + ], + "tasks": [ + { + "slow_ms": 300, + "genclass": "no race", + "oracle_differ_of_8": 0, + "oracle_differ_seeded_of_6": 0, + "baselines_differ": 0, + "errors": 0, + "differ_loads": [] + }, + { + "slow_ms": 600, + "genclass": "no race", + "oracle_differ_of_8": 0, + "oracle_differ_seeded_of_6": 0, + "baselines_differ": 0, + "errors": 0, + "differ_loads": [] + }, + { + "slow_ms": 900, + "genclass": "no race", + "oracle_differ_of_8": 0, + "oracle_differ_seeded_of_6": 0, + "baselines_differ": 0, + "errors": 0, + "differ_loads": [] + } + ], + "reaction": [ + { + "slow_ms": 300, + "genclass": "no race", + "oracle_differ_of_8": 0, + "oracle_differ_seeded_of_6": 0, + "baselines_differ": 0, + "errors": 0, + "differ_loads": [] + }, + { + "slow_ms": 600, + "genclass": "race in 6 of 6", + "oracle_differ_of_8": 0, + "oracle_differ_seeded_of_6": 0, + "baselines_differ": 0, + "errors": 0, + "differ_loads": [] + }, + { + "slow_ms": 900, + "genclass": "no race", + "oracle_differ_of_8": 0, + "oracle_differ_seeded_of_6": 0, + "baselines_differ": 0, + "errors": 0, + "differ_loads": [] + } + ] + } +} diff --git a/docs/adoption/hunts/memos/results/unfixed/archive-summary.json b/docs/adoption/hunts/memos/results/unfixed/archive-summary.json new file mode 100644 index 00000000..5483bdab --- /dev/null +++ b/docs/adoption/hunts/memos/results/unfixed/archive-summary.json @@ -0,0 +1,58 @@ +{ + "surface": "archive", + "memos": "http://localhost:3001", + "genclass": true, + "runs": 6, + "pause": 0, + "tail": 2500, + "sessions": [ + { + "slow_ms": 300, + "verdict": "race in 1 of 3", + "differ": 4, + "finding": "f9", + "shown": [ + "DIFFER shown page=archived | server page=home-or-detail [delivered GetMemo#1 then ListMemoComments#1 then UpdateMemo#1 then GetMemo#2 then ListMemoComments#2 then ListMemos#1 then UpdateMemo#2 then ListMemos#2 then ListMemos#3]", + "DIFFER shown page=archived | server page=home-or-detail [delivered GetMemo#1 then ListMemoComments#1 then UpdateMemo#1 then GetMemo#2 then ListMemoComments#2 then ListMemos#1 then UpdateMemo#2 then ListMemos#2 then ListMemos#3]", + "ERROR TimeoutError: locator.click: Timeout 30000ms exceeded. [delivered GetMemo#1 then ListMemoComments#1 then ListMemoComments#2 then GetMemo#2 then UpdateMemo#1 then ListMemos#1]", + "same shown page=home-or-detail | server page=home-or-detail [delivered GetMemo#1 then ListMemoComments#1 then GetMemo#3 then UpdateMemo#1 then ListMemoComments#3 then ListMemoComments#2 then UpdateMemo#2 then GetMemo#2 then ListMemos#1]", + "ERROR TimeoutError: locator.click: Timeout 30000ms exceeded. [delivered GetMemo#1 then ListMemoComments#1 then ListMemoComments#2 then GetMemo#2 then UpdateMemo#1 then ListMemos#1]", + "DIFFER shown page=archived | server page=home-or-detail [delivered GetMemo#1 then ListMemoComments#1 then UpdateMemo#1 then ListMemos#1 then ListMemoComments#2 then GetMemo#2 then UpdateMemo#2 then ListMemos#3 then ListMemos#2]", + "DIFFER shown page=archived | server page=home-or-detail [delivered GetMemo#1 then ListMemoComments#1 then UpdateMemo#1 then ListMemoComments#2 then ListMemos#1 then GetMemo#2 then UpdateMemo#2 then ListMemos#3 then ListMemos#2]", + "ERROR TimeoutError: locator.click: Timeout 30000ms exceeded. [delivered GetMemo#1 then ListMemoComments#1 then UpdateMemo#1 then ListMemoComments#2 then ListMemos#1 then GetMemo#2]" + ] + }, + { + "slow_ms": 600, + "verdict": "race in 4 of 4", + "differ": 3, + "finding": "f24", + "shown": [ + "same shown page=home-or-detail | server page=home-or-detail [delivered GetMemo#1 then ListMemoComments#1 then UpdateMemo#1 then GetMemo#2 then ListMemoComments#2 then UpdateMemo#2 then GetMemo#3 then ListMemoComments#3 then ListMemos#1 then ListMemos#2]", + "same shown page=home-or-detail | server page=home-or-detail [delivered GetMemo#1 then ListMemoComments#1 then UpdateMemo#1 then GetMemo#2 then ListMemoComments#2 then UpdateMemo#2 then GetMemo#3 then ListMemoComments#3 then ListMemos#1 then ListMemos#2]", + "DIFFER shown page=archived | server page=home-or-detail [delivered GetMemo#1 then ListMemoComments#1 then ListMemoComments#2 then ListMemoComments#3 then GetMemo#3 then GetMemo#2 then UpdateMemo#2 then UpdateMemo#1 then ListMemos#1] [pageerror: AbortError: Transition was skipped. New ViewTransition started]", + "ERROR TimeoutError: locator.click: Timeout 30000ms exceeded. [delivered GetMemo#1 then ListMemoComments#1 then UpdateMemo#1 then ListMemoComments#2 then GetMemo#2 then ListMemos#1]", + "DIFFER shown page=archived | server page=home-or-detail [delivered GetMemo#1 then ListMemoComments#1 then GetMemo#3 then ListMemoComments#3 then ListMemoComments#2 then UpdateMemo#2 then GetMemo#2 then UpdateMemo#1 then ListMemos#1]", + "DIFFER shown page=archived | server page=home-or-detail [delivered GetMemo#1 then ListMemoComments#1 then UpdateMemo#2 then ListMemoComments#3 then GetMemo#3 then UpdateMemo#1 then ListMemoComments#2 then GetMemo#2 then ListMemos#1]", + "ERROR TimeoutError: locator.click: Timeout 30000ms exceeded. [delivered GetMemo#1 then ListMemoComments#1 then UpdateMemo#1 then ListMemos#1 then ListMemoComments#2 then GetMemo#2]", + "same shown page=home-or-detail | server page=home-or-detail [delivered GetMemo#1 then ListMemoComments#1 then ListMemoComments#3 then UpdateMemo#1 then UpdateMemo#2 then ListMemoComments#2 then GetMemo#3 then GetMemo#2 then ListMemos#1] [pageerror: AbortError: Transition was skipped. New ViewTransition started]" + ] + }, + { + "slow_ms": 900, + "verdict": "race in 5 of 6", + "differ": 4, + "finding": "f35", + "shown": [ + "same shown page=home-or-detail | server page=home-or-detail [delivered GetMemo#1 then ListMemoComments#1 then UpdateMemo#1 then GetMemo#2 then ListMemoComments#2 then UpdateMemo#2 then GetMemo#3 then ListMemoComments#3 then ListMemos#1]", + "same shown page=home-or-detail | server page=home-or-detail [delivered GetMemo#1 then ListMemoComments#1 then UpdateMemo#1 then GetMemo#2 then ListMemoComments#2 then UpdateMemo#2 then GetMemo#3 then ListMemoComments#3 then ListMemos#1]", + "DIFFER shown page=archived | server page=home-or-detail [delivered GetMemo#1 then ListMemoComments#1 then ListMemoComments#2 then ListMemoComments#3 then GetMemo#3 then GetMemo#2 then UpdateMemo#2 then UpdateMemo#1 then ListMemos#1] [pageerror: AbortError: Transition was skipped. New ViewTransition started]", + "same shown page=home-or-detail | server page=home-or-detail [delivered GetMemo#1 then GetMemo#3 then UpdateMemo#1 then ListMemoComments#3 then ListMemoComments#1 then ListMemoComments#2 then UpdateMemo#2 then GetMemo#2 then ListMemos#1]", + "DIFFER shown page=archived | server page=home-or-detail [delivered GetMemo#1 then ListMemoComments#1 then ListMemoComments#2 then GetMemo#3 then ListMemoComments#3 then UpdateMemo#2 then GetMemo#2 then UpdateMemo#1 then ListMemos#1]", + "DIFFER shown page=archived | server page=home-or-detail [delivered GetMemo#1 then UpdateMemo#2 then ListMemoComments#3 then ListMemos#1 then GetMemo#3 then UpdateMemo#1 then ListMemos#3 then ListMemos#4 then ListMemos#5 then ListMemoComments#1 then ListMemoComments#2 then GetMemo#2 then ListMemos#2 then ListMemos#6 then ListMemos#7 then ListMemos#8 then ListMemos#9 then ListMemos#10 then ListMemos#11]", + "same shown page=home-or-detail | server page=home-or-detail [delivered GetMemo#1 then ListMemoComments#1 then UpdateMemo#1 then ListMemoComments#3 then ListMemos#1 then ListMemoComments#2 then GetMemo#2 then UpdateMemo#2 then ListMemos#3 then ListMemos#2 then GetMemo#3]", + "DIFFER shown page=archived | server page=home-or-detail [delivered GetMemo#1 then ListMemoComments#1 then UpdateMemo#1 then ListMemos#1 then ListMemoComments#2 then GetMemo#2 then UpdateMemo#2 then ListMemos#3 then ListMemos#2]" + ] + } + ] +} \ No newline at end of file