From 61ff3a3dff3e8d047edae750dc98d5accc983cc6 Mon Sep 17 00:00:00 2001 From: geyulong Date: Thu, 4 Jun 2026 18:13:27 +0800 Subject: [PATCH] fix: skip non-directory entries in listScopeIds and ensureScopeStorage --- dist/index.d.ts | 2 + dist/index.js | 22 ++++++----- src/index.ts | 24 ++++++------ tests/scope-listing.test.ts | 74 +++++++++++++++++++++++++++++++++++++ 4 files changed, 101 insertions(+), 21 deletions(-) create mode 100644 tests/scope-listing.test.ts diff --git a/dist/index.d.ts b/dist/index.d.ts index fc86752..827fed0 100644 --- a/dist/index.d.ts +++ b/dist/index.d.ts @@ -12,5 +12,7 @@ * - Environment variable injection (PATH for node/npx) */ import type { Plugin } from "@opencode-ai/plugin"; +/** @internal Exported for testing. */ +export declare function listScopeIds(root?: string): string[]; export declare const SchedulerPlugin: Plugin; export default SchedulerPlugin; diff --git a/dist/index.js b/dist/index.js index edf86d3..a5a24a8 100644 --- a/dist/index.js +++ b/dist/index.js @@ -12331,7 +12331,7 @@ function tool(input) { } tool.schema = exports_external; // src/index.ts -import { createWriteStream, existsSync, mkdirSync, readdirSync, readFileSync, rmSync, writeFileSync, unlinkSync } from "fs"; +import { createWriteStream, existsSync, mkdirSync, readdirSync, readFileSync, rmSync, statSync, writeFileSync, unlinkSync } from "fs"; import { basename, dirname, join, resolve as resolvePath } from "path"; import { homedir, platform } from "os"; import { execFileSync, execSync, spawn } from "child_process"; @@ -13517,6 +13517,13 @@ function uninstallJob(job) { function ensureScopeStorage(scopeId) { ensureDir(SCHEDULER_DIR); ensureDir(SCOPES_DIR); + const dir = scopeDir(scopeId); + try { + if (!existsSync(dir) || !statSync(dir).isDirectory()) + return; + } catch { + return; + } ensureDir(scopeJobsDir(scopeId)); ensureDir(scopeLocksDir(scopeId)); ensureDir(scopeRunsDir(scopeId)); @@ -13544,16 +13551,10 @@ function loadAllScopedJobs(scopeId) { } }).filter(Boolean); } -function listScopeIds() { - ensureDir(SCOPES_DIR); +function listScopeIds(root = SCOPES_DIR) { + ensureDir(root); try { - return readdirSync(SCOPES_DIR).filter((name) => { - try { - return existsSync(scopeDir(name)); - } catch { - return false; - } - }).sort(); + return readdirSync(root, { withFileTypes: true }).filter((entry) => entry.isDirectory()).map((entry) => entry.name).sort(); } catch { return []; } @@ -14968,6 +14969,7 @@ ${logs}`, { job, logPath, logs }); }; var src_default = SchedulerPlugin; export { + listScopeIds, src_default as default, SchedulerPlugin }; diff --git a/src/index.ts b/src/index.ts index f69c172..6045d0a 100644 --- a/src/index.ts +++ b/src/index.ts @@ -13,7 +13,7 @@ */ import type { Plugin } from "@opencode-ai/plugin" import { tool } from "@opencode-ai/plugin" -import { createWriteStream, existsSync, mkdirSync, readdirSync, readFileSync, rmSync, writeFileSync, unlinkSync } from "fs" +import { createWriteStream, existsSync, mkdirSync, readdirSync, readFileSync, rmSync, statSync, writeFileSync, unlinkSync } from "fs" import { basename, dirname, join, resolve as resolvePath } from "path" import { homedir, platform } from "os" import { execFileSync, execSync, spawn, type ChildProcess } from "child_process" @@ -1503,6 +1503,12 @@ function uninstallJob(job: Job): void { function ensureScopeStorage(scopeId: string): void { ensureDir(SCHEDULER_DIR) ensureDir(SCOPES_DIR) + const dir = scopeDir(scopeId) + try { + if (!existsSync(dir) || !statSync(dir).isDirectory()) return + } catch { + return + } ensureDir(scopeJobsDir(scopeId)) ensureDir(scopeLocksDir(scopeId)) ensureDir(scopeRunsDir(scopeId)) @@ -1534,17 +1540,13 @@ function loadAllScopedJobs(scopeId: string): Job[] { .filter(Boolean) as Job[] } -function listScopeIds(): string[] { - ensureDir(SCOPES_DIR) +/** @internal Exported for testing. */ +export function listScopeIds(root: string = SCOPES_DIR): string[] { + ensureDir(root) try { - return readdirSync(SCOPES_DIR) - .filter((name) => { - try { - return existsSync(scopeDir(name)) - } catch { - return false - } - }) + return readdirSync(root, { withFileTypes: true }) + .filter((entry) => entry.isDirectory()) + .map((entry) => entry.name) .sort() } catch { return [] diff --git a/tests/scope-listing.test.ts b/tests/scope-listing.test.ts new file mode 100644 index 0000000..801f8f9 --- /dev/null +++ b/tests/scope-listing.test.ts @@ -0,0 +1,74 @@ +import { describe, expect, test, beforeEach, afterEach } from "bun:test" +import { mkdtempSync, mkdirSync, writeFileSync, rmSync } from "fs" +import { tmpdir } from "os" +import { join } from "path" + +import { listScopeIds } from "../src/index" + +let root: string + +beforeEach(() => { + root = mkdtempSync(join(tmpdir(), "scheduler-scope-listing-")) +}) + +afterEach(() => { + rmSync(root, { recursive: true, force: true }) +}) + +describe("listScopeIds", () => { + test("returns empty array when the scopes root does not exist", () => { + const missing = join(root, "does-not-exist") + expect(listScopeIds(missing)).toEqual([]) + }) + + test("returns empty array when the scopes root is empty", () => { + expect(listScopeIds(root)).toEqual([]) + }) + + test("returns names of real scope subdirectories sorted ascending", () => { + mkdirSync(join(root, "beta-scope", "jobs"), { recursive: true }) + mkdirSync(join(root, "alpha-scope", "jobs"), { recursive: true }) + + const result = listScopeIds(root) + + expect(result).toEqual(["alpha-scope", "beta-scope"]) + }) + + test("skips macOS Finder metadata files like .DS_Store", () => { + mkdirSync(join(root, "real-scope", "jobs"), { recursive: true }) + writeFileSync(join(root, ".DS_Store"), Buffer.from([0x00, 0x01, 0x02, 0x03, 0x04])) + + const result = listScopeIds(root) + + expect(result).toEqual(["real-scope"]) + }) + + test("skips plain files that share the scopes root with directories", () => { + mkdirSync(join(root, "real-scope", "jobs"), { recursive: true }) + writeFileSync(join(root, "foo.txt"), "noise") + writeFileSync(join(root, "leftover.json"), "{}") + + const result = listScopeIds(root) + + expect(result).toEqual(["real-scope"]) + }) + + test("mixed real scope, .DS_Store, and stray file returns only the real scope", () => { + mkdirSync(join(root, "real-scope", "jobs"), { recursive: true }) + mkdirSync(join(root, "another-real", "jobs"), { recursive: true }) + writeFileSync(join(root, ".DS_Store"), "mac noise") + writeFileSync(join(root, "stray.txt"), "stray") + + const result = listScopeIds(root) + + expect(result).toEqual(["another-real", "real-scope"]) + }) + + test("does not throw when a non-directory entry is encountered", () => { + mkdirSync(join(root, "real-scope", "jobs"), { recursive: true }) + writeFileSync(join(root, ".DS_Store"), "mac noise") + writeFileSync(join(root, "stray.txt"), "stray") + + expect(() => listScopeIds(root)).not.toThrow() + }) +})