* Build a complete fixture. Returns paths to everything created.
()
| 72 | * Build a complete fixture. Returns paths to everything created. |
| 73 | */ |
| 74 | function buildFixture() { |
| 75 | const root = fs.mkdtempSync(path.join(os.tmpdir(), 'citadel-hygiene-')); |
| 76 | const pluginDataDir = path.join(root, 'plugin-data'); |
| 77 | const planningDir = path.join(root, '.planning'); |
| 78 | fs.mkdirSync(pluginDataDir, { recursive: true }); |
| 79 | fs.mkdirSync(path.join(planningDir, 'telemetry'), { recursive: true }); |
| 80 | |
| 81 | const fx = { root, pluginDataDir, planningDir }; |
| 82 | |
| 83 | // Consent markers: one fresh, one expired (6h TTL) |
| 84 | fx.sessionFresh = path.join(pluginDataDir, 'consent-session-externalActions.json'); |
| 85 | fx.sessionExpired = path.join(pluginDataDir, 'consent-session-daemonSpend.json'); |
| 86 | writeMarker(fx.sessionFresh, 1 * HOUR); |
| 87 | writeMarker(fx.sessionExpired, 7 * HOUR); |
| 88 | |
| 89 | // One-time tokens: one fresh, one expired (120s TTL) |
| 90 | fx.onetimeFresh = path.join(pluginDataDir, 'consent-onetime-fleetSpawn.json'); |
| 91 | fx.onetimeExpired = path.join(pluginDataDir, 'consent-onetime-externalActions.json'); |
| 92 | writeMarker(fx.onetimeFresh, 30 * 1000); |
| 93 | writeMarker(fx.onetimeExpired, 5 * MINUTE); |
| 94 | |
| 95 | // Decoys in plugin data dir: must never be touched |
| 96 | fx.decoyConfig = path.join(pluginDataDir, 'harness.json'); |
| 97 | fs.writeFileSync(fx.decoyConfig, '{}'); |
| 98 | fx.decoyWrongExt = path.join(pluginDataDir, 'consent-session-externalActions.txt'); |
| 99 | fs.writeFileSync(fx.decoyWrongExt, 'not a marker'); |
| 100 | |
| 101 | // Lockdirs: stale top-level, stale nested, fresh top-level (10 min cutoff) |
| 102 | fx.lockStale = path.join(planningDir, 'watch-state.json.lock'); |
| 103 | fx.lockStaleNested = path.join(planningDir, 'telemetry', 'some-state.json.lock'); |
| 104 | fx.lockFresh = path.join(planningDir, 'fresh-state.json.lock'); |
| 105 | makeLockdir(fx.lockStale, 30 * MINUTE); |
| 106 | makeLockdir(fx.lockStaleNested, 30 * MINUTE); |
| 107 | makeLockdir(fx.lockFresh, 0); |
| 108 | |
| 109 | // Decoy: session-end.lock is a regular FILE (idempotency marker written by |
| 110 | // hooks_src/session-end.js). Stale mtime, but files are never removed. |
| 111 | fx.lockFileDecoy = path.join(planningDir, 'telemetry', 'session-end.lock'); |
| 112 | fs.writeFileSync(fx.lockFileDecoy, 'session-id-123'); |
| 113 | const old = (Date.now() - 2 * HOUR) / 1000; |
| 114 | fs.utimesSync(fx.lockFileDecoy, old, old); |
| 115 | |
| 116 | // Decoy: symlinked .lock entry must be skipped, never followed or deleted. |
| 117 | // Junction type works on Windows without elevation. Best effort. |
| 118 | fx.symlinkTarget = path.join(root, 'symlink-target'); |
| 119 | fx.lockSymlink = path.join(planningDir, 'evil.lock'); |
| 120 | fx.hasSymlink = false; |
| 121 | try { |
| 122 | makeLockdir(fx.symlinkTarget, 30 * MINUTE); |
| 123 | fs.symlinkSync(fx.symlinkTarget, fx.lockSymlink, 'junction'); |
| 124 | fx.hasSymlink = true; |
| 125 | } catch { /* symlink unsupported on this system, sub-asserts skipped */ } |
| 126 | |
| 127 | return fx; |
| 128 | } |
| 129 | |
| 130 | function destroyFixture(fx) { |
| 131 | try { fs.rmSync(fx.root, { recursive: true, force: true }); } catch { /* best effort */ } |
no test coverage detected