mirror of
https://github.com/affaan-m/ECC.git
synced 2026-10-18 05:57:40 +02:00
fix(install): preserve locale selections during auto-update
This commit is contained in:
@@ -4,6 +4,7 @@ const { validateInstallModuleIds, LOCALE_ALIAS_TO_COMPONENT_ID, listSupportedLoc
|
||||
const { resolveHookConsentFlags } = require('./hook-consent');
|
||||
|
||||
const LEGACY_INSTALL_TARGETS = ['claude', 'claude-project', 'cursor', 'antigravity'];
|
||||
const LOCALE_COMPONENT_IDS = new Set(Object.values(LOCALE_ALIAS_TO_COMPONENT_ID));
|
||||
|
||||
function dedupeStrings(values) {
|
||||
return [...new Set((Array.isArray(values) ? values : []).map(value => String(value).trim()).filter(Boolean))];
|
||||
@@ -103,6 +104,7 @@ function normalizeInstallRequest(options = {}) {
|
||||
: null;
|
||||
const profileId = options.profileId || config?.profileId || null;
|
||||
const target = options.target || config?.target || 'claude';
|
||||
const supportsLocale = target === 'claude' || target === 'claude-project';
|
||||
const moduleIds = validateInstallModuleIds(
|
||||
dedupeStrings([...(config?.moduleIds || []), ...(options.moduleIds || [])])
|
||||
);
|
||||
@@ -113,7 +115,7 @@ function normalizeInstallRequest(options = {}) {
|
||||
`Unsupported locale: "${locale}". Supported locales: ${listSupportedLocales().join(', ')}`
|
||||
);
|
||||
}
|
||||
if (locale && target !== 'claude' && target !== 'claude-project') {
|
||||
if (locale && !supportsLocale) {
|
||||
throw new Error('--locale can only be used with --target claude or --target claude-project');
|
||||
}
|
||||
const requestedIncludeComponentIds = dedupeStrings([
|
||||
@@ -139,7 +141,8 @@ function normalizeInstallRequest(options = {}) {
|
||||
const hasManifestBaseSelection = Boolean(profileId) || moduleIds.length > 0 || includeComponentIds.length > 0;
|
||||
const hasNonLocaleManifestSelection = Boolean(profileId)
|
||||
|| moduleIds.length > 0
|
||||
|| requestedIncludeComponentIds.length > 0
|
||||
// Install-state persists --locale as a component; auto-update replays it via --with.
|
||||
|| requestedIncludeComponentIds.some(id => !supportsLocale || !LOCALE_COMPONENT_IDS.has(id))
|
||||
|| excludeComponentIds.length > 0;
|
||||
const usingManifestMode = hasManifestBaseSelection || excludeComponentIds.length > 0;
|
||||
|
||||
|
||||
@@ -143,6 +143,60 @@ function runTests() {
|
||||
assert.deepStrictEqual(request.includeComponentIds, ['locale:ja']);
|
||||
})) passed++; else failed++;
|
||||
|
||||
if (test('accepts canonical locale components with legacy languages on Claude targets', () => {
|
||||
for (const target of ['claude', 'claude-project']) {
|
||||
const request = normalizeInstallRequest(parseInstallArgs([
|
||||
'node', 'install-apply.js', '--target', target,
|
||||
'--with', 'locale:zh-cn', '--with', 'locale:ja',
|
||||
'typescript', '--no-hooks',
|
||||
]));
|
||||
assert.strictEqual(request.mode, 'legacy-compat');
|
||||
assert.deepStrictEqual(request.includeComponentIds, ['locale:zh-cn', 'locale:ja']);
|
||||
assert.deepStrictEqual(request.legacyLanguages, ['typescript']);
|
||||
assert.strictEqual(request.hookConsent, 'declined');
|
||||
}
|
||||
})) passed++; else failed++;
|
||||
|
||||
if (test('accepts config-backed locale components with legacy languages', () => {
|
||||
const request = normalizeInstallRequest({
|
||||
languages: ['typescript'],
|
||||
config: { target: 'claude', includeComponentIds: ['locale:zh-cn'] },
|
||||
});
|
||||
assert.strictEqual(request.mode, 'legacy-compat');
|
||||
assert.deepStrictEqual(request.includeComponentIds, ['locale:zh-cn']);
|
||||
})) passed++; else failed++;
|
||||
|
||||
if (test('keeps non-locale manifest selections incompatible with legacy languages', () => {
|
||||
const invalidSelections = [
|
||||
{ profileId: 'core' },
|
||||
{ moduleIds: ['rules-core'] },
|
||||
{ includeComponentIds: ['locale:zh-cn', 'skill:tdd-workflow'] },
|
||||
{ includeComponentIds: ['locale:fr'] },
|
||||
{ includeComponentIds: ['locale:zh-CN'] },
|
||||
{ excludeComponentIds: ['skill:tdd-workflow'] },
|
||||
{ config: { includeComponentIds: ['skill:tdd-workflow'] } },
|
||||
{ target: 'cursor' },
|
||||
{ target: 'antigravity' },
|
||||
];
|
||||
for (const selection of invalidSelections) {
|
||||
assert.throws(() => normalizeInstallRequest({
|
||||
target: 'claude', languages: ['typescript'],
|
||||
includeComponentIds: ['locale:zh-cn'], ...selection,
|
||||
}), /cannot be combined/, JSON.stringify(selection));
|
||||
}
|
||||
})) passed++; else failed++;
|
||||
|
||||
if (test('locale components without legacy languages retain manifest mode', () => {
|
||||
for (const profileId of [null, 'core']) {
|
||||
const request = normalizeInstallRequest({
|
||||
target: 'claude', profileId, includeComponentIds: ['locale:zh-cn'],
|
||||
});
|
||||
assert.strictEqual(request.mode, 'manifest');
|
||||
assert.strictEqual(request.profileId, profileId);
|
||||
assert.deepStrictEqual(request.includeComponentIds, ['locale:zh-cn']);
|
||||
}
|
||||
})) passed++; else failed++;
|
||||
|
||||
if (test('rejects unsupported locale codes', () => {
|
||||
assert.throws(
|
||||
() => normalizeInstallRequest({
|
||||
|
||||
@@ -6,6 +6,9 @@ const assert = require('assert');
|
||||
const fs = require('fs');
|
||||
const os = require('os');
|
||||
const path = require('path');
|
||||
const { execFileSync } = require('child_process');
|
||||
const { LOCALE_ALIAS_TO_COMPONENT_ID } = require('../../scripts/lib/install-manifests');
|
||||
const { normalizeInstallRequest, parseInstallArgs } = require('../../scripts/lib/install/request');
|
||||
|
||||
const {
|
||||
parseArgs,
|
||||
@@ -157,6 +160,76 @@ function runTests() {
|
||||
]);
|
||||
})) passed += 1; else failed += 1;
|
||||
|
||||
if (test('replays every locale alias with legacy languages and hook consent', () => {
|
||||
for (const target of ['claude', 'claude-project']) {
|
||||
for (const [alias, componentId] of Object.entries(LOCALE_ALIAS_TO_COMPONENT_ID)) {
|
||||
const original = normalizeInstallRequest(parseInstallArgs([
|
||||
'node', 'install-apply.js', '--target', target,
|
||||
'--locale', alias, 'typescript', 'python', '--no-hooks',
|
||||
]));
|
||||
const record = makeRecord({
|
||||
repoRoot: '/tmp/ecc', homeDir: '/tmp/home', projectRoot: '/tmp/project',
|
||||
adapter: { id: target, target, kind: target === 'claude' ? 'home' : 'project' },
|
||||
request: {
|
||||
profile: original.profileId, modules: original.moduleIds,
|
||||
includeComponents: original.includeComponentIds,
|
||||
excludeComponents: original.excludeComponentIds,
|
||||
legacyLanguages: original.legacyLanguages,
|
||||
legacyMode: true, hookConsent: original.hookConsent,
|
||||
},
|
||||
resolution: { selectedModules: [], skippedModules: [] }, operations: [],
|
||||
});
|
||||
const replay = normalizeInstallRequest(parseInstallArgs([
|
||||
'node', 'install-apply.js', ...buildInstallApplyArgs(record),
|
||||
]));
|
||||
assert.deepStrictEqual(replay, original, `${target}: ${alias} -> ${componentId}`);
|
||||
}
|
||||
}
|
||||
})) passed += 1; else failed += 1;
|
||||
|
||||
for (const target of ['claude', 'claude-project']) {
|
||||
if (test(`end-to-end: auto-update plans a recorded ${target} locale and language install`, () => {
|
||||
const fixture = createTempDir('auto-update-locale-');
|
||||
const homeDir = path.join(fixture, 'home');
|
||||
const projectRoot = path.join(fixture, 'project');
|
||||
const repoRoot = path.resolve(__dirname, '..', '..');
|
||||
const env = { ...process.env, HOME: homeDir, USERPROFILE: homeDir, ECC_DRY_RUN: '0' };
|
||||
fs.mkdirSync(homeDir);
|
||||
fs.mkdirSync(projectRoot);
|
||||
try {
|
||||
const installed = JSON.parse(execFileSync(process.execPath, [
|
||||
path.join(repoRoot, 'scripts', 'install-apply.js'),
|
||||
'--target', target, '--locale', 'zh', 'typescript', '--no-hooks', '--json',
|
||||
], { cwd: projectRoot, env, encoding: 'utf8', maxBuffer: 16 * 1024 * 1024 }));
|
||||
assert.strictEqual(installed.result.applied, true);
|
||||
const statePath = installed.result.installStatePath;
|
||||
const stateBefore = fs.readFileSync(statePath, 'utf8');
|
||||
const state = JSON.parse(stateBefore);
|
||||
assert.deepStrictEqual(state.request.includeComponents, ['locale:zh-cn']);
|
||||
assert.deepStrictEqual(state.request.legacyLanguages, ['typescript']);
|
||||
|
||||
const updated = JSON.parse(execFileSync(process.execPath, [
|
||||
path.join(repoRoot, 'scripts', 'auto-update.js'), '--target', target,
|
||||
'--repo-root', repoRoot, '--dry-run', '--json',
|
||||
], { cwd: projectRoot, env, encoding: 'utf8', maxBuffer: 16 * 1024 * 1024 }));
|
||||
assert.strictEqual(updated.summary.errorCount, 0);
|
||||
assert.strictEqual(updated.summary.checkedCount, 1);
|
||||
assert.strictEqual(updated.results[0].status, 'planned');
|
||||
const plan = updated.results[0].payload.plan;
|
||||
assert.strictEqual(plan.mode, 'legacy-compat');
|
||||
assert.deepStrictEqual(plan.legacyLanguages, ['typescript']);
|
||||
assert.deepStrictEqual(plan.includedComponentIds, ['locale:zh-cn']);
|
||||
assert.ok(plan.selectedModuleIds.includes('docs-zh-cn'));
|
||||
assert.ok(plan.selectedModuleIds.includes('framework-language'));
|
||||
assert.ok(!plan.selectedModuleIds.includes('hooks-runtime'));
|
||||
assert.strictEqual(plan.statePreview.request.hookConsent, 'declined');
|
||||
assert.strictEqual(fs.readFileSync(statePath, 'utf8'), stateBefore);
|
||||
} finally {
|
||||
cleanup(fixture);
|
||||
}
|
||||
})) passed += 1; else failed += 1;
|
||||
}
|
||||
|
||||
if (test('buildInstallApplyArgs reconstructs manifest installs', () => {
|
||||
const record = {
|
||||
adapter: { target: 'cursor', kind: 'project' },
|
||||
|
||||
Reference in New Issue
Block a user