mirror of
https://github.com/nexu-io/open-design.git
synced 2026-06-01 03:14:35 +07:00
* feat(web): introduce Automations tab with dual-track capability for routines This commit adds a new Automations tab that consolidates routines, schedules, and live artifacts, allowing users to manage automations seamlessly. The tab features a modal for creating and editing automations, which supports various scheduling options (hourly, daily, weekdays, weekly) and project modes (create_each_run, reuse). The CLI is also updated to expose automation commands, ensuring consistency between the web UI and CLI interfaces. Key changes include: - New `NewAutomationModal` component for automation creation and editing. - Updated `TasksView` to integrate the new Automations functionality. - Enhanced styling for the Automations tab to improve user experience. This implementation aligns with the dual-track capability exposure policy, ensuring all features are accessible via both the web UI and CLI. * feat(daemon): enhance automation context handling and CLI commands This commit introduces several improvements to the automation context management and updates the CLI commands accordingly. Key changes include: - Added support for new context fields (`plugin`, `mcp`, `connector`) in automation commands. - Updated the CLI to reflect new target options (`new-project`). - Enhanced error messages for invalid target inputs. - Introduced functions to handle context selection and normalization for routines, including the ability to parse and store context data in the database. - Updated the database schema to include a new `context_json` field for routines. - Improved the handling of context in routine routes and the web interface, ensuring that selected contexts are properly managed and displayed. These changes aim to provide a more robust and flexible automation experience, aligning with the recent enhancements in the web UI. * feat(web): enhance TasksView with automation run history and status indicators This commit introduces several new features to the TasksView component, including: - Added functionality to display automation run history for each routine, showing metadata such as status, timestamps, and project details. - Implemented status indicators for routine runs, providing visual feedback on their current state (succeeded, failed, running, queued). - Enhanced the UI to allow users to expand and view detailed run history, including the ability to open the corresponding project conversation. - Updated styles to improve the presentation of automation statuses and history. These changes aim to provide users with better insights into their automation routines and improve overall usability. * feat(daemon): implement automation ingestion and proposal management This commit introduces several new features related to automation ingestion and proposal management within the daemon. Key changes include: - Added new modules for handling automation source packets and proposals, allowing for the storage, retrieval, and management of automation-related data. - Implemented functions to list, create, and apply automation proposals, enhancing the automation workflow. - Introduced new CLI commands for interacting with memory entries and automation sources, providing users with more control over their automation processes. - Enhanced the server routes to support automation source and proposal APIs, enabling seamless integration with the existing system. These changes aim to improve the overall automation experience, making it easier for users to manage and utilize automation proposals and ingestions effectively.
214 lines
7.4 KiB
TypeScript
214 lines
7.4 KiB
TypeScript
import { promises as fsp } from 'node:fs';
|
|
import os from 'node:os';
|
|
import path from 'node:path';
|
|
import { afterEach, beforeEach, describe, expect, it } from 'vitest';
|
|
|
|
import {
|
|
applyAutomationProposal,
|
|
createAutomationProposal,
|
|
listAutomationProposals,
|
|
rejectAutomationProposal,
|
|
} from '../src/automation-proposals.js';
|
|
import { listAllAutomationTemplates } from '../src/automation-templates.js';
|
|
import { listDesignSystems } from '../src/design-systems.js';
|
|
import { readMemoryEntry } from '../src/memory.js';
|
|
import { listSkills } from '../src/skills.js';
|
|
|
|
let dataDir = '';
|
|
|
|
beforeEach(async () => {
|
|
dataDir = await fsp.mkdtemp(path.join(os.tmpdir(), 'od-automation-proposals-'));
|
|
});
|
|
|
|
afterEach(async () => {
|
|
await fsp.rm(dataDir, { recursive: true, force: true });
|
|
});
|
|
|
|
describe('automation evolution proposals', () => {
|
|
it('creates a reviewable memory proposal and applies it into the memory store', async () => {
|
|
const proposal = await createAutomationProposal(dataDir, {
|
|
id: 'proposal-memory-1',
|
|
title: 'Project memory from connector digest',
|
|
summary: 'Preserve a durable project decision found by an automation.',
|
|
targetKind: 'memory-node',
|
|
action: 'create',
|
|
sourcePacketIds: ['packet-1'],
|
|
patch: {
|
|
format: 'json',
|
|
after: JSON.stringify({
|
|
name: 'Connector decision',
|
|
description: 'Decision captured from connector activity',
|
|
type: 'project',
|
|
body: '- Decision: keep design-system extraction behind review.',
|
|
}),
|
|
},
|
|
metadata: { memoryType: 'project' },
|
|
});
|
|
|
|
expect(proposal.status).toBe('pending-review');
|
|
|
|
const applied = await applyAutomationProposal(dataDir, proposal.id);
|
|
|
|
expect(applied.proposal.status).toBe('applied');
|
|
expect(applied.result).toMatchObject({ action: 'create' });
|
|
|
|
const memoryId = (applied.result as { memoryId: string }).memoryId;
|
|
const entry = await readMemoryEntry(dataDir, memoryId);
|
|
expect(entry).toMatchObject({
|
|
name: 'Connector decision',
|
|
type: 'project',
|
|
});
|
|
expect(entry?.body).toContain('keep design-system extraction behind review');
|
|
});
|
|
|
|
it('rejects a pending proposal without applying it', async () => {
|
|
const proposal = await createAutomationProposal(dataDir, {
|
|
id: 'proposal-reject-1',
|
|
title: 'Unwanted memory',
|
|
summary: 'This should stay review-only.',
|
|
targetKind: 'memory-node',
|
|
action: 'create',
|
|
patch: {
|
|
format: 'markdown',
|
|
after: '- Avoid applying this proposal.',
|
|
},
|
|
});
|
|
|
|
const rejected = await rejectAutomationProposal(dataDir, proposal.id, 'not durable');
|
|
|
|
expect(rejected.status).toBe('rejected');
|
|
expect(rejected.metadata).toMatchObject({ rejectedReason: 'not durable' });
|
|
expect(await listAutomationProposals(dataDir, { status: 'pending-review' })).toEqual([]);
|
|
});
|
|
|
|
it('applies design-system proposals into the user design-system root', async () => {
|
|
const proposal = await createAutomationProposal(dataDir, {
|
|
id: 'proposal-design-system-1',
|
|
title: 'Draft design system',
|
|
summary: 'Create a user design-system draft.',
|
|
targetKind: 'design-system',
|
|
action: 'create',
|
|
targetRef: 'design-systems/acme/DESIGN.md',
|
|
patch: {
|
|
format: 'markdown',
|
|
after: '# Acme Design System\n\n> Category: Self-evolved\n',
|
|
},
|
|
});
|
|
|
|
const applied = await applyAutomationProposal(dataDir, proposal.id);
|
|
|
|
expect(applied.result).toMatchObject({
|
|
designSystemId: 'acme',
|
|
path: 'design-systems/acme/DESIGN.md',
|
|
});
|
|
await expect(
|
|
fsp.readFile(path.join(dataDir, 'design-systems', 'acme', 'DESIGN.md'), 'utf8'),
|
|
).resolves.toContain('Acme Design System');
|
|
await expect(listDesignSystems(path.join(dataDir, 'design-systems'))).resolves.toEqual(
|
|
expect.arrayContaining([expect.objectContaining({ id: 'acme' })]),
|
|
);
|
|
});
|
|
|
|
it('applies skill proposals into the user skill root', async () => {
|
|
const proposal = await createAutomationProposal(dataDir, {
|
|
id: 'proposal-skill-1',
|
|
title: 'Draft reusable skill',
|
|
summary: 'Create a user skill draft.',
|
|
targetKind: 'skill',
|
|
action: 'create',
|
|
targetRef: 'skills/reusable-flow/SKILL.md',
|
|
patch: {
|
|
format: 'markdown',
|
|
after: [
|
|
'---',
|
|
'name: "Reusable Flow"',
|
|
'description: "Captured workflow"',
|
|
'---',
|
|
'',
|
|
'# Reusable Flow',
|
|
].join('\n'),
|
|
},
|
|
});
|
|
|
|
const applied = await applyAutomationProposal(dataDir, proposal.id);
|
|
|
|
expect(applied.result).toMatchObject({
|
|
skillSlug: 'reusable-flow',
|
|
path: 'skills/reusable-flow/SKILL.md',
|
|
});
|
|
await expect(
|
|
fsp.readFile(path.join(dataDir, 'skills', 'reusable-flow', 'SKILL.md'), 'utf8'),
|
|
).resolves.toContain('Reusable Flow');
|
|
await expect(listSkills(path.join(dataDir, 'skills'))).resolves.toEqual(
|
|
expect.arrayContaining([expect.objectContaining({ id: 'Reusable Flow' })]),
|
|
);
|
|
});
|
|
|
|
it('applies automation-template proposals into the user template store', async () => {
|
|
const proposal = await createAutomationProposal(dataDir, {
|
|
id: 'proposal-template-1',
|
|
title: 'Draft automation template',
|
|
summary: 'Create a user automation template.',
|
|
targetKind: 'automation-template',
|
|
action: 'create',
|
|
patch: {
|
|
format: 'json',
|
|
after: JSON.stringify({
|
|
id: 'daily-context',
|
|
title: 'Daily context digest',
|
|
description: 'Turn one trusted source into context proposals every day.',
|
|
purpose: 'Self-evolve project context from recurring source material.',
|
|
triggerKinds: ['schedule'],
|
|
sourceKinds: ['connector'],
|
|
stages: [
|
|
{ id: 'ingest', kind: 'ingest', title: 'Capture source' },
|
|
{ id: 'propose', kind: 'propose', title: 'Create proposals' },
|
|
],
|
|
outputSinks: ['memory', 'automation-template'],
|
|
reviewPolicy: 'always',
|
|
tokenCompression: 'balanced',
|
|
tags: ['self-evolution'],
|
|
}),
|
|
},
|
|
});
|
|
|
|
const applied = await applyAutomationProposal(dataDir, proposal.id);
|
|
|
|
expect(applied.result).toMatchObject({
|
|
automationTemplateId: 'daily-context',
|
|
path: 'automation-templates/daily-context.json',
|
|
});
|
|
await expect(listAllAutomationTemplates(dataDir)).resolves.toEqual(
|
|
expect.arrayContaining([expect.objectContaining({ id: 'daily-context' })]),
|
|
);
|
|
});
|
|
|
|
it('guards built-in automation templates from proposal overwrite', async () => {
|
|
const proposal = await createAutomationProposal(dataDir, {
|
|
id: 'proposal-template-built-in-1',
|
|
title: 'Overwrite built-in template',
|
|
summary: 'This should remain protected.',
|
|
targetKind: 'automation-template',
|
|
action: 'update',
|
|
patch: {
|
|
format: 'json',
|
|
after: JSON.stringify({
|
|
id: 'extract-design-system',
|
|
title: 'Changed',
|
|
description: 'Changed',
|
|
purpose: 'Changed',
|
|
triggerKinds: ['manual'],
|
|
sourceKinds: ['chat'],
|
|
stages: [{ id: 'propose', kind: 'propose', title: 'Propose' }],
|
|
outputSinks: ['memory'],
|
|
reviewPolicy: 'always',
|
|
tokenCompression: 'off',
|
|
}),
|
|
},
|
|
});
|
|
|
|
await expect(applyAutomationProposal(dataDir, proposal.id)).rejects.toThrow(
|
|
'cannot overwrite built-in automation template',
|
|
);
|
|
});
|
|
});
|