Skip to content

fix(desktop): normalize source slug validation errors#5911

Merged
wenshao merged 5 commits into
QwenLM:mainfrom
VectorPeak:codex/source-slug-validation-errors
Jun 28, 2026
Merged

fix(desktop): normalize source slug validation errors#5911
wenshao merged 5 commits into
QwenLM:mainfrom
VectorPeak:codex/source-slug-validation-errors

Conversation

@VectorPeak

@VectorPeak VectorPeak commented Jun 27, 2026

Copy link
Copy Markdown
Contributor

This PR keeps the stricter desktop source-slug path-traversal guard intact and normalizes the remaining invalid/legacy-slug paths so callers get predictable validation output instead of uncaught generic failures.

What this PR does

  • Makes config_validate return a structured validation result for invalid source slug input such as { target: "sources", sourceSlug: "../sessions" }, instead of letting getSourceConfigPath() throw through the fallback handler.
  • Makes source_test use the requested source slug for guide.md lookup, so a valid directory like sources/my-source/ still checks the right guide path even if config.json contains a legacy-formatted slug such as my--source.
  • Makes validateAllSources skip legacy invalid source directories such as sources/legacy-source-/config.json with a warning, matching the existing validateAllPermissions behavior.
  • Maps invalid source delete slugs such as ../sessions to the RPC INVALID_ARGUMENT code instead of the generic handler error code.
  • Adds focused regression coverage for the validation fallback, source-test completeness check, full-source validation, and source-delete RPC paths.

Why it's needed

This is a follow-up to #5908 and #5829. #5829 fixed the core source deletion path traversal issue by rejecting path-like slugs before they can be resolved as filesystem paths. In practical terms, inputs such as ../sessions or ..\\sessions should never be allowed to turn a source path into sources/../sessions or another neighboring workspace directory.

After that stricter guard, a few unchanged desktop paths still handled invalid or legacy slugs inconsistently. The fallback config_validate path could throw instead of formatting a validation result. source_test could load a source from a valid directory, then use the slug field from config.json for a later guide-path lookup. validateAllSources treated legacy invalid-slug directories as hard validation errors, while validateAllPermissions already skipped the same kind of directory with a warning. Source deletion correctly rejected invalid slugs, but the RPC boundary surfaced that caller mistake as a generic handler failure.

That made the guard technically correct but operationally noisy: users and maintainers would see different behavior depending on which validation or RPC entry point touched the same bad slug. This PR keeps the strict validation in place and makes each affected path behave according to its API boundary: validation tools return validation results, full validation skips legacy invalid folders with warnings, and RPC delete reports bad caller input as INVALID_ARGUMENT.

Possible call chain / impact

The affected paths are desktop source validation, source testing, all-source validation, and source deletion RPC handling:

Session tool call: config_validate({ target: "sources", sourceSlug })
  -> handleConfigValidate(...)
    -> getSourceConfigPath(ctx.workspacePath, sourceSlug)
      -> getSourcePath(...)
        -> assertValidSourceSlug(...)

Before this PR, an invalid slug such as ../sessions could throw through the fallback handler and be reported as a generic tool failure. This PR keeps the strict slug check but returns a formatted validation result for that validation entry point.

Session tool call: source_test({ sourceSlug: "my-source" })
  -> handleSourceTest(...)
    -> sourceExists(ctx.workspacePath, sourceSlug)
    -> ctx.loadSourceConfig(sourceSlug)
    -> checkCompleteness(...)
      -> getSourceGuidePath(ctx.workspacePath, sourceSlug)

Before this PR, the final guide-path lookup used source.slug from config.json. A valid folder such as sources/my-source/ with a legacy config slug like my--source could pass the earlier load path but fail later when the stricter path helper rejected the config slug. This PR uses the already requested source slug for filesystem lookup.

Workspace validation: validateAllSources(workspaceRoot)
  -> readdir(workspaceRoot/sources)
    -> assertValidSourceSlug(folder)
    -> validateSource(workspaceRoot, folder)

Legacy invalid source directories are now skipped with a warning during all-source validation, matching the existing validateAllPermissions behavior. Direct validation of an explicitly requested invalid slug still reports an error.

Source delete RPC request
  -> RPC sources:delete
    -> registerSourcesHandlers DELETE handler
      -> deleteSource(workspace.rootPath, sourceSlug)
        -> getSourcePath(...)
          -> assertValidSourceSlug(...)

Invalid delete slugs such as ../sessions are still rejected before deletion. The difference is that the RPC response now classifies that caller-input failure as INVALID_ARGUMENT instead of the generic handler error code.

This PR only changes invalid/legacy source slug handling at these API boundaries. It does not relax the source slug regex, change valid source deletion behavior, change source creation, alter source config parsing, change guide content parsing, modify connection tests, or cover the broader hardening/diagnostics work tracked in #5909.

Reviewer Test Plan

How to verify

Confirm that:

  • config_validate with sourceSlug: "../sessions" returns Validation failed with a sourceSlug validation error.
  • source_test for sourceSlug: "my-source" still checks sources/my-source/guide.md when the source config contains a legacy slug like my--source.
  • validateAllSources skips sources/legacy-source-/config.json with an invalid-slug warning.
  • Source delete RPC reports invalid slug input such as ../sessions as INVALID_ARGUMENT.

