writer-lock.test.ts 2.9 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687
  1. /**
  2. * Project writer lock (#1740) — unit coverage for acquire / re-entrant /
  3. * stale-dead-pid / live-holder refusal.
  4. */
  5. import { afterEach, describe, expect, it } from 'vitest';
  6. import * as fs from 'fs';
  7. import * as os from 'os';
  8. import * as path from 'path';
  9. import {
  10. decodeWriterLockInfo,
  11. getWriterPidPath,
  12. releaseWriterLock,
  13. tryAcquireWriterLock,
  14. writerLockHeldMessage,
  15. } from '../src/mcp/writer-lock';
  16. describe('writer lock (#1740)', () => {
  17. let dir: string;
  18. afterEach(() => {
  19. if (dir) {
  20. releaseWriterLock(dir);
  21. try { fs.rmSync(dir, { recursive: true, force: true }); } catch { /* ignore */ }
  22. }
  23. });
  24. function makeProject(): string {
  25. dir = fs.mkdtempSync(path.join(os.tmpdir(), 'cg1740-lock-'));
  26. fs.mkdirSync(path.join(dir, '.codegraph'), { recursive: true });
  27. return dir;
  28. }
  29. it('acquires and releases writer.pid', () => {
  30. const root = makeProject();
  31. const r = tryAcquireWriterLock(root, 'direct');
  32. expect(r.kind).toBe('acquired');
  33. expect(fs.existsSync(getWriterPidPath(root))).toBe(true);
  34. const info = decodeWriterLockInfo(fs.readFileSync(getWriterPidPath(root), 'utf8'));
  35. expect(info?.pid).toBe(process.pid);
  36. expect(info?.mode).toBe('direct');
  37. releaseWriterLock(root);
  38. expect(fs.existsSync(getWriterPidPath(root))).toBe(false);
  39. });
  40. it('is re-entrant for the same pid', () => {
  41. const root = makeProject();
  42. expect(tryAcquireWriterLock(root, 'daemon').kind).toBe('acquired');
  43. const again = tryAcquireWriterLock(root, 'fallback');
  44. expect(again.kind).toBe('acquired');
  45. releaseWriterLock(root);
  46. });
  47. it('reports taken when a live foreign pid holds the lock', () => {
  48. const root = makeProject();
  49. // Use our own pid first, then overwrite with a fake live-looking pid by
  50. // writing a pid that is alive: process.pid of this test — simulate foreign
  51. // by writing a different alive pid. On Linux, PID 1 is almost always alive.
  52. fs.writeFileSync(
  53. getWriterPidPath(root),
  54. JSON.stringify({ pid: 1, mode: 'direct', startedAt: Date.now() }) + '\n',
  55. { flag: 'wx' },
  56. );
  57. const r = tryAcquireWriterLock(root, 'direct');
  58. expect(r.kind).toBe('taken');
  59. if (r.kind === 'taken') {
  60. expect(r.existing?.pid).toBe(1);
  61. const msg = writerLockHeldMessage(r.existing, r.pidPath);
  62. expect(msg).toMatch(/writer lock held/i);
  63. expect(msg).toMatch(/CODEGRAPH_NO_DAEMON/);
  64. expect(msg).toMatch(/daemon stop/);
  65. }
  66. });
  67. it('clears a stale dead-pid lock and acquires', () => {
  68. const root = makeProject();
  69. // Pick a pid that is extremely unlikely to be alive.
  70. const deadPid = 2147483646;
  71. fs.writeFileSync(
  72. getWriterPidPath(root),
  73. JSON.stringify({ pid: deadPid, mode: 'direct', startedAt: Date.now() }) + '\n',
  74. );
  75. const r = tryAcquireWriterLock(root, 'direct');
  76. expect(r.kind).toBe('acquired');
  77. releaseWriterLock(root);
  78. });
  79. });