mirror of
https://github.com/nexu-io/open-design.git
synced 2026-05-31 19:04:39 +07:00
* 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)
542 lines
21 KiB
TypeScript
542 lines
21 KiB
TypeScript
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 });
|
||
}
|
||
});
|
||
});
|