Files
PaperClipAI/ui/src/lib/workspace-routines.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

103 lines
3.1 KiB
TypeScript

import type { RoutineListItem } from "@paperclipai/shared";
import { describe, expect, it } from "vitest";
import {
getWorkspaceSpecificRoutineVariableNames,
routineHasWorkspaceSpecificVariables,
sortWorkspaceRoutinesByName,
} from "./workspace-routines";
function createRoutine(overrides: Partial<RoutineListItem> = {}): RoutineListItem {
return {
id: "routine-1",
companyId: "company-1",
projectId: "project-1",
goalId: null,
parentIssueId: null,
responsibleUserId: null,
title: "Routine title",
description: null,
assigneeAgentId: "agent-1",
priority: "medium",
status: "active",
concurrencyPolicy: "coalesce_if_active",
catchUpPolicy: "skip_missed",
activityGatePolicy: "always",
activityGateScope: "company",
variables: [],
latestRevisionId: null,
latestRevisionNumber: 1,
createdByAgentId: null,
createdByUserId: null,
updatedByAgentId: null,
updatedByUserId: null,
lastTriggeredAt: null,
lastEnqueuedAt: null,
createdAt: new Date("2026-04-30T00:00:00.000Z"),
updatedAt: new Date("2026-04-30T00:00:00.000Z"),
triggers: [],
lastRun: null,
activeIssue: null,
...overrides,
};
}
describe("workspace routine helpers", () => {
it("matches routines with explicit workspace variables", () => {
const routine = createRoutine({
variables: [
{ name: "workspaceBranch", label: null, type: "text", defaultValue: null, required: true, options: [] },
],
});
expect(routineHasWorkspaceSpecificVariables(routine)).toBe(true);
expect(getWorkspaceSpecificRoutineVariableNames(routine)).toEqual(["workspaceBranch"]);
});
it("matches routines that reference workspace variables in templates", () => {
const routine = createRoutine({
title: "Review {{ workspaceBranch }}",
description: "Check branch {{workspaceBranch}}",
});
expect(getWorkspaceSpecificRoutineVariableNames(routine)).toEqual(["workspaceBranch"]);
});
it("ignores routines with only non-workspace variables", () => {
const routine = createRoutine({
title: "Review {{repo}}",
variables: [
{ name: "repo", label: null, type: "text", defaultValue: null, required: true, options: [] },
],
});
expect(routineHasWorkspaceSpecificVariables(routine)).toBe(false);
});
it("sorts workspace routines by name regardless of update order", () => {
const routines = [
createRoutine({
id: "routine-2",
title: "zeta review",
updatedAt: new Date("2026-05-02T00:00:00.000Z"),
}),
createRoutine({
id: "routine-3",
title: "Alpha review",
updatedAt: new Date("2026-04-30T00:00:00.000Z"),
}),
createRoutine({
id: "routine-1",
title: "alpha review",
updatedAt: new Date("2026-05-03T00:00:00.000Z"),
}),
];
expect(sortWorkspaceRoutinesByName(routines).map((routine) => routine.id)).toEqual([
"routine-1",
"routine-3",
"routine-2",
]);
expect(routines.map((routine) => routine.id)).toEqual(["routine-2", "routine-3", "routine-1"]);
});
});