chore: merge dev into v2 (#35591)
Co-authored-by: Frank <frank@anoma.ly> Co-authored-by: Aarav Sareen <96787824+arvsrn@users.noreply.github.com> Co-authored-by: Brendan Allan <git@brendonovich.dev> Co-authored-by: opencode-agent[bot] <opencode-agent[bot]@users.noreply.github.com> Co-authored-by: Jack <jack@anoma.ly> Co-authored-by: Brendan Allan <14191578+Brendonovich@users.noreply.github.com> Co-authored-by: Shoubhit Dash <shoubhit2005@gmail.com> Co-authored-by: opencode-agent[bot] <219766164+opencode-agent[bot]@users.noreply.github.com> Co-authored-by: James Long <longster@gmail.com> Co-authored-by: Dustin Deus <deusdustin@gmail.com> Co-authored-by: starptech <starptech@starptechs-MBP.fritz.box> Co-authored-by: Luke Parker <10430890+Hona@users.noreply.github.com> Co-authored-by: 𝓛𝓲𝓽𝓽𝓵𝓮 𝓕𝓻𝓪𝓷𝓴 <little-frank@opencord.local> Co-authored-by: Dax <mail@thdxr.com> Co-authored-by: usrnk1 <7547651+usrnk1@users.noreply.github.com> Co-authored-by: Jay <53023+jayair@users.noreply.github.com> Co-authored-by: runvip <164729189+runvip@users.noreply.github.com> Co-authored-by: opencode <opencode@sst.dev> Co-authored-by: Julian Coy <julian@ex-machina.co> Co-authored-by: Vladimir Glafirov <vglafirov@gitlab.com> Co-authored-by: Adam <2363879+adamdotdevin@users.noreply.github.com> Co-authored-by: Kit Langton <kit.langton@gmail.com> Co-authored-by: Simon Klee <hello@simonklee.dk> Co-authored-by: Jay <air@live.ca> Co-authored-by: David Hill <1879069+iamdavidhill@users.noreply.github.com>
This commit is contained in:
@@ -38,6 +38,23 @@ describe("prompt submission state", () => {
|
||||
expect(session.context.items()[0]).toMatchObject({ type: "file", path: "src/index.ts" })
|
||||
})
|
||||
|
||||
test("clears the original first-submit prompt after retargeting", () => {
|
||||
const workspace = createPromptState()
|
||||
const session = createPromptState()
|
||||
workspace.set([{ type: "text", content: "first prompt", start: 0, end: 12 }])
|
||||
const submission = createPromptSubmissionState({
|
||||
target: workspace,
|
||||
prompt: workspace.current(),
|
||||
context: [],
|
||||
})
|
||||
|
||||
submission.retarget(session)
|
||||
submission.clear()
|
||||
|
||||
expect(workspace.current()[0]).toMatchObject({ type: "text", content: "" })
|
||||
expect(session.current()[0]).toMatchObject({ type: "text", content: "" })
|
||||
})
|
||||
|
||||
test("does not restore over a prompt edited after submission", () => {
|
||||
const target = createPromptState()
|
||||
target.set([{ type: "text", content: "submitted", start: 0, end: 9 }])
|
||||
|
||||
@@ -0,0 +1,231 @@
|
||||
import { expect, test } from "bun:test"
|
||||
import { createRoot, createSignal } from "solid-js"
|
||||
import { createSessionLineage } from "@/pages/session/session-lineage"
|
||||
|
||||
type Lineage = { session: { id: string; directory: string } }
|
||||
|
||||
const lineageOf = (id: string): Lineage => ({ session: { id, directory: `/dir/${id}` } })
|
||||
|
||||
// Fake sync lineage store: peek reads a reactive cache, resolve returns a
|
||||
// deferred promise the test settles or fails explicitly. The lineage memo is
|
||||
// live (read below), so it recomputes eagerly on cache/status writes — throws
|
||||
// surface at the write site, which is also where the enclosing ErrorBoundary
|
||||
// would see them in the app. Assertions wrap write + read to cover both.
|
||||
function createFixture(initial: Record<string, Lineage> = {}) {
|
||||
const [cache, setCache] = createSignal(initial)
|
||||
const deferred = new Map<string, PromiseWithResolvers<unknown>>()
|
||||
const resolves: string[] = []
|
||||
return {
|
||||
resolves,
|
||||
lineage: {
|
||||
peek: (id: string) => cache()[id],
|
||||
resolve: (id: string) => {
|
||||
resolves.push(id)
|
||||
const entry = deferred.get(id) ?? Promise.withResolvers<unknown>()
|
||||
deferred.set(id, entry)
|
||||
return entry.promise
|
||||
},
|
||||
},
|
||||
settle(id: string) {
|
||||
setCache({ ...cache(), [id]: lineageOf(id) })
|
||||
deferred.get(id)?.resolve(undefined)
|
||||
},
|
||||
fail(id: string, error: unknown) {
|
||||
deferred.get(id)?.reject(error)
|
||||
// The real store does not cache failures: the inflight request entry is
|
||||
// dropped on rejection so the next resolve retries (server-session.ts).
|
||||
deferred.delete(id)
|
||||
},
|
||||
remove(id: string) {
|
||||
const next = { ...cache() }
|
||||
delete next[id]
|
||||
setCache(next)
|
||||
},
|
||||
}
|
||||
}
|
||||
|
||||
// Two microtask ticks: one for the resolve promise handed back by the fixture,
|
||||
// one for the .then/.catch chain inside createSessionLineage.
|
||||
const flush = async () => {
|
||||
await Promise.resolve()
|
||||
await Promise.resolve()
|
||||
}
|
||||
|
||||
test("resolves an uncached session and exposes its lineage", async () => {
|
||||
await createRoot(async (dispose) => {
|
||||
const fixture = createFixture()
|
||||
const current = createSessionLineage(
|
||||
() => "ses_a",
|
||||
() => fixture.lineage,
|
||||
)
|
||||
|
||||
expect(current()).toBeUndefined()
|
||||
await flush()
|
||||
expect(fixture.resolves).toEqual(["ses_a"])
|
||||
|
||||
fixture.settle("ses_a")
|
||||
await flush()
|
||||
expect(current()?.session.id).toBe("ses_a")
|
||||
|
||||
dispose()
|
||||
})
|
||||
})
|
||||
|
||||
// Session tabs on the same server share one route instance, so navigating to
|
||||
// another session changes the id in place; resolution must follow it instead
|
||||
// of reporting the new session as missing.
|
||||
test("re-resolves when navigating to an uncached session without a remount", async () => {
|
||||
await createRoot(async (dispose) => {
|
||||
const fixture = createFixture({ ses_a: lineageOf("ses_a") })
|
||||
const [id, setId] = createSignal("ses_a")
|
||||
const current = createSessionLineage(id, () => fixture.lineage)
|
||||
|
||||
await flush()
|
||||
expect(current()?.session.id).toBe("ses_a")
|
||||
|
||||
expect(() => {
|
||||
setId("ses_b")
|
||||
current()
|
||||
}).not.toThrow()
|
||||
expect(fixture.resolves).toEqual(["ses_b"])
|
||||
|
||||
fixture.settle("ses_b")
|
||||
await flush()
|
||||
expect(current()?.session.id).toBe("ses_b")
|
||||
|
||||
dispose()
|
||||
})
|
||||
})
|
||||
|
||||
// A late failure from a session the user already navigated away from must not
|
||||
// poison the currently viewed session.
|
||||
test("ignores a stale resolution failure after the target changes", async () => {
|
||||
await createRoot(async (dispose) => {
|
||||
const fixture = createFixture()
|
||||
const [id, setId] = createSignal("ses_a")
|
||||
const current = createSessionLineage(id, () => fixture.lineage)
|
||||
|
||||
await flush()
|
||||
setId("ses_b")
|
||||
fixture.fail("ses_a", new Error("Session not found: ses_a"))
|
||||
await flush()
|
||||
|
||||
expect(() => current()).not.toThrow()
|
||||
fixture.settle("ses_b")
|
||||
await flush()
|
||||
expect(current()?.session.id).toBe("ses_b")
|
||||
|
||||
dispose()
|
||||
})
|
||||
})
|
||||
|
||||
test("returning to a pruned session re-resolves instead of throwing not found", async () => {
|
||||
await createRoot(async (dispose) => {
|
||||
const fixture = createFixture()
|
||||
const [id, setId] = createSignal("ses_a")
|
||||
const current = createSessionLineage(id, () => fixture.lineage)
|
||||
|
||||
await flush()
|
||||
fixture.settle("ses_a")
|
||||
await flush()
|
||||
|
||||
setId("ses_b")
|
||||
fixture.settle("ses_b")
|
||||
await flush()
|
||||
|
||||
fixture.remove("ses_a")
|
||||
expect(() => {
|
||||
setId("ses_a")
|
||||
current()
|
||||
}).not.toThrow()
|
||||
expect(fixture.resolves).toEqual(["ses_a", "ses_b", "ses_a"])
|
||||
|
||||
fixture.settle("ses_a")
|
||||
await flush()
|
||||
expect(current()?.session.id).toBe("ses_a")
|
||||
|
||||
dispose()
|
||||
})
|
||||
})
|
||||
|
||||
// A resolution that fails while its session is unfocused must not leave a
|
||||
// poisoned status behind: revisiting that session retries cleanly instead of
|
||||
// rethrowing the stale failure before the retry can start.
|
||||
test("revisiting a session whose resolution failed while unfocused retries cleanly", async () => {
|
||||
await createRoot(async (dispose) => {
|
||||
const fixture = createFixture()
|
||||
const [id, setId] = createSignal("ses_a")
|
||||
const current = createSessionLineage(id, () => fixture.lineage)
|
||||
|
||||
await flush()
|
||||
setId("ses_b")
|
||||
fixture.fail("ses_a", new Error("resolve failed"))
|
||||
await flush()
|
||||
|
||||
expect(() => {
|
||||
setId("ses_a")
|
||||
current()
|
||||
}).not.toThrow()
|
||||
expect(fixture.resolves).toEqual(["ses_a", "ses_b", "ses_a"])
|
||||
|
||||
fixture.settle("ses_a")
|
||||
await flush()
|
||||
expect(current()?.session.id).toBe("ses_a")
|
||||
|
||||
dispose()
|
||||
})
|
||||
})
|
||||
|
||||
// The lineage accessor is reactive: replacing the sync store (for example after
|
||||
// the server context is rebuilt) must gate out the old store's status and
|
||||
// re-resolve against the new one instead of fabricating a not-found.
|
||||
test("re-resolves against a replaced lineage store", async () => {
|
||||
await createRoot(async (dispose) => {
|
||||
const first = createFixture()
|
||||
const second = createFixture()
|
||||
const [store, setStore] = createSignal(first.lineage)
|
||||
const current = createSessionLineage(() => "ses_a", store)
|
||||
|
||||
await flush()
|
||||
first.settle("ses_a")
|
||||
await flush()
|
||||
expect(current()?.session.id).toBe("ses_a")
|
||||
|
||||
expect(() => {
|
||||
setStore(second.lineage)
|
||||
current()
|
||||
}).not.toThrow()
|
||||
await flush()
|
||||
expect(second.resolves).toEqual(["ses_a"])
|
||||
|
||||
second.settle("ses_a")
|
||||
await flush()
|
||||
expect(current()?.session.id).toBe("ses_a")
|
||||
|
||||
dispose()
|
||||
})
|
||||
})
|
||||
|
||||
// The viewed session is pinned in the cache, so disappearing after settlement
|
||||
// means it was deleted; the boundary must show the not found fallback.
|
||||
test("throws not found when the settled session is deleted", async () => {
|
||||
await createRoot(async (dispose) => {
|
||||
const fixture = createFixture()
|
||||
const current = createSessionLineage(
|
||||
() => "ses_a",
|
||||
() => fixture.lineage,
|
||||
)
|
||||
|
||||
await flush()
|
||||
fixture.settle("ses_a")
|
||||
await flush()
|
||||
expect(current()?.session.id).toBe("ses_a")
|
||||
|
||||
expect(() => {
|
||||
fixture.remove("ses_a")
|
||||
current()
|
||||
}).toThrow("Session not found: ses_a")
|
||||
|
||||
dispose()
|
||||
})
|
||||
})
|
||||
@@ -27,6 +27,21 @@ test("reactive count updates preserve measured row sizes", () => {
|
||||
})
|
||||
})
|
||||
|
||||
test("initial rect projects rows before a scroll element connects", () => {
|
||||
createRoot((dispose) => {
|
||||
const virtualizer = createVirtualizer<HTMLDivElement, HTMLDivElement>({
|
||||
count: 100,
|
||||
getScrollElement: () => null,
|
||||
estimateSize: () => 28,
|
||||
initialRect: { width: 0, height: 600 },
|
||||
overscan: 10,
|
||||
})
|
||||
|
||||
expect(virtualizer.getVirtualItems().length).toBeGreaterThan(0)
|
||||
dispose()
|
||||
})
|
||||
})
|
||||
|
||||
test("logical scroll offset includes pending measurement adjustments", () => {
|
||||
createRoot((dispose) => {
|
||||
const virtualizer = createVirtualizer<HTMLDivElement, HTMLDivElement>({
|
||||
|
||||
@@ -0,0 +1,26 @@
|
||||
import { describe, expect, test } from "bun:test"
|
||||
import { createSignal, type JSX } from "solid-js"
|
||||
import { showToastV2, toasterV2 } from "@opencode-ai/ui/v2/toast-v2"
|
||||
|
||||
describe("showToastV2", () => {
|
||||
test("creates no reactive computations at call time", () => {
|
||||
const [tick, setTick] = createSignal(0)
|
||||
let reads = 0
|
||||
const icon = (() => {
|
||||
reads++
|
||||
tick()
|
||||
return undefined
|
||||
}) as unknown as JSX.Element
|
||||
|
||||
const id = showToastV2({ description: "test", icon })
|
||||
|
||||
// Resolving the icon at call time creates an ownerless computation that is
|
||||
// never disposed and tracks its dependencies forever; it must only resolve
|
||||
// once the toast component renders.
|
||||
expect(reads).toBe(0)
|
||||
setTick(1)
|
||||
expect(reads).toBe(0)
|
||||
|
||||
toasterV2.dismiss(id)
|
||||
})
|
||||
})
|
||||
Reference in New Issue
Block a user