import path from 'node:path'; import os from 'node:os'; import { mkdir, mkdtemp, rm, writeFile } from 'node:fs/promises'; import { describe, expect, it, vi } from 'vitest'; import { buildOrbitPrompt, buildOrbitSystemPrompt, OrbitService, renderOrbitTemplateSystemPrompt, type OrbitRunHandler, type OrbitTemplateSelection, } from '../src/orbit.js'; import { skillCwdAliasSegment } from '../src/cwd-aliases.js'; function formatExpectedLocalOrbitPromptTimestamp(date: Date): string { const yyyy = date.getFullYear(); const mm = String(date.getMonth() + 1).padStart(2, '0'); const dd = String(date.getDate()).padStart(2, '0'); const hh = String(date.getHours()).padStart(2, '0'); const mi = String(date.getMinutes()).padStart(2, '0'); const timeZoneName = new Intl.DateTimeFormat(undefined, { timeZoneName: 'shortOffset' }) .formatToParts(date) .find((part) => part.type === 'timeZoneName')?.value; return `${yyyy}-${mm}-${dd} ${hh}:${mi}${timeZoneName ? ` (${timeZoneName})` : ''}`; } describe('buildOrbitPrompt', () => { it('keeps the user-visible Orbit prompt concise', () => { const template: OrbitTemplateSelection = { id: 'orbit-general', name: 'orbit-general', examplePrompt: 'Render the editorial bento dashboard.', dir: path.join('/repo', 'skills', 'orbit-general'), body: 'Open and mirror the shipped `example.html` before writing output. Use exclusively the canvas tokens.', designSystemRequired: false, }; const now = new Date('2026-05-06T15:32:52.361Z'); const start = new Date(now.getTime() - 24 * 60 * 60_000); const prompt = buildOrbitPrompt(now, template); expect(prompt).toContain('Create today\'s Orbit daily digest as a Live Artifact.'); expect(prompt).toContain( `Use my connected work data from ${formatExpectedLocalOrbitPromptTimestamp(start)} through ${formatExpectedLocalOrbitPromptTimestamp(now)}.`, ); expect(prompt).not.toContain('2026-05-05T15:32:52.361Z'); expect(prompt).toContain('Use the selected Orbit template: orbit-general.'); expect(prompt).not.toContain('DAILY DIGEST CONNECTOR CURATION IS REQUIRED WHEN SUPPORTED'); expect(prompt).not.toContain('Selected template example prompt:'); expect(prompt).not.toContain('Render the editorial bento dashboard.'); }); it('localizes the user-visible Orbit prompt when the app language is Chinese', () => { const template: OrbitTemplateSelection = { id: 'orbit-github', name: 'orbit-github', examplePrompt: 'Generate today\'s Open Orbit GitHub briefing.', dir: path.join('/repo', 'skills', 'orbit-github'), body: 'Mirror the shipped `example.html` before writing output.', designSystemRequired: false, }; const prompt = buildOrbitPrompt(new Date('2026-05-06T15:32:52.361Z'), template, 'zh-CN'); expect(prompt).toContain('请将今天的 Orbit 每日摘要制作成 Live Artifact。'); expect(prompt).toContain('使用已选中的 Orbit 模板:orbit-github。'); }); }); describe('buildOrbitSystemPrompt', () => { it('embeds selected Orbit template instructions and staged side-file guidance', () => { const template: OrbitTemplateSelection = { id: 'orbit-general', name: 'orbit-general', examplePrompt: 'Render the editorial bento dashboard.', dir: path.join('/repo', 'skills', 'orbit-general'), body: 'Open and mirror the shipped `example.html` before writing output. Use exclusively the canvas tokens.', designSystemRequired: false, }; const prompt = buildOrbitSystemPrompt(new Date('2026-05-06T15:32:52.361Z'), template); const stagedAlias = skillCwdAliasSegment(template.dir); expect(prompt).toContain('Skill id: orbit-general'); expect(prompt).toContain(`Staged root: .od-skills/${stagedAlias}/`); expect(prompt).toContain(`read ".od-skills/${stagedAlias}/SKILL.md"`); expect(prompt).toContain(`".od-skills/${stagedAlias}/example.html"`); expect(prompt).toContain('visual/domain guidance'); expect(prompt).not.toContain('Selected template skill instructions:'); expect(prompt).toContain('Selected template example prompt:'); expect(prompt).toContain('Render the editorial bento dashboard.'); }); it('prioritizes curated daily digest connector discovery before fallback listing', () => { const prompt = buildOrbitSystemPrompt(new Date('2026-05-06T15:32:52.361Z')); expect(prompt).toContain('DAILY DIGEST CONNECTOR CURATION IS REQUIRED WHEN SUPPORTED'); expect(prompt).toContain('tools connectors list --use-case personal_daily_digest --format compact'); expect(prompt).toContain('do not stop just because `--use-case` is unsupported'); }); it('renders the selected template skill body as authoritative run instructions', () => { const template: OrbitTemplateSelection = { id: 'orbit-general', name: 'orbit-general', examplePrompt: 'Render the editorial bento dashboard.', dir: path.join('/repo', 'skills', 'orbit-general'), body: 'Open and mirror the shipped `example.html` before writing output. Use exclusively the canvas tokens.', designSystemRequired: false, }; const prompt = renderOrbitTemplateSystemPrompt(template); expect(prompt).toContain('Selected Orbit template skill — orbit-general'); expect(prompt).toContain('Treat it as authoritative'); expect(prompt).toContain('must not override the selected template'); expect(prompt).toContain('opts out of external design-system injection'); expect(prompt).toContain('Do not apply the workspace design system'); expect(prompt).toContain('Open and mirror the shipped `example.html`'); expect(prompt).toContain('Use exclusively the canvas tokens.'); }); it('pins Chinese as the final output language when the app locale is Chinese', () => { const template: OrbitTemplateSelection = { id: 'orbit-github', name: 'orbit-github', examplePrompt: 'Generate today\'s Open Orbit GitHub briefing.', dir: path.join('/repo', 'skills', 'orbit-github'), body: 'Mirror the shipped `example.html` before writing output.', designSystemRequired: false, }; const prompt = buildOrbitSystemPrompt(new Date('2026-05-06T15:32:52.361Z'), template, 'zh-CN'); expect(prompt).toContain('App language: Simplified Chinese (zh-CN).'); expect(prompt).toContain('The final Orbit artifact itself must stay in Simplified Chinese.'); expect(prompt).toContain('Generate today\'s Open Orbit GitHub briefing.'); }); it('treats script-tagged Traditional Chinese locales as zh-TW', () => { const template: OrbitTemplateSelection = { id: 'orbit-github', name: 'orbit-github', examplePrompt: 'Generate today\'s Open Orbit GitHub briefing.', dir: path.join('/repo', 'skills', 'orbit-github'), body: 'Mirror the shipped `example.html` before writing output.', designSystemRequired: false, }; const prompt = buildOrbitSystemPrompt(new Date('2026-05-06T15:32:52.361Z'), template, 'zh-Hant-TW'); expect(prompt).toContain('App language: Traditional Chinese (zh-TW).'); expect(prompt).toContain('The final Orbit artifact itself must stay in Traditional Chinese.'); }); }); describe('OrbitService', () => { it('passes concise user prompt and detailed system prompt to the run handler', async () => { const dataDir = await mkdtemp(path.join(os.tmpdir(), 'orbit-test-')); try { const service = new OrbitService(dataDir); const captured: { request?: Parameters[0] } = {}; service.setRunHandler(async (request) => { captured.request = request; return { projectId: 'project-1', agentRunId: 'agent-1', completion: Promise.resolve({ agentRunId: 'agent-1', status: 'succeeded', }), }; }); await service.start('manual'); expect(captured.request?.prompt).toContain( 'Create today\'s Orbit daily digest as a Live Artifact.', ); expect(captured.request?.prompt).not.toContain( 'DAILY DIGEST CONNECTOR CURATION IS REQUIRED WHEN SUPPORTED', ); expect(captured.request?.systemPrompt).toContain( 'DAILY DIGEST CONNECTOR CURATION IS REQUIRED WHEN SUPPORTED', ); let status = await service.status(); for (let attempt = 0; attempt < 10 && !status.lastRun; attempt += 1) { await new Promise((resolve) => setTimeout(resolve, 0)); status = await service.status(); } } finally { await rm(dataDir, { recursive: true, force: true }); } }); it('localizes the template example prompt passed to the run handler for Chinese Orbit runs', async () => { const dataDir = await mkdtemp(path.join(os.tmpdir(), 'orbit-test-')); try { const service = new OrbitService(dataDir); const captured: { request?: Parameters[0] } = {}; service.setTemplateResolver(async () => ({ id: 'orbit-github', name: 'orbit-github', examplePrompt: 'Generate today\'s Open Orbit GitHub briefing.', dir: path.join('/repo', 'skills', 'orbit-github'), body: 'Mirror the shipped `example.html` before writing output.', designSystemRequired: false, })); service.configure({ enabled: true, time: '08:00', templateSkillId: 'orbit-github' }); service.setRunHandler(async (request) => { captured.request = request; return { projectId: 'project-1', agentRunId: 'agent-1', completion: Promise.resolve({ agentRunId: 'agent-1', status: 'succeeded', }), }; }); await service.start('manual', { locale: 'zh-CN' }); let status = await service.status(); for (let attempt = 0; attempt < 10 && !status.lastRun; attempt += 1) { await new Promise((resolve) => setTimeout(resolve, 0)); status = await service.status(); } expect(captured.request?.template?.examplePrompt).toContain('生成今天的 Open Orbit GitHub 简报'); expect(captured.request?.systemPrompt).toContain('The final Orbit artifact itself must stay in Simplified Chinese.'); } finally { await rm(dataDir, { recursive: true, force: true }); } }); it('preserves the default template when config omits the field', async () => { const dataDir = await mkdtemp(path.join(os.tmpdir(), 'orbit-test-')); try { const service = new OrbitService(dataDir); service.configure({ enabled: true, time: '08:00' }); await expect(service.status()).resolves.toMatchObject({ config: { templateSkillId: 'orbit-general' }, }); service.stop(); } finally { await rm(dataDir, { recursive: true, force: true }); } }); it('falls back to the default time when config has an out-of-range time', async () => { const dataDir = await mkdtemp(path.join(os.tmpdir(), 'orbit-test-')); try { const service = new OrbitService(dataDir); service.configure({ enabled: true, time: '24:99' }); await expect(service.status()).resolves.toMatchObject({ config: { time: '08:00' }, }); service.stop(); } finally { await rm(dataDir, { recursive: true, force: true }); } }); it('treats a malformed activity summary file as missing state', async () => { const dataDir = await mkdtemp(path.join(os.tmpdir(), 'orbit-test-')); try { await mkdir(path.join(dataDir, 'orbit'), { recursive: true }); await writeFile(path.join(dataDir, 'orbit', 'activity-summary.json'), '{not json', 'utf8'); const service = new OrbitService(dataDir); await expect(service.status()).resolves.toMatchObject({ lastRun: null, }); } finally { await rm(dataDir, { recursive: true, force: true }); } }); it('reschedules after an early scheduled start rejection', async () => { vi.useFakeTimers(); vi.setSystemTime(new Date(2026, 4, 6, 7, 59, 0, 0)); const dataDir = await mkdtemp(path.join(os.tmpdir(), 'orbit-test-')); try { const service = new OrbitService(dataDir); service.setRunHandler(async () => { throw new Error('agent unavailable'); }); service.configure({ enabled: true, time: '08:00' }); const firstNextRunAt = (await service.status()).nextRunAt; expect(firstNextRunAt).not.toBeNull(); await vi.advanceTimersByTimeAsync(Date.parse(firstNextRunAt!) - Date.now()); const secondNextRunAt = (await service.status()).nextRunAt; expect(secondNextRunAt).not.toBeNull(); expect(secondNextRunAt).not.toBe(firstNextRunAt); expect(Date.parse(secondNextRunAt!)).toBeGreaterThan(Date.parse(firstNextRunAt!)); service.stop(); } finally { vi.useRealTimers(); await rm(dataDir, { recursive: true, force: true }); } }); it('does not report the fired schedule time as nextRunAt while a scheduled run is in flight', async () => { vi.useFakeTimers(); vi.setSystemTime(new Date(2026, 4, 6, 7, 59, 0, 0)); const dataDir = await mkdtemp(path.join(os.tmpdir(), 'orbit-test-')); try { const completion = new Promise(() => {}); const service = new OrbitService(dataDir); service.setRunHandler(async () => ({ projectId: 'project-1', agentRunId: 'agent-1', completion, })); service.configure({ enabled: true, time: '08:00' }); const firstNextRunAt = (await service.status()).nextRunAt; expect(firstNextRunAt).not.toBeNull(); await vi.advanceTimersByTimeAsync(Date.parse(firstNextRunAt!) - Date.now()); await expect(service.status()).resolves.toMatchObject({ running: true, nextRunAt: null, }); service.stop(); } finally { vi.useRealTimers(); await rm(dataDir, { recursive: true, force: true }); } }); it('sets connectorsChecked to the summed connector outcomes', async () => { const dataDir = await mkdtemp(path.join(os.tmpdir(), 'orbit-test-')); try { const service = new OrbitService(dataDir); service.setRunHandler(async () => ({ projectId: 'project-1', agentRunId: 'agent-1', completion: Promise.resolve({ agentRunId: 'agent-1', status: 'succeeded', }), })); await service.start('manual'); let status = await service.status(); for (let attempt = 0; attempt < 10 && !status.lastRun; attempt += 1) { await new Promise((resolve) => setTimeout(resolve, 0)); status = await service.status(); } expect(status.lastRun).not.toBeNull(); expect(status.lastRun?.connectorsSucceeded).toBe(1); expect(status.lastRun?.connectorsFailed).toBe(0); expect(status.lastRun?.connectorsSkipped).toBe(0); expect(status.lastRun?.connectorsChecked).toBe(1); } finally { await rm(dataDir, { recursive: true, force: true }); } }); it('persists failed Orbit agent summaries in the last-run receipt markdown', async () => { const dataDir = await mkdtemp(path.join(os.tmpdir(), 'orbit-test-')); try { const service = new OrbitService(dataDir); service.setRunHandler(async () => ({ projectId: 'project-1', agentRunId: 'agent-1', completion: Promise.resolve({ agentRunId: 'agent-1', status: 'failed', summary: 'Agent succeeded but did not register a live artifact for this Orbit run.\n\nGitHub auth failed, so I did not create a daily digest artifact.', }), })); await service.start('manual'); let status = await service.status(); for (let attempt = 0; attempt < 10 && (status.running || !status.lastRun); attempt += 1) { await new Promise((resolve) => setTimeout(resolve, 0)); status = await service.status(); } expect(status.lastRun).not.toBeNull(); expect(status.running).toBe(false); expect(status.lastRun?.connectorsSucceeded).toBe(0); expect(status.lastRun?.connectorsFailed).toBe(1); expect(status.lastRun?.markdown).toContain( 'Agent succeeded but did not register a live artifact for this Orbit run.', ); expect(status.lastRun?.markdown).toContain( 'GitHub auth failed, so I did not create a daily digest artifact.', ); } finally { await rm(dataDir, { recursive: true, force: true }); } }); it('tracks the most recent run per template alongside the global last run', async () => { vi.useFakeTimers(); const dataDir = await mkdtemp(path.join(os.tmpdir(), 'orbit-test-')); try { const service = new OrbitService(dataDir); let runCount = 0; service.setTemplateResolver(async (skillId) => ({ id: skillId, name: skillId, examplePrompt: `${skillId} prompt`, dir: path.join('/repo', 'skills', skillId), body: `${skillId} body`, designSystemRequired: false, })); service.setRunHandler(async (request) => { runCount += 1; return { projectId: `project-${runCount}`, agentRunId: `agent-${runCount}`, completion: Promise.resolve({ agentRunId: `agent-${runCount}`, status: 'succeeded', }), }; }); service.configure({ enabled: false, time: '08:00', templateSkillId: 'orbit-general' }); vi.setSystemTime(new Date('2026-05-06T08:00:00.000Z')); await service.start('manual'); let status = await service.status(); for ( let attempt = 0; attempt < 10 && (status.running || status.lastRunsByTemplate['orbit-general']?.agentRunId !== 'agent-1'); attempt += 1 ) { await vi.advanceTimersByTimeAsync(1); status = await service.status(); } service.configure({ enabled: false, time: '08:00', templateSkillId: 'orbit-editorial' }); vi.setSystemTime(new Date('2026-05-06T09:00:00.000Z')); await service.start('manual'); for ( let attempt = 0; attempt < 10 && (status.running || status.lastRunsByTemplate['orbit-editorial']?.agentRunId !== 'agent-2'); attempt += 1 ) { await vi.advanceTimersByTimeAsync(1); status = await service.status(); } service.configure({ enabled: false, time: '08:00', templateSkillId: 'orbit-general' }); vi.setSystemTime(new Date('2026-05-06T10:00:00.000Z')); await service.start('manual'); for ( let attempt = 0; attempt < 10 && (status.running || status.lastRunsByTemplate['orbit-general']?.agentRunId !== 'agent-3'); attempt += 1 ) { await vi.advanceTimersByTimeAsync(1); status = await service.status(); } status = await service.status(); expect(status.lastRun).toMatchObject({ agentRunId: 'agent-3', templateSkillId: 'orbit-general', }); expect(status.lastRunsByTemplate).toMatchObject({ 'orbit-general': { agentRunId: 'agent-3', templateSkillId: 'orbit-general', }, 'orbit-editorial': { agentRunId: 'agent-2', templateSkillId: 'orbit-editorial', }, }); } finally { vi.useRealTimers(); await rm(dataDir, { recursive: true, force: true }); } }); it('pins the configured template id at run start when template resolution falls back to null', async () => { const dataDir = await mkdtemp(path.join(os.tmpdir(), 'orbit-test-')); try { const service = new OrbitService(dataDir); let resolveCompletion!: (value: { agentRunId: string; status: 'succeeded'; }) => void; const completion = new Promise<{ agentRunId: string; status: 'succeeded'; }>((resolve) => { resolveCompletion = resolve; }); service.setTemplateResolver(async () => null); service.setRunHandler(async () => ({ projectId: 'project-1', agentRunId: 'agent-1', completion, })); service.configure({ enabled: false, time: '08:00', templateSkillId: 'orbit-general' }); await service.start('manual'); service.configure({ enabled: false, time: '08:00', templateSkillId: 'orbit-editorial' }); resolveCompletion({ agentRunId: 'agent-1', status: 'succeeded', }); let status = await service.status(); for ( let attempt = 0; attempt < 10 && (status.running || status.lastRun?.agentRunId !== 'agent-1'); attempt += 1 ) { await new Promise((resolve) => setTimeout(resolve, 0)); status = await service.status(); } expect(status.lastRun).toMatchObject({ agentRunId: 'agent-1', templateSkillId: 'orbit-general', }); } finally { await rm(dataDir, { recursive: true, force: true }); } }); });