sinapse-ai 1.9.0 → 1.9.1

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (88) hide show
  1. package/.claude/rules/mandatory-delegation.md +1 -1
  2. package/.codex/delegation-matrix.json +4 -3
  3. package/.codex/delegation-parity.json +4 -3
  4. package/.codex/instructions.md +2 -2
  5. package/.sinapse-ai/constitution.md +2 -2
  6. package/.sinapse-ai/core/doctor/checks/git-hooks.js +76 -10
  7. package/.sinapse-ai/core/execution/subagent-dispatcher.js +1 -1
  8. package/.sinapse-ai/core/synapse/engine.js +15 -0
  9. package/.sinapse-ai/data/entity-registry.yaml +13 -13
  10. package/.sinapse-ai/development/agents/snps-orqx.md +4 -4
  11. package/.sinapse-ai/git-hooks/lib/secret-scanner-core.js +76 -4
  12. package/.sinapse-ai/git-hooks/pre-push +7 -1
  13. package/.sinapse-ai/install-manifest.yaml +9 -9
  14. package/AGENTS.md +2 -2
  15. package/CHANGELOG.md +1247 -0
  16. package/bin/commands/uninstall.js +2 -2
  17. package/bin/utils/secret-scanner-core.js +76 -4
  18. package/docs/agent-reference-guide.md +1 -1
  19. package/docs/framework/architecture-overview.md +4 -4
  20. package/docs/framework/guiding-principles.md +9 -9
  21. package/docs/getting-started.md +1 -1
  22. package/docs/guides/agent-reference.md +1 -1
  23. package/docs/guides/codex-config.md +4 -5
  24. package/docs/pt/architecture/sub-orqx-pattern.md +20 -18
  25. package/package.json +8 -2
  26. package/packages/installer/src/installer/git-hooks-installer.js +3 -1
  27. package/packages/installer/src/wizard/ide-config-generator.js +9 -1
  28. package/packages/installer/src/wizard/index.js +3 -4
  29. package/scripts/regenerate-orqx-stubs.ps1 +0 -1
  30. package/scripts/sync-counts.js +10 -2
  31. package/scripts/sync-squad-yaml-components.js +108 -6
  32. package/scripts/validate-squad-orqx.js +19 -9
  33. package/sinapse/agents/sinapse-orqx.md +4 -4
  34. package/sinapse/agents/snps-orqx.md +4 -4
  35. package/sinapse/knowledge-base/routing-catalog.md +1 -1
  36. package/sinapse/tasks/diagnose-and-route.md +1 -1
  37. package/sinapse/tasks/squad-status-report.md +1 -1
  38. package/squads/claude-code-mastery/agents/claude-mastery-chief.md +1 -1
  39. package/squads/claude-code-mastery/agents/hooks-architect.md +60 -68
  40. package/squads/claude-code-mastery/knowledge-base/swarm-orchestration-patterns.md +1 -1
  41. package/squads/claude-code-mastery/tasks/audit-setup.md +1 -1
  42. package/squads/claude-code-mastery/workflows/optimization-cycle.yaml +4 -4
  43. package/squads/claude-code-mastery/workflows/project-setup-cycle.yaml +4 -4
  44. package/squads/squad-animations/README.md +1 -1
  45. package/squads/squad-cloning/README.md +1 -1
  46. package/squads/squad-commercial/README.md +1 -1
  47. package/squads/squad-content/README.md +1 -1
  48. package/squads/squad-copy/README.md +1 -1
  49. package/squads/squad-council/README.md +1 -1
  50. package/squads/squad-courses/README.md +1 -1
  51. package/squads/squad-cybersecurity/README.md +1 -1
  52. package/squads/squad-design/README.md +1 -1
  53. package/squads/squad-finance/README.md +1 -1
  54. package/squads/squad-growth/README.md +1 -1
  55. package/squads/squad-paidmedia/README.md +1 -1
  56. package/squads/squad-product/README.md +1 -1
  57. package/squads/squad-research/README.md +1 -1
  58. package/squads/squad-storytelling/README.md +1 -1
  59. package/.sinapse-ai/core/memory/__tests__/active-modules.verify.js +0 -265
  60. package/.sinapse-ai/core/permissions/__tests__/permission-mode.test.js +0 -293
  61. package/.sinapse-ai/infrastructure/tests/project-status-loader.test.js +0 -569
  62. package/.sinapse-ai/infrastructure/tests/regression-suite-v2.md +0 -622
  63. package/.sinapse-ai/infrastructure/tests/validate-module.js +0 -98
  64. package/.sinapse-ai/infrastructure/tests/worktree-manager.test.js +0 -620
  65. package/.sinapse-ai/workflow-intelligence/__tests__/confidence-scorer.test.js +0 -335
  66. package/.sinapse-ai/workflow-intelligence/__tests__/integration.test.js +0 -340
  67. package/.sinapse-ai/workflow-intelligence/__tests__/suggestion-engine.test.js +0 -438
  68. package/.sinapse-ai/workflow-intelligence/__tests__/wave-analyzer.test.js +0 -448
  69. package/.sinapse-ai/workflow-intelligence/__tests__/workflow-registry.test.js +0 -303
  70. package/packages/installer/src/__tests__/performance-benchmark.js +0 -383
  71. package/packages/installer/tests/integration/environment-configuration.test.js +0 -332
  72. package/packages/installer/tests/integration/wizard-detection.test.js +0 -352
  73. package/packages/installer/tests/unit/artifact-copy-pipeline/artifact-copy-pipeline.test.js +0 -402
  74. package/packages/installer/tests/unit/claude-md-template-v5/claude-md-template-v5.test.js +0 -193
  75. package/packages/installer/tests/unit/config-validator.test.js +0 -315
  76. package/packages/installer/tests/unit/detection/detect-project-type.test.js +0 -539
  77. package/packages/installer/tests/unit/doctor/doctor-checks.test.js +0 -675
  78. package/packages/installer/tests/unit/doctor/doctor-orchestrator.test.js +0 -192
  79. package/packages/installer/tests/unit/entity-registry-bootstrap.test.js +0 -192
  80. package/packages/installer/tests/unit/env-template.test.js +0 -187
  81. package/packages/installer/tests/unit/generate-settings-json/generate-settings-json.test.js +0 -310
  82. package/packages/installer/tests/unit/git-hooks-installer.test.js +0 -262
  83. package/packages/installer/tests/unit/ide-sync-integration/ide-sync-integration.test.js +0 -231
  84. package/packages/installer/tests/unit/merger/env-merger.test.js +0 -191
  85. package/packages/installer/tests/unit/merger/markdown-merger.test.js +0 -262
  86. package/packages/installer/tests/unit/merger/strategies.test.js +0 -154
  87. package/packages/installer/tests/unit/merger/yaml-merger.test.js +0 -328
  88. package/packages/sinapse-install/tests/unit/chrome-brain.smoke.test.js +0 -66
