/ src / lib / server / swarmfeed-runtime.test.ts
swarmfeed-runtime.test.ts
  1  import assert from 'node:assert/strict'
  2  import fs from 'node:fs'
  3  import os from 'node:os'
  4  import path from 'node:path'
  5  import { after, before, describe, it } from 'node:test'
  6  import type { Agent } from '@/types'
  7  
  8  const originalEnv = {
  9    DATA_DIR: process.env.DATA_DIR,
 10    WORKSPACE_DIR: process.env.WORKSPACE_DIR,
 11    SWARMCLAW_BUILD_MODE: process.env.SWARMCLAW_BUILD_MODE,
 12  }
 13  
 14  let tempDir = ''
 15  let workspaceDir = ''
 16  let runtimeMod: typeof import('./swarmfeed-runtime')
 17  let agentRepoMod: typeof import('./agents/agent-repository')
 18  
 19  function makeAgent(overrides: Partial<Agent> = {}): Agent {
 20    return {
 21      id: 'agent-1',
 22      name: 'Test Agent',
 23      description: 'Social test agent',
 24      systemPrompt: 'Be useful',
 25      provider: 'openai',
 26      model: 'gpt-4o-mini',
 27      createdAt: Date.now(),
 28      updatedAt: Date.now(),
 29      swarmfeedEnabled: true,
 30      heartbeatEnabled: true,
 31      swarmfeedAutoPostChannels: ['builders'],
 32      swarmfeedHeartbeat: {
 33        enabled: true,
 34        browseFeed: true,
 35        postFrequency: 'manual_only',
 36        autoReply: false,
 37        autoFollow: false,
 38        channelsToMonitor: ['builders'],
 39      },
 40      ...overrides,
 41    }
 42  }
 43  
 44  before(async () => {
 45    tempDir = fs.mkdtempSync(path.join(os.tmpdir(), 'swarmclaw-swarmfeed-runtime-'))
 46    workspaceDir = path.join(tempDir, 'workspace')
 47    fs.mkdirSync(workspaceDir, { recursive: true })
 48    process.env.DATA_DIR = path.join(tempDir, 'data')
 49    process.env.WORKSPACE_DIR = workspaceDir
 50    process.env.SWARMCLAW_BUILD_MODE = '1'
 51  
 52    runtimeMod = await import('./swarmfeed-runtime')
 53    agentRepoMod = await import('./agents/agent-repository')
 54  })
 55  
 56  after(() => {
 57    if (originalEnv.DATA_DIR === undefined) delete process.env.DATA_DIR
 58    else process.env.DATA_DIR = originalEnv.DATA_DIR
 59    if (originalEnv.WORKSPACE_DIR === undefined) delete process.env.WORKSPACE_DIR
 60    else process.env.WORKSPACE_DIR = originalEnv.WORKSPACE_DIR
 61    if (originalEnv.SWARMCLAW_BUILD_MODE === undefined) delete process.env.SWARMCLAW_BUILD_MODE
 62    else process.env.SWARMCLAW_BUILD_MODE = originalEnv.SWARMCLAW_BUILD_MODE
 63    fs.rmSync(tempDir, { recursive: true, force: true })
 64  })
 65  
 66  describe('buildSwarmFeedHeartbeatGuidance', () => {
 67    it('returns empty string when SwarmFeed social heartbeat is disabled', () => {
 68      const guidance = runtimeMod.buildSwarmFeedHeartbeatGuidance(
 69        makeAgent({ swarmfeedEnabled: false }),
 70      )
 71      assert.equal(guidance, '')
 72    })
 73  
 74    it('explains that social automation is inactive when the main heartbeat is disabled', () => {
 75      const guidance = runtimeMod.buildSwarmFeedHeartbeatGuidance(
 76        makeAgent({ heartbeatEnabled: false }),
 77      )
 78      assert.match(guidance, /currently inactive/i)
 79      assert.match(guidance, /heartbeat is disabled/i)
 80    })
 81  
 82    it('includes the manual-only guardrail for autonomous posting', () => {
 83      const guidance = runtimeMod.buildSwarmFeedHeartbeatGuidance(
 84        makeAgent({
 85          swarmfeedHeartbeat: {
 86            enabled: true,
 87            browseFeed: false,
 88            postFrequency: 'manual_only',
 89            autoReply: false,
 90            autoFollow: false,
 91            channelsToMonitor: [],
 92          },
 93        }),
 94      )
 95      assert.match(guidance, /manual only/i)
 96      assert.match(guidance, /Do not author new SwarmFeed posts or replies/i)
 97    })
 98  
 99    it('mentions the recent-post limit for daily posting', () => {
100      const guidance = runtimeMod.buildSwarmFeedHeartbeatGuidance(
101        makeAgent({
102          swarmfeedLastAutoPostAt: Date.now() - 60_000,
103          swarmfeedHeartbeat: {
104            enabled: true,
105            browseFeed: true,
106            postFrequency: 'daily',
107            autoReply: true,
108            autoFollow: true,
109            channelsToMonitor: ['builders'],
110          },
111        }),
112      )
113      assert.match(guidance, /daily auto-post already happened/i)
114    })
115  
116    it('describes the on-task-completion policy explicitly', () => {
117      const guidance = runtimeMod.buildSwarmFeedHeartbeatGuidance(
118        makeAgent({
119          swarmfeedHeartbeat: {
120            enabled: true,
121            browseFeed: true,
122            postFrequency: 'on_task_completion',
123            autoReply: false,
124            autoFollow: false,
125            channelsToMonitor: ['builders'],
126          },
127        }),
128      )
129      assert.match(guidance, /on task completion/i)
130      assert.match(guidance, /completed task/i)
131    })
132  })
133  
134  describe('canAutoPostToSwarmFeed', () => {
135    it('blocks autonomous posting when manual_only is configured', () => {
136      const result = runtimeMod.canAutoPostToSwarmFeed(makeAgent())
137      assert.equal(result.allowed, false)
138      assert.match(result.reason || '', /manual_only/i)
139    })
140  
141    it('blocks autonomous posting after a recent daily post', () => {
142      const result = runtimeMod.canAutoPostToSwarmFeed(
143        makeAgent({
144          swarmfeedLastAutoPostAt: Date.now() - 60_000,
145          swarmfeedHeartbeat: {
146            enabled: true,
147            browseFeed: true,
148            postFrequency: 'daily',
149            autoReply: false,
150            autoFollow: false,
151            channelsToMonitor: [],
152          },
153        }),
154      )
155      assert.equal(result.allowed, false)
156      assert.match(result.reason || '', /daily autonomous SwarmFeed post/i)
157    })
158  
159    it('allows daily posting when the last automatic post is older than 24 hours', () => {
160      const result = runtimeMod.canAutoPostToSwarmFeed(
161        makeAgent({
162          swarmfeedLastAutoPostAt: Date.now() - (25 * 60 * 60 * 1000),
163          swarmfeedHeartbeat: {
164            enabled: true,
165            browseFeed: true,
166            postFrequency: 'daily',
167            autoReply: false,
168            autoFollow: false,
169            channelsToMonitor: [],
170          },
171        }),
172      )
173      assert.equal(result.allowed, true)
174    })
175  })
176  
177  describe('markSwarmFeedAutoPost', () => {
178    it('persists the last auto-post timestamp on the agent record', () => {
179      const agent = makeAgent({ id: 'agent-mark-post' })
180      agentRepoMod.saveAgent(agent.id, agent)
181  
182      runtimeMod.markSwarmFeedAutoPost(agent.id)
183  
184      const updated = agentRepoMod.getAgent(agent.id)
185      assert.equal(typeof updated?.swarmfeedLastAutoPostAt, 'number')
186      assert.ok((updated?.swarmfeedLastAutoPostAt || 0) >= agent.createdAt)
187    })
188  })