forked from farhoodlabs/paperclip
[codex] Respect manual workspace runtime controls (#4125)
## Thinking Path > - Paperclip orchestrates AI agents inside execution and project workspaces > - Workspace runtime services can be controlled manually by operators and reused by agent runs > - Manual start/stop state was not preserved consistently across workspace policies and routine launches > - Routine launches also needed branch/workspace variables to default from the selected workspace context > - This pull request makes runtime policy state explicit, preserves manual control, and auto-fills routine branch variables from workspace data > - The benefit is less surprising workspace service behavior and fewer manual inputs when running workspace-scoped routines ## What Changed - Added runtime-state handling for manual workspace control across execution and project workspace validators, routes, and services. - Updated heartbeat/runtime startup behavior so manually stopped services are respected. - Auto-filled routine workspace branch variables from available workspace context. - Added focused server and UI tests for workspace runtime and routine variable behavior. - Removed muted gray background styling from workspace pages and cards for a cleaner workspace UI. ## Verification - `pnpm install --frozen-lockfile --ignore-scripts` - `pnpm exec vitest run server/src/__tests__/routines-service.test.ts server/src/__tests__/workspace-runtime.test.ts ui/src/components/RoutineRunVariablesDialog.test.tsx` - Result: 55 tests passed, 21 skipped. The embedded Postgres routines tests skipped on this host with the existing PGlite/Postgres init warning; workspace-runtime and UI tests passed. ## Risks - Medium risk: this touches runtime service start/stop policy and heartbeat launch behavior. - The focused tests cover manual runtime state, routine variables, and workspace runtime reuse paths. > For core feature work, check [`ROADMAP.md`](ROADMAP.md) first and discuss it in `#dev` before opening the PR. Feature PRs that overlap with planned core work may need to be redirected — check the roadmap first. See `CONTRIBUTING.md`. ## Model Used - OpenAI Codex coding agent based on GPT-5, tool-enabled local shell and GitHub workflow, exact runtime context window not exposed in this session. ## 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 run tests locally and they pass - [x] I have added or updated tests where applicable - [x] If this change affects the UI, I have included before/after screenshots, or documented why targeted component/service verification is sufficient here - [x] I have updated relevant documentation to reflect my changes - [x] I have considered and documented any risks above - [x] I will address all Greptile and reviewer comments before requesting merge --------- Co-authored-by: Paperclip <noreply@paperclip.ing>
This commit is contained in:
@@ -8,6 +8,12 @@ import { afterEach, beforeEach, describe, expect, it, vi } from "vitest";
|
||||
import { RoutineRunVariablesDialog } from "./RoutineRunVariablesDialog";
|
||||
|
||||
let issueWorkspaceDraftCalls = 0;
|
||||
let issueWorkspaceDraft = {
|
||||
executionWorkspaceId: null as string | null,
|
||||
executionWorkspacePreference: "shared_workspace",
|
||||
executionWorkspaceSettings: { mode: "shared_workspace" },
|
||||
};
|
||||
let issueWorkspaceBranchName: string | null = null;
|
||||
|
||||
vi.mock("../api/instanceSettings", () => ({
|
||||
instanceSettingsApi: {
|
||||
@@ -22,18 +28,20 @@ vi.mock("./IssueWorkspaceCard", async () => {
|
||||
IssueWorkspaceCard: ({
|
||||
onDraftChange,
|
||||
}: {
|
||||
onDraftChange?: (data: Record<string, unknown>, meta: { canSave: boolean }) => void;
|
||||
onDraftChange?: (
|
||||
data: Record<string, unknown>,
|
||||
meta: { canSave: boolean; workspaceBranchName?: string | null },
|
||||
) => void;
|
||||
}) => {
|
||||
React.useEffect(() => {
|
||||
issueWorkspaceDraftCalls += 1;
|
||||
if (issueWorkspaceDraftCalls > 20) {
|
||||
throw new Error("IssueWorkspaceCard onDraftChange looped");
|
||||
}
|
||||
onDraftChange?.({
|
||||
executionWorkspaceId: null,
|
||||
executionWorkspacePreference: "shared_workspace",
|
||||
executionWorkspaceSettings: { mode: "shared_workspace" },
|
||||
}, { canSave: true });
|
||||
onDraftChange?.(issueWorkspaceDraft, {
|
||||
canSave: true,
|
||||
workspaceBranchName: issueWorkspaceBranchName,
|
||||
});
|
||||
}, [onDraftChange]);
|
||||
|
||||
return <div data-testid="workspace-card">Workspace card</div>;
|
||||
@@ -119,6 +127,12 @@ describe("RoutineRunVariablesDialog", () => {
|
||||
container = document.createElement("div");
|
||||
document.body.appendChild(container);
|
||||
issueWorkspaceDraftCalls = 0;
|
||||
issueWorkspaceDraft = {
|
||||
executionWorkspaceId: null,
|
||||
executionWorkspacePreference: "shared_workspace",
|
||||
executionWorkspaceSettings: { mode: "shared_workspace" },
|
||||
};
|
||||
issueWorkspaceBranchName = null;
|
||||
});
|
||||
|
||||
afterEach(() => {
|
||||
@@ -155,6 +169,7 @@ describe("RoutineRunVariablesDialog", () => {
|
||||
);
|
||||
await Promise.resolve();
|
||||
await Promise.resolve();
|
||||
await new Promise((resolve) => setTimeout(resolve, 0));
|
||||
});
|
||||
|
||||
expect(issueWorkspaceDraftCalls).toBeLessThanOrEqual(2);
|
||||
@@ -166,4 +181,87 @@ describe("RoutineRunVariablesDialog", () => {
|
||||
root.unmount();
|
||||
});
|
||||
});
|
||||
|
||||
it("renders workspaceBranch as a read-only selected workspace value", async () => {
|
||||
issueWorkspaceDraft = {
|
||||
executionWorkspaceId: "workspace-1",
|
||||
executionWorkspacePreference: "reuse_existing",
|
||||
executionWorkspaceSettings: { mode: "isolated_workspace" },
|
||||
};
|
||||
issueWorkspaceBranchName = "pap-1634-routine-branch";
|
||||
const onSubmit = vi.fn();
|
||||
const root = createRoot(container);
|
||||
const queryClient = new QueryClient({
|
||||
defaultOptions: {
|
||||
queries: {
|
||||
retry: false,
|
||||
},
|
||||
},
|
||||
});
|
||||
|
||||
await act(async () => {
|
||||
root.render(
|
||||
<QueryClientProvider client={queryClient}>
|
||||
<RoutineRunVariablesDialog
|
||||
open
|
||||
onOpenChange={() => {}}
|
||||
companyId="company-1"
|
||||
projects={[createProject()]}
|
||||
agents={[createAgent()]}
|
||||
defaultProjectId="project-1"
|
||||
defaultAssigneeAgentId="agent-1"
|
||||
variables={[
|
||||
{
|
||||
name: "workspaceBranch",
|
||||
label: null,
|
||||
type: "text",
|
||||
defaultValue: null,
|
||||
required: true,
|
||||
options: [],
|
||||
},
|
||||
]}
|
||||
isPending={false}
|
||||
onSubmit={onSubmit}
|
||||
/>
|
||||
</QueryClientProvider>,
|
||||
);
|
||||
await Promise.resolve();
|
||||
await Promise.resolve();
|
||||
await new Promise((resolve) => setTimeout(resolve, 0));
|
||||
});
|
||||
|
||||
for (let i = 0; i < 10 && !document.querySelector('[data-testid="workspace-card"]'); i += 1) {
|
||||
await act(async () => {
|
||||
await new Promise((resolve) => setTimeout(resolve, 0));
|
||||
});
|
||||
}
|
||||
|
||||
const branchInput = Array.from(document.querySelectorAll("input"))
|
||||
.find((input) => input.value === "pap-1634-routine-branch");
|
||||
expect(branchInput?.disabled).toBe(true);
|
||||
expect(document.body.textContent).not.toContain("Missing: workspaceBranch");
|
||||
|
||||
const runButton = Array.from(document.querySelectorAll("button"))
|
||||
.find((button) => button.textContent === "Run routine");
|
||||
expect(runButton).toBeTruthy();
|
||||
|
||||
await act(async () => {
|
||||
runButton?.dispatchEvent(new MouseEvent("click", { bubbles: true }));
|
||||
});
|
||||
|
||||
expect(onSubmit).toHaveBeenCalledWith({
|
||||
variables: {
|
||||
workspaceBranch: "pap-1634-routine-branch",
|
||||
},
|
||||
assigneeAgentId: "agent-1",
|
||||
projectId: "project-1",
|
||||
executionWorkspaceId: "workspace-1",
|
||||
executionWorkspacePreference: "reuse_existing",
|
||||
executionWorkspaceSettings: { mode: "isolated_workspace" },
|
||||
});
|
||||
|
||||
await act(async () => {
|
||||
root.unmount();
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
Reference in New Issue
Block a user