Skip to content

Commit 297e740

Browse files
authored
Merge pull request #62 from kenryu42/fix/issue-61-home-rule-scope
fix(rules): avoid duplicate home rule scope
2 parents d33edb5 + 141fb3d commit 297e740

6 files changed

Lines changed: 114 additions & 8 deletions

File tree

dist/bin/cc-safety-net.js

Lines changed: 16 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -5126,7 +5126,7 @@ function writeJsonAtomic(path, value) {
51265126
}
51275127

51285128
// src/core/rules/policy/scope-policy.ts
5129-
import { existsSync as existsSync7, readFileSync as readFileSync6 } from "node:fs";
5129+
import { existsSync as existsSync7, readFileSync as readFileSync6, realpathSync as realpathSync7 } from "node:fs";
51305130
import { dirname as dirname6, isAbsolute as isAbsolute6, join as join6, relative, resolve as resolve5, sep as sep4 } from "node:path";
51315131

51325132
// src/core/rules/rulebook.ts
@@ -5552,8 +5552,9 @@ function sha256Digest(content) {
55525552
// src/core/rules/policy/scope-policy.ts
55535553
function loadRulesPolicy(options2 = {}) {
55545554
const paths = getPolicyPaths(options2);
5555+
const sameConfigPath = isSameConfigPath(paths.userConfigPath, paths.projectConfigPath);
55555556
const user = readRulesConfig(paths.userConfigPath);
5556-
const project = readRulesConfig(paths.projectConfigPath);
5557+
const project = sameConfigPath ? { config: null, errors: [] } : readRulesConfig(paths.projectConfigPath);
55575558
const errors = [
55585559
...getLegacyRulesConfigErrors(paths, options2),
55595560
...user.errors.map((error) => `${paths.userConfigPath}: ${error}`),
@@ -5724,6 +5725,19 @@ function rulesPolicyToConfig(policy) {
57245725
}
57255726
return { version: 1, rules: policy.rules };
57265727
}
5728+
function isSameConfigPath(userConfigPath, projectConfigPath) {
5729+
if (resolve5(userConfigPath) === resolve5(projectConfigPath)) {
5730+
return true;
5731+
}
5732+
if (!existsSync7(userConfigPath) || !existsSync7(projectConfigPath)) {
5733+
return false;
5734+
}
5735+
try {
5736+
return realpathSync7(userConfigPath) === realpathSync7(projectConfigPath);
5737+
} catch {
5738+
return false;
5739+
}
5740+
}
57275741
function getLegacyRulesConfigErrors(paths, options2) {
57285742
return Array.from(new Set([
57295743
...getLegacyRulesConfigError(getLegacyUserRulesConfigPath(options2), paths.userConfigPath, "~/.cc-safety-net/config.json"),

dist/index.js

Lines changed: 16 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -5070,7 +5070,7 @@ function writeJsonAtomic(path, value) {
50705070
}
50715071

50725072
// src/core/rules/policy/scope-policy.ts
5073-
import { existsSync as existsSync7, readFileSync as readFileSync6 } from "node:fs";
5073+
import { existsSync as existsSync7, readFileSync as readFileSync6, realpathSync as realpathSync7 } from "node:fs";
50745074
import { dirname as dirname6, isAbsolute as isAbsolute6, join as join6, relative, resolve as resolve5, sep as sep4 } from "node:path";
50755075

50765076
// src/core/rules/rulebook.ts
@@ -5496,8 +5496,9 @@ function sha256Digest(content) {
54965496
// src/core/rules/policy/scope-policy.ts
54975497
function loadRulesPolicy(options2 = {}) {
54985498
const paths = getPolicyPaths(options2);
5499+
const sameConfigPath = isSameConfigPath(paths.userConfigPath, paths.projectConfigPath);
54995500
const user = readRulesConfig(paths.userConfigPath);
5500-
const project = readRulesConfig(paths.projectConfigPath);
5501+
const project = sameConfigPath ? { config: null, errors: [] } : readRulesConfig(paths.projectConfigPath);
55015502
const errors = [
55025503
...getLegacyRulesConfigErrors(paths, options2),
55035504
...user.errors.map((error) => `${paths.userConfigPath}: ${error}`),
@@ -5668,6 +5669,19 @@ function rulesPolicyToConfig(policy) {
56685669
}
56695670
return { version: 1, rules: policy.rules };
56705671
}
5672+
function isSameConfigPath(userConfigPath, projectConfigPath) {
5673+
if (resolve5(userConfigPath) === resolve5(projectConfigPath)) {
5674+
return true;
5675+
}
5676+
if (!existsSync7(userConfigPath) || !existsSync7(projectConfigPath)) {
5677+
return false;
5678+
}
5679+
try {
5680+
return realpathSync7(userConfigPath) === realpathSync7(projectConfigPath);
5681+
} catch {
5682+
return false;
5683+
}
5684+
}
56715685
function getLegacyRulesConfigErrors(paths, options2) {
56725686
return Array.from(new Set([
56735687
...getLegacyRulesConfigError(getLegacyUserRulesConfigPath(options2), paths.userConfigPath, "~/.cc-safety-net/config.json"),

dist/pi/index.js

Lines changed: 16 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -5134,7 +5134,7 @@ function writeJsonAtomic(path, value) {
51345134
}
51355135

51365136
// src/core/rules/policy/scope-policy.ts
5137-
import { existsSync as existsSync7, readFileSync as readFileSync6 } from "node:fs";
5137+
import { existsSync as existsSync7, readFileSync as readFileSync6, realpathSync as realpathSync7 } from "node:fs";
51385138
import { dirname as dirname6, isAbsolute as isAbsolute6, join as join6, relative, resolve as resolve5, sep as sep4 } from "node:path";
51395139

51405140
// src/core/rules/rulebook.ts
@@ -5560,8 +5560,9 @@ function sha256Digest(content) {
55605560
// src/core/rules/policy/scope-policy.ts
55615561
function loadRulesPolicy(options2 = {}) {
55625562
const paths = getPolicyPaths(options2);
5563+
const sameConfigPath = isSameConfigPath(paths.userConfigPath, paths.projectConfigPath);
55635564
const user = readRulesConfig(paths.userConfigPath);
5564-
const project = readRulesConfig(paths.projectConfigPath);
5565+
const project = sameConfigPath ? { config: null, errors: [] } : readRulesConfig(paths.projectConfigPath);
55655566
const errors = [
55665567
...getLegacyRulesConfigErrors(paths, options2),
55675568
...user.errors.map((error) => `${paths.userConfigPath}: ${error}`),
@@ -5732,6 +5733,19 @@ function rulesPolicyToConfig(policy) {
57325733
}
57335734
return { version: 1, rules: policy.rules };
57345735
}
5736+
function isSameConfigPath(userConfigPath, projectConfigPath) {
5737+
if (resolve5(userConfigPath) === resolve5(projectConfigPath)) {
5738+
return true;
5739+
}
5740+
if (!existsSync7(userConfigPath) || !existsSync7(projectConfigPath)) {
5741+
return false;
5742+
}
5743+
try {
5744+
return realpathSync7(userConfigPath) === realpathSync7(projectConfigPath);
5745+
} catch {
5746+
return false;
5747+
}
5748+
}
57355749
function getLegacyRulesConfigErrors(paths, options2) {
57365750
return Array.from(new Set([
57375751
...getLegacyRulesConfigError(getLegacyUserRulesConfigPath(options2), paths.userConfigPath, "~/.cc-safety-net/config.json"),

src/core/rules/policy/scope-policy.ts

Lines changed: 19 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,4 @@
1-
import { existsSync, readFileSync } from 'node:fs';
1+
import { existsSync, readFileSync, realpathSync } from 'node:fs';
22
import { dirname, isAbsolute, join, relative, resolve, sep } from 'node:path';
33
import { assertValidRulebook, type Rulebook } from '@/core/rules/rulebook';
44
import type { Config, CustomRule } from '@/types';
@@ -36,8 +36,11 @@ interface ScopePolicy {
3636

3737
export function loadRulesPolicy(options: RulesPolicyOptions = {}): LoadedRulesPolicy {
3838
const paths = getPolicyPaths(options);
39+
const sameConfigPath = isSameConfigPath(paths.userConfigPath, paths.projectConfigPath);
3940
const user = readRulesConfig(paths.userConfigPath);
40-
const project = readRulesConfig(paths.projectConfigPath);
41+
const project = sameConfigPath
42+
? { config: null, errors: [] }
43+
: readRulesConfig(paths.projectConfigPath);
4144
const errors = [
4245
...getLegacyRulesConfigErrors(paths, options),
4346
...user.errors.map((error) => `${paths.userConfigPath}: ${error}`),
@@ -283,6 +286,20 @@ export function rulesPolicyToConfig(policy: LoadedRulesPolicy): Config {
283286
return { version: 1, rules: policy.rules };
284287
}
285288

289+
function isSameConfigPath(userConfigPath: string, projectConfigPath: string): boolean {
290+
if (resolve(userConfigPath) === resolve(projectConfigPath)) {
291+
return true;
292+
}
293+
if (!existsSync(userConfigPath) || !existsSync(projectConfigPath)) {
294+
return false;
295+
}
296+
try {
297+
return realpathSync(userConfigPath) === realpathSync(projectConfigPath);
298+
} catch {
299+
return false;
300+
}
301+
}
302+
286303
function getLegacyRulesConfigErrors(
287304
paths: ReturnType<typeof getPolicyPaths>,
288305
options: RulesPolicyOptions,

tests/bin/rule.test.ts

Lines changed: 23 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -236,6 +236,29 @@ describe('rule list', () => {
236236
expect(result.stderr).toContain('Unknown option for rule list: --global');
237237
});
238238

239+
test('does not load global rules twice when listing from home directory', async () => {
240+
await withTempDir('safety-net-rule-list-home-cwd-', async (tempDir) => {
241+
const homeDir = join(tempDir, 'home');
242+
mkdirSync(homeDir, { recursive: true });
243+
const env = {
244+
CC_SAFETY_NET_HOME: join(homeDir, '.cc-safety-net'),
245+
HOME: homeDir,
246+
};
247+
248+
expect((await runCCSafetyNetCli(['rule', 'init', '--global'], env, homeDir)).exitCode).toBe(
249+
0,
250+
);
251+
const result = await runCCSafetyNetCli(['rule', 'list'], env, homeDir);
252+
253+
expect(result.exitCode).toBe(0);
254+
expect(result.stderr).toBe('');
255+
expect(result.output).toContain('Active sources (1):');
256+
expect(result.output).toContain('[user] user-rules 1.0.0');
257+
expect(result.output).not.toContain('[project] user-rules 1.0.0');
258+
expect(result.output).not.toContain('duplicate active rulebook name');
259+
});
260+
});
261+
239262
test('prints empty merged policy', async () => {
240263
await withTempDir('safety-net-rule-list-empty-', async (tempDir) => {
241264
const result = await runRuleList(tempDir);

tests/core/rules-policy-recovery.test.ts

Lines changed: 24 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -11,6 +11,7 @@ import {
1111
} from 'node:fs';
1212
import { tmpdir } from 'node:os';
1313
import { dirname, join } from 'node:path';
14+
import { analyzeCommand } from '@/core/analyze';
1415
import {
1516
addRulebookSource,
1617
getProjectRulesConfigPath,
@@ -1028,4 +1029,27 @@ describe('rules policy recovery coverage', () => {
10281029
rmSync(tempDir, { recursive: true, force: true });
10291030
}
10301031
});
1032+
1033+
test('loads user rule config once when cwd is the home directory', async () => {
1034+
const tempDir = makeTempDir('rules-policy-home-cwd');
1035+
const homeDir = join(tempDir, 'home');
1036+
const userConfigDir = join(homeDir, '.cc-safety-net', 'rules');
1037+
1038+
try {
1039+
writeRulebook(join(userConfigDir, 'user-rules', 'rulebook.json'), 'user-rules');
1040+
writeDefaultRulesConfig(getUserRulesConfigPath({ userConfigDir }), ['user-rules']);
1041+
expect((await syncRulesConfig({ cwd: homeDir, userConfigDir, global: true })).ok).toBe(true);
1042+
1043+
const policy = loadRulesPolicy({ cwd: homeDir, userConfigDir });
1044+
const config = rulesPolicyToConfig(policy);
1045+
1046+
expect(policy.errors).toEqual([]);
1047+
expect(policy.rulebooks.map((rulebook) => rulebook.source)).toEqual(['user']);
1048+
expect(policy.rules.map((rule) => rule.name)).toEqual(['user-rules/block-docker-prune']);
1049+
expect(config.failClosedReason).toBeUndefined();
1050+
expect(analyzeCommand('echo ok', { cwd: homeDir, config })).toBeNull();
1051+
} finally {
1052+
rmSync(tempDir, { recursive: true, force: true });
1053+
}
1054+
});
10311055
});

0 commit comments

Comments
 (0)