@@ -1,310 +0,0 @@
1
- 'use strict';
2
-
3
- const fs = require('fs');
4
- const path = require('path');
5
- const os = require('os');
6
-
7
- const {
8
- generate,
9
- validateBoundaryPath,
10
- readBoundaryConfig,
11
- expandProtectedPaths,
12
- expandExceptionPaths,
13
- generatePermissions,
14
- writeSettingsJson,
15
- } = require('../../../../../.sinapse-ai/infrastructure/scripts/generate-settings-json');
16
-
17
- function createTempProject(boundary, existingSettings) {
18
- const tmpDir = fs.mkdtempSync(path.join(os.tmpdir(), 'gen-settings-'));
19
-
20
- // Create core-config.yaml with boundary section
21
- const sinapseCoreDir = path.join(tmpDir, '.sinapse-ai');
22
- fs.mkdirSync(sinapseCoreDir, { recursive: true });
23
-
24
- const yamlContent = [
25
- 'boundary:',
26
- ` frameworkProtection: ${boundary.frameworkProtection}`,
27
- ' protected:',
28
- ...boundary.protected.map(p => ` - ${p}`),
29
- ' exceptions:',
30
- ...boundary.exceptions.map(p => ` - ${p}`),
31
- ].join('\n') + '\n';
32
-
33
- fs.writeFileSync(path.join(tmpDir, '.sinapse-ai', 'core-config.yaml'), yamlContent, 'utf8');
34
-
35
- // Create directory structure for expansion tests
36
- if (boundary.protected.includes('.sinapse-ai/core/**')) {
37
- const coreDir = path.join(tmpDir, '.sinapse-ai', 'core');
38
- fs.mkdirSync(coreDir, { recursive: true });
39
- fs.mkdirSync(path.join(coreDir, 'utils'), { recursive: true });
40
- fs.mkdirSync(path.join(coreDir, 'events'), { recursive: true });
41
- fs.writeFileSync(path.join(coreDir, 'index.js'), '', 'utf8');
42
- }
43
-
44
- // Create .claude directory and optionally existing settings
45
- const claudeDir = path.join(tmpDir, '.claude');
46
- fs.mkdirSync(claudeDir, { recursive: true });
47
-
48
- if (existingSettings) {
49
- fs.writeFileSync(
50
- path.join(claudeDir, 'settings.json'),
51
- JSON.stringify(existingSettings, null, 2) + '\n',
52
- 'utf8'
53
- );
54
- }
55
-
56
- return tmpDir;
57
- }
58
-
59
- function cleanupTempProject(tmpDir) {
60
- fs.rmSync(tmpDir, { recursive: true, force: true });
61
- }
62
-
63
- describe('generate-settings-json', () => {
64
- describe('readBoundaryConfig', () => {
65
- test('reads boundary config from core-config.yaml', () => {
66
- const tmpDir = createTempProject({
67
- frameworkProtection: true,
68
- protected: ['.sinapse-ai/core/**', 'bin/sinapse.js'],
69
- exceptions: ['.sinapse-ai/data/**'],
70
- });
71
-
72
- try {
73
- const config = readBoundaryConfig(tmpDir);
74
-
75
- expect(config.frameworkProtection).toBe(true);
76
- expect(config.protected).toContain('.sinapse-ai/core/**');
77
- expect(config.protected).toContain('bin/sinapse.js');
78
- expect(config.exceptions).toContain('.sinapse-ai/data/**');
79
- } finally {
80
- cleanupTempProject(tmpDir);
81
- }
82
- });
83
-
84
- test('throws when core-config.yaml not found', () => {
85
- expect(() => readBoundaryConfig('/nonexistent/path')).toThrow('core-config.yaml not found');
86
- });
87
-
88
- test('rejects path traversal in protected paths', () => {
89
- const tmpDir = createTempProject({
90
- frameworkProtection: true,
91
- protected: ['../../etc/passwd'],
92
- exceptions: [],
93
- });
94
-
95
- try {
96
- expect(() => readBoundaryConfig(tmpDir)).toThrow('Path traversal detected');
97
- } finally {
98
- cleanupTempProject(tmpDir);
99
- }
100
- });
101
-
102
- test('rejects absolute paths in boundary config', () => {
103
- const tmpDir = createTempProject({
104
- frameworkProtection: true,
105
- protected: ['/etc/passwd'],
106
- exceptions: [],
107
- });
108
-
109
- try {
110
- expect(() => readBoundaryConfig(tmpDir)).toThrow('Absolute path not allowed');
111
- } finally {
112
- cleanupTempProject(tmpDir);
113
- }
114
- });
115
- });
116
-
117
- describe('generatePermissions — frameworkProtection: true', () => {
118
- test('generates deny rules covering all protected paths', () => {
119
- const tmpDir = createTempProject({
120
- frameworkProtection: true,
121
- protected: ['.sinapse-ai/core/**', '.sinapse-ai/infrastructure/**', 'bin/sinapse.js'],
122
- exceptions: ['.sinapse-ai/data/**'],
123
- });
124
-
125
- // Create infrastructure dir (no expansion for non-core paths)
126
- fs.mkdirSync(path.join(tmpDir, '.sinapse-ai', 'infrastructure'), { recursive: true });
127
-
128
- try {
129
- const boundary = readBoundaryConfig(tmpDir);
130
- const permissions = generatePermissions(boundary, tmpDir);
131
-
132
- // Should have deny rules for core subdirs (events/**, utils/**, index.js) + infrastructure/** + bin/sinapse.js
133
- expect(permissions.deny.length).toBeGreaterThan(0);
134
-
135
- // Core expansion: events/**, utils/**, index.js → 6 deny rules (3 paths x 2 tools)
136
- expect(permissions.deny).toContain('Edit(.sinapse-ai/core/events/**)');
137
- expect(permissions.deny).toContain('Write(.sinapse-ai/core/events/**)');
138
- expect(permissions.deny).toContain('Edit(.sinapse-ai/core/utils/**)');
139
- expect(permissions.deny).toContain('Write(.sinapse-ai/core/utils/**)');
140
- expect(permissions.deny).toContain('Edit(.sinapse-ai/core/index.js)');
141
- expect(permissions.deny).toContain('Write(.sinapse-ai/core/index.js)');
142
-
143
- // Non-core paths stay as globs
144
- expect(permissions.deny).toContain('Edit(.sinapse-ai/infrastructure/**)');
145
- expect(permissions.deny).toContain('Write(.sinapse-ai/infrastructure/**)');
146
- expect(permissions.deny).toContain('Edit(bin/sinapse.js)');
147
- expect(permissions.deny).toContain('Write(bin/sinapse.js)');
148
-
149
- // Allow rules from exceptions
150
- expect(permissions.allow).toContain('Edit(.sinapse-ai/data/**)');
151
- expect(permissions.allow).toContain('Write(.sinapse-ai/data/**)');
152
- expect(permissions.allow).toContain('Read(.sinapse-ai/**)');
153
- } finally {
154
- cleanupTempProject(tmpDir);
155
- }
156
- });
157
-
158
- test('all 9 protected paths from core-config produce deny rules', () => {
159
- const projectRoot = path.resolve(__dirname, '../../../../..');
160
- const boundary = readBoundaryConfig(projectRoot);
161
- // Force frameworkProtection: true for this test (core-config may have it disabled for contributor mode)
162
- boundary.frameworkProtection = true;
163
- const permissions = generatePermissions(boundary, projectRoot);
164
-
165
- // Verify all 9 config paths are covered
166
- const protectedRoots = [
167
- '.sinapse-ai/core/',
168
- '.sinapse-ai/development/tasks/',
169
- '.sinapse-ai/development/templates/',
170
- '.sinapse-ai/development/checklists/',
171
- '.sinapse-ai/development/workflows/',
172
- '.sinapse-ai/infrastructure/',
173
- '.sinapse-ai/constitution.md',
174
- 'bin/sinapse.js',
175
- 'bin/sinapse-init.js',
176
- ];
177
-
178
- for (const root of protectedRoots) {
179
- const hasDenyRule = permissions.deny.some(r => r.includes(root));
180
- expect(hasDenyRule).toBe(true);
181
- }
182
-
183
- // Verify deny rules use only Edit and Write (no MultiEdit)
184
- for (const rule of permissions.deny) {
185
- expect(rule).toMatch(/^(Edit|Write)\(/);
186
- }
187
- });
188
- });
189
-
190
- describe('generatePermissions — frameworkProtection: false', () => {
191
- test('produces no boundary deny rules', () => {
192
- const boundary = {
193
- frameworkProtection: false,
194
- protected: ['.sinapse-ai/core/**', '.sinapse-ai/infrastructure/**'],
195
- exceptions: ['.sinapse-ai/data/**'],
196
- };
197
-
198
- const permissions = generatePermissions(boundary, '/tmp');
199
-
200
- expect(permissions.deny).toEqual([]);
201
- expect(permissions.allow).toEqual([]);
202
- });
203
- });
204
-
205
- describe('idempotency', () => {
206
- test('running generator twice produces identical output', () => {
207
- const tmpDir = createTempProject({
208
- frameworkProtection: true,
209
- protected: ['.sinapse-ai/core/**', 'bin/sinapse.js'],
210
- exceptions: ['.sinapse-ai/data/**'],
211
- });
212
-
213
- try {
214
- // First run
215
- generate(tmpDir);
216
- const firstRun = fs.readFileSync(path.join(tmpDir, '.claude', 'settings.json'), 'utf8');
217
-
218
- // Second run
219
- generate(tmpDir);
220
- const secondRun = fs.readFileSync(path.join(tmpDir, '.claude', 'settings.json'), 'utf8');
221
-
222
- expect(firstRun).toBe(secondRun);
223
- } finally {
224
- cleanupTempProject(tmpDir);
225
- }
226
- });
227
-
228
- test('JSON output is valid and parseable', () => {
229
- const tmpDir = createTempProject({
230
- frameworkProtection: true,
231
- protected: ['.sinapse-ai/core/**'],
232
- exceptions: ['.sinapse-ai/data/**'],
233
- });
234
-
235
- try {
236
- generate(tmpDir);
237
- const content = fs.readFileSync(path.join(tmpDir, '.claude', 'settings.json'), 'utf8');
238
- const parsed = JSON.parse(content);
239
-
240
- expect(parsed).toHaveProperty('permissions');
241
- expect(parsed.permissions).toHaveProperty('deny');
242
- expect(parsed.permissions).toHaveProperty('allow');
243
- expect(Array.isArray(parsed.permissions.deny)).toBe(true);
244
- expect(Array.isArray(parsed.permissions.allow)).toBe(true);
245
- } finally {
246
- cleanupTempProject(tmpDir);
247
- }
248
- });
249
- });
250
-
251
- describe('section preservation', () => {
252
- test('preserves user-set language key after generator run', () => {
253
- const tmpDir = createTempProject(
254
- {
255
- frameworkProtection: true,
256
- protected: ['bin/sinapse.js'],
257
- exceptions: [],
258
- },
259
- { language: 'pt', customSetting: true }
260
- );
261
-
262
- try {
263
- generate(tmpDir);
264
- const content = fs.readFileSync(path.join(tmpDir, '.claude', 'settings.json'), 'utf8');
265
- const parsed = JSON.parse(content);
266
-
267
- expect(parsed.language).toBe('pt');
268
- expect(parsed.customSetting).toBe(true);
269
- expect(parsed.permissions).toBeDefined();
270
- expect(parsed.permissions.deny.length).toBeGreaterThan(0);
271
- } finally {
272
- cleanupTempProject(tmpDir);
273
- }
274
- });
275
-
276
- test('frameworkProtection false preserves user settings and removes permissions', () => {
277
- const tmpDir = createTempProject(
278
- {
279
- frameworkProtection: false,
280
- protected: ['bin/sinapse.js'],
281
- exceptions: [],
282
- },
283
- { language: 'pt', permissions: { deny: ['old-rule'], allow: [] } }
284
- );
285
-
286
- try {
287
- generate(tmpDir);
288
- const content = fs.readFileSync(path.join(tmpDir, '.claude', 'settings.json'), 'utf8');
289
- const parsed = JSON.parse(content);
290
-
291
- expect(parsed.language).toBe('pt');
292
- expect(parsed.permissions).toBeUndefined();
293
- } finally {
294
- cleanupTempProject(tmpDir);
295
- }
296
- });
297
- });
298
-
299
- describe('CLI entry point', () => {
300
- test('module exports required functions', () => {
301
- expect(typeof generate).toBe('function');
302
- expect(typeof readBoundaryConfig).toBe('function');
303
- expect(typeof expandProtectedPaths).toBe('function');
304
- expect(typeof expandExceptionPaths).toBe('function');
305
- expect(typeof generatePermissions).toBe('function');
306
- expect(typeof writeSettingsJson).toBe('function');
307
- });
308
- });
309
- });
310
-
@@ -1,262 +0,0 @@
1
- 'use strict';
2
-
3
- /**
4
- * Stream B (Frente 4.2) — git-hooks-installer propagation tests.
5
- *
6
- * Verifies that installGitHooks():
7
- * - sets git core.hooksPath to the managed directory
8
- * - writes a Node `.js` pre-commit hook with a `#!/usr/bin/env node` shebang
9
- * - bundles the staged-secret-scan library so the hook is self-contained
10
- * - is idempotent (running twice does not duplicate / corrupt)
11
- * - detects husky and chains to it
12
- * - actually BLOCKS a commit with a staged secret, and ALLOWS a placeholder
13
- *
14
- * All git is driven with `git -C <dir> ...` against a tmpdir created with
15
- * fs.mkdtempSync(os.tmpdir()) so the global workspace-routing PreToolUse hook
16
- * (which rewrites bare `git init` / `mkdir` outside the Workspace) is never
17
- * triggered — we never run `mkdir` or `git init` as a bare cwd-based command.
18
- */
19
-
20
- const fs = require('fs');
21
- const os = require('os');
22
- const path = require('path');
23
-
24
- const { runSafe } = require('../../../../.sinapse-ai/core/utils/spawn-safe');
25
- const {
26
- installGitHooks,
27
- detectHusky,
28
- buildPreCommitHook,
29
- getCoreHooksPath,
30
- MANAGED_HOOKS_DIRNAME,
31
- MANAGED_MARKER,
32
- } = require('../../src/installer/git-hooks-installer');
33
-
34
- const GIT_IDENTITY_ARGS = [
35
- '-c', 'user.email=test@sinapse.local',
36
- '-c', 'user.name=SINAPSE Test',
37
- '-c', 'commit.gpgsign=false',
38
- ];
39
-
40
- /** Create an isolated temp dir (no mkdir command — pure Node fs). */
41
- function makeTempDir() {
42
- return fs.mkdtempSync(path.join(os.tmpdir(), 'snps-githooks-'));
43
- }
44
-
45
- /** Initialize a git repo at dir using `git -C` (avoids workspace-routing). */
46
- async function initRepo(dir) {
47
- const res = await runSafe('git', ['-C', dir, 'init', '-q']);
48
- if (!res.success) throw new Error(`git init failed: ${res.stderr}`);
49
- // Ensure a deterministic default branch / identity for commits.
50
- await runSafe('git', ['-C', dir, 'symbolic-ref', 'HEAD', 'refs/heads/main']);
51
- }
52
-
53
- /** Stage a file (write via Node fs, add via `git -C`). */
54
- async function writeAndStage(dir, relPath, content) {
55
- const full = path.join(dir, relPath);
56
- fs.mkdirSync(path.dirname(full), { recursive: true });
57
- fs.writeFileSync(full, content, 'utf8');
58
- const res = await runSafe('git', ['-C', dir, 'add', '--', relPath]);
59
- if (!res.success) throw new Error(`git add failed: ${res.stderr}`);
60
- }
61
-
62
- /** Attempt a commit; returns the runSafe result ({success, code, stderr}). */
63
- async function commit(dir, message) {
64
- return runSafe('git', [...GIT_IDENTITY_ARGS, '-C', dir, 'commit', '-m', message]);
65
- }
66
-
67
- function rmrf(dir) {
68
- try {
69
- fs.rmSync(dir, { recursive: true, force: true });
70
- } catch {
71
- /* best-effort cleanup */
72
- }
73
- }
74
-
75
- describe('git-hooks-installer (Stream B propagation)', () => {
76
- let gitAvailable = true;
77
-
78
- beforeAll(async () => {
79
- const res = await runSafe('git', ['--version']).catch(() => ({ success: false }));
80
- gitAvailable = res.success === true;
81
- });
82
-
83
- describe('buildPreCommitHook (pure)', () => {
84
- test('emits a Node hook with the node shebang and managed marker', () => {
85
- const content = buildPreCommitHook();
86
- expect(content.startsWith('#!/usr/bin/env node')).toBe(true);
87
- expect(content).toContain(MANAGED_MARKER);
88
- expect(content).toContain('staged-secret-scan.js');
89
- // Never a shell or python hook.
90
- expect(content).not.toMatch(/^#!\/usr\/bin\/env (sh|bash|python)/);
91
- });
92
-
93
- test('bakes an explicit chain path when provided', () => {
94
- const content = buildPreCommitHook({ chainHookRel: '.git/hooks-old/pre-commit' });
95
- expect(content).toContain('.git/hooks-old/pre-commit');
96
- });
97
- });
98
-
99
- describe('detectHusky', () => {
100
- test('reports false when no .husky/pre-commit exists', () => {
101
- const dir = makeTempDir();
102
- try {
103
- expect(detectHusky(dir).hasHusky).toBe(false);
104
- } finally {
105
- rmrf(dir);
106
- }
107
- });
108
-
109
- test('reports true when .husky/pre-commit exists', () => {
110
- const dir = makeTempDir();
111
- try {
112
- const huskyDir = path.join(dir, '.husky');
113
- fs.mkdirSync(huskyDir, { recursive: true });
114
- fs.writeFileSync(path.join(huskyDir, 'pre-commit'), '#!/usr/bin/env sh\nexit 0\n');
115
- expect(detectHusky(dir).hasHusky).toBe(true);
116
- } finally {
117
- rmrf(dir);
118
- }
119
- });
120
- });
121
-
122
- describe('installGitHooks (filesystem + git config)', () => {
123
- test('writes a managed Node pre-commit and sets core.hooksPath', async () => {
124
- if (!gitAvailable) return;
125
- const dir = makeTempDir();
126
- try {
127
- await initRepo(dir);
128
- const result = await installGitHooks({ projectDir: dir });
129
-
130
- expect(result.success).toBe(true);
131
- expect(result.coreHooksPathSet).toBe(true);
132
-
133
- const hookPath = path.join(dir, MANAGED_HOOKS_DIRNAME, 'pre-commit');
134
- expect(fs.existsSync(hookPath)).toBe(true);
135
-
136
- const hookContent = fs.readFileSync(hookPath, 'utf8');
137
- expect(hookContent.startsWith('#!/usr/bin/env node')).toBe(true);
138
- expect(hookContent).toContain(MANAGED_MARKER);
139
-
140
- // The scanner lib is bundled next to the hook.
141
- const libScanner = path.join(dir, MANAGED_HOOKS_DIRNAME, 'lib', 'staged-secret-scan.js');
142
- expect(fs.existsSync(libScanner)).toBe(true);
143
-
144
- // git config points at the managed dir (forward-slash, project-relative).
145
- const configured = await getCoreHooksPath(dir);
146
- expect(configured).toBe('.sinapse-ai/git-hooks');
147
- } finally {
148
- rmrf(dir);
149
- }
150
- });
151
-
152
- test('is idempotent — running twice keeps a single managed hook', async () => {
153
- if (!gitAvailable) return;
154
- const dir = makeTempDir();
155
- try {
156
- await initRepo(dir);
157
- await installGitHooks({ projectDir: dir });
158
- const firstContent = fs.readFileSync(path.join(dir, MANAGED_HOOKS_DIRNAME, 'pre-commit'), 'utf8');
159
-
160
- const second = await installGitHooks({ projectDir: dir });
161
- expect(second.success).toBe(true);
162
-
163
- const secondContent = fs.readFileSync(path.join(dir, MANAGED_HOOKS_DIRNAME, 'pre-commit'), 'utf8');
164
- expect(secondContent).toBe(firstContent);
165
-
166
- // Exactly one pre-commit file in the managed dir (no .bak duplicates).
167
- const entries = fs.readdirSync(path.join(dir, MANAGED_HOOKS_DIRNAME));
168
- expect(entries.filter((e) => e.startsWith('pre-commit'))).toEqual(['pre-commit']);
169
-
170
- // core.hooksPath is set once, not appended.
171
- expect(await getCoreHooksPath(dir)).toBe('.sinapse-ai/git-hooks');
172
- } finally {
173
- rmrf(dir);
174
- }
175
- });
176
-
177
- test('detects husky and chains to it', async () => {
178
- if (!gitAvailable) return;
179
- const dir = makeTempDir();
180
- try {
181
- await initRepo(dir);
182
- const huskyDir = path.join(dir, '.husky');
183
- fs.mkdirSync(huskyDir, { recursive: true });
184
- fs.writeFileSync(path.join(huskyDir, 'pre-commit'), '#!/usr/bin/env sh\nexit 0\n');
185
-
186
- const result = await installGitHooks({ projectDir: dir });
187
- expect(result.success).toBe(true);
188
- expect(result.huskyDetected).toBe(true);
189
-
190
- const hookContent = fs.readFileSync(path.join(dir, MANAGED_HOOKS_DIRNAME, 'pre-commit'), 'utf8');
191
- // The runtime chain auto-detects .husky/pre-commit.
192
- expect(hookContent).toContain('.husky');
193
- expect(hookContent).toContain('pre-commit');
194
- } finally {
195
- rmrf(dir);
196
- }
197
- });
198
-
199
- test('rejects a missing projectDir', async () => {
200
- const result = await installGitHooks({});
201
- expect(result.success).toBe(false);
202
- expect(result.error).toMatch(/projectDir/);
203
- });
204
- });
205
-
206
- describe('end-to-end commit gating', () => {
207
- test('BLOCKS a commit that stages a real secret', async () => {
208
- if (!gitAvailable) return;
209
- const dir = makeTempDir();
210
- try {
211
- await initRepo(dir);
212
- await installGitHooks({ projectDir: dir });
213
-
214
- // AWS access key — matches the scanner's AKIA pattern.
215
- await writeAndStage(dir, 'config.js', 'const k = "AKIAIOSFODNN7EXAMPLE";\n');
216
-
217
- const res = await commit(dir, 'should be blocked');
218
- expect(res.success).toBe(false);
219
- expect((res.stderr || '') + (res.stdout || '')).toMatch(/Secret Scan|blocked/i);
220
- } finally {
221
- rmrf(dir);
222
- }
223
- });
224
-
225
- test('ALLOWS a commit of a placeholder .env.example', async () => {
226
- if (!gitAvailable) return;
227
- const dir = makeTempDir();
228
- try {
229
- await initRepo(dir);
230
- await installGitHooks({ projectDir: dir });
231
-
232
- await writeAndStage(dir, '.env.example', 'STRIPE_KEY=your-key-here\nDB_PASSWORD=changeme\n');
233
-
234
- const res = await commit(dir, 'add example env');
235
- expect(res.success).toBe(true);
236
- } finally {
237
- rmrf(dir);
238
- }
239
- });
240
-
241
- test('fail-CLOSED: a broken scanner lib blocks the commit', async () => {
242
- if (!gitAvailable) return;
243
- const dir = makeTempDir();
244
- try {
245
- await initRepo(dir);
246
- await installGitHooks({ projectDir: dir });
247
-
248
- // Corrupt the bundled scanner so require() throws inside the hook.
249
- const libScanner = path.join(dir, MANAGED_HOOKS_DIRNAME, 'lib', 'staged-secret-scan.js');
250
- fs.writeFileSync(libScanner, 'throw new Error("corrupted scanner");\n', 'utf8');
251
-
252
- await writeAndStage(dir, 'safe.txt', 'totally harmless content\n');
253
-
254
- const res = await commit(dir, 'should still be blocked');
255
- expect(res.success).toBe(false);
256
- expect((res.stderr || '') + (res.stdout || '')).toMatch(/fail-closed|blocking commit/i);
257
- } finally {
258
- rmrf(dir);
259
- }
260
- });
261
- });
262
- });