import fs from 'fs'; import os from 'os'; import path from 'path'; import YAML from 'yaml'; import { describe, it, expect, beforeEach, afterEach } from 'vitest'; import { persistSubreddits } from '../src/config/persistence.js'; const SAMPLE = `# top-level note that must survive reddit: # weights bias picking subreddits: - { name: AverageNudes, weight: 4, tone: 'direct' } # validation seekers - { name: gonewild40plus, weight: 4, tone: 'gentle' } # mature - { name: normalnudes, weight: 3 } # real bodies - { name: tightandtoned, weight: 1 } pollIntervalMs: 30000 userAgent: "ua" `; describe('persistSubreddits', () => { let tmpDir: string; let configPath: string; beforeEach(() => { tmpDir = fs.mkdtempSync(path.join(os.tmpdir(), 'rb-config-')); configPath = path.join(tmpDir, 'config.yaml'); fs.writeFileSync(configPath, SAMPLE, 'utf-8'); }); afterEach(() => { fs.rmSync(tmpDir, { recursive: true, force: true }); }); it('mutates an existing row in place and preserves its inline comment', () => { persistSubreddits( [ { name: 'AverageNudes', type: 'image', weight: 4, tone: 'playful' }, { name: 'gonewild40plus', type: 'image', weight: 4, tone: 'gentle' }, { name: 'normalnudes', type: 'image', weight: 3, tone: 'default' }, { name: 'tightandtoned', type: 'image', weight: 1, tone: 'default' }, ], configPath, ); const out = fs.readFileSync(configPath, 'utf-8'); expect(out).toMatch(/tone:\s*['"]?playful['"]?/); expect(out).toContain('# validation seekers'); expect(out).toContain('# top-level note that must survive'); expect(out).toContain('# weights bias picking'); expect(out).toContain('# mature'); expect(out).toContain('pollIntervalMs: 30000'); }); it('drops removed rows (and their attached comments)', () => { persistSubreddits( [ { name: 'AverageNudes', type: 'image', weight: 4, tone: 'direct' }, { name: 'tightandtoned', type: 'image', weight: 1, tone: 'default' }, ], configPath, ); const out = fs.readFileSync(configPath, 'utf-8'); expect(out).not.toContain('gonewild40plus'); expect(out).not.toContain('normalnudes'); expect(out).not.toContain('# mature'); expect(out).toContain('AverageNudes'); expect(out).toContain('tightandtoned'); expect(out).toContain('# validation seekers'); }); it('appends new rows in caller order', () => { persistSubreddits( [ { name: 'AverageNudes', type: 'image', weight: 4, tone: 'direct' }, { name: 'gonewild40plus', type: 'image', weight: 4, tone: 'gentle' }, { name: 'normalnudes', type: 'image', weight: 3, tone: 'default' }, { name: 'tightandtoned', type: 'image', weight: 1, tone: 'default' }, { name: 'NewSub', type: 'chat', weight: 2, tone: 'playful' }, ], configPath, ); const parsed = YAML.parse(fs.readFileSync(configPath, 'utf-8')); const subs = parsed.reddit.subreddits; expect(subs[subs.length - 1]).toMatchObject({ name: 'NewSub', type: 'chat', weight: 2, tone: 'playful', }); }); it("omits 'default' tone and 'image' type on write to match on-disk style", () => { persistSubreddits( [{ name: 'OnlyOne', type: 'image', weight: 1, tone: 'default' }], configPath, ); const out = fs.readFileSync(configPath, 'utf-8'); expect(out).not.toMatch(/tone:\s*default/); expect(out).not.toMatch(/type:\s*image/); }); it('atomic write — tmp file not left behind on success', () => { persistSubreddits( [{ name: 'OnlyOne', type: 'image', weight: 1, tone: 'direct' }], configPath, ); expect(fs.existsSync(configPath + '.tmp')).toBe(false); }); it('reorders rows according to input order', () => { persistSubreddits( [ { name: 'tightandtoned', type: 'image', weight: 1, tone: 'default' }, { name: 'AverageNudes', type: 'image', weight: 4, tone: 'direct' }, { name: 'gonewild40plus', type: 'image', weight: 4, tone: 'gentle' }, { name: 'normalnudes', type: 'image', weight: 3, tone: 'default' }, ], configPath, ); const parsed = YAML.parse(fs.readFileSync(configPath, 'utf-8')); expect(parsed.reddit.subreddits.map((s: { name: string }) => s.name)).toEqual([ 'tightandtoned', 'AverageNudes', 'gonewild40plus', 'normalnudes', ]); }); });