Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
43 changes: 42 additions & 1 deletion scripts/unit-smoke.mjs
Original file line number Diff line number Diff line change
Expand Up @@ -14,14 +14,15 @@
* 零敏感:全部用通用样例,不触碰任何真实仓库与配置。
*/
import { join } from 'node:path';
import { homedir } from 'node:os';
import { fileURLToPath } from 'node:url';
import assert from 'node:assert/strict';

const ROOT = join(fileURLToPath(import.meta.url), '..', '..');
const { buildReviewArgs, runEngine, prepareEngineEnv, collectPendingPaths } = await import(new URL('../src/runner.js', import.meta.url).href);
const { isSensitivePath, listSensitiveFiles } = await import(new URL('../src/sensitive-list.js', import.meta.url).href);
const { reportFilePath, repoNameOf } = await import(new URL('../src/report.js', import.meta.url).href);
const { normalizeConfig, DEFAULT_CONFIG } = await import(new URL('../src/config.js', import.meta.url).href);
const { normalizeConfig, DEFAULT_CONFIG, resolveDshRoot } = await import(new URL('../src/config.js', import.meta.url).href);
const { createSettingsSchema } = await import(new URL('../src/settings-schema.js', import.meta.url).href);
const { createModelSource, SESSION_MODEL, DEEPSEEK_OFFICIAL_DEFAULT } = await import(new URL('../src/model-provider.js', import.meta.url).href);
const { parseCredentialsText } = await import(new URL('../src/credentials.js', import.meta.url).href);
Expand Down Expand Up @@ -198,6 +199,46 @@ t('normalizeConfig:并发开关类型清洗 + 广度钳制 1..32', () => {
assert.equal(normalizeConfig({ concurrency: 'x' }).concurrency, 8); // 非数 → 默认
});

t('resolveDshRoot:优先 DSH_HOME,未设置回退 homedir()/.dsh(issue #13)', () => {
// 注:用例全走 env 注入,不碰进程全局 process.env(不影响其他用例)。
assert.equal(resolveDshRoot({ DSH_HOME: 'D:/dsh-home' }), 'D:/dsh-home');
// trim 后采用(DSH_HOME 值带首尾空白时去空白,Windows 反斜杠尾分隔符保留)。
assert.equal(resolveDshRoot({ DSH_HOME: ' D:\\dsh-home\\ ' }), 'D:\\dsh-home\\');
// 未设置 / 空串 / 纯空白 → 一律回退默认 ~/.dsh。
assert.equal(resolveDshRoot({}), join(homedir(), '.dsh'));
assert.equal(resolveDshRoot({ DSH_HOME: '' }), join(homedir(), '.dsh'));
assert.equal(resolveDshRoot({ DSH_HOME: ' ' }), join(homedir(), '.dsh'));
// 不传参 = 默认走进程真实 env:按解析口径动态自洽(进程若设了 DSH_HOME 也一致)。
const expectDefault =
process.env.DSH_HOME && process.env.DSH_HOME.trim() !== ''
? process.env.DSH_HOME.trim()
: join(homedir(), '.dsh');
assert.equal(resolveDshRoot(), expectDefault);
});

t('task-book:REVIEW_HOME 回退分支跟随 DSH_HOME,未设置回退 ~/.dsh/dsh-code-review(issue #13)', async () => {
// REVIEW_HOME 是模块顶层常量、import 时求值:用带 query 的独立 URL 重新求值
// 模块实例(不同 URL = 不命中既有实例缓存),config.js 的 resolveDshRoot 是
// 函数引用、默认参数调用时才读 process.env,故能观察到当前环境。
const prevReview = process.env.DSH_REVIEW_HOME;
const prevDsh = process.env.DSH_HOME;
try {
delete process.env.DSH_REVIEW_HOME; // 只测回退分支,DSH_REVIEW_HOME 优先逻辑由既有用例覆盖
// 场景 1:设置自定义 DSH_HOME → REVIEW_HOME = <DSH_HOME>/dsh-code-review
process.env.DSH_HOME = 'D:/dsh-home';
const tb1 = await import(new URL('../src/task-book.js?case=dshhome', import.meta.url).href);
assert.equal(tb1.REVIEW_HOME, join('D:/dsh-home', 'dsh-code-review'));
// 场景 2:未设置 DSH_HOME → 回退 homedir()/.dsh/dsh-code-review(默认布局)
delete process.env.DSH_HOME;
const tb2 = await import(new URL('../src/task-book.js?case=fallback', import.meta.url).href);
assert.equal(tb2.REVIEW_HOME, join(homedir(), '.dsh', 'dsh-code-review'));
} finally {
process.env.DSH_REVIEW_HOME = prevReview;
if (prevDsh === undefined) delete process.env.DSH_HOME;
else process.env.DSH_HOME = prevDsh;
}
});

