From 8f6e95330cca214fc1dc5c0bee6b8a26b130fb41 Mon Sep 17 00:00:00 2001 From: Gonzalo Aune <219565+gonzaloaune@users.noreply.github.com> Date: Wed, 2 Sep 2026 13:19:09 +0000 Subject: [PATCH 1/2] [Jamie] Wrap Stakwork /projects payload under `project` key so webhook_full_output survives MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit ## Problem For plan_mode / workflow_editor runs, `callStakworkAPI` sets `webhook_full_output: false` as a top-level key in the Stakwork `/projects` request body. But Stakwork stores `webhook_full_output: true` anyway. ## Root cause Hive sends a **flat** JSON body (no `{ project: ... }` envelope). Stakwork's `ProjectsController` relies on Rails `wrap_parameters` (`format: [:json]`, no explicit `include`), which wraps **only real `Project` DB columns** into `params[:project]`. `webhook_full_output` (like `stop_on_errors` and `debug_mode`) is NOT a column — it lives inside the `project_configs` JSON blob — so the wrapper drops it. Strong params then see `nil`, and `create_project` defaults it back to `true`. If the body is sent pre-nested under `project`, Rails uses the explicit nested key as-is and the drop does not happen. ## Fix Wrap the outbound payload as `{ project: stakworkPayload }` in `callStakworkAPI` so `webhook_full_output` (and the other non-column config keys) are correctly nested. Sending a body that already has a `project` root key is safe: `wrap_parameters` only *adds* a synthetic copy, it never alters an already-present `project` key. --- src/services/task-workflow.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/services/task-workflow.ts b/src/services/task-workflow.ts index 56dbb4a298..90e6d9399f 100644 --- a/src/services/task-workflow.ts +++ b/src/services/task-workflow.ts @@ -908,7 +908,7 @@ export async function callStakworkAPI(params: { try { const response = await fetch(stakworkURL, { method: "POST", - body: JSON.stringify(stakworkPayload), + body: JSON.stringify({ project: stakworkPayload }), headers: { Authorization: `Token token=${config.STAKWORK_API_KEY}`, "Content-Type": "application/json", From 8410262ddbc85c83c52c56d4d93b9cdc437591a3 Mon Sep 17 00:00:00 2001 From: tomsmith8 Date: Wed, 2 Sep 2026 13:46:50 +0000 Subject: [PATCH 2/2] Fix unit tests to unwrap Stakwork payload's `project` envelope callStakworkAPI now wraps the outbound Stakwork /projects request body as { project: stakworkPayload } (see previous commit). Unit tests were still parsing the fetch body and reading fields (workflow_id, webhook_url, workflow_params, name, etc.) directly off the top level, so they broke once the envelope was introduced. Update all affected test files to unwrap the parsed body when it has a top-level `project` key before asserting on its contents: - src/__tests__/unit/services/task-workflow.test.ts - src/__tests__/unit/api/chat/call-stakwork.test.ts - src/__tests__/unit/api/chat/message/call-stakwork.test.ts - src/__tests__/unit/api/chat/message/route.test.ts - src/__tests__/unit/api/api-chat-message.test.ts - src/__tests__/unit/services/call-stakwork-api.test.ts - src/__tests__/unit/services/task-workflow-createChatMessageAndTriggerStakwork.test.ts --- .../unit/api/api-chat-message.test.ts | 6 ++- .../unit/api/chat/call-stakwork.test.ts | 18 ++++--- .../api/chat/message/call-stakwork.test.ts | 21 +++++--- .../unit/api/chat/message/route.test.ts | 51 ++++++++++++------ .../unit/services/call-stakwork-api.test.ts | 39 +++++++++----- ...reateChatMessageAndTriggerStakwork.test.ts | 9 ++-- .../unit/services/task-workflow.test.ts | 54 ++++++++++++------- 7 files changed, 132 insertions(+), 66 deletions(-) diff --git a/src/__tests__/unit/api/api-chat-message.test.ts b/src/__tests__/unit/api/api-chat-message.test.ts index 207bab5548..c3069f61d5 100644 --- a/src/__tests__/unit/api/api-chat-message.test.ts +++ b/src/__tests__/unit/api/api-chat-message.test.ts @@ -582,7 +582,8 @@ describe("POST /api/chat/message", () => { expect(fetchCall).toBeTruthy(); - const body = JSON.parse(fetchCall![1]!.body as string); + const _rawBody = JSON.parse(fetchCall![1]!.body as string); + const body = (_rawBody && typeof _rawBody === "object" && "project" in _rawBody) ? _rawBody.project : _rawBody; expect(body.workflow_params.set_var.attributes.vars).toMatchObject({ alias: "testuser", username: "testuser", @@ -616,7 +617,8 @@ describe("POST /api/chat/message", () => { const fetchCall = mockFetch.mock.calls.find((call) => call[0].toString().includes("stakwork")); - const body = JSON.parse(fetchCall![1]!.body as string); + const _rawBody = JSON.parse(fetchCall![1]!.body as string); + const body = (_rawBody && typeof _rawBody === "object" && "project" in _rawBody) ? _rawBody.project : _rawBody; expect(body.workflow_id).toBe(parseInt(expectedWorkflowId)); }); }); diff --git a/src/__tests__/unit/api/chat/call-stakwork.test.ts b/src/__tests__/unit/api/chat/call-stakwork.test.ts index 72b7db14d8..e063b4993c 100644 --- a/src/__tests__/unit/api/chat/call-stakwork.test.ts +++ b/src/__tests__/unit/api/chat/call-stakwork.test.ts @@ -206,13 +206,15 @@ const TestHelpers = { ); const fetchCall = mockFetch.mock.calls[0]; - const payload = JSON.parse(fetchCall[1]?.body as string); + const _rawPayload = JSON.parse(fetchCall[1]?.body as string); + const payload = (_rawPayload && typeof _rawPayload === 'object' && 'project' in _rawPayload) ? _rawPayload.project : _rawPayload; expect(payload.workflow_id).toBe(workflowId); }, expectFetchCalledWithVarsContaining: (expectedVars: Record) => { const fetchCall = mockFetch.mock.calls[0]; - const payload = JSON.parse(fetchCall[1]?.body as string); + const _rawPayload = JSON.parse(fetchCall[1]?.body as string); + const payload = (_rawPayload && typeof _rawPayload === 'object' && 'project' in _rawPayload) ? _rawPayload.project : _rawPayload; const vars = payload.workflow_params.set_var.attributes.vars; Object.entries(expectedVars).forEach(([key, value]) => { @@ -500,7 +502,8 @@ describe("callStakwork Function Unit Tests", () => { await POST(request); const fetchCall = mockFetch.mock.calls[0]; - const payload = JSON.parse(fetchCall[1]?.body as string); + const _rawPayload = JSON.parse(fetchCall[1]?.body as string); + const payload = (_rawPayload && typeof _rawPayload === 'object' && 'project' in _rawPayload) ? _rawPayload.project : _rawPayload; expect(payload.webhook_url).toBe("http://localhost:3000/api/stakwork/webhook?task_id=test-task-id"); }); @@ -719,7 +722,8 @@ describe("callStakwork Function Unit Tests", () => { await POST(request); const fetchCall = mockFetch.mock.calls[0]; - const payload = JSON.parse(fetchCall[1]?.body as string); + const _rawPayload = JSON.parse(fetchCall[1]?.body as string); + const payload = (_rawPayload && typeof _rawPayload === 'object' && 'project' in _rawPayload) ? _rawPayload.project : _rawPayload; expect(payload).toMatchObject({ name: expect.stringMatching(/^hive-task-/), @@ -745,7 +749,8 @@ describe("callStakwork Function Unit Tests", () => { await POST(request); const fetchCall = mockFetch.mock.calls[0]; - const payload = JSON.parse(fetchCall[1]?.body as string); + const _rawPayload = JSON.parse(fetchCall[1]?.body as string); + const payload = (_rawPayload && typeof _rawPayload === 'object' && 'project' in _rawPayload) ? _rawPayload.project : _rawPayload; expect(payload.name).toBe("hive-task-test-task-id"); }); @@ -757,7 +762,8 @@ describe("callStakwork Function Unit Tests", () => { await POST(request); const fetchCall = mockFetch.mock.calls[0]; - const payload = JSON.parse(fetchCall[1]?.body as string); + const _rawPayload = JSON.parse(fetchCall[1]?.body as string); + const payload = (_rawPayload && typeof _rawPayload === 'object' && 'project' in _rawPayload) ? _rawPayload.project : _rawPayload; expect(payload.workflow_params).toHaveProperty("set_var"); expect(payload.workflow_params.set_var).toHaveProperty("attributes"); diff --git a/src/__tests__/unit/api/chat/message/call-stakwork.test.ts b/src/__tests__/unit/api/chat/message/call-stakwork.test.ts index d7d57f23c8..b9c1119f50 100644 --- a/src/__tests__/unit/api/chat/message/call-stakwork.test.ts +++ b/src/__tests__/unit/api/chat/message/call-stakwork.test.ts @@ -588,7 +588,8 @@ describe("callStakwork Function - Chat Message Processing", () => { // Verify context tags were passed to Stakwork API const fetchCall = mockFetch.mock.calls[0]; - const requestBody = JSON.parse(fetchCall[1].body as string); + const _rawRequestBody = JSON.parse(fetchCall[1].body as string); + const requestBody = (_rawRequestBody && typeof _rawRequestBody === 'object' && 'project' in _rawRequestBody) ? _rawRequestBody.project : _rawRequestBody; const vars = requestBody.workflow_params.set_var.attributes.vars; expect(vars.contextTags).toEqual(contextTags); @@ -1027,7 +1028,8 @@ describe("callStakwork Function - Chat Message Processing", () => { await POST(request); const fetchCall = mockFetch.mock.calls[0]; - const requestBody = JSON.parse(fetchCall[1].body as string); + const _rawRequestBody = JSON.parse(fetchCall[1].body as string); + const requestBody = (_rawRequestBody && typeof _rawRequestBody === 'object' && 'project' in _rawRequestBody) ? _rawRequestBody.project : _rawRequestBody; expect(requestBody.workflow_id).toBe(101); // First ID from "101,102,103" }); @@ -1048,7 +1050,8 @@ describe("callStakwork Function - Chat Message Processing", () => { await POST(request); const fetchCall = mockFetch.mock.calls[0]; - const requestBody = JSON.parse(fetchCall[1].body as string); + const _rawRequestBody = JSON.parse(fetchCall[1].body as string); + const requestBody = (_rawRequestBody && typeof _rawRequestBody === 'object' && 'project' in _rawRequestBody) ? _rawRequestBody.project : _rawRequestBody; expect(requestBody.workflow_id).toBe(103); // Third ID from "101,102,103" }); @@ -1069,7 +1072,8 @@ describe("callStakwork Function - Chat Message Processing", () => { await POST(request); const fetchCall = mockFetch.mock.calls[0]; - const requestBody = JSON.parse(fetchCall[1].body as string); + const _rawRequestBody = JSON.parse(fetchCall[1].body as string); + const requestBody = (_rawRequestBody && typeof _rawRequestBody === 'object' && 'project' in _rawRequestBody) ? _rawRequestBody.project : _rawRequestBody; expect(requestBody.workflow_id).toBe(102); // Second ID from "101,102,103" }); @@ -1239,7 +1243,8 @@ describe("callStakwork Function - Chat Message Processing", () => { await POST(request); const fetchCall = mockFetch.mock.calls[0]; - const requestBody = JSON.parse(fetchCall[1].body as string); + const _rawRequestBody = JSON.parse(fetchCall[1].body as string); + const requestBody = (_rawRequestBody && typeof _rawRequestBody === 'object' && 'project' in _rawRequestBody) ? _rawRequestBody.project : _rawRequestBody; const vars = requestBody.workflow_params.set_var.attributes.vars; expect(vars).toMatchObject({ @@ -1309,7 +1314,8 @@ describe("callStakwork Function - Chat Message Processing", () => { await POST(request); const fetchCall = mockFetch.mock.calls[0]; - const requestBody = JSON.parse(fetchCall[1].body as string); + const _rawRequestBody = JSON.parse(fetchCall[1].body as string); + const requestBody = (_rawRequestBody && typeof _rawRequestBody === 'object' && 'project' in _rawRequestBody) ? _rawRequestBody.project : _rawRequestBody; expect(requestBody.webhook_url).toBe( `http://localhost:3000/api/stakwork/webhook?task_id=${mockTaskId}` @@ -1386,7 +1392,8 @@ describe("callStakwork Function - Chat Message Processing", () => { expect(mockS3Service.generatePresignedDownloadUrl).toHaveBeenCalledWith("uploads/file2.jpg"); const fetchCall = mockFetch.mock.calls[0]; - const requestBody = JSON.parse(fetchCall[1].body as string); + const _rawRequestBody = JSON.parse(fetchCall[1].body as string); + const requestBody = (_rawRequestBody && typeof _rawRequestBody === 'object' && 'project' in _rawRequestBody) ? _rawRequestBody.project : _rawRequestBody; const vars = requestBody.workflow_params.set_var.attributes.vars; expect(vars.attachments).toEqual([ diff --git a/src/__tests__/unit/api/chat/message/route.test.ts b/src/__tests__/unit/api/chat/message/route.test.ts index 53186f4a3f..dec6a43640 100644 --- a/src/__tests__/unit/api/chat/message/route.test.ts +++ b/src/__tests__/unit/api/chat/message/route.test.ts @@ -709,7 +709,8 @@ describe("POST /api/chat/message - callStakwork Unit Tests", () => { await POST(request); const fetchCall = mockFetch.mock.calls[0]; - const requestBody = JSON.parse(fetchCall[1].body as string); + const _rawRequestBody = JSON.parse(fetchCall[1].body as string); + const requestBody = (_rawRequestBody && typeof _rawRequestBody === 'object' && 'project' in _rawRequestBody) ? _rawRequestBody.project : _rawRequestBody; expect(requestBody.workflow_id).toBe(101); // First workflow ID }); @@ -728,7 +729,8 @@ describe("POST /api/chat/message - callStakwork Unit Tests", () => { await POST(request); const fetchCall = mockFetch.mock.calls[0]; - const requestBody = JSON.parse(fetchCall[1].body as string); + const _rawRequestBody = JSON.parse(fetchCall[1].body as string); + const requestBody = (_rawRequestBody && typeof _rawRequestBody === 'object' && 'project' in _rawRequestBody) ? _rawRequestBody.project : _rawRequestBody; expect(requestBody.workflow_id).toBe(103); // Third workflow ID }); @@ -747,7 +749,8 @@ describe("POST /api/chat/message - callStakwork Unit Tests", () => { await POST(request); const fetchCall = mockFetch.mock.calls[0]; - const requestBody = JSON.parse(fetchCall[1].body as string); + const _rawRequestBody = JSON.parse(fetchCall[1].body as string); + const requestBody = (_rawRequestBody && typeof _rawRequestBody === 'object' && 'project' in _rawRequestBody) ? _rawRequestBody.project : _rawRequestBody; expect(requestBody.workflow_id).toBe(103); // Third workflow ID }); @@ -766,7 +769,8 @@ describe("POST /api/chat/message - callStakwork Unit Tests", () => { await POST(request); const fetchCall = mockFetch.mock.calls[0]; - const requestBody = JSON.parse(fetchCall[1].body as string); + const _rawRequestBody = JSON.parse(fetchCall[1].body as string); + const requestBody = (_rawRequestBody && typeof _rawRequestBody === 'object' && 'project' in _rawRequestBody) ? _rawRequestBody.project : _rawRequestBody; expect(requestBody.workflow_id).toBe(102); // Second workflow ID (default) }); @@ -784,7 +788,8 @@ describe("POST /api/chat/message - callStakwork Unit Tests", () => { await POST(request); const fetchCall = mockFetch.mock.calls[0]; - const requestBody = JSON.parse(fetchCall[1].body as string); + const _rawRequestBody = JSON.parse(fetchCall[1].body as string); + const requestBody = (_rawRequestBody && typeof _rawRequestBody === 'object' && 'project' in _rawRequestBody) ? _rawRequestBody.project : _rawRequestBody; expect(requestBody.workflow_id).toBe(102); // Second workflow ID (default) }); @@ -850,7 +855,8 @@ describe("POST /api/chat/message - callStakwork Unit Tests", () => { await POST(request); const fetchCall = mockFetch.mock.calls[0]; - const requestBody = JSON.parse(fetchCall[1].body as string); + const _rawRequestBody = JSON.parse(fetchCall[1].body as string); + const requestBody = (_rawRequestBody && typeof _rawRequestBody === 'object' && 'project' in _rawRequestBody) ? _rawRequestBody.project : _rawRequestBody; const vars = requestBody.workflow_params.set_var.attributes.vars; expect(vars).toMatchObject({ @@ -919,7 +925,8 @@ describe("POST /api/chat/message - callStakwork Unit Tests", () => { expect(s3Service.generatePresignedDownloadUrl).toHaveBeenCalledWith("uploads/test/file.pdf"); const fetchCall = mockFetch.mock.calls[0]; - const requestBody = JSON.parse(fetchCall[1].body as string); + const _rawRequestBody = JSON.parse(fetchCall[1].body as string); + const requestBody = (_rawRequestBody && typeof _rawRequestBody === 'object' && 'project' in _rawRequestBody) ? _rawRequestBody.project : _rawRequestBody; const vars = requestBody.workflow_params.set_var.attributes.vars; expect(vars.attachments).toEqual(["https://s3.test.com/presigned-url"]); @@ -948,7 +955,8 @@ describe("POST /api/chat/message - callStakwork Unit Tests", () => { expect(vi.mocked(getBaseUrl)).toHaveBeenCalledWith(); const fetchCall = mockFetch.mock.calls[0]; - const requestBody = JSON.parse(fetchCall[1].body as string); + const _rawRequestBody = JSON.parse(fetchCall[1].body as string); + const requestBody = (_rawRequestBody && typeof _rawRequestBody === 'object' && 'project' in _rawRequestBody) ? _rawRequestBody.project : _rawRequestBody; const vars = requestBody.workflow_params.set_var.attributes.vars; expect(vars.webhookUrl).toBe("https://production.example.com/api/chat/response"); @@ -969,7 +977,8 @@ describe("POST /api/chat/message - callStakwork Unit Tests", () => { await POST(request); const fetchCall = mockFetch.mock.calls[0]; - const requestBody = JSON.parse(fetchCall[1].body as string); + const _rawRequestBody = JSON.parse(fetchCall[1].body as string); + const requestBody = (_rawRequestBody && typeof _rawRequestBody === 'object' && 'project' in _rawRequestBody) ? _rawRequestBody.project : _rawRequestBody; const vars = requestBody.workflow_params.set_var.attributes.vars; expect(vars.webhookUrl).toBe("https://custom-webhook.example.com"); @@ -990,7 +999,8 @@ describe("POST /api/chat/message - callStakwork Unit Tests", () => { await POST(request); const fetchCall = mockFetch.mock.calls[0]; - const requestBody = JSON.parse(fetchCall[1].body as string); + const _rawRequestBody = JSON.parse(fetchCall[1].body as string); + const requestBody = (_rawRequestBody && typeof _rawRequestBody === 'object' && 'project' in _rawRequestBody) ? _rawRequestBody.project : _rawRequestBody; expect(requestBody.webhook_url).toBe(`http://localhost:3000/api/stakwork/webhook?task_id=${mockTaskId}`); }); @@ -1012,7 +1022,8 @@ describe("POST /api/chat/message - callStakwork Unit Tests", () => { expect(vi.mocked(transformSwarmUrlToRepo2Graph)).toHaveBeenCalledWith("https://test-swarm.com/api"); const fetchCall = mockFetch.mock.calls[0]; - const requestBody = JSON.parse(fetchCall[1].body as string); + const _rawRequestBody = JSON.parse(fetchCall[1].body as string); + const requestBody = (_rawRequestBody && typeof _rawRequestBody === 'object' && 'project' in _rawRequestBody) ? _rawRequestBody.project : _rawRequestBody; const vars = requestBody.workflow_params.set_var.attributes.vars; expect(vars.repo2graph_url).toBe("https://custom-graph.com:3355"); @@ -1404,7 +1415,8 @@ describe("POST /api/chat/message - callStakwork Unit Tests", () => { await POST(request); const fetchCall = mockFetch.mock.calls[0]; - const requestBody = JSON.parse(fetchCall[1].body as string); + const _rawRequestBody = JSON.parse(fetchCall[1].body as string); + const requestBody = (_rawRequestBody && typeof _rawRequestBody === 'object' && 'project' in _rawRequestBody) ? _rawRequestBody.project : _rawRequestBody; const vars = requestBody.workflow_params.set_var.attributes.vars; expect(vars.runBuild).toBe(false); @@ -1427,7 +1439,8 @@ describe("POST /api/chat/message - callStakwork Unit Tests", () => { await POST(request); const fetchCall = mockFetch.mock.calls[0]; - const requestBody = JSON.parse(fetchCall[1].body as string); + const _rawRequestBody = JSON.parse(fetchCall[1].body as string); + const requestBody = (_rawRequestBody && typeof _rawRequestBody === 'object' && 'project' in _rawRequestBody) ? _rawRequestBody.project : _rawRequestBody; const vars = requestBody.workflow_params.set_var.attributes.vars; expect(vars.runTestSuite).toBe(false); @@ -1450,7 +1463,8 @@ describe("POST /api/chat/message - callStakwork Unit Tests", () => { await POST(request); const fetchCall = mockFetch.mock.calls[0]; - const requestBody = JSON.parse(fetchCall[1].body as string); + const _rawRequestBody = JSON.parse(fetchCall[1].body as string); + const requestBody = (_rawRequestBody && typeof _rawRequestBody === 'object' && 'project' in _rawRequestBody) ? _rawRequestBody.project : _rawRequestBody; const vars = requestBody.workflow_params.set_var.attributes.vars; expect(vars.autoMergePr).toBe(true); @@ -1473,7 +1487,8 @@ describe("POST /api/chat/message - callStakwork Unit Tests", () => { await POST(request); const fetchCall = mockFetch.mock.calls[0]; - const requestBody = JSON.parse(fetchCall[1].body as string); + const _rawRequestBody = JSON.parse(fetchCall[1].body as string); + const requestBody = (_rawRequestBody && typeof _rawRequestBody === 'object' && 'project' in _rawRequestBody) ? _rawRequestBody.project : _rawRequestBody; const vars = requestBody.workflow_params.set_var.attributes.vars; expect(vars.runBuild).toBe(false); @@ -1557,7 +1572,8 @@ describe("POST /api/chat/message - callStakwork Unit Tests", () => { ]); const fetchCall = mockFetch.mock.calls[0]; - const requestBody = JSON.parse(fetchCall[1].body as string); + const _rawRequestBody = JSON.parse(fetchCall[1].body as string); + const requestBody = (_rawRequestBody && typeof _rawRequestBody === 'object' && 'project' in _rawRequestBody) ? _rawRequestBody.project : _rawRequestBody; const vars = requestBody.workflow_params.set_var.attributes.vars; expect(vars.contextTags).toEqual([ @@ -1679,7 +1695,8 @@ describe("POST /api/chat/message - callStakwork Unit Tests", () => { expect(s3Service.generatePresignedDownloadUrl).toHaveBeenCalledWith("uploads/file2.jpg"); const fetchCall = mockFetch.mock.calls[0]; - const requestBody = JSON.parse(fetchCall[1].body as string); + const _rawRequestBody = JSON.parse(fetchCall[1].body as string); + const requestBody = (_rawRequestBody && typeof _rawRequestBody === 'object' && 'project' in _rawRequestBody) ? _rawRequestBody.project : _rawRequestBody; const vars = requestBody.workflow_params.set_var.attributes.vars; expect(vars.attachments).toEqual(["https://s3.test.com/file1.pdf", "https://s3.test.com/file2.jpg"]); diff --git a/src/__tests__/unit/services/call-stakwork-api.test.ts b/src/__tests__/unit/services/call-stakwork-api.test.ts index d6d1128084..01531fd8f0 100644 --- a/src/__tests__/unit/services/call-stakwork-api.test.ts +++ b/src/__tests__/unit/services/call-stakwork-api.test.ts @@ -96,7 +96,8 @@ describe("callStakworkAPI", () => { await callStakworkAPI(createTestParams()); const fetchCall = mockFetch.mock.calls[0]; - const body = JSON.parse(fetchCall[1]?.body as string); + const _rawBody = JSON.parse(fetchCall[1]?.body as string); + const body = (_rawBody && typeof _rawBody === 'object' && 'project' in _rawBody) ? _rawBody.project : _rawBody; expect(body.workflow_params.set_var.attributes.vars.tokenReference).toBe( "{{HIVE_STAGING}}" @@ -112,7 +113,8 @@ describe("callStakworkAPI", () => { await callStakworkAPI(createTestParams()); const fetchCall = mockFetch.mock.calls[0]; - const body = JSON.parse(fetchCall[1]?.body as string); + const _rawBody = JSON.parse(fetchCall[1]?.body as string); + const body = (_rawBody && typeof _rawBody === 'object' && 'project' in _rawBody) ? _rawBody.project : _rawBody; expect(body.workflow_params.set_var.attributes.vars.tokenReference).toBe( "{{HIVE_PROD}}" @@ -271,7 +273,8 @@ describe("callStakworkAPI", () => { await callStakworkAPI(params); const fetchCall = mockFetch.mock.calls[0]; - const body = JSON.parse(fetchCall[1]?.body as string); + const _rawBody = JSON.parse(fetchCall[1]?.body as string); + const body = (_rawBody && typeof _rawBody === 'object' && 'project' in _rawBody) ? _rawBody.project : _rawBody; expect(body.workflow_params.set_var.attributes.vars).toMatchObject({ message: "Test message content", @@ -295,7 +298,8 @@ describe("callStakworkAPI", () => { await callStakworkAPI(createTestParams()); const fetchCall = mockFetch.mock.calls[0]; - const body = JSON.parse(fetchCall[1]?.body as string); + const _rawBody = JSON.parse(fetchCall[1]?.body as string); + const body = (_rawBody && typeof _rawBody === 'object' && 'project' in _rawBody) ? _rawBody.project : _rawBody; expect(body).toHaveProperty("name", "hive-task-test-task-123"); expect(body).toHaveProperty("workflow_id"); @@ -311,7 +315,8 @@ describe("callStakworkAPI", () => { ); const fetchCall = mockFetch.mock.calls[0]; - const body = JSON.parse(fetchCall[1]?.body as string); + const _rawBody = JSON.parse(fetchCall[1]?.body as string); + const body = (_rawBody && typeof _rawBody === 'object' && 'project' in _rawBody) ? _rawBody.project : _rawBody; expect(body.workflow_params.set_var.attributes.vars.taskSource).toBe( "codebase_recommendation" @@ -548,7 +553,8 @@ describe("callStakworkAPI", () => { await callStakworkAPI(createTestParams({ mode: "plan_mode" })); const fetchCall = mockFetch.mock.calls[0]; - const body = JSON.parse(fetchCall[1]?.body as string); + const _rawBody = JSON.parse(fetchCall[1]?.body as string); + const body = (_rawBody && typeof _rawBody === 'object' && 'project' in _rawBody) ? _rawBody.project : _rawBody; expect(body.workflow_params.set_var.attributes.vars.model).toBe( "claude-3-5-sonnet" @@ -571,7 +577,8 @@ describe("callStakworkAPI", () => { await callStakworkAPI(createTestParams({ mode: "plan_mode" })); const fetchCall = mockFetch.mock.calls[0]; - const body = JSON.parse(fetchCall[1]?.body as string); + const _rawBody = JSON.parse(fetchCall[1]?.body as string); + const body = (_rawBody && typeof _rawBody === 'object' && 'project' in _rawBody) ? _rawBody.project : _rawBody; expect(body.workflow_params.set_var.attributes.vars.model).toBeUndefined(); @@ -589,7 +596,8 @@ describe("callStakworkAPI", () => { await callStakworkAPI(createTestParams({ contextTags: [] })); const fetchCall = mockFetch.mock.calls[0]; - const body = JSON.parse(fetchCall[1]?.body as string); + const _rawBody = JSON.parse(fetchCall[1]?.body as string); + const body = (_rawBody && typeof _rawBody === 'object' && 'project' in _rawBody) ? _rawBody.project : _rawBody; expect(body.workflow_params.set_var.attributes.vars.contextTags).toEqual( [] @@ -602,7 +610,8 @@ describe("callStakworkAPI", () => { await callStakworkAPI(createTestParams({ attachments: [] })); const fetchCall = mockFetch.mock.calls[0]; - const body = JSON.parse(fetchCall[1]?.body as string); + const _rawBody = JSON.parse(fetchCall[1]?.body as string); + const body = (_rawBody && typeof _rawBody === 'object' && 'project' in _rawBody) ? _rawBody.project : _rawBody; expect(body.workflow_params.set_var.attributes.vars.attachments).toEqual( [] @@ -615,7 +624,8 @@ describe("callStakworkAPI", () => { await callStakworkAPI(createTestParams({ userName: null })); const fetchCall = mockFetch.mock.calls[0]; - const body = JSON.parse(fetchCall[1]?.body as string); + const _rawBody = JSON.parse(fetchCall[1]?.body as string); + const body = (_rawBody && typeof _rawBody === 'object' && 'project' in _rawBody) ? _rawBody.project : _rawBody; expect(body.workflow_params.set_var.attributes.vars.alias).toBeNull(); expect(body.workflow_params.set_var.attributes.vars.username).toBeNull(); @@ -627,7 +637,8 @@ describe("callStakworkAPI", () => { await callStakworkAPI(createTestParams({ accessToken: null })); const fetchCall = mockFetch.mock.calls[0]; - const body = JSON.parse(fetchCall[1]?.body as string); + const _rawBody = JSON.parse(fetchCall[1]?.body as string); + const body = (_rawBody && typeof _rawBody === 'object' && 'project' in _rawBody) ? _rawBody.project : _rawBody; expect(body.workflow_params.set_var.attributes.vars.accessToken).toBeNull(); }); @@ -638,7 +649,8 @@ describe("callStakworkAPI", () => { await callStakworkAPI(createTestParams({ swarmSecretAlias: null })); const fetchCall = mockFetch.mock.calls[0]; - const body = JSON.parse(fetchCall[1]?.body as string); + const _rawBody = JSON.parse(fetchCall[1]?.body as string); + const body = (_rawBody && typeof _rawBody === 'object' && 'project' in _rawBody) ? _rawBody.project : _rawBody; expect( body.workflow_params.set_var.attributes.vars.swarmSecretAlias @@ -651,7 +663,8 @@ describe("callStakworkAPI", () => { await callStakworkAPI(createTestParams({ poolName: null })); const fetchCall = mockFetch.mock.calls[0]; - const body = JSON.parse(fetchCall[1]?.body as string); + const _rawBody = JSON.parse(fetchCall[1]?.body as string); + const body = (_rawBody && typeof _rawBody === 'object' && 'project' in _rawBody) ? _rawBody.project : _rawBody; expect(body.workflow_params.set_var.attributes.vars.poolName).toBeNull(); }); diff --git a/src/__tests__/unit/services/task-workflow-createChatMessageAndTriggerStakwork.test.ts b/src/__tests__/unit/services/task-workflow-createChatMessageAndTriggerStakwork.test.ts index 9e88d415ed..870b315c21 100644 --- a/src/__tests__/unit/services/task-workflow-createChatMessageAndTriggerStakwork.test.ts +++ b/src/__tests__/unit/services/task-workflow-createChatMessageAndTriggerStakwork.test.ts @@ -321,7 +321,8 @@ describe("createChatMessageAndTriggerStakwork", () => { ); const call = mockFetch.mock.calls[0]; - const body = JSON.parse(call[1].body); + const _rawBody = JSON.parse(call[1].body); + const body = (_rawBody && typeof _rawBody === 'object' && 'project' in _rawBody) ? _rawBody.project : _rawBody; expect(body.workflow_id).toBeDefined(); expect(body.webhook_url).toContain("/api/stakwork/webhook"); }); @@ -356,7 +357,8 @@ describe("createChatMessageAndTriggerStakwork", () => { // Assert const call = mockFetch.mock.calls[0]; - const body = JSON.parse(call[1].body); + const _rawBody = JSON.parse(call[1].body); + const body = (_rawBody && typeof _rawBody === 'object' && 'project' in _rawBody) ? _rawBody.project : _rawBody; expect(body.workflow_id).toBeDefined(); if (mode === "live") { @@ -599,7 +601,8 @@ describe("createChatMessageAndTriggerStakwork", () => { // Assert - Feature context should be included in the Stakwork API call expect(mockFetch).toHaveBeenCalled(); const call = mockFetch.mock.calls[0]; - const body = JSON.parse(call[1].body); + const _rawBody = JSON.parse(call[1].body); + const body = (_rawBody && typeof _rawBody === 'object' && 'project' in _rawBody) ? _rawBody.project : _rawBody; expect(body.workflow_params.set_var.attributes.vars.featureContext).toEqual(featureContext); }); }); diff --git a/src/__tests__/unit/services/task-workflow.test.ts b/src/__tests__/unit/services/task-workflow.test.ts index 52e0337c53..c6e18cce07 100644 --- a/src/__tests__/unit/services/task-workflow.test.ts +++ b/src/__tests__/unit/services/task-workflow.test.ts @@ -297,7 +297,8 @@ const TestHelpers = { expectStakworkCalledWithVars: (expectedVars: Record) => { const fetchCall = mockFetch.mock.calls[0]; - const payload = JSON.parse(fetchCall[1]?.body as string); + const _rawPayload = JSON.parse(fetchCall[1]?.body as string); + const payload = (_rawPayload && typeof _rawPayload === 'object' && 'project' in _rawPayload) ? _rawPayload.project : _rawPayload; const vars = payload.workflow_params.set_var.attributes.vars; Object.entries(expectedVars).forEach(([key, value]) => { @@ -311,7 +312,8 @@ const TestHelpers = { vars?: Record; }) => { const fetchCall = mockFetch.mock.calls[0]; - const payload = JSON.parse(fetchCall[1]?.body as string); + const _rawPayload = JSON.parse(fetchCall[1]?.body as string); + const payload = (_rawPayload && typeof _rawPayload === 'object' && 'project' in _rawPayload) ? _rawPayload.project : _rawPayload; if (expectedPayload.workflow_id !== undefined) { expect(payload.workflow_id).toBe(expectedPayload.workflow_id); @@ -701,7 +703,8 @@ describe("createChatMessageAndTriggerStakwork (via sendMessageToStakwork)", () = }); const fetchCall = mockFetch.mock.calls[0]; - const payload = JSON.parse(fetchCall[1]?.body as string); + const _rawPayload = JSON.parse(fetchCall[1]?.body as string); + const payload = (_rawPayload && typeof _rawPayload === 'object' && 'project' in _rawPayload) ? _rawPayload.project : _rawPayload; expect(payload.webhook_url).toBe( "http://localhost:3000/api/stakwork/webhook?task_id=test-task-id" @@ -729,7 +732,8 @@ describe("createChatMessageAndTriggerStakwork (via sendMessageToStakwork)", () = }); const fetchCall = mockFetch.mock.calls[0]; - const payload = JSON.parse(fetchCall[1]?.body as string); + const _rawPayload = JSON.parse(fetchCall[1]?.body as string); + const payload = (_rawPayload && typeof _rawPayload === 'object' && 'project' in _rawPayload) ? _rawPayload.project : _rawPayload; expect(payload.workflow_id).toBe(123); // First ID in "123,456,789" for live mode }); @@ -1028,7 +1032,8 @@ describe("createChatMessageAndTriggerStakwork (via createTaskWithStakworkWorkflo }); const fetchCall = mockFetch.mock.calls[0]; - const payload = JSON.parse(fetchCall[1]?.body as string); + const _rawPayload = JSON.parse(fetchCall[1]?.body as string); + const payload = (_rawPayload && typeof _rawPayload === 'object' && 'project' in _rawPayload) ? _rawPayload.project : _rawPayload; expect(payload.workflow_id).toBe(123); // First ID in "123,456,789" for live mode TestHelpers.expectStakworkCalledWithVars({ taskMode: "live", @@ -1048,7 +1053,8 @@ describe("createChatMessageAndTriggerStakwork (via createTaskWithStakworkWorkflo }); const fetchCall = mockFetch.mock.calls[0]; - const payload = JSON.parse(fetchCall[1]?.body as string); + const _rawPayload = JSON.parse(fetchCall[1]?.body as string); + const payload = (_rawPayload && typeof _rawPayload === 'object' && 'project' in _rawPayload) ? _rawPayload.project : _rawPayload; expect(payload.workflow_id).toBe(456); // Second ID for default mode TestHelpers.expectStakworkCalledWithVars({ taskMode: "default", @@ -1069,7 +1075,8 @@ describe("createChatMessageAndTriggerStakwork (via createTaskWithStakworkWorkflo }); const fetchCall = mockFetch.mock.calls[0]; - const payload = JSON.parse(fetchCall[1]?.body as string); + const _rawPayload = JSON.parse(fetchCall[1]?.body as string); + const payload = (_rawPayload && typeof _rawPayload === 'object' && 'project' in _rawPayload) ? _rawPayload.project : _rawPayload; expect(payload.workflow_id).toBe(789); // Third ID for unit mode }); @@ -1087,7 +1094,8 @@ describe("createChatMessageAndTriggerStakwork (via createTaskWithStakworkWorkflo }); const fetchCall = mockFetch.mock.calls[0]; - const payload = JSON.parse(fetchCall[1]?.body as string); + const _rawPayload = JSON.parse(fetchCall[1]?.body as string); + const payload = (_rawPayload && typeof _rawPayload === 'object' && 'project' in _rawPayload) ? _rawPayload.project : _rawPayload; expect(payload.workflow_id).toBe(789); // Third ID for integration mode }); }); @@ -1501,7 +1509,8 @@ describe("Feature Context Integration", () => { }); const fetchCall = mockFetch.mock.calls[0]; - const payload = JSON.parse(fetchCall[1]?.body as string); + const _rawPayload = JSON.parse(fetchCall[1]?.body as string); + const payload = (_rawPayload && typeof _rawPayload === 'object' && 'project' in _rawPayload) ? _rawPayload.project : _rawPayload; const vars = payload.workflow_params.set_var.attributes.vars; expect(vars.featureContext).toBeUndefined(); @@ -1937,7 +1946,8 @@ describe("generateChatTitle Parameter Handling", () => { }); const fetchCall = mockFetch.mock.calls[0]; - const payload = JSON.parse(fetchCall[1]?.body as string); + const _rawPayload = JSON.parse(fetchCall[1]?.body as string); + const payload = (_rawPayload && typeof _rawPayload === 'object' && 'project' in _rawPayload) ? _rawPayload.project : _rawPayload; const vars = payload.workflow_params.set_var.attributes.vars; expect(vars.generateChatTitle).toBeUndefined(); @@ -2190,7 +2200,8 @@ describe("Feature Context Integration", () => { }); const fetchCall = mockFetch.mock.calls[0]; - const payload = JSON.parse(fetchCall[1]?.body as string); + const _rawPayload = JSON.parse(fetchCall[1]?.body as string); + const payload = (_rawPayload && typeof _rawPayload === 'object' && 'project' in _rawPayload) ? _rawPayload.project : _rawPayload; const vars = payload.workflow_params.set_var.attributes.vars; expect(vars.featureContext).toBeUndefined(); @@ -2625,7 +2636,8 @@ describe("generateChatTitle Parameter Handling", () => { }); const fetchCall = mockFetch.mock.calls[0]; - const payload = JSON.parse(fetchCall[1]?.body as string); + const _rawPayload = JSON.parse(fetchCall[1]?.body as string); + const payload = (_rawPayload && typeof _rawPayload === 'object' && 'project' in _rawPayload) ? _rawPayload.project : _rawPayload; const vars = payload.workflow_params.set_var.attributes.vars; expect(vars.generateChatTitle).toBeUndefined(); @@ -2782,7 +2794,8 @@ describe("callStakworkAPI - Direct Unit Tests", () => { await callStakworkAPI(params); const fetchCall = mockFetch.mock.calls[0]; - const payload = JSON.parse(fetchCall[1]?.body as string); + const _rawPayload = JSON.parse(fetchCall[1]?.body as string); + const payload = (_rawPayload && typeof _rawPayload === 'object' && 'project' in _rawPayload) ? _rawPayload.project : _rawPayload; expect(payload.name).toBe("hive-task-test-task-id"); }); @@ -2804,7 +2817,8 @@ describe("callStakworkAPI - Direct Unit Tests", () => { await callStakworkAPI(params); const fetchCall = mockFetch.mock.calls[0]; - const payload = JSON.parse(fetchCall[1]?.body as string); + const _rawPayload = JSON.parse(fetchCall[1]?.body as string); + const payload = (_rawPayload && typeof _rawPayload === 'object' && 'project' in _rawPayload) ? _rawPayload.project : _rawPayload; expect(payload.name).toBe(`hive-plan-${featureId}`); }); @@ -3063,7 +3077,8 @@ describe("callStakworkAPI - Direct Unit Tests", () => { const body = fetchCall[1]?.body as string; expect(() => JSON.parse(body)).not.toThrow(); - const payload = JSON.parse(body); + const _rawPayload = JSON.parse(body); + const payload = (_rawPayload && typeof _rawPayload === "object" && "project" in _rawPayload) ? _rawPayload.project : _rawPayload; expect(payload).toHaveProperty("workflow_id"); expect(payload).toHaveProperty("workflow_params"); }); @@ -4849,7 +4864,8 @@ describe("startTaskWorkflow with includeHistory", () => { }); const fetchCall = mockFetch.mock.calls[0]; - const payload = JSON.parse(fetchCall[1]?.body as string); + const _rawPayload = JSON.parse(fetchCall[1]?.body as string); + const payload = (_rawPayload && typeof _rawPayload === 'object' && 'project' in _rawPayload) ? _rawPayload.project : _rawPayload; const vars = payload.workflow_params.set_var.attributes.vars; expect(vars).not.toHaveProperty("podId"); @@ -4896,7 +4912,8 @@ describe("startTaskWorkflow - featureId forwarding", () => { }); const fetchCall = mockFetch.mock.calls[0]; - const payload = JSON.parse(fetchCall[1]?.body as string); + const _rawPayload = JSON.parse(fetchCall[1]?.body as string); + const payload = (_rawPayload && typeof _rawPayload === 'object' && 'project' in _rawPayload) ? _rawPayload.project : _rawPayload; const vars = payload.workflow_params.set_var.attributes.vars; expect(vars.featureId).toBe("feature-123"); @@ -4927,7 +4944,8 @@ describe("startTaskWorkflow - featureId forwarding", () => { }); const fetchCall = mockFetch.mock.calls[0]; - const payload = JSON.parse(fetchCall[1]?.body as string); + const _rawPayload = JSON.parse(fetchCall[1]?.body as string); + const payload = (_rawPayload && typeof _rawPayload === 'object' && 'project' in _rawPayload) ? _rawPayload.project : _rawPayload; const vars = payload.workflow_params.set_var.attributes.vars; expect(vars).not.toHaveProperty("featureId");