mirror of
https://github.com/affaan-m/everything-claude-code.git
synced 2026-04-15 22:43:28 +08:00
fix: unblock urgent install and gateguard regressions
This commit is contained in:
@@ -14,6 +14,7 @@ const stateDir = externalStateDir || fs.mkdtempSync(path.join(tmpRoot, 'gateguar
|
||||
// Use a fixed session ID so test process and spawned hook process share the same state file
|
||||
const TEST_SESSION_ID = 'gateguard-test-session';
|
||||
const stateFile = path.join(stateDir, `state-${TEST_SESSION_ID}.json`);
|
||||
const READ_HEARTBEAT_MS = 60 * 1000;
|
||||
|
||||
function test(name, fn) {
|
||||
try {
|
||||
@@ -29,11 +30,15 @@ function test(name, fn) {
|
||||
|
||||
function clearState() {
|
||||
try {
|
||||
if (fs.existsSync(stateFile)) {
|
||||
fs.unlinkSync(stateFile);
|
||||
if (fs.existsSync(stateDir)) {
|
||||
for (const entry of fs.readdirSync(stateDir)) {
|
||||
if (entry.startsWith('state-') && entry.endsWith('.json')) {
|
||||
fs.unlinkSync(path.join(stateDir, entry));
|
||||
}
|
||||
}
|
||||
}
|
||||
} catch (err) {
|
||||
console.error(` [clearState] failed to remove ${stateFile}: ${err.message}`);
|
||||
console.error(` [clearState] failed to remove state files in ${stateDir}: ${err.message}`);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -363,18 +368,45 @@ function runTests() {
|
||||
}
|
||||
})) passed++; else failed++;
|
||||
|
||||
// --- Test 12: reads refresh active session state ---
|
||||
// --- Test 12: hot-path reads do not rewrite state within heartbeat ---
|
||||
clearState();
|
||||
if (test('touches last_active on read so active sessions do not age out', () => {
|
||||
const staleButActive = Date.now() - (29 * 60 * 1000);
|
||||
if (test('does not rewrite state on hot-path reads within heartbeat window', () => {
|
||||
const recentlyActive = Date.now() - (READ_HEARTBEAT_MS - 10 * 1000);
|
||||
writeState({
|
||||
checked: ['/src/keep-alive.js'],
|
||||
last_active: recentlyActive
|
||||
});
|
||||
|
||||
const beforeStat = fs.statSync(stateFile);
|
||||
const before = JSON.parse(fs.readFileSync(stateFile, 'utf8'));
|
||||
assert.strictEqual(before.last_active, recentlyActive, 'seed state should use the expected timestamp');
|
||||
|
||||
const result = runHook({
|
||||
tool_name: 'Edit',
|
||||
tool_input: { file_path: '/src/keep-alive.js', old_string: 'a', new_string: 'b' }
|
||||
});
|
||||
const output = parseOutput(result.stdout);
|
||||
assert.ok(output, 'should produce valid JSON output');
|
||||
if (output.hookSpecificOutput) {
|
||||
assert.notStrictEqual(output.hookSpecificOutput.permissionDecision, 'deny',
|
||||
'already-checked file should still be allowed');
|
||||
}
|
||||
|
||||
const afterStat = fs.statSync(stateFile);
|
||||
const after = JSON.parse(fs.readFileSync(stateFile, 'utf8'));
|
||||
assert.strictEqual(after.last_active, recentlyActive, 'read should not touch last_active within heartbeat');
|
||||
assert.strictEqual(afterStat.mtimeMs, beforeStat.mtimeMs, 'read should not rewrite the state file within heartbeat');
|
||||
})) passed++; else failed++;
|
||||
|
||||
// --- Test 13: reads refresh stale active state after heartbeat ---
|
||||
clearState();
|
||||
if (test('refreshes last_active after heartbeat elapses', () => {
|
||||
const staleButActive = Date.now() - (READ_HEARTBEAT_MS + 5 * 1000);
|
||||
writeState({
|
||||
checked: ['/src/keep-alive.js'],
|
||||
last_active: staleButActive
|
||||
});
|
||||
|
||||
const before = JSON.parse(fs.readFileSync(stateFile, 'utf8'));
|
||||
assert.strictEqual(before.last_active, staleButActive, 'seed state should use the expected timestamp');
|
||||
|
||||
const result = runHook({
|
||||
tool_name: 'Edit',
|
||||
tool_input: { file_path: '/src/keep-alive.js', old_string: 'a', new_string: 'b' }
|
||||
@@ -387,10 +419,10 @@ function runTests() {
|
||||
}
|
||||
|
||||
const after = JSON.parse(fs.readFileSync(stateFile, 'utf8'));
|
||||
assert.ok(after.last_active > staleButActive, 'successful reads should refresh last_active');
|
||||
assert.ok(after.last_active > staleButActive, 'read should refresh last_active after heartbeat');
|
||||
})) passed++; else failed++;
|
||||
|
||||
// --- Test 13: pruning preserves routine bash gate marker ---
|
||||
// --- Test 14: pruning preserves routine bash gate marker ---
|
||||
clearState();
|
||||
if (test('preserves __bash_session__ when pruning oversized state', () => {
|
||||
const checked = ['__bash_session__'];
|
||||
@@ -419,6 +451,71 @@ function runTests() {
|
||||
assert.ok(persisted.checked.length <= 500, 'pruned state should still honor the checked-entry cap');
|
||||
})) passed++; else failed++;
|
||||
|
||||
// --- Test 15: raw input session IDs provide stable retry state without env vars ---
|
||||
clearState();
|
||||
if (test('uses raw input session_id when hook env vars are missing', () => {
|
||||
const input = {
|
||||
session_id: 'raw-session-1234',
|
||||
tool_name: 'Bash',
|
||||
tool_input: { command: 'ls -la' }
|
||||
};
|
||||
|
||||
const first = runBashHook(input, {
|
||||
CLAUDE_SESSION_ID: '',
|
||||
ECC_SESSION_ID: '',
|
||||
});
|
||||
const firstOutput = parseOutput(first.stdout);
|
||||
assert.strictEqual(firstOutput.hookSpecificOutput.permissionDecision, 'deny');
|
||||
|
||||
const second = runBashHook(input, {
|
||||
CLAUDE_SESSION_ID: '',
|
||||
ECC_SESSION_ID: '',
|
||||
});
|
||||
const secondOutput = parseOutput(second.stdout);
|
||||
if (secondOutput.hookSpecificOutput) {
|
||||
assert.notStrictEqual(secondOutput.hookSpecificOutput.permissionDecision, 'deny',
|
||||
'retry should be allowed when raw session_id is stable');
|
||||
} else {
|
||||
assert.strictEqual(secondOutput.tool_name, 'Bash');
|
||||
}
|
||||
})) passed++; else failed++;
|
||||
|
||||
// --- Test 16: allows Claude settings edits so the hook can be disabled safely ---
|
||||
clearState();
|
||||
if (test('allows edits to .claude/settings.json without gating', () => {
|
||||
const input = {
|
||||
tool_name: 'Edit',
|
||||
tool_input: { file_path: '/workspace/app/.claude/settings.json', old_string: '{}', new_string: '{"hooks":[]}' }
|
||||
};
|
||||
const result = runHook(input);
|
||||
const output = parseOutput(result.stdout);
|
||||
assert.ok(output, 'should produce valid JSON output');
|
||||
if (output.hookSpecificOutput) {
|
||||
assert.notStrictEqual(output.hookSpecificOutput.permissionDecision, 'deny',
|
||||
'settings edits must not be blocked by gateguard');
|
||||
} else {
|
||||
assert.strictEqual(output.tool_name, 'Edit');
|
||||
}
|
||||
})) passed++; else failed++;
|
||||
|
||||
// --- Test 17: allows read-only git introspection without first-bash gating ---
|
||||
clearState();
|
||||
if (test('allows read-only git status without first-bash gating', () => {
|
||||
const input = {
|
||||
tool_name: 'Bash',
|
||||
tool_input: { command: 'git status --short' }
|
||||
};
|
||||
const result = runBashHook(input);
|
||||
const output = parseOutput(result.stdout);
|
||||
assert.ok(output, 'should produce valid JSON output');
|
||||
if (output.hookSpecificOutput) {
|
||||
assert.notStrictEqual(output.hookSpecificOutput.permissionDecision, 'deny',
|
||||
'read-only git introspection should not be blocked');
|
||||
} else {
|
||||
assert.strictEqual(output.tool_name, 'Bash');
|
||||
}
|
||||
})) passed++; else failed++;
|
||||
|
||||
// Cleanup only the temp directory created by this test file.
|
||||
if (!externalStateDir) {
|
||||
try {
|
||||
|
||||
@@ -124,11 +124,11 @@ function runTests() {
|
||||
);
|
||||
assert.ok(
|
||||
plan.operations.some(operation => (
|
||||
operation.sourceRelativePath === 'rules/common/agents.md'
|
||||
operation.sourceRelativePath === '.cursor/rules/common-agents.md'
|
||||
&& operation.destinationPath === path.join(projectRoot, '.cursor', 'rules', 'common-agents.mdc')
|
||||
&& operation.strategy === 'flatten-copy'
|
||||
)),
|
||||
'Should produce Cursor .mdc rules while preferring rules-core over duplicate platform copies'
|
||||
'Should produce Cursor .mdc rules while preferring native Cursor platform copies over duplicate rules-core files'
|
||||
);
|
||||
})) passed++; else failed++;
|
||||
|
||||
|
||||
@@ -94,14 +94,14 @@ function runTests() {
|
||||
normalizedRelativePath(operation.sourceRelativePath) === '.cursor/hooks.json'
|
||||
));
|
||||
const preserved = plan.operations.find(operation => (
|
||||
normalizedRelativePath(operation.sourceRelativePath) === 'rules/common/coding-style.md'
|
||||
normalizedRelativePath(operation.sourceRelativePath) === '.cursor/rules/common-coding-style.md'
|
||||
));
|
||||
|
||||
assert.ok(hooksJson, 'Should preserve non-rule Cursor platform config files');
|
||||
assert.strictEqual(hooksJson.strategy, 'preserve-relative-path');
|
||||
assert.strictEqual(hooksJson.destinationPath, path.join(projectRoot, '.cursor', 'hooks.json'));
|
||||
|
||||
assert.ok(preserved, 'Should include flattened rules scaffold operations');
|
||||
assert.ok(preserved, 'Should include flattened Cursor rule scaffold operations');
|
||||
assert.strictEqual(preserved.strategy, 'flatten-copy');
|
||||
assert.strictEqual(
|
||||
preserved.destinationPath,
|
||||
@@ -236,8 +236,8 @@ function runTests() {
|
||||
assert.strictEqual(commonAgentsDestinations.length, 1, 'Should keep only one common-agents.mdc operation');
|
||||
assert.strictEqual(
|
||||
normalizedRelativePath(commonAgentsDestinations[0].sourceRelativePath),
|
||||
'rules/common/agents.md',
|
||||
'Should prefer rules-core when cursor platform rules would collide'
|
||||
'.cursor/rules/common-agents.md',
|
||||
'Should prefer native .cursor/rules content when cursor platform rules would collide'
|
||||
);
|
||||
})) passed++; else failed++;
|
||||
|
||||
|
||||
@@ -79,6 +79,28 @@ function assertSafeRepoRelativePath(relativePath, label) {
|
||||
);
|
||||
}
|
||||
|
||||
function collectMarkdownFiles(rootPath) {
|
||||
if (!fs.existsSync(rootPath)) {
|
||||
return [];
|
||||
}
|
||||
|
||||
const stat = fs.statSync(rootPath);
|
||||
if (stat.isFile()) {
|
||||
return rootPath.endsWith('.md') ? [rootPath] : [];
|
||||
}
|
||||
|
||||
const files = [];
|
||||
for (const entry of fs.readdirSync(rootPath, { withFileTypes: true })) {
|
||||
const nextPath = path.join(rootPath, entry.name);
|
||||
if (entry.isDirectory()) {
|
||||
files.push(...collectMarkdownFiles(nextPath));
|
||||
} else if (entry.isFile() && nextPath.endsWith('.md')) {
|
||||
files.push(nextPath);
|
||||
}
|
||||
}
|
||||
return files;
|
||||
}
|
||||
|
||||
const rootPackage = loadJsonObject(packageJsonPath, 'package.json');
|
||||
const packageLock = loadJsonObject(packageLockPath, 'package-lock.json');
|
||||
const opencodePackageLock = loadJsonObject(opencodePackageLockPath, '.opencode/package-lock.json');
|
||||
@@ -454,6 +476,29 @@ test('README version row matches package.json', () => {
|
||||
assert.strictEqual(match[1], expectedVersion);
|
||||
});
|
||||
|
||||
test('user-facing docs do not reference deprecated ecc@ecc plugin identifier', () => {
|
||||
const markdownFiles = [
|
||||
path.join(repoRoot, 'README.md'),
|
||||
path.join(repoRoot, 'README.zh-CN.md'),
|
||||
path.join(repoRoot, 'skills', 'configure-ecc', 'SKILL.md'),
|
||||
...collectMarkdownFiles(path.join(repoRoot, 'docs')),
|
||||
];
|
||||
|
||||
const offenders = [];
|
||||
for (const filePath of markdownFiles) {
|
||||
const source = fs.readFileSync(filePath, 'utf8');
|
||||
if (source.includes('ecc@ecc')) {
|
||||
offenders.push(path.relative(repoRoot, filePath));
|
||||
}
|
||||
}
|
||||
|
||||
assert.deepStrictEqual(
|
||||
offenders,
|
||||
[],
|
||||
`Deprecated ecc@ecc identifier must not appear in user-facing docs: ${offenders.join(', ')}`,
|
||||
);
|
||||
});
|
||||
|
||||
test('docs/zh-CN/README.md version row matches package.json', () => {
|
||||
const readme = fs.readFileSync(zhCnReadmePath, 'utf8');
|
||||
const match = readme.match(/^\| \*\*版本\*\* \| 插件 \| 插件 \| 参考配置 \| ([0-9][0-9.]*) \|$/m);
|
||||
|
||||
@@ -132,6 +132,39 @@ function runTests() {
|
||||
}
|
||||
})) passed++; else failed++;
|
||||
|
||||
if (test('detects marketplace-installed Claude plugins under marketplaces/', () => {
|
||||
const homeDir = createTempDir('harness-audit-marketplace-home-');
|
||||
const projectRoot = createTempDir('harness-audit-marketplace-project-');
|
||||
|
||||
try {
|
||||
fs.mkdirSync(path.join(homeDir, '.claude', 'plugins', 'marketplaces', 'everything-claude-code', '.claude-plugin'), { recursive: true });
|
||||
fs.writeFileSync(
|
||||
path.join(homeDir, '.claude', 'plugins', 'marketplaces', 'everything-claude-code', '.claude-plugin', 'plugin.json'),
|
||||
JSON.stringify({ name: 'everything-claude-code' }, null, 2)
|
||||
);
|
||||
|
||||
fs.mkdirSync(path.join(projectRoot, '.github', 'workflows'), { recursive: true });
|
||||
fs.mkdirSync(path.join(projectRoot, 'tests'), { recursive: true });
|
||||
fs.mkdirSync(path.join(projectRoot, '.claude'), { recursive: true });
|
||||
fs.writeFileSync(path.join(projectRoot, 'AGENTS.md'), '# Project instructions\n');
|
||||
fs.writeFileSync(path.join(projectRoot, '.mcp.json'), JSON.stringify({ mcpServers: {} }, null, 2));
|
||||
fs.writeFileSync(path.join(projectRoot, '.gitignore'), 'node_modules\n.env\n');
|
||||
fs.writeFileSync(path.join(projectRoot, '.github', 'workflows', 'ci.yml'), 'name: ci\n');
|
||||
fs.writeFileSync(path.join(projectRoot, 'tests', 'app.test.js'), 'test placeholder\n');
|
||||
fs.writeFileSync(path.join(projectRoot, '.claude', 'settings.json'), JSON.stringify({ hooks: ['PreToolUse'] }, null, 2));
|
||||
fs.writeFileSync(
|
||||
path.join(projectRoot, 'package.json'),
|
||||
JSON.stringify({ name: 'consumer-project', scripts: { test: 'node tests/app.test.js' } }, null, 2)
|
||||
);
|
||||
|
||||
const parsed = JSON.parse(run(['repo', '--format', 'json'], { cwd: projectRoot, homeDir }));
|
||||
assert.ok(parsed.checks.some(check => check.id === 'consumer-plugin-install' && check.pass));
|
||||
} finally {
|
||||
cleanup(homeDir);
|
||||
cleanup(projectRoot);
|
||||
}
|
||||
})) passed++; else failed++;
|
||||
|
||||
console.log(`\nResults: Passed: ${passed}, Failed: ${failed}`);
|
||||
process.exit(failed > 0 ? 1 : 0);
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user