open-design/apps/daemon/tests/orbit.test.ts
nettee 052f8097de
fix(daemon): inject @-mention skills into system prompt (#2552)
* fix(daemon): inject @-mention skills into system prompt

Generated-By: looper 0.8.1 (runner=worker, agent=opencode)

* fix(daemon): compose ad-hoc skill mode and aliases

Generated-By: looper 0.8.1 (runner=fixer, agent=opencode)

* fix(daemon): lazily load and stage ad-hoc skills

Generated-By: looper 0.8.1 (runner=fixer, agent=opencode)

* test(daemon): assert staged skill files before spawn

Generated-By: looper 0.8.1 (runner=fixer, agent=opencode)

* fix(daemon): compose skill metadata across @ mentions

Generated-By: looper 0.8.1 (runner=fixer, agent=opencode)

* test(daemon): cover ad-hoc critique skill policy

Generated-By: looper 0.8.1 (runner=fixer, agent=opencode)

* fix(daemon): preserve plugin skill composition

Generated-By: looper 0.8.1 (runner=fixer, agent=opencode)

* fix(daemon): resolve conflicting composed skill surfaces

Generated-By: looper 0.8.1 (runner=fixer, agent=opencode)

* fix(daemon): preserve primary skill surface

Generated-By: looper 0.8.1 (runner=fixer, agent=opencode)

* fix(daemon): share resolved critique surface

Generated-By: looper 0.8.1 (runner=fixer, agent=opencode)
2026-05-21 22:20:21 +08:00

542 lines
21 KiB
TypeScript
Raw Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

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<OrbitRunHandler>[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<OrbitRunHandler>[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<never>(() => {});
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 });
}
});
});