Files
PaperClipAI/packages/plugins/sdk/tests/testing-actions.test.ts
T
DottaandPaperclip 30ff3d7c58 feat(routines): expose activity gate API (#9438)
## 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>
2026-07-24 16:47:24 -05:00

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,
},
});
});
});