chore: initialize qiming workspace repository
This commit is contained in:
14
qimingcode/packages/opencode/test/storage/db.test.ts
Normal file
14
qimingcode/packages/opencode/test/storage/db.test.ts
Normal file
@@ -0,0 +1,14 @@
|
||||
import { describe, expect, test } from "bun:test"
|
||||
import path from "path"
|
||||
import { Global } from "@opencode-ai/core/global"
|
||||
import { InstallationChannel } from "@opencode-ai/core/installation/version"
|
||||
import { Database } from "../../src/storage"
|
||||
|
||||
describe("Database.Path", () => {
|
||||
test("returns database path for the current channel", () => {
|
||||
const expected = ["latest", "beta"].includes(InstallationChannel)
|
||||
? path.join(Global.Path.data, "opencode.db")
|
||||
: path.join(Global.Path.data, `opencode-${InstallationChannel.replace(/[^a-zA-Z0-9._-]/g, "-")}.db`)
|
||||
expect(Database.getChannelPath()).toBe(expected)
|
||||
})
|
||||
})
|
||||
832
qimingcode/packages/opencode/test/storage/json-migration.test.ts
Normal file
832
qimingcode/packages/opencode/test/storage/json-migration.test.ts
Normal file
@@ -0,0 +1,832 @@
|
||||
import { describe, test, expect, beforeEach, afterEach } from "bun:test"
|
||||
import { Database } from "bun:sqlite"
|
||||
import { drizzle, SQLiteBunDatabase } from "drizzle-orm/bun-sqlite"
|
||||
import { migrate } from "drizzle-orm/bun-sqlite/migrator"
|
||||
import path from "path"
|
||||
import fs from "fs/promises"
|
||||
import { readFileSync, readdirSync } from "fs"
|
||||
import { JsonMigration } from "../../src/storage"
|
||||
import { Global } from "@opencode-ai/core/global"
|
||||
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, MessageID, PartID } from "../../src/session/schema"
|
||||
|
||||
// Test fixtures
|
||||
const fixtures = {
|
||||
project: {
|
||||
id: "proj_test123abc",
|
||||
name: "Test Project",
|
||||
worktree: "/test/path",
|
||||
vcs: "git" as const,
|
||||
sandboxes: [],
|
||||
},
|
||||
session: {
|
||||
id: "ses_test456def",
|
||||
projectID: "proj_test123abc",
|
||||
slug: "test-session",
|
||||
directory: "/test/path",
|
||||
title: "Test Session",
|
||||
version: "1.0.0",
|
||||
time: { created: 1700000000000, updated: 1700000001000 },
|
||||
},
|
||||
message: {
|
||||
id: "msg_test789ghi",
|
||||
sessionID: "ses_test456def",
|
||||
role: "user" as const,
|
||||
agent: "default",
|
||||
model: { providerID: "openai", modelID: "gpt-4" },
|
||||
time: { created: 1700000000000 },
|
||||
},
|
||||
part: {
|
||||
id: "prt_testabc123",
|
||||
messageID: "msg_test789ghi",
|
||||
sessionID: "ses_test456def",
|
||||
type: "text" as const,
|
||||
text: "Hello, world!",
|
||||
},
|
||||
}
|
||||
|
||||
// Helper to create test storage directory structure
|
||||
async function setupStorageDir() {
|
||||
const storageDir = path.join(Global.Path.data, "storage")
|
||||
await fs.rm(storageDir, { recursive: true, force: true })
|
||||
await fs.mkdir(path.join(storageDir, "project"), { recursive: true })
|
||||
await fs.mkdir(path.join(storageDir, "session", "proj_test123abc"), { recursive: true })
|
||||
await fs.mkdir(path.join(storageDir, "message", "ses_test456def"), { recursive: true })
|
||||
await fs.mkdir(path.join(storageDir, "part", "msg_test789ghi"), { recursive: true })
|
||||
await fs.mkdir(path.join(storageDir, "session_diff"), { recursive: true })
|
||||
await fs.mkdir(path.join(storageDir, "todo"), { recursive: true })
|
||||
await fs.mkdir(path.join(storageDir, "permission"), { recursive: true })
|
||||
await fs.mkdir(path.join(storageDir, "session_share"), { recursive: true })
|
||||
// Create legacy marker to indicate JSON storage exists
|
||||
await Bun.write(path.join(storageDir, "migration"), "1")
|
||||
return storageDir
|
||||
}
|
||||
|
||||
async function writeProject(storageDir: string, project: Record<string, unknown>) {
|
||||
await Bun.write(path.join(storageDir, "project", `${project.id}.json`), JSON.stringify(project))
|
||||
}
|
||||
|
||||
async function writeSession(storageDir: string, projectID: string, session: Record<string, unknown>) {
|
||||
await Bun.write(path.join(storageDir, "session", projectID, `${session.id}.json`), JSON.stringify(session))
|
||||
}
|
||||
|
||||
// Helper to create in-memory test database with schema
|
||||
function createTestDb() {
|
||||
const sqlite = new Database(":memory:")
|
||||
sqlite.exec("PRAGMA foreign_keys = ON")
|
||||
|
||||
// Apply schema migrations using drizzle migrate
|
||||
const dir = path.join(import.meta.dirname, "../../migration")
|
||||
const entries = readdirSync(dir, { withFileTypes: true })
|
||||
const migrations = entries
|
||||
.filter((entry) => entry.isDirectory())
|
||||
.map((entry) => ({
|
||||
sql: readFileSync(path.join(dir, entry.name, "migration.sql"), "utf-8"),
|
||||
timestamp: Number(entry.name.split("_")[0]),
|
||||
name: entry.name,
|
||||
}))
|
||||
.sort((a, b) => a.timestamp - b.timestamp)
|
||||
|
||||
const db = drizzle({ client: sqlite })
|
||||
migrate(db, migrations)
|
||||
|
||||
return [sqlite, db] as const
|
||||
}
|
||||
|
||||
describe("JSON to SQLite migration", () => {
|
||||
let storageDir: string
|
||||
let sqlite: Database
|
||||
let db: SQLiteBunDatabase
|
||||
|
||||
beforeEach(async () => {
|
||||
storageDir = await setupStorageDir()
|
||||
;[sqlite, db] = createTestDb()
|
||||
})
|
||||
|
||||
afterEach(async () => {
|
||||
sqlite.close()
|
||||
await fs.rm(storageDir, { recursive: true, force: true })
|
||||
})
|
||||
|
||||
test("migrates project", async () => {
|
||||
await writeProject(storageDir, {
|
||||
id: "proj_test123abc",
|
||||
worktree: "/test/path",
|
||||
vcs: "git",
|
||||
name: "Test Project",
|
||||
time: { created: 1700000000000, updated: 1700000001000 },
|
||||
sandboxes: ["/test/sandbox"],
|
||||
})
|
||||
|
||||
const stats = await JsonMigration.run(db)
|
||||
|
||||
expect(stats?.projects).toBe(1)
|
||||
|
||||
const projects = db.select().from(ProjectTable).all()
|
||||
expect(projects.length).toBe(1)
|
||||
expect(projects[0].id).toBe(ProjectID.make("proj_test123abc"))
|
||||
expect(projects[0].worktree).toBe("/test/path")
|
||||
expect(projects[0].name).toBe("Test Project")
|
||||
expect(projects[0].sandboxes).toEqual(["/test/sandbox"])
|
||||
})
|
||||
|
||||
test("uses filename for project id when JSON has different value", async () => {
|
||||
await Bun.write(
|
||||
path.join(storageDir, "project", "proj_filename.json"),
|
||||
JSON.stringify({
|
||||
id: "proj_different_in_json", // Stale! Should be ignored
|
||||
worktree: "/test/path",
|
||||
vcs: "git",
|
||||
name: "Test Project",
|
||||
sandboxes: [],
|
||||
}),
|
||||
)
|
||||
|
||||
const stats = await JsonMigration.run(db)
|
||||
|
||||
expect(stats?.projects).toBe(1)
|
||||
|
||||
const projects = db.select().from(ProjectTable).all()
|
||||
expect(projects.length).toBe(1)
|
||||
expect(projects[0].id).toBe(ProjectID.make("proj_filename")) // Uses filename, not JSON id
|
||||
})
|
||||
|
||||
test("migrates project with commands", async () => {
|
||||
await writeProject(storageDir, {
|
||||
id: "proj_with_commands",
|
||||
worktree: "/test/path",
|
||||
vcs: "git",
|
||||
name: "Project With Commands",
|
||||
time: { created: 1700000000000, updated: 1700000001000 },
|
||||
sandboxes: ["/test/sandbox"],
|
||||
commands: { start: "npm run dev" },
|
||||
})
|
||||
|
||||
const stats = await JsonMigration.run(db)
|
||||
|
||||
expect(stats?.projects).toBe(1)
|
||||
|
||||
const projects = db.select().from(ProjectTable).all()
|
||||
expect(projects.length).toBe(1)
|
||||
expect(projects[0].id).toBe(ProjectID.make("proj_with_commands"))
|
||||
expect(projects[0].commands).toEqual({ start: "npm run dev" })
|
||||
})
|
||||
|
||||
test("migrates project without commands field", async () => {
|
||||
await writeProject(storageDir, {
|
||||
id: "proj_no_commands",
|
||||
worktree: "/test/path",
|
||||
vcs: "git",
|
||||
name: "Project Without Commands",
|
||||
time: { created: 1700000000000, updated: 1700000001000 },
|
||||
sandboxes: [],
|
||||
})
|
||||
|
||||
const stats = await JsonMigration.run(db)
|
||||
|
||||
expect(stats?.projects).toBe(1)
|
||||
|
||||
const projects = db.select().from(ProjectTable).all()
|
||||
expect(projects.length).toBe(1)
|
||||
expect(projects[0].id).toBe(ProjectID.make("proj_no_commands"))
|
||||
expect(projects[0].commands).toBeNull()
|
||||
})
|
||||
|
||||
test("migrates session with individual columns", async () => {
|
||||
await writeProject(storageDir, {
|
||||
id: "proj_test123abc",
|
||||
worktree: "/test/path",
|
||||
time: { created: Date.now(), updated: Date.now() },
|
||||
sandboxes: [],
|
||||
})
|
||||
|
||||
await writeSession(storageDir, "proj_test123abc", {
|
||||
id: "ses_test456def",
|
||||
projectID: "proj_test123abc",
|
||||
slug: "test-session",
|
||||
directory: "/test/dir",
|
||||
title: "Test Session Title",
|
||||
version: "1.0.0",
|
||||
time: { created: 1700000000000, updated: 1700000001000 },
|
||||
summary: { additions: 10, deletions: 5, files: 3 },
|
||||
share: { url: "https://example.com/share" },
|
||||
})
|
||||
|
||||
await JsonMigration.run(db)
|
||||
|
||||
const sessions = db.select().from(SessionTable).all()
|
||||
expect(sessions.length).toBe(1)
|
||||
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")
|
||||
expect(sessions[0].summary_additions).toBe(10)
|
||||
expect(sessions[0].summary_deletions).toBe(5)
|
||||
expect(sessions[0].share_url).toBe("https://example.com/share")
|
||||
})
|
||||
|
||||
test("migrates messages and parts", async () => {
|
||||
await writeProject(storageDir, {
|
||||
id: "proj_test123abc",
|
||||
worktree: "/",
|
||||
time: { created: Date.now(), updated: Date.now() },
|
||||
sandboxes: [],
|
||||
})
|
||||
await writeSession(storageDir, "proj_test123abc", { ...fixtures.session })
|
||||
await Bun.write(
|
||||
path.join(storageDir, "message", "ses_test456def", "msg_test789ghi.json"),
|
||||
JSON.stringify({ ...fixtures.message }),
|
||||
)
|
||||
await Bun.write(
|
||||
path.join(storageDir, "part", "msg_test789ghi", "prt_testabc123.json"),
|
||||
JSON.stringify({ ...fixtures.part }),
|
||||
)
|
||||
|
||||
const stats = await JsonMigration.run(db)
|
||||
|
||||
expect(stats?.messages).toBe(1)
|
||||
expect(stats?.parts).toBe(1)
|
||||
|
||||
const messages = db.select().from(MessageTable).all()
|
||||
expect(messages.length).toBe(1)
|
||||
expect(messages[0].id).toBe(MessageID.make("msg_test789ghi"))
|
||||
|
||||
const parts = db.select().from(PartTable).all()
|
||||
expect(parts.length).toBe(1)
|
||||
expect(parts[0].id).toBe(PartID.make("prt_testabc123"))
|
||||
})
|
||||
|
||||
test("migrates legacy parts without ids in body", async () => {
|
||||
await writeProject(storageDir, {
|
||||
id: "proj_test123abc",
|
||||
worktree: "/",
|
||||
time: { created: Date.now(), updated: Date.now() },
|
||||
sandboxes: [],
|
||||
})
|
||||
await writeSession(storageDir, "proj_test123abc", { ...fixtures.session })
|
||||
await Bun.write(
|
||||
path.join(storageDir, "message", "ses_test456def", "msg_test789ghi.json"),
|
||||
JSON.stringify({
|
||||
role: "user",
|
||||
agent: "default",
|
||||
model: { providerID: "openai", modelID: "gpt-4" },
|
||||
time: { created: 1700000000000 },
|
||||
}),
|
||||
)
|
||||
await Bun.write(
|
||||
path.join(storageDir, "part", "msg_test789ghi", "prt_testabc123.json"),
|
||||
JSON.stringify({
|
||||
type: "text",
|
||||
text: "Hello, world!",
|
||||
}),
|
||||
)
|
||||
|
||||
const stats = await JsonMigration.run(db)
|
||||
|
||||
expect(stats?.messages).toBe(1)
|
||||
expect(stats?.parts).toBe(1)
|
||||
|
||||
const messages = db.select().from(MessageTable).all()
|
||||
expect(messages.length).toBe(1)
|
||||
expect(messages[0].id).toBe(MessageID.make("msg_test789ghi"))
|
||||
expect(messages[0].session_id).toBe(SessionID.make("ses_test456def"))
|
||||
expect(messages[0].data).not.toHaveProperty("id")
|
||||
expect(messages[0].data).not.toHaveProperty("sessionID")
|
||||
|
||||
const parts = db.select().from(PartTable).all()
|
||||
expect(parts.length).toBe(1)
|
||||
expect(parts[0].id).toBe(PartID.make("prt_testabc123"))
|
||||
expect(parts[0].message_id).toBe(MessageID.make("msg_test789ghi"))
|
||||
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")
|
||||
})
|
||||
|
||||
test("uses filename for message id when JSON has different value", async () => {
|
||||
await writeProject(storageDir, {
|
||||
id: "proj_test123abc",
|
||||
worktree: "/",
|
||||
time: { created: Date.now(), updated: Date.now() },
|
||||
sandboxes: [],
|
||||
})
|
||||
await writeSession(storageDir, "proj_test123abc", { ...fixtures.session })
|
||||
await Bun.write(
|
||||
path.join(storageDir, "message", "ses_test456def", "msg_from_filename.json"),
|
||||
JSON.stringify({
|
||||
id: "msg_different_in_json", // Stale! Should be ignored
|
||||
sessionID: "ses_test456def",
|
||||
role: "user",
|
||||
agent: "default",
|
||||
time: { created: 1700000000000 },
|
||||
}),
|
||||
)
|
||||
|
||||
const stats = await JsonMigration.run(db)
|
||||
|
||||
expect(stats?.messages).toBe(1)
|
||||
|
||||
const messages = db.select().from(MessageTable).all()
|
||||
expect(messages.length).toBe(1)
|
||||
expect(messages[0].id).toBe(MessageID.make("msg_from_filename")) // Uses filename, not JSON id
|
||||
expect(messages[0].session_id).toBe(SessionID.make("ses_test456def"))
|
||||
})
|
||||
|
||||
test("uses paths for part id and messageID when JSON has different values", async () => {
|
||||
await writeProject(storageDir, {
|
||||
id: "proj_test123abc",
|
||||
worktree: "/",
|
||||
time: { created: Date.now(), updated: Date.now() },
|
||||
sandboxes: [],
|
||||
})
|
||||
await writeSession(storageDir, "proj_test123abc", { ...fixtures.session })
|
||||
await Bun.write(
|
||||
path.join(storageDir, "message", "ses_test456def", "msg_realmsgid.json"),
|
||||
JSON.stringify({
|
||||
role: "user",
|
||||
agent: "default",
|
||||
time: { created: 1700000000000 },
|
||||
}),
|
||||
)
|
||||
await Bun.write(
|
||||
path.join(storageDir, "part", "msg_realmsgid", "prt_from_filename.json"),
|
||||
JSON.stringify({
|
||||
id: "prt_different_in_json", // Stale! Should be ignored
|
||||
messageID: "msg_different_in_json", // Stale! Should be ignored
|
||||
sessionID: "ses_test456def",
|
||||
type: "text",
|
||||
text: "Hello",
|
||||
}),
|
||||
)
|
||||
|
||||
const stats = await JsonMigration.run(db)
|
||||
|
||||
expect(stats?.parts).toBe(1)
|
||||
|
||||
const parts = db.select().from(PartTable).all()
|
||||
expect(parts.length).toBe(1)
|
||||
expect(parts[0].id).toBe(PartID.make("prt_from_filename")) // Uses filename, not JSON id
|
||||
expect(parts[0].message_id).toBe(MessageID.make("msg_realmsgid")) // Uses parent dir, not JSON messageID
|
||||
})
|
||||
|
||||
test("skips orphaned sessions (no parent project)", async () => {
|
||||
await Bun.write(
|
||||
path.join(storageDir, "session", "proj_test123abc", "ses_orphan.json"),
|
||||
JSON.stringify({
|
||||
id: "ses_orphan",
|
||||
projectID: "proj_nonexistent",
|
||||
slug: "orphan",
|
||||
directory: "/",
|
||||
title: "Orphan",
|
||||
version: "1.0.0",
|
||||
time: { created: Date.now(), updated: Date.now() },
|
||||
}),
|
||||
)
|
||||
|
||||
const stats = await JsonMigration.run(db)
|
||||
|
||||
expect(stats?.sessions).toBe(0)
|
||||
})
|
||||
|
||||
test("uses directory path for projectID when JSON has stale value", async () => {
|
||||
// Simulates the scenario where earlier migration moved sessions to new
|
||||
// git-based project directories but didn't update the projectID field
|
||||
const gitBasedProjectID = "abc123gitcommit"
|
||||
await writeProject(storageDir, {
|
||||
id: gitBasedProjectID,
|
||||
worktree: "/test/path",
|
||||
vcs: "git",
|
||||
time: { created: Date.now(), updated: Date.now() },
|
||||
sandboxes: [],
|
||||
})
|
||||
|
||||
// Session is in the git-based directory but JSON still has old projectID
|
||||
await writeSession(storageDir, gitBasedProjectID, {
|
||||
id: "ses_migrated",
|
||||
projectID: "old-project-name", // Stale! Should be ignored
|
||||
slug: "migrated-session",
|
||||
directory: "/test/path",
|
||||
title: "Migrated Session",
|
||||
version: "1.0.0",
|
||||
time: { created: 1700000000000, updated: 1700000001000 },
|
||||
})
|
||||
|
||||
const stats = await JsonMigration.run(db)
|
||||
|
||||
expect(stats?.sessions).toBe(1)
|
||||
|
||||
const sessions = db.select().from(SessionTable).all()
|
||||
expect(sessions.length).toBe(1)
|
||||
expect(sessions[0].id).toBe(SessionID.make("ses_migrated"))
|
||||
expect(sessions[0].project_id).toBe(ProjectID.make(gitBasedProjectID)) // Uses directory, not stale JSON
|
||||
})
|
||||
|
||||
test("uses filename for session id when JSON has different value", async () => {
|
||||
await writeProject(storageDir, {
|
||||
id: "proj_test123abc",
|
||||
worktree: "/test/path",
|
||||
time: { created: Date.now(), updated: Date.now() },
|
||||
sandboxes: [],
|
||||
})
|
||||
|
||||
await Bun.write(
|
||||
path.join(storageDir, "session", "proj_test123abc", "ses_from_filename.json"),
|
||||
JSON.stringify({
|
||||
id: "ses_different_in_json", // Stale! Should be ignored
|
||||
projectID: "proj_test123abc",
|
||||
slug: "test-session",
|
||||
directory: "/test/path",
|
||||
title: "Test Session",
|
||||
version: "1.0.0",
|
||||
time: { created: 1700000000000, updated: 1700000001000 },
|
||||
}),
|
||||
)
|
||||
|
||||
const stats = await JsonMigration.run(db)
|
||||
|
||||
expect(stats?.sessions).toBe(1)
|
||||
|
||||
const sessions = db.select().from(SessionTable).all()
|
||||
expect(sessions.length).toBe(1)
|
||||
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"))
|
||||
})
|
||||
|
||||
test("is idempotent (running twice doesn't duplicate)", async () => {
|
||||
await writeProject(storageDir, {
|
||||
id: "proj_test123abc",
|
||||
worktree: "/",
|
||||
time: { created: Date.now(), updated: Date.now() },
|
||||
sandboxes: [],
|
||||
})
|
||||
|
||||
await JsonMigration.run(db)
|
||||
await JsonMigration.run(db)
|
||||
|
||||
const projects = db.select().from(ProjectTable).all()
|
||||
expect(projects.length).toBe(1) // Still only 1 due to onConflictDoNothing
|
||||
})
|
||||
|
||||
test("migrates todos", async () => {
|
||||
await writeProject(storageDir, {
|
||||
id: "proj_test123abc",
|
||||
worktree: "/",
|
||||
time: { created: Date.now(), updated: Date.now() },
|
||||
sandboxes: [],
|
||||
})
|
||||
await writeSession(storageDir, "proj_test123abc", { ...fixtures.session })
|
||||
|
||||
// Create todo file (named by sessionID, contains array of todos)
|
||||
await Bun.write(
|
||||
path.join(storageDir, "todo", "ses_test456def.json"),
|
||||
JSON.stringify([
|
||||
{
|
||||
id: "todo_1",
|
||||
content: "First todo",
|
||||
status: "pending",
|
||||
priority: "high",
|
||||
},
|
||||
{
|
||||
id: "todo_2",
|
||||
content: "Second todo",
|
||||
status: "completed",
|
||||
priority: "medium",
|
||||
},
|
||||
]),
|
||||
)
|
||||
|
||||
const stats = await JsonMigration.run(db)
|
||||
|
||||
expect(stats?.todos).toBe(2)
|
||||
|
||||
const todos = db.select().from(TodoTable).orderBy(TodoTable.position).all()
|
||||
expect(todos.length).toBe(2)
|
||||
expect(todos[0].content).toBe("First todo")
|
||||
expect(todos[0].status).toBe("pending")
|
||||
expect(todos[0].priority).toBe("high")
|
||||
expect(todos[0].position).toBe(0)
|
||||
expect(todos[1].content).toBe("Second todo")
|
||||
expect(todos[1].position).toBe(1)
|
||||
})
|
||||
|
||||
test("todos are ordered by position", async () => {
|
||||
await writeProject(storageDir, {
|
||||
id: "proj_test123abc",
|
||||
worktree: "/",
|
||||
time: { created: Date.now(), updated: Date.now() },
|
||||
sandboxes: [],
|
||||
})
|
||||
await writeSession(storageDir, "proj_test123abc", { ...fixtures.session })
|
||||
|
||||
await Bun.write(
|
||||
path.join(storageDir, "todo", "ses_test456def.json"),
|
||||
JSON.stringify([
|
||||
{ content: "Third", status: "pending", priority: "low" },
|
||||
{ content: "First", status: "pending", priority: "high" },
|
||||
{ content: "Second", status: "in_progress", priority: "medium" },
|
||||
]),
|
||||
)
|
||||
|
||||
await JsonMigration.run(db)
|
||||
|
||||
const todos = db.select().from(TodoTable).orderBy(TodoTable.position).all()
|
||||
|
||||
expect(todos.length).toBe(3)
|
||||
expect(todos[0].content).toBe("Third")
|
||||
expect(todos[0].position).toBe(0)
|
||||
expect(todos[1].content).toBe("First")
|
||||
expect(todos[1].position).toBe(1)
|
||||
expect(todos[2].content).toBe("Second")
|
||||
expect(todos[2].position).toBe(2)
|
||||
})
|
||||
|
||||
test("migrates permissions", async () => {
|
||||
await writeProject(storageDir, {
|
||||
id: "proj_test123abc",
|
||||
worktree: "/",
|
||||
time: { created: Date.now(), updated: Date.now() },
|
||||
sandboxes: [],
|
||||
})
|
||||
|
||||
// Create permission file (named by projectID, contains array of rules)
|
||||
const permissionData = [
|
||||
{ permission: "file.read", pattern: "/test/file1.ts", action: "allow" as const },
|
||||
{ permission: "file.write", pattern: "/test/file2.ts", action: "ask" as const },
|
||||
{ permission: "command.run", pattern: "npm install", action: "deny" as const },
|
||||
]
|
||||
await Bun.write(path.join(storageDir, "permission", "proj_test123abc.json"), JSON.stringify(permissionData))
|
||||
|
||||
const stats = await JsonMigration.run(db)
|
||||
|
||||
expect(stats?.permissions).toBe(1)
|
||||
|
||||
const permissions = db.select().from(PermissionTable).all()
|
||||
expect(permissions.length).toBe(1)
|
||||
expect(permissions[0].project_id).toBe("proj_test123abc")
|
||||
expect(permissions[0].data).toEqual(permissionData)
|
||||
})
|
||||
|
||||
test("migrates session shares", async () => {
|
||||
await writeProject(storageDir, {
|
||||
id: "proj_test123abc",
|
||||
worktree: "/",
|
||||
time: { created: Date.now(), updated: Date.now() },
|
||||
sandboxes: [],
|
||||
})
|
||||
await writeSession(storageDir, "proj_test123abc", { ...fixtures.session })
|
||||
|
||||
// Create session share file (named by sessionID)
|
||||
await Bun.write(
|
||||
path.join(storageDir, "session_share", "ses_test456def.json"),
|
||||
JSON.stringify({
|
||||
id: "share_123",
|
||||
secret: "supersecretkey",
|
||||
url: "https://share.example.com/ses_test456def",
|
||||
}),
|
||||
)
|
||||
|
||||
const stats = await JsonMigration.run(db)
|
||||
|
||||
expect(stats?.shares).toBe(1)
|
||||
|
||||
const shares = db.select().from(SessionShareTable).all()
|
||||
expect(shares.length).toBe(1)
|
||||
expect(shares[0].session_id).toBe("ses_test456def")
|
||||
expect(shares[0].id).toBe("share_123")
|
||||
expect(shares[0].secret).toBe("supersecretkey")
|
||||
expect(shares[0].url).toBe("https://share.example.com/ses_test456def")
|
||||
})
|
||||
|
||||
test("returns empty stats when storage directory does not exist", async () => {
|
||||
await fs.rm(storageDir, { recursive: true, force: true })
|
||||
|
||||
const stats = await JsonMigration.run(db)
|
||||
|
||||
expect(stats.projects).toBe(0)
|
||||
expect(stats.sessions).toBe(0)
|
||||
expect(stats.messages).toBe(0)
|
||||
expect(stats.parts).toBe(0)
|
||||
expect(stats.todos).toBe(0)
|
||||
expect(stats.permissions).toBe(0)
|
||||
expect(stats.shares).toBe(0)
|
||||
expect(stats.errors).toEqual([])
|
||||
})
|
||||
|
||||
test("continues when a JSON file is unreadable and records an error", async () => {
|
||||
await writeProject(storageDir, {
|
||||
id: "proj_test123abc",
|
||||
worktree: "/",
|
||||
time: { created: Date.now(), updated: Date.now() },
|
||||
sandboxes: [],
|
||||
})
|
||||
await Bun.write(path.join(storageDir, "project", "broken.json"), "{ invalid json")
|
||||
|
||||
const stats = await JsonMigration.run(db)
|
||||
|
||||
expect(stats.projects).toBe(1)
|
||||
expect(stats.errors.some((x) => x.includes("failed to read") && x.includes("broken.json"))).toBe(true)
|
||||
|
||||
const projects = db.select().from(ProjectTable).all()
|
||||
expect(projects.length).toBe(1)
|
||||
expect(projects[0].id).toBe(ProjectID.make("proj_test123abc"))
|
||||
})
|
||||
|
||||
test("skips invalid todo entries while preserving source positions", async () => {
|
||||
await writeProject(storageDir, {
|
||||
id: "proj_test123abc",
|
||||
worktree: "/",
|
||||
time: { created: Date.now(), updated: Date.now() },
|
||||
sandboxes: [],
|
||||
})
|
||||
await writeSession(storageDir, "proj_test123abc", { ...fixtures.session })
|
||||
|
||||
await Bun.write(
|
||||
path.join(storageDir, "todo", "ses_test456def.json"),
|
||||
JSON.stringify([
|
||||
{ content: "keep-0", status: "pending", priority: "high" },
|
||||
{ content: "drop-1", priority: "low" },
|
||||
{ content: "keep-2", status: "completed", priority: "medium" },
|
||||
]),
|
||||
)
|
||||
|
||||
const stats = await JsonMigration.run(db)
|
||||
expect(stats.todos).toBe(2)
|
||||
|
||||
const todos = db.select().from(TodoTable).orderBy(TodoTable.position).all()
|
||||
expect(todos.length).toBe(2)
|
||||
expect(todos[0].content).toBe("keep-0")
|
||||
expect(todos[0].position).toBe(0)
|
||||
expect(todos[1].content).toBe("keep-2")
|
||||
expect(todos[1].position).toBe(2)
|
||||
})
|
||||
|
||||
test("skips orphaned todos, permissions, and shares", async () => {
|
||||
await writeProject(storageDir, {
|
||||
id: "proj_test123abc",
|
||||
worktree: "/",
|
||||
time: { created: Date.now(), updated: Date.now() },
|
||||
sandboxes: [],
|
||||
})
|
||||
await writeSession(storageDir, "proj_test123abc", { ...fixtures.session })
|
||||
|
||||
await Bun.write(
|
||||
path.join(storageDir, "todo", "ses_test456def.json"),
|
||||
JSON.stringify([{ content: "valid", status: "pending", priority: "high" }]),
|
||||
)
|
||||
await Bun.write(
|
||||
path.join(storageDir, "todo", "ses_missing.json"),
|
||||
JSON.stringify([{ content: "orphan", status: "pending", priority: "high" }]),
|
||||
)
|
||||
|
||||
await Bun.write(
|
||||
path.join(storageDir, "permission", "proj_test123abc.json"),
|
||||
JSON.stringify([{ permission: "file.read" }]),
|
||||
)
|
||||
await Bun.write(
|
||||
path.join(storageDir, "permission", "proj_missing.json"),
|
||||
JSON.stringify([{ permission: "file.write" }]),
|
||||
)
|
||||
|
||||
await Bun.write(
|
||||
path.join(storageDir, "session_share", "ses_test456def.json"),
|
||||
JSON.stringify({ id: "share_ok", secret: "secret", url: "https://ok.example.com" }),
|
||||
)
|
||||
await Bun.write(
|
||||
path.join(storageDir, "session_share", "ses_missing.json"),
|
||||
JSON.stringify({ id: "share_missing", secret: "secret", url: "https://missing.example.com" }),
|
||||
)
|
||||
|
||||
const stats = await JsonMigration.run(db)
|
||||
|
||||
expect(stats.todos).toBe(1)
|
||||
expect(stats.permissions).toBe(1)
|
||||
expect(stats.shares).toBe(1)
|
||||
|
||||
expect(db.select().from(TodoTable).all().length).toBe(1)
|
||||
expect(db.select().from(PermissionTable).all().length).toBe(1)
|
||||
expect(db.select().from(SessionShareTable).all().length).toBe(1)
|
||||
})
|
||||
|
||||
test("handles mixed corruption and partial validity in one migration run", async () => {
|
||||
await writeProject(storageDir, {
|
||||
id: "proj_test123abc",
|
||||
worktree: "/ok",
|
||||
time: { created: 1700000000000, updated: 1700000001000 },
|
||||
sandboxes: [],
|
||||
})
|
||||
await Bun.write(
|
||||
path.join(storageDir, "project", "proj_missing_id.json"),
|
||||
JSON.stringify({ worktree: "/bad", sandboxes: [] }),
|
||||
)
|
||||
await Bun.write(path.join(storageDir, "project", "proj_broken.json"), "{ nope")
|
||||
|
||||
await writeSession(storageDir, "proj_test123abc", {
|
||||
id: "ses_test456def",
|
||||
projectID: "proj_test123abc",
|
||||
slug: "ok",
|
||||
directory: "/ok",
|
||||
title: "Ok",
|
||||
version: "1",
|
||||
time: { created: 1700000000000, updated: 1700000001000 },
|
||||
})
|
||||
await Bun.write(
|
||||
path.join(storageDir, "session", "proj_test123abc", "ses_missing_project.json"),
|
||||
JSON.stringify({
|
||||
id: "ses_missing_project",
|
||||
slug: "bad",
|
||||
directory: "/bad",
|
||||
title: "Bad",
|
||||
version: "1",
|
||||
}),
|
||||
)
|
||||
await Bun.write(
|
||||
path.join(storageDir, "session", "proj_test123abc", "ses_orphan.json"),
|
||||
JSON.stringify({
|
||||
id: "ses_orphan",
|
||||
projectID: "proj_missing",
|
||||
slug: "orphan",
|
||||
directory: "/bad",
|
||||
title: "Orphan",
|
||||
version: "1",
|
||||
}),
|
||||
)
|
||||
|
||||
await Bun.write(
|
||||
path.join(storageDir, "message", "ses_test456def", "msg_ok.json"),
|
||||
JSON.stringify({ role: "user", time: { created: 1700000000000 } }),
|
||||
)
|
||||
await Bun.write(path.join(storageDir, "message", "ses_test456def", "msg_broken.json"), "{ nope")
|
||||
await Bun.write(
|
||||
path.join(storageDir, "message", "ses_missing", "msg_orphan.json"),
|
||||
JSON.stringify({ role: "user", time: { created: 1700000000000 } }),
|
||||
)
|
||||
|
||||
await Bun.write(
|
||||
path.join(storageDir, "part", "msg_ok", "part_ok.json"),
|
||||
JSON.stringify({ type: "text", text: "ok" }),
|
||||
)
|
||||
await Bun.write(
|
||||
path.join(storageDir, "part", "msg_missing", "part_missing_message.json"),
|
||||
JSON.stringify({ type: "text", text: "bad" }),
|
||||
)
|
||||
await Bun.write(path.join(storageDir, "part", "msg_ok", "part_broken.json"), "{ nope")
|
||||
|
||||
await Bun.write(
|
||||
path.join(storageDir, "todo", "ses_test456def.json"),
|
||||
JSON.stringify([
|
||||
{ content: "ok", status: "pending", priority: "high" },
|
||||
{ content: "skip", status: "pending" },
|
||||
]),
|
||||
)
|
||||
await Bun.write(
|
||||
path.join(storageDir, "todo", "ses_missing.json"),
|
||||
JSON.stringify([{ content: "orphan", status: "pending", priority: "high" }]),
|
||||
)
|
||||
await Bun.write(path.join(storageDir, "todo", "ses_broken.json"), "{ nope")
|
||||
|
||||
await Bun.write(
|
||||
path.join(storageDir, "permission", "proj_test123abc.json"),
|
||||
JSON.stringify([{ permission: "file.read" }]),
|
||||
)
|
||||
await Bun.write(
|
||||
path.join(storageDir, "permission", "proj_missing.json"),
|
||||
JSON.stringify([{ permission: "file.write" }]),
|
||||
)
|
||||
await Bun.write(path.join(storageDir, "permission", "proj_broken.json"), "{ nope")
|
||||
|
||||
await Bun.write(
|
||||
path.join(storageDir, "session_share", "ses_test456def.json"),
|
||||
JSON.stringify({ id: "share_ok", secret: "secret", url: "https://ok.example.com" }),
|
||||
)
|
||||
await Bun.write(
|
||||
path.join(storageDir, "session_share", "ses_missing.json"),
|
||||
JSON.stringify({ id: "share_orphan", secret: "secret", url: "https://missing.example.com" }),
|
||||
)
|
||||
await Bun.write(path.join(storageDir, "session_share", "ses_broken.json"), "{ nope")
|
||||
|
||||
const stats = await JsonMigration.run(db)
|
||||
|
||||
// Projects: proj_test123abc (valid), proj_missing_id (now derives id from filename)
|
||||
// Sessions: ses_test456def (valid), ses_missing_project (now uses dir path),
|
||||
// ses_orphan (now uses dir path, ignores stale projectID)
|
||||
expect(stats.projects).toBe(2)
|
||||
expect(stats.sessions).toBe(3)
|
||||
expect(stats.messages).toBe(1)
|
||||
expect(stats.parts).toBe(1)
|
||||
expect(stats.todos).toBe(1)
|
||||
expect(stats.permissions).toBe(1)
|
||||
expect(stats.shares).toBe(1)
|
||||
expect(stats.errors.length).toBeGreaterThanOrEqual(6)
|
||||
|
||||
expect(db.select().from(ProjectTable).all().length).toBe(2)
|
||||
expect(db.select().from(SessionTable).all().length).toBe(3)
|
||||
expect(db.select().from(MessageTable).all().length).toBe(1)
|
||||
expect(db.select().from(PartTable).all().length).toBe(1)
|
||||
expect(db.select().from(TodoTable).all().length).toBe(1)
|
||||
expect(db.select().from(PermissionTable).all().length).toBe(1)
|
||||
expect(db.select().from(SessionShareTable).all().length).toBe(1)
|
||||
})
|
||||
})
|
||||
293
qimingcode/packages/opencode/test/storage/storage.test.ts
Normal file
293
qimingcode/packages/opencode/test/storage/storage.test.ts
Normal file
@@ -0,0 +1,293 @@
|
||||
import { describe, expect } from "bun:test"
|
||||
import path from "path"
|
||||
import { Effect, Exit, Layer } from "effect"
|
||||
import { AppFileSystem } from "@opencode-ai/core/filesystem"
|
||||
import { CrossSpawnSpawner } from "@opencode-ai/core/cross-spawn-spawner"
|
||||
import { Git } from "../../src/git"
|
||||
import { Global } from "@opencode-ai/core/global"
|
||||
import { Storage } from "../../src/storage"
|
||||
import { tmpdirScoped } from "../fixture/fixture"
|
||||
import { testEffect } from "../lib/effect"
|
||||
|
||||
const dir = path.join(Global.Path.data, "storage")
|
||||
|
||||
const it = testEffect(Layer.mergeAll(Storage.defaultLayer, AppFileSystem.defaultLayer, CrossSpawnSpawner.defaultLayer))
|
||||
|
||||
const scope = Effect.fnUntraced(function* () {
|
||||
const root = ["storage_test", crypto.randomUUID()]
|
||||
const fs = yield* AppFileSystem.Service
|
||||
const svc = yield* Storage.Service
|
||||
yield* Effect.addFinalizer(() =>
|
||||
fs.remove(path.join(dir, ...root), { recursive: true, force: true }).pipe(Effect.ignore),
|
||||
)
|
||||
return { root, svc }
|
||||
})
|
||||
|
||||
// remap(root) rewrites any path under Global.Path.data to live under `root` instead.
|
||||
// Used by remappedFs to build an AppFileSystem that Storage thinks is the real global
|
||||
// data dir but actually targets a tmp dir — letting migration tests stage legacy layouts.
|
||||
// NOTE: only the 6 methods below are intercepted. If Storage starts using a different
|
||||
// AppFileSystem method that touches Global.Path.data, add it here.
|
||||
function remap(root: string, file: string) {
|
||||
if (file === Global.Path.data) return root
|
||||
if (file.startsWith(Global.Path.data + path.sep)) return path.join(root, path.relative(Global.Path.data, file))
|
||||
return file
|
||||
}
|
||||
|
||||
function remappedFs(root: string) {
|
||||
return Layer.effect(
|
||||
AppFileSystem.Service,
|
||||
Effect.gen(function* () {
|
||||
const fs = yield* AppFileSystem.Service
|
||||
return AppFileSystem.Service.of({
|
||||
...fs,
|
||||
isDir: (file) => fs.isDir(remap(root, file)),
|
||||
readJson: (file) => fs.readJson(remap(root, file)),
|
||||
writeWithDirs: (file, content, mode) => fs.writeWithDirs(remap(root, file), content, mode),
|
||||
readFileString: (file) => fs.readFileString(remap(root, file)),
|
||||
remove: (file) => fs.remove(remap(root, file)),
|
||||
glob: (pattern, options) =>
|
||||
fs.glob(pattern, options?.cwd ? { ...options, cwd: remap(root, options.cwd) } : options),
|
||||
})
|
||||
}),
|
||||
).pipe(Layer.provide(AppFileSystem.defaultLayer))
|
||||
}
|
||||
|
||||
// Layer.fresh forces a new Storage instance — without it, Effect's in-test layer cache
|
||||
// returns the outer testEffect's Storage (which uses the real AppFileSystem), not a new
|
||||
// one built on top of remappedFs.
|
||||
const remappedStorage = (root: string) =>
|
||||
Layer.fresh(Storage.layer.pipe(Layer.provide(remappedFs(root)), Layer.provide(Git.defaultLayer)))
|
||||
|
||||
describe("Storage", () => {
|
||||
it.live("round-trips JSON content", () =>
|
||||
Effect.gen(function* () {
|
||||
const { root, svc } = yield* scope()
|
||||
const key = [...root, "session_diff", "roundtrip"]
|
||||
const value = [{ file: "a.ts", additions: 2, deletions: 1 }]
|
||||
|
||||
yield* svc.write(key, value)
|
||||
expect(yield* svc.read<typeof value>(key)).toEqual(value)
|
||||
}),
|
||||
)
|
||||
|
||||
it.live("maps missing reads to NotFoundError", () =>
|
||||
Effect.gen(function* () {
|
||||
const { root, svc } = yield* scope()
|
||||
const exit = yield* svc.read([...root, "missing", "value"]).pipe(Effect.exit)
|
||||
expect(Exit.isFailure(exit)).toBe(true)
|
||||
}),
|
||||
)
|
||||
|
||||
it.live("update on missing key throws NotFoundError", () =>
|
||||
Effect.gen(function* () {
|
||||
const { root, svc } = yield* scope()
|
||||
const exit = yield* svc
|
||||
.update<{ value: number }>([...root, "missing", "key"], (draft) => {
|
||||
draft.value += 1
|
||||
})
|
||||
.pipe(Effect.exit)
|
||||
expect(Exit.isFailure(exit)).toBe(true)
|
||||
}),
|
||||
)
|
||||
|
||||
it.live("write overwrites existing value", () =>
|
||||
Effect.gen(function* () {
|
||||
const { root, svc } = yield* scope()
|
||||
const key = [...root, "overwrite", "test"]
|
||||
|
||||
yield* svc.write<{ v: number }>(key, { v: 1 })
|
||||
yield* svc.write<{ v: number }>(key, { v: 2 })
|
||||
|
||||
expect(yield* svc.read<{ v: number }>(key)).toEqual({ v: 2 })
|
||||
}),
|
||||
)
|
||||
|
||||
it.live("remove on missing key is a no-op", () =>
|
||||
Effect.gen(function* () {
|
||||
const { root, svc } = yield* scope()
|
||||
yield* svc.remove([...root, "nonexistent", "key"])
|
||||
}),
|
||||
)
|
||||
|
||||
it.live("list on missing prefix returns empty", () =>
|
||||
Effect.gen(function* () {
|
||||
const { root, svc } = yield* scope()
|
||||
expect(yield* svc.list([...root, "nonexistent"])).toEqual([])
|
||||
}),
|
||||
)
|
||||
|
||||
it.live("serializes concurrent updates for the same key", () =>
|
||||
Effect.gen(function* () {
|
||||
const { root, svc } = yield* scope()
|
||||
const key = [...root, "counter", "shared"]
|
||||
|
||||
yield* svc.write(key, { value: 0 })
|
||||
|
||||
yield* Effect.all(
|
||||
Array.from({ length: 25 }, () =>
|
||||
svc.update<{ value: number }>(key, (draft) => {
|
||||
draft.value += 1
|
||||
}),
|
||||
),
|
||||
{ concurrency: "unbounded" },
|
||||
)
|
||||
|
||||
expect(yield* svc.read<{ value: number }>(key)).toEqual({ value: 25 })
|
||||
}),
|
||||
)
|
||||
|
||||
it.live("concurrent reads do not block each other", () =>
|
||||
Effect.gen(function* () {
|
||||
const { root, svc } = yield* scope()
|
||||
const key = [...root, "concurrent", "reads"]
|
||||
|
||||
yield* svc.write(key, { ok: true })
|
||||
|
||||
const results = yield* Effect.all(
|
||||
Array.from({ length: 10 }, () => svc.read(key)),
|
||||
{ concurrency: "unbounded" },
|
||||
)
|
||||
|
||||
expect(results).toHaveLength(10)
|
||||
for (const r of results) expect(r).toEqual({ ok: true })
|
||||
}),
|
||||
)
|
||||
|
||||
it.live("nested keys create deep paths", () =>
|
||||
Effect.gen(function* () {
|
||||
const { root, svc } = yield* scope()
|
||||
const key = [...root, "a", "b", "c", "deep"]
|
||||
|
||||
yield* svc.write<{ nested: boolean }>(key, { nested: true })
|
||||
|
||||
expect(yield* svc.read<{ nested: boolean }>(key)).toEqual({ nested: true })
|
||||
expect(yield* svc.list([...root, "a"])).toEqual([key])
|
||||
}),
|
||||
)
|
||||
|
||||
it.live("lists and removes stored entries", () =>
|
||||
Effect.gen(function* () {
|
||||
const { root, svc } = yield* scope()
|
||||
const a = [...root, "list", "a"]
|
||||
const b = [...root, "list", "b"]
|
||||
const prefix = [...root, "list"]
|
||||
|
||||
yield* svc.write(b, { value: 2 })
|
||||
yield* svc.write(a, { value: 1 })
|
||||
|
||||
expect(yield* svc.list(prefix)).toEqual([a, b])
|
||||
|
||||
yield* svc.remove(a)
|
||||
|
||||
expect(yield* svc.list(prefix)).toEqual([b])
|
||||
const exit = yield* svc.read(a).pipe(Effect.exit)
|
||||
expect(Exit.isFailure(exit)).toBe(true)
|
||||
}),
|
||||
)
|
||||
|
||||
it.live("migration 2 runs when marker contents are invalid", () =>
|
||||
Effect.gen(function* () {
|
||||
const fs = yield* AppFileSystem.Service
|
||||
const tmp = yield* tmpdirScoped()
|
||||
const storage = path.join(tmp, "storage")
|
||||
const diffs = [
|
||||
{ additions: 2, deletions: 1 },
|
||||
{ additions: 3, deletions: 4 },
|
||||
]
|
||||
|
||||
yield* fs.writeWithDirs(path.join(storage, "migration"), "wat")
|
||||
yield* fs.writeWithDirs(
|
||||
path.join(storage, "session", "proj_test", "ses_test.json"),
|
||||
JSON.stringify({
|
||||
id: "ses_test",
|
||||
projectID: "proj_test",
|
||||
title: "legacy",
|
||||
summary: { diffs },
|
||||
}),
|
||||
)
|
||||
|
||||
yield* Effect.gen(function* () {
|
||||
const svc = yield* Storage.Service
|
||||
expect(yield* svc.list(["session_diff"])).toEqual([["session_diff", "ses_test"]])
|
||||
expect(yield* svc.read<typeof diffs>(["session_diff", "ses_test"])).toEqual(diffs)
|
||||
expect(
|
||||
yield* svc.read<{
|
||||
id: string
|
||||
projectID: string
|
||||
title: string
|
||||
summary: { additions: number; deletions: number }
|
||||
}>(["session", "proj_test", "ses_test"]),
|
||||
).toEqual({
|
||||
id: "ses_test",
|
||||
projectID: "proj_test",
|
||||
title: "legacy",
|
||||
summary: { additions: 5, deletions: 5 },
|
||||
})
|
||||
}).pipe(Effect.provide(remappedStorage(tmp)))
|
||||
|
||||
expect(yield* fs.readFileString(path.join(storage, "migration"))).toBe("2")
|
||||
}),
|
||||
)
|
||||
|
||||
it.live("migration 1 tolerates malformed legacy records", () =>
|
||||
Effect.gen(function* () {
|
||||
const fs = yield* AppFileSystem.Service
|
||||
const tmp = yield* tmpdirScoped({ git: true })
|
||||
const storage = path.join(tmp, "storage")
|
||||
const legacy = path.join(tmp, "project", "legacy")
|
||||
|
||||
yield* fs.writeWithDirs(path.join(legacy, "storage", "session", "message", "probe", "0.json"), "[]")
|
||||
yield* fs.writeWithDirs(
|
||||
path.join(legacy, "storage", "session", "message", "probe", "1.json"),
|
||||
JSON.stringify({ path: { root: tmp } }),
|
||||
)
|
||||
yield* fs.writeWithDirs(
|
||||
path.join(legacy, "storage", "session", "info", "ses_legacy.json"),
|
||||
JSON.stringify({ id: "ses_legacy", title: "legacy" }),
|
||||
)
|
||||
yield* fs.writeWithDirs(
|
||||
path.join(legacy, "storage", "session", "message", "ses_legacy", "msg_legacy.json"),
|
||||
JSON.stringify({ role: "user", text: "hello" }),
|
||||
)
|
||||
|
||||
yield* Effect.gen(function* () {
|
||||
const svc = yield* Storage.Service
|
||||
const projects = yield* svc.list(["project"])
|
||||
expect(projects).toHaveLength(1)
|
||||
const project = projects[0]![1]
|
||||
|
||||
expect(yield* svc.list(["session", project])).toEqual([["session", project, "ses_legacy"]])
|
||||
expect(yield* svc.read<{ id: string; title: string }>(["session", project, "ses_legacy"])).toEqual({
|
||||
id: "ses_legacy",
|
||||
title: "legacy",
|
||||
})
|
||||
expect(yield* svc.read<{ role: string; text: string }>(["message", "ses_legacy", "msg_legacy"])).toEqual({
|
||||
role: "user",
|
||||
text: "hello",
|
||||
})
|
||||
}).pipe(Effect.provide(remappedStorage(tmp)))
|
||||
|
||||
expect(yield* fs.readFileString(path.join(storage, "migration"))).toBe("2")
|
||||
}),
|
||||
)
|
||||
|
||||
it.live("failed migrations do not advance the marker", () =>
|
||||
Effect.gen(function* () {
|
||||
const fs = yield* AppFileSystem.Service
|
||||
const tmp = yield* tmpdirScoped()
|
||||
const storage = path.join(tmp, "storage")
|
||||
const legacy = path.join(tmp, "project", "legacy")
|
||||
|
||||
yield* fs.writeWithDirs(path.join(legacy, "storage", "session", "message", "probe", "0.json"), "{")
|
||||
|
||||
yield* Effect.gen(function* () {
|
||||
const svc = yield* Storage.Service
|
||||
expect(yield* svc.list(["project"])).toEqual([])
|
||||
}).pipe(Effect.provide(remappedStorage(tmp)))
|
||||
|
||||
const exit = yield* fs.access(path.join(storage, "migration")).pipe(Effect.exit)
|
||||
expect(Exit.isFailure(exit)).toBe(true)
|
||||
}),
|
||||
)
|
||||
})
|
||||
Reference in New Issue
Block a user