/* ---------- 5. runEngine ---------- */
t('runEngine:stdout 收集 + 退出码', async () => {
const r = await runEngine({ bin: process.execPath, args: ['-e', 'console.log("hello")'], timeoutMs: 5000 });
Expand Down
27 changes: 27 additions & 0 deletions src/config.js
Original file line number Diff line number Diff line change
@@ -1,6 +1,9 @@
// SPDX-License-Identifier: Apache-2.0
// Copyright 2026 dsh-code-review contributors

import { join } from 'node:path';
import { homedir } from 'node:os';

/**
* config.js —— dsh-code-review 配置模型:默认值、归一化、宿主 volatile 解包。
*
Expand Down Expand Up @@ -160,3 +163,27 @@ export function normalizeConfig(raw) {
}
return cfg;
}

/**
* 解析 DSH 根目录(issue #13:homedir()/.dsh 硬编码——自定义 DSH_HOME 时
* 路由/凭据解析失败)。
*
* 口径:
* - 优先 env.DSH_HOME:非空字符串才采用,trim 后作为根(防值带意外空白;
* 空串/纯空白/非字符串一律视为未设置)——Windows 用户示例 D:\dsh-home;
* - 未设置/空 → 回退 homedir()/.dsh(默认安装布局,行为与旧版完全一致)。
*
* 所有「home 下 .dsh 目录」相关的路径来源(模型路由 profiles、凭据文件等)
* 必须统一走这里,避免自定义 DSH_HOME 布局下各处解析不一致。
*
* @param {object} [env] 环境变量对象(默认 process.env;单测经此注入,
* 不污染进程全局环境)。
* @returns {string} DSH 根目录绝对路径。
*/
export function resolveDshRoot(env = process.env) {
const home =
env && typeof env.DSH_HOME === 'string' && env.DSH_HOME.trim() !== ''
? env.DSH_HOME.trim()
: '';
return home !== '' ? home : join(homedir(), '.dsh');
}
5 changes: 3 additions & 2 deletions src/credentials.js
Original file line number Diff line number Diff line change
Expand Up @@ -26,7 +26,7 @@
*/
import { existsSync, readFileSync } from 'node:fs';
import { join } from 'node:path';
import { homedir } from 'node:os';
import { resolveDshRoot } from './config.js';

/** 从凭据文本中提取指定引用名的值。
* 实况(实证 DSH 凭据 yaml):所有凭据引用在
Expand Down Expand Up @@ -62,7 +62,8 @@ function escapeRe(s) {
* @returns {string|null} 值 / ''(显式空值)/ null(文件缺失、键不存在或读取失败)。 */
function readCredentialsYaml(name) {
try {
const file = join(homedir(), '.dsh', '.credentials.yaml');
// 根目录统一走 resolveDshRoot:优先 DSH_HOME,回退 ~/.dsh(issue #13)。
const file = join(resolveDshRoot(), '.credentials.yaml');
if (!existsSync(file)) return null;
return parseCredentialsText(readFileSync(file, 'utf8'), name);
} catch (error) {
Expand Down
5 changes: 3 additions & 2 deletions src/model-provider.js
Original file line number Diff line number Diff line change
Expand Up @@ -30,8 +30,8 @@
*/
import { existsSync, readFileSync, readdirSync } from 'node:fs';
import { join } from 'node:path';
import { homedir } from 'node:os';
import { createRequire } from 'node:module';
import { resolveDshRoot } from './config.js';

/** 评审模型「沿用会话模型」的保留标识(与 settings-schema / config 一致)。 */
export const SESSION_MODEL = 'session';
Expand Down Expand Up @@ -67,7 +67,8 @@ function loadProfileLlmConfig() {
} catch {
return null; // 开发环境若 junction 未建也能继续(路由表可能为空,resolveProvider 返回 null)
}
const profilesRoot = join(homedir(), '.dsh', 'profiles');
// 根目录统一走 resolveDshRoot:优先 DSH_HOME,回退 ~/.dsh(issue #13)。
const profilesRoot = join(resolveDshRoot(), 'profiles');
// readdirSync 包 try/catch——目录无读权限(EACCES)或
// existsSync 后被删的竞态不再一路抛到调用方(文件头约定「任何一步失败都
// 不抛异常」);目录名排序使多 profile 时「取第一个 deepseek/alternate」的
Expand Down
9 changes: 5 additions & 4 deletions src/task-book.js
Original file line number Diff line number Diff line change
Expand Up @@ -22,16 +22,17 @@
*/
import { existsSync, mkdirSync, readFileSync, readdirSync, writeFileSync, renameSync, rmSync, openSync, closeSync } from 'node:fs';
import { join, resolve } from 'node:path';
import { homedir } from 'node:os';
import { spawn } from 'node:child_process';
import { randomInt } from 'node:crypto';
import { fileURLToPath } from 'node:url';
import { DEFAULT_CONFIG } from './config.js'; // 仅取键集合作 config 白名单(不构成循环依赖:config.js 无本地导入)
import { DEFAULT_CONFIG, resolveDshRoot } from './config.js'; // 键集白名单 + DSH 根解析(不构成循环依赖:config.js 无本地导入)

/** 评审工作根目录(env 可覆盖,测试/多用户场景用)。 */
/** 评审工作根目录(env 可覆盖,测试/多用户场景用)。
* 回退分支跟随 resolveDshRoot()(优先 DSH_HOME,未设置回退 ~/.dsh)——
* 保证 DSH_HOME 自定义布局下任务簿/凭据/路由根一致,不出现 split-brain(issue #13)。 */
export const REVIEW_HOME = process.env.DSH_REVIEW_HOME
? resolve(process.env.DSH_REVIEW_HOME)
: join(homedir(), '.dsh', 'dsh-code-review');
: join(resolveDshRoot(), 'dsh-code-review');

/** 任务目录。 */
export const TASKS_ROOT = join(REVIEW_HOME, 'tasks');
Expand Down
Loading