mirror of
https://github.com/alkimake/paperclip.git
synced 2026-06-14 18:10:39 +09:00
merge master into pap-1078-qol-fixes
Resolve the keyboard shortcut conflicts after [#2539](https://github.com/paperclipai/paperclip/pull/2539) and [#2540](https://github.com/paperclipai/paperclip/pull/2540), keep the release package rewrite working with cliVersion, and stabilize the provisioning timeout in the full suite. Co-Authored-By: Paperclip <noreply@paperclip.ing>
This commit is contained in:
commit
fb3b57ab1f
59 changed files with 16794 additions and 375 deletions
|
|
@ -231,11 +231,31 @@ describe("agent skill routes", () => {
|
|||
);
|
||||
});
|
||||
|
||||
it("keeps runtime materialization for persistent skill adapters", async () => {
|
||||
it("skips runtime materialization when listing Codex skills", async () => {
|
||||
mockAgentService.getById.mockResolvedValue(makeAgent("codex_local"));
|
||||
mockAdapter.listSkills.mockResolvedValue({
|
||||
adapterType: "codex_local",
|
||||
supported: true,
|
||||
mode: "ephemeral",
|
||||
desiredSkills: ["paperclipai/paperclip/paperclip"],
|
||||
entries: [],
|
||||
warnings: [],
|
||||
});
|
||||
|
||||
const res = await request(createApp())
|
||||
.get("/api/agents/11111111-1111-4111-8111-111111111111/skills?companyId=company-1");
|
||||
|
||||
expect(res.status, JSON.stringify(res.body)).toBe(200);
|
||||
expect(mockCompanySkillService.listRuntimeSkillEntries).toHaveBeenCalledWith("company-1", {
|
||||
materializeMissing: false,
|
||||
});
|
||||
});
|
||||
|
||||
it("keeps runtime materialization for persistent skill adapters", async () => {
|
||||
mockAgentService.getById.mockResolvedValue(makeAgent("cursor"));
|
||||
mockAdapter.listSkills.mockResolvedValue({
|
||||
adapterType: "cursor",
|
||||
supported: true,
|
||||
mode: "persistent",
|
||||
desiredSkills: ["paperclipai/paperclip/paperclip"],
|
||||
entries: [],
|
||||
|
|
|
|||
|
|
@ -35,6 +35,7 @@ describe("instance settings routes", () => {
|
|||
vi.clearAllMocks();
|
||||
mockInstanceSettingsService.getGeneral.mockResolvedValue({
|
||||
censorUsernameInLogs: false,
|
||||
keyboardShortcuts: false,
|
||||
feedbackDataSharingPreference: "prompt",
|
||||
});
|
||||
mockInstanceSettingsService.getExperimental.mockResolvedValue({
|
||||
|
|
@ -45,6 +46,7 @@ describe("instance settings routes", () => {
|
|||
id: "instance-settings-1",
|
||||
general: {
|
||||
censorUsernameInLogs: true,
|
||||
keyboardShortcuts: true,
|
||||
feedbackDataSharingPreference: "allowed",
|
||||
},
|
||||
});
|
||||
|
|
@ -114,6 +116,7 @@ describe("instance settings routes", () => {
|
|||
expect(getRes.status).toBe(200);
|
||||
expect(getRes.body).toEqual({
|
||||
censorUsernameInLogs: false,
|
||||
keyboardShortcuts: false,
|
||||
feedbackDataSharingPreference: "prompt",
|
||||
});
|
||||
|
||||
|
|
@ -121,18 +124,20 @@ describe("instance settings routes", () => {
|
|||
.patch("/api/instance/settings/general")
|
||||
.send({
|
||||
censorUsernameInLogs: true,
|
||||
keyboardShortcuts: true,
|
||||
feedbackDataSharingPreference: "allowed",
|
||||
});
|
||||
|
||||
expect(patchRes.status).toBe(200);
|
||||
expect(mockInstanceSettingsService.updateGeneral).toHaveBeenCalledWith({
|
||||
censorUsernameInLogs: true,
|
||||
keyboardShortcuts: true,
|
||||
feedbackDataSharingPreference: "allowed",
|
||||
});
|
||||
expect(mockLogActivity).toHaveBeenCalledTimes(2);
|
||||
});
|
||||
|
||||
it("rejects non-admin board users", async () => {
|
||||
it("allows non-admin board users to read general settings", async () => {
|
||||
const app = createApp({
|
||||
type: "board",
|
||||
userId: "user-1",
|
||||
|
|
@ -143,8 +148,25 @@ describe("instance settings routes", () => {
|
|||
|
||||
const res = await request(app).get("/api/instance/settings/general");
|
||||
|
||||
expect(res.status).toBe(200);
|
||||
expect(mockInstanceSettingsService.getGeneral).toHaveBeenCalled();
|
||||
});
|
||||
|
||||
it("rejects non-admin board users from updating general settings", async () => {
|
||||
const app = createApp({
|
||||
type: "board",
|
||||
userId: "user-1",
|
||||
source: "session",
|
||||
isInstanceAdmin: false,
|
||||
companyIds: ["company-1"],
|
||||
});
|
||||
|
||||
const res = await request(app)
|
||||
.patch("/api/instance/settings/general")
|
||||
.send({ censorUsernameInLogs: true, keyboardShortcuts: true });
|
||||
|
||||
expect(res.status).toBe(403);
|
||||
expect(mockInstanceSettingsService.getGeneral).not.toHaveBeenCalled();
|
||||
expect(mockInstanceSettingsService.updateGeneral).not.toHaveBeenCalled();
|
||||
});
|
||||
|
||||
it("rejects agent callers", async () => {
|
||||
|
|
|
|||
|
|
@ -1,4 +1,5 @@
|
|||
import { randomUUID } from "node:crypto";
|
||||
import { eq } from "drizzle-orm";
|
||||
import { afterAll, afterEach, beforeAll, describe, expect, it } from "vitest";
|
||||
import {
|
||||
activityLog,
|
||||
|
|
@ -428,6 +429,160 @@ describeEmbeddedPostgres("issueService.list participantAgentId", () => {
|
|||
resurfacedIssueId,
|
||||
]));
|
||||
});
|
||||
|
||||
it("resurfaces archived issue when status/updatedAt changes after archiving", async () => {
|
||||
const companyId = randomUUID();
|
||||
const userId = "user-1";
|
||||
const otherUserId = "user-2";
|
||||
|
||||
await db.insert(companies).values({
|
||||
id: companyId,
|
||||
name: "Paperclip",
|
||||
issuePrefix: `T${companyId.replace(/-/g, "").slice(0, 6).toUpperCase()}`,
|
||||
requireBoardApprovalForNewAgents: false,
|
||||
});
|
||||
|
||||
const issueId = randomUUID();
|
||||
|
||||
await db.insert(issues).values({
|
||||
id: issueId,
|
||||
companyId,
|
||||
title: "Issue with old comment then status change",
|
||||
status: "todo",
|
||||
priority: "medium",
|
||||
createdByUserId: userId,
|
||||
createdAt: new Date("2026-03-26T10:00:00.000Z"),
|
||||
updatedAt: new Date("2026-03-26T10:00:00.000Z"),
|
||||
});
|
||||
|
||||
// Old external comment before archiving
|
||||
await db.insert(issueComments).values({
|
||||
companyId,
|
||||
issueId,
|
||||
authorUserId: otherUserId,
|
||||
body: "Old comment before archive",
|
||||
createdAt: new Date("2026-03-26T11:00:00.000Z"),
|
||||
updatedAt: new Date("2026-03-26T11:00:00.000Z"),
|
||||
});
|
||||
|
||||
// Archive after seeing the comment
|
||||
await svc.archiveInbox(
|
||||
companyId,
|
||||
issueId,
|
||||
userId,
|
||||
new Date("2026-03-26T12:00:00.000Z"),
|
||||
);
|
||||
|
||||
// Verify it's archived
|
||||
const afterArchive = await svc.list(companyId, {
|
||||
touchedByUserId: userId,
|
||||
inboxArchivedByUserId: userId,
|
||||
});
|
||||
expect(afterArchive.map((i) => i.id)).not.toContain(issueId);
|
||||
|
||||
// Status/work update changes updatedAt (no new comment)
|
||||
await db
|
||||
.update(issues)
|
||||
.set({
|
||||
status: "in_progress",
|
||||
updatedAt: new Date("2026-03-26T13:00:00.000Z"),
|
||||
})
|
||||
.where(eq(issues.id, issueId));
|
||||
|
||||
// Should resurface because updatedAt > archivedAt
|
||||
const afterUpdate = await svc.list(companyId, {
|
||||
touchedByUserId: userId,
|
||||
inboxArchivedByUserId: userId,
|
||||
});
|
||||
expect(afterUpdate.map((i) => i.id)).toContain(issueId);
|
||||
});
|
||||
|
||||
it("sorts and exposes last activity from comments and non-local issue activity logs", async () => {
|
||||
const companyId = randomUUID();
|
||||
const olderIssueId = randomUUID();
|
||||
const commentIssueId = randomUUID();
|
||||
const activityIssueId = randomUUID();
|
||||
|
||||
await db.insert(companies).values({
|
||||
id: companyId,
|
||||
name: "Paperclip",
|
||||
issuePrefix: `T${companyId.replace(/-/g, "").slice(0, 6).toUpperCase()}`,
|
||||
requireBoardApprovalForNewAgents: false,
|
||||
});
|
||||
|
||||
await db.insert(issues).values([
|
||||
{
|
||||
id: olderIssueId,
|
||||
companyId,
|
||||
title: "Older issue",
|
||||
status: "todo",
|
||||
priority: "medium",
|
||||
updatedAt: new Date("2026-03-26T10:00:00.000Z"),
|
||||
},
|
||||
{
|
||||
id: commentIssueId,
|
||||
companyId,
|
||||
title: "Comment activity issue",
|
||||
status: "todo",
|
||||
priority: "medium",
|
||||
updatedAt: new Date("2026-03-26T10:00:00.000Z"),
|
||||
},
|
||||
{
|
||||
id: activityIssueId,
|
||||
companyId,
|
||||
title: "Logged activity issue",
|
||||
status: "todo",
|
||||
priority: "medium",
|
||||
updatedAt: new Date("2026-03-26T10:00:00.000Z"),
|
||||
},
|
||||
]);
|
||||
|
||||
await db.insert(issueComments).values({
|
||||
companyId,
|
||||
issueId: commentIssueId,
|
||||
body: "New comment without touching issue.updatedAt",
|
||||
createdAt: new Date("2026-03-26T11:00:00.000Z"),
|
||||
updatedAt: new Date("2026-03-26T11:00:00.000Z"),
|
||||
});
|
||||
|
||||
await db.insert(activityLog).values([
|
||||
{
|
||||
companyId,
|
||||
actorType: "system",
|
||||
actorId: "system",
|
||||
action: "issue.document_updated",
|
||||
entityType: "issue",
|
||||
entityId: activityIssueId,
|
||||
createdAt: new Date("2026-03-26T12:00:00.000Z"),
|
||||
},
|
||||
{
|
||||
companyId,
|
||||
actorType: "user",
|
||||
actorId: "user-1",
|
||||
action: "issue.read_marked",
|
||||
entityType: "issue",
|
||||
entityId: olderIssueId,
|
||||
createdAt: new Date("2026-03-26T13:00:00.000Z"),
|
||||
},
|
||||
]);
|
||||
|
||||
const result = await svc.list(companyId, {});
|
||||
|
||||
expect(result.map((issue) => issue.id)).toEqual([
|
||||
activityIssueId,
|
||||
commentIssueId,
|
||||
olderIssueId,
|
||||
]);
|
||||
expect(result.find((issue) => issue.id === activityIssueId)?.lastActivityAt?.toISOString()).toBe(
|
||||
"2026-03-26T12:00:00.000Z",
|
||||
);
|
||||
expect(result.find((issue) => issue.id === commentIssueId)?.lastActivityAt?.toISOString()).toBe(
|
||||
"2026-03-26T11:00:00.000Z",
|
||||
);
|
||||
expect(result.find((issue) => issue.id === olderIssueId)?.lastActivityAt?.toISOString()).toBe(
|
||||
"2026-03-26T10:00:00.000Z",
|
||||
);
|
||||
});
|
||||
});
|
||||
|
||||
describeEmbeddedPostgres("issueService.create workspace inheritance", () => {
|
||||
|
|
|
|||
|
|
@ -10,11 +10,13 @@ import {
|
|||
companies,
|
||||
companyMemberships,
|
||||
createDb,
|
||||
executionWorkspaces,
|
||||
heartbeatRunEvents,
|
||||
heartbeatRuns,
|
||||
instanceSettings,
|
||||
issues,
|
||||
principalPermissionGrants,
|
||||
projectWorkspaces,
|
||||
projects,
|
||||
routineRuns,
|
||||
routines,
|
||||
|
|
@ -102,6 +104,8 @@ describeEmbeddedPostgres("routine routes end-to-end", () => {
|
|||
await db.delete(heartbeatRuns);
|
||||
await db.delete(agentWakeupRequests);
|
||||
await db.delete(issues);
|
||||
await db.delete(executionWorkspaces);
|
||||
await db.delete(projectWorkspaces);
|
||||
await db.delete(principalPermissionGrants);
|
||||
await db.delete(companyMemberships);
|
||||
await db.delete(routines);
|
||||
|
|
@ -272,4 +276,136 @@ describeEmbeddedPostgres("routine routes end-to-end", () => {
|
|||
]),
|
||||
);
|
||||
});
|
||||
|
||||
it("runs routines with variable inputs and interpolates the execution issue description", async () => {
|
||||
const { companyId, agentId, projectId, userId } = await seedFixture();
|
||||
const app = await createApp({
|
||||
type: "board",
|
||||
userId,
|
||||
source: "session",
|
||||
isInstanceAdmin: false,
|
||||
companyIds: [companyId],
|
||||
});
|
||||
|
||||
const createRes = await request(app)
|
||||
.post(`/api/companies/${companyId}/routines`)
|
||||
.send({
|
||||
projectId,
|
||||
title: "Repository triage",
|
||||
description: "Review {{repo}} for {{priority}} bugs",
|
||||
assigneeAgentId: agentId,
|
||||
variables: [
|
||||
{ name: "repo", type: "text", required: true },
|
||||
{ name: "priority", type: "select", required: true, defaultValue: "high", options: ["high", "low"] },
|
||||
],
|
||||
});
|
||||
|
||||
expect(createRes.status).toBe(201);
|
||||
|
||||
const runRes = await request(app)
|
||||
.post(`/api/routines/${createRes.body.id}/run`)
|
||||
.send({
|
||||
source: "manual",
|
||||
variables: { repo: "paperclip" },
|
||||
});
|
||||
|
||||
expect(runRes.status).toBe(202);
|
||||
expect(runRes.body.triggerPayload).toEqual({
|
||||
variables: {
|
||||
repo: "paperclip",
|
||||
priority: "high",
|
||||
},
|
||||
});
|
||||
|
||||
const [issue] = await db
|
||||
.select({ description: issues.description })
|
||||
.from(issues)
|
||||
.where(eq(issues.id, runRes.body.linkedIssueId));
|
||||
|
||||
expect(issue?.description).toBe("Review paperclip for high bugs");
|
||||
});
|
||||
|
||||
it("persists execution workspace selections from manual routine runs", async () => {
|
||||
const { companyId, agentId, projectId, userId } = await seedFixture();
|
||||
const projectWorkspaceId = randomUUID();
|
||||
const executionWorkspaceId = randomUUID();
|
||||
const app = await createApp({
|
||||
type: "board",
|
||||
userId,
|
||||
source: "session",
|
||||
isInstanceAdmin: false,
|
||||
companyIds: [companyId],
|
||||
});
|
||||
|
||||
await db.insert(projectWorkspaces).values({
|
||||
id: projectWorkspaceId,
|
||||
companyId,
|
||||
projectId,
|
||||
name: "Primary workspace",
|
||||
isPrimary: true,
|
||||
sharedWorkspaceKey: "routine-primary",
|
||||
});
|
||||
await db.insert(executionWorkspaces).values({
|
||||
id: executionWorkspaceId,
|
||||
companyId,
|
||||
projectId,
|
||||
projectWorkspaceId,
|
||||
mode: "isolated_workspace",
|
||||
strategyType: "git_worktree",
|
||||
name: "Routine worktree",
|
||||
status: "active",
|
||||
providerType: "git_worktree",
|
||||
});
|
||||
await db
|
||||
.update(projects)
|
||||
.set({
|
||||
executionWorkspacePolicy: {
|
||||
enabled: true,
|
||||
defaultMode: "shared_workspace",
|
||||
defaultProjectWorkspaceId: projectWorkspaceId,
|
||||
},
|
||||
})
|
||||
.where(eq(projects.id, projectId));
|
||||
await db.insert(instanceSettings).values({
|
||||
experimental: { enableIsolatedWorkspaces: true },
|
||||
});
|
||||
|
||||
const createRes = await request(app)
|
||||
.post(`/api/companies/${companyId}/routines`)
|
||||
.send({
|
||||
projectId,
|
||||
title: "Workspace-aware routine",
|
||||
assigneeAgentId: agentId,
|
||||
});
|
||||
|
||||
expect(createRes.status).toBe(201);
|
||||
|
||||
const runRes = await request(app)
|
||||
.post(`/api/routines/${createRes.body.id}/run`)
|
||||
.send({
|
||||
source: "manual",
|
||||
executionWorkspaceId,
|
||||
executionWorkspacePreference: "reuse_existing",
|
||||
executionWorkspaceSettings: { mode: "isolated_workspace" },
|
||||
});
|
||||
|
||||
expect(runRes.status).toBe(202);
|
||||
|
||||
const [issue] = await db
|
||||
.select({
|
||||
projectWorkspaceId: issues.projectWorkspaceId,
|
||||
executionWorkspaceId: issues.executionWorkspaceId,
|
||||
executionWorkspacePreference: issues.executionWorkspacePreference,
|
||||
executionWorkspaceSettings: issues.executionWorkspaceSettings,
|
||||
})
|
||||
.from(issues)
|
||||
.where(eq(issues.id, runRes.body.linkedIssueId));
|
||||
|
||||
expect(issue).toEqual({
|
||||
projectWorkspaceId,
|
||||
executionWorkspaceId,
|
||||
executionWorkspacePreference: "reuse_existing",
|
||||
executionWorkspaceSettings: { mode: "isolated_workspace" },
|
||||
});
|
||||
});
|
||||
});
|
||||
|
|
|
|||
|
|
@ -8,8 +8,11 @@ import {
|
|||
companySecrets,
|
||||
companySecretVersions,
|
||||
createDb,
|
||||
executionWorkspaces,
|
||||
heartbeatRuns,
|
||||
instanceSettings,
|
||||
issues,
|
||||
projectWorkspaces,
|
||||
projects,
|
||||
routineRuns,
|
||||
routines,
|
||||
|
|
@ -20,6 +23,7 @@ import {
|
|||
startEmbeddedPostgresTestDatabase,
|
||||
} from "./helpers/embedded-postgres.js";
|
||||
import { issueService } from "../services/issues.ts";
|
||||
import { instanceSettingsService } from "../services/instance-settings.ts";
|
||||
import { routineService } from "../services/routines.ts";
|
||||
|
||||
const embeddedPostgresSupport = await getEmbeddedPostgresTestSupport();
|
||||
|
|
@ -49,9 +53,12 @@ describeEmbeddedPostgres("routine service live-execution coalescing", () => {
|
|||
await db.delete(companySecrets);
|
||||
await db.delete(heartbeatRuns);
|
||||
await db.delete(issues);
|
||||
await db.delete(executionWorkspaces);
|
||||
await db.delete(projectWorkspaces);
|
||||
await db.delete(projects);
|
||||
await db.delete(agents);
|
||||
await db.delete(companies);
|
||||
await db.delete(instanceSettings);
|
||||
});
|
||||
|
||||
afterAll(async () => {
|
||||
|
|
@ -317,6 +324,196 @@ describeEmbeddedPostgres("routine service live-execution coalescing", () => {
|
|||
expect(routineIssues[0]?.id).toBe(previousIssue.id);
|
||||
});
|
||||
|
||||
it("interpolates routine variables into the execution issue and stores resolved values", async () => {
|
||||
const { companyId, agentId, projectId, svc } = await seedFixture();
|
||||
const variableRoutine = await svc.create(
|
||||
companyId,
|
||||
{
|
||||
projectId,
|
||||
goalId: null,
|
||||
parentIssueId: null,
|
||||
title: "repo triage",
|
||||
description: "Review {{repo}} for {{priority}} bugs",
|
||||
assigneeAgentId: agentId,
|
||||
priority: "medium",
|
||||
status: "active",
|
||||
concurrencyPolicy: "coalesce_if_active",
|
||||
catchUpPolicy: "skip_missed",
|
||||
variables: [
|
||||
{ name: "repo", label: null, type: "text", defaultValue: null, required: true, options: [] },
|
||||
{ name: "priority", label: null, type: "select", defaultValue: "high", required: true, options: ["high", "low"] },
|
||||
],
|
||||
},
|
||||
{},
|
||||
);
|
||||
|
||||
const run = await svc.runRoutine(variableRoutine.id, {
|
||||
source: "manual",
|
||||
variables: { repo: "paperclip" },
|
||||
});
|
||||
|
||||
const storedIssue = await db
|
||||
.select({ description: issues.description })
|
||||
.from(issues)
|
||||
.where(eq(issues.id, run.linkedIssueId!))
|
||||
.then((rows) => rows[0] ?? null);
|
||||
const storedRun = await db
|
||||
.select({ triggerPayload: routineRuns.triggerPayload })
|
||||
.from(routineRuns)
|
||||
.where(eq(routineRuns.id, run.id))
|
||||
.then((rows) => rows[0] ?? null);
|
||||
|
||||
expect(storedIssue?.description).toBe("Review paperclip for high bugs");
|
||||
expect(storedRun?.triggerPayload).toEqual({
|
||||
variables: {
|
||||
repo: "paperclip",
|
||||
priority: "high",
|
||||
},
|
||||
});
|
||||
});
|
||||
|
||||
it("attaches the selected execution workspace to manually triggered routine issues", async () => {
|
||||
const { companyId, projectId, routine, svc } = await seedFixture();
|
||||
const projectWorkspaceId = randomUUID();
|
||||
const executionWorkspaceId = randomUUID();
|
||||
|
||||
await instanceSettingsService(db).updateExperimental({ enableIsolatedWorkspaces: true });
|
||||
await db
|
||||
.update(projects)
|
||||
.set({
|
||||
executionWorkspacePolicy: {
|
||||
enabled: true,
|
||||
defaultMode: "shared_workspace",
|
||||
defaultProjectWorkspaceId: projectWorkspaceId,
|
||||
},
|
||||
})
|
||||
.where(eq(projects.id, projectId));
|
||||
await db.insert(projectWorkspaces).values({
|
||||
id: projectWorkspaceId,
|
||||
companyId,
|
||||
projectId,
|
||||
name: "Primary workspace",
|
||||
isPrimary: true,
|
||||
sharedWorkspaceKey: "routine-primary",
|
||||
});
|
||||
await db.insert(executionWorkspaces).values({
|
||||
id: executionWorkspaceId,
|
||||
companyId,
|
||||
projectId,
|
||||
projectWorkspaceId,
|
||||
mode: "isolated_workspace",
|
||||
strategyType: "git_worktree",
|
||||
name: "Routine worktree",
|
||||
status: "active",
|
||||
providerType: "git_worktree",
|
||||
});
|
||||
|
||||
const run = await svc.runRoutine(routine.id, {
|
||||
source: "manual",
|
||||
executionWorkspaceId,
|
||||
executionWorkspacePreference: "reuse_existing",
|
||||
executionWorkspaceSettings: { mode: "isolated_workspace" },
|
||||
});
|
||||
|
||||
const storedIssue = await db
|
||||
.select({
|
||||
projectWorkspaceId: issues.projectWorkspaceId,
|
||||
executionWorkspaceId: issues.executionWorkspaceId,
|
||||
executionWorkspacePreference: issues.executionWorkspacePreference,
|
||||
executionWorkspaceSettings: issues.executionWorkspaceSettings,
|
||||
})
|
||||
.from(issues)
|
||||
.where(eq(issues.id, run.linkedIssueId!))
|
||||
.then((rows) => rows[0] ?? null);
|
||||
|
||||
expect(storedIssue).toEqual({
|
||||
projectWorkspaceId,
|
||||
executionWorkspaceId,
|
||||
executionWorkspacePreference: "reuse_existing",
|
||||
executionWorkspaceSettings: { mode: "isolated_workspace" },
|
||||
});
|
||||
});
|
||||
|
||||
it("blocks schedule triggers when required variables do not have defaults", async () => {
|
||||
const { companyId, agentId, projectId, svc } = await seedFixture();
|
||||
const variableRoutine = await svc.create(
|
||||
companyId,
|
||||
{
|
||||
projectId,
|
||||
goalId: null,
|
||||
parentIssueId: null,
|
||||
title: "repo triage",
|
||||
description: "Review {{repo}}",
|
||||
assigneeAgentId: agentId,
|
||||
priority: "medium",
|
||||
status: "active",
|
||||
concurrencyPolicy: "coalesce_if_active",
|
||||
catchUpPolicy: "skip_missed",
|
||||
variables: [
|
||||
{ name: "repo", label: null, type: "text", defaultValue: null, required: true, options: [] },
|
||||
],
|
||||
},
|
||||
{},
|
||||
);
|
||||
|
||||
await expect(
|
||||
svc.createTrigger(variableRoutine.id, {
|
||||
kind: "schedule",
|
||||
label: "daily",
|
||||
cronExpression: "0 10 * * *",
|
||||
timezone: "UTC",
|
||||
}, {}),
|
||||
).rejects.toThrow(/require defaults for required variables/i);
|
||||
});
|
||||
|
||||
it("treats malformed stored defaults as missing when validating schedule triggers", async () => {
|
||||
const { companyId, agentId, projectId, svc } = await seedFixture();
|
||||
const variableRoutine = await svc.create(
|
||||
companyId,
|
||||
{
|
||||
projectId,
|
||||
goalId: null,
|
||||
parentIssueId: null,
|
||||
title: "ship check",
|
||||
description: "Review {{approved}}",
|
||||
assigneeAgentId: agentId,
|
||||
priority: "medium",
|
||||
status: "active",
|
||||
concurrencyPolicy: "coalesce_if_active",
|
||||
catchUpPolicy: "skip_missed",
|
||||
variables: [
|
||||
{ name: "approved", label: null, type: "boolean", defaultValue: true, required: true, options: [] },
|
||||
],
|
||||
},
|
||||
{},
|
||||
);
|
||||
|
||||
await db
|
||||
.update(routines)
|
||||
.set({
|
||||
variables: [
|
||||
{
|
||||
name: "approved",
|
||||
label: null,
|
||||
type: "boolean",
|
||||
defaultValue: "definitely",
|
||||
required: true,
|
||||
options: [],
|
||||
},
|
||||
],
|
||||
})
|
||||
.where(eq(routines.id, variableRoutine.id));
|
||||
|
||||
await expect(
|
||||
svc.createTrigger(variableRoutine.id, {
|
||||
kind: "schedule",
|
||||
label: "daily",
|
||||
cronExpression: "0 10 * * *",
|
||||
timezone: "UTC",
|
||||
}, {}),
|
||||
).rejects.toThrow(/require defaults for required variables/i);
|
||||
});
|
||||
|
||||
it("serializes concurrent dispatches until the first execution issue is linked to a queued run", async () => {
|
||||
const { routine, svc } = await seedFixture({
|
||||
wakeup: async (wakeupAgentId, wakeupOpts) => {
|
||||
|
|
|
|||
|
|
@ -552,7 +552,7 @@ describe("realizeExecutionWorkspace", () => {
|
|||
} finally {
|
||||
process.chdir(previousCwd);
|
||||
}
|
||||
});
|
||||
}, 15_000);
|
||||
|
||||
it("records worktree setup and provision operations when a recorder is provided", async () => {
|
||||
const repoRoot = await createTempRepo();
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue