Ensure worktree execution starts only after activation (#9374)

## Thinking Path

> - Paperclip is the open-source control plane people use to manage AI
agents and their work.
> - Its scheduler, routines, and heartbeat services decide when agents
automatically begin work.
> - Experimental per-worktree execution is useful for isolated
development, but enabling it previously allowed automatic services to
consider an existing backlog.
> - A worktree activation must therefore create a durable eligibility
boundary rather than merely toggle execution on.
> - This pull request records an activation cutoff and applies it
consistently to automatic routine and heartbeat dispatch.
> - The result is that an enabled worktree executes only work created
after its own activation, while non-worktree behavior remains unchanged.

## Linked Issues or Issue Description

**Problem type:** Bug / safety regression

**Summary:** Enabling experimental run execution in an existing worktree
could start automatic scheduler, routine, watchdog, and heartbeat
activity for work created before that worktree was explicitly armed.

**Expected behavior:** A worktree that has execution enabled only
considers automatically dispatched work created on or after its
activation timestamp. Ambiguous activation state fails closed.
Non-worktree instances keep their existing behavior.

**Related public work:** Refs #8275 (runtime worktree policy gating);
this PR adds an activation-time boundary for automatic execution rather
than changing the general runtime policy.

## What Changed

- Persist a worktree execution activation timestamp and originating
instance ID; stamp them only when the experimental toggle changes from
disabled to enabled.
- Resolve activation state fail-closed when the cutoff is missing,
invalid, disabled, or belongs to another instance.
- Gate automatic routine scheduling, webhooks, watchdog activity, and
heartbeat selection at the activation cutoff; manual runs remain
available.
- Share the canonical worktree truthy-environment helper across routine
dispatch and agent inbox filtering.
- Add cutoff and truthy-runtime regression coverage, plus
experimental-settings UI states that explain armed and suppressed
execution.

## Verification

- `pnpm exec vitest run server/src/__tests__/routines-service.test.ts
server/src/__tests__/instance-settings-service.test.ts` — passes: 2
files, 60 tests.
- `pnpm --filter @paperclipai/server typecheck` — passes.
- Existing CI completed successfully before the follow-up review fixes;
this branch was rebased onto the latest `origin/master` before
retesting.

## Risks

- **Behavioral:** Automatic worktree execution is intentionally more
restrictive; pre-existing work is suppressed until newly created after
activation.
- **Operational:** A malformed or cross-instance activation record fails
closed, requiring an operator to disable and re-enable the experimental
toggle on the intended worktree.
- **Compatibility:** The worktree environment now accepts all canonical
truthy values (`1`, `true`, `yes`, and `on`) consistently; non-worktree
instances are unaffected.
- **Branch metadata:** This existing execution-workspace branch predates
the current naming rule and cannot be renamed under this task's
workspace contract; the code and PR title do not include internal ticket
references.

> `ROADMAP.md` was checked; this targeted execution-safety fix does not
duplicate planned core work.

## Model Used

- Anthropic Claude Code — assisted with the original implementation;
exact model identifier and context window were not recorded in the
repository metadata.
- OpenAI Codex CLI — assisted with PR preparation and review fixes;
exact model identifier and context window are not exposed in this
execution environment. Used with terminal tooling, code editing, and
targeted test execution.

## 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)
- [ ] 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
- [ ] All Paperclip CI gates are green
- [ ] 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>
Co-authored-by: Claude Opus 4.8 <noreply@anthropic.com>
This commit is contained in:
authored and GitHub committed 2026-07-10 16:11:26 -05:00
1 parent 23f34491e2
commit 70ce005bef
22 files changed
+1082 -69

No files matched your search

@@ -1249,4 +1249,14 @@ describeEmbeddedPostgres("heartbeat issue graph liveness escalation", () => {
.where(eq(issueRelations.relatedIssueId, blockedIssueId));
expect(blockers.some((row) => row.blockerIssueId === escalations[0]!.id)).toBe(false);
});
it("handles an armed cutoff when no liveness findings exist", async () => {
const heartbeat = heartbeatService(db);
const result = await heartbeat.reconcileIssueGraphLiveness({
issueCreatedAtGte: new Date(),
});
expect(result.findings).toBe(0);
});
});
@@ -133,6 +133,16 @@ describeEmbeddedPostgres("heartbeat worktree suppression", () => {
return { companyId, agentId, issueId };
}
async function armWorktreeRunExecution(cutoff: Date) {
await instanceSettingsService(db, {
runtimeEnv: {
PAPERCLIP_IN_WORKTREE: "true",
PAPERCLIP_INSTANCE_ID: "test-worktree",
},
now: () => cutoff,
}).updateExperimental({ enableWorktreeRunExecution: true });
}
async function waitForCompletedRun(runId: string, agentId: string) {
let latestStatus: string | null = null;
let latestLastRunId: string | null = null;
@@ -243,6 +253,48 @@ describeEmbeddedPostgres("heartbeat worktree suppression", () => {
expect(runningCount).toBe(0);
});
it("skips pre-cutoff system wakes but allows user wakes in an armed worktree", async () => {
const { agentId, issueId } = await insertAgentAndIssue();
await armWorktreeRunExecution(new Date(Date.now() + 1_000));
const heartbeat = heartbeatService(db, {
runtimeEnv: {
PAPERCLIP_IN_WORKTREE: "true",
PAPERCLIP_INSTANCE_ID: "test-worktree",
},
});
const systemRun = await heartbeat.wakeup(agentId, {
source: "assignment",
triggerDetail: "system",
payload: { issueId },
contextSnapshot: { issueId },
requestedByActorType: "system",
});
expect(systemRun).toBeNull();
const skippedWake = await db
.select({ reason: agentWakeupRequests.reason, payload: agentWakeupRequests.payload })
.from(agentWakeupRequests)
.orderBy(sql`${agentWakeupRequests.createdAt} desc`)
.limit(1)
.then((rows) => rows[0] ?? null);
expect(skippedWake).toMatchObject({ reason: "heartbeat.worktree_execution_cutoff" });
expect(skippedWake?.payload).toMatchObject({
heartbeatSkip: { reason: "worktree_execution_cutoff", issueId },
});
const userRun = await heartbeat.wakeup(agentId, {
source: "on_demand",
triggerDetail: "user",
payload: { issueId },
contextSnapshot: { issueId, skipIssueComment: true },
requestedByActorType: "user",
requestedByActorId: "operator",
});
expect(userRun).not.toBeNull();
await heartbeat.waitForRunExecutionDrain(userRun!.id);
}, 10_000);
it("still creates live-plane assignment runs when suppression is not active", async () => {
const { agentId, issueId } = await insertAgentAndIssue();
await db
@@ -89,6 +89,9 @@ describe("instance settings routes", () => {
enableIssueGraphLivenessAutoRecovery: true,
enableWorkspaceBranchReconcileForward: true,
enableWorkspaceDirtyQuarantineRepair: true,
enableWorktreeRunExecution: false,
worktreeRunExecutionActivatedAt: null,
worktreeRunExecutionActivationInstanceId: null,
issueGraphLivenessAutoRecoveryLookbackHours: 24,
},
createdAt: "2026-06-20T00:00:00.000Z",
@@ -114,6 +117,9 @@ describe("instance settings routes", () => {
enableIssueGraphLivenessAutoRecovery: true,
enableWorkspaceBranchReconcileForward: true,
enableWorkspaceDirtyQuarantineRepair: true,
enableWorktreeRunExecution: false,
worktreeRunExecutionActivatedAt: null,
worktreeRunExecutionActivationInstanceId: null,
issueGraphLivenessAutoRecoveryLookbackHours: 24,
});
mockInstanceSettingsService.update.mockResolvedValue({
@@ -138,6 +144,9 @@ describe("instance settings routes", () => {
enableIssueGraphLivenessAutoRecovery: true,
enableWorkspaceBranchReconcileForward: true,
enableWorkspaceDirtyQuarantineRepair: true,
enableWorktreeRunExecution: false,
worktreeRunExecutionActivatedAt: null,
worktreeRunExecutionActivationInstanceId: null,
issueGraphLivenessAutoRecoveryLookbackHours: 24,
},
createdAt: "2026-06-20T00:00:00.000Z",
@@ -168,6 +177,9 @@ describe("instance settings routes", () => {
enableIssueGraphLivenessAutoRecovery: true,
enableWorkspaceBranchReconcileForward: true,
enableWorkspaceDirtyQuarantineRepair: true,
enableWorktreeRunExecution: false,
worktreeRunExecutionActivatedAt: null,
worktreeRunExecutionActivationInstanceId: null,
issueGraphLivenessAutoRecoveryLookbackHours: 24,
},
});
@@ -226,6 +238,9 @@ describe("instance settings routes", () => {
enableIssueGraphLivenessAutoRecovery: true,
enableWorkspaceBranchReconcileForward: true,
enableWorkspaceDirtyQuarantineRepair: true,
enableWorktreeRunExecution: false,
worktreeRunExecutionActivatedAt: null,
worktreeRunExecutionActivationInstanceId: null,
issueGraphLivenessAutoRecoveryLookbackHours: 24,
});
@@ -240,6 +255,28 @@ describe("instance settings routes", () => {
expect(mockLogActivity).toHaveBeenCalledTimes(2);
}, 10_000);
it("strips server-managed worktree run execution fields before updating experimental settings", async () => {
const app = await createApp({
type: "board",
userId: "local-board",
source: "local_implicit",
isInstanceAdmin: true,
});
await request(app)
.patch("/api/instance/settings/experimental")
.send({
enableWorktreeRunExecution: true,
worktreeRunExecutionActivatedAt: "2026-07-10T12:00:00.000Z",
worktreeRunExecutionActivationInstanceId: "copied-instance",
})
.expect(200);
expect(mockInstanceSettingsService.updateExperimental).toHaveBeenCalledWith({
enableWorktreeRunExecution: true,
});
});
it("allows local board users to read and update the instance default environment", async () => {
const app = await createApp({
type: "board",
@@ -1,5 +1,10 @@
import { describe, expect, it } from "vitest";
import { normalizeExperimentalSettings } from "../services/instance-settings.js";
import { describe, expect, it, vi } from "vitest";
import type { InstanceExperimentalSettings } from "@paperclipai/shared";
import {
applyExperimentalSettingsPatch,
normalizeExperimentalSettings,
resolveWorktreeRunExecutionActivationState,
} from "../services/instance-settings.js";
describe("instance settings service", () => {
it("ignores retired experimental flags without resetting current settings", () => {
@@ -39,6 +44,8 @@ describe("instance settings service", () => {
enableWorkspaceBranchReconcileForward: true,
enableWorkspaceDirtyQuarantineRepair: false,
enableWorktreeRunExecution: false,
worktreeRunExecutionActivatedAt: null,
worktreeRunExecutionActivationInstanceId: null,
issueGraphLivenessAutoRecoveryLookbackHours: 48,
});
});
@@ -115,4 +122,208 @@ describe("instance settings service", () => {
expect(normalizeExperimentalSettings({}).enableBuiltInAgents).toBe(false);
expect(normalizeExperimentalSettings({ enableExternalObjects: true }).enableBuiltInAgents).toBe(false);
});
it("sets worktree run execution activation fields on a false to true transition", () => {
const activatedAt = new Date("2026-07-10T12:00:00.000Z");
const next = applyExperimentalSettingsPatch(
{ enableWorktreeRunExecution: false },
{ enableWorktreeRunExecution: true },
{
now: () => activatedAt,
runtimeEnv: {
PAPERCLIP_IN_WORKTREE: "true",
PAPERCLIP_INSTANCE_ID: "worktree-instance",
},
},
);
expect(next.enableWorktreeRunExecution).toBe(true);
expect(next.worktreeRunExecutionActivatedAt).toBe("2026-07-10T12:00:00.000Z");
expect(next.worktreeRunExecutionActivationInstanceId).toBe("worktree-instance");
});
it("clears worktree run execution activation fields on a true to false transition", () => {
const next = applyExperimentalSettingsPatch(
{
enableWorktreeRunExecution: true,
worktreeRunExecutionActivatedAt: "2026-07-10T12:00:00.000Z",
worktreeRunExecutionActivationInstanceId: "worktree-instance",
},
{ enableWorktreeRunExecution: false },
{
runtimeEnv: {
PAPERCLIP_IN_WORKTREE: "true",
PAPERCLIP_INSTANCE_ID: "worktree-instance",
},
},
);
expect(next.enableWorktreeRunExecution).toBe(false);
expect(next.worktreeRunExecutionActivatedAt).toBeNull();
expect(next.worktreeRunExecutionActivationInstanceId).toBeNull();
});
it("refreshes the activation cutoff when worktree run execution is re-toggled", () => {
const firstActivation = applyExperimentalSettingsPatch(
{ enableWorktreeRunExecution: false },
{ enableWorktreeRunExecution: true },
{
now: () => new Date("2026-07-10T12:00:00.000Z"),
runtimeEnv: {
PAPERCLIP_IN_WORKTREE: "true",
PAPERCLIP_INSTANCE_ID: "worktree-instance",
},
},
);
const disabled = applyExperimentalSettingsPatch(
firstActivation,
{ enableWorktreeRunExecution: false },
{
runtimeEnv: {
PAPERCLIP_IN_WORKTREE: "true",
PAPERCLIP_INSTANCE_ID: "worktree-instance",
},
},
);
const secondActivation = applyExperimentalSettingsPatch(
disabled,
{ enableWorktreeRunExecution: true },
{
now: () => new Date("2026-07-10T12:05:00.000Z"),
runtimeEnv: {
PAPERCLIP_IN_WORKTREE: "true",
PAPERCLIP_INSTANCE_ID: "worktree-instance",
},
},
);
expect(secondActivation.worktreeRunExecutionActivatedAt).toBe("2026-07-10T12:05:00.000Z");
expect(secondActivation.worktreeRunExecutionActivatedAt).not.toBe(
firstActivation.worktreeRunExecutionActivatedAt,
);
});
it("strips client-supplied activation fields before applying experimental patches", () => {
const next = applyExperimentalSettingsPatch(
{ enableWorktreeRunExecution: false },
{
enableWorktreeRunExecution: false,
worktreeRunExecutionActivatedAt: "2026-07-10T12:00:00.000Z",
worktreeRunExecutionActivationInstanceId: "copied-instance",
},
{
runtimeEnv: {
PAPERCLIP_IN_WORKTREE: "true",
PAPERCLIP_INSTANCE_ID: "worktree-instance",
},
},
);
expect(next.worktreeRunExecutionActivatedAt).toBeNull();
expect(next.worktreeRunExecutionActivationInstanceId).toBeNull();
});
it("resolves worktree run execution as armed only when the cutoff matches the current instance", async () => {
const experimental = normalizeExperimentalSettings({
enableWorktreeRunExecution: true,
worktreeRunExecutionActivatedAt: "2026-07-10T12:00:00.000Z",
worktreeRunExecutionActivationInstanceId: "worktree-instance",
});
await expect(
resolveWorktreeRunExecutionActivationState({
getExperimental: async () => experimental,
runtimeEnv: {
PAPERCLIP_IN_WORKTREE: "true",
PAPERCLIP_INSTANCE_ID: "worktree-instance",
},
}),
).resolves.toEqual({
armed: true,
cutoff: "2026-07-10T12:00:00.000Z",
activationInstanceId: "worktree-instance",
reason: null,
});
});
it("fails closed when worktree run execution is missing a cutoff", async () => {
const experimental = normalizeExperimentalSettings({
enableWorktreeRunExecution: true,
worktreeRunExecutionActivationInstanceId: "worktree-instance",
});
await expect(
resolveWorktreeRunExecutionActivationState({
getExperimental: async () => experimental,
runtimeEnv: {
PAPERCLIP_IN_WORKTREE: "true",
PAPERCLIP_INSTANCE_ID: "worktree-instance",
},
}),
).resolves.toMatchObject({
armed: false,
cutoff: null,
reason: "missing_cutoff",
});
});
it("fails closed when worktree run execution was activated by another instance", async () => {
const experimental = normalizeExperimentalSettings({
enableWorktreeRunExecution: true,
worktreeRunExecutionActivatedAt: "2026-07-10T12:00:00.000Z",
worktreeRunExecutionActivationInstanceId: "source-instance",
});
await expect(
resolveWorktreeRunExecutionActivationState({
getExperimental: async () => experimental,
runtimeEnv: {
PAPERCLIP_IN_WORKTREE: "true",
PAPERCLIP_INSTANCE_ID: "target-instance",
},
}),
).resolves.toMatchObject({
armed: false,
cutoff: null,
activationInstanceId: "source-instance",
reason: "instance_id_mismatch",
});
});
it("fails closed on settings read errors and avoids reads outside worktree runtimes", async () => {
await expect(
resolveWorktreeRunExecutionActivationState({
getExperimental: async () => {
throw new Error("settings unavailable");
},
runtimeEnv: {
PAPERCLIP_IN_WORKTREE: "true",
PAPERCLIP_INSTANCE_ID: "worktree-instance",
},
}),
).resolves.toMatchObject({
armed: false,
cutoff: null,
reason: "settings_read_error",
});
const getExperimental = vi.fn<() => Promise<InstanceExperimentalSettings>>();
await expect(
resolveWorktreeRunExecutionActivationState({
getExperimental,
runtimeEnv: {
PAPERCLIP_IN_WORKTREE: "false",
PAPERCLIP_INSTANCE_ID: "worktree-instance",
},
}),
).resolves.toMatchObject({
armed: false,
cutoff: null,
reason: "not_worktree_runtime",
});
expect(getExperimental).not.toHaveBeenCalled();
});
});
@@ -88,6 +88,7 @@ describeEmbeddedPostgres("routine service live-execution coalescing", () => {
});
async function seedFixture(opts?: {
runtimeEnv?: Record<string, string | undefined>;
wakeup?: (
agentId: string,
wakeupOpts: {
@@ -147,6 +148,7 @@ describeEmbeddedPostgres("routine service live-execution coalescing", () => {
});
const svc = routineService(db, {
runtimeEnv: opts?.runtimeEnv,
heartbeat: {
wakeup: async (wakeupAgentId, wakeupOpts) => {
wakeups.push({ agentId: wakeupAgentId, opts: wakeupOpts });
@@ -204,6 +206,18 @@ describeEmbeddedPostgres("routine service live-execution coalescing", () => {
return { companyId, agentId, issueSvc, projectId, routine, svc, wakeups };
}
async function armWorktreeExecution(cutoff: Date, instanceId = "worktree-routines-test") {
await db.insert(instanceSettings).values({
singletonKey: "default",
general: {},
experimental: {
enableWorktreeRunExecution: true,
worktreeRunExecutionActivatedAt: cutoff.toISOString(),
worktreeRunExecutionActivationInstanceId: instanceId,
},
});
}
it("filters listed routines by project", async () => {
const { companyId, agentId, projectId, routine, svc } = await seedFixture();
const otherProjectId = randomUUID();
@@ -1746,6 +1760,80 @@ describeEmbeddedPostgres("routine service live-execution coalescing", () => {
expect(run.status).toBe("issue_created");
});
it("records suppressed automatic runs when worktree execution is disabled while allowing manual runs", async () => {
const runtimeEnv = { PAPERCLIP_IN_WORKTREE: "yes", PAPERCLIP_INSTANCE_ID: "worktree-routines-test" };
const { companyId, routine, svc } = await seedFixture({ runtimeEnv });
const { trigger: scheduleTrigger } = await svc.createTrigger(
routine.id,
{ kind: "schedule", cronExpression: "0 0 * * *", timezone: "UTC" },
{},
);
const { trigger: webhookTrigger } = await svc.createTrigger(
routine.id,
{ kind: "webhook", signingMode: "none" },
{},
);
const pastDue = new Date("2020-01-01T00:00:00.000Z");
await db.update(routineTriggers).set({ nextRunAt: pastDue }).where(eq(routineTriggers.id, scheduleTrigger.id));
expect(await svc.tickScheduledTriggers(new Date())).toEqual({ triggered: 0 });
const webhookRun = await svc.firePublicTrigger(webhookTrigger.publicId!, { payload: { event: "created" } });
expect(webhookRun).toMatchObject({ source: "webhook", status: "skipped", failureReason: "worktree_execution_cutoff" });
const manualRun = await svc.runRoutine(routine.id, { source: "manual" });
expect(manualRun.status).toBe("issue_created");
const automatedRuns = await db.select().from(routineRuns).where(eq(routineRuns.routineId, routine.id));
expect(automatedRuns.filter((run) => run.failureReason === "worktree_execution_cutoff")).toHaveLength(2);
expect(automatedRuns.filter((run) => run.linkedIssueId)).toHaveLength(1);
const scheduleAfter = await db.select().from(routineTriggers).where(eq(routineTriggers.id, scheduleTrigger.id)).then((rows) => rows[0]);
expect(scheduleAfter!.nextRunAt!.getTime()).toBeGreaterThan(pastDue.getTime());
expect((await db.select().from(issues).where(eq(issues.companyId, companyId))).filter((issue) => issue.originKind === "routine_execution")).toHaveLength(1);
});
it("dispatches only post-cutoff scheduled routines in an armed worktree", async () => {
const runtimeEnv = { PAPERCLIP_IN_WORKTREE: "true", PAPERCLIP_INSTANCE_ID: "worktree-routines-test" };
const { companyId, agentId, projectId, routine: oldRoutine, svc } = await seedFixture({ runtimeEnv });
const cutoff = new Date("2025-01-01T00:00:00.000Z");
await armWorktreeExecution(cutoff);
const newRoutine = await svc.create(companyId, {
projectId,
goalId: null,
parentIssueId: null,
title: "new routine",
description: null,
assigneeAgentId: agentId,
priority: "medium",
status: "active",
concurrencyPolicy: "coalesce_if_active",
catchUpPolicy: "skip_missed",
}, {});
await db.update(routines).set({ createdAt: new Date("2024-12-31T23:59:59.000Z") }).where(eq(routines.id, oldRoutine.id));
await db.update(routines).set({ createdAt: new Date("2025-01-01T00:00:01.000Z") }).where(eq(routines.id, newRoutine.id));
const { trigger: oldTrigger } = await svc.createTrigger(oldRoutine.id, { kind: "schedule", cronExpression: "0 0 * * *", timezone: "UTC" }, {});
const { trigger: newTrigger } = await svc.createTrigger(newRoutine.id, { kind: "schedule", cronExpression: "0 0 * * *", timezone: "UTC" }, {});
await db.update(routineTriggers).set({ nextRunAt: new Date("2020-01-01T00:00:00.000Z") }).where(eq(routineTriggers.id, oldTrigger.id));
await db.update(routineTriggers).set({ nextRunAt: new Date("2020-01-01T00:00:00.000Z") }).where(eq(routineTriggers.id, newTrigger.id));
expect(await svc.tickScheduledTriggers(new Date())).toEqual({ triggered: 1 });
const oldRuns = await db.select().from(routineRuns).where(eq(routineRuns.routineId, oldRoutine.id));
expect(oldRuns).toMatchObject([{ status: "skipped", failureReason: "worktree_execution_cutoff", linkedIssueId: null }]);
const newRuns = await db.select().from(routineRuns).where(eq(routineRuns.routineId, newRoutine.id));
expect(newRuns).toMatchObject([{ status: "issue_created" }]);
});
it("applies the armed cutoff to webhook dispatch but not manual API runs", async () => {
const runtimeEnv = { PAPERCLIP_IN_WORKTREE: "true", PAPERCLIP_INSTANCE_ID: "worktree-routines-test" };
const { routine, svc } = await seedFixture({ runtimeEnv });
await armWorktreeExecution(new Date("2025-01-01T00:00:00.000Z"));
await db.update(routines).set({ createdAt: new Date("2024-12-31T23:59:59.000Z") }).where(eq(routines.id, routine.id));
const { trigger } = await svc.createTrigger(routine.id, { kind: "webhook", signingMode: "none" }, {});
const webhookRun = await svc.firePublicTrigger(trigger.publicId!, { payload: { event: "created" } });
expect(webhookRun).toMatchObject({ status: "skipped", failureReason: "worktree_execution_cutoff", linkedIssueId: null });
expect((await svc.runRoutine(routine.id, { source: "api" })).status).toBe("issue_created");
});
it("suppresses scheduled ticks while the routine project is paused, then resumes when unpaused", async () => {
const { companyId, projectId, routine, svc } = await seedFixture();
const { trigger } = await svc.createTrigger(
@@ -663,4 +663,16 @@ describeEmbeddedPostgres("task watchdog scheduler", () => {
.where(and(eq(issues.companyId, companyId), eq(issues.originKind, "task_watchdog")));
expect(watchdogIssues).toHaveLength(1);
});
it("handles an armed cutoff when no watchdogs are active", async () => {
const companyId = await seedCompany();
const { service } = createService();
const result = await service.reconcileTaskWatchdogs({
companyId,
issueCreatedAtGte: new Date(),
});
expect(result).toMatchObject({ checked: 0, triggered: 0 });
});
});
+24
View File
@@ -65,6 +65,30 @@ describe("ui branding", () => {
expect(meta).toContain('name="paperclip-worktree-color"');
});
it("surfaces the runtime instance id so the UI can fail closed on copied rows", () => {
const branding = getWorktreeUiBranding({
PAPERCLIP_IN_WORKTREE: "true",
PAPERCLIP_WORKTREE_NAME: "paperclip-pr-432",
PAPERCLIP_WORKTREE_COLOR: "#4f86f7",
PAPERCLIP_INSTANCE_ID: "inst-abc123",
});
expect(branding.instanceId).toBe("inst-abc123");
const meta = renderRuntimeBrandingMeta(branding);
expect(meta).toContain('name="paperclip-instance-id"');
expect(meta).toContain('content="inst-abc123"');
});
it("omits the instance-id meta when the runtime id is unset", () => {
const branding = getWorktreeUiBranding({
PAPERCLIP_IN_WORKTREE: "true",
PAPERCLIP_WORKTREE_NAME: "paperclip-pr-432",
PAPERCLIP_WORKTREE_COLOR: "#4f86f7",
});
expect(branding.instanceId).toBeNull();
expect(renderRuntimeBrandingMeta(branding)).not.toContain('name="paperclip-instance-id"');
});
it("rewrites the favicon and runtime branding blocks for worktree instances only", () => {
const branded = applyUiBranding(TEMPLATE, {
PAPERCLIP_IN_WORKTREE: "true",