mirror of
https://github.com/nexu-io/open-design.git
synced 2026-05-31 19:04:39 +07:00
* feat: add Orbit activity summaries * fix(orbit): make runs navigable while agent continues * fix(web): widen minimum chat panel * feat: support Orbit template selection * fix(daemon): avoid bogus skill side-file preflight * fix(web): collapse orbit artifact project cards * fix(web): preserve orbit project card titles * fix: improve Orbit run daily briefing * fix: handle Orbit digest data failures * fix: load Orbit templates and connector tools reliably * fix: keep Orbit summary counts consistent Generated-By: looper 0.6.1 (runner=fixer, agent=opencode) * fix: apply Orbit template skill context * fix: cache and curate connector tools for Orbit * fix: align Orbit defaults and connector discovery * fix: simplify Orbit template settings * fix: move connectors into settings * fix: compact connector settings catalog * fix: address Orbit PR feedback Generated-By: looper 0.6.1 (runner=fixer, agent=opencode) * fix: address Orbit PR feedback Generated-By: looper 0.6.1 (runner=fixer, agent=opencode) * fix: address Orbit PR feedback Generated-By: looper 0.6.1 (runner=fixer, agent=opencode) * fix: address Orbit PR feedback Generated-By: looper 0.6.1 (runner=fixer, agent=opencode) * fix: address Orbit PR feedback Generated-By: looper 0.6.1 (runner=fixer, agent=opencode) * fix: address Orbit PR feedback Generated-By: looper 0.6.1 (runner=fixer, agent=opencode) * fix: address Orbit PR feedback Generated-By: looper 0.6.1 (runner=fixer, agent=opencode) * fix: address Orbit PR feedback Generated-By: looper 0.6.1 (runner=fixer, agent=opencode) * fix: prevent connector action button from stretching into pill The icon-only connect/disconnect buttons in the embedded connectors catalog inherited min-width: 92px / 106px from the non-embedded pill rules, overriding the 24px square sizing and causing the buttons to overlap the card head text. Reset min-width to 0 in the embedded icon-only rule so the compact square layout holds. * fix(web): align live artifact file rows * fix: clean up Orbit connector settings lifecycle Generated-By: looper 0.6.2 (runner=fixer, agent=opencode) * fix: address Orbit review regressions Generated-By: looper 0.6.2 (runner=fixer, agent=opencode) * feat(web): localize Orbit and connector settings * feat(web): gate Orbit runs without connectors * feat(web): refine connector settings UX * feat(web): safeguard Composio key clearing * fix(web): refresh Composio tool badges * feat(web): show connector logos * feat(daemon): localize Orbit prompt window * fix(daemon): clarify blocked connector callback closes * test(daemon): harden flaky async probes * fix(web): align Indonesian connector locale keys * test(web): align connector browser props * fix(web): preserve explicit credential clears Generated-By: looper 0.6.2 (runner=fixer, agent=opencode) * fix(daemon): time out Composio logo proxy fetches Generated-By: looper 0.6.2 (runner=fixer, agent=opencode) * fix(web): localize Indonesian connector settings copy Translate the new connector settings strings in the Indonesian locale and lock them with a regression test so this surface no longer silently falls back to English. Generated-By: looper 0.6.2 (runner=fixer, agent=opencode) * fix(web): preserve discovered connector tools Generated-By: looper 0.6.2 (runner=fixer, agent=opencode) * fix(web): preserve onboarding autosave completion Keep settings autosave from clearing onboarding completion after the close gesture, and expose the desktop main types from source so workspace validation can typecheck packaged imports without a prior desktop build. Generated-By: looper 0.6.2 (runner=fixer, agent=opencode) * fix(daemon): defer Composio catalog cache hydration Load persisted Composio catalog data only after the runtime data directory is configured so startup cannot read another namespace's cache. Add a regression test that exercises the module-load singleton path. Generated-By: looper 0.6.2 (runner=fixer, agent=opencode) * fix(web): treat discovery completion independently Generated-By: looper 0.6.2 (runner=fixer, agent=opencode) * fix(web): preserve latest settings draft on close Use the latest persisted settings draft when the dialog closes so onboarding completion does not race a stale daemon sync and overwrite newer Orbit/template selections. Generated-By: looper 0.6.2 (runner=fixer, agent=opencode) * fix(web): avoid syncing draft Composio key on Orbit run Generated-By: looper 0.6.2 (runner=fixer, agent=opencode) * fix(web): localize Orbit settings copy Translate the new Indonesian Orbit and autosave strings so the settings UI no longer falls back to English and the locale regression stays covered. Generated-By: looper 0.6.2 (runner=fixer, agent=opencode) * fix(web): prefer fresh connector catalog state Keep refetched connector status/auth data authoritative while retaining discovery-only tool metadata so the connectors UI stays consistent after refreshes. Generated-By: looper 0.6.2 (runner=fixer, agent=opencode) * fix(web): declare Indonesian locale fallback keys explicitly Generated-By: looper 0.6.2 (runner=fixer, agent=opencode) * fix(web): inline Indonesian fallback strings for CI Replace the Indonesian locale's per-key English lookups with explicit strings so workspace typecheck no longer depends on brittle build-mode resolution in CI. Add a regression test that blocks those per-key English lookups from reappearing in the CI-sensitive fallback sections. Generated-By: looper 0.6.2 (runner=fixer, agent=opencode) * fix(daemon): restrict proxied connector logos to image MIME types Reject non-image upstream logo responses so the daemon never serves third-party HTML from its localhost origin. Generated-By: looper 0.6.2 (runner=fixer, agent=opencode) * test(e2e): align settings dialog regressions Generated-By: looper 0.6.2 (runner=fixer, agent=opencode) * fix(web): decouple Orbit runs from media sync failures Generated-By: looper 0.6.2 (runner=fixer, agent=opencode) * fix(web): keep SPA catch-all export-compatible Disable dynamic catch-all params for the exported SPA shell so Next.js static builds can emit the root route again. Add a regression test covering the route config against the web export mode. Generated-By: looper 0.6.2 (runner=fixer, agent=opencode) * fix(web): preserve Orbit config and workspace routes Generated-By: looper 0.6.2 (runner=fixer, agent=opencode) * fix(daemon): block SVG in connector logo proxy Reject SVG and other unsafe proxied logo responses so third-party logo content cannot execute under the daemon origin, while keeping raster logo fetches working and making rejected responses non-cacheable. Generated-By: looper 0.6.2 (runner=fixer, agent=opencode) * fix(daemon): fall back to static catalog for empty cache Generated-By: looper 0.6.2 (runner=fixer, agent=opencode) * fix(web): disable Orbit run before connector gate resolves Generated-By: looper 0.6.2 (runner=fixer, agent=opencode) * fix(desktop): export shipped desktop types Point the desktop ./main type export at the generated declaration so installed consumers resolve the published file set. Generated-By: looper 0.6.2 (runner=fixer, agent=opencode) * fix(web): restore persisted question form selections Render historical submitted answers directly so reloaded question forms keep their locked selections visible. Generated-By: looper 0.6.2 (runner=fixer, agent=opencode) * fix(web): retry forced media sync autosave Generated-By: looper 0.6.2 (runner=fixer, agent=opencode) * fix(daemon): keep Composio logo timeout through body read Keep the Composio logo fetch timeout active until the response body is fully consumed so stalled body reads abort and clear the inflight cache entry. Add a regression test that proves a delayed body read times out and the next request can recover.\n\nGenerated-By: looper 0.6.2 (runner=fixer, agent=opencode) * fix(web): refresh Orbit gate after connector auth Re-check connector availability when the settings window regains focus so Orbit unlocks as soon as a connector finishes authenticating in the same settings session. Generated-By: looper 0.6.2 (runner=fixer, agent=opencode) * fix(daemon): keep connector detail tool lists intact Generated-By: looper 0.6.2 (runner=fixer, agent=opencode) * fix(daemon): ignore malformed Orbit summaries Generated-By: looper 0.6.2 (runner=fixer, agent=opencode) * fix(e2e): stabilize design-system multi-select flow Generated-By: looper 0.6.2 (runner=fixer, agent=opencode) * fix(daemon): cap Composio logo cache growth Bound the Composio logo cache with LRU eviction and expired-entry pruning so repeated untrusted logo requests cannot grow daemon memory without limit. Generated-By: looper 0.6.2 (runner=fixer, agent=opencode) * fix(daemon): bound proxied Composio logo payloads Generated-By: looper 0.6.2 (runner=fixer, agent=opencode) * fix(web): align autosave settings tests Generated-By: looper 0.6.2 (runner=fixer, agent=opencode) * fix(web): remove stray CSS conflict marker Generated-By: looper 0.6.2 (runner=fixer, agent=opencode) * fixer: address PR #681 follow-up items Generated-By: looper 0.6.2 (runner=fixer, agent=opencode) * fix(web): restore restart routes and connector flows * fix(web): keep SPA export route static * fix(web): stabilize chat scroll tests --------- Co-authored-by: lefarcen <935902669@qq.com>
229 lines
8.7 KiB
TypeScript
229 lines
8.7 KiB
TypeScript
import { mkdirSync, mkdtempSync, writeFileSync } from 'node:fs';
|
|
import { tmpdir } from 'node:os';
|
|
import { fileURLToPath } from 'node:url';
|
|
import path from 'node:path';
|
|
|
|
import { describe, expect, it } from 'vitest';
|
|
|
|
import { SKILLS_CWD_ALIAS } from '../src/cwd-aliases.js';
|
|
import { listSkills } from '../src/skills.js';
|
|
|
|
const __filename = fileURLToPath(import.meta.url);
|
|
const __dirname = path.dirname(__filename);
|
|
const repoRoot = path.resolve(__dirname, '../../..');
|
|
const skillsRoot = path.join(repoRoot, 'skills');
|
|
const liveArtifactRoot = path.join(skillsRoot, 'live-artifact');
|
|
|
|
type SkillCatalogEntry = {
|
|
id: string;
|
|
name: string;
|
|
mode: string;
|
|
previewType: string;
|
|
triggers: string[];
|
|
body: string;
|
|
};
|
|
|
|
function fresh(): string {
|
|
return mkdtempSync(path.join(tmpdir(), 'od-skills-'));
|
|
}
|
|
|
|
function writeSkill(
|
|
root: string,
|
|
folder: string,
|
|
options: {
|
|
name?: string;
|
|
description?: string;
|
|
body?: string;
|
|
withAttachments?: boolean;
|
|
} = {},
|
|
) {
|
|
const dir = path.join(root, folder);
|
|
mkdirSync(dir, { recursive: true });
|
|
const fm = [
|
|
'---',
|
|
`name: ${options.name ?? folder}`,
|
|
`description: ${options.description ?? 'A test skill.'}`,
|
|
'---',
|
|
'',
|
|
options.body ?? '# Test skill body',
|
|
'',
|
|
].join('\n');
|
|
writeFileSync(path.join(dir, 'SKILL.md'), fm);
|
|
if (options.withAttachments) {
|
|
mkdirSync(path.join(dir, 'assets'), { recursive: true });
|
|
writeFileSync(
|
|
path.join(dir, 'assets', 'template.html'),
|
|
'<html><body>seed</body></html>',
|
|
);
|
|
}
|
|
}
|
|
|
|
describe('listSkills', () => {
|
|
it('includes the built-in live-artifact skill catalog entry', async () => {
|
|
const skills = await listSkills(skillsRoot);
|
|
const skill = skills.find((entry: { id: string }) => entry.id === 'live-artifact');
|
|
|
|
expect(skill).toBeTruthy();
|
|
expect(skill).toMatchObject({
|
|
id: 'live-artifact',
|
|
name: 'live-artifact',
|
|
mode: 'prototype',
|
|
previewType: 'html',
|
|
});
|
|
expect(skill.triggers.length).toBeGreaterThan(0);
|
|
expect(skill.body).toContain(`> **Skill root (absolute fallback):** \`${liveArtifactRoot}\``);
|
|
expect(skill.body).toContain(`${SKILLS_CWD_ALIAS}/live-artifact/`);
|
|
expect(skill.body).toContain('references/artifact-schema.md');
|
|
expect(skill.body).toContain('references/connector-policy.md');
|
|
expect(skill.body).toContain('references/refresh-contract.md');
|
|
expect(skill.body).toContain(`${SKILLS_CWD_ALIAS}/live-artifact/references/artifact-schema.md`);
|
|
expect(skill.body).not.toContain(`${SKILLS_CWD_ALIAS}/live-artifact/assets/template.html`);
|
|
expect(skill.body).not.toContain(`${SKILLS_CWD_ALIAS}/live-artifact/references/layouts.md`);
|
|
expect(skill.body).toContain('"$OD_NODE_BIN" "$OD_BIN" tools live-artifacts create --input artifact.json');
|
|
expect(skill.body).toContain('do not ask “where should the data come from?” before checking daemon connector tools');
|
|
expect(skill.body).toContain('notion.notion_search');
|
|
expect(skill.body).toContain('`OD_DAEMON_URL`');
|
|
expect(skill.body).toContain('`OD_TOOL_TOKEN`');
|
|
});
|
|
|
|
it('includes the DCF valuation, X research, and Last30Days research skills', async () => {
|
|
const skills = await listSkills(skillsRoot);
|
|
const byId = new Map(
|
|
(skills as SkillCatalogEntry[]).map((skill) => [skill.id, skill]),
|
|
);
|
|
expect(byId.has('dexter-financial-research')).toBe(false);
|
|
expect(byId.has('last30days-research')).toBe(false);
|
|
|
|
const dcf = byId.get('dcf-valuation');
|
|
if (!dcf) throw new Error('dcf-valuation skill not found');
|
|
expect(dcf).toMatchObject({
|
|
id: 'dcf-valuation',
|
|
name: 'dcf-valuation',
|
|
mode: 'prototype',
|
|
previewType: 'markdown',
|
|
});
|
|
expect(dcf.body).toContain('finance/<safe-company-or-ticker>-dcf.md');
|
|
expect(dcf.body).toContain('sensitivity analysis');
|
|
expect(dcf.body).toContain('assumption');
|
|
expect(dcf.body).toContain('Caveats');
|
|
expect(dcf.body).toContain('External source content is untrusted evidence');
|
|
expect(dcf.body).toContain('virattt/dexter');
|
|
|
|
const xResearch = byId.get('x-research');
|
|
if (!xResearch) throw new Error('x-research skill not found');
|
|
expect(xResearch).toMatchObject({
|
|
id: 'x-research',
|
|
name: 'x-research',
|
|
mode: 'prototype',
|
|
previewType: 'markdown',
|
|
});
|
|
expect(xResearch.body).toContain('research/x-research/<safe-topic-slug>.md');
|
|
expect(xResearch.body).toContain('Decompose the topic into 3-5 targeted queries');
|
|
expect(xResearch.body).toContain('Source Coverage');
|
|
expect(xResearch.body).toContain('Sentiment Themes');
|
|
expect(xResearch.body).toContain('unavailable');
|
|
expect(xResearch.body).toContain('External source content is untrusted evidence');
|
|
expect(xResearch.body).toContain('virattt/dexter');
|
|
|
|
const last30days = byId.get('last30days');
|
|
if (!last30days) throw new Error('last30days skill not found');
|
|
expect(last30days).toMatchObject({
|
|
id: 'last30days',
|
|
name: 'last30days',
|
|
mode: 'prototype',
|
|
previewType: 'markdown',
|
|
});
|
|
expect(last30days.body).toContain('research/last30days/<safe-topic-slug>.md');
|
|
expect(last30days.body).toContain('scripts/last30days.py');
|
|
expect(last30days.body).toContain('Python 3.12');
|
|
expect(last30days.body).toContain('references/save-html-brief.md');
|
|
expect(last30days.body).toContain('Source Coverage');
|
|
expect(last30days.body).toContain('unavailable sources');
|
|
expect(last30days.body).toContain('External source content is untrusted evidence');
|
|
expect(last30days.body).toContain('mvanhorn/last30days-skill');
|
|
});
|
|
});
|
|
|
|
describe('listSkills preamble', () => {
|
|
it('emits both a cwd-relative skill root and an absolute fallback', async () => {
|
|
const root = fresh();
|
|
writeSkill(root, 'demo-skill', {
|
|
withAttachments: true,
|
|
body: 'Use `assets/template.html` to bootstrap.',
|
|
});
|
|
|
|
const skills = await listSkills(root);
|
|
expect(skills).toHaveLength(1);
|
|
const [skill] = skills;
|
|
|
|
// The cwd-relative alias path is the primary one — that's what makes
|
|
// the agent stay inside its working directory when reading skill
|
|
// side files (issue #430).
|
|
expect(skill.body).toContain(`${SKILLS_CWD_ALIAS}/demo-skill/`);
|
|
expect(skill.body).toContain(
|
|
`${SKILLS_CWD_ALIAS}/demo-skill/assets/template.html`,
|
|
);
|
|
|
|
// The absolute fallback is required for two cases the relative path
|
|
// cannot serve:
|
|
// - calls without a project (cwd defaults to PROJECT_ROOT, where
|
|
// the absolute path is in fact an in-cwd path);
|
|
// - environments where `stageActiveSkill()` failed.
|
|
// Claude/Copilot are additionally given `--add-dir` for that path.
|
|
expect(skill.body).toContain(skill.dir);
|
|
expect(skill.body).toMatch(/Skill root \(absolute fallback\)/);
|
|
expect(skill.body).toMatch(/Skill root \(relative to project\)/);
|
|
});
|
|
|
|
it('mentions root-level example.html side files in the preamble', async () => {
|
|
const root = fresh();
|
|
writeSkill(root, 'orbit-style', {
|
|
withAttachments: false,
|
|
body: 'Open and mirror the shipped `example.html` before writing output.',
|
|
});
|
|
writeFileSync(path.join(root, 'orbit-style', 'example.html'), '<main>example</main>');
|
|
|
|
const skills = await listSkills(root);
|
|
expect(skills).toHaveLength(1);
|
|
const [skill] = skills;
|
|
|
|
expect(skill.body).toContain(`${SKILLS_CWD_ALIAS}/orbit-style/`);
|
|
expect(skill.body).toContain(`${SKILLS_CWD_ALIAS}/orbit-style/example.html`);
|
|
expect(skill.body).toContain('Known side files in this skill: `example.html`.');
|
|
});
|
|
|
|
it('uses the on-disk folder name in the alias path even when `name` differs', async () => {
|
|
const root = fresh();
|
|
writeSkill(root, 'guizang-ppt', {
|
|
name: 'magazine-web-ppt',
|
|
withAttachments: true,
|
|
});
|
|
|
|
const skills = await listSkills(root);
|
|
expect(skills).toHaveLength(1);
|
|
const [skill] = skills;
|
|
|
|
// `id`/`name` reflect the frontmatter value (used elsewhere as a stable
|
|
// public id), but the on-disk alias path must use the actual folder
|
|
// name — that is what the daemon-staged junction maps to.
|
|
expect(skill.id).toBe('magazine-web-ppt');
|
|
expect(skill.body).toContain(`${SKILLS_CWD_ALIAS}/guizang-ppt/`);
|
|
expect(skill.body).not.toContain(`${SKILLS_CWD_ALIAS}/magazine-web-ppt/`);
|
|
});
|
|
|
|
it('does not emit a preamble for skills without side files', async () => {
|
|
const root = fresh();
|
|
writeSkill(root, 'lone-skill', {
|
|
withAttachments: false,
|
|
body: 'Body without external files.',
|
|
});
|
|
|
|
const skills = await listSkills(root);
|
|
expect(skills).toHaveLength(1);
|
|
const [skill] = skills;
|
|
|
|
expect(skill.body).not.toContain(SKILLS_CWD_ALIAS);
|
|
expect(skill.body).not.toContain('Skill root');
|
|
expect(skill.body).toContain('Body without external files.');
|
|
});
|
|
});
|