Commands run:

  • cd packages/desktop && bun test packages/session-tools-core/src/handlers/config-validate.test.ts packages/session-tools-core/src/handlers/source-test.test.ts packages/shared/src/config/__tests__/source-slug-validation.test.ts packages/server-core/src/handlers/rpc/sources.test.ts -> 16 pass, 0 fail
  • cd packages/desktop/packages/session-tools-core && bun run tsc --noEmit -> pass
  • cd packages/desktop/packages/shared && bun run tsc --noEmit -> pass
  • cd packages/desktop/packages/server-core && bun run tsc --noEmit -> pass
  • JD-Cloud Linux (Ubuntu 24.04.2 LTS, Linux 6.8.0-53-generic, Node v22.23.1, Bun 1.3.14): cd packages/desktop && ELECTRON_SKIP_BINARY_DOWNLOAD=1 bun install -> pass
  • JD-Cloud Linux: cd packages/desktop && bun test packages/session-tools-core/src/handlers/config-validate.test.ts packages/session-tools-core/src/handlers/source-test.test.ts packages/shared/src/config/__tests__/source-slug-validation.test.ts packages/server-core/src/handlers/rpc/sources.test.ts -> 16 pass, 0 fail
  • JD-Cloud Linux: cd packages/desktop/packages/session-tools-core && bun run tsc --noEmit -> pass
  • JD-Cloud Linux: cd packages/desktop/packages/shared && bun run tsc --noEmit -> pass
  • JD-Cloud Linux: cd packages/desktop/packages/server-core && bun run tsc --noEmit -> pass
  • cd packages/desktop && bun run typecheck:all -> fails in pre-existing Electron/UI type errors unrelated to this PR (apps/electron/src/main/auto-update.ts, apps/electron/src/main/handlers/__tests__/settings-default-thinking.test.ts, apps/electron/src/renderer/lib/__tests__/*, apps/electron/src/renderer/pages/settings/MemorySettingsPage.tsx); full desktop preflight is therefore not claimed as passing in this checkout

Evidence (Before & After)

No UI evidence; this is non-UI behavior covered by focused regression tests.

Tested on

OS Status
🍏 macOS ⚠️ not tested
🪟 Windows ✅ tested
🐧 Linux ✅ tested on JD-Cloud

Environment (optional)

Windows PowerShell, Bun 1.3.12, Node v22.22.3. JD-Cloud Ubuntu 24.04.2 LTS (Linux 6.8.0-53-generic), Bun 1.3.14, Node v22.23.1.

Risk & Scope

  • Main risk or tradeoff: source delete RPC now uses the new INVALID_ARGUMENT protocol error code for invalid slug input; other handler failures continue to use the existing generic error behavior.
  • Not validated / out of scope: the broader slug/name-to-path hardening and diagnostics items tracked in Harden remaining slug-to-path call sites and invalid slug diagnostics #5909.
  • Breaking changes / migration notes: None expected; strict slug validation is not relaxed.

Linked Issues

Closes #5908

中文说明

本 PR 保留桌面端更严格的 source slug 路径穿越防护,同时统一剩余 invalid / legacy slug 路径的行为,让调用方拿到可预测的验证输出或输入错误,而不是未捕获的泛化 handler 失败。

What this PR does

  • config_validate 在收到 { target: "sources", sourceSlug: "../sessions" } 这类无效 source slug 输入时返回结构化验证结果,而不是让 getSourceConfigPath() 的异常穿透 fallback handler。
  • source_test 使用请求中的 source slug 查找 guide.md,因此即使 config.json 中仍有 my--source 这类旧格式 slug,像 sources/my-source/ 这样的有效目录仍会检查正确的 guide 路径。
  • validateAllSourcessources/legacy-source-/config.json 这类旧版无效 source 目录发出 warning 并跳过,与已有的 validateAllPermissions 行为保持一致。
  • 让 source delete RPC 对 ../sessions 这类无效 slug 返回 INVALID_ARGUMENT 错误码,而不是泛化的 handler error code。
  • 为 validation fallback、source-test completeness check、full-source validation 和 source-delete RPC 路径添加聚焦回归测试。

Why it's needed

这是 #5908#5829 的后续修复。#5829 已经通过在 source slug 被解析成文件系统路径前拒绝 path-like slug,修复了 source 删除路径的核心 traversal 风险。也就是说,../sessions..\\sessions 这类输入不应被允许把 source 路径解析成 sources/../sessions 或其他相邻 workspace 目录。

在这个更严格的 guard 之后,少数未改动的桌面端路径仍然对 invalid / legacy slug 表现不一致。fallback config_validate 路径可能直接抛异常,而不是格式化验证结果。source_test 可能先从有效目录加载 source,然后在后续 guide 路径查找时使用 config.json 里的 slug 字段。validateAllSources 会把旧版无效 slug 目录当作硬错误,但 validateAllPermissions 已经会对同类目录 warning + skip。source deletion 本身会正确拒绝无效 slug,但 RPC 边界会把这个调用方输入错误暴露成泛化 handler failure。

这会让 guard 在安全上正确、但在操作上嘈杂:同一个坏 slug 会因为经过不同验证或 RPC 入口而表现不同。本 PR 保留严格验证,并让每个受影响路径按自己的 API 边界返回:验证工具返回验证结果,全量验证对旧版无效目录 warning + skip,RPC delete 把调用方坏输入报告为 INVALID_ARGUMENT

Possible call chain / impact

受影响路径是桌面端 source validation、source testing、all-source validation 以及 source deletion RPC handling:

Session tool call: config_validate({ target: "sources", sourceSlug })
  -> handleConfigValidate(...)
    -> getSourceConfigPath(ctx.workspacePath, sourceSlug)
      -> getSourcePath(...)
        -> assertValidSourceSlug(...)

在本 PR 之前,../sessions 这类无效 slug 可能从 fallback handler 直接抛出,并表现为泛化 tool failure。本 PR 保留严格 slug 检查,但在 validation 入口返回格式化验证结果。

Session tool call: source_test({ sourceSlug: "my-source" })
  -> handleSourceTest(...)
    -> sourceExists(ctx.workspacePath, sourceSlug)
    -> ctx.loadSourceConfig(sourceSlug)
    -> checkCompleteness(...)
      -> getSourceGuidePath(ctx.workspacePath, sourceSlug)

在本 PR 之前,最后的 guide-path lookup 使用的是 config.json 中的 source.slug。如果有效目录是 sources/my-source/,但 config 里仍有 my--source 这类旧格式 slug,前面的加载路径可以通过,后续更严格的 path helper 却会拒绝 config slug。本 PR 改为使用请求中的 source slug 做文件系统路径查找。

Workspace validation: validateAllSources(workspaceRoot)
  -> readdir(workspaceRoot/sources)
    -> assertValidSourceSlug(folder)
    -> validateSource(workspaceRoot, folder)

旧版无效 source 目录现在会在 all-source validation 中 warning + skip,与已有的 validateAllPermissions 行为保持一致。直接验证一个显式传入的无效 slug 仍然会返回 error。

Source delete RPC request
  -> RPC sources:delete
    -> registerSourcesHandlers DELETE handler
      -> deleteSource(workspace.rootPath, sourceSlug)
        -> getSourcePath(...)
          -> assertValidSourceSlug(...)

../sessions 这类无效 delete slug 仍然会在删除前被拒绝。区别是 RPC response 现在会把这个调用方输入错误标记为 INVALID_ARGUMENT,而不是泛化 handler error code。

本 PR 只改变这些 API 边界上的 invalid / legacy source slug 处理。它不会放宽 source slug regex,不会改变有效 source 的删除行为,不会改变 source 创建,不会改变 source config parsing,不会改变 guide 内容解析,不会修改 connection tests,也不覆盖 #5909 中跟踪的更广泛 hardening / diagnostics 工作。

Reviewer Test Plan

How to verify

确认:

  • config_validate 携带 sourceSlug: "../sessions" 时返回包含 sourceSlug 错误的 Validation failed
  • source_test 携带 sourceSlug: "my-source" 时,即使 source config 里包含 my--source 这样的旧格式 slug,也仍然检查 sources/my-source/guide.md
  • validateAllSourcessources/legacy-source-/config.json 发出 invalid-slug warning 并跳过。
  • Source delete RPC 对 ../sessions 这类无效 slug 返回 INVALID_ARGUMENT

已运行命令:

  • cd packages/desktop && bun test packages/session-tools-core/src/handlers/config-validate.test.ts packages/session-tools-core/src/handlers/source-test.test.ts packages/shared/src/config/__tests__/source-slug-validation.test.ts packages/server-core/src/handlers/rpc/sources.test.ts -> 16 pass, 0 fail
  • cd packages/desktop/packages/session-tools-core && bun run tsc --noEmit -> pass
  • cd packages/desktop/packages/shared && bun run tsc --noEmit -> pass
  • cd packages/desktop/packages/server-core && bun run tsc --noEmit -> pass
  • JD-Cloud Linux(Ubuntu 24.04.2 LTSLinux 6.8.0-53-genericNode v22.23.1Bun 1.3.14):cd packages/desktop && ELECTRON_SKIP_BINARY_DOWNLOAD=1 bun install -> pass
  • JD-Cloud Linux:cd packages/desktop && bun test packages/session-tools-core/src/handlers/config-validate.test.ts packages/session-tools-core/src/handlers/source-test.test.ts packages/shared/src/config/__tests__/source-slug-validation.test.ts packages/server-core/src/handlers/rpc/sources.test.ts -> 16 pass, 0 fail
  • JD-Cloud Linux:cd packages/desktop/packages/session-tools-core && bun run tsc --noEmit -> pass
  • JD-Cloud Linux:cd packages/desktop/packages/shared && bun run tsc --noEmit -> pass
  • JD-Cloud Linux:cd packages/desktop/packages/server-core && bun run tsc --noEmit -> pass
  • cd packages/desktop && bun run typecheck:all -> 因既有 Electron/UI 类型错误失败,错误位置与本 PR 无关:apps/electron/src/main/auto-update.tsapps/electron/src/main/handlers/__tests__/settings-default-thinking.test.tsapps/electron/src/renderer/lib/__tests__/*apps/electron/src/renderer/pages/settings/MemorySettingsPage.tsx;因此本 checkout 不声称 full desktop preflight 已通过

Evidence (Before & After)

无 UI 证据;这是非 UI 行为,已由聚焦回归测试覆盖。

Tested on

OS Status
🍏 macOS ⚠️ not tested
🪟 Windows ✅ tested
🐧 Linux ✅ tested on JD-Cloud

Environment (optional)

Windows PowerShell, Bun 1.3.12, Node v22.22.3. JD-Cloud Ubuntu 24.04.2 LTS(Linux 6.8.0-53-generic)、Bun 1.3.14、Node v22.23.1。

Risk & Scope

  • Main risk or tradeoff: source delete RPC 现在会对无效 slug 输入使用新的 INVALID_ARGUMENT protocol error code;其他 handler 失败仍保持原有泛化错误行为。
  • Not validated / out of scope: Harden remaining slug-to-path call sites and invalid slug diagnostics #5909 跟踪的更广泛 slug/name-to-path 加固和诊断改进不在本 PR 范围内。
  • Breaking changes / migration notes: 预期没有破坏性变更;严格 slug validation 没有被放宽。

Linked Issues

Closes #5908

@wenshao

wenshao commented Jun 27, 2026

Copy link
Copy Markdown
Collaborator

@qwen-code /triage

@qwen-code-ci-bot

qwen-code-ci-bot commented Jun 27, 2026

Copy link
Copy Markdown
Collaborator

Thanks for the PR!

Template looks good ✓

On direction: This is a natural follow-up to #5829 (path traversal fix). After the stricter slug guard landed, a few adjacent code paths still handled invalid/legacy slugs inconsistently — some threw generic errors, some used config slugs for filesystem lookups, some treated legacy dirs as hard errors. Normalizing these at the API boundary is the right call. Aligned with the security hardening direction.

On approach: Scope is tight and focused — four specific code paths, each with a targeted fix and matching regression test. No drive-by refactors, no scope creep. The changes follow the existing skip-and-warn pattern from validateAllPermissions exactly, which is the right instinct. Moving on to code review. 🔍

中文说明

感谢贡献!

模板完整 ✓

方向:这是 #5829(路径穿越修复)的自然后续。更严格的 slug guard 落地后,相邻的几个代码路径对 invalid/legacy slug 的处理仍然不一致——有的抛泛化错误,有的用 config slug 做文件系统查找,有的把旧目录当硬错误。在 API 边界统一这些行为是正确的方向,与安全加固一致。

方案:范围紧凑、聚焦——四个具体代码路径,每个都有针对性修复和对应的回归测试。没有顺手重构,没有范围蔓延。改动完全复用了 validateAllPermissions 已有的 skip-and-warn 模式,这是正确的做法。进入代码审查 🔍

Qwen Code · qwen3.7-max

@qwen-code-ci-bot

qwen-code-ci-bot commented Jun 27, 2026

Copy link
Copy Markdown
Collaborator

Code Review

No critical blockers. The diff is clean and well-structured.

config-validate.ts — Pre-validates sourceSlug with assertValidSourceSlug before calling getSourceConfigPath, returning a structured validation result instead of letting the exception propagate through the fallback handler. The all-sources branch skips invalid slug directories with a warning, exactly mirroring the existing validateAllPermissions pattern (same try/catch shape, same warning message format). ✓

source-test.tscheckCompleteness now takes sourceSlug (the requested slug) instead of sourcePath, and uses it for the guide.md lookup via getSourceGuidePath. This fixes the case where a valid directory sources/my-source/ has a legacy config slug my--source that would fail the stricter path helper. Only one caller of checkCompleteness exists, so the parameter rename is safe. ✓

validators.tsvalidateAllSources adds the same skip-and-warn pattern for legacy invalid slug directories. Consistent with validateAllPermissions. ✓

sources.ts (RPC delete) — Wraps deleteSource in try/catch, tags invalid-slug errors with INVALID_ARGUMENT code. The isInvalidSourceSlugError helper is a simple message-prefix check — adequate for the narrow use case. ✓

types.ts — Adds INVALID_ARGUMENT to the ErrorCode union. Backwards-compatible addition. ✓

Tests — 16 focused regression tests covering all four changed paths: validation fallback, source-test completeness with legacy slugs, all-source validation skip behavior, and RPC delete error classification. Tests are well-scoped and don't over-mock.

Real-Scenario Testing

This PR modifies desktop package internals (source slug validation at API boundaries). There is no CLI-level observable behavior change — the affected paths are config_validate, source_test, validateAllSources, and the source delete RPC handler, all of which are desktop-only backend logic.

Bun is not available in this CI environment, so the desktop package tests (bun test) could not be re-run independently. The PR author reports 16 pass / 0 fail on both Windows and Linux (JD-Cloud Ubuntu).

Tmux real-scenario testing: N/A — no TUI output is affected by these changes.

中文说明

代码审查

无关键阻塞项。Diff 干净且结构良好。

config-validate.ts — 在调用 getSourceConfigPath 前用 assertValidSourceSlug 预验证 sourceSlug,返回结构化验证结果而不是让异常穿透 fallback handler。全量 sources 分支对无效 slug 目录 warning + skip,完全复用了 validateAllPermissions 已有的模式(相同的 try/catch 结构、相同的 warning 消息格式)。✓

source-test.tscheckCompleteness 现在接收 sourceSlug(请求中的 slug)而非 sourcePath,用它来查找 guide.md。修复了有效目录 sources/my-source/ 但 config 中有旧格式 slug my--source 时,后续更严格 path helper 拒绝 config slug 的问题。checkCompleteness 只有一个调用方,参数重命名安全。✓

validators.tsvalidateAllSources 对旧版无效 slug 目录添加了相同的 skip-and-warn 模式,与 validateAllPermissions 一致。✓

sources.ts(RPC delete) — 用 try/catch 包裹 deleteSource,对 invalid-slug 错误标记 INVALID_ARGUMENT code。isInvalidSourceSlugError 辅助函数用简单的消息前缀检查——对这个窄场景足够。✓

types.ts — 向 ErrorCode 联合类型添加 INVALID_ARGUMENT,向后兼容的添加。✓

测试 — 16 个聚焦回归测试覆盖所有四个修改路径:validation fallback、legacy slug 下的 source-test completeness、全量 validation 跳过行为、RPC delete 错误分类。测试范围合理,没有过度 mock。

真实场景测试

本 PR 修改桌面端内部包(API 边界的 source slug 验证)。没有 CLI 层面可观察的行为变化——受影响路径是 config_validatesource_testvalidateAllSources 和 source delete RPC handler,全部是桌面端后端逻辑。

当前 CI 环境没有 Bun,无法独立重跑桌面端测试(bun test)。PR 作者报告在 Windows 和 Linux(JD-Cloud Ubuntu)上 16 pass / 0 fail。

Tmux 真实场景测试:N/A — 这些修改不影响 TUI 输出。

Qwen Code · qwen3.7-max

@qwen-code-ci-bot

qwen-code-ci-bot commented Jun 27, 2026

Copy link
Copy Markdown
Collaborator

This PR does exactly what it says: takes the strict slug guard from #5829 and makes every adjacent code path behave consistently at its own API boundary. Validation tools return validation results, full-validation skips legacy dirs with warnings, and RPC delete surfaces bad input as INVALID_ARGUMENT. No scope creep, no unnecessary abstractions — each change is a small, targeted patch that mirrors an existing pattern.

The diff matches what I would have proposed independently. The test coverage is focused and adequate. The only minor caveat is that I couldn't re-run the bun tests in this environment, but the changes are straightforward enough that the test descriptions and code review give me confidence.

LGTM, looks ready to ship. ✅

中文说明

本 PR 完全做到了它所描述的事情:把 #5829 的严格 slug guard 扩展到每个相邻代码路径,让它们在各自的 API 边界行为一致。验证工具返回验证结果,全量验证对旧目录 warning + skip,RPC delete 把坏输入标记为 INVALID_ARGUMENT。没有范围蔓延,没有不必要的抽象——每个修改都是小型、有针对性的补丁,复用了已有模式。

Diff 与我独立提出的方案一致。测试覆盖聚焦且充分。唯一的小限制是当前环境无法重跑 bun 测试,但改动足够直观,测试描述和代码审查给了我信心。

LGTM,可以合并。✅

Qwen Code · qwen3.7-max

@qwen-code-ci-bot qwen-code-ci-bot left a comment

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

LGTM, looks ready to ship. ✅

@wenshao

wenshao commented Jun 27, 2026

Copy link
Copy Markdown
Collaborator

@qwen-code /resolve

@qwen-code-dev-bot

Copy link
Copy Markdown
Collaborator

Qwen Code resolved the merge conflicts and pushed the branch update.

Merge Conflict Resolution Summary

PR

Conflicted File

packages/desktop/packages/server-core/src/handlers/rpc/sources.test.ts (add/add)

Both the PR branch and main added this test file independently with different scopes:

Side What it tested
HEAD (PR) DELETE handler rejects invalid slugs with INVALID_ARGUMENT error code
origin/main GET_PERMISSIONS handler logs invalid slugs as warnings, not errors

Resolution

Combined both test suites into a single file with a shared createHandlers() helper that returns { handlers, warnings, errors }. This unified helper captures logger output so both test suites can assert on it.

Key decisions:

  • Used node: prefixed imports (node:fs, node:os, node:path) from main's convention
  • Used await import(...) for the module under test from main (needed for mock.module to take effect before evaluation)
  • Used a mutable workspaceRoot variable so each test can set up its own temp directory
  • Used main's process: async (buffer: Buffer) => buffer signature (matches current source code)
  • Used main's warn/error capture pattern (needed for GET_PERMISSIONS assertions)
  • Preserved HEAD's beforeEach(() => mockGetWorkspaceByNameOrId.mockClear()) and DELETE test intact
  • Added the GET_PERMISSIONS test suite from main as a second describe block

No other files were modified — only the one conflicted file was touched.

Commit

42f794e7ffix(desktop): normalize source slug validation errors

@VectorPeak

Copy link
Copy Markdown
Contributor Author

@wenshao quick update after the merge-conflict resolution on this PR.

I re-checked the current head 42f794e7ff73cca78fcc86cb067fab58c81e70d0 and reran the focused validation on both Windows and JD-Cloud Linux.

Results:

  • Windows: cd packages/desktop && bun test packages/session-tools-core/src/handlers/config-validate.test.ts packages/session-tools-core/src/handlers/source-test.test.ts packages/shared/src/config/__tests__/source-slug-validation.test.ts packages/server-core/src/handlers/rpc/sources.test.ts -> 17 pass, 0 fail
  • Windows: packages/session-tools-core, packages/shared, and packages/server-core bun run tsc --noEmit -> pass
  • JD-Cloud Linux (Ubuntu 24.04.2 LTS, Linux 6.8.0-53-generic, Node v22.23.1, Bun 1.3.14): same focused test command -> 17 pass, 0 fail
  • JD-Cloud Linux: packages/session-tools-core, packages/shared, and packages/server-core bun run tsc --noEmit -> pass

I also reran cd packages/desktop && bun run typecheck:all; it still fails only in existing Electron/UI paths outside this PR diff, so I am not claiming full desktop preflight as passing.

@wenshao

wenshao commented Jun 27, 2026

Copy link
Copy Markdown
Collaborator

@qwen-code /triage

@qwen-code-ci-bot qwen-code-ci-bot left a comment

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

LGTM, looks ready to ship. ✅

@wenshao wenshao left a comment

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

[Suggestion] The fallback "validate all sources" path in config-validate.ts (the else branch at ~line 117, when ctx.validators is unavailable) iterates source directories and calls validateJsonFileHasFields without the assertValidSourceSlug pre-filter that was added to validateAllSources in validators.ts. Legacy directories like legacy-source- would produce spurious field-validation errors in this fallback path instead of the clean skip-with-warning behavior the primary path now provides. Consider adding a matching slug-validation guard to the loop.

— qwen3.7-max via Qwen Code /review

try {
deleteSource(workspace.rootPath, sourceSlug)
} catch (error) {
if (error instanceof Error && error.message.startsWith('Invalid source slug')) {

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

[Suggestion] This inline check duplicates the existing isInvalidSourceSlugError() helper defined at line 9 of this file, and the two matching strings are inconsistent: the helper uses 'Invalid source slug:' (with colon) while this line uses 'Invalid source slug' (without colon). Use the helper for consistency:

Suggested change
if (error instanceof Error && error.message.startsWith('Invalid source slug')) {
} catch (error) {
if (isInvalidSourceSlugError(error)) {
;(error as Error & { code?: string }).code = 'INVALID_ARGUMENT'
}
throw error
}

— qwen3.7-max via Qwen Code /review

return successResponse(formatValidationResult(result));
try {
const sourcePath = getSourceConfigPath(ctx.workspacePath, sourceSlug);
const result = validateJsonFileHasFields(sourcePath, ['slug', 'name', 'type']);

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

[Suggestion] This try/catch wraps both getSourceConfigPath (which throws on invalid slug via assertValidSourceSlug) and validateJsonFileHasFields (which returns a ValidationResult and doesn't throw today). If validateJsonFileHasFields ever throws in the future (e.g., due to a filesystem error), it would be misattributed as a slug validation failure. Consider narrowing the catch to only the slug validation call, or validating the slug explicitly before the path call.

— qwen3.7-max via Qwen Code /review

return { handlers, warnings, errors }
}

describe('registerSourcesHandlers DELETE', () => {

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

[Suggestion] This describe block only tests the "Invalid source slug" error path. No test verifies that a different error from deleteSource (e.g., a filesystem error like ENOENT) is rethrown without the INVALID_ARGUMENT code being stamped. If someone accidentally removed the if (isInvalidSourceSlugError(error)) guard, no test would catch the regression. Consider adding a test that mocks deleteSource to throw a non-slug error and asserts the rethrown error has no code property.

— qwen3.7-max via Qwen Code /review

expect(text).not.toContain('available on your next message');
});

it('uses the requested source slug for guide checks when config slug is legacy-formatted', async () => {

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

[Suggestion] writeSource creates guide.md in the source directory, so this test only exercises the success branch ('guide.md exists') of the completeness check. The warning branch ('⚠ No guide.md file') — which is the exact code path the PR changes (using sourceSlug instead of source.slug for the guide path lookup at source-test.ts:317) — is never tested. Consider adding a test that omits guide.md and asserts the warning references the requested slug, not the config's legacy slug.

— qwen3.7-max via Qwen Code /review

@wenshao

wenshao commented Jun 27, 2026

Copy link
Copy Markdown
Collaborator

✅ Local real-handler verification — all four slug paths behave as described

I drove the actual desktop handlers (not the test files) against real on-disk fixtures with bun 1.3.14 — the same runtime CI/desktop uses — and confirmed each fix with a mutation A/B: run on the PR tree, then git checkout <base> -- <source> and re-run (bun transpiles on the fly, so the revert is the before/after). Every behavior flips exactly as the PR claims.

⚠️ Why local matters here: packages/desktop is a separate bun workspace, excluded from the root npm workspaces, so the green Test (ubuntu-latest) CI leg does not run any of these desktop tests. Local bun is the only real signal for this PR.

Verdict: PASS — 4/4 behaviors verified, each with a decisive before/after.

  • Head verified: 42f794e7ff73cca78fcc86cb067fab58c81e70d0 (matches current PR head)
  • Build: bun run tsc --noEmit in session-tools-core, shared, server-core → all exit 0 (confirms the PR's typecheck claim)
  • Tests: the 4 touched bun test files → 17 pass, 0 fail (CI never runs these)

Behaviors verified (real handler driven; revert-to-base flips each)

# Behavior AFTER (PR) BEFORE (base, mutation)
A config_validate({target:'sources', sourceSlug:'../sessions'}) on the fallback path structured ✗ Validation failed … sourceSlug: Invalid source slug: "../sessions" (isError:false, no throw) throws Invalid source slug: "../sessions" → generic tool failure
B source_test({sourceSlug:'my-source'}) when config.json slug is legacy my--source completes: ✓ guide.md exists, Validation passed (guide path uses the requested slug) throws Invalid source slug: "my--source" (config slug fed to guide-path helper)
C validateAllSources over sources/legacy-source-/ valid:true + warning Source 'legacy-source-' has invalid slug format, skipping source validation valid:false + hard error Invalid source slug: "legacy-source-"
D source delete RPC with ../sessions throws → error.code = 'INVALID_ARGUMENT' throws → no code (→ HANDLER_ERROR); message is byte-identical

Why each is faithful (not a unit-test rerun)

  • A — the changed try/catch is only on the ctx.validators-absent fallback. That path is live in production: the session-mcp-server runtime builds its SessionToolContext without validators (session-mcp-server/src/index.ts:210 explicitly omits it). My driver leaves validators unset, reproducing that runtime exactly — so this is a real fix, not dead code.
  • B — drove the full handleSourceTest with a stubbed MCP connection (no network). The legacy my--source config slug passes the early load but, pre-PR, blows up at the guide-path lookup; post-PR the requested my-source slug is used and sources/my-source/guide.md is found.
  • CvalidateAllSources(rootPath) reads <rootPath>/sources/ directly (the param is the workspace root), so a real fixture dir drives it with no global state. The skip-with-warning now matches validateAllPermissions.
  • D — invoked the real registered RPC_CHANNELS.sources.DELETE handler exactly as WsRpcServer.onRequest does (handler(ctx, ...args)), with a real workspace registry (CRAFT_CONFIG_DIR fixture, not a mock). The server turns error.code into the wire error code at transport/server.ts:661 ((err as any)?.code ?? 'HANDLER_ERROR'), so the tag genuinely reaches the wire.
    • 🔍 Control 1 — workspace-not-found (Workspace not found: …) → no INVALID_ARGUMENT, falls back to HANDLER_ERROR. The tag is specific to invalid slugs, not all delete failures.
    • 🔍 Control 2 — a valid-but-nonexistent slug (no-such-source) → no throw (delete is idempotent), so well-formed callers are unaffected.

Notes for the reviewer

  • The whole change is additive and strictly tightens error shape, not validation strictness — the slug regex and the path-traversal rejection from fix(desktop): reject unsafe source slugs before deletion #5829 are untouched (verified: ../sessions and my--source are still rejected; only how the rejection surfaces changed).
  • mergeStateStatus: BLOCKED is REVIEW_REQUIRED (needs a maintainer approval), not a CI failure.
  • Scope I did not run: the live Electron desktop UI and the full WS transport round-trip (I invoked the registered RPC handler in-process, which is exactly how onRequest calls it, and separately confirmed the server's err.code → wire code mapping by source). macOS (Darwin, Node 22, bun 1.3.14).
🇨🇳 中文版本

✅ 本地真实 handler 验证 —— 四条 slug 路径行为都与描述一致

我用 bun 1.3.14(桌面端真实运行时)直接驱动真实的桌面 handler(不是测试文件),对接真实磁盘 fixture,并用变异 A/B 确认每个修复:先在 PR 树跑,再 git checkout <base> -- <源文件> 后重跑(bun 即时转译,revert 本身就是 before/after)。每条行为都精确翻转,与 PR 描述一致。

⚠️ 为什么本地验证在这里很关键:packages/desktop 是独立 bun workspace,被 root npm workspaces 排除,所以绿色的 Test (ubuntu-latest) CI leg 根本不会跑这些桌面测试。本地 bun 是本 PR 唯一的真实信号。

结论:PASS —— 4/4 行为已验证,每条都有决定性的 before/after。

  • 验证的 head: 42f794e7ff…(与当前 PR head 一致)
  • 构建: session-tools-coresharedserver-core 三个包 bun run tsc --noEmit 全部 exit 0(印证 PR 的 typecheck 声明)
  • 测试: 4 个改动到的 bun test 文件 → 17 pass, 0 fail(CI 从不跑这些)

已验证行为(真实 handler 驱动;revert 到 base 即翻转)

# 行为 AFTER(PR) BEFORE(base,变异)
A fallback 路径上的 config_validate({target:'sources', sourceSlug:'../sessions'}) 结构化 ✗ Validation failed … sourceSlug: Invalid source slug: "../sessions"isError:false,不抛) 抛出 Invalid source slug: "../sessions" → 泛化 tool 失败
B config.json slug 为旧格式 my--sourcesource_test({sourceSlug:'my-source'}) 完成:✓ guide.md existsValidation passed(guide 路径用请求的 slug) 抛出 Invalid source slug: "my--source"(config slug 被喂给 guide 路径 helper)
C sources/legacy-source-/validateAllSources valid:true + 警告 Source 'legacy-source-' has invalid slug format, skipping source validation valid:false + 硬错误 Invalid source slug: "legacy-source-"
D source 删除 RPC 传 ../sessions 抛出 → error.code = 'INVALID_ARGUMENT' 抛出 → 无 code(→ HANDLER_ERROR);message 完全相同

每条为何是"真实驱动"而非"重跑单测"

  • A —— 改动的 try/catch 只在 ctx.validators 缺失的 fallback 路径上,而该路径在生产中是活的:session-mcp-server 运行时构造 SessionToolContext不设 validatorssession-mcp-server/src/index.ts:210 明确省略)。我的 driver 不设 validators,正好复现该运行时 —— 所以这是真实修复,不是死代码。
  • B —— 驱动了完整的 handleSourceTest(MCP 连接打桩,无网络)。旧格式 my--source config slug 能通过前面的加载,但改前会在 guide 路径查找处炸掉;改后用请求的 my-source slug,找到 sources/my-source/guide.md
  • C —— validateAllSources(rootPath) 直接读 <rootPath>/sources/(参数就是 workspace 根),所以真实 fixture 目录即可驱动、无全局状态。skip+warning 现在与 validateAllPermissions 一致。
  • D —— 像 WsRpcServer.onRequest 那样(handler(ctx, ...args))调用真实注册RPC_CHANNELS.sources.DELETE handler,并用真实 workspace 注册表(CRAFT_CONFIG_DIR fixture,非 mock)。server 在 transport/server.ts:661(err as any)?.code ?? 'HANDLER_ERROR')把 error.code 变成 wire 错误码,所以打的 tag 确实到达 wire。
    • 🔍 对照 1 —— workspace 未找到(Workspace not found: …)→ 不是 INVALID_ARGUMENT,回退到 HANDLER_ERROR。tag 只针对无效 slug,而非所有删除失败。
    • 🔍 对照 2 —— 合法但不存在的 slug(no-such-source)→ 不抛(删除幂等),正常调用方不受影响。

给 reviewer 的说明

  • 整个改动是增量的、只收紧错误形态而非验证严格度 —— slug 正则与 fix(desktop): reject unsafe source slugs before deletion #5829 的路径穿越拒绝都没动(已验证:../sessionsmy--source 仍被拒,只是拒绝的呈现方式变了)。
  • mergeStateStatus: BLOCKEDREVIEW_REQUIRED(需维护者 approve),不是 CI 失败。
  • 未覆盖范围: 实时 Electron 桌面 UI 和完整 WS transport 往返(我在进程内调用了注册的 RPC handler,这正是 onRequest 的调用方式,并另行按源码确认了 server 的 err.code → wire code 映射)。环境 macOS(Darwin, Node 22, bun 1.3.14)。

@VectorPeak

Copy link
Copy Markdown
Contributor Author

@wenshao thanks for the follow-up review. I updated this PR in 848ebe22b352431f021ae99dc4e5f9d0d52874ca to address the latest suggestions:

  • config_validate({ target: sources }) fallback now pre-validates source directory slugs and skips legacy invalid source folders with a warning before field validation.
  • The single-source fallback path now narrows slug validation handling so validateJsonFileHasFields() is no longer wrapped by the slug-validation catch.
  • Source DELETE RPC now reuses isInvalidSourceSlugError(error) for invalid slug classification.
  • Added regression coverage for non-slug deleteSource failures to ensure they are rethrown without INVALID_ARGUMENT.
  • Added coverage for the missing guide.md warning branch when config.json contains a legacy-formatted slug, proving the requested source slug is used for guide lookup.

Validation completed:

  • Windows (Bun 1.3.12): focused desktop tests -> 20 pass, 0 fail
  • Windows: bun run tsc --noEmit in session-tools-core, shared, and server-core -> pass
  • JD-Cloud Linux (Bun 1.3.14, Node v22.23.1): same focused desktop tests -> 20 pass, 0 fail
  • JD-Cloud Linux: bun run tsc --noEmit in session-tools-core, shared, and server-core -> pass

I also had a separate agent review the updated diff against your comments; it found the requested behavior and test coverage aligned with the review expectations. GitHub's ubuntu CI leg is still running at the moment; the PR is otherwise blocked on review state, not on the local Windows/Linux validation above.

@wenshao

wenshao commented Jun 27, 2026

Copy link
Copy Markdown
Collaborator

🔄 Re-verification at head 848ebe22b (review-feedback commit) — still ✅ PASS

The PR moved since my previous verification (head 42f794e7f): commit 848ebe22b "address source slug review feedback" landed. I re-ran the same real-handler + mutation-A/B harness on the new head, isolating this commit's delta (A/B vs the prior head 42f794e7f, not just the merge base).

Verdict: PASS — the four original behaviors are intact, one new behavior is added and verified, and two refactors are behavior-preserving.

  • Head verified: 848ebe22b352431f021ae99dc4e5f9d0d52874ca (matches current PR head)
  • Build: bun run tsc --noEmit in session-tools-core / shared / server-core → all exit 0
  • Tests: the 4 touched bun test files → 20 pass, 0 fail (was 17; this commit added 3). CI still does not run these (desktop is outside the root workspaces).

What this commit changed, and what I observed

Change Type Verified behavior
config_validate (no sourceSlug) now skips an invalid-slug source dir with a warning 🆕 new behavior A/B vs prior head: AFTER → ✓ Validation passed + sources/legacy-source-/config.json: Source 'legacy-source-' has invalid slug format, skipping source validation. BEFORE (42f794e7f) → ✗ Validation failed + legacy-source-/{slug,name,type}: Missing required field.
config_validate (with sourceSlug) narrows the try/catch to just assertValidSourceSlug ♻️ refactor Behavior-preserving: the invalid-slug result is byte-identical to the prior head (✗ Validation failed … sourceSlug: Invalid source slug: "../sessions"). validateJsonFileHasFields returns an invalid result rather than throwing, so narrowing the catch changes no observable output for current inputs — it just stops a future real validation error from being mislabeled as a slug error.
delete RPC: inline startsWith('Invalid source slug') → shared isInvalidSourceSlugError(error) helper ♻️ refactor Behavior-preserving: ../sessions still throws with code = 'INVALID_ARGUMENT'. The helper matches 'Invalid source slug:' (with colon) and assertValidSourceSlug always emits the colon form, so it's equivalent — and it removes the inconsistency where the helper was defined but the delete handler used an ad-hoc inline check.

Re-confirmed (unchanged) on the new head

All four behaviors from the prior verification still hold — driven through the real handlers:

  1. config_validate({sourceSlug:'../sessions'}) → structured Validation failed result, no throw.
  2. source_test({sourceSlug:'my-source'}) with legacy config slug my--sourceguide.md exists, Validation passed (requested slug used for the guide path).
  3. validateAllSources (shared) over sources/legacy-source-/valid:true + skip warning. (file untouched by this commit; re-ran to confirm.)
  4. ✅ delete RPC ../sessionsINVALID_ARGUMENT; non-slug failure (workspace-not-found) → generic HANDLER_ERROR; valid-but-missing slug → idempotent no-op.

Notes

  • The new validate-all-fallback skip now matches the shared validateAllSources and validateAllPermissions behavior — the same bad slug is treated consistently across both the validator path and the session-tool fallback path. That consistency was the stated goal of the PR, and it now holds on both entry points.
  • BLOCKED = REVIEW_REQUIRED (awaiting a maintainer approval), not a CI failure. macOS (Darwin, Node 22, bun 1.3.14). Supersedes my prior comment at 42f794e7f.
🇨🇳 中文版本

🔄 在 head 848ebe22b(处理 review 反馈的提交)上重新验证 —— 仍然 ✅ PASS

PR 自我上次验证(head 42f794e7f)后又有新提交 848ebe22b「address source slug review feedback」。我在新 head 上重跑了同一套"真实 handler + 变异 A/B"工具,并专门隔离本次提交的 delta(A/B 对照前一个 head 42f794e7f,而非仅对 merge base)。

结论:PASS —— 原有四条行为保持不变,新增一条行为已验证,两处重构行为等价。

  • 验证的 head: 848ebe22b…(与当前 PR head 一致)
  • 构建: session-tools-core / shared / server-core 三个包 bun run tsc --noEmit 全部 exit 0
  • 测试: 4 个改动测试文件 bun test20 pass, 0 fail(此前 17;本提交加了 3 个)。CI 仍不跑这些(desktop 在 root workspaces 之外)。

本提交改了什么,我观察到什么

改动 类型 验证到的行为
config_validate(不带 sourceSlug)现在对无效 slug 目录 warning + skip 🆕 新行为 对照前一个 head 的 A/B: AFTER → ✓ Validation passed + sources/legacy-source-/config.json: Source 'legacy-source-' has invalid slug format, skipping source validation。BEFORE(42f794e7f)→ ✗ Validation failed + legacy-source-/{slug,name,type}: Missing required field
config_validate(带 sourceSlug)把 try/catch 收窄到只包 assertValidSourceSlug ♻️ 重构 行为等价: 无效 slug 的结果与前一个 head 逐字节相同(✗ Validation failed … sourceSlug: Invalid source slug: "../sessions")。validateJsonFileHasFields返回无效结果而非抛异常,所以收窄 catch 对当前输入不改变任何可观察输出 —— 只是避免将来真实验证错误被误标成 slug 错误。
删除 RPC:内联 startsWith('Invalid source slug') → 共享 isInvalidSourceSlugError(error) helper ♻️ 重构 行为等价: ../sessions 仍抛出且 code = 'INVALID_ARGUMENT'。helper 匹配 'Invalid source slug:'(带冒号),而 assertValidSourceSlug 总是带冒号 —— 等价,并消除了"helper 已定义但删除 handler 用临时内联检查"的不一致。

在新 head 上重新确认(未变)

上次验证的四条行为依然成立 —— 都通过真实 handler 驱动:

  1. config_validate({sourceSlug:'../sessions'}) → 结构化 Validation failed,不抛。
  2. source_test({sourceSlug:'my-source'}) 配 legacy slug my--sourceguide.md existsValidation passed(guide 路径用请求 slug)。
  3. validateAllSources(shared)对 sources/legacy-source-/valid:true + skip 警告。(本提交未动该文件;重跑确认。)
  4. ✅ 删除 RPC ../sessionsINVALID_ARGUMENT;非 slug 失败(workspace 未找到)→ 泛化 HANDLER_ERROR;合法但不存在的 slug → 幂等无操作。

说明

  • 新的 validate-all fallback skip 现在与共享的 validateAllSourcesvalidateAllPermissions 行为一致 —— 同一个坏 slug 在 validator 路径和 session-tool fallback 路径上被一致处理。这正是本 PR 的目标,现在两个入口都成立。
  • BLOCKED = REVIEW_REQUIRED(等维护者 approve),非 CI 失败。环境 macOS(Darwin, Node 22, bun 1.3.14)。本评论取代我在 42f794e7f 的上一条。

@doudouOUC doudouOUC left a comment

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

LGTM. The error normalization across the four entry points is clean and consistent. The assertValidSourceSlug catch in config-validate.ts is correctly scoped to only the slug validation call, and the validateAllSources fallback properly skips legacy directories with warnings. Tests cover the key scenarios (invalid slug rejection, non-slug error passthrough, legacy directory skip, and legacy-slug guide resolution). ⚠️ Downgraded from Approve to Comment: CI still running.

— qwen3.7-max via Qwen Code /review

@wenshao

wenshao commented Jun 28, 2026

Copy link
Copy Markdown
Collaborator

@qwen-code /triage

@qwen-code-ci-bot qwen-code-ci-bot left a comment

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

LGTM, looks ready to ship. ✅

@wenshao
wenshao added this pull request to the merge queue Jun 28, 2026
Merged via the queue into QwenLM:main with commit 877e8ab Jun 28, 2026
18 checks passed
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

Normalize source slug validation and structured error handling

5 participants