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