Compare commits

...
Author SHA1 Message Date
Brendan Allan 9dbba2fa54 feat(core): restore session archiving 2026-10-07 22:33:20 +08:00
17 changed files with 312 additions and 29 deletions

No files matched your search

+30
View File
@@ -118,6 +118,7 @@ export type SessionListInput = {
readonly order?: "asc" | "desc" | undefined
readonly search?: string | undefined
readonly parentID?: Session.ID | null | undefined
readonly archived?: boolean | undefined
readonly directory?: AbsolutePath | undefined
readonly project?: Project.ID | undefined
readonly subpath?: RelativePath | undefined
@@ -254,6 +255,7 @@ export type SessionUpdateInput = {
readonly title?: string | undefined
readonly metadata?: Session.Metadata | undefined
readonly permissions?: Permission.Ruleset | undefined
readonly archived?: boolean | undefined
}
export type SessionUpdateOutput = void
export type SessionUpdateOperation<E = never> = (input: SessionUpdateInput) => Effect.Effect<SessionUpdateOutput, E>
@@ -570,6 +572,34 @@ export type SessionLogOutput =
| undefined
readonly data: { readonly sessionID: Session.ID; readonly idle: number }
}
| {
readonly id: Event.ID
readonly created: number
readonly metadata?: { readonly [x: string]: unknown } | undefined
readonly type: "session.archived"
readonly durable: { readonly aggregateID: string; readonly seq: Event.Seq; readonly version: Event.Version }
readonly location?:
| {
readonly directory: AbsolutePath
readonly workspaceID?: (string & Brand.Brand<"Workspace.ID">) | undefined
}
| undefined
readonly data: { readonly sessionID: Session.ID }
}
| {
readonly id: Event.ID
readonly created: number
readonly metadata?: { readonly [x: string]: unknown } | undefined
readonly type: "session.unarchived"
readonly durable: { readonly aggregateID: string; readonly seq: Event.Seq; readonly version: Event.Version }
readonly location?:
| {
readonly directory: AbsolutePath
readonly workspaceID?: (string & Brand.Brand<"Workspace.ID">) | undefined
}
| undefined
readonly data: { readonly sessionID: Session.ID }
}
| {
readonly id: Event.ID
readonly created: number
@@ -366,6 +366,7 @@ const EndpointSessionList = (raw: RawClient["server.session"]) => (input?: Sessi
order: input?.["order"],
search: input?.["search"],
parentID: input?.["parentID"],
archived: input?.["archived"],
directory: input?.["directory"],
project: input?.["project"],
subpath: input?.["subpath"],
@@ -474,7 +475,12 @@ const EndpointSessionUpdate = (raw: RawClient["server.session"]) => (input: Sess
preserveEffect<SessionUpdateOutput>()(
raw["session.update"]({
params: { sessionID: input["sessionID"] },
payload: { title: input["title"], metadata: input["metadata"], permissions: input["permissions"] },
payload: {
title: input["title"],
metadata: input["metadata"],
permissions: input["permissions"],
archived: input["archived"],
},
}).pipe(Effect.mapError(mapClientError)),
)
@@ -543,6 +543,7 @@ export function make(options: ClientOptions) {
order: input?.["order"],
search: input?.["search"],
parentID: input?.["parentID"],
archived: input?.["archived"],
directory: input?.["directory"],
project: input?.["project"],
subpath: input?.["subpath"],
@@ -691,7 +692,12 @@ export function make(options: ClientOptions) {
{
method: "PATCH",
path: `/api/session/${encodeURIComponent(input.sessionID)}`,
body: { title: input["title"], metadata: input["metadata"], permissions: input["permissions"] },
body: {
title: input["title"],
metadata: input["metadata"],
permissions: input["permissions"],
archived: input["archived"],
},
successStatus: 204,
declaredStatuses: [400, 401, 404],
empty: true,
@@ -623,6 +623,26 @@ export type SessionViewed = {
data: { sessionID: string; idle: number }
}
export type SessionArchived = {
id: string
created: number
metadata?: { [x: string]: any }
type: "session.archived"
durable: { aggregateID: string; seq: number; version: 1 }
location?: LocationRef
data: { sessionID: string }
}
export type SessionUnarchived = {
id: string
created: number
metadata?: { [x: string]: any }
type: "session.unarchived"
durable: { aggregateID: string; seq: number; version: 1 }
location?: LocationRef
data: { sessionID: string }
}
export type SessionDeleted = {
id: string
created: number
@@ -2378,6 +2398,8 @@ export type SessionEventDurable =
| SessionMetadataUpdated
| SessionPermissions
| SessionViewed
| SessionArchived
| SessionUnarchived
| SessionDeleted
| SessionForked
| SessionInboxDelivered
@@ -2441,6 +2463,8 @@ export type V2Event =
| SessionMetadataUpdated
| SessionPermissions
| SessionViewed
| SessionArchived
| SessionUnarchived
| SessionUsageUpdated
| SessionDeleted
| SessionForked
@@ -2823,6 +2847,7 @@ export type SessionListInput = {
readonly order?: "asc" | "desc" | undefined
readonly search?: string | undefined
readonly parentID?: string | null | undefined
readonly archived?: boolean | undefined
readonly directory?: string | undefined
readonly project?: string | undefined
readonly subpath?: string | undefined
@@ -2833,6 +2858,7 @@ export type SessionListInput = {
readonly order?: "asc" | "desc" | undefined
readonly search?: string | undefined
readonly parentID?: string | null | undefined
readonly archived?: boolean | undefined
readonly directory?: string | undefined
readonly project?: string | undefined
readonly subpath?: string | undefined
@@ -2843,6 +2869,7 @@ export type SessionListInput = {
readonly order?: "asc" | "desc" | undefined
readonly search?: string | undefined
readonly parentID?: string | null | undefined
readonly archived?: boolean | undefined
readonly directory?: string | undefined
readonly project?: string | undefined
readonly subpath?: string | undefined
@@ -2853,16 +2880,29 @@ export type SessionListInput = {
readonly order?: "asc" | "desc" | undefined
readonly search?: string | undefined
readonly parentID?: string | null | undefined
readonly archived?: boolean | undefined
readonly directory?: string | undefined
readonly project?: string | undefined
readonly subpath?: string | undefined
readonly cursor?: string | undefined
}["parentID"]
readonly archived?: {
readonly limit?: number | undefined
readonly order?: "asc" | "desc" | undefined
readonly search?: string | undefined
readonly parentID?: string | null | undefined
readonly archived?: boolean | undefined
readonly directory?: string | undefined
readonly project?: string | undefined
readonly subpath?: string | undefined
readonly cursor?: string | undefined
}["archived"]
readonly directory?: {
readonly limit?: number | undefined
readonly order?: "asc" | "desc" | undefined
readonly search?: string | undefined
readonly parentID?: string | null | undefined
readonly archived?: boolean | undefined
readonly directory?: string | undefined
readonly project?: string | undefined
readonly subpath?: string | undefined
@@ -2873,6 +2913,7 @@ export type SessionListInput = {
readonly order?: "asc" | "desc" | undefined
readonly search?: string | undefined
readonly parentID?: string | null | undefined
readonly archived?: boolean | undefined
readonly directory?: string | undefined
readonly project?: string | undefined
readonly subpath?: string | undefined
@@ -2883,6 +2924,7 @@ export type SessionListInput = {
readonly order?: "asc" | "desc" | undefined
readonly search?: string | undefined
readonly parentID?: string | null | undefined
readonly archived?: boolean | undefined
readonly directory?: string | undefined
readonly project?: string | undefined
readonly subpath?: string | undefined
@@ -2893,6 +2935,7 @@ export type SessionListInput = {
readonly order?: "asc" | "desc" | undefined
readonly search?: string | undefined
readonly parentID?: string | null | undefined
readonly archived?: boolean | undefined
readonly directory?: string | undefined
readonly project?: string | undefined
readonly subpath?: string | undefined
@@ -4123,6 +4166,7 @@ export type SessionUpdateInput = {
readonly permissions?:
| ReadonlyArray<{ readonly action: string; readonly resource: string; readonly effect: "allow" | "deny" | "ask" }>
| undefined
readonly archived?: boolean | undefined
}["title"]
readonly metadata?: {
readonly title?: string | undefined
@@ -4130,6 +4174,7 @@ export type SessionUpdateInput = {
readonly permissions?:
| ReadonlyArray<{ readonly action: string; readonly resource: string; readonly effect: "allow" | "deny" | "ask" }>
| undefined
readonly archived?: boolean | undefined
}["metadata"]
readonly permissions?: {
readonly title?: string | undefined
@@ -4137,7 +4182,16 @@ export type SessionUpdateInput = {
readonly permissions?:
| ReadonlyArray<{ readonly action: string; readonly resource: string; readonly effect: "allow" | "deny" | "ask" }>
| undefined
readonly archived?: boolean | undefined
}["permissions"]
readonly archived?: {
readonly title?: string | undefined
readonly metadata?: { readonly [x: string]: JsonValue } | undefined
readonly permissions?:
| ReadonlyArray<{ readonly action: string; readonly resource: string; readonly effect: "allow" | "deny" | "ask" }>
| undefined
readonly archived?: boolean | undefined
}["archived"]
}
export type SessionUpdateOutput = void
+8
View File
@@ -701,6 +701,14 @@ export function createData(config: CreateDataInput) {
if (store.session.info[event.data.sessionID])
setStore("session", "info", event.data.sessionID, "permissions", event.data.permissions)
return
case "session.archived":
if (store.session.info[event.data.sessionID])
setStore("session", "info", event.data.sessionID, "time", "archived", event.created)
return
case "session.unarchived":
if (store.session.info[event.data.sessionID])
setStore("session", "info", event.data.sessionID, "time", "archived", undefined)
return
case "session.moved": {
const current = store.session.info[event.data.sessionID]
if (current) {
+4
View File
@@ -174,6 +174,8 @@ export interface Interface {
readonly switchAgent: (input: { sessionID: SessionSchema.ID; agent: Agent.ID }) => Effect.Effect<void, NotFoundError>
readonly switchModel: (input: { sessionID: SessionSchema.ID; model: Model.Ref }) => Effect.Effect<void, NotFoundError>
readonly rename: (input: { sessionID: SessionSchema.ID; title: string }) => Effect.Effect<void, NotFoundError>
readonly archive: (sessionID: SessionSchema.ID) => Effect.Effect<void, NotFoundError>
readonly unarchive: (sessionID: SessionSchema.ID) => Effect.Effect<void, NotFoundError>
readonly setMetadata: (input: {
sessionID: SessionSchema.ID
metadata: SessionSchema.Metadata
@@ -433,6 +435,8 @@ const layer = Layer.effect(
switchAgent: (input) => sessions.forSession(input.sessionID).switchAgent(input),
switchModel: (input) => sessions.forSession(input.sessionID).switchModel(input),
rename: (input) => sessions.forSession(input.sessionID).rename(input),
archive: (sessionID) => sessions.forSession(sessionID).archive(),
unarchive: (sessionID) => sessions.forSession(sessionID).unarchive(),
setMetadata: (input) => sessions.forSession(input.sessionID).setMetadata(input),
setPermissions: (input) => sessions.forSession(input.sessionID).setPermissions(input),
move: moves.move,
+1 -1
View File
@@ -58,7 +58,7 @@ export function fromRow(row: typeof SessionTable.$inferSelect): SessionSchema.In
updated: DateTime.makeUnsafe(row.time_updated),
idle: row.time_idle === null ? undefined : DateTime.makeUnsafe(row.time_idle),
viewed: row.time_viewed === null ? undefined : DateTime.makeUnsafe(row.time_viewed),
archived: row.time_archived ? DateTime.makeUnsafe(row.time_archived) : undefined,
archived: row.time_archived === null ? undefined : DateTime.makeUnsafe(row.time_archived),
},
})
}
@@ -80,6 +80,8 @@ export function update(adapter: Adapter, event: SessionEvent.DurableEvent) {
Match.discriminatorsExhaustive("type")({
"session.created": () => Effect.void,
"session.viewed": () => Effect.void,
"session.archived": () => Effect.void,
"session.unarchived": () => Effect.void,
"session.message.content.updated": (event) =>
updateOwnedAssistant(event.data.messageID, (draft) => {
draft.content = castDraft(
+16
View File
@@ -603,6 +603,22 @@ const layer = Layer.effectDiscard(
.run()
.pipe(Effect.orDie)
})
yield* bus.project(SessionEvent.Archived, (event) =>
db
.update(SessionTable)
.set({ time_archived: event.created, time_updated: event.created })
.where(eq(SessionTable.id, event.data.sessionID))
.run()
.pipe(Effect.orDie),
)
yield* bus.project(SessionEvent.Unarchived, (event) =>
db
.update(SessionTable)
.set({ time_archived: null, time_updated: event.created })
.where(eq(SessionTable.id, event.data.sessionID))
.run()
.pipe(Effect.orDie),
)
yield* bus.project(SessionEvent.MessageContentUpdated, (event) => run(db, event))
yield* bus.project(SessionEvent.UsageRecorded, (event) => applyUsage(db, event.data.sessionID, event.data))
yield* bus.project(SessionEvent.Forked, (event) => projectFork(db, event))
+16
View File
@@ -72,6 +72,16 @@ export const make = Effect.fn("Session.make")(function* () {
yield* get(sessionID)
yield* bus.publish(SessionEvent.Renamed, { sessionID, title: input.title })
})
const archive = Effect.fn("Session.archive")(function* (sessionID: SessionSchema.ID) {
const session = yield* get(sessionID)
if (session.time.archived) return
yield* bus.publish(SessionEvent.Archived, { sessionID })
})
const unarchive = Effect.fn("Session.unarchive")(function* (sessionID: SessionSchema.ID) {
const session = yield* get(sessionID)
if (!session.time.archived) return
yield* bus.publish(SessionEvent.Unarchived, { sessionID })
})
const setMetadata = Effect.fn("Session.setMetadata")(function* (
sessionID: SessionSchema.ID,
input: { metadata: SessionSchema.Metadata },
@@ -348,6 +358,8 @@ export const make = Effect.fn("Session.make")(function* () {
message,
view,
rename,
archive,
unarchive,
setMetadata,
setPermissions,
switchAgent,
@@ -372,6 +384,8 @@ export const make = Effect.fn("Session.make")(function* () {
const message = operations.message.bind(undefined, sessionID)
const view = operations.view.bind(undefined, sessionID)
const rename = operations.rename.bind(undefined, sessionID)
const archive = operations.archive.bind(undefined, sessionID)
const unarchive = operations.unarchive.bind(undefined, sessionID)
const setMetadata = operations.setMetadata.bind(undefined, sessionID)
const setPermissions = operations.setPermissions.bind(undefined, sessionID)
const switchAgent = operations.switchAgent.bind(undefined, sessionID)
@@ -399,6 +413,8 @@ export const make = Effect.fn("Session.make")(function* () {
message,
view,
rename,
archive,
unarchive,
setMetadata,
setPermissions,
switchAgent,
+3
View File
@@ -20,6 +20,7 @@ const ListInputBase = {
limit: PositiveInt.pipe(Schema.optional),
order: Schema.Literals(["asc", "desc"]).pipe(Schema.optional),
parentID: Schema.NullOr(Session.ID).pipe(Schema.optional),
archived: Schema.Boolean.pipe(Schema.optional),
anchor: Session.ListAnchor.pipe(Schema.optional),
}
@@ -111,6 +112,8 @@ const layer = Layer.effect(
conditions.push(
input.parentID === null ? isNull(SessionTable.parent_id) : eq(SessionTable.parent_id, input.parentID),
)
if (input.archived !== undefined)
conditions.push(input.archived ? isNotNull(SessionTable.time_archived) : isNull(SessionTable.time_archived))
if (input.anchor) {
conditions.push(
order === "asc"
@@ -0,0 +1,81 @@
import { describe, expect } from "bun:test"
import { Bus } from "@opencode/core/bus"
import { Database } from "@opencode/core/database/database"
import { AppNodeBuilder } from "@opencode/core/effect/app-node-builder"
import { EventTable } from "@opencode/core/event/sql"
import { Location } from "@opencode/core/location"
import { Project } from "@opencode/core/project"
import { AbsolutePath } from "@opencode/core/schema"
import { Session } from "@opencode/core/session"
import { SessionEvent } from "@opencode/core/session/event"
import { SessionExecution } from "@opencode/core/session/execution"
import { SessionProjector } from "@opencode/core/session/projector"
import { SessionStore } from "@opencode/core/session/store"
import { LayerNode } from "@opencode/util/effect/layer-node"
import { Effect } from "effect"
import { eq } from "drizzle-orm"
import { testEffect } from "./lib/effect"
import { globalProjectNode } from "./lib/project"
const it = testEffect(
AppNodeBuilder.build(
LayerNode.group([Database.node, Bus.node, SessionProjector.node, SessionStore.node, Session.node]),
[
Bus.node.replace(Bus.configured({ persist: true })),
Project.node.replace(globalProjectNode),
SessionExecution.node.replace(SessionExecution.noopLayer),
],
),
)
const location = Location.Ref.make({ directory: AbsolutePath.make("/project") })
describe("Session.archive", () => {
it.effect("archives and unarchives through one durable event each", () =>
Effect.gen(function* () {
const session = yield* Session.Service
const { db } = yield* Database.Service
const created = yield* session.create({ location })
expect(created.time.archived).toBeUndefined()
yield* session.archive(created.id)
yield* session.archive(created.id)
expect((yield* session.get(created.id)).time.archived).toBeDefined()
yield* session.unarchive(created.id)
yield* session.unarchive(created.id)
expect((yield* session.get(created.id)).time.archived).toBeUndefined()
const types = (yield* db
.select({ type: EventTable.type })
.from(EventTable)
.where(eq(EventTable.aggregate_id, created.id))
.all()).map((event) => event.type)
expect(types.filter((type) => type === Bus.versionedType(SessionEvent.Archived.type, 1))).toHaveLength(1)
expect(types.filter((type) => type === Bus.versionedType(SessionEvent.Unarchived.type, 1))).toHaveLength(1)
}),
)
it.effect("filters listed sessions by archive state", () =>
Effect.gen(function* () {
const session = yield* Session.Service
const active = yield* session.create({ location })
const archived = yield* session.create({ location })
yield* session.archive(archived.id)
const ids = (input?: Session.ListInput) =>
session.list(input).pipe(Effect.map((page) => page.data.map((item) => item.id).toSorted()))
expect(yield* ids()).toEqual([active.id, archived.id].toSorted())
expect(yield* ids({ archived: false })).toEqual([active.id])
expect(yield* ids({ archived: true })).toEqual([archived.id])
}),
)
it.effect("rejects an unknown session", () =>
Effect.gen(function* () {
const session = yield* Session.Service
const sessionID = Session.ID.make("ses_missing_archive")
expect(yield* Effect.flip(session.archive(sessionID))).toEqual(new Session.NotFoundError({ sessionID }))
expect(yield* Effect.flip(session.unarchive(sessionID))).toEqual(new Session.NotFoundError({ sessionID }))
}),
)
})
+14 -7
View File
@@ -56,6 +56,13 @@ const ParentIDFilter = Schema.Union([
description: "Filter by parent session. Use null to return only root sessions.",
})
const BooleanFromString = Schema.Literals(["true", "false"]).pipe(
Schema.decodeTo(Schema.Boolean, {
decode: SchemaGetter.transform((value) => value === "true"),
encode: SchemaGetter.transform((value): "true" | "false" => (value ? "true" : "false")),
}),
)
const SessionsQueryFields = {
limit: Schema.NumberFromString.pipe(Schema.decodeTo(PositiveInt), Schema.optional).annotate({
description: "Maximum number of sessions to return. Defaults to the newest 50 sessions.",
@@ -65,6 +72,10 @@ const SessionsQueryFields = {
}),
search: Schema.optional(Schema.String),
parentID: ParentIDFilter.pipe(Schema.optional),
archived: BooleanFromString.pipe(Schema.optional).annotate({
description:
"Filter by archive state. Use true to return only archived sessions or false to exclude them. Omit to include both.",
}),
}
const SessionsDirectoryQuery = Schema.Struct({
@@ -152,13 +163,6 @@ const FormCreatePayload = Schema.Struct({
fields: Form.Info.fields.fields,
}).annotate({ identifier: "Form.CreatePayload" })
const BooleanFromString = Schema.Literals(["true", "false"]).pipe(
Schema.decodeTo(Schema.Boolean, {
decode: SchemaGetter.transform((value) => value === "true"),
encode: SchemaGetter.transform((value): "true" | "false" => (value ? "true" : "false")),
}),
)
const SessionsQueryCursor = SessionsCursor.annotate({
description: "Opaque pagination cursor returned as cursor.previous or cursor.next in the previous response.",
})
@@ -363,6 +367,9 @@ export const makeSessionGroup = <I extends HttpApiMiddleware.AnyId, S, FormI ext
title: Schema.String.pipe(Schema.optional),
metadata: Session.Metadata.pipe(Schema.optional),
permissions: Permission.Ruleset.pipe(Schema.optional),
archived: Schema.Boolean.pipe(Schema.optional).annotate({
description: "Archive the session with true or restore it with false. Repeating the current state is a no-op.",
}),
}),
success: HttpApiSchema.NoContent,
error: SessionNotFoundError,
+16
View File
@@ -139,6 +139,20 @@ export const Viewed = Event.durable({
})
export type Viewed = typeof Viewed.Type
export const Archived = Event.durable({
type: "session.archived",
...options,
schema: Base,
})
export type Archived = typeof Archived.Type
export const Unarchived = Event.durable({
type: "session.unarchived",
...options,
schema: Base,
})
export type Unarchived = typeof Unarchived.Type
// Replay-only: older releases allowed replacing completed assistant content.
export const MessageContentUpdated = Event.durable({
type: "session.message.content.updated",
@@ -658,6 +672,8 @@ export const Definitions = Event.inventory(
MetadataUpdated,
Permissions,
Viewed,
Archived,
Unarchived,
UsageUpdated,
Deleted,
Forked,
@@ -111,6 +111,8 @@ describe("public event manifest", () => {
"session.metadata.updated.1",
"session.permissions.1",
"session.viewed.1",
"session.archived.1",
"session.unarchived.1",
"session.message.content.updated.1",
"session.usage.recorded.1",
"session.forked.2",
+4
View File
@@ -277,6 +277,10 @@ export const SessionHandler = HttpApiBuilder.group(Api, "server.session", (handl
yield* session
.setPermissions({ sessionID: ctx.params.sessionID, permissions: ctx.payload.permissions })
.pipe(Effect.catchTag("Session.NotFoundError", missingSession))
if (ctx.payload.archived !== undefined)
yield* (ctx.payload.archived ? session.archive : session.unarchive)(ctx.params.sessionID).pipe(
Effect.catchTag("Session.NotFoundError", missingSession),
)
return HttpApiSchema.NoContent.make()
}),
)
+47 -19
View File
@@ -5,28 +5,32 @@ import { it } from "../../core/test/lib/effect"
import { ServerFetch } from "../src/fetch"
const SessionResponse = Schema.Struct({ data: Schema.toEncoded(Session.Info) })
const SessionsResponse = Schema.Struct({ data: Schema.Array(Schema.toEncoded(Session.Info)) })
const makeRequest = Effect.fn(function* () {
const handler = yield* ServerFetch.make({
app: { version: "test" },
database: { path: ":memory:" },
fs: { filewatcher: false },
models: { fetch: false },
})
return (path: string, method: string, body?: unknown) =>
Effect.promise(async () => {
const response = await handler(
new Request(`http://opencode.local${path}`, {
method,
headers: { "content-type": "application/json" },
body: body === undefined ? undefined : JSON.stringify(body),
}),
)
expect(response.status).toBe(method === "PATCH" ? 204 : 200)
return response.status === 204 ? undefined : response.json()
})
})
it.live("updates session metadata through PATCH", () =>
Effect.gen(function* () {
const handler = yield* ServerFetch.make({
app: { version: "test" },
database: { path: ":memory:" },
fs: { filewatcher: false },
models: { fetch: false },
})
const request = (path: string, method: string, body?: unknown) =>
Effect.promise(async () => {
const response = await handler(
new Request(`http://opencode.local${path}`, {
method,
headers: { "content-type": "application/json" },
body: body === undefined ? undefined : JSON.stringify(body),
}),
)
expect(response.status).toBe(method === "PATCH" ? 204 : 200)
return response.status === 204 ? undefined : response.json()
})
const request = yield* makeRequest()
const created = Schema.decodeUnknownSync(SessionResponse)(
yield* request("/api/session", "POST", { metadata: { source: "create", stale: true } }),
)
@@ -36,3 +40,27 @@ it.live("updates session metadata through PATCH", () =>
expect(updated.data.metadata).toEqual({ source: "patch" })
}).pipe(Effect.scoped),
)
it.live("archives and unarchives a session through PATCH", () =>
Effect.gen(function* () {
const request = yield* makeRequest()
const created = Schema.decodeUnknownSync(SessionResponse)(yield* request("/api/session", "POST", {}))
const get = () =>
request(`/api/session/${created.data.id}`, "GET").pipe(
Effect.map((body) => Schema.decodeUnknownSync(SessionResponse)(body).data),
)
const listed = (archived: boolean) =>
request(`/api/session?archived=${archived}`, "GET").pipe(
Effect.map((body) => Schema.decodeUnknownSync(SessionsResponse)(body).data.map((item) => item.id)),
)
yield* request(`/api/session/${created.data.id}`, "PATCH", { archived: true })
expect((yield* get()).time.archived).toBeNumber()
expect(yield* listed(true)).toContain(created.data.id)
expect(yield* listed(false)).not.toContain(created.data.id)
yield* request(`/api/session/${created.data.id}`, "PATCH", { archived: false })
expect((yield* get()).time.archived).toBeUndefined()
expect(yield* listed(false)).toContain(created.data.id)
}).pipe(Effect.scoped),
)