mirror of
https://github.com/Tencent/teamai-cli.git
synced 2026-10-02 03:14:40 +08:00
fix(dry-run): thread { dryRun } through the loaders contribute, session save and recall use (#853)
#837 threaded LoadOptions through the config loaders, and #850 fixes the three commands that still reach them bare (pull, push, status). Three more commands load their config before their own dry-run guard and pass nothing: - contribute (--scope project loads, then --scope user / auto-detect) - session save (same three branches) - recall (detection, the inherited user scope, and the user branch) On a config pending the legacy role migration each of them rewrote ~/.teamai/config.yaml under --dry-run, printing the migration line without any [dry-run] marker; the auto-detect and project branches can also adopt a pre-#546 partition and run the single-repo self-heal bootstrap. loadLocalConfigForScope is the loader #837 missed: it now takes LoadOptions and forwards them to detectProjectConfig and both migrateLegacyRoleConfig calls. Callers that pass nothing behave as before — a real run still migrates in place. Co-authored-by: ydflow <ydflow@users.noreply.github.com>
This commit is contained in:
@@ -29,6 +29,9 @@ vi.mock('../utils/reports-branch.js', async (importOriginal) => ({
|
||||
updateReports: vi.fn(),
|
||||
}));
|
||||
|
||||
import { contribute } from '../contribute.js';
|
||||
import { loadLocalConfigForScope } from '../config.js';
|
||||
import { recall } from '../recall.js';
|
||||
import { rolesSet } from '../roles-cmd.js';
|
||||
import { tagsSubscribe, tagsUnsubscribe } from '../tags.js';
|
||||
import { updateReports } from '../utils/reports-branch.js';
|
||||
@@ -181,3 +184,56 @@ describe.each(FIXTURES)('--dry-run on %s', (_fixture, setup) => {
|
||||
expect(log.info).toHaveBeenCalledWith(expect.stringContaining('[dry-run] Would'));
|
||||
});
|
||||
});
|
||||
|
||||
describe('--dry-run through the loaders the commands share (#850)', () => {
|
||||
const originalCwd = process.cwd();
|
||||
const roots: string[] = [];
|
||||
|
||||
afterEach(() => {
|
||||
vi.restoreAllMocks();
|
||||
vi.unstubAllEnvs();
|
||||
process.chdir(originalCwd);
|
||||
for (const dir of roots.splice(0)) fs.rmSync(dir, { recursive: true, force: true });
|
||||
});
|
||||
|
||||
function legacyRoot(): { root: string; configPath: string } {
|
||||
const root = fs.mkdtempSync(path.join(os.tmpdir(), 'teamai-dry-run-loader-'));
|
||||
roots.push(root);
|
||||
const home = path.join(root, 'home');
|
||||
fs.mkdirSync(path.join(home, '.teamai'), { recursive: true });
|
||||
vi.stubEnv('HOME', home);
|
||||
process.chdir(setupLegacyRoleConfig(root));
|
||||
return { root, configPath: path.join(home, '.teamai', 'config.yaml') };
|
||||
}
|
||||
|
||||
it('recall --dry-run writes no file: the user scope loads through the flag (#850)', async () => {
|
||||
const { root } = legacyRoot();
|
||||
const before = snapshotTree(root);
|
||||
await recall('dry run probe', { dryRun: true });
|
||||
expect(snapshotTree(root)).toEqual(before);
|
||||
});
|
||||
|
||||
it('contribute --scope user --dry-run writes no file on a config pending the role migration (#850)', async () => {
|
||||
const { root } = legacyRoot();
|
||||
// In the tree before the snapshot, so the run itself adds nothing.
|
||||
const file = path.join(process.cwd(), 'note.md');
|
||||
fs.writeFileSync(file, 'Learned: a dry run must not migrate the teamai config.\n');
|
||||
const before = snapshotTree(root);
|
||||
await contribute({ file, scope: 'user', dryRun: true });
|
||||
expect(snapshotTree(root)).toEqual(before);
|
||||
});
|
||||
|
||||
it('the loader previews the legacy role migration under --dry-run and writes nothing (#850)', async () => {
|
||||
const { configPath } = legacyRoot();
|
||||
const loaded = await loadLocalConfigForScope('user', undefined, { dryRun: true });
|
||||
expect(loaded?.primaryRole).toBe('hai');
|
||||
expect(fs.readFileSync(configPath, 'utf-8')).not.toContain('primaryRole');
|
||||
});
|
||||
|
||||
it('the loader still migrates in place when the caller passes nothing, as before (#850)', async () => {
|
||||
const { configPath } = legacyRoot();
|
||||
const loaded = await loadLocalConfigForScope('user');
|
||||
expect(loaded?.primaryRole).toBe('hai');
|
||||
expect(fs.readFileSync(configPath, 'utf-8')).toContain('primaryRole: hai');
|
||||
});
|
||||
});
|
||||
|
||||
+4
-3
@@ -232,14 +232,15 @@ async function throwTeamConfigMissingOrInvalid(repoPath: string): Promise<never>
|
||||
export async function loadLocalConfigForScope(
|
||||
scope: Scope,
|
||||
projectRoot?: string,
|
||||
options: LoadOptions = {},
|
||||
): Promise<LocalConfig | null> {
|
||||
if (scope === 'project') {
|
||||
if (!projectRoot) return null;
|
||||
// Reuse the single detection path so config location never drifts between
|
||||
// "detect the active project" and "load a named project's config".
|
||||
const detected = await detectProjectConfig(projectRoot);
|
||||
const detected = await detectProjectConfig(projectRoot, undefined, options);
|
||||
if (!detected) return null;
|
||||
return migrateLegacyRoleConfig(detected, path.join(getDataHome(detected), 'config.yaml'));
|
||||
return migrateLegacyRoleConfig(detected, path.join(getDataHome(detected), 'config.yaml'), options);
|
||||
}
|
||||
const configPath = getConfigPath(scope, projectRoot);
|
||||
const content = await readFileSafe(expandHome(configPath));
|
||||
@@ -247,7 +248,7 @@ export async function loadLocalConfigForScope(
|
||||
try {
|
||||
const raw = YAML.parse(content);
|
||||
const parsed = LocalConfigSchema.parse(raw);
|
||||
return await migrateLegacyRoleConfig(parsed, configPath);
|
||||
return await migrateLegacyRoleConfig(parsed, configPath, options);
|
||||
} catch (e) {
|
||||
log.error(`Invalid ${scope} config at ${configPath}: ${describeConfigError(e)}`);
|
||||
return null;
|
||||
|
||||
+8
-5
@@ -191,19 +191,22 @@ export async function contribute(
|
||||
return;
|
||||
}
|
||||
|
||||
// Init check — select scope based on --scope flag or auto-detect
|
||||
// Init check — select scope based on --scope flag or auto-detect. The flag
|
||||
// reaches the loaders: a bare load migrates the legacy role config in place,
|
||||
// which would write under --dry-run (#850).
|
||||
const loadOpts = { dryRun: options.dryRun };
|
||||
let localConfig: LocalConfig;
|
||||
if (options.scope === 'project') {
|
||||
const cfg = await loadLocalConfigForScope('project', process.cwd());
|
||||
const cfg = await loadLocalConfigForScope('project', process.cwd(), loadOpts);
|
||||
if (!cfg) { log.error('No project-level teamai config in this directory'); return; }
|
||||
localConfig = cfg;
|
||||
} else if (options.scope === 'user') {
|
||||
const { localConfig: userCfg } = await requireInit();
|
||||
const { localConfig: userCfg } = await requireInit(loadOpts);
|
||||
localConfig = userCfg;
|
||||
} else {
|
||||
// Auto-detect (unchanged default behavior)
|
||||
const projectConfig = await detectProjectConfig();
|
||||
localConfig = projectConfig ?? (await requireInit()).localConfig;
|
||||
const projectConfig = await detectProjectConfig(undefined, undefined, loadOpts);
|
||||
localConfig = projectConfig ?? (await requireInit(loadOpts)).localConfig;
|
||||
}
|
||||
assertNotReadOnly(localConfig, 'teamai contribute');
|
||||
const username = localConfig.username;
|
||||
|
||||
+5
-3
@@ -459,7 +459,9 @@ export async function recall(
|
||||
// not reach that scope's team (#787).
|
||||
let projectUnreadable = false;
|
||||
try {
|
||||
projectConfig = await detectProjectConfig(undefined, (configPath, error) => { unreadable.push(`${configPath}: ${error}`); });
|
||||
// The flag reaches detection: a bare load migrates the legacy role config
|
||||
// in place, which would write under --dry-run (#850).
|
||||
projectConfig = await detectProjectConfig(undefined, (configPath, error) => { unreadable.push(`${configPath}: ${error}`); }, { dryRun: options.dryRun });
|
||||
} catch (e) {
|
||||
// A cwd that no longer exists holds no project: user scope, as in
|
||||
// resolveConfigForDir.
|
||||
@@ -510,7 +512,7 @@ export async function recall(
|
||||
|
||||
if (projectConfig.inheritUserScope === true) {
|
||||
try {
|
||||
const userConfig = await loadLocalConfigForScope('user');
|
||||
const userConfig = await loadLocalConfigForScope('user', undefined, { dryRun: options.dryRun });
|
||||
if (userConfig) {
|
||||
const result = await loadOrBuildScopeIndex(userConfig, 'user');
|
||||
if (result === 'build-failed') indexBuildFailed = true;
|
||||
@@ -525,7 +527,7 @@ export async function recall(
|
||||
} else {
|
||||
// User mode: user scope only.
|
||||
try {
|
||||
const { localConfig: userConfig } = await requireInit();
|
||||
const { localConfig: userConfig } = await requireInit({ dryRun: options.dryRun });
|
||||
const result = await loadOrBuildScopeIndex(userConfig, 'user');
|
||||
if (result === 'build-failed') indexBuildFailed = true;
|
||||
else if (result && result.index.entries.length > 0) {
|
||||
|
||||
+7
-4
@@ -96,20 +96,23 @@ export async function saveSession(options: SaveSessionOptions): Promise<void> {
|
||||
return;
|
||||
}
|
||||
|
||||
// The flag reaches the loaders: a bare load migrates the legacy role config
|
||||
// in place, which would write under --dry-run (#850).
|
||||
const loadOpts = { dryRun: options.dryRun };
|
||||
let localConfig: LocalConfig;
|
||||
try {
|
||||
if (options.scope === 'project') {
|
||||
const cfg = await loadLocalConfigForScope('project', process.cwd());
|
||||
const cfg = await loadLocalConfigForScope('project', process.cwd(), loadOpts);
|
||||
if (!cfg) {
|
||||
log.error('No project-level teamai config in the current directory.');
|
||||
return;
|
||||
}
|
||||
localConfig = cfg;
|
||||
} else if (options.scope === 'user') {
|
||||
localConfig = (await requireInit()).localConfig;
|
||||
localConfig = (await requireInit(loadOpts)).localConfig;
|
||||
} else {
|
||||
const projectConfig = await detectProjectConfig();
|
||||
localConfig = projectConfig ?? (await requireInit()).localConfig;
|
||||
const projectConfig = await detectProjectConfig(undefined, undefined, loadOpts);
|
||||
localConfig = projectConfig ?? (await requireInit(loadOpts)).localConfig;
|
||||
}
|
||||
} catch (e) {
|
||||
log.error(`Cannot push: ${(e as Error).message}`);
|
||||
|
||||
Reference in New Issue
Block a user