feat(core): register built-in skill
This commit is contained in:
parent
0bd61d2826
commit
8a97cb55d3
18 changed files with 159 additions and 41 deletions
|
|
@ -18,9 +18,14 @@ export const Plugin = PluginV2.define({
|
|||
const skill = yield* SkillV2.Service
|
||||
const transform = yield* skill.transform()
|
||||
const entries = yield* config.entries()
|
||||
const directories = entries.flatMap((entry) => (entry.type === "directory" ? [entry.path] : []))
|
||||
const items = entries.flatMap((entry) => (entry.type === "document" ? (entry.info.skills ?? []) : []))
|
||||
|
||||
yield* transform((editor) => {
|
||||
for (const directory of directories) {
|
||||
editor.source(new SkillV2.DirectorySource({ type: "directory", path: AbsolutePath.make(path.join(directory, "skill")) }))
|
||||
editor.source(new SkillV2.DirectorySource({ type: "directory", path: AbsolutePath.make(path.join(directory, "skills")) }))
|
||||
}
|
||||
for (const item of items) {
|
||||
if (URL.canParse(item) && /^(https?:)$/.test(new URL(item).protocol)) {
|
||||
editor.source(new SkillV2.UrlSource({ type: "url", url: item }))
|
||||
|
|
|
|||
4
packages/core/src/markdown.d.ts
vendored
Normal file
4
packages/core/src/markdown.d.ts
vendored
Normal file
|
|
@ -0,0 +1,4 @@
|
|||
declare module "*.md" {
|
||||
const content: string
|
||||
export default content
|
||||
}
|
||||
|
|
@ -19,6 +19,7 @@ import { PluginV2 } from "../plugin"
|
|||
import { AccountPlugin } from "./account"
|
||||
import { AgentPlugin } from "./agent"
|
||||
import { CommandPlugin } from "./command"
|
||||
import { SkillPlugin } from "./skill"
|
||||
import { ConfigProviderPlugin } from "../config/plugin/provider"
|
||||
import { EnvPlugin } from "./env"
|
||||
import { ModelsDevPlugin } from "./models-dev"
|
||||
|
|
@ -94,6 +95,7 @@ export const layer = Layer.effect(
|
|||
yield* add(AccountPlugin)
|
||||
yield* add(AgentPlugin.Plugin)
|
||||
yield* add(CommandPlugin.Plugin)
|
||||
yield* add(SkillPlugin.Plugin)
|
||||
for (const item of ProviderPlugins) {
|
||||
yield* add(item)
|
||||
}
|
||||
|
|
|
|||
30
packages/core/src/plugin/skill.ts
Normal file
30
packages/core/src/plugin/skill.ts
Normal file
|
|
@ -0,0 +1,30 @@
|
|||
export * as SkillPlugin from "./skill"
|
||||
|
||||
import { Effect } from "effect"
|
||||
import { PluginV2 } from "../plugin"
|
||||
import { AbsolutePath } from "../schema"
|
||||
import { SkillV2 } from "../skill"
|
||||
import CUSTOMIZE_OPENCODE_SKILL_BODY from "./skill/customize-opencode.md" with { type: "text" }
|
||||
|
||||
export const Plugin = PluginV2.define({
|
||||
id: PluginV2.ID.make("skill"),
|
||||
effect: Effect.gen(function* () {
|
||||
const skill = yield* SkillV2.Service
|
||||
const transform = yield* skill.transform()
|
||||
|
||||
yield* transform((editor) => {
|
||||
editor.source(
|
||||
new SkillV2.EmbeddedSource({
|
||||
type: "embedded",
|
||||
skill: new SkillV2.Info({
|
||||
name: "customize-opencode",
|
||||
description:
|
||||
"Use ONLY when the user is editing or creating opencode's own configuration: opencode.json, opencode.jsonc, files under .opencode/, or files under ~/.config/opencode/. Also use when creating or fixing opencode agents, subagents, skills, plugins, MCP servers, or permission rules. Do not use for the user's own application code, or for any project that is not configuring opencode itself.",
|
||||
location: AbsolutePath.make("/builtin/customize-opencode.md"),
|
||||
content: CUSTOMIZE_OPENCODE_SKILL_BODY,
|
||||
}),
|
||||
}),
|
||||
)
|
||||
})
|
||||
}),
|
||||
})
|
||||
|
|
@ -1,6 +1,6 @@
|
|||
<!--
|
||||
Built-in skill. Name and description are registered in code at
|
||||
packages/opencode/src/skill/index.ts (see CUSTOMIZE_OPENCODE_SKILL_NAME
|
||||
packages/core/src/plugin/skill.ts
|
||||
and CUSTOMIZE_OPENCODE_SKILL_DESCRIPTION). The body below becomes the
|
||||
skill's content.
|
||||
-->
|
||||
|
|
@ -21,17 +21,23 @@ export class UrlSource extends Schema.Class<UrlSource>("SkillV2.UrlSource")({
|
|||
url: Schema.String,
|
||||
}) {}
|
||||
|
||||
export const Source = Schema.Union([DirectorySource, UrlSource]).pipe(
|
||||
export class EmbeddedSource extends Schema.Class<EmbeddedSource>("SkillV2.EmbeddedSource")({
|
||||
type: Schema.Literal("embedded"),
|
||||
skill: Schema.suspend(() => Info),
|
||||
}) {}
|
||||
|
||||
export const Source = Schema.Union([DirectorySource, UrlSource, EmbeddedSource]).pipe(
|
||||
Schema.toTaggedUnion("type"),
|
||||
withStatics(() => ({
|
||||
equals: (a: DirectorySource | UrlSource, b: DirectorySource | UrlSource) => {
|
||||
equals: (a: DirectorySource | UrlSource | EmbeddedSource, b: DirectorySource | UrlSource | EmbeddedSource) => {
|
||||
if (a.type !== b.type) return false
|
||||
if (a.type === "directory" && b.type === "directory") return a.path === b.path
|
||||
if (a.type === "url" && b.type === "url") return a.url === b.url
|
||||
if (a.type === "embedded" && b.type === "embedded") return a.skill.name === b.skill.name
|
||||
return false
|
||||
},
|
||||
key: (source: DirectorySource | UrlSource) =>
|
||||
source.type === "directory" ? `directory:${source.path}` : `url:${source.url}`,
|
||||
key: (source: DirectorySource | UrlSource | EmbeddedSource) =>
|
||||
source.type === "directory" ? `directory:${source.path}` : source.type === "url" ? `url:${source.url}` : `embedded:${source.skill.name}`,
|
||||
})),
|
||||
)
|
||||
export type Source = typeof Source.Type
|
||||
|
|
@ -89,6 +95,7 @@ export const layer = Layer.effect(
|
|||
|
||||
const load = Effect.fn("SkillV2.load")(function* (source: Source) {
|
||||
const skills: Info[] = []
|
||||
if (source.type === "embedded") return [source.skill]
|
||||
const directories = source.type === "directory" ? [source.path] : yield* discovery.pull(source.url)
|
||||
for (const directory of directories) {
|
||||
const files = yield* fs
|
||||
|
|
|
|||
|
|
@ -33,6 +33,7 @@ describe("ConfigSkillPlugin.Plugin", () => {
|
|||
Config.Service.of({
|
||||
entries: () =>
|
||||
Effect.succeed([
|
||||
new Config.Directory({ type: "directory", path: AbsolutePath.make("/repo/.opencode") }),
|
||||
new Config.Document({
|
||||
type: "document",
|
||||
info: decode({
|
||||
|
|
@ -56,6 +57,8 @@ describe("ConfigSkillPlugin.Plugin", () => {
|
|||
)
|
||||
|
||||
expect(sources).toEqual([
|
||||
new SkillV2.DirectorySource({ type: "directory", path: AbsolutePath.make(path.join("/repo/.opencode", "skill")) }),
|
||||
new SkillV2.DirectorySource({ type: "directory", path: AbsolutePath.make(path.join("/repo/.opencode", "skills")) }),
|
||||
new SkillV2.DirectorySource({ type: "directory", path: AbsolutePath.make(path.join(directory, "skills")) }),
|
||||
new SkillV2.DirectorySource({
|
||||
type: "directory",
|
||||
|
|
|
|||
32
packages/core/test/plugin/skill.test.ts
Normal file
32
packages/core/test/plugin/skill.test.ts
Normal file
|
|
@ -0,0 +1,32 @@
|
|||
import { describe, expect } from "bun:test"
|
||||
import { Effect, Layer } from "effect"
|
||||
import { AgentV2 } from "@opencode-ai/core/agent"
|
||||
import { FSUtil } from "@opencode-ai/core/fs-util"
|
||||
import { SkillPlugin } from "@opencode-ai/core/plugin/skill"
|
||||
import { SkillV2 } from "@opencode-ai/core/skill"
|
||||
import { SkillDiscovery } from "@opencode-ai/core/skill/discovery"
|
||||
import { testEffect } from "../lib/effect"
|
||||
|
||||
const it = testEffect(
|
||||
SkillV2.layer.pipe(
|
||||
Layer.provide(FSUtil.defaultLayer),
|
||||
Layer.provide(SkillDiscovery.defaultLayer),
|
||||
Layer.provideMerge(AgentV2.locationLayer),
|
||||
),
|
||||
)
|
||||
|
||||
describe("SkillPlugin.Plugin", () => {
|
||||
it.effect("registers the built-in customize-opencode skill", () =>
|
||||
Effect.gen(function* () {
|
||||
const skill = yield* SkillV2.Service
|
||||
yield* SkillPlugin.Plugin.effect.pipe(Effect.provideService(SkillV2.Service, skill))
|
||||
|
||||
expect(yield* skill.list()).toContainEqual(
|
||||
expect.objectContaining({
|
||||
name: "customize-opencode",
|
||||
description: expect.stringContaining("opencode's own configuration"),
|
||||
}),
|
||||
)
|
||||
}),
|
||||
)
|
||||
})
|
||||
|
|
@ -5,6 +5,7 @@ import { HttpApiEndpoint, HttpApiGroup, OpenApi } from "effect/unstable/httpapi"
|
|||
import { InvalidCursorError, SessionNotFoundError, UnknownError } from "../../errors"
|
||||
import { V2Authorization } from "../../middleware/authorization"
|
||||
import { WorkspaceRoutingQueryFields } from "../../middleware/workspace-routing"
|
||||
import { data } from "./response"
|
||||
|
||||
export const MessagesQuery = Schema.Struct({
|
||||
...WorkspaceRoutingQueryFields,
|
||||
|
|
@ -29,13 +30,13 @@ export const MessageGroup = HttpApiGroup.make("v2.message")
|
|||
HttpApiEndpoint.get("messages", "/api/session/:sessionID/message", {
|
||||
params: { sessionID: SessionID },
|
||||
query: MessagesQuery,
|
||||
success: Schema.Struct({
|
||||
success: data(Schema.Struct({
|
||||
items: Schema.Array(SessionMessage.Message),
|
||||
cursor: Schema.Struct({
|
||||
previous: Schema.String.pipe(Schema.optional),
|
||||
next: Schema.String.pipe(Schema.optional),
|
||||
}),
|
||||
}).annotate({ identifier: "V2SessionMessagesResponse" }),
|
||||
}).annotate({ identifier: "V2SessionMessagesResponse" })),
|
||||
error: [InvalidCursorError, SessionNotFoundError, UnknownError],
|
||||
}).annotateMerge(
|
||||
OpenApi.annotations({
|
||||
|
|
|
|||
|
|
@ -8,6 +8,7 @@ import { HttpApiEndpoint, HttpApiGroup, HttpApiSchema, OpenApi } from "effect/un
|
|||
import { PermissionNotFoundError, SessionNotFoundError } from "../../errors"
|
||||
import { V2Authorization } from "../../middleware/authorization"
|
||||
import { LocationQuery, locationQueryOpenApi, V2LocationMiddleware } from "./location"
|
||||
import { data } from "./response"
|
||||
|
||||
export const PermissionGroup = HttpApiGroup.make("v2.permission")
|
||||
.add(
|
||||
|
|
@ -32,7 +33,7 @@ export const SessionPermissionGroup = HttpApiGroup.make("v2.session.permission")
|
|||
.add(
|
||||
HttpApiEndpoint.get("sessionPermissionRequests", "/api/session/:sessionID/permission/request", {
|
||||
params: { sessionID: SessionV2.ID },
|
||||
success: Schema.Array(PermissionV2.Request),
|
||||
success: data(Schema.Array(PermissionV2.Request)),
|
||||
error: SessionNotFoundError,
|
||||
}).annotateMerge(
|
||||
OpenApi.annotations({
|
||||
|
|
@ -68,7 +69,7 @@ export const PermissionSavedGroup = HttpApiGroup.make("v2.permission.saved")
|
|||
.add(
|
||||
HttpApiEndpoint.get("savedPermissions", "/api/permission/saved", {
|
||||
query: Schema.Struct({ projectID: ProjectV2.ID.pipe(Schema.optional) }),
|
||||
success: Schema.Array(PermissionSaved.Info),
|
||||
success: data(Schema.Array(PermissionSaved.Info)),
|
||||
}).annotateMerge(
|
||||
OpenApi.annotations({
|
||||
identifier: "v2.permission.saved.list",
|
||||
|
|
|
|||
|
|
@ -0,0 +1,9 @@
|
|||
import { Schema } from "effect"
|
||||
|
||||
export function data<S extends Schema.Top>(schema: S) {
|
||||
return Schema.Struct({ data: schema })
|
||||
}
|
||||
|
||||
export function make<A>(data: A) {
|
||||
return { data }
|
||||
}
|
||||
|
|
@ -16,6 +16,7 @@ import {
|
|||
} from "../../errors"
|
||||
import { V2Authorization } from "../../middleware/authorization"
|
||||
import { WorkspaceRoutingQuery } from "../../middleware/workspace-routing"
|
||||
import { data } from "./response"
|
||||
|
||||
const SessionsQueryFields = {
|
||||
workspace: WorkspaceV2.ID.pipe(Schema.optional),
|
||||
|
|
@ -88,13 +89,13 @@ export const SessionGroup = HttpApiGroup.make("v2.session")
|
|||
.add(
|
||||
HttpApiEndpoint.get("sessions", "/api/session", {
|
||||
query: SessionsQuery,
|
||||
success: Schema.Struct({
|
||||
success: data(Schema.Struct({
|
||||
items: Schema.Array(SessionV2.Info),
|
||||
cursor: Schema.Struct({
|
||||
previous: SessionsCursor.pipe(Schema.optional),
|
||||
next: SessionsCursor.pipe(Schema.optional),
|
||||
}),
|
||||
}).annotate({ identifier: "V2SessionsResponse" }),
|
||||
}).annotate({ identifier: "V2SessionsResponse" })),
|
||||
error: [InvalidCursorError, InvalidRequestError],
|
||||
}).annotateMerge(
|
||||
OpenApi.annotations({
|
||||
|
|
@ -113,7 +114,7 @@ export const SessionGroup = HttpApiGroup.make("v2.session")
|
|||
prompt: Prompt,
|
||||
delivery: SessionV2.Delivery.pipe(Schema.optional),
|
||||
}),
|
||||
success: SessionMessage.Message,
|
||||
success: data(SessionMessage.Message),
|
||||
error: [SessionNotFoundError, ServiceUnavailableError],
|
||||
}).annotateMerge(
|
||||
OpenApi.annotations({
|
||||
|
|
@ -155,7 +156,7 @@ export const SessionGroup = HttpApiGroup.make("v2.session")
|
|||
HttpApiEndpoint.get("context", "/api/session/:sessionID/context", {
|
||||
params: { sessionID: SessionID },
|
||||
query: WorkspaceRoutingQuery,
|
||||
success: Schema.Array(SessionMessage.Message),
|
||||
success: data(Schema.Array(SessionMessage.Message)),
|
||||
error: [SessionNotFoundError, UnknownError],
|
||||
}).annotateMerge(
|
||||
OpenApi.annotations({
|
||||
|
|
|
|||
|
|
@ -5,6 +5,7 @@ import * as DateTime from "effect/DateTime"
|
|||
import { HttpApiBuilder } from "effect/unstable/httpapi"
|
||||
import { InstanceHttpApi } from "../../api"
|
||||
import { InvalidCursorError, SessionNotFoundError, UnknownError } from "../../errors"
|
||||
import { make } from "../../groups/v2/response"
|
||||
|
||||
const DefaultMessagesLimit = 50
|
||||
|
||||
|
|
@ -75,13 +76,13 @@ export const messageHandlers = HttpApiBuilder.group(InstanceHttpApi, "v2.message
|
|||
)
|
||||
const first = messages[0]
|
||||
const last = messages.at(-1)
|
||||
return {
|
||||
return make({
|
||||
items: messages,
|
||||
cursor: {
|
||||
previous: first ? cursor.encode(first, order, "previous") : undefined,
|
||||
next: last ? cursor.encode(last, order, "next") : undefined,
|
||||
},
|
||||
}
|
||||
})
|
||||
}),
|
||||
)
|
||||
}),
|
||||
|
|
|
|||
|
|
@ -10,6 +10,7 @@ import { HttpApiBuilder, HttpApiSchema } from "effect/unstable/httpapi"
|
|||
import { InstanceHttpApi } from "../../api"
|
||||
import { PermissionNotFoundError, SessionNotFoundError } from "../../errors"
|
||||
import { response } from "../../groups/v2/location"
|
||||
import { make } from "../../groups/v2/response"
|
||||
|
||||
function missingRequest(id: PermissionV2.ID) {
|
||||
return new PermissionNotFoundError({ requestID: id, message: `Permission request not found: ${id}` })
|
||||
|
|
@ -62,7 +63,7 @@ export const sessionPermissionHandlers = HttpApiBuilder.group(InstanceHttpApi, "
|
|||
"sessionPermissionRequests",
|
||||
Effect.fn(function* (ctx) {
|
||||
return yield* withSessionPermission(ctx.params.sessionID, (permission) =>
|
||||
permission.forSession(ctx.params.sessionID),
|
||||
permission.forSession(ctx.params.sessionID).pipe(Effect.map(make)),
|
||||
)
|
||||
}),
|
||||
)
|
||||
|
|
@ -92,7 +93,7 @@ export const savedPermissionHandlers = HttpApiBuilder.group(InstanceHttpApi, "v2
|
|||
.handle(
|
||||
"savedPermissions",
|
||||
Effect.fn(function* (ctx) {
|
||||
return yield* saved.list({ projectID: ctx.query.projectID })
|
||||
return make(yield* saved.list({ projectID: ctx.query.projectID }))
|
||||
}),
|
||||
)
|
||||
.handle(
|
||||
|
|
|
|||
|
|
@ -4,6 +4,7 @@ import { HttpApiBuilder, HttpApiSchema } from "effect/unstable/httpapi"
|
|||
import { InstanceHttpApi } from "../../api"
|
||||
import { SessionsCursor } from "../../groups/v2/session"
|
||||
import { InvalidCursorError, ServiceUnavailableError, SessionNotFoundError, UnknownError } from "../../errors"
|
||||
import { make } from "../../groups/v2/response"
|
||||
|
||||
const DefaultSessionsLimit = 50
|
||||
|
||||
|
|
@ -28,7 +29,7 @@ export const sessionHandlers = HttpApiBuilder.group(InstanceHttpApi, "v2.session
|
|||
})
|
||||
const first = sessions[0]
|
||||
const last = sessions.at(-1)
|
||||
return {
|
||||
return make({
|
||||
items: sessions,
|
||||
cursor: {
|
||||
previous: first
|
||||
|
|
@ -52,13 +53,13 @@ export const sessionHandlers = HttpApiBuilder.group(InstanceHttpApi, "v2.session
|
|||
})
|
||||
: undefined,
|
||||
},
|
||||
}
|
||||
})
|
||||
}),
|
||||
)
|
||||
.handle(
|
||||
"prompt",
|
||||
Effect.fn(function* (ctx) {
|
||||
return yield* session
|
||||
return make(yield* session
|
||||
.prompt({
|
||||
sessionID: ctx.params.sessionID,
|
||||
prompt: ctx.payload.prompt,
|
||||
|
|
@ -81,7 +82,7 @@ export const sessionHandlers = HttpApiBuilder.group(InstanceHttpApi, "v2.session
|
|||
}),
|
||||
),
|
||||
),
|
||||
)
|
||||
))
|
||||
}),
|
||||
)
|
||||
.handle(
|
||||
|
|
@ -135,7 +136,7 @@ export const sessionHandlers = HttpApiBuilder.group(InstanceHttpApi, "v2.session
|
|||
.handle(
|
||||
"context",
|
||||
Effect.fn(function* (ctx) {
|
||||
return yield* session.context(ctx.params.sessionID).pipe(
|
||||
return make(yield* session.context(ctx.params.sessionID).pipe(
|
||||
Effect.catchTag("Session.NotFoundError", (error) =>
|
||||
Effect.fail(
|
||||
new SessionNotFoundError({
|
||||
|
|
@ -158,7 +159,7 @@ export const sessionHandlers = HttpApiBuilder.group(InstanceHttpApi, "v2.session
|
|||
),
|
||||
)
|
||||
}),
|
||||
)
|
||||
))
|
||||
}),
|
||||
)
|
||||
}),
|
||||
|
|
|
|||
|
|
@ -15,7 +15,7 @@ import { RuntimeFlags } from "@/effect/runtime-flags"
|
|||
import { Glob } from "@opencode-ai/core/util/glob"
|
||||
import * as Log from "@opencode-ai/core/util/log"
|
||||
import { Discovery } from "./discovery"
|
||||
import CUSTOMIZE_OPENCODE_SKILL_BODY from "./prompt/customize-opencode.md" with { type: "text" }
|
||||
import CUSTOMIZE_OPENCODE_SKILL_BODY from "../../../core/src/plugin/skill/customize-opencode.md" with { type: "text" }
|
||||
import { isRecord } from "@/util/record"
|
||||
|
||||
const log = Log.create({ service: "skill" })
|
||||
|
|
|
|||
|
|
@ -42,6 +42,22 @@ function cursor(input: Record<string, unknown>) {
|
|||
return Buffer.from(JSON.stringify(input)).toString("base64url")
|
||||
}
|
||||
|
||||
function data(validate: (value: any) => void) {
|
||||
return (body: any) => {
|
||||
object(body)
|
||||
validate(body.data)
|
||||
}
|
||||
}
|
||||
|
||||
function locationData(validate: (value: any) => void) {
|
||||
return (body: any) => {
|
||||
object(body)
|
||||
object(body.location)
|
||||
object(body.location.project)
|
||||
validate(body.data)
|
||||
}
|
||||
}
|
||||
|
||||
const scenarios: Scenario[] = [
|
||||
http.protected
|
||||
.get("/global/health", "global.health")
|
||||
|
|
@ -608,19 +624,19 @@ const scenarios: Scenario[] = [
|
|||
check(auth.test === undefined, "auth remove should delete provider from isolated auth file")
|
||||
}),
|
||||
),
|
||||
http.protected.get("/api/model", "v2.model.list").json(200, array),
|
||||
http.protected.get("/api/provider", "v2.provider.list").json(200, array),
|
||||
http.protected.get("/api/model", "v2.model.list").json(200, locationData(array)),
|
||||
http.protected.get("/api/provider", "v2.provider.list").json(200, locationData(array)),
|
||||
http.protected
|
||||
.get("/api/fs/read", "v2.fs.read")
|
||||
.seeded((ctx) => ctx.file("hello.txt", "hello\n"))
|
||||
.at((ctx) => ({ path: "/api/fs/read?path=hello.txt", headers: ctx.headers() }))
|
||||
.json(200, object),
|
||||
http.protected.get("/api/fs/list", "v2.fs.list").json(200, array),
|
||||
.json(200, locationData(object)),
|
||||
http.protected.get("/api/fs/list", "v2.fs.list").json(200, locationData(array)),
|
||||
http.protected
|
||||
.get("/api/provider/{providerID}", "v2.provider.get")
|
||||
.at((ctx) => ({ path: route("/api/provider/{providerID}", { providerID: "missing" }), headers: ctx.headers() }))
|
||||
.json(404, object, "status"),
|
||||
http.protected.get("/api/permission/request", "v2.permission.request.list").json(200, array),
|
||||
http.protected.get("/api/permission/request", "v2.permission.request.list").json(200, locationData(array)),
|
||||
http.protected
|
||||
.get("/api/session/{sessionID}/permission/request", "v2.session.permission.list")
|
||||
.seeded((ctx) => ctx.session({ title: "Permission list owner" }))
|
||||
|
|
@ -628,7 +644,7 @@ const scenarios: Scenario[] = [
|
|||
path: route("/api/session/{sessionID}/permission/request", { sessionID: ctx.state.id }),
|
||||
headers: ctx.headers(),
|
||||
}))
|
||||
.json(200, array),
|
||||
.json(200, data(array)),
|
||||
http.protected
|
||||
.post("/api/session/{sessionID}/permission/request/{requestID}/reply", "v2.session.permission.reply")
|
||||
.seeded((ctx) => ctx.session({ title: "Permission owner" }))
|
||||
|
|
@ -641,7 +657,7 @@ const scenarios: Scenario[] = [
|
|||
body: { reply: "once" },
|
||||
}))
|
||||
.json(404, object, "status"),
|
||||
http.protected.get("/api/permission/saved", "v2.permission.saved.list").json(200, array),
|
||||
http.protected.get("/api/permission/saved", "v2.permission.saved.list").json(200, data(array)),
|
||||
http.protected
|
||||
.delete("/api/permission/saved/{id}", "v2.permission.saved.remove")
|
||||
.at((ctx) => ({ path: route("/api/permission/saved/{id}", { id: "psv_httpapi_missing" }), headers: ctx.headers() }))
|
||||
|
|
@ -653,8 +669,9 @@ const scenarios: Scenario[] = [
|
|||
200,
|
||||
(body) => {
|
||||
object(body)
|
||||
array(body.items)
|
||||
object(body.cursor)
|
||||
object(body.data)
|
||||
array(body.data.items)
|
||||
object(body.data.cursor)
|
||||
},
|
||||
"none",
|
||||
),
|
||||
|
|
@ -676,8 +693,9 @@ const scenarios: Scenario[] = [
|
|||
200,
|
||||
(body) => {
|
||||
object(body)
|
||||
array(body.items)
|
||||
object(body.cursor)
|
||||
object(body.data)
|
||||
array(body.data.items)
|
||||
object(body.data.cursor)
|
||||
},
|
||||
"none",
|
||||
),
|
||||
|
|
@ -698,8 +716,9 @@ const scenarios: Scenario[] = [
|
|||
200,
|
||||
(body) => {
|
||||
object(body)
|
||||
array(body.items)
|
||||
object(body.cursor)
|
||||
object(body.data)
|
||||
array(body.data.items)
|
||||
object(body.data.cursor)
|
||||
},
|
||||
"none",
|
||||
),
|
||||
|
|
|
|||
|
|
@ -388,8 +388,9 @@ describe("session HttpApi", () => {
|
|||
yield* insertLegacyAssistantMessage(parent.id)
|
||||
|
||||
expect(
|
||||
(yield* requestJson<{ items: SessionMessage.Message[] }>(`/api/session/${parent.id}/message`, { headers }))
|
||||
.items,
|
||||
(yield* requestJson<{ data: { items: SessionMessage.Message[] } }>(`/api/session/${parent.id}/message`, {
|
||||
headers,
|
||||
})).data.items,
|
||||
).toMatchObject([{ type: "assistant" }])
|
||||
}),
|
||||
{ git: true, config: { formatter: false, lsp: false } },
|
||||
|
|
@ -453,7 +454,7 @@ describe("session HttpApi", () => {
|
|||
})}`,
|
||||
{ headers },
|
||||
)
|
||||
const sessionCursor = (yield* json<{ cursor: { next?: string } }>(sessionPage)).cursor.next
|
||||
const sessionCursor = (yield* json<{ data: { cursor: { next?: string } } }>(sessionPage)).data.cursor.next
|
||||
expect(sessionCursor).toBeTruthy()
|
||||
expect(JSON.parse(Buffer.from(sessionCursor!, "base64url").toString("utf8"))).toMatchObject({
|
||||
order: "asc",
|
||||
|
|
@ -480,7 +481,7 @@ describe("session HttpApi", () => {
|
|||
})
|
||||
|
||||
const messagePage = yield* request(`/api/session/${session.id}/message?limit=1`, { headers })
|
||||
const messageCursor = (yield* json<{ cursor: { next?: string } }>(messagePage)).cursor.next
|
||||
const messageCursor = (yield* json<{ data: { cursor: { next?: string } } }>(messagePage)).data.cursor.next
|
||||
expect(messageCursor).toBeTruthy()
|
||||
|
||||
const messageCursorWithOrder = yield* request(
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue