feat(id): brand SessionID through Drizzle and Zod schemas (#16953)

This commit is contained in:
Kit Langton
2026-03-11 19:16:56 -04:00
committed by GitHub
parent 4e73473119
commit cb67465675
44 changed files with 226 additions and 158 deletions

View File

@@ -1,12 +1,13 @@
import { test, expect, describe } from "bun:test"
import { extractResponseText, formatPromptTooLargeError } from "../../src/cli/cmd/github"
import type { MessageV2 } from "../../src/session/message-v2"
import { SessionID } from "../../src/session/schema"
// Helper to create minimal valid parts
function createTextPart(text: string): MessageV2.Part {
return {
id: "1",
sessionID: "s",
sessionID: SessionID.make("s"),
messageID: "m",
type: "text" as const,
text,
@@ -16,7 +17,7 @@ function createTextPart(text: string): MessageV2.Part {
function createReasoningPart(text: string): MessageV2.Part {
return {
id: "1",
sessionID: "s",
sessionID: SessionID.make("s"),
messageID: "m",
type: "reasoning" as const,
text,
@@ -28,7 +29,7 @@ function createToolPart(tool: string, title: string, status: "completed" | "runn
if (status === "completed") {
return {
id: "1",
sessionID: "s",
sessionID: SessionID.make("s"),
messageID: "m",
type: "tool" as const,
callID: "c1",
@@ -45,7 +46,7 @@ function createToolPart(tool: string, title: string, status: "completed" | "runn
}
return {
id: "1",
sessionID: "s",
sessionID: SessionID.make("s"),
messageID: "m",
type: "tool" as const,
callID: "c1",
@@ -61,7 +62,7 @@ function createToolPart(tool: string, title: string, status: "completed" | "runn
function createStepStartPart(): MessageV2.Part {
return {
id: "1",
sessionID: "s",
sessionID: SessionID.make("s"),
messageID: "m",
type: "step-start" as const,
}
@@ -70,7 +71,7 @@ function createStepStartPart(): MessageV2.Part {
function createStepFinishPart(): MessageV2.Part {
return {
id: "1",
sessionID: "s",
sessionID: SessionID.make("s"),
messageID: "m",
type: "step-finish" as const,
reason: "done",

View File

@@ -2,11 +2,12 @@ import { describe, test, expect } from "bun:test"
import path from "path"
import { Instance } from "../../src/project/instance"
import { WebFetchTool } from "../../src/tool/webfetch"
import { SessionID } from "../../src/session/schema"
const projectRoot = path.join(__dirname, "../..")
const ctx = {
sessionID: "test",
sessionID: SessionID.make("ses_test"),
messageID: "",
callID: "",
agent: "build",

View File

@@ -3,6 +3,7 @@ import os from "os"
import { PermissionNext } from "../../src/permission/next"
import { Instance } from "../../src/project/instance"
import { tmpdir } from "../fixture/fixture"
import { SessionID } from "../../src/session/schema"
// fromConfig tests
@@ -462,7 +463,7 @@ test("ask - resolves immediately when action is allow", async () => {
directory: tmp.path,
fn: async () => {
const result = await PermissionNext.ask({
sessionID: "session_test",
sessionID: SessionID.make("session_test"),
permission: "bash",
patterns: ["ls"],
metadata: {},
@@ -481,7 +482,7 @@ test("ask - throws RejectedError when action is deny", async () => {
fn: async () => {
await expect(
PermissionNext.ask({
sessionID: "session_test",
sessionID: SessionID.make("session_test"),
permission: "bash",
patterns: ["rm -rf /"],
metadata: {},
@@ -499,7 +500,7 @@ test("ask - returns pending promise when action is ask", async () => {
directory: tmp.path,
fn: async () => {
const promise = PermissionNext.ask({
sessionID: "session_test",
sessionID: SessionID.make("session_test"),
permission: "bash",
patterns: ["ls"],
metadata: {},
@@ -522,7 +523,7 @@ test("reply - once resolves the pending ask", async () => {
fn: async () => {
const askPromise = PermissionNext.ask({
id: "permission_test1",
sessionID: "session_test",
sessionID: SessionID.make("session_test"),
permission: "bash",
patterns: ["ls"],
metadata: {},
@@ -547,7 +548,7 @@ test("reply - reject throws RejectedError", async () => {
fn: async () => {
const askPromise = PermissionNext.ask({
id: "permission_test2",
sessionID: "session_test",
sessionID: SessionID.make("session_test"),
permission: "bash",
patterns: ["ls"],
metadata: {},
@@ -572,7 +573,7 @@ test("reply - always persists approval and resolves", async () => {
fn: async () => {
const askPromise = PermissionNext.ask({
id: "permission_test3",
sessionID: "session_test",
sessionID: SessionID.make("session_test"),
permission: "bash",
patterns: ["ls"],
metadata: {},
@@ -594,7 +595,7 @@ test("reply - always persists approval and resolves", async () => {
fn: async () => {
// Stored approval should allow without asking
const result = await PermissionNext.ask({
sessionID: "session_test2",
sessionID: SessionID.make("session_test2"),
permission: "bash",
patterns: ["ls"],
metadata: {},
@@ -613,7 +614,7 @@ test("reply - reject cancels all pending for same session", async () => {
fn: async () => {
const askPromise1 = PermissionNext.ask({
id: "permission_test4a",
sessionID: "session_same",
sessionID: SessionID.make("session_same"),
permission: "bash",
patterns: ["ls"],
metadata: {},
@@ -623,7 +624,7 @@ test("reply - reject cancels all pending for same session", async () => {
const askPromise2 = PermissionNext.ask({
id: "permission_test4b",
sessionID: "session_same",
sessionID: SessionID.make("session_same"),
permission: "edit",
patterns: ["foo.ts"],
metadata: {},
@@ -655,7 +656,7 @@ test("ask - checks all patterns and stops on first deny", async () => {
fn: async () => {
await expect(
PermissionNext.ask({
sessionID: "session_test",
sessionID: SessionID.make("session_test"),
permission: "bash",
patterns: ["echo hello", "rm -rf /"],
metadata: {},
@@ -676,7 +677,7 @@ test("ask - allows all patterns when all match allow rules", async () => {
directory: tmp.path,
fn: async () => {
const result = await PermissionNext.ask({
sessionID: "session_test",
sessionID: SessionID.make("session_test"),
permission: "bash",
patterns: ["echo hello", "ls -la", "pwd"],
metadata: {},

View File

@@ -2,6 +2,7 @@ import { test, expect } from "bun:test"
import { Question } from "../../src/question"
import { Instance } from "../../src/project/instance"
import { tmpdir } from "../fixture/fixture"
import { SessionID } from "../../src/session/schema"
test("ask - returns pending promise", async () => {
await using tmp = await tmpdir({ git: true })
@@ -9,7 +10,7 @@ test("ask - returns pending promise", async () => {
directory: tmp.path,
fn: async () => {
const promise = Question.ask({
sessionID: "ses_test",
sessionID: SessionID.make("ses_test"),
questions: [
{
question: "What would you like to do?",
@@ -43,7 +44,7 @@ test("ask - adds to pending list", async () => {
]
Question.ask({
sessionID: "ses_test",
sessionID: SessionID.make("ses_test"),
questions,
})
@@ -73,7 +74,7 @@ test("reply - resolves the pending ask with answers", async () => {
]
const askPromise = Question.ask({
sessionID: "ses_test",
sessionID: SessionID.make("ses_test"),
questions,
})
@@ -97,7 +98,7 @@ test("reply - removes from pending list", async () => {
directory: tmp.path,
fn: async () => {
Question.ask({
sessionID: "ses_test",
sessionID: SessionID.make("ses_test"),
questions: [
{
question: "What would you like to do?",
@@ -146,7 +147,7 @@ test("reject - throws RejectedError", async () => {
directory: tmp.path,
fn: async () => {
const askPromise = Question.ask({
sessionID: "ses_test",
sessionID: SessionID.make("ses_test"),
questions: [
{
question: "What would you like to do?",
@@ -173,7 +174,7 @@ test("reject - removes from pending list", async () => {
directory: tmp.path,
fn: async () => {
const askPromise = Question.ask({
sessionID: "ses_test",
sessionID: SessionID.make("ses_test"),
questions: [
{
question: "What would you like to do?",
@@ -236,7 +237,7 @@ test("ask - handles multiple questions", async () => {
]
const askPromise = Question.ask({
sessionID: "ses_test",
sessionID: SessionID.make("ses_test"),
questions,
})
@@ -261,7 +262,7 @@ test("list - returns all pending requests", async () => {
directory: tmp.path,
fn: async () => {
Question.ask({
sessionID: "ses_test1",
sessionID: SessionID.make("ses_test1"),
questions: [
{
question: "Question 1?",
@@ -272,7 +273,7 @@ test("list - returns all pending requests", async () => {
})
Question.ask({
sessionID: "ses_test2",
sessionID: SessionID.make("ses_test2"),
questions: [
{
question: "Question 2?",

View File

@@ -11,6 +11,7 @@ import { Filesystem } from "../../src/util/filesystem"
import { tmpdir } from "../fixture/fixture"
import type { Agent } from "../../src/agent/agent"
import type { MessageV2 } from "../../src/session/message-v2"
import { SessionID } from "../../src/session/schema"
describe("session.llm.hasToolCalls", () => {
test("returns false for empty messages array", () => {
@@ -265,7 +266,7 @@ describe("session.llm.stream", () => {
directory: tmp.path,
fn: async () => {
const resolved = await Provider.getModel(providerID, model.id)
const sessionID = "session-test-1"
const sessionID = SessionID.make("session-test-1")
const agent = {
name: "test",
mode: "primary",
@@ -395,7 +396,7 @@ describe("session.llm.stream", () => {
directory: tmp.path,
fn: async () => {
const resolved = await Provider.getModel("openai", model.id)
const sessionID = "session-test-2"
const sessionID = SessionID.make("session-test-2")
const agent = {
name: "test",
mode: "primary",
@@ -517,7 +518,7 @@ describe("session.llm.stream", () => {
directory: tmp.path,
fn: async () => {
const resolved = await Provider.getModel(providerID, model.id)
const sessionID = "session-test-3"
const sessionID = SessionID.make("session-test-3")
const agent = {
name: "test",
mode: "primary",
@@ -618,7 +619,7 @@ describe("session.llm.stream", () => {
directory: tmp.path,
fn: async () => {
const resolved = await Provider.getModel(providerID, model.id)
const sessionID = "session-test-4"
const sessionID = SessionID.make("session-test-4")
const agent = {
name: "test",
mode: "primary",

View File

@@ -2,8 +2,9 @@ import { describe, expect, test } from "bun:test"
import { APICallError } from "ai"
import { MessageV2 } from "../../src/session/message-v2"
import type { Provider } from "../../src/provider/provider"
import { SessionID } from "../../src/session/schema"
const sessionID = "session"
const sessionID = SessionID.make("session")
const model: Provider.Model = {
id: "test-model",
providerID: "test",

View File

@@ -1,6 +1,7 @@
import { describe, expect, test } from "bun:test"
import { MessageV2 } from "../../src/session/message-v2"
import { SessionPrompt } from "../../src/session/prompt"
import { SessionID } from "../../src/session/schema"
describe("structured-output.OutputFormat", () => {
test("parses text format", () => {
@@ -96,7 +97,7 @@ describe("structured-output.UserMessage", () => {
test("user message accepts outputFormat", () => {
const result = MessageV2.User.safeParse({
id: "test-id",
sessionID: "test-session",
sessionID: SessionID.descending(),
role: "user",
time: { created: Date.now() },
agent: "default",
@@ -112,7 +113,7 @@ describe("structured-output.UserMessage", () => {
test("user message works without outputFormat (optional)", () => {
const result = MessageV2.User.safeParse({
id: "test-id",
sessionID: "test-session",
sessionID: SessionID.descending(),
role: "user",
time: { created: Date.now() },
agent: "default",
@@ -125,7 +126,7 @@ describe("structured-output.UserMessage", () => {
describe("structured-output.AssistantMessage", () => {
const baseAssistantMessage = {
id: "test-id",
sessionID: "test-session",
sessionID: SessionID.descending(),
role: "assistant" as const,
parentID: "parent-id",
modelID: "claude-3",

View File

@@ -11,6 +11,7 @@ import { ProjectTable } from "../../src/project/project.sql"
import { ProjectID } from "../../src/project/schema"
import { SessionTable, MessageTable, PartTable, TodoTable, PermissionTable } from "../../src/session/session.sql"
import { SessionShareTable } from "../../src/share/share.sql"
import { SessionID } from "../../src/session/schema"
// Test fixtures
const fixtures = {
@@ -220,7 +221,7 @@ describe("JSON to SQLite migration", () => {
const db = drizzle({ client: sqlite })
const sessions = db.select().from(SessionTable).all()
expect(sessions.length).toBe(1)
expect(sessions[0].id).toBe("ses_test456def")
expect(sessions[0].id).toBe(SessionID.make("ses_test456def"))
expect(sessions[0].project_id).toBe(ProjectID.make("proj_test123abc"))
expect(sessions[0].slug).toBe("test-session")
expect(sessions[0].title).toBe("Test Session Title")
@@ -295,7 +296,7 @@ describe("JSON to SQLite migration", () => {
const messages = db.select().from(MessageTable).all()
expect(messages.length).toBe(1)
expect(messages[0].id).toBe("msg_test789ghi")
expect(messages[0].session_id).toBe("ses_test456def")
expect(messages[0].session_id).toBe(SessionID.make("ses_test456def"))
expect(messages[0].data).not.toHaveProperty("id")
expect(messages[0].data).not.toHaveProperty("sessionID")
@@ -303,7 +304,7 @@ describe("JSON to SQLite migration", () => {
expect(parts.length).toBe(1)
expect(parts[0].id).toBe("prt_testabc123")
expect(parts[0].message_id).toBe("msg_test789ghi")
expect(parts[0].session_id).toBe("ses_test456def")
expect(parts[0].session_id).toBe(SessionID.make("ses_test456def"))
expect(parts[0].data).not.toHaveProperty("id")
expect(parts[0].data).not.toHaveProperty("messageID")
expect(parts[0].data).not.toHaveProperty("sessionID")
@@ -336,7 +337,7 @@ describe("JSON to SQLite migration", () => {
const messages = db.select().from(MessageTable).all()
expect(messages.length).toBe(1)
expect(messages[0].id).toBe("msg_from_filename") // Uses filename, not JSON id
expect(messages[0].session_id).toBe("ses_test456def")
expect(messages[0].session_id).toBe(SessionID.make("ses_test456def"))
})
test("uses paths for part id and messageID when JSON has different values", async () => {
@@ -426,7 +427,7 @@ describe("JSON to SQLite migration", () => {
const db = drizzle({ client: sqlite })
const sessions = db.select().from(SessionTable).all()
expect(sessions.length).toBe(1)
expect(sessions[0].id).toBe("ses_migrated")
expect(sessions[0].id).toBe(SessionID.make("ses_migrated"))
expect(sessions[0].project_id).toBe(ProjectID.make(gitBasedProjectID)) // Uses directory, not stale JSON
})
@@ -458,7 +459,7 @@ describe("JSON to SQLite migration", () => {
const db = drizzle({ client: sqlite })
const sessions = db.select().from(SessionTable).all()
expect(sessions.length).toBe(1)
expect(sessions[0].id).toBe("ses_from_filename") // Uses filename, not JSON id
expect(sessions[0].id).toBe(SessionID.make("ses_from_filename")) // Uses filename, not JSON id
expect(sessions[0].project_id).toBe(ProjectID.make("proj_test123abc"))
})

View File

@@ -4,9 +4,10 @@ import * as fs from "fs/promises"
import { ApplyPatchTool } from "../../src/tool/apply_patch"
import { Instance } from "../../src/project/instance"
import { tmpdir } from "../fixture/fixture"
import { SessionID } from "../../src/session/schema"
const baseCtx = {
sessionID: "test",
sessionID: SessionID.make("ses_test"),
messageID: "",
callID: "",
agent: "build",

View File

@@ -7,9 +7,10 @@ import { Filesystem } from "../../src/util/filesystem"
import { tmpdir } from "../fixture/fixture"
import type { PermissionNext } from "../../src/permission/next"
import { Truncate } from "../../src/tool/truncation"
import { SessionID } from "../../src/session/schema"
const ctx = {
sessionID: "test",
sessionID: SessionID.make("ses_test"),
messageID: "",
callID: "",
agent: "build",

View File

@@ -5,9 +5,10 @@ import { EditTool } from "../../src/tool/edit"
import { Instance } from "../../src/project/instance"
import { tmpdir } from "../fixture/fixture"
import { FileTime } from "../../src/file/time"
import { SessionID } from "../../src/session/schema"
const ctx = {
sessionID: "test-edit-session",
sessionID: SessionID.make("ses_test-edit-session"),
messageID: "",
callID: "",
agent: "build",

View File

@@ -4,9 +4,10 @@ import type { Tool } from "../../src/tool/tool"
import { Instance } from "../../src/project/instance"
import { assertExternalDirectory } from "../../src/tool/external-directory"
import type { PermissionNext } from "../../src/permission/next"
import { SessionID } from "../../src/session/schema"
const baseCtx: Omit<Tool.Context, "ask"> = {
sessionID: "test",
sessionID: SessionID.make("ses_test"),
messageID: "",
callID: "",
agent: "build",

View File

@@ -3,9 +3,10 @@ import path from "path"
import { GrepTool } from "../../src/tool/grep"
import { Instance } from "../../src/project/instance"
import { tmpdir } from "../fixture/fixture"
import { SessionID } from "../../src/session/schema"
const ctx = {
sessionID: "test",
sessionID: SessionID.make("ses_test"),
messageID: "",
callID: "",
agent: "build",

View File

@@ -2,9 +2,10 @@ import { describe, expect, test, spyOn, beforeEach, afterEach } from "bun:test"
import { z } from "zod"
import { QuestionTool } from "../../src/tool/question"
import * as QuestionModule from "../../src/question"
import { SessionID } from "../../src/session/schema"
const ctx = {
sessionID: "test-session",
sessionID: SessionID.make("ses_test-session"),
messageID: "test-message",
callID: "test-call",
agent: "test-agent",

View File

@@ -6,11 +6,12 @@ import { Filesystem } from "../../src/util/filesystem"
import { tmpdir } from "../fixture/fixture"
import { PermissionNext } from "../../src/permission/next"
import { Agent } from "../../src/agent/agent"
import { SessionID } from "../../src/session/schema"
const FIXTURES_DIR = path.join(import.meta.dir, "fixtures")
const ctx = {
sessionID: "test",
sessionID: SessionID.make("ses_test"),
messageID: "",
callID: "",
agent: "build",

View File

@@ -6,9 +6,10 @@ import type { Tool } from "../../src/tool/tool"
import { Instance } from "../../src/project/instance"
import { SkillTool } from "../../src/tool/skill"
import { tmpdir } from "../fixture/fixture"
import { SessionID } from "../../src/session/schema"
const baseCtx: Omit<Tool.Context, "ask"> = {
sessionID: "test",
sessionID: SessionID.make("ses_test"),
messageID: "",
callID: "",
agent: "build",

View File

@@ -2,11 +2,12 @@ import { describe, expect, test } from "bun:test"
import path from "path"
import { Instance } from "../../src/project/instance"
import { WebFetchTool } from "../../src/tool/webfetch"
import { SessionID } from "../../src/session/schema"
const projectRoot = path.join(import.meta.dir, "../..")
const ctx = {
sessionID: "test",
sessionID: SessionID.make("ses_test"),
messageID: "message",
callID: "",
agent: "build",

View File

@@ -4,9 +4,10 @@ import fs from "fs/promises"
import { WriteTool } from "../../src/tool/write"
import { Instance } from "../../src/project/instance"
import { tmpdir } from "../fixture/fixture"
import { SessionID } from "../../src/session/schema"
const ctx = {
sessionID: "test-write-session",
sessionID: SessionID.make("ses_test-write-session"),
messageID: "",
callID: "",
agent: "build",