Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
8 changes: 7 additions & 1 deletion eval/harness/driver.mjs
Original file line number Diff line number Diff line change
Expand Up @@ -23,7 +23,13 @@ export async function serve(dist) {
}

/** CHROMIUM=/path/to/chromium drives a system browser (NixOS and the like, where Playwright's download does not run). */
export async function launch() { return chromium.launch(process.env.CHROMIUM ? { executablePath: process.env.CHROMIUM } : {}); }
// Without CHROMIUM, a Playwright whose own browser is missing falls back to a preinstalled one (/opt/pw-browsers/chromium,
// as in cloud containers), so an agent's ./test runs there too.
export async function launch() {
if (process.env.CHROMIUM) return chromium.launch({ executablePath: process.env.CHROMIUM });
try { return await chromium.launch(); }
catch (e) { const alt = "/opt/pw-browsers/chromium"; if (existsSync(alt)) return chromium.launch({ executablePath: alt }); throw e; }
}

/** One test: a fresh page on the app, `run(app)` with the helpers below. Returns { ok, error }. */
export async function runTest(browser, url, test) {
Expand Down
87 changes: 87 additions & 0 deletions eval/results/desk13-probe-2026-11-13/README.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,87 @@
# desk9 on the new guide, Opus, 6 apps (2026-11-13)

Not preregistered. Question: with `guide/async.md`'s two new sections (#58: "lay every view of a write over the
answer", "data belongs with its lifetime"), does Opus get near the hand-written reference's 0?

**Setup:** Solid `next` + the #4828 fix (b8a8c2805, solidjs/solid#4829), rxc from main at 7e5bc81, desk9 scaffolded
`--brief plain --inline-docs`, Opus via general-purpose agents, graded with `harness/grade.sh`. The reference
(`reference/desk9/rx`) grades 77/77, 38/38, 0/20 on this build.

## Results

| app | guide | hidden | overlap | fuzz holes | defects |
| --- | --- | ---: | ---: | ---: | ---: |
| rx-1 | #58 | 76 | 37 | 2 | 4 |
| rx-2 | #58 | 76 | 37 | 0 | 2 |
| rx-3 | #58 | 76 | 37 | 3 | 5 |
| rx-4 | #58 + `visit := -1` | 77 | 37 | 1 | 2 |
| rx-5 | #58 + `visit := -1` | 77 | 37 | 1 | 2 |
| rx-6 | #58 + `visit := -1` | 77 | 36 | 1 | 3 |

rx-1..3 could not run `./test`: the eval's Playwright wanted its own Chromium build, which this container lacks, and
`./test` did not pass `CHROMIUM`. They wrote blind. `harness/driver.mjs` now falls back to `/opt/pw-browsers/chromium`.
rx-4..6 tested normally.

## What the defects are

- **Guide-caused, every app: settings moved into a module.** All six applied "data belongs with its lifetime" to the
settings page too, with `guess saved := next` on the module's question. Reopening settings while a save is out then
waits for the save: the tab switch is held with the reload, so the page never shows "Loading settings…" (overlap
"a save that answers after the page was left"). rx-1..3 also loaded settings at app start (`visit := 0`), one ask
too many (hidden "loads again when reopened"); the `-1` line fixed that for rx-4..6.
- **Guide-caused, rx-4/5/6: the open count moved out of the list's region.** With the data in a module, all three
put the count `effect` beside it (module or App). An effect outside the list's `loading` waits for every re-ask, so
a close guessed while a refresh is out shows in the rows but not in the tab (fuzz rx-4 s13, rx-5 s17, rx-6 s16).
The reference's `OpenCount` sits inside the region.
- **Detail heading blank** (rx-1 s11, s15; rx-3 s15): `heading = detail == null ? "" : titleOf(renaming, …)` reads
`""` while `detail.title` in the same frame reads the answer (probed). Needs a rename guess out, a failed list
re-ask and a close's re-ask; a minimal scenario does not reproduce it. Unresolved: likely Solid.
- rx-3 s11, s19: page hangs (120 s). Not traced.
- rx-6: a row error from a write that answered after the issues page was left shows on its return (agent).

## Guide change after this probe

`guide/async.md` "Data belongs with its lifetime" now gives one rule (each value has one lifetime: the visit, the write
or the screen; split a value that would need two) and derives the module list, the save, the guess and the count from it.

## Rerun on the lifetime rule (rx-7..9)

| app | hidden | overlap | fuzz holes | defects |
| --- | ---: | ---: | ---: | ---: |
| rx-7 | 75 | 38 | 0 | 2 |
| rx-8 | 77 | 38 | 1 | 1 |
| rx-9 | 77 | 38 | 0 | **0** |

Mean 1.0 (rx-4..6: 2.33). No settings or count-placement failures: all three kept settings in the page and put the
count inside the list's region.

- rx-7: the help item's `errored` sits outside its `loading on(selected)`, so a new selection keeps the old failure
or not-found message instead of showing loading (agent; not a lifetime case).
- rx-8 s16: the rows render as empty text (`["","",""]`) after the open list lands, while the tab counts 3. Same
shape as rx-1/rx-3's blank heading: a text node left empty while its value is there. Likely Solid; no minimal repro.

## The blank text, in plain Solid

`repro/blank-rows.spec.tsx` (drop into solid-next `packages/web/test/`) reproduces rx-8 s16 in about 50 lines: a
keyed `createProjection` over a memo of an async list, two `createOptimistic` guess lists (assigns, closes), a declined
close among overlapping assigns, then a switch to another list. When the new list's re-ask lands, `For` renders one
`<li>` per row with no text. It fails on next b8a8c2805 and on cd4de2874 (before #4828). Without the projection
(`For` over the memo) it passes. Errored, `Loading on` and the page remount are not needed.

## On next f00ce6f52 (with the #4831 fix)

The repro passes. rx-8 regrades 77/77, 38/38, 0/20: **0 defects** (rx-7..9 now 2, 0, 0). The reference stays at 0.
rx-1 and rx-3 keep their blank detail heading (s11, s15): a plain memo, not a projection, so a separate bug.

## The blank heading, in plain Solid

`repro/blank-heading.spec.tsx` reproduces rx-1/rx-3 s15 on next f00ce6f52 in about 45 lines. There is no projection
and no error boundary. Setup:

- a list memo, and a detail memo that is still loading;
- one `createOptimistic` rename guess, read by both the list's rows and the heading memo;
- another action, out at the same time, that re-asks the list;
- the rename itself, still out.

When the other action lands (its re-ask goes out), and then the detail lands, the `<h2>` renders empty while
`detail.body` beside it shows. Without the other action the test passes.
6 changes: 6 additions & 0 deletions eval/results/desk13-probe-2026-11-13/reference.grade.txt
Original file line number Diff line number Diff line change
@@ -0,0 +1,6 @@
== hidden
77/77 passed
== overlap
38/38 passed
== fuzz
ref-desk9: 0/20 seeds found a hole
46 changes: 46 additions & 0 deletions eval/results/desk13-probe-2026-11-13/repro/blank-heading.spec.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,46 @@
/**
* @jsxImportSource @solidjs/web
* @vitest-environment jsdom
*/
import { test, expect } from "vitest";
import { For, Loading, Show, action, createOptimistic, createMemo, createSignal, refresh, flush } from "solid-js";
import { render } from "../src/index.js";
const tick = async () => { for (let i = 0; i < 4; i++) { await new Promise(r => setTimeout(r, 0)); flush(); } };
test("a heading that reads a guess shows it when its async source lands", async () => {
const asks: { key: string; res: (v: any) => void }[] = [];
const ask = (key: string) => new Promise<any>(res => asks.push({ key, res }));
const pick = (key: string) => { const i = asks.findIndex(a => a.key === key); if (i < 0) throw new Error("no ask " + key + " in " + asks.map(a => a.key)); return asks.splice(i, 1)[0]; };
const c = document.body.appendChild(document.createElement("div"));
const frames: string[] = []; const snap = (l: string) => frames.push(`${l.padEnd(18)} ${c.innerHTML} | in the air: ${asks.map(a => a.key)}`);

const [selected, setSelected] = createSignal(false);
const [renamesT] = createSignal<string[]>(() => []);
const [renames, guessRename] = createOptimistic(() => renamesT());

const list = createMemo(() => ask("list"));
const rows = createMemo(() => (list() as any[]).map(i => ({ id: i.id, title: renames().reduce((t, r) => r, i.title) })));
const detail = createMemo(() => selected() ? ask("detail") : null);
const heading = createMemo(() => { const d: any = detail(); return d == null ? "" : renames().reduce((t, r) => r, d.title); });

const rename = action(function* (title: string) { guessRename(rs => [...rs, title]); yield ask("rename"); refresh(list); refresh(detail); });
const other = action(function* () { yield ask("other"); refresh(list); });

render(() => <main>
<Loading fallback={<p>loading</p>}><ul><For each={rows()}>{(it: any) => <li>{it.title}</li>}</For></ul></Loading>
<Show when={selected()} fallback={<p>select</p>}>
<Loading fallback={<p>loading detail</p>}><h2>{heading()}</h2><p>{(detail() as any)?.body}</p></Loading>
</Show>
</main>, c);

const steps: [string, () => void][] = [
["list lands", () => pick("list").res([{ id: 2, title: "Old bug" }])],
["select", () => setSelected(true)],
["other action", () => other()],
["rename to Delta", () => rename("Delta")],
["other lands", () => pick("other").res({})], // re-asks the list
["detail lands", () => pick("detail").res({ title: "Old bug", body: "long gone" })],
];
for (const [n, f] of steps) { f(); await tick(); snap(n); }
console.log(frames.join("\n"));
expect(c.querySelector("h2")?.textContent).toBe("Delta");
});
55 changes: 55 additions & 0 deletions eval/results/desk13-probe-2026-11-13/repro/blank-rows.spec.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,55 @@
/**
* @jsxImportSource @solidjs/web
* @vitest-environment jsdom
*/
import { test, expect } from "vitest";
import { For, Loading, action, createOptimistic, createMemo, createSignal, createProjection, refresh, flush } from "solid-js";
import { render } from "../src/index.js";
const tick = async () => { for (let i = 0; i < 4; i++) { await new Promise(r => setTimeout(r, 0)); flush(); } };
test("rows that land while guesses are out render their text", async () => {
const asks: { key: string; res: (v: any) => void; rej: (e: any) => void }[] = [];
const inAir = new Map<string, Promise<any>>();
const ask = (key: string) => new Promise<any>((res, rej) => asks.push({ key, res, rej }));
const askQ = (key: string) => { let p = inAir.get(key); if (!p) { p = ask(key).finally(() => inAir.delete(key)); inAir.set(key, p); } return p; }; // a question asked again while in the air joins it
const pick = (key: string) => { const i = asks.findIndex(a => a.key === key); if (i < 0) throw new Error("no ask " + key + " in " + asks.map(a => a.key)); return asks.splice(i, 1)[0]; };
const c = document.body.appendChild(document.createElement("div"));
const frames: string[] = []; const snap = (l: string) => frames.push(`${l.padEnd(22)} ${c.innerHTML} | in the air: ${asks.map(a => a.key)}`);

const [filter, setFilter] = createSignal("closed");
const server = createMemo(() => askQ(filter()));
const [assigningT] = createSignal<{ id: number; user: string }[]>(() => []);
const [assigning, guessAssigning] = createOptimistic(() => assigningT());
const [closingT] = createSignal<number[]>(() => []);
const [closing, guessClosing] = createOptimistic(() => closingT());
const rowsSrc = createMemo(() => (server() as any[]).filter(i => !closing().includes(i.id))
.map(i => ({ id: i.id, title: i.title, assignee: assigning().reduce((n, x) => x.id === i.id ? x.user : n, i.assignee) })));
const rows = createProjection(() => rowsSrc() as any, [] as any, { key: "id" } as any);
const showOpen = action(function* () { setFilter("open"); });
const assignTo = action(function* (id: number, user: string) {
try { guessAssigning(xs => [...xs, { id, user }]); yield ask(`assign:${user}`); refresh(server); } catch (e) {}
});
const closeOne = action(function* (id: number) {
try { guessClosing(xs => [...xs, id]); yield ask(`close`); refresh(server); } catch (e) {}
});
const list = () => <ul><For each={rows as any}>{(it: any) => <li>{`#${it.id} ${it.title} (${it.assignee})`}</li>}</For></ul>;
render(() => <Loading fallback={<p>loading</p>}>{list()}</Loading>, c);

const steps: [string, () => void][] = [
["mount", () => {}],
["closed list lands", () => pick("closed").res([{ id: 2, title: "Old bug", assignee: "eve" }])],
["assign #2 ann", () => assignTo(2, "ann")],
["close #2", () => closeOne(2)],
["assign #2 guest", () => assignTo(2, "guest")],
["that assign lands", () => pick("assign:guest").res({})], // re-asks the closed list
["close declined", () => pick("close").rej(new Error("declined"))],
["assign #2 guest again", () => assignTo(2, "guest")],
["assign ann lands", () => pick("assign:ann").res({})], // re-asks the closed list (joins the one in the air)
["switch to open", () => showOpen()],
["open list lands", () => pick("open").res([{ id: 1, title: "Login fails", assignee: null }, { id: 3, title: "Crash on save", assignee: null }])],
["second assign lands", () => pick("assign:guest").res({})], // re-asks the open list
["open list lands again", () => pick("open").res([{ id: 1, title: "Login fails", assignee: null }, { id: 3, title: "Crash on save", assignee: null }])],
];
for (const [n, f] of steps) { f(); await tick(); snap(n); }
console.log(frames.join("\n"));
expect(c.textContent).toBe("#1 Login fails (null)#3 Crash on save (null)");
});
12 changes: 12 additions & 0 deletions eval/results/desk13-probe-2026-11-13/rx-1.grade.txt
Original file line number Diff line number Diff line change
@@ -0,0 +1,12 @@
== hidden
✗ base: tabs: the settings page loads again when reopened, with a fresh draft
76/77 passed
== overlap
✗ base: tabs: a save that answers after the page was left shows the reloaded settings
37/38 passed
== fuzz
desk13-rx-1: 2/20 seeds found a hole
seed 11 step 44: detail title: "#3 Bravofooter says 2019finding related helpretry assign to ", server "Delta"
tab help ; tab issues ; tab help ; land loadSettings: ; tab issues ; filter closed ; tab help ; land fetchIssues:open ; land fetchIssues:closed ; tab issues ; tab help ; tab issues ; focus ; land fetchIssues:open ; select #4 ; rename Alpha ; rename Bravo ; post me too ; post fixed? ; land fetchIssue:4 ; row-assign 0 ; row-close 0 ; select #3 ; focus ; close ; land fetchComments:4 ; row-close 0
seed 15 step 33: detail title: "#2 long gonefinding related helpretry assign to ann close re", server "Old bug"
land fetchIssues:open ; row-assign 1 ; row-assign 0 ; land assign:3,guest ; land fetchIssues:open ; land loadSettings: ; filter closed ; land assign:1,guest ; land fetchIssues:closed ; user ann ; row-close 0 ; row-close 0 ; row-close 0 ; land closeIssue:2 ; row-close 0 ; select #2 ; land fetchIssues:closed ; assign ; row-close 0 ; land fetchComments:2 ; land closeIssue:2 ; assign ; land fetchI
8 changes: 8 additions & 0 deletions eval/results/desk13-probe-2026-11-13/rx-2.grade.txt
Original file line number Diff line number Diff line change
@@ -0,0 +1,8 @@
== hidden
✗ base: tabs: the settings page loads again when reopened, with a fresh draft
76/77 passed
== overlap
✗ base: tabs: a save that answers after the page was left shows the reloaded settings
37/38 passed
== fuzz
desk13-rx-2: 0/20 seeds found a hole
14 changes: 14 additions & 0 deletions eval/results/desk13-probe-2026-11-13/rx-3.grade.txt
Original file line number Diff line number Diff line change
@@ -0,0 +1,14 @@
== hidden
✗ base: tabs: the settings page loads again when reopened, with a fresh draft
76/77 passed
== overlap
✗ base: tabs: a save that answers after the page was left shows the reloaded settings
37/38 passed
== fuzz
desk13-rx-3: 3/20 seeds found a hole
seed 11 step -1: page crash: session timed out (120 s): the page hangs

seed 15 step 33: detail title: "#2 long gonefinding related helpretryassign to ann renameclo", server "Old bug"
land fetchIssues:open ; row-assign 1 ; row-assign 0 ; land assign:3,guest ; land fetchIssues:open ; land loadSettings: ; filter closed ; land assign:1,guest ; land fetchIssues:closed ; user ann ; row-close 0 ; row-close 0 ; row-close 0 ; land closeIssue:2 ; row-close 0 ; select #2 ; land fetchIssues:closed ; assign ; row-close 0 ; land fetchComments:2 ; land closeIssue:2 ; assign ; land fetchI
seed 19 step -1: page crash: session timed out (120 s): the page hangs

9 changes: 9 additions & 0 deletions eval/results/desk13-probe-2026-11-13/rx-4.grade.txt
Original file line number Diff line number Diff line change
@@ -0,0 +1,9 @@
== hidden
77/77 passed
== overlap
✗ base: tabs: a save that answers after the page was left shows the reloaded settings
37/38 passed
== fuzz
desk13-rx-4: 1/20 seeds found a hole
seed 13 step 7: count: tab Issues (3), 0 open rows shown
filter closed ; fail fetchIssues:open ; land fetchIssues:closed ; row-assign 0 ; filter open ; land assign:2,guest ; land fetchIssues:open
9 changes: 9 additions & 0 deletions eval/results/desk13-probe-2026-11-13/rx-5.grade.txt
Original file line number Diff line number Diff line change
@@ -0,0 +1,9 @@
== hidden
77/77 passed
== overlap
✗ base: tabs: a save that answers after the page was left shows the reloaded settings
37/38 passed
== fuzz
desk13-rx-5: 1/20 seeds found a hole
seed 17 step 8: count: tab Issues (2), 1 open rows shown
user bob ; land fetchIssues:open ; row-assign 1 ; land assign:3,bob ; land fetchIssues:open ; row-close 2 ; land closeIssue:4 ; row-close 1
10 changes: 10 additions & 0 deletions eval/results/desk13-probe-2026-11-13/rx-6.grade.txt
Original file line number Diff line number Diff line change
@@ -0,0 +1,10 @@
== hidden
77/77 passed
== overlap
✗ base: tabs: a save that answers after the page was left shows the reloaded settings
✗ rows: a row action that answers after the issues page was left changes nothing on its return
36/38 passed
== fuzz
desk13-rx-6: 1/20 seeds found a hole
seed 16 step 34: count: tab Issues (3), 0 open rows shown
user guest ; fail fetchIssues:open ; focus ; tab help ; tab issues ; filter closed ; land fetchIssues:open ; focus ; land fetchIssues:closed ; select #2 ; land fetchComments:2 ; close ; user ann ; fail closeIssue:2 ; row-assign 0 ; close ; user bob ; user guest ; assign ; land assign:2,guest ; land fetchLabels:2 ; fail closeIssue:2 ; assign ; land assign:2,ann ; focus ; land fetchIssue:2 ; lan
8 changes: 8 additions & 0 deletions eval/results/desk13-probe-2026-11-13/rx-7.grade.txt
Original file line number Diff line number Diff line change
@@ -0,0 +1,8 @@
== hidden
✗ base: help: selecting after a failure shows loading…, not the old failure
✗ base: help: selecting after not found shows loading…, not the old message
75/77 passed
== overlap
38/38 passed
== fuzz
desk13-rx-7: 0/20 seeds found a hole
8 changes: 8 additions & 0 deletions eval/results/desk13-probe-2026-11-13/rx-8.grade.txt
Original file line number Diff line number Diff line change
@@ -0,0 +1,8 @@
== hidden
77/77 passed
== overlap
38/38 passed
== fuzz
desk13-rx-8: 1/20 seeds found a hole
seed 16 step 34: count: tab Issues (3), 0 open rows shown
user guest ; fail fetchIssues:open ; focus ; tab help ; tab issues ; filter closed ; land fetchIssues:open ; focus ; land fetchIssues:closed ; select #2 ; land fetchComments:2 ; close ; user ann ; fail closeIssue:2 ; row-assign 0 ; close ; user bob ; user guest ; assign ; land assign:2,guest ; land fetchLabels:2 ; fail closeIssue:2 ; assign ; land assign:2,ann ; focus ; land fetchIssue:2 ; lan
6 changes: 6 additions & 0 deletions eval/results/desk13-probe-2026-11-13/rx-9.grade.txt
Original file line number Diff line number Diff line change
@@ -0,0 +1,6 @@
== hidden
77/77 passed
== overlap
38/38 passed
== fuzz
desk13-rx-9: 0/20 seeds found a hole
Loading