refactor(shared): consolidate 13+ getMessageDir copies into single shared function

This commit is contained in:
YeonGyu-Kim 2026-02-14 17:50:08 +09:00
parent e90734d6d9
commit c9c02e0525
21 changed files with 86 additions and 223 deletions

View File

@ -1 +1 @@
export { getMessageDir } from "./message-storage-locator" export { getMessageDir } from "../../shared"

View File

@ -1,17 +1 @@
import { existsSync, readdirSync } from "node:fs" import { getMessageDir } from "../../shared"
import { join } from "node:path"
import { MESSAGE_STORAGE } from "../hook-message-injector"
export function getMessageDir(sessionID: string): string | null {
if (!existsSync(MESSAGE_STORAGE)) return null
const directPath = join(MESSAGE_STORAGE, sessionID)
if (existsSync(directPath)) return directPath
for (const dir of readdirSync(MESSAGE_STORAGE)) {
const sessionPath = join(MESSAGE_STORAGE, dir, sessionID)
if (existsSync(sessionPath)) return sessionPath
}
return null
}

View File

@ -1,7 +1,7 @@
import type { OpencodeClient } from "./constants" import type { OpencodeClient } from "./constants"
import type { BackgroundTask } from "./types" import type { BackgroundTask } from "./types"
import { findNearestMessageWithFields } from "../hook-message-injector" import { findNearestMessageWithFields } from "../hook-message-injector"
import { getMessageDir } from "./message-storage-locator" import { getMessageDir } from "../../shared"
type AgentModel = { providerID: string; modelID: string } type AgentModel = { providerID: string; modelID: string }

View File

@ -1,6 +1,6 @@
export type { ResultHandlerContext } from "./result-handler-context" export type { ResultHandlerContext } from "./result-handler-context"
export { formatDuration } from "./duration-formatter" export { formatDuration } from "./duration-formatter"
export { getMessageDir } from "./message-storage-locator" export { getMessageDir } from "../../shared"
export { checkSessionTodos } from "./session-todo-checker" export { checkSessionTodos } from "./session-todo-checker"
export { validateSessionHasOutput } from "./session-output-validator" export { validateSessionHasOutput } from "./session-output-validator"
export { tryCompleteTask } from "./background-task-completer" export { tryCompleteTask } from "./background-task-completer"

View File

@ -1,3 +1,4 @@
import { existsSync, readdirSync } from "node:fs"
import { getMessageDir } from "../../shared/opencode-message-dir" import { getMessageDir } from "../../shared/opencode-message-dir"
export { getMessageDir } export { getMessageDir }

View File

@ -1,9 +1,9 @@
import { existsSync, readdirSync, readFileSync } from "node:fs" import { readdirSync, readFileSync } from "node:fs"
import { join } from "node:path" import { join } from "node:path"
import type { PruningState, ToolCallSignature } from "./pruning-types" import type { PruningState, ToolCallSignature } from "./pruning-types"
import { estimateTokens } from "./pruning-types" import { estimateTokens } from "./pruning-types"
import { log } from "../../shared/logger" import { log } from "../../shared/logger"
import { MESSAGE_STORAGE } from "../../features/hook-message-injector" import { getMessageDir } from "../../shared/opencode-message-dir"
export interface DeduplicationConfig { export interface DeduplicationConfig {
enabled: boolean enabled: boolean
@ -43,20 +43,6 @@ function sortObject(obj: unknown): unknown {
return sorted return sorted
} }
function getMessageDir(sessionID: string): string | null {
if (!existsSync(MESSAGE_STORAGE)) return null
const directPath = join(MESSAGE_STORAGE, sessionID)
if (existsSync(directPath)) return directPath
for (const dir of readdirSync(MESSAGE_STORAGE)) {
const sessionPath = join(MESSAGE_STORAGE, dir, sessionID)
if (existsSync(sessionPath)) return sessionPath
}
return null
}
function readMessages(sessionID: string): MessagePart[] { function readMessages(sessionID: string): MessagePart[] {
const messageDir = getMessageDir(sessionID) const messageDir = getMessageDir(sessionID)
if (!messageDir) return [] if (!messageDir) return []
@ -64,7 +50,7 @@ function readMessages(sessionID: string): MessagePart[] {
const messages: MessagePart[] = [] const messages: MessagePart[] = []
try { try {
const files = readdirSync(messageDir).filter(f => f.endsWith(".json")) const files = readdirSync(messageDir).filter((f: string) => f.endsWith(".json"))
for (const file of files) { for (const file of files) {
const content = readFileSync(join(messageDir, file), "utf-8") const content = readFileSync(join(messageDir, file), "utf-8")
const data = JSON.parse(content) const data = JSON.parse(content)

View File

@ -3,6 +3,7 @@ import { join } from "node:path"
import { getOpenCodeStorageDir } from "../../shared/data-path" import { getOpenCodeStorageDir } from "../../shared/data-path"
import { truncateToolResult } from "./storage" import { truncateToolResult } from "./storage"
import { log } from "../../shared/logger" import { log } from "../../shared/logger"
import { getMessageDir } from "../../shared/opencode-message-dir"
interface StoredToolPart { interface StoredToolPart {
type?: string type?: string
@ -21,21 +22,6 @@ function getPartStorage(): string {
return join(getOpenCodeStorageDir(), "part") return join(getOpenCodeStorageDir(), "part")
} }
function getMessageDir(sessionID: string): string | null {
const messageStorage = getMessageStorage()
if (!existsSync(messageStorage)) return null
const directPath = join(messageStorage, sessionID)
if (existsSync(directPath)) return directPath
for (const dir of readdirSync(messageStorage)) {
const sessionPath = join(messageStorage, dir, sessionID)
if (existsSync(sessionPath)) return sessionPath
}
return null
}
function getMessageIds(sessionID: string): string[] { function getMessageIds(sessionID: string): string[] {
const messageDir = getMessageDir(sessionID) const messageDir = getMessageDir(sessionID)
if (!messageDir) return [] if (!messageDir) return []

View File

@ -1,6 +1,6 @@
import type { PluginInput } from "@opencode-ai/plugin" import type { PluginInput } from "@opencode-ai/plugin"
import { findNearestMessageWithFields } from "../../features/hook-message-injector" import { findNearestMessageWithFields } from "../../features/hook-message-injector"
import { getMessageDir } from "../../shared/session-utils" import { getMessageDir } from "../../shared"
import type { ModelInfo } from "./types" import type { ModelInfo } from "./types"
export async function resolveRecentModelForSession( export async function resolveRecentModelForSession(

View File

@ -1,5 +1,5 @@
import { findNearestMessageWithFields } from "../../features/hook-message-injector" import { findNearestMessageWithFields } from "../../features/hook-message-injector"
import { getMessageDir } from "../../shared/session-utils" import { getMessageDir } from "../../shared"
export function getLastAgentFromSession(sessionID: string): string | null { export function getLastAgentFromSession(sessionID: string): string | null {
const messageDir = getMessageDir(sessionID) const messageDir = getMessageDir(sessionID)

View File

@ -1,22 +1,7 @@
import { existsSync, readdirSync } from "node:fs" import { findNearestMessageWithFields, findFirstMessageWithAgent } from "../../features/hook-message-injector"
import { join } from "node:path"
import { findNearestMessageWithFields, findFirstMessageWithAgent, MESSAGE_STORAGE } from "../../features/hook-message-injector"
import { getSessionAgent } from "../../features/claude-code-session-state" import { getSessionAgent } from "../../features/claude-code-session-state"
import { readBoulderState } from "../../features/boulder-state" import { readBoulderState } from "../../features/boulder-state"
import { getMessageDir } from "../../shared/opencode-message-dir"
function getMessageDir(sessionID: string): string | null {
if (!existsSync(MESSAGE_STORAGE)) return null
const directPath = join(MESSAGE_STORAGE, sessionID)
if (existsSync(directPath)) return directPath
for (const dir of readdirSync(MESSAGE_STORAGE)) {
const sessionPath = join(MESSAGE_STORAGE, dir, sessionID)
if (existsSync(sessionPath)) return sessionPath
}
return null
}
function getAgentFromMessageFiles(sessionID: string): string | undefined { function getAgentFromMessageFiles(sessionID: string): string | undefined {
const messageDir = getMessageDir(sessionID) const messageDir = getMessageDir(sessionID)

View File

@ -1,16 +1 @@
import { existsSync, readdirSync } from "node:fs" export { getMessageDir } from "../../shared/opencode-message-dir"
import { join } from "node:path"
import { MESSAGE_STORAGE } from "../../features/hook-message-injector"
export function getMessageDir(sessionID: string): string | null {
if (!existsSync(MESSAGE_STORAGE)) return null
const directPath = join(MESSAGE_STORAGE, sessionID)
if (existsSync(directPath)) return directPath
for (const dir of readdirSync(MESSAGE_STORAGE)) {
const sessionPath = join(MESSAGE_STORAGE, dir, sessionID)
if (existsSync(sessionPath)) return sessionPath
}
return null
}

View File

@ -1,18 +1 @@
import { existsSync, readdirSync } from "node:fs" export { getMessageDir } from "../../shared/opencode-message-dir"
import { join } from "node:path"
import { MESSAGE_STORAGE } from "../../features/hook-message-injector"
export function getMessageDir(sessionID: string): string | null {
if (!existsSync(MESSAGE_STORAGE)) return null
const directPath = join(MESSAGE_STORAGE, sessionID)
if (existsSync(directPath)) return directPath
for (const dir of readdirSync(MESSAGE_STORAGE)) {
const sessionPath = join(MESSAGE_STORAGE, dir, sessionID)
if (existsSync(sessionPath)) return sessionPath
}
return null
}

View File

@ -37,7 +37,7 @@ export { resolveModelPipeline } from "./model-resolution-pipeline"
export type { export type {
ModelResolutionRequest, ModelResolutionRequest,
ModelResolutionProvenance, ModelResolutionProvenance,
ModelResolutionPipelineResult, ModelResolutionResult,
} from "./model-resolution-types" } from "./model-resolution-types"
export * from "./model-availability" export * from "./model-availability"
export * from "./connected-providers-cache" export * from "./connected-providers-cache"

View File

@ -1,83 +1,95 @@
import { describe, it, expect, vi, beforeEach, afterEach } from "vitest" declare const require: (name: string) => any
import { existsSync, readdirSync } from "node:fs" const { describe, it, expect, beforeEach, afterEach, beforeAll, mock } = require("bun:test")
import { join } from "node:path"
import { getMessageDir } from "./opencode-message-dir"
// Mock the constants let getMessageDir: (sessionID: string) => string | null
vi.mock("../tools/session-manager/constants", () => ({
MESSAGE_STORAGE: "/mock/message/storage",
}))
vi.mock("node:fs", () => ({ beforeAll(async () => {
existsSync: vi.fn(), // Mock the data-path module
readdirSync: vi.fn(), mock.module("./data-path", () => ({
})) getOpenCodeStorageDir: () => "/mock/opencode/storage",
}))
vi.mock("node:path", () => ({ // Mock fs functions
join: vi.fn(), mock.module("node:fs", () => ({
})) existsSync: mock(() => false),
readdirSync: mock(() => []),
}))
const mockExistsSync = vi.mocked(existsSync) mock.module("node:path", () => ({
const mockReaddirSync = vi.mocked(readdirSync) join: mock((...args: string[]) => args.join("/")),
const mockJoin = vi.mocked(join) }))
;({ getMessageDir } = await import("./opencode-message-dir"))
})
describe("getMessageDir", () => { describe("getMessageDir", () => {
beforeEach(() => { beforeEach(() => {
vi.clearAllMocks() // Reset mocks
mockJoin.mockImplementation((...args) => args.join("/")) mock.restore()
}) })
afterEach(() => { it("returns null when sessionID does not start with ses_", () => {
vi.restoreAllMocks() // given
// no mocks needed
// when
const result = getMessageDir("invalid")
// then
expect(result).toBe(null)
}) })
it("returns null when MESSAGE_STORAGE does not exist", () => { it("returns null when MESSAGE_STORAGE does not exist", () => {
// given // given
mockExistsSync.mockReturnValue(false) mock.module("node:fs", () => ({
existsSync: mock(() => false),
readdirSync: mock(() => []),
}))
// when // when
const result = getMessageDir("session123") const result = getMessageDir("ses_123")
// then // then
expect(result).toBe(null) expect(result).toBe(null)
expect(mockExistsSync).toHaveBeenCalledWith("/mock/message/storage")
}) })
it("returns direct path when session exists directly", () => { it("returns direct path when session exists directly", () => {
// given // given
mockExistsSync.mockImplementation((path) => path === "/mock/message/storage" || path === "/mock/message/storage/session123") mock.module("node:fs", () => ({
existsSync: mock((path: string) => path === "/mock/opencode/storage/message" || path === "/mock/opencode/storage/message/ses_123"),
readdirSync: mock(() => []),
}))
// when // when
const result = getMessageDir("session123") const result = getMessageDir("ses_123")
// then // then
expect(result).toBe("/mock/message/storage/session123") expect(result).toBe("/mock/opencode/storage/message/ses_123")
expect(mockExistsSync).toHaveBeenCalledWith("/mock/message/storage")
expect(mockExistsSync).toHaveBeenCalledWith("/mock/message/storage/session123")
}) })
it("returns subdirectory path when session exists in subdirectory", () => { it("returns subdirectory path when session exists in subdirectory", () => {
// given // given
mockExistsSync.mockImplementation((path) => { mock.module("node:fs", () => ({
return path === "/mock/message/storage" || path === "/mock/message/storage/subdir/session123" existsSync: mock((path: string) => path === "/mock/opencode/storage/message" || path === "/mock/opencode/storage/message/subdir/ses_123"),
}) readdirSync: mock(() => ["subdir"]),
mockReaddirSync.mockReturnValue(["subdir"]) }))
// when // when
const result = getMessageDir("session123") const result = getMessageDir("ses_123")
// then // then
expect(result).toBe("/mock/message/storage/subdir/session123") expect(result).toBe("/mock/opencode/storage/message/subdir/ses_123")
expect(mockReaddirSync).toHaveBeenCalledWith("/mock/message/storage")
}) })
it("returns null when session not found anywhere", () => { it("returns null when session not found anywhere", () => {
// given // given
mockExistsSync.mockImplementation((path) => path === "/mock/message/storage") mock.module("node:fs", () => ({
mockReaddirSync.mockReturnValue(["subdir1", "subdir2"]) existsSync: mock((path: string) => path === "/mock/opencode/storage/message"),
readdirSync: mock(() => ["subdir1", "subdir2"]),
}))
// when // when
const result = getMessageDir("session123") const result = getMessageDir("ses_123")
// then // then
expect(result).toBe(null) expect(result).toBe(null)
@ -85,13 +97,15 @@ describe("getMessageDir", () => {
it("returns null when readdirSync throws", () => { it("returns null when readdirSync throws", () => {
// given // given
mockExistsSync.mockImplementation((path) => path === "/mock/message/storage") mock.module("node:fs", () => ({
mockReaddirSync.mockImplementation(() => { existsSync: mock((path: string) => path === "/mock/opencode/storage/message"),
throw new Error("Permission denied") readdirSync: mock(() => {
}) throw new Error("Permission denied")
}),
}))
// when // when
const result = getMessageDir("session123") const result = getMessageDir("ses_123")
// then // then
expect(result).toBe(null) expect(result).toBe(null)

View File

@ -1,8 +1,11 @@
import { existsSync, readdirSync } from "node:fs" import { existsSync, readdirSync } from "node:fs"
import { join } from "node:path" import { join } from "node:path"
import { MESSAGE_STORAGE } from "../tools/session-manager/constants" import { getOpenCodeStorageDir } from "./data-path"
const MESSAGE_STORAGE = join(getOpenCodeStorageDir(), "message")
export function getMessageDir(sessionID: string): string | null { export function getMessageDir(sessionID: string): string | null {
if (!sessionID.startsWith("ses_")) return null
if (!existsSync(MESSAGE_STORAGE)) return null if (!existsSync(MESSAGE_STORAGE)) return null
const directPath = join(MESSAGE_STORAGE, sessionID) const directPath = join(MESSAGE_STORAGE, sessionID)

View File

@ -1,17 +1 @@
import { existsSync, readdirSync } from "node:fs" export { getMessageDir } from "../../shared/opencode-message-dir"
import { join } from "node:path"
import { MESSAGE_STORAGE } from "../../features/hook-message-injector"
export function getMessageDir(sessionID: string): string | null {
if (!existsSync(MESSAGE_STORAGE)) return null
const directPath = join(MESSAGE_STORAGE, sessionID)
if (existsSync(directPath)) return directPath
for (const dir of readdirSync(MESSAGE_STORAGE)) {
const sessionPath = join(MESSAGE_STORAGE, dir, sessionID)
if (existsSync(sessionPath)) return sessionPath
}
return null
}

View File

@ -1,20 +1,6 @@
import { existsSync, readdirSync } from "node:fs" import { getMessageDir } from "../../../shared"
import { join } from "node:path"
import { MESSAGE_STORAGE } from "../../../features/hook-message-injector"
export function getMessageDir(sessionID: string): string | null { export { getMessageDir }
if (!existsSync(MESSAGE_STORAGE)) return null
const directPath = join(MESSAGE_STORAGE, sessionID)
if (existsSync(directPath)) return directPath
for (const dir of readdirSync(MESSAGE_STORAGE)) {
const sessionPath = join(MESSAGE_STORAGE, dir, sessionID)
if (existsSync(sessionPath)) return sessionPath
}
return null
}
export function formatDuration(start: Date, end?: Date): string { export function formatDuration(start: Date, end?: Date): string {
const duration = (end ?? new Date()).getTime() - start.getTime() const duration = (end ?? new Date()).getTime() - start.getTime()

View File

@ -1,18 +1 @@
import { existsSync, readdirSync } from "node:fs" export { getMessageDir } from "../../shared/opencode-message-dir"
import { join } from "node:path"
import { MESSAGE_STORAGE } from "../../features/hook-message-injector"
export function getMessageDir(sessionID: string): string | null {
if (!sessionID.startsWith("ses_")) return null
if (!existsSync(MESSAGE_STORAGE)) return null
const directPath = join(MESSAGE_STORAGE, sessionID)
if (existsSync(directPath)) return directPath
for (const dir of readdirSync(MESSAGE_STORAGE)) {
const sessionPath = join(MESSAGE_STORAGE, dir, sessionID)
if (existsSync(sessionPath)) return sessionPath
}
return null
}

View File

@ -1,18 +1 @@
import { existsSync, readdirSync } from "node:fs" export { getMessageDir } from "../../shared"
import { join } from "node:path"
import { MESSAGE_STORAGE } from "../../features/hook-message-injector"
export function getMessageDir(sessionID: string): string | null {
if (!sessionID.startsWith("ses_")) return null
if (!existsSync(MESSAGE_STORAGE)) return null
const directPath = join(MESSAGE_STORAGE, sessionID)
if (existsSync(directPath)) return directPath
for (const dir of readdirSync(MESSAGE_STORAGE)) {
const sessionPath = join(MESSAGE_STORAGE, dir, sessionID)
if (existsSync(sessionPath)) return sessionPath
}
return null
}

View File

@ -3,7 +3,7 @@ import type { ParentContext } from "./executor-types"
import { findNearestMessageWithFields, findFirstMessageWithAgent } from "../../features/hook-message-injector" import { findNearestMessageWithFields, findFirstMessageWithAgent } from "../../features/hook-message-injector"
import { getSessionAgent } from "../../features/claude-code-session-state" import { getSessionAgent } from "../../features/claude-code-session-state"
import { log } from "../../shared/logger" import { log } from "../../shared/logger"
import { getMessageDir } from "../../shared/session-utils" import { getMessageDir } from "../../shared"
export function resolveParentContext(ctx: ToolContextWithMetadata): ParentContext { export function resolveParentContext(ctx: ToolContextWithMetadata): ParentContext {
const messageDir = getMessageDir(ctx.sessionID) const messageDir = getMessageDir(ctx.sessionID)

View File

@ -4,7 +4,7 @@ import { isPlanFamily } from "./constants"
import { storeToolMetadata } from "../../features/tool-metadata-store" import { storeToolMetadata } from "../../features/tool-metadata-store"
import { getTaskToastManager } from "../../features/task-toast-manager" import { getTaskToastManager } from "../../features/task-toast-manager"
import { getAgentToolRestrictions } from "../../shared/agent-tool-restrictions" import { getAgentToolRestrictions } from "../../shared/agent-tool-restrictions"
import { getMessageDir } from "../../shared/session-utils" import { getMessageDir } from "../../shared"
import { promptWithModelSuggestionRetry } from "../../shared/model-suggestion-retry" import { promptWithModelSuggestionRetry } from "../../shared/model-suggestion-retry"
import { findNearestMessageWithFields } from "../../features/hook-message-injector" import { findNearestMessageWithFields } from "../../features/hook-message-injector"
import { formatDuration } from "./time-formatter" import { formatDuration } from "./time-formatter"