mirror of
https://github.com/paperclipai/paperclip.git
synced 2026-10-08 00:54:38 +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>
155 lines
4.5 KiB
TypeScript
155 lines
4.5 KiB
TypeScript
import { describe, expect, it } from "vitest";
|
|
|
|
import { createTestHarness } from "../src/testing.js";
|
|
import type { PaperclipPluginManifestV1 } from "../src/types.js";
|
|
|
|
const manifest = {
|
|
id: "paperclip.test-actions",
|
|
apiVersion: 1,
|
|
version: "1.0.0",
|
|
displayName: "Test Actions",
|
|
description: "Test plugin",
|
|
author: "Paperclip",
|
|
categories: ["automation"],
|
|
capabilities: [],
|
|
entrypoints: {},
|
|
} satisfies PaperclipPluginManifestV1;
|
|
|
|
describe("createTestHarness action context", () => {
|
|
it("passes immutable authenticated actor context and overrides caller company scope", async () => {
|
|
const harness = createTestHarness({ manifest });
|
|
|
|
harness.ctx.actions.register("inspect", async (params, context) => ({
|
|
paramsCompanyId: params.companyId,
|
|
actor: context.actor,
|
|
companyId: context.companyId,
|
|
contextFrozen: Object.isFrozen(context),
|
|
actorFrozen: Object.isFrozen(context.actor),
|
|
}));
|
|
|
|
const result = await harness.performAction<{
|
|
paramsCompanyId: unknown;
|
|
actor: {
|
|
type: string;
|
|
userId: string | null;
|
|
agentId: string | null;
|
|
runId: string | null;
|
|
companyId: string | null;
|
|
};
|
|
companyId: string | null;
|
|
contextFrozen: boolean;
|
|
actorFrozen: boolean;
|
|
}>(
|
|
"inspect",
|
|
{ companyId: "spoofed-company", value: true },
|
|
{
|
|
companyId: "host-company",
|
|
actor: {
|
|
type: "user",
|
|
userId: "board-user-1",
|
|
runId: "run-1",
|
|
},
|
|
},
|
|
);
|
|
|
|
expect(result.paramsCompanyId).toBe("host-company");
|
|
expect(result.companyId).toBe("host-company");
|
|
expect(result.actor).toEqual({
|
|
type: "user",
|
|
userId: "board-user-1",
|
|
agentId: null,
|
|
runId: "run-1",
|
|
companyId: "host-company",
|
|
});
|
|
expect(result.contextFrozen).toBe(true);
|
|
expect(result.actorFrozen).toBe(true);
|
|
});
|
|
|
|
it("keeps existing one-argument action handlers compatible", async () => {
|
|
const harness = createTestHarness({ manifest });
|
|
harness.ctx.actions.register("legacy", async (params) => ({ ok: params.ok }));
|
|
|
|
await expect(harness.performAction("legacy", { ok: true })).resolves.toEqual({ ok: true });
|
|
});
|
|
});
|
|
|
|
describe("createTestHarness managed routines", () => {
|
|
it("preserves declared activity gate settings", async () => {
|
|
const harness = createTestHarness({
|
|
manifest: {
|
|
...manifest,
|
|
capabilities: ["routines.managed"],
|
|
routines: [{
|
|
routineKey: "quiet-watcher",
|
|
title: "Quiet watcher",
|
|
activityGatePolicy: "require_external_activity",
|
|
activityGateScope: "project",
|
|
}],
|
|
},
|
|
});
|
|
|
|
const resolved = await harness.ctx.routines.managed.reconcile("quiet-watcher", "company-1");
|
|
|
|
expect(resolved.routine).toMatchObject({
|
|
activityGatePolicy: "require_external_activity",
|
|
activityGateScope: "project",
|
|
});
|
|
});
|
|
});
|
|
|
|
describe("createTestHarness issue interactions", () => {
|
|
it("creates request_checkbox_confirmation interactions through the typed host helper", async () => {
|
|
const harness = createTestHarness({
|
|
manifest,
|
|
capabilities: ["issues.create", "issue.interactions.create"],
|
|
});
|
|
const issue = await harness.ctx.issues.create({
|
|
companyId: "company-1",
|
|
title: "Pick files",
|
|
});
|
|
|
|
const interaction = await harness.ctx.issues.requestCheckboxConfirmation(
|
|
issue.id,
|
|
{
|
|
idempotencyKey: "checkbox:files",
|
|
title: "Choose files",
|
|
payload: {
|
|
version: 1,
|
|
prompt: "Which files should be included?",
|
|
options: [
|
|
{ id: "file-a", label: "File A" },
|
|
{ id: "file-b", label: "File B", description: "Secondary draft" },
|
|
],
|
|
defaultSelectedOptionIds: ["file-a"],
|
|
minSelected: 1,
|
|
maxSelected: 2,
|
|
},
|
|
},
|
|
"company-1",
|
|
{ authorAgentId: "agent-1" },
|
|
);
|
|
|
|
expect(interaction).toMatchObject({
|
|
issueId: issue.id,
|
|
companyId: "company-1",
|
|
kind: "request_checkbox_confirmation",
|
|
status: "pending",
|
|
continuationPolicy: "wake_assignee",
|
|
idempotencyKey: "checkbox:files",
|
|
title: "Choose files",
|
|
createdByAgentId: "agent-1",
|
|
payload: {
|
|
version: 1,
|
|
prompt: "Which files should be included?",
|
|
options: [
|
|
{ id: "file-a", label: "File A" },
|
|
{ id: "file-b", label: "File B", description: "Secondary draft" },
|
|
],
|
|
defaultSelectedOptionIds: ["file-a"],
|
|
minSelected: 1,
|
|
maxSelected: 2,
|
|
},
|
|
});
|
|
});
|
|
});
|