authorgravatar for me@pixp.ccAlex <me@pixp.cc> 2026-08-12 13:43:18-07:00
committergravatar for noreply@github.comGitHub <noreply@github.com> 2026-08-12 16:43:18-04:00
loge3a9c2518d5f71ca24872cb3238c1b921c5ae5a1
treeda1a0919c6015ae7ceb362ed146e117b2dfd6c0f
parent18918d1c4d0933b565d1336a75cc5069547ff5e6
signature Signed by PGP key B5690EEEBB952194

test(mobile): seed snoozed showcase threads (#5155)


2 files changed, 51 insertions(+), 5 deletions(-)

scripts/mobile-showcase-environment.ts+35-4
...@@ -198,6 +198,7 @@ export const SHOWCASE_THREADS = [...@@ -198,6 +198,7 @@ export const SHOWCASE_THREADS = [
198 "Keep hydration errors precise, but make the development copy unexpectedly delightful.",198 "Keep hydration errors precise, but make the development copy unexpectedly delightful.",
199 response:199 response:
200 "The diagnostics still lead with the exact mismatch and component stack. A tiny optional haiku now closes the expanded explanation.",200 "The diagnostics still lead with the exact mismatch and component stack. A tiny optional haiku now closes the expanded explanation.",
201 snoozeMinutes: 90,
201 },202 },
202 {203 {
203 id: "beautiful-boot",204 id: "beautiful-boot",
...@@ -211,6 +212,17 @@ export const SHOWCASE_THREADS = [...@@ -211,6 +212,17 @@ export const SHOWCASE_THREADS = [
211 response:212 response:
212 "The plan groups milestones without changing the underlying log stream, preserves plain-text output, and adds zero work to the hot path.",213 "The plan groups milestones without changing the underlying log stream, preserves plain-text output, and adds zero work to the hot path.",
213 },214 },
215 {
216 id: "patient-penguins",
217 projectId: "linux",
218 title: "Teach penguins to wait patiently",
219 branch: "feat/patient-penguins",
220 minutesAgo: 52,
221 request: "Make delayed work easier to follow without adding noise to the scheduler trace.",
222 response:
223 "Delayed work now carries a concise reason through the trace, so the wait is legible without changing scheduling behavior.",
224 snoozeMinutes: 8 * 60,
225 },
214 // Finished work, settled by hand: the list keeps it as a receded tail so226 // Finished work, settled by hand: the list keeps it as a receded tail so
215 // the active block above reads as everything still in flight. The active227 // the active block above reads as everything still in flight. The active
216 // block stays small enough that the settled tail begins above the fold —228 // block stays small enough that the settled tail begins above the fold —
...@@ -337,20 +349,29 @@ function insertThread(...@@ -337,20 +349,29 @@ function insertThread(
337 readonly minutesAgo: number;349 readonly minutesAgo: number;
338 readonly state?: "working" | "approval" | "plan";350 readonly state?: "working" | "approval" | "plan";
339 readonly settled?: boolean;351 readonly settled?: boolean;
352 readonly snoozeMinutes?: number;
340 readonly workspaceRoot: string;353 readonly workspaceRoot: string;
341 },354 },
342): void {355): void {
343 const turnId = `${input.id}-turn`;356 const turnId = `${input.id}-turn`;
344 const updatedAt = minutesBefore(now, input.minutesAgo);357 const updatedAt = minutesBefore(now, input.minutesAgo);
345 const isWorking = input.state === "working";358 const isWorking = input.state === "working";
359 const snoozedUntil =
360 input.snoozeMinutes === undefined
361 ? null
362 : new Date(now + input.snoozeMinutes * 60_000).toISOString();
363 const snoozedAt =
364 input.snoozeMinutes === undefined
365 ? null
366 : minutesBefore(now, Math.max(1, Math.floor(input.minutesAgo / 2)));
346 database367 database
347 .prepare(368 .prepare(
348 `INSERT INTO projection_threads (369 `INSERT INTO projection_threads (
349 thread_id, project_id, title, model_selection_json, runtime_mode, interaction_mode,370 thread_id, project_id, title, model_selection_json, runtime_mode, interaction_mode,
350 branch, worktree_path, latest_turn_id, latest_user_message_at, pending_approval_count,371 branch, worktree_path, latest_turn_id, latest_user_message_at, pending_approval_count,
351 pending_user_input_count, has_actionable_proposed_plan, created_at, updated_at,372 pending_user_input_count, has_actionable_proposed_plan, created_at, updated_at,
352 archived_at, deleted_at, settled_override, settled_at373 archived_at, deleted_at, settled_override, settled_at, snoozed_until, snoozed_at
353 ) VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, 0, ?, ?, ?, NULL, NULL, ?, ?)`,374 ) VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, 0, ?, ?, ?, NULL, NULL, ?, ?, ?, ?)`,
354 )375 )
355 .run(376 .run(
356 input.id,377 input.id,
...@@ -369,6 +390,8 @@ function insertThread(...@@ -369,6 +390,8 @@ function insertThread(
369 updatedAt,390 updatedAt,
370 input.settled ? "settled" : null,391 input.settled ? "settled" : null,
371 input.settled ? updatedAt : null,392 input.settled ? updatedAt : null,
393 snoozedUntil,
394 snoozedAt,
372 );395 );
373 database396 database
374 .prepare(397 .prepare(
...@@ -409,6 +432,8 @@ const SEEDED_PROJECTION_TABLES = [...@@ -409,6 +432,8 @@ const SEEDED_PROJECTION_TABLES = [
409 "projection_state",432 "projection_state",
410] as const;433] as const;
411434
435const SEEDED_THREAD_COLUMNS = ["snoozed_until", "snoozed_at"] as const;
436
412function hasSeedableSchema(dbPath: string): boolean {437function hasSeedableSchema(dbPath: string): boolean {
413 let database: NodeSqlite.DatabaseSync;438 let database: NodeSqlite.DatabaseSync;
414 try {439 try {
...@@ -417,12 +442,18 @@ function hasSeedableSchema(dbPath: string): boolean {...@@ -417,12 +442,18 @@ function hasSeedableSchema(dbPath: string): boolean {
417 return false;442 return false;
418 }443 }
419 try {444 try {
420 const row = database445 const tableCount = database
421 .prepare(446 .prepare(
422 `SELECT COUNT(*) AS count FROM sqlite_master WHERE type = 'table' AND name IN (${SEEDED_PROJECTION_TABLES.map(() => "?").join(", ")})`,447 `SELECT COUNT(*) AS count FROM sqlite_master WHERE type = 'table' AND name IN (${SEEDED_PROJECTION_TABLES.map(() => "?").join(", ")})`,
423 )448 )
424 .get(...SEEDED_PROJECTION_TABLES) as { count: number };449 .get(...SEEDED_PROJECTION_TABLES) as { count: number };
425 return row.count === SEEDED_PROJECTION_TABLES.length;450 if (tableCount.count !== SEEDED_PROJECTION_TABLES.length) return false;
451
452 const threadColumns = database.prepare("PRAGMA table_info(projection_threads)").all() as Array<{
453 name: string;
454 }>;
455 const threadColumnNames = new Set(threadColumns.map((column) => column.name));
456 return SEEDED_THREAD_COLUMNS.every((column) => threadColumnNames.has(column));
426 } catch {457 } catch {
427 return false;458 return false;
428 } finally {459 } finally {
scripts/mobile-showcase.test.ts+16-1
...@@ -270,8 +270,23 @@ it("seeds a playful multi-environment project spectrum", () => {...@@ -270,8 +270,23 @@ it("seeds a playful multi-environment project spectrum", () => {
270 SHOWCASE_ENVIRONMENTS.map((environment) => environment.label),270 SHOWCASE_ENVIRONMENTS.map((environment) => environment.label),
271 ["Moonbase Terminal", "Suspense Station", "Kernel Cabin"],271 ["Moonbase Terminal", "Suspense Station", "Kernel Cabin"],
272 );272 );
273 assert.equal(SHOWCASE_THREADS.length, 8);273 assert.equal(SHOWCASE_THREADS.length, 9);
274 assert.equal(new Set(SHOWCASE_THREADS.map((thread) => thread.projectId)).size, 3);274 assert.equal(new Set(SHOWCASE_THREADS.map((thread) => thread.projectId)).size, 3);
275 const snoozedThreads = SHOWCASE_THREADS.filter((thread) => "snoozeMinutes" in thread);
276 assert.equal(snoozedThreads.length, 2);
277 assert.deepStrictEqual(
278 snoozedThreads.map((thread) => thread.id),
279 ["hydration-haikus", "patient-penguins"],
280 );
281 assert.equal(new Set(snoozedThreads.map((thread) => thread.snoozeMinutes)).size, 2);
282 for (const thread of snoozedThreads) {
283 assert.equal(thread.response !== null, true, `${thread.title} is not completed`);
284 assert.equal("state" in thread, false, `${thread.title} is blocked or working`);
285 assert.equal("settled" in thread, false, `${thread.title} is settled`);
286 assert.equal(thread.snoozeMinutes > 60, true, `${thread.title} wakes too soon`);
287 }
288 const primaryThread = SHOWCASE_THREADS.find((thread) => thread.id === "remote-command-center");
289 assert.equal(primaryThread !== undefined && !("snoozeMinutes" in primaryThread), true);
275 // Every project contributes to both the active block and the settled tail,290 // Every project contributes to both the active block and the settled tail,
276 // so each list scope screenshots with the same two-part structure.291 // so each list scope screenshots with the same two-part structure.
277 for (const project of SHOWCASE_PROJECTS) {292 for (const project of SHOWCASE_PROJECTS) {