mirror of
https://github.com/paperclipai/paperclip.git
synced 2026-10-12 18:06:11 +02:00
## Thinking Path > - Paperclip is the open source app people use to manage AI agents for work > - Scheduled routines provide recurring control-plane work without manual intervention > - The new activity gate can suppress scheduled runs when no external work occurred > - The core scheduler and database support landed without a public create/update contract > - Agents, operators, and managed plugins need validated fields plus discoverable semantics to opt in safely > - This pull request exposes the activity gate through routine APIs, revisions, plugin contracts, tests, and skill documentation > - The benefit is backward-compatible control over idle scheduled work without losing activity-triggered follow-up ## Linked Issues or Issue Description - Refs #8534 ## What Changed - Added shared activity-gate policy and scope enums with create/PATCH validation. - Persisted activity-gate fields through routine creation, updates, revision snapshots, pipeline snapshots, and revision restores. - Defaulted legacy revision snapshots during restore and added regression coverage for pre-field snapshots. - Extended managed-plugin routine declarations, production reconciliation, and the SDK test harness to preserve non-default gate settings. - Added end-to-end API coverage for create/PATCH/list/detail round-trips, defaults, and invalid enum rejection. - Documented schedule-only semantics, activity windows, own-run/read-action exclusions, scopes, and an hourly quiet-night watcher example. ## Verification - `pnpm exec vitest run packages/shared/src/validators/routine.test.ts server/src/__tests__/routines-service.test.ts server/src/__tests__/routines-e2e.test.ts` - `pnpm exec vitest run packages/shared/src/validators/plugin.test.ts packages/plugins/sdk/tests/testing-actions.test.ts server/src/__tests__/plugin-managed-routines.test.ts server/src/__tests__/routines-service.test.ts -t 'activity gate|preserves declared activity gate settings|resolves routine agent and project refs'` - `pnpm exec vitest run ui/src/lib/workspace-routines.test.ts ui/src/pages/Routines.test.tsx` - `pnpm --filter @paperclipai/shared typecheck` - `pnpm --filter @paperclipai/plugin-sdk typecheck` - `pnpm --filter @paperclipai/server typecheck` - `pnpm --filter @paperclipai/ui typecheck` - `pnpm check:token-gates` - GitHub CI: all final-head checks green; Storybook visual regression skipped by path rules. - Greptile: 5/5 with no unresolved review threads. ## Risks - Low risk: defaults remain `always` and `company`, preserving existing routine behavior and old revision snapshots. - Managed plugin manifests can now declare the same validated gate settings as the public routine API; omitted values retain core defaults. - Revision snapshots now include the new fields so policy changes are not lost or treated as no-ops during restore. > For core feature work, checked `ROADMAP.md`: this extends the existing Scheduled Routines roadmap item and does not duplicate a separate planned capability. ## Model Used - OpenAI GPT-5.5 via Codex CLI, with repository tool use and code execution; context-window size was not exposed by the runtime. ## Checklist - [x] I have included a thinking path that traces from project context to this change - [x] I have specified the model used (with version and capability details) - [x] I have checked ROADMAP.md and confirmed this PR does not duplicate planned core work - [x] I have searched GitHub for duplicate or related PRs and linked them above - [x] I have either (a) linked existing issues with `Fixes: #` / `Closes #` / `Refs #` OR (b) described the issue in-PR following the relevant issue template - [x] I have not referenced internal/instance-local Paperclip issues or links (only public GitHub `#NNN` / `github.com/paperclipai/paperclip` URLs) - [x] My branch name describes the change (e.g. `docs/...`, `fix/...`) and contains no internal Paperclip ticket id or instance-derived details - [x] I have run tests locally and they pass - [x] I have added or updated tests where applicable - [x] I have updated relevant documentation to reflect my changes - [x] I have considered and documented any risks above - [x] All Paperclip CI gates are green - [x] Greptile is 5/5 with no open P2s, recommendations, or follow-ups - [x] I will address all Greptile and reviewer comments before requesting merge --------- Co-authored-by: Paperclip <noreply@paperclip.ing>
129 lines
3.7 KiB
TypeScript
129 lines
3.7 KiB
TypeScript
import { describe, expect, it } from "vitest";
|
|
import {
|
|
routineRevisionSnapshotV1Schema,
|
|
routineVariableSchema,
|
|
updateRoutineSchema,
|
|
} from "./routine.js";
|
|
|
|
const routineId = "11111111-1111-4111-8111-111111111111";
|
|
const companyId = "22222222-2222-4222-8222-222222222222";
|
|
const triggerId = "33333333-3333-4333-8333-333333333333";
|
|
const baseRevisionId = "44444444-4444-4444-8444-444444444444";
|
|
|
|
describe("routine validators", () => {
|
|
it("accepts versioned routine revision snapshots with safe trigger metadata", () => {
|
|
const parsed = routineRevisionSnapshotV1Schema.parse({
|
|
version: 1,
|
|
routine: {
|
|
id: routineId,
|
|
companyId,
|
|
projectId: null,
|
|
goalId: null,
|
|
parentIssueId: null,
|
|
title: "Daily triage",
|
|
description: null,
|
|
assigneeAgentId: null,
|
|
priority: "medium",
|
|
status: "active",
|
|
concurrencyPolicy: "coalesce_if_active",
|
|
catchUpPolicy: "skip_missed",
|
|
variables: [],
|
|
},
|
|
triggers: [{
|
|
id: triggerId,
|
|
kind: "webhook",
|
|
label: "Inbound",
|
|
enabled: true,
|
|
cronExpression: null,
|
|
timezone: null,
|
|
publicId: "routine_webhook_123",
|
|
signingMode: "bearer",
|
|
replayWindowSec: 300,
|
|
}],
|
|
});
|
|
|
|
expect(parsed.triggers[0]?.publicId).toBe("routine_webhook_123");
|
|
expect(parsed.routine.activityGatePolicy).toBe("always");
|
|
expect(parsed.routine.activityGateScope).toBe("company");
|
|
});
|
|
|
|
it("rejects secret-bearing trigger fields in routine revision snapshots", () => {
|
|
expect(() => routineRevisionSnapshotV1Schema.parse({
|
|
version: 1,
|
|
routine: {
|
|
id: routineId,
|
|
companyId,
|
|
projectId: null,
|
|
goalId: null,
|
|
parentIssueId: null,
|
|
title: "Daily triage",
|
|
description: null,
|
|
assigneeAgentId: null,
|
|
priority: "medium",
|
|
status: "active",
|
|
concurrencyPolicy: "coalesce_if_active",
|
|
catchUpPolicy: "skip_missed",
|
|
variables: [],
|
|
},
|
|
triggers: [{
|
|
id: triggerId,
|
|
kind: "webhook",
|
|
label: "Inbound",
|
|
enabled: true,
|
|
cronExpression: null,
|
|
timezone: null,
|
|
publicId: "routine_webhook_123",
|
|
signingMode: "bearer",
|
|
replayWindowSec: 300,
|
|
secretId: "55555555-5555-4555-8555-555555555555",
|
|
}],
|
|
})).toThrow();
|
|
});
|
|
|
|
it("accepts optional base revision ids on routine updates", () => {
|
|
expect(updateRoutineSchema.parse({
|
|
title: "Daily triage",
|
|
baseRevisionId,
|
|
}).baseRevisionId).toBe(baseRevisionId);
|
|
});
|
|
|
|
it("validates routine activity gate values", () => {
|
|
expect(updateRoutineSchema.parse({
|
|
activityGatePolicy: "require_external_activity",
|
|
activityGateScope: "project",
|
|
})).toMatchObject({
|
|
activityGatePolicy: "require_external_activity",
|
|
activityGateScope: "project",
|
|
});
|
|
|
|
expect(() => updateRoutineSchema.parse({ activityGatePolicy: "when_busy" })).toThrow();
|
|
expect(() => updateRoutineSchema.parse({ activityGateScope: "agent" })).toThrow();
|
|
});
|
|
|
|
it("accepts date variables with valid YYYY-MM-DD defaults", () => {
|
|
expect(routineVariableSchema.parse({
|
|
name: "startDate",
|
|
type: "date",
|
|
defaultValue: "2024-02-29",
|
|
})).toMatchObject({
|
|
name: "startDate",
|
|
type: "date",
|
|
defaultValue: "2024-02-29",
|
|
});
|
|
});
|
|
|
|
it("rejects date variables with non-calendar or non-string defaults", () => {
|
|
expect(() => routineVariableSchema.parse({
|
|
name: "startDate",
|
|
type: "date",
|
|
defaultValue: "2024-02-30",
|
|
})).toThrow(/YYYY-MM-DD/);
|
|
|
|
expect(() => routineVariableSchema.parse({
|
|
name: "startDate",
|
|
type: "date",
|
|
defaultValue: 20240229,
|
|
})).toThrow(/YYYY-MM-DD/);
|
|
});
|
|
});
|