Merge remote-tracking branch 'origin/v2' into service-restart
This commit is contained in:
commit
54e2b7e0a7
40 changed files with 1832 additions and 1007 deletions
|
|
@ -4,10 +4,11 @@ import { testRender } from "@opentui/solid"
|
|||
import type { OpenCodeEvent } from "@opencode-ai/client"
|
||||
import { SessionMessage } from "@opencode-ai/core/session/message"
|
||||
import { EventV2 } from "@opencode-ai/core/event"
|
||||
import { onMount } from "solid-js"
|
||||
import { createEffect, onMount, type ParentProps } from "solid-js"
|
||||
import { ProjectProvider } from "../../../src/context/project"
|
||||
import { ClientProvider, useClient } from "../../../src/context/client"
|
||||
import { DataProvider, useData } from "../../../src/context/data"
|
||||
import { DataProvider as DataProviderBase, useData } from "../../../src/context/data"
|
||||
import { LocationProvider, useSetLocation } from "../../../src/context/location"
|
||||
import { createSessionRows, type SessionRow } from "../../../src/routes/session/rows"
|
||||
import { createApi, createEventStream, createFetch, directory, json } from "../../fixture/tui-client"
|
||||
import { TestTuiContexts } from "../../fixture/tui-environment"
|
||||
|
|
@ -32,6 +33,24 @@ function emitEvent(events: ReturnType<typeof createEventStream>, event: OpenCode
|
|||
events.emit({ ...event, location: { directory } })
|
||||
}
|
||||
|
||||
function DataProvider(props: ParentProps) {
|
||||
return (
|
||||
<DataProviderBase>
|
||||
<LocationProvider>
|
||||
<SyncLocation />
|
||||
{props.children}
|
||||
</LocationProvider>
|
||||
</DataProviderBase>
|
||||
)
|
||||
}
|
||||
|
||||
function SyncLocation() {
|
||||
const data = useData()
|
||||
const setLocation = useSetLocation()
|
||||
createEffect(() => setLocation(data.location.default()))
|
||||
return null
|
||||
}
|
||||
|
||||
function durable(sessionID: string, seq?: number): { aggregateID: string; seq: number; version: 1 }
|
||||
function durable<const Version extends number>(
|
||||
sessionID: string,
|
||||
|
|
@ -64,16 +83,13 @@ test("bootstraps MCP data for the TUI location", async () => {
|
|||
|
||||
try {
|
||||
await wait(() => requests.length === 2)
|
||||
expect(requests.map((url) => url.searchParams.get("location[directory]"))).toEqual([
|
||||
process.cwd(),
|
||||
process.cwd(),
|
||||
])
|
||||
expect(requests.map((url) => url.searchParams.get("location[directory]"))).toEqual([directory, directory])
|
||||
} finally {
|
||||
app.renderer.destroy()
|
||||
}
|
||||
})
|
||||
|
||||
test("refreshes MCP status when a connection settles during bootstrap", async () => {
|
||||
test("syncs MCP status when a connection settles during bootstrap", async () => {
|
||||
const events = createEventStream()
|
||||
let mcpRequests = 0
|
||||
let resolveModels!: (response: Response) => void
|
||||
|
|
@ -190,9 +206,9 @@ test("refreshes resources into reactive getters", async () => {
|
|||
expect(data.session.get("ses_test")).toBeUndefined()
|
||||
expect(data.location.agent.list(location)).toBeUndefined()
|
||||
|
||||
await data.session.refresh("ses_test")
|
||||
await data.session.message.refresh("ses_test")
|
||||
await data.location.agent.refresh()
|
||||
await data.session.sync("ses_test")
|
||||
await data.session.message.sync("ses_test")
|
||||
await data.location.agent.sync()
|
||||
|
||||
expect(data.session.get("ses_test")?.title).toBe("Test session")
|
||||
expect(data.session.message.list("ses_test").map((message) => message.id)).toEqual(["msg_first", "msg_second"])
|
||||
|
|
@ -243,7 +259,7 @@ test("applies absolute usage events to session info", async () => {
|
|||
))
|
||||
|
||||
try {
|
||||
await data.session.refresh(sessionID)
|
||||
await data.session.sync(sessionID)
|
||||
emitEvent(events, {
|
||||
id: "evt_usage_2",
|
||||
created: 2,
|
||||
|
|
@ -328,7 +344,7 @@ test("truncates committed revert messages without changing lifetime usage", asyn
|
|||
))
|
||||
|
||||
try {
|
||||
await data.session.refresh(sessionID)
|
||||
await data.session.sync(sessionID)
|
||||
emitEvent(events, {
|
||||
id: "evt_revert_boundary_started",
|
||||
created: 1,
|
||||
|
|
@ -467,7 +483,7 @@ test("updates session location when moved", async () => {
|
|||
|
||||
try {
|
||||
await mounted
|
||||
await data.session.refresh("ses_test")
|
||||
await data.session.sync("ses_test")
|
||||
emitEvent(events, {
|
||||
id: "evt_moved_1",
|
||||
created: 1,
|
||||
|
|
@ -525,7 +541,7 @@ test("restores running manual compaction before applying live deltas", async ()
|
|||
))
|
||||
|
||||
try {
|
||||
await data.session.message.refresh("session-compaction")
|
||||
await data.session.message.sync("session-compaction")
|
||||
expect(data.session.message.get("session-compaction", "message-compaction")).toMatchObject({
|
||||
type: "compaction",
|
||||
status: "running",
|
||||
|
|
@ -548,7 +564,7 @@ test("restores running manual compaction before applying live deltas", async ()
|
|||
}
|
||||
})
|
||||
|
||||
test("reconnects the event stream and bootstraps fresh data", async () => {
|
||||
test("reconnects the event stream and resyncs active data", async () => {
|
||||
const events = createEventStream()
|
||||
const requests = { active: 0, event: 0, message: 0, model: 0 }
|
||||
let resolveActive!: (response: Response) => void
|
||||
|
|
@ -621,7 +637,7 @@ test("reconnects the event stream and bootstraps fresh data", async () => {
|
|||
try {
|
||||
await wait(() => data.location.model.list()?.[0]?.id === "model-1")
|
||||
await wait(() => data.session.status("session-stale") === "running")
|
||||
await data.session.message.refresh("session-stale")
|
||||
await data.session.message.sync("session-stale")
|
||||
expect(data.session.message.get("session-stale", "message-stale")?.id).toBe("message-stale")
|
||||
expect(client.connection.status()).toBe("connected")
|
||||
expect(client.connection.attempt()).toBe(0)
|
||||
|
|
@ -633,6 +649,7 @@ test("reconnects the event stream and bootstraps fresh data", async () => {
|
|||
|
||||
await wait(() => requests.active === 2 && client.connection.status() === "connected", 4000)
|
||||
resolveActive(json({ data: { "session-new": { type: "running" } } }))
|
||||
void data.session.message.sync("session-stale")
|
||||
|
||||
await wait(() => data.location.model.list()?.[0]?.id === "model-2", 4000)
|
||||
await wait(() => data.session.status("session-stale") === "idle")
|
||||
|
|
@ -664,8 +681,10 @@ test("completes exploration when a queued prompt is promoted", async () => {
|
|||
if (url.pathname === `/api/session/${sessionID}/message`) return json({ data: [], cursor: {} })
|
||||
}, events)
|
||||
let rows!: ReturnType<typeof createSessionRows>
|
||||
let client!: ReturnType<typeof useClient>
|
||||
|
||||
function Probe() {
|
||||
client = useClient()
|
||||
rows = createSessionRows(() => sessionID)
|
||||
return <box />
|
||||
}
|
||||
|
|
@ -683,6 +702,7 @@ test("completes exploration when a queued prompt is promoted", async () => {
|
|||
))
|
||||
|
||||
try {
|
||||
await wait(() => client.connection.status() === "connected")
|
||||
emitEvent(events, {
|
||||
id: "evt_step_started",
|
||||
created: 1,
|
||||
|
|
@ -954,7 +974,7 @@ test("tracks session status from active sessions and execution events", async ()
|
|||
try {
|
||||
await wait(() => data.session.status("session-active") === "running")
|
||||
expect(data.session.status("session-idle")).toBe("idle")
|
||||
await data.session.refresh("session-live")
|
||||
await data.session.sync("session-live")
|
||||
|
||||
settled = true
|
||||
emitEvent(events, {
|
||||
|
|
@ -1021,7 +1041,7 @@ test("tracks session status from active sessions and execution events", async ()
|
|||
})
|
||||
await wait(() => data.session.status("session-live") === "idle")
|
||||
|
||||
await data.session.refresh("session-failed")
|
||||
await data.session.sync("session-failed")
|
||||
emitEvent(events, {
|
||||
id: "evt_failed_execution_started",
|
||||
created: 0,
|
||||
|
|
@ -1184,7 +1204,7 @@ test("tracks session status from active sessions and execution events", async ()
|
|||
durable: durable("session-manual", 1),
|
||||
data: { sessionID: "session-manual", inputID: "message-compaction" },
|
||||
})
|
||||
await wait(() => data.session.compaction.list("session-manual").includes("message-compaction"))
|
||||
await wait(() => data.session.pending.list("session-manual").some((item) => item.id === "message-compaction"))
|
||||
emitEvent(events, {
|
||||
id: "evt_manual_compaction_started",
|
||||
created: 1,
|
||||
|
|
@ -1202,10 +1222,8 @@ test("tracks session status from active sessions and execution events", async ()
|
|||
const message = data.session.message.get("session-manual", "message-compaction")
|
||||
return message?.type === "compaction" && message.status === "running" && message.summary === "Streamed summary"
|
||||
})
|
||||
expect(data.session.compaction.list("session-manual")).toEqual([])
|
||||
const compactionRow = manualRows.find(
|
||||
(row) => row.type === "message" && row.messageID === "message-compaction",
|
||||
)
|
||||
expect(data.session.pending.list("session-manual")).toEqual([])
|
||||
const compactionRow = manualRows.find((row) => row.type === "message" && row.messageID === "message-compaction")
|
||||
emitEvent(events, {
|
||||
id: "evt_manual_compaction_ended",
|
||||
created: 3,
|
||||
|
|
@ -1247,9 +1265,7 @@ test("tracks session status from active sessions and execution events", async ()
|
|||
const message = data.session.message.get("session-live", "msg_compaction_started")
|
||||
return message?.type === "compaction" && message.status === "running" && message.summary === "Live summary"
|
||||
})
|
||||
const autoCompactionRow = rows.find(
|
||||
(row) => row.type === "message" && row.messageID === "msg_compaction_started",
|
||||
)
|
||||
const autoCompactionRow = rows.find((row) => row.type === "message" && row.messageID === "msg_compaction_started")
|
||||
|
||||
emitEvent(events, {
|
||||
id: "evt_compaction_ended",
|
||||
|
|
@ -1301,9 +1317,11 @@ test("restores queued compaction from durable pending input", async () => {
|
|||
}, events)
|
||||
let data!: ReturnType<typeof useData>
|
||||
let rows!: ReturnType<typeof createSessionRows>
|
||||
let client!: ReturnType<typeof useClient>
|
||||
|
||||
function Probe() {
|
||||
data = useData()
|
||||
client = useClient()
|
||||
rows = createSessionRows(() => sessionID)
|
||||
return <box />
|
||||
}
|
||||
|
|
@ -1321,8 +1339,9 @@ test("restores queued compaction from durable pending input", async () => {
|
|||
))
|
||||
|
||||
try {
|
||||
await wait(() => data.session.compaction.list(sessionID).length === 2)
|
||||
expect(data.session.compaction.list(sessionID)).toEqual([
|
||||
await wait(() => client.connection.status() === "connected")
|
||||
await wait(() => data.session.pending.list(sessionID).length === 2)
|
||||
expect(data.session.pending.list(sessionID).map((item) => item.id)).toEqual([
|
||||
"message-compaction-queued",
|
||||
"message-compaction-later",
|
||||
])
|
||||
|
|
@ -1359,8 +1378,8 @@ test("restores queued compaction from durable pending input", async () => {
|
|||
inputID: "message-compaction-queued",
|
||||
},
|
||||
})
|
||||
await wait(() => data.session.compaction.list(sessionID).length === 1)
|
||||
expect(data.session.compaction.list(sessionID)).toEqual(["message-compaction-later"])
|
||||
await wait(() => data.session.pending.list(sessionID).length === 1)
|
||||
expect(data.session.pending.list(sessionID).map((item) => item.id)).toEqual(["message-compaction-later"])
|
||||
|
||||
emitEvent(events, {
|
||||
id: "evt_compaction_ended",
|
||||
|
|
@ -1369,15 +1388,12 @@ test("restores queued compaction from durable pending input", async () => {
|
|||
durable: durable(sessionID, 5),
|
||||
data: { sessionID, reason: "manual", text: "Summary", recent: "" },
|
||||
})
|
||||
expect(data.session.compaction.list(sessionID)).toEqual(["message-compaction-later"])
|
||||
expect(data.session.pending.list(sessionID).map((item) => item.id)).toEqual(["message-compaction-later"])
|
||||
|
||||
pending = []
|
||||
emitEvent(events, {
|
||||
id: "evt_reconnected",
|
||||
type: "server.connected",
|
||||
data: {},
|
||||
})
|
||||
await wait(() => data.session.compaction.list(sessionID).length === 0)
|
||||
data.session.pending.invalidate(sessionID)
|
||||
await data.session.pending.sync(sessionID)
|
||||
await wait(() => data.session.pending.list(sessionID).length === 0)
|
||||
} finally {
|
||||
app.renderer.destroy()
|
||||
}
|
||||
|
|
@ -1689,7 +1705,7 @@ test("keeps shell state scoped to location", async () => {
|
|||
|
||||
try {
|
||||
await wait(() => data.shell.list().some((shell) => shell.id === "sh_default"))
|
||||
await data.shell.refresh({ directory: other })
|
||||
await data.shell.sync({ directory: other })
|
||||
|
||||
expect(data.shell.list().map((shell) => shell.id)).toEqual(["sh_default"])
|
||||
expect(data.shell.list({ directory: other }).map((shell) => shell.id)).toEqual(["sh_other"])
|
||||
|
|
@ -1790,22 +1806,27 @@ test("adds and dismisses permission requests from live events", async () => {
|
|||
}
|
||||
})
|
||||
|
||||
test("reconciles all pending permission requests when the event stream reconnects", async () => {
|
||||
test("reconciles active session permissions when the event stream reconnects", async () => {
|
||||
const events = createEventStream()
|
||||
let requests = [
|
||||
{ id: "per_old", sessionID: "ses_old", action: "read", resources: ["old.txt"] },
|
||||
{ id: "per_keep", sessionID: "ses_keep", action: "shell", resources: ["bun test"] },
|
||||
{ id: "per_old", sessionID: "ses_active", action: "read", resources: ["old.txt"] },
|
||||
{ id: "per_keep", sessionID: "ses_active", action: "shell", resources: ["bun test"] },
|
||||
]
|
||||
let calls = 0
|
||||
const fetch = createFetch((url) => {
|
||||
if (url.pathname !== "/api/permission/request") return
|
||||
if (url.pathname !== "/api/session/ses_active/permission") return
|
||||
calls++
|
||||
return json({ location: { directory, project: { id: "proj_test", directory } }, data: requests })
|
||||
return json({ data: requests })
|
||||
}, events)
|
||||
let data!: ReturnType<typeof useData>
|
||||
|
||||
function Probe() {
|
||||
data = useData()
|
||||
const client = useClient()
|
||||
createEffect(() => {
|
||||
if (client.connection.status() !== "connected") return
|
||||
void data.session.permission.sync("ses_active")
|
||||
})
|
||||
return <box />
|
||||
}
|
||||
|
||||
|
|
@ -1822,15 +1843,12 @@ test("reconciles all pending permission requests when the event stream reconnect
|
|||
))
|
||||
|
||||
try {
|
||||
await wait(() => data.session.permission.list("ses_old")?.[0]?.id === "per_old")
|
||||
expect(data.session.permission.list("ses_keep")?.[0]?.id).toBe("per_keep")
|
||||
await wait(() => data.session.permission.list("ses_active")?.length === 2)
|
||||
|
||||
requests = [{ id: "per_new", sessionID: "ses_new", action: "edit", resources: ["new.txt"] }]
|
||||
requests = [{ id: "per_new", sessionID: "ses_active", action: "edit", resources: ["new.txt"] }]
|
||||
events.disconnect()
|
||||
|
||||
await wait(() => calls === 2 && data.session.permission.list("ses_new")?.[0]?.id === "per_new")
|
||||
expect(data.session.permission.list("ses_old")).toBeUndefined()
|
||||
expect(data.session.permission.list("ses_keep")).toBeUndefined()
|
||||
await wait(() => calls === 2 && data.session.permission.list("ses_active")?.[0]?.id === "per_new")
|
||||
} finally {
|
||||
app.renderer.destroy()
|
||||
}
|
||||
|
|
@ -1903,7 +1921,7 @@ test("adds, dismisses, and refreshes form requests", async () => {
|
|||
})
|
||||
await wait(() => data.session.form.list("ses_1")?.length === 0)
|
||||
|
||||
await data.session.form.refresh("ses_1")
|
||||
await data.session.form.sync("ses_1")
|
||||
expect(data.session.form.list("ses_1")?.map((form) => form.id)).toEqual(["frm_remote"])
|
||||
} finally {
|
||||
app.renderer.destroy()
|
||||
|
|
@ -1975,7 +1993,7 @@ test("tracks global forms by location", async () => {
|
|||
}
|
||||
})
|
||||
|
||||
test("refreshes global forms for the requested location", async () => {
|
||||
test("syncs global forms once for each requested location", async () => {
|
||||
const events = createEventStream()
|
||||
const requests: URL[] = []
|
||||
const other = { directory: "/tmp/opencode-other", workspaceID: "wrk_other" }
|
||||
|
|
@ -2025,20 +2043,24 @@ test("refreshes global forms for the requested location", async () => {
|
|||
await wait(() => client.connection.status() === "connected" && requests.length > 0)
|
||||
requests.length = 0
|
||||
|
||||
await data.session.form.refresh("global", { directory })
|
||||
await data.session.form.refresh("global", other)
|
||||
await data.session.form.sync("global", { directory })
|
||||
await data.session.form.sync("global", other)
|
||||
|
||||
expect(requests).toHaveLength(2)
|
||||
expect(requests[1]?.searchParams.get("location[directory]")).toBe(other.directory)
|
||||
expect(requests[1]?.searchParams.get("location[workspace]")).toBe(other.workspaceID)
|
||||
expect(requests).toHaveLength(1)
|
||||
expect(requests[0]?.searchParams.get("location[directory]")).toBe(other.directory)
|
||||
expect(requests[0]?.searchParams.get("location[workspace]")).toBe(other.workspaceID)
|
||||
expect(data.session.form.list("global", other)?.map((form) => form.id)).toEqual(["frm_other"])
|
||||
expect(data.session.form.list("global", { directory })?.map((form) => form.id)).toEqual(["frm_default"])
|
||||
|
||||
data.session.form.invalidate("global", other)
|
||||
await data.session.form.sync("global", other)
|
||||
expect(requests).toHaveLength(2)
|
||||
} finally {
|
||||
app.renderer.destroy()
|
||||
}
|
||||
})
|
||||
|
||||
test("refreshes global forms once per loaded location after reconnect", async () => {
|
||||
test("resyncs global forms only for the active location after reconnect", async () => {
|
||||
const events = createEventStream()
|
||||
const requests: URL[] = []
|
||||
const counts = new Map<string, number>()
|
||||
|
|
@ -2098,58 +2120,54 @@ test("refreshes global forms once per loaded location after reconnect", async ()
|
|||
))
|
||||
|
||||
try {
|
||||
await wait(
|
||||
() =>
|
||||
data.session.form.list("global", home)?.[0]?.id === "frm_default_1" &&
|
||||
data.session.form.list("global", other)?.[0]?.id === "frm_other_1",
|
||||
)
|
||||
await wait(() => data.session.form.list("global", home)?.[0]?.id === "frm_default_1")
|
||||
await data.session.form.sync("global", other)
|
||||
expect(data.session.form.list("global", other)?.[0]?.id).toBe("frm_other_1")
|
||||
expect(requests).toHaveLength(2)
|
||||
requests.length = 0
|
||||
|
||||
events.disconnect()
|
||||
|
||||
await wait(
|
||||
() =>
|
||||
data.session.form.list("global", home)?.[0]?.id === "frm_default_2" &&
|
||||
data.session.form.list("global", other)?.[0]?.id === "frm_other_2",
|
||||
4000,
|
||||
)
|
||||
expect(requests).toHaveLength(2)
|
||||
await wait(() => data.session.form.list("global", home)?.[0]?.id === "frm_default_2", 4000)
|
||||
expect(data.session.form.list("global", other)?.[0]?.id).toBe("frm_other_1")
|
||||
expect(requests).toHaveLength(1)
|
||||
expect(
|
||||
requests.map((url) => [
|
||||
url.searchParams.get("location[directory]") ?? directory,
|
||||
url.searchParams.get("location[workspace]") ?? undefined,
|
||||
]),
|
||||
).toEqual([
|
||||
[home.directory, undefined],
|
||||
[other.directory, other.workspaceID],
|
||||
])
|
||||
).toEqual([[home.directory, undefined]])
|
||||
} finally {
|
||||
app.renderer.destroy()
|
||||
}
|
||||
})
|
||||
|
||||
test("reconciles all pending form requests when the event stream reconnects", async () => {
|
||||
test("reconciles active session forms when the event stream reconnects", async () => {
|
||||
const events = createEventStream()
|
||||
let requests = [
|
||||
{ id: "frm_old", sessionID: "ses_old", title: "Input requested", fields: formFields },
|
||||
{ id: "frm_old", sessionID: "ses_active", title: "Input requested", fields: formFields },
|
||||
{
|
||||
id: "frm_keep",
|
||||
sessionID: "ses_keep",
|
||||
sessionID: "ses_active",
|
||||
title: "Input requested",
|
||||
fields: [{ key: "authorization", type: "external" as const, url: "https://example.com" }],
|
||||
},
|
||||
]
|
||||
let calls = 0
|
||||
const fetch = createFetch((url) => {
|
||||
if (url.pathname !== "/api/form/request") return
|
||||
if (url.pathname !== "/api/session/ses_active/form") return
|
||||
calls++
|
||||
return json({ location: { directory, project: { id: "proj_test", directory } }, data: requests })
|
||||
return json({ data: requests })
|
||||
}, events)
|
||||
let data!: ReturnType<typeof useData>
|
||||
|
||||
function Probe() {
|
||||
data = useData()
|
||||
const client = useClient()
|
||||
createEffect(() => {
|
||||
if (client.connection.status() !== "connected") return
|
||||
void data.session.form.sync("ses_active")
|
||||
})
|
||||
return <box />
|
||||
}
|
||||
|
||||
|
|
@ -2166,15 +2184,12 @@ test("reconciles all pending form requests when the event stream reconnects", as
|
|||
))
|
||||
|
||||
try {
|
||||
await wait(() => data.session.form.list("ses_old")?.[0]?.id === "frm_old")
|
||||
expect(data.session.form.list("ses_keep")?.[0]?.id).toBe("frm_keep")
|
||||
await wait(() => data.session.form.list("ses_active")?.length === 2)
|
||||
|
||||
requests = [{ id: "frm_new", sessionID: "ses_new", title: "Input requested", fields: formFields }]
|
||||
requests = [{ id: "frm_new", sessionID: "ses_active", title: "Input requested", fields: formFields }]
|
||||
events.disconnect()
|
||||
|
||||
await wait(() => calls === 2 && data.session.form.list("ses_new")?.[0]?.id === "frm_new")
|
||||
expect(data.session.form.list("ses_old")).toBeUndefined()
|
||||
expect(data.session.form.list("ses_keep")).toBeUndefined()
|
||||
await wait(() => calls === 2 && data.session.form.list("ses_active")?.[0]?.id === "frm_new")
|
||||
} finally {
|
||||
app.renderer.destroy()
|
||||
}
|
||||
|
|
@ -2421,7 +2436,7 @@ test("renders admitted prompts immediately and tracks them until promoted", asyn
|
|||
])
|
||||
expect(sync.session.input.list(sessionID)).toEqual([messageID])
|
||||
|
||||
await sync.session.message.refresh(sessionID)
|
||||
await sync.session.message.sync(sessionID)
|
||||
expect(sync.session.message.list(sessionID)?.[0]?.metadata).toBeUndefined()
|
||||
|
||||
emitEvent(events, {
|
||||
|
|
@ -2538,8 +2553,7 @@ function sessionInfo(id: string, parentID: string | undefined, cost = 0) {
|
|||
async function mountData(parents: Record<string, string>, costs: Record<string, number> = {}) {
|
||||
const calls = createFetch((url) => {
|
||||
const match = url.pathname.match(/^\/api\/session\/([^/]+)$/)
|
||||
if (match && match[1] !== "active")
|
||||
return json({ data: sessionInfo(match[1], parents[match[1]], costs[match[1]]) })
|
||||
if (match && match[1] !== "active") return json({ data: sessionInfo(match[1], parents[match[1]], costs[match[1]]) })
|
||||
})
|
||||
let data!: ReturnType<typeof useData>
|
||||
let ready!: () => void
|
||||
|
|
@ -2569,13 +2583,13 @@ async function mountData(parents: Record<string, string>, costs: Record<string,
|
|||
test("groups an orphan child under its missing parent until the root arrives", async () => {
|
||||
const { data, app } = await mountData({ child: "root" })
|
||||
try {
|
||||
await data.session.refresh("child")
|
||||
await data.session.sync("child")
|
||||
// Parent info is absent, so the missing parent is the furthest-known ancestor.
|
||||
expect(data.session.root("child")).toBe("root")
|
||||
expect(data.session.family("child")).toEqual(["child"])
|
||||
expect(data.session.family("root")).toEqual(["child"])
|
||||
|
||||
await data.session.refresh("root")
|
||||
await data.session.sync("root")
|
||||
expect(data.session.root("root")).toBe("root")
|
||||
// The tentative root entry folds into the now-known root's family.
|
||||
expect(data.session.family("child")).toEqual(["child", "root"])
|
||||
|
|
@ -2588,17 +2602,17 @@ test("groups an orphan child under its missing parent until the root arrives", a
|
|||
test("indexes arbitrarily deep nesting under a single root", async () => {
|
||||
const { data, app } = await mountData({ grandchild: "child", child: "root" })
|
||||
try {
|
||||
await data.session.refresh("grandchild")
|
||||
await data.session.sync("grandchild")
|
||||
expect(data.session.root("grandchild")).toBe("child")
|
||||
expect(data.session.family("grandchild")).toEqual(["grandchild"])
|
||||
|
||||
await data.session.refresh("child")
|
||||
await data.session.sync("child")
|
||||
// grandchild's tentative family (keyed by the missing "child") merges up
|
||||
// toward the still-missing "root".
|
||||
expect(data.session.root("child")).toBe("root")
|
||||
expect(data.session.family("grandchild")).toEqual(["grandchild", "child"])
|
||||
|
||||
await data.session.refresh("root")
|
||||
await data.session.sync("root")
|
||||
expect(data.session.root("grandchild")).toBe("root")
|
||||
expect(data.session.root("child")).toBe("root")
|
||||
expect(data.session.family("root")).toEqual(["grandchild", "child", "root"])
|
||||
|
|
@ -2608,14 +2622,11 @@ test("indexes arbitrarily deep nesting under a single root", async () => {
|
|||
})
|
||||
|
||||
test("totals family cost for roots and keeps subagent cost scoped", async () => {
|
||||
const { data, app } = await mountData(
|
||||
{ grandchild: "child", child: "root" },
|
||||
{ root: 1, child: 2, grandchild: 3 },
|
||||
)
|
||||
const { data, app } = await mountData({ grandchild: "child", child: "root" }, { root: 1, child: 2, grandchild: 3 })
|
||||
try {
|
||||
await data.session.refresh("grandchild")
|
||||
await data.session.refresh("child")
|
||||
await data.session.refresh("root")
|
||||
await data.session.sync("grandchild")
|
||||
await data.session.sync("child")
|
||||
await data.session.sync("root")
|
||||
|
||||
expect(data.session.cost("root")).toBe(6)
|
||||
expect(data.session.cost("child")).toBe(2)
|
||||
|
|
@ -2628,15 +2639,15 @@ test("totals family cost for roots and keeps subagent cost scoped", async () =>
|
|||
test("re-registering an existing session is idempotent", async () => {
|
||||
const { data, app } = await mountData({ grandchild: "child", child: "root" })
|
||||
try {
|
||||
await data.session.refresh("grandchild")
|
||||
await data.session.refresh("child")
|
||||
await data.session.refresh("root")
|
||||
await data.session.sync("grandchild")
|
||||
await data.session.sync("child")
|
||||
await data.session.sync("root")
|
||||
const before = data.session.family("root")
|
||||
expect(before).toEqual(["grandchild", "child", "root"])
|
||||
|
||||
await data.session.refresh("child")
|
||||
await data.session.refresh("root")
|
||||
await data.session.refresh("grandchild")
|
||||
await data.session.sync("child")
|
||||
await data.session.sync("root")
|
||||
await data.session.sync("grandchild")
|
||||
expect(data.session.family("root")).toEqual(before)
|
||||
expect(data.session.family("root")).toHaveLength(3)
|
||||
} finally {
|
||||
|
|
@ -2647,8 +2658,8 @@ test("re-registering an existing session is idempotent", async () => {
|
|||
test("stops at the last non-repeating ancestor on a parent cycle", async () => {
|
||||
const { data, app } = await mountData({ x: "y", y: "x" })
|
||||
try {
|
||||
await data.session.refresh("x")
|
||||
await data.session.refresh("y")
|
||||
await data.session.sync("x")
|
||||
await data.session.sync("y")
|
||||
// Does not hang; walking up from "y" stops before re-entering "x".
|
||||
expect(data.session.root("y")).toBe("x")
|
||||
expect(data.session.family("y")).toEqual(["x", "y"])
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue