feat(tasks): migrate storage to global config dir with ULTRAWORK_TASK_LIST_ID support

This commit is contained in:
YeonGyu-Kim
2026-02-04 15:08:06 +09:00
parent 7b8204924a
commit bf31e7289e
2 changed files with 152 additions and 11 deletions

View File

@@ -1,26 +1,99 @@
import { describe, test, expect, beforeEach, afterEach } from "bun:test"
import { existsSync, mkdirSync, rmSync, writeFileSync } from "fs"
import { join } from "path"
import { join, basename } from "path"
import { z } from "zod"
import { getTaskDir, readJsonSafe, writeJsonAtomic, acquireLock, generateTaskId, listTaskFiles } from "./storage"
import { getOpenCodeConfigDir } from "../../shared/opencode-config-dir"
import {
getTaskDir,
readJsonSafe,
writeJsonAtomic,
acquireLock,
generateTaskId,
listTaskFiles,
resolveTaskListId,
sanitizePathSegment,
} from "./storage"
import type { OhMyOpenCodeConfig } from "../../config/schema"
const TEST_DIR = ".test-claude-tasks"
const TEST_DIR_ABS = join(process.cwd(), TEST_DIR)
describe("getTaskDir", () => {
test("returns correct path for default config", () => {
const originalTaskListId = process.env.ULTRAWORK_TASK_LIST_ID
beforeEach(() => {
if (originalTaskListId === undefined) {
delete process.env.ULTRAWORK_TASK_LIST_ID
} else {
process.env.ULTRAWORK_TASK_LIST_ID = originalTaskListId
}
})
afterEach(() => {
if (originalTaskListId === undefined) {
delete process.env.ULTRAWORK_TASK_LIST_ID
} else {
process.env.ULTRAWORK_TASK_LIST_ID = originalTaskListId
}
})
test("returns global config path for default config", () => {
//#given
const config: Partial<OhMyOpenCodeConfig> = {}
const configDir = getOpenCodeConfigDir({ binary: "opencode" })
const expectedListId = sanitizePathSegment(basename(process.cwd()))
//#when
const result = getTaskDir(config)
//#then
expect(result).toBe(join(process.cwd(), ".sisyphus/tasks"))
expect(result).toBe(join(configDir, "tasks", expectedListId))
})
test("returns correct path with custom storage_path", () => {
test("respects ULTRAWORK_TASK_LIST_ID env var", () => {
//#given
process.env.ULTRAWORK_TASK_LIST_ID = "custom list/id"
const configDir = getOpenCodeConfigDir({ binary: "opencode" })
//#when
const result = getTaskDir()
//#then
expect(result).toBe(join(configDir, "tasks", "custom-list-id"))
})
test("falls back to sanitized cwd basename when env var not set", () => {
//#given
delete process.env.ULTRAWORK_TASK_LIST_ID
const configDir = getOpenCodeConfigDir({ binary: "opencode" })
const expectedListId = sanitizePathSegment(basename(process.cwd()))
//#when
const result = getTaskDir()
//#then
expect(result).toBe(join(configDir, "tasks", expectedListId))
})
test("returns absolute storage_path without joining cwd", () => {
//#given
const config: Partial<OhMyOpenCodeConfig> = {
sisyphus: {
tasks: {
storage_path: "/tmp/custom-task-path",
claude_code_compat: false,
},
},
}
//#when
const result = getTaskDir(config)
//#then
expect(result).toBe("/tmp/custom-task-path")
})
test("joins relative storage_path with cwd", () => {
//#given
const config: Partial<OhMyOpenCodeConfig> = {
sisyphus: {
@@ -37,13 +110,59 @@ describe("getTaskDir", () => {
//#then
expect(result).toBe(join(process.cwd(), ".custom/tasks"))
})
})
describe("resolveTaskListId", () => {
const originalTaskListId = process.env.ULTRAWORK_TASK_LIST_ID
beforeEach(() => {
if (originalTaskListId === undefined) {
delete process.env.ULTRAWORK_TASK_LIST_ID
} else {
process.env.ULTRAWORK_TASK_LIST_ID = originalTaskListId
}
})
afterEach(() => {
if (originalTaskListId === undefined) {
delete process.env.ULTRAWORK_TASK_LIST_ID
} else {
process.env.ULTRAWORK_TASK_LIST_ID = originalTaskListId
}
})
test("returns env var when set", () => {
//#given
process.env.ULTRAWORK_TASK_LIST_ID = "custom-list"
test("returns correct path with default config parameter", () => {
//#when
const result = getTaskDir()
const result = resolveTaskListId()
//#then
expect(result).toBe(join(process.cwd(), ".sisyphus/tasks"))
expect(result).toBe("custom-list")
})
test("sanitizes special characters", () => {
//#given
process.env.ULTRAWORK_TASK_LIST_ID = "custom list/id"
//#when
const result = resolveTaskListId()
//#then
expect(result).toBe("custom-list-id")
})
test("returns sanitized cwd basename when env var not set", () => {
//#given
delete process.env.ULTRAWORK_TASK_LIST_ID
const expected = sanitizePathSegment(basename(process.cwd()))
//#when
const result = resolveTaskListId()
//#then
expect(result).toBe(expected)
})
})

View File

@@ -1,13 +1,35 @@
import { join, dirname } from "path"
import { join, dirname, basename } from "path"
import { existsSync, mkdirSync, readFileSync, writeFileSync, renameSync, unlinkSync, readdirSync } from "fs"
import { randomUUID } from "crypto"
import { getOpenCodeConfigDir } from "../../shared/opencode-config-dir"
import type { z } from "zod"
import type { OhMyOpenCodeConfig } from "../../config/schema"
export function getTaskDir(config: Partial<OhMyOpenCodeConfig> = {}): string {
const tasksConfig = config.sisyphus?.tasks
const storagePath = tasksConfig?.storage_path ?? ".sisyphus/tasks"
return join(process.cwd(), storagePath)
const storagePath = tasksConfig?.storage_path
if (storagePath) {
return storagePath.startsWith("/") ? storagePath : join(process.cwd(), storagePath)
}
const configDir = getOpenCodeConfigDir({ binary: "opencode" })
const listId = resolveTaskListId(config)
return join(configDir, "tasks", listId)
}
export function sanitizePathSegment(value: string): string {
return value.replace(/[^a-zA-Z0-9_-]/g, "-") || "default"
}
export function resolveTaskListId(config: Partial<OhMyOpenCodeConfig> = {}): string {
const envId = process.env.ULTRAWORK_TASK_LIST_ID?.trim()
if (envId) return sanitizePathSegment(envId)
const configId = config.sisyphus?.tasks?.task_list_id?.trim()
if (configId) return sanitizePathSegment(configId)
return sanitizePathSegment(basename(process.cwd()))
}
export function ensureDir(dirPath: string): void {