merge main

This commit is contained in:
Alex Newman
2026-01-07 16:53:54 -05:00
2 changed files with 214 additions and 0 deletions
+79
View File
@@ -0,0 +1,79 @@
# Version Consistency Fix (Issue #XXX)
## Problem
Version mismatch between plugin and worker caused infinite restart loop:
- Plugin version: 9.0.0 (from plugin/.claude-plugin/plugin.json)
- Worker binary version: 8.5.9 (hardcoded in bundled worker-service.cjs)
This triggered the auto-restart mechanism on every hook call, which killed the SDK generator before it could complete the Claude API call to generate observations. Result: 0 observations were ever saved to the database despite hooks firing successfully.
## Root Cause
The `plugin/package.json` file had version `8.5.10` instead of `9.0.0`. When the project was last built, the build script correctly injected the version from root `package.json` into the bundled worker service. However, the `plugin/package.json` was manually created/edited and fell out of sync.
At runtime:
1. Worker service reads version from `~/.claude/plugins/marketplaces/thedotmack/package.json` → gets `8.5.10`
2. Running worker returns built-in version via `/api/version` → returns `8.5.9` (from old build)
3. Version check in `worker-service.ts` start command detects mismatch
4. Auto-restart triggered on every hook call
5. Observations never saved
## Solution
1. Updated `plugin/package.json` from version `8.5.10` to `9.0.0`
2. Rebuilt all hooks and worker service to inject correct version (`9.0.0`) into bundled artifacts
3. Added comprehensive test suite to prevent future version mismatches
## Verification
All versions now match:
```
Root package.json: 9.0.0 ✓
plugin/package.json: 9.0.0 ✓
plugin.json: 9.0.0 ✓
marketplace.json: 9.0.0 ✓
worker-service.cjs: 9.0.0 ✓
```
## Prevention
To prevent this issue in the future:
1. **Automated Build Process**: The `scripts/build-hooks.js` now regenerates `plugin/package.json` automatically with the correct version from root `package.json`
2. **Version Consistency Tests**: Added `tests/infrastructure/version-consistency.test.ts` to verify all version sources match
3. **Version Management Best Practices**:
- NEVER manually edit `plugin/package.json` - it's auto-generated during build
- Always update version in root `package.json` only
- Run `npm run build` after version changes
- The build script will sync the version to all necessary locations
## Files Changed
- `plugin/package.json` - Updated version from 8.5.10 to 9.0.0
- `plugin/scripts/worker-service.cjs` - Rebuilt with version 9.0.0 injected
- `plugin/scripts/mcp-server.cjs` - Rebuilt with version 9.0.0 injected
- `plugin/scripts/*.js` (hooks) - Rebuilt with version 9.0.0 injected
- `tests/infrastructure/version-consistency.test.ts` - New test suite
## Testing
Run the version consistency test:
```bash
npm run test:infra
```
Or manually verify:
```bash
node -e "
const fs = require('fs');
const rootPkg = JSON.parse(fs.readFileSync('package.json', 'utf-8'));
const pluginPkg = JSON.parse(fs.readFileSync('plugin/package.json', 'utf-8'));
const workerContent = fs.readFileSync('plugin/scripts/worker-service.cjs', 'utf-8');
const workerMatch = workerContent.match(/Bre=\"([0-9.]+)\"/);
console.log('Root:', rootPkg.version);
console.log('Plugin:', pluginPkg.version);
console.log('Worker:', workerMatch ? workerMatch[1] : 'NOT_FOUND');
"
```
## Related Code Locations
- **Version Injection**: `scripts/build-hooks.js` line 43-45, 105, 130, 155, 178
- **Version Check**: `src/services/infrastructure/HealthMonitor.ts` line 133-143
- **Auto-Restart Logic**: `src/services/worker-service.ts` line 627-645
- **Runtime Version Read**: `src/shared/worker-utils.ts` line 73-76, 82-91
@@ -0,0 +1,135 @@
import { describe, it, expect } from 'bun:test';
import { readFileSync, existsSync } from 'fs';
import path from 'path';
import { fileURLToPath } from 'url';
const __dirname = path.dirname(fileURLToPath(import.meta.url));
const projectRoot = path.resolve(__dirname, '../..');
/**
* Test suite to ensure version consistency across all package.json files
* and built artifacts.
*
* This prevents the infinite restart loop issue where:
* - Plugin reads version from plugin/package.json
* - Worker returns built-in version from bundled code
* - Mismatch triggers restart on every hook call
*/
describe('Version Consistency', () => {
let rootVersion: string;
it('should read version from root package.json', () => {
const packageJsonPath = path.join(projectRoot, 'package.json');
expect(existsSync(packageJsonPath)).toBe(true);
const packageJson = JSON.parse(readFileSync(packageJsonPath, 'utf-8'));
expect(packageJson.version).toBeDefined();
expect(packageJson.version).toMatch(/^\d+\.\d+\.\d+$/);
rootVersion = packageJson.version;
});
it('should have matching version in plugin/package.json', () => {
const pluginPackageJsonPath = path.join(projectRoot, 'plugin/package.json');
expect(existsSync(pluginPackageJsonPath)).toBe(true);
const pluginPackageJson = JSON.parse(readFileSync(pluginPackageJsonPath, 'utf-8'));
expect(pluginPackageJson.version).toBe(rootVersion);
});
it('should have matching version in plugin/.claude-plugin/plugin.json', () => {
const pluginJsonPath = path.join(projectRoot, 'plugin/.claude-plugin/plugin.json');
expect(existsSync(pluginJsonPath)).toBe(true);
const pluginJson = JSON.parse(readFileSync(pluginJsonPath, 'utf-8'));
expect(pluginJson.version).toBe(rootVersion);
});
it('should have matching version in .claude-plugin/marketplace.json', () => {
const marketplaceJsonPath = path.join(projectRoot, '.claude-plugin/marketplace.json');
expect(existsSync(marketplaceJsonPath)).toBe(true);
const marketplaceJson = JSON.parse(readFileSync(marketplaceJsonPath, 'utf-8'));
expect(marketplaceJson.plugins).toBeDefined();
expect(marketplaceJson.plugins.length).toBeGreaterThan(0);
const claudeMemPlugin = marketplaceJson.plugins.find((p: any) => p.name === 'claude-mem');
expect(claudeMemPlugin).toBeDefined();
expect(claudeMemPlugin.version).toBe(rootVersion);
});
it('should have version injected into built worker-service.cjs', () => {
const workerServicePath = path.join(projectRoot, 'plugin/scripts/worker-service.cjs');
// Skip if file doesn't exist (e.g., before first build)
if (!existsSync(workerServicePath)) {
console.log('⚠️ worker-service.cjs not found - run npm run build first');
return;
}
const workerServiceContent = readFileSync(workerServicePath, 'utf-8');
// The build script injects version via esbuild define:
// define: { '__DEFAULT_PACKAGE_VERSION__': `"${version}"` }
// This becomes: const BUILT_IN_VERSION = "9.0.0" (or minified: Bre="9.0.0")
// Check for the version string in the minified code
const versionPattern = new RegExp(`"${rootVersion.replace(/\./g, '\\.')}"`, 'g');
const matches = workerServiceContent.match(versionPattern);
expect(matches).toBeTruthy();
expect(matches!.length).toBeGreaterThan(0);
});
it('should have version injected into built mcp-server.cjs', () => {
const mcpServerPath = path.join(projectRoot, 'plugin/scripts/mcp-server.cjs');
// Skip if file doesn't exist (e.g., before first build)
if (!existsSync(mcpServerPath)) {
console.log('⚠️ mcp-server.cjs not found - run npm run build first');
return;
}
const mcpServerContent = readFileSync(mcpServerPath, 'utf-8');
// Check for the version string in the minified code
const versionPattern = new RegExp(`"${rootVersion.replace(/\./g, '\\.')}"`, 'g');
const matches = mcpServerContent.match(versionPattern);
expect(matches).toBeTruthy();
expect(matches!.length).toBeGreaterThan(0);
});
it('should validate version format is semver compliant', () => {
// Ensure version follows semantic versioning: MAJOR.MINOR.PATCH
expect(rootVersion).toMatch(/^\d+\.\d+\.\d+$/);
const [major, minor, patch] = rootVersion.split('.').map(Number);
expect(major).toBeGreaterThanOrEqual(0);
expect(minor).toBeGreaterThanOrEqual(0);
expect(patch).toBeGreaterThanOrEqual(0);
});
});
/**
* Additional test to ensure build script properly reads and injects version
*/
describe('Build Script Version Handling', () => {
it('should read version from package.json in build-hooks.js', () => {
const buildScriptPath = path.join(projectRoot, 'scripts/build-hooks.js');
expect(existsSync(buildScriptPath)).toBe(true);
const buildScriptContent = readFileSync(buildScriptPath, 'utf-8');
// Verify build script reads from package.json
expect(buildScriptContent).toContain("readFileSync('package.json'");
expect(buildScriptContent).toContain('packageJson.version');
// Verify it generates plugin/package.json with the version
expect(buildScriptContent).toContain('version: version');
// Verify it injects version into esbuild define
expect(buildScriptContent).toContain('__DEFAULT_PACKAGE_VERSION__');
expect(buildScriptContent).toContain('`"${version}"`');
});
});