feat(assistant): optimize MCP and custom widget authoring (#6743)
* feat(assistant): optimize MCP and custom widget authoring * fix(assistant): isolate server-safe JSX analyzer * refactor(assistant): generate MCP tool staging * fix(assistant): preserve widget placement follow-up * fix(ci): allow fast gate to finish * fix(custom-widgets): address review findings * fix(custom-widgets): split preview evidence module * style: format assistant custom widget files * fix(custom-widgets): sync formatted skill schema
This commit is contained in:
1 parent
38f4b0e1f8
commit
5e23659d99
95 files changed
+6799
-1778
No files matched your search
@@ -18,6 +18,7 @@ import { configureCustomWidgetSourceFromRequest } from "../../custom-widget/secr
|
||||
import {
|
||||
appendPreviewJournal,
|
||||
configurePreviewSessionSource,
|
||||
getPreviewJournal,
|
||||
getPreviewSession,
|
||||
} from "../../custom-widget/preview-sessions";
|
||||
import { serializeCustomWidgetDefinition } from "../../custom-widget/stored-definition";
|
||||
@@ -212,6 +213,125 @@ describe("custom widget definition persistence", () => {
|
||||
});
|
||||
});
|
||||
|
||||
test("requires current-revision query and action evidence after a template-only preview revision", async () => {
|
||||
const db = await prepareDatabase();
|
||||
const caller = createCaller(db);
|
||||
const definition = customWidgetDefinitionSchema.parse({
|
||||
$schema: "homarr-custom-widget-v2",
|
||||
name: "Request queue",
|
||||
sources: {
|
||||
default: { baseUrl: "https://example.com", networkScope: "public", auth: "none" },
|
||||
},
|
||||
requests: {
|
||||
queue: { path: "/requests" },
|
||||
approve: {
|
||||
kind: "action",
|
||||
method: "POST",
|
||||
path: "/requests/{param:id}/approve",
|
||||
confirmation: "Approve this request?",
|
||||
invalidates: ["queue"],
|
||||
},
|
||||
},
|
||||
options: {},
|
||||
template:
|
||||
'<Stack><Text>{data.queue?.length ?? 0} requests</Text><ActionButton requestId="approve" params={{ id: 1 }}>Approve</ActionButton></Stack>',
|
||||
});
|
||||
const preview = await caller.previewCreate({ definition, secrets: [] });
|
||||
const createInput = { previewSessionId: preview.previewSession.id };
|
||||
const initialSession = await getPreviewSession(preview.previewSession.id, userId);
|
||||
await appendPreviewJournal(initialSession, {
|
||||
requestId: "queue",
|
||||
kind: "query",
|
||||
method: "GET",
|
||||
path: "/requests",
|
||||
status: 200,
|
||||
durationMs: 1,
|
||||
simulated: false,
|
||||
});
|
||||
|
||||
await expect(caller.createFromPreview(createInput)).rejects.toThrow(
|
||||
"Test every final preview action before creating the widget: approve",
|
||||
);
|
||||
await appendPreviewJournal(initialSession, {
|
||||
requestId: "approve",
|
||||
kind: "action",
|
||||
method: "POST",
|
||||
path: "/requests/{param:id}/approve",
|
||||
status: null,
|
||||
durationMs: 0,
|
||||
simulated: true,
|
||||
});
|
||||
const revisedTemplate = definition.template.replace("requests</Text>", "pending requests</Text>");
|
||||
await caller.previewReviseTemplate({
|
||||
sessionId: initialSession.id,
|
||||
expectedRevision: initialSession.revision,
|
||||
template: revisedTemplate,
|
||||
});
|
||||
|
||||
await expect(caller.createFromPreview(createInput)).rejects.toThrow(
|
||||
"Test every final preview query successfully before creating the widget: queue",
|
||||
);
|
||||
const revisedSession = await getPreviewSession(preview.previewSession.id, userId);
|
||||
await appendPreviewJournal(revisedSession, {
|
||||
requestId: "queue",
|
||||
kind: "query",
|
||||
method: "GET",
|
||||
path: "/requests",
|
||||
status: 200,
|
||||
durationMs: 1,
|
||||
simulated: false,
|
||||
});
|
||||
await appendPreviewJournal(revisedSession, {
|
||||
requestId: "approve",
|
||||
kind: "action",
|
||||
method: "POST",
|
||||
path: "/requests/{param:id}/approve",
|
||||
status: null,
|
||||
durationMs: 0,
|
||||
simulated: true,
|
||||
});
|
||||
|
||||
const created = await caller.createFromPreview(createInput);
|
||||
await expect(caller.get({ id: created.id })).resolves.toMatchObject({ template: revisedTemplate });
|
||||
});
|
||||
|
||||
test("retains persistence evidence for every request beyond the bounded journal", async () => {
|
||||
const db = await prepareDatabase();
|
||||
const caller = createCaller(db);
|
||||
const requests = Object.fromEntries(
|
||||
Array.from({ length: 64 }, (_, index) => [`query-${index}`, { path: `/queries/${index}` }]),
|
||||
);
|
||||
const definition = customWidgetDefinitionSchema.parse({
|
||||
$schema: "homarr-custom-widget-v2",
|
||||
name: "Large request set",
|
||||
sources: {
|
||||
default: { baseUrl: "https://example.com", networkScope: "public", auth: "none" },
|
||||
},
|
||||
requests,
|
||||
options: {},
|
||||
template: "<Text>All requests tested</Text>",
|
||||
});
|
||||
const preview = await caller.previewCreate({ definition, secrets: [] });
|
||||
const previewSession = await getPreviewSession(preview.previewSession.id, userId);
|
||||
|
||||
for (const [requestId, request] of Object.entries(previewSession.requests)) {
|
||||
await appendPreviewJournal(previewSession, {
|
||||
requestId,
|
||||
kind: "query",
|
||||
method: request.method,
|
||||
path: request.path,
|
||||
status: 200,
|
||||
durationMs: 1,
|
||||
simulated: false,
|
||||
});
|
||||
}
|
||||
|
||||
expect(await getPreviewJournal(previewSession.id, userId)).toHaveLength(50);
|
||||
await expect(caller.createFromPreview({ previewSessionId: previewSession.id })).resolves.toMatchObject({
|
||||
managementPath: expect.stringContaining("/manage/custom-widgets/edit/"),
|
||||
});
|
||||
});
|
||||
|
||||
test("updates a saved widget with MCP-friendly multiline templateLines", async () => {
|
||||
const db = await prepareDatabase();
|
||||
const caller = createCaller(db);
|
||||
|
||||
@@ -91,4 +91,52 @@ describe("custom widget agent preview workflow", () => {
|
||||
});
|
||||
expect(mocks.executeRequest).not.toHaveBeenCalled();
|
||||
});
|
||||
|
||||
test("revises a preview template without resending or changing its manifest", async () => {
|
||||
const db = createDb();
|
||||
const userId = createId();
|
||||
await db.insert(users).values({ id: userId });
|
||||
const session = {
|
||||
user: { id: userId, permissions: ["admin"], colorScheme: "light" },
|
||||
expires: new Date(Date.now() + 60_000).toISOString(),
|
||||
} satisfies Session;
|
||||
const caller = customWidgetRouter.createCaller({ db, deviceType: undefined, session });
|
||||
const { template, ...definition } = CUSTOM_WIDGET_STARTER;
|
||||
const preview = await caller.previewCreate({
|
||||
definition: {
|
||||
...definition,
|
||||
requests: { fixtures: { path: "/fixtures" } },
|
||||
templateLines: template.split("\n"),
|
||||
},
|
||||
secrets: [],
|
||||
});
|
||||
const revisedTemplate = "<Stack><Text>{data.fixtures?.name}</Text><Badge>Ready</Badge></Stack>";
|
||||
|
||||
const revised = await caller.previewReviseTemplate({
|
||||
sessionId: preview.previewSession.id,
|
||||
expectedRevision: 0,
|
||||
templateLines: revisedTemplate.split("\n"),
|
||||
});
|
||||
|
||||
expect(revised).toMatchObject({
|
||||
success: true,
|
||||
evidenceReset: true,
|
||||
previewSession: { id: preview.previewSession.id, revision: 1 },
|
||||
queries: [expect.objectContaining({ requestId: "fixtures" })],
|
||||
});
|
||||
expect(revised).not.toHaveProperty("definition");
|
||||
expect(revised).not.toHaveProperty("template");
|
||||
await expect(caller.previewGet({ sessionId: preview.previewSession.id })).resolves.toMatchObject({
|
||||
revision: 1,
|
||||
template: revisedTemplate,
|
||||
requests: [expect.objectContaining({ id: "fixtures" })],
|
||||
});
|
||||
await expect(
|
||||
caller.previewReviseTemplate({
|
||||
sessionId: preview.previewSession.id,
|
||||
expectedRevision: 1,
|
||||
templateLines: ['<ActionButton requestId="missing">Run</ActionButton>'],
|
||||
}),
|
||||
).rejects.toMatchObject({ code: "BAD_REQUEST" });
|
||||
});
|
||||
});
|
||||
Reference in new issue
Block a user