| 123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313 |
- import { describe, expect, test } from "bun:test"
- import { Schema } from "effect"
- import { Session } from "@/session/session"
- import { SessionPrompt } from "../../src/session/prompt"
- import { SessionRevert } from "../../src/session/revert"
- import { SessionStatus } from "../../src/session/status"
- import { SessionSummary } from "../../src/session/summary"
- import { Todo } from "../../src/session/todo"
- import { SessionID, MessageID, PartID } from "../../src/session/schema"
- import { ProjectV2 } from "@kirincode-ai/core/project"
- import { WorkspaceV2 } from "@kirincode-ai/core/workspace"
- // Covers the session-domain Effect Schema migration. For each migrated
- // schema we assert:
- // 1. The Effect decoder (`Schema.decodeUnknownSync`) accepts valid input.
- // 2. Clearly-invalid input is rejected.
- // Representative valid IDs — the branded schemas require the right prefix
- // (see src/id/id.ts).
- const sessionID = Schema.decodeUnknownSync(SessionID)("ses_01J5Y5H0AH4Q4NXJ6P4C3P5V2K")
- const sessionIDChild = Schema.decodeUnknownSync(SessionID)("ses_01J5Y5H0AH4Q4NXJ6P4C3P5V2L")
- const messageID = Schema.decodeUnknownSync(MessageID)("msg_01J5Y5H0AH4Q4NXJ6P4C3P5V2M")
- const partID = Schema.decodeUnknownSync(PartID)("prt_01J5Y5H0AH4Q4NXJ6P4C3P5V2N")
- const projectID = ProjectV2.ID.make("proj-alpha")
- const workspaceID = Schema.decodeUnknownSync(WorkspaceV2.ID)("wrk-primary")
- function decodeUnknown<S extends Schema.Top>(schema: S) {
- const decode = Schema.decodeUnknownSync(schema as any)
- return (input: unknown): Schema.Schema.Type<S> => decode(input) as Schema.Schema.Type<S>
- }
- describe("Session.Info", () => {
- const decode = decodeUnknown(Session.Info)
- test("accepts minimal session", () => {
- const input = {
- id: sessionID,
- slug: "hello",
- projectID,
- directory: "/tmp/proj",
- title: "First session",
- version: "0.1.0",
- time: { created: 1, updated: 2 },
- }
- expect(decode(input)).toEqual(input)
- })
- test("round-trips every optional field", () => {
- const input = {
- id: sessionID,
- slug: "fullshape",
- projectID,
- workspaceID,
- directory: "/tmp/proj",
- path: "packages/opencode",
- parentID: sessionIDChild,
- summary: {
- additions: 10,
- deletions: 5,
- files: 2,
- diffs: [{ additions: 1, deletions: 0, file: "a.ts", patch: "--- a/a.ts" }],
- },
- share: { url: "https://share.example.com/s/1" },
- title: "Full session",
- version: "1.0.0",
- metadata: { source: "test" },
- time: { created: 100, updated: 200, compacting: 150, archived: 300 },
- permission: [{ action: "allow" as const, pattern: "*", permission: "read" }],
- revert: {
- messageID,
- partID,
- snapshot: "snap-1",
- diff: "diff-1",
- },
- }
- expect(decode(input)).toEqual(input)
- })
- test("accepts migrated summary diffs without file details", () => {
- const input = {
- id: sessionID,
- slug: "legacy-diff",
- projectID,
- directory: "/tmp/proj",
- title: "Legacy diff",
- version: "0.1.0",
- summary: {
- additions: 1,
- deletions: 0,
- files: 1,
- diffs: [{ additions: 1, deletions: 0 }],
- },
- time: { created: 1, updated: 2 },
- }
- expect(decode(input)).toEqual(input)
- })
- test("rejects unbranded session id", () => {
- const bad = { id: "not-a-session-id" } as unknown
- expect(() => decode(bad)).toThrow()
- })
- test("rejects missing required fields", () => {
- const bad = { id: sessionID } as unknown
- expect(() => decode(bad)).toThrow()
- })
- })
- describe("Session.ProjectInfo", () => {
- const decode = decodeUnknown(Session.ProjectInfo)
- test("accepts with and without optional name", () => {
- const noName = { id: projectID, worktree: "/tmp/wt" }
- const withName = { ...noName, name: "alpha" }
- expect(decode(noName)).toEqual(noName)
- expect(decode(withName)).toEqual(withName)
- })
- })
- describe("Session.GlobalInfo", () => {
- const decode = decodeUnknown(Session.GlobalInfo)
- test("accepts null project", () => {
- const input = {
- id: sessionID,
- slug: "global",
- projectID,
- directory: "/tmp/proj",
- title: "global",
- version: "0",
- time: { created: 0, updated: 0 },
- project: null,
- }
- expect(decode(input)).toEqual(input)
- })
- test("accepts populated project", () => {
- const input = {
- id: sessionID,
- slug: "global",
- projectID,
- directory: "/tmp/proj",
- title: "global",
- version: "0",
- time: { created: 0, updated: 0 },
- project: { id: projectID, worktree: "/tmp/wt", name: "alpha" },
- }
- expect(decode(input)).toEqual(input)
- })
- })
- describe("Session input schemas", () => {
- test("CreateInput accepts undefined and populated forms", () => {
- const decode = decodeUnknown(Session.CreateInput)
- expect(decode(undefined)).toBeUndefined()
- const populated = {
- parentID: sessionID,
- title: "child",
- metadata: { source: "test" },
- permission: [{ action: "ask" as const, pattern: "*", permission: "bash" }],
- workspaceID,
- }
- expect(decode(populated)).toEqual(populated)
- })
- test("ForkInput round-trips", () => {
- const decode = decodeUnknown(Session.ForkInput)
- const input = { sessionID, messageID }
- expect(decode(input)).toEqual(input)
- // messageID is optional
- const bare = { sessionID }
- expect(decode(bare)).toEqual(bare)
- })
- test("SetTitleInput rejects missing title", () => {
- expect(() => decodeUnknown(Session.SetTitleInput)({ sessionID })).toThrow()
- })
- test("SetArchivedInput accepts both with and without time", () => {
- const decode = decodeUnknown(Session.SetArchivedInput)
- expect(decode({ sessionID })).toEqual({ sessionID })
- expect(decode({ sessionID, time: 123 })).toEqual({ sessionID, time: 123 })
- })
- test("SetPermissionInput requires a ruleset", () => {
- const decode = decodeUnknown(Session.SetPermissionInput)
- const input = { sessionID, permission: [{ action: "deny" as const, pattern: "*", permission: "write" }] }
- expect(decode(input)).toEqual(input)
- expect(() => decode({ sessionID })).toThrow()
- })
- test("MessagesInput accepts optional limit", () => {
- const decode = decodeUnknown(Session.MessagesInput)
- expect(decode({ sessionID })).toEqual({ sessionID })
- expect(decode({ sessionID, limit: 50 })).toEqual({ sessionID, limit: 50 })
- })
- })
- describe("SessionRevert.RevertInput", () => {
- const decode = decodeUnknown(SessionRevert.RevertInput)
- test("messageID is required, partID is optional", () => {
- const withPart = { sessionID, messageID, partID }
- expect(decode(withPart)).toEqual(withPart)
- const noPart = { sessionID, messageID }
- expect(decode(noPart)).toEqual(noPart)
- expect(() => decode({ sessionID })).toThrow()
- })
- })
- describe("SessionSummary.DiffInput", () => {
- const decode = decodeUnknown(SessionSummary.DiffInput)
- test("messageID optional", () => {
- expect(decode({ sessionID })).toEqual({ sessionID })
- expect(decode({ sessionID, messageID })).toEqual({ sessionID, messageID })
- })
- })
- describe("SessionStatus.Info", () => {
- const decode = decodeUnknown(SessionStatus.Info)
- test("idle / busy discriminators", () => {
- expect(decode({ type: "idle" })).toEqual({ type: "idle" })
- expect(decode({ type: "busy" })).toEqual({ type: "busy" })
- })
- test("retry carries attempt/message/action/next", () => {
- const input = {
- type: "retry" as const,
- attempt: 1,
- message: "transient",
- action: {
- reason: "free_tier_limit",
- provider: "kirincode",
- title: "Free limit reached",
- message: "Subscribe to KirinCode Go.",
- label: "subscribe",
- link: "https://kirincode.ai/go",
- },
- next: 500,
- }
- expect(decode(input)).toEqual(input)
- })
- test("rejects unknown type", () => {
- expect(() => decode({ type: "bogus" })).toThrow()
- })
- })
- describe("Todo.Info", () => {
- const decode = decodeUnknown(Todo.Info)
- test("three-field round-trip", () => {
- const input = Todo.Info.make({ content: "do a thing", status: "pending", priority: "high" })
- expect(decode(input)).toEqual(input)
- })
- })
- describe("SessionPrompt input schemas", () => {
- test("LoopInput is just sessionID", () => {
- const decode = decodeUnknown(SessionPrompt.LoopInput)
- expect(decode({ sessionID })).toEqual({ sessionID })
- })
- test("ShellInput requires agent + command", () => {
- const decode = decodeUnknown(SessionPrompt.ShellInput)
- const expected = { sessionID, agent: "build", command: "echo hi" }
- const input: unknown = expected
- expect(decode(input)).toEqual(expected)
- expect(() => decode({ sessionID })).toThrow()
- })
- test("PromptInput accepts a text part and a file part", () => {
- const decode = decodeUnknown(SessionPrompt.PromptInput)
- const expected = {
- sessionID,
- parts: [
- { type: "text" as const, text: "hello" },
- { type: "file" as const, mime: "image/png", url: "data:image/png;base64,AAAA" },
- ],
- }
- const input: unknown = expected
- const decoded = decode(input)
- expect(decoded.parts).toHaveLength(2)
- expect(decoded.parts[0]).toMatchObject({ type: "text", text: "hello" })
- expect(decoded.parts[1]).toMatchObject({ type: "file", mime: "image/png" })
- })
- test("PromptInput rejects unknown part type", () => {
- const decode = decodeUnknown(SessionPrompt.PromptInput)
- const bad = {
- sessionID,
- parts: [{ type: "nonsense", payload: 42 }],
- }
- expect(() => decode(bad)).toThrow()
- })
- test("CommandInput round-trips core fields", () => {
- const decode = decodeUnknown(SessionPrompt.CommandInput)
- const expected = {
- sessionID,
- arguments: "--flag",
- command: "deploy",
- }
- const input: unknown = expected
- expect(decode(input)).toEqual(expected)
- })
- })
|