Skip to content

refactor: full process in instance#18220

Open
adamhaeger wants to merge 36 commits intomainfrom
refactor/full-process-in-instance-2
Open

refactor: full process in instance#18220
adamhaeger wants to merge 36 commits intomainfrom
refactor/full-process-in-instance-2

Conversation

@adamhaeger
Copy link
Copy Markdown
Contributor

@adamhaeger adamhaeger commented Mar 18, 2026

Description

Extracts process state enrichment logic from ProcessController into a shared ProcessStateEnricher service and updates the form bootstrap endpoint to return the full Instance object and enriched AppProcessState (with authorized actions, read/write access, element types, and process task metadata).

Changes

  • New ProcessStateEnricher service — Extracted ConvertAndAuthorizeActions and AuthorizeActions from ProcessController into a reusable service in Altinn.App.Core/Internal/Process/
  • Split FormBootstrapResponse into separate types for each endpoint:
    • InstanceFormBootstrapResponse — adds Instance, Process (enriched AppProcessState), and ValidationIssues
    • StatelessFormBootstrapResponse — only shared properties (layouts, data models, static options)
  • Updated FormBootstrapService — injects ProcessStateEnricher, runs process enrichment in parallel with other bootstrap tasks
  • Refactored ProcessController — delegates to ProcessStateEnricher instead of private methods

This eliminates the need for the frontend to make separate GET /process and GET instance calls when bootstrapping a form.

Related issue(s)

Verification

  • Related issues are connected (if applicable)
  • Your code builds clean without any errors or warnings
  • Manual testing done (required)
  • Relevant automated test added (if you find this hard, leave it and we'll help out)

Summary by CodeRabbit

Release Notes

  • New Features

    • Process state responses now include enriched authorization details, displaying available user actions and access permissions per task.
    • Process tasks list is now included in instance responses.
  • Improvements

    • Process state is consolidated into instance data, eliminating separate process queries for improved performance.
    • Enhanced task navigation and process state caching efficiency.

@github-actions github-actions bot added the skip-releasenotes Issues that do not make sense to list in our release notes label Mar 18, 2026
@coderabbitai
Copy link
Copy Markdown
Contributor

coderabbitai bot commented Mar 18, 2026

Note

Reviews paused

It looks like this branch is under active development. To avoid overwhelming you with review comments due to an influx of new commits, CodeRabbit has automatically paused this review. You can configure this behavior by changing the reviews.auto_review.auto_pause_after_reviewed_commits setting.

Use the following commands to manage reviews:

  • @coderabbitai resume to resume automatic reviews.
  • @coderabbitai review to trigger a single review.

Use the checkboxes below for quick actions:

  • ▶️ Resume reviews
  • 🔍 Trigger review
📝 Walkthrough

Walkthrough

This pull request consolidates enriched process state into the instance endpoint by introducing ProcessStateEnricher on the backend to authorise and enrich process metadata, modifying instance responses to include this enriched state, and refactoring the frontend to fetch process data from the instance response rather than via a separate API call.

Changes

Cohort / File(s) Summary
Process State Enrichment Infrastructure
src/App/backend/src/Altinn.App.Core/Internal/Process/ProcessStateEnricher.cs, src/App/backend/src/Altinn.App.Core/Extensions/ServiceCollectionExtensions.cs
New ProcessStateEnricher class that constructs AppProcessState by authorising task actions, applying user/read/write access decisions, and enumerating all process tasks; registered as transient service.
Backend Controllers
src/App/backend/src/Altinn.App.Api/Controllers/InstancesController.cs, src/App/backend/src/Altinn.App.Api/Controllers/ProcessController.cs
InstancesController now retrieves enriched process state concurrently for both Get and Post endpoints. ProcessController delegates all enrichment to ProcessStateEnricher, removing internal authorisation logic and IAuthorizationService dependency in favour of injected ProcessStateEnricher.
Backend Response Model
src/App/backend/src/Altinn.App.Api/Models/InstanceResponse.cs
InstanceResponse.Process property type changed from ProcessState to AppProcessState; factory method updated to accept and assign enriched process state.
Backend Controller Tests
src/App/backend/test/Altinn.App.Api.Tests/Controllers/DataController_RequiredActionTests.cs, src/App/backend/test/Altinn.App.Api.Tests/Controllers/InstancesControllerFixture.cs
Mock setup extended to register IProcessReader, IAuthorizationService, and ProcessStateEnricher; authorisation mock configured to return all requested actions as authorised.
Backend Verified Output Tests
src/App/backend/test/Altinn.App.Api.Tests/Controllers/InstancesController_GetTests.*, src/App/backend/test/Altinn.App.Api.Tests/Controllers/InstancesController_PostNewInstanceTests.*, src/App/backend/test/Altinn.App.Api.Tests/OpenApi/OpenApiSpecChangeDetection.*, src/App/backend/test/Altinn.App.Core.Tests/PublicApiTests.*, src/App/backend/test/Altinn.App.Api.Tests/PublicApiTests.*
Verified outputs updated to reflect enriched process state in instance responses, including authorised actions, user actions, read/write access flags, element types, and process tasks list. OpenAPI schema reference updated to AppProcessState. Public API signatures reflect ProcessStateEnricher addition and InstanceResponse.Process type change.
Frontend: Query and Instance Data
src/App/frontend/src/core/queries/instance/index.ts, src/App/frontend/src/core/queries/instance/instance.queries.ts, src/App/frontend/src/features/instance/useProcessQuery.ts, src/App/frontend/src/features/instance/InstanceContext.tsx
Process fetching removed from separate query layer; fetchFreshInstanceData and useOptimisticallyUpdateInstance added for instance-based prefetching. useProcessQuery now reads process from instance query cache; process loading state removed from InstanceProvider.
Frontend: Instance and Process Utilities
src/App/frontend/src/features/instance/instanceUtils.ts, src/App/frontend/src/types/shared.ts
removeProcessFromInstance utility removed; IInstance type extended with optional process property.
Frontend: Feature Components
src/App/frontend/src/components/wrappers/ProcessWrapper.tsx, src/App/frontend/src/features/formData/FormDataWriteStateMachine.tsx, src/App/frontend/src/features/instance/useProcessNext.tsx, src/App/frontend/src/features/devtools/components/PermissionsEditor/PermissionsEditor.tsx, src/App/frontend/src/features/process/feedback/Feedback.tsx
Process data sourced from useCurrentInstance() instead of useProcessQuery() or separate process fetch; optimistic updates refactored to use useOptimisticallyUpdateInstance; process polling logic simplified to fetch fresh instance data.
Frontend: Route Loaders and Navigation
src/App/frontend/src/routes/instance/instance.loader.ts, src/App/frontend/src/routes/task/task.loader.ts, src/App/frontend/src/routes/instance/instance.route.tsx, src/App/frontend/src/features/instantiate/containers/InstantiateContainer.tsx, src/App/frontend/src/router.tsx
Loaders now await fresh instance data fetch instead of process-state prefetch; instance.route handles loader errors; route configuration reordered.
Frontend: URL Helpers and API
src/App/frontend/src/utils/urls/appUrlHelper.ts, src/App/frontend/src/queries/queries.ts
fetchProcessState query function and getProcessStateUrl helper removed; IgnoredQueriesAndMutations list updated accordingly.
Frontend: Tests
src/App/frontend/src/features/expressions/shared-functions.test.tsx, src/App/frontend/src/features/pdf/PDFWrapper.test.tsx, src/App/frontend/src/features/receipt/ReceiptContainer.test.tsx, src/App/frontend/src/layout/CustomButton/CustomButtonComponent.test.tsx, src/App/frontend/src/utils/layout/all.test.tsx, src/App/frontend/src/utils/urls/appUrlHelper.test.ts, src/App/frontend/src/setupTests.ts, src/App/frontend/test/e2e/integration/frontend-test/pdf.ts, src/App/frontend/src/queries/types.ts
Mock setup refactored to source process data from InstanceApi.getInstance instead of fetchProcessState; process-state prefetching removed from loaders; e2e test stub target changed from /process endpoint to /instances endpoint.

Sequence Diagram(s)

sequenceDiagram
    participant Client as Frontend Client
    participant Controller as InstancesController
    participant Enricher as ProcessStateEnricher
    participant Reader as IProcessReader
    participant Auth as IAuthorizationService
    participant Instance as Instance Data

    Client->>Controller: GET /instances/{partyId}/{guid}
    Controller->>Instance: Retrieve instance
    Note over Controller: Start concurrent tasks
    Controller->>Enricher: Enrich(instance, processState, user)
    Enricher->>Reader: Get current task element
    Enricher->>Auth: AuthorizeActions(actions)
    Auth-->>Enricher: Authorization decisions
    Enricher->>Reader: Get all ProcessTask elements
    Reader-->>Enricher: Task list
    Enricher-->>Controller: AppProcessState (enriched)
    Controller->>Controller: Build InstanceResponse
    Controller-->>Client: InstanceResponse with enriched process

    Note over Client,Auth: Process data now included in instance response
Loading

Estimated code review effort

🎯 4 (Complex) | ⏱️ ~60 minutes

Poem

🐰 A hop and a skip through process enriched,
Where instance and state are finally stitched,
No more separate calls in the digital flight,
One endpoint returns both data and might!
Authorised actions now bundled so tight,
The frontend rejoices—one fetch feels just right! 🎉

🚥 Pre-merge checks | ✅ 2 | ❌ 2

❌ Failed checks (2 inconclusive)

Check name Status Explanation Resolution
Linked Issues check ❓ Inconclusive The PR description mentions 'Split FormBootstrapResponse' and changes to FormBootstrapService, but the raw_summary shows no changes to FormBootstrapResponse or FormBootstrapService files. Clarify discrepancy: confirm whether FormBootstrapResponse/Service changes are included or if the description mentions planned work not yet implemented in this PR.
Out of Scope Changes check ❓ Inconclusive Frontend changes (removing fetchProcessState, updating instance queries, refactoring routing) go beyond the stated backend-focused objective and may extend into cross-cutting concerns not detailed in the PR description. Clarify the scope: confirm whether this PR intentionally includes frontend changes alongside backend refactoring, or if frontend changes should be in a separate PR.
✅ Passed checks (2 passed)
Check name Status Explanation
Title check ✅ Passed The PR title 'refactor: full process in instance' clearly summarizes the main change: consolidating process state into the instance response.
Description check ✅ Passed The PR description provides comprehensive detail on changes, includes a list of modifications, references the related issue, and confirms verification checks.

✏️ Tip: You can configure your own custom pre-merge checks in the settings.

✨ Finishing Touches
🧪 Generate unit tests (beta)
  • Create PR with unit tests
  • Commit unit tests in branch refactor/full-process-in-instance-2

Thanks for using CodeRabbit! It's free for OSS, and your support helps us grow. If you like it, consider giving us a shout-out.

❤️ Share

Comment @coderabbitai help to get the list of available commands and usage tips.

@adamhaeger adamhaeger changed the title Refactor/full process in instance 2 refactor: full process in instance 2 Mar 18, 2026
@github-actions
Copy link
Copy Markdown
Contributor

github-actions bot commented Mar 18, 2026

🔍 Lighthouse Report

URL Performance LCP CLS TBT Link to Report
/ttd/component-library/ 25/100 🟢 41757 ms 🟢 0 🟢 4108 ms 🟢 Report

Edited

@adamhaeger adamhaeger marked this pull request as ready for review March 19, 2026 08:51
@adamhaeger adamhaeger changed the title refactor: full process in instance 2 refactor: full process in instance Mar 19, 2026
Base automatically changed from refactor/api-client-instance to main March 24, 2026 13:37
Copy link
Copy Markdown
Contributor

@coderabbitai coderabbitai bot left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Actionable comments posted: 11

🧹 Nitpick comments (2)
src/App/frontend/src/core/queries/instance/index.ts (1)

56-69: Duplicated cache-lookup logic with useCurrentInstance.

The logic to find the instance entry in the cache (lines 60-61) duplicates the pattern in useCurrentInstance (lines 26-28). Consider extracting this into a shared helper to avoid divergence.

♻️ Proposed refactor
+function findInstanceCacheEntry(queryClient: QueryClient) {
+  const cached = queryClient.getQueriesData<IInstance>({ queryKey: instanceQueryKeys.all() });
+  return cached.find(([key, data]) => data && key.length === 2 && typeof key[1] === 'object');
+}
+
 function useCurrentInstance(): IInstance | undefined {
   const queryClient = useQueryClient();
-  return queryClient
-    .getQueriesData<IInstance>({ queryKey: instanceQueryKeys.all() })
-    .find(([key, data]) => data && key.length === 2 && typeof key[1] === 'object')?.[1];
+  return findInstanceCacheEntry(queryClient)?.[1];
 }

 function useOptimisticallyUpdateInstance() {
   const queryClient = useQueryClient();

   return (updater: (oldData: IInstance) => IInstance) => {
-    const cached = queryClient.getQueriesData<IInstance>({ queryKey: instanceQueryKeys.all() });
-    const entry = cached.find(([key, data]) => data && key.length === 2 && typeof key[1] === 'object');
+    const entry = findInstanceCacheEntry(queryClient);
     if (entry) {
       const [queryKey, oldData] = entry;
       if (oldData) {
         queryClient.setQueryData(queryKey, updater(oldData));
       }
     }
   };
 }
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@src/App/frontend/src/core/queries/instance/index.ts` around lines 56 - 69,
The cache-lookup logic in useOptimisticallyUpdateInstance duplicates the pattern
in useCurrentInstance; extract the shared logic into a small helper (e.g.,
findInstanceCacheEntry or getCachedCurrentInstance) that accepts the QueryClient
(or a getQueriesData function) and instanceQueryKeys.all() and returns the
matching [queryKey, data] tuple (or undefined), then replace the duplicated
lines in both useOptimisticallyUpdateInstance and useCurrentInstance to call
that helper; ensure the helper checks key.length === 2 and typeof key[1] ===
'object' and preserves the generic IInstance typing so
queryClient.setQueryData(queryKey, updater(oldData)) continues to work in
useOptimisticallyUpdateInstance.
src/App/backend/test/Altinn.App.Core.Tests/PublicApiTests.PublicApi_ShouldNotChange_Unintentionally.verified.txt (1)

3788-3792: Confirm whether ProcessStateEnricher should be part of the public contract.

Line 3788 exposes Altinn.App.Core.Internal.Process.ProcessStateEnricher as a public API type. If this is only an internal wiring detail, making it public in an .Internal. namespace can create long-term compatibility obligations. Please verify whether it can be internal (or moved to a non-internal public namespace if it is intentionally supported).

#!/bin/bash
# Verify declaration and usage footprint before deciding on API visibility.

# 1) Find the concrete declaration
rg -nP 'class\s+ProcessStateEnricher\b|public\s+sealed\s+class\s+ProcessStateEnricher\b'

# 2) Find constructor injections/usages to assess whether internal visibility is sufficient
rg -nP '\bProcessStateEnricher\b' -g '!**/*.verified.txt' -C 2

# 3) Check if any public APIs expose ProcessStateEnricher directly in signatures
rg -nP 'public\s+.*ProcessStateEnricher|ProcessStateEnricher\s+\w+\s*[\),]' src/App/backend/src -C 2
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In
`@src/App/backend/test/Altinn.App.Core.Tests/PublicApiTests.PublicApi_ShouldNotChange_Unintentionally.verified.txt`
around lines 3788 - 3792, The review flags ProcessStateEnricher (type
ProcessStateEnricher in namespace Altinn.App.Core.Internal.Process) being
publicly exposed; decide whether it should be internal or intentionally part of
the public contract and update its visibility accordingly: if it is only used
for internal wiring (constructor
ProcessStateEnricher(Altinn.App.Core.Internal.Process.IProcessReader,
Altinn.App.Core.Internal.Auth.IAuthorizationService) and method Enrich(...)),
change the class declaration to internal sealed class ProcessStateEnricher and
remove any public API signatures that directly expose it; if it must remain
public, move it out of the .Internal. namespace into a supported public
namespace and ensure public APIs that reference it remain stable. Ensure all
references/usages (constructor calls and Enrich usages) are updated to compile
after the visibility/namespace change.
🤖 Prompt for all review comments with AI agents
Verify each finding against the current code and only fix it if needed.

Inline comments:
In `@src/App/backend/src/Altinn.App.Api/Controllers/InstancesController.cs`:
- Around line 647-648: PostSimplified constructs the response from the
unfiltered instance; apply the same data-element filtering used elsewhere by
calling instance.WithOnlyAccessibleDataElements(_dataElementAccessChecker)
before building the DTO. Locate PostSimplified where processState is obtained
via _processStateEnricher.Enrich and replace (or wrap) the instance passed to
InstanceResponse.From(...) with the result of
instance.WithOnlyAccessibleDataElements(_dataElementAccessChecker) so the
response uses the filtered instance consistent with Post, Get, and
DataController.

In `@src/App/backend/src/Altinn.App.Api/Controllers/ProcessController.cs`:
- Around line 149-153: Remove the unnecessary null-conditional operators when
accessing ProcessChangeResult.ProcessStateChange in StartProcess and
NextElement: replace result.ProcessStateChange?.Events with
result.ProcessStateChange.Events and replace
result.ProcessStateChange?.NewProcessState with
result.ProcessStateChange.NewProcessState, because the method checks
result.Success and the type uses [MemberNotNullWhen(true,
nameof(ProcessStateChange))]; update the accesses in the StartProcess and
NextElement methods (and mirror the pattern used at the other occurrence) so the
code consistently reflects the non-null contract.

In `@src/App/backend/src/Altinn.App.Api/Models/InstanceResponse.cs`:
- Around line 51-53: The InstanceResponse DTO was changed to expose an enriched
CoreAppProcessState via the Process property which leaks an internal/enriched
shape into the public contract; revert the Process property on InstanceResponse
back to its original, shared contract (remove the
AppProcessState/CoreAppProcessState replacement) and keep the enriched payload
behind a new separate endpoint or DTO (e.g., create a dedicated enriched
response type and endpoint that returns CoreAppProcessState), ensuring
InstanceResponse.Process uses the original public type and does not reference
CoreAppProcessState or AppProcessState.

In
`@src/App/backend/src/Altinn.App.Core/Internal/Process/ProcessStateEnricher.cs`:
- Around line 46-55: The actions list built in ProcessStateEnricher (the local
variable actions that seeds new AltinnAction("read"), new("write") and then adds
processTask.ExtensionElements?.TaskExtension?.AltinnActions) can contain
duplicate Ids which causes ToDictionary and Single calls to throw; fix by
deduplicating action IDs before calling
_authorization.AuthorizeActions(instance, user, actions) (e.g. collapse
AltinnActions into the seeded list by Id so each action.Id is unique), then pass
the deduplicated list to _authorization.AuthorizeActions and continue assigning
appProcessState.CurrentTask.Actions, HasReadAccess and HasWriteAccess from the
returned authDecisions.

In
`@src/App/backend/test/Altinn.App.Api.Tests/Controllers/DataController_RequiredActionTests.cs`:
- Around line 277-281: The batched authorization mock on authorizationClientMock
for AuthorizeActions currently grants true for all actions; update it to mirror
the single-action logic used by AuthorizeAction by inspecting the incoming
actions list and returning hasOrgAuthorization-dependent results for
customReadAction and customWriteAction (i.e., consult the same
hasOrgAuthorization flag used by AuthorizeAction and return true/false
accordingly), so ProcessStateEnricher.cs and other callers using
AuthorizeActions get consistent behavior with the single-action path.

In
`@src/App/backend/test/Altinn.App.Api.Tests/Controllers/InstancesControllerFixture.cs`:
- Around line 136-138: The fixture registers loose mocks that return null for
critical calls; update the setup for the Mock<IProcessReader> and
Mock<IAuthorizationService> before calling .Object so they return safe defaults:
configure IProcessReader.GetAllFlowElements() to return an empty IEnumerable
(not null) and configure IAuthorizationService.AuthorizeActions(...) to return a
completed Task/result representing an empty/neutral authorization response (use
ReturnsAsync or Task.FromResult as appropriate). Keep the registration of
ProcessStateEnricher the same, but replace services.AddSingleton(new
Mock<IProcessReader>(...).Object) and services.AddSingleton(new
Mock<IAuthorizationService>(...).Object) with instances where the mocks are
created, set up (GetAllFlowElements and AuthorizeActions), then registered via
mock.Object so ProcessStateEnricher.Enrich() and its calls to
GetAllFlowElements() and AuthorizeActions() never encounter null.

In
`@src/App/backend/test/Altinn.App.Api.Tests/OpenApi/OpenApiSpecChangeDetection.SaveJsonSwagger.verified.json`:
- Around line 8688-8690: The change replaces InstanceResponse.process type from
ProcessState to AppProcessState which breaks the public response contract for
the GET /instances/{instanceOwnerPartyId}/{instanceGuid} endpoint; revert
InstanceResponse.process back to ProcessState to preserve backward compatibility
and, if you need the enriched AppProcessState data, add a new field or a
separate response schema/endpoint (e.g., InstanceResponseV2 or
/instances/{...}/details) that returns AppProcessState while leaving the
original InstanceResponse and ProcessState intact.

In `@src/App/frontend/src/layout/CustomButton/CustomButtonComponent.test.tsx`:
- Around line 128-167: The test mock in CustomButtonComponent.test.tsx sets
currentTask.flowType = 'CompleteCurrentMoveToNext', but that property isn't
declared on the ITask type; either remove flowType from the mock or add it to
the ITask interface in src/App/frontend/src/types/shared.ts. Locate the mock
inside the jest.mocked(InstanceApi.getInstance).mockImplementation block and
either delete the flowType line if it’s not required for assertions, or add
flowType?: string (or a proper union type like 'CompleteCurrentMoveToNext') to
the ITask definition so the mock conforms to the interface.

In `@src/App/frontend/src/routes/task/task.loader.ts`:
- Around line 8-14: The loader is incorrectly awaiting prefetchInstanceData
which contradicts the "fire-and-forget" comment and can cause navigation
failures if queryClient.prefetchQuery rejects; change the loader so it calls
prefetchInstanceData(instanceOwnerPartyId, instanceGuid) without await and
ensure the returned promise is swallowed (e.g., call it without awaiting and
attach a noop .catch to prevent rejections from bubbling) so warm-up remains
best-effort; update references in the loader function and ensure
prefetchInstanceData/queryClient.prefetchQuery errors are not allowed to reject
the loader.

In `@src/Runtime/workflow-engine/docs/presentation-technical.md`:
- Line 93: The prose in the table cell for the row "Timeout during auto-loop
iteration N" uses the range notation "Iterations 1..N-1 persisted. Iteration N
partially executed." which looks like accidental double punctuation; update that
phrase to a clearer range format such as "Iterations 1 to N-1 persisted.
Iteration N partially executed." (or "Iterations 1–(N-1) persisted...") so the
slide reads cleanly—change the text in the table cell containing "Iterations
1..N-1 persisted. Iteration N partially executed." accordingly.

In `@src/Runtime/workflow-engine/src/WorkflowEngine.Core/wwwroot/style.css`:
- Line 1279: Search the stylesheet for any occurrences of the deprecated
declaration "word-break: break-word" and replace each with "overflow-wrap:
break-word" (preserving any related vendor prefixes or other declarations in the
same rule); ensure the replacement is applied inside the same selector block so
layout/line-wrapping behavior remains unchanged.

---

Nitpick comments:
In
`@src/App/backend/test/Altinn.App.Core.Tests/PublicApiTests.PublicApi_ShouldNotChange_Unintentionally.verified.txt`:
- Around line 3788-3792: The review flags ProcessStateEnricher (type
ProcessStateEnricher in namespace Altinn.App.Core.Internal.Process) being
publicly exposed; decide whether it should be internal or intentionally part of
the public contract and update its visibility accordingly: if it is only used
for internal wiring (constructor
ProcessStateEnricher(Altinn.App.Core.Internal.Process.IProcessReader,
Altinn.App.Core.Internal.Auth.IAuthorizationService) and method Enrich(...)),
change the class declaration to internal sealed class ProcessStateEnricher and
remove any public API signatures that directly expose it; if it must remain
public, move it out of the .Internal. namespace into a supported public
namespace and ensure public APIs that reference it remain stable. Ensure all
references/usages (constructor calls and Enrich usages) are updated to compile
after the visibility/namespace change.

In `@src/App/frontend/src/core/queries/instance/index.ts`:
- Around line 56-69: The cache-lookup logic in useOptimisticallyUpdateInstance
duplicates the pattern in useCurrentInstance; extract the shared logic into a
small helper (e.g., findInstanceCacheEntry or getCachedCurrentInstance) that
accepts the QueryClient (or a getQueriesData function) and
instanceQueryKeys.all() and returns the matching [queryKey, data] tuple (or
undefined), then replace the duplicated lines in both
useOptimisticallyUpdateInstance and useCurrentInstance to call that helper;
ensure the helper checks key.length === 2 and typeof key[1] === 'object' and
preserves the generic IInstance typing so queryClient.setQueryData(queryKey,
updater(oldData)) continues to work in useOptimisticallyUpdateInstance.

ℹ️ Review info
⚙️ Run configuration

Configuration used: Path: .coderabbit.yaml

Review profile: CHILL

Plan: Pro

Run ID: c8f38351-0467-4e53-aa47-f300683715f0

📥 Commits

Reviewing files that changed from the base of the PR and between 74a047b and af10ae2.

📒 Files selected for processing (63)
  • .github/scripts/codeowners-generate.mjs
  • README.md
  • src/App/backend/src/Altinn.App.Api/Controllers/InstancesController.cs
  • src/App/backend/src/Altinn.App.Api/Controllers/ProcessController.cs
  • src/App/backend/src/Altinn.App.Api/Models/InstanceResponse.cs
  • src/App/backend/src/Altinn.App.Core/Extensions/ServiceCollectionExtensions.cs
  • src/App/backend/src/Altinn.App.Core/Internal/Process/ProcessStateEnricher.cs
  • src/App/backend/test/Altinn.App.Api.Tests/Controllers/DataController_RequiredActionTests.cs
  • src/App/backend/test/Altinn.App.Api.Tests/Controllers/InstancesControllerFixture.cs
  • src/App/backend/test/Altinn.App.Api.Tests/Controllers/InstancesController_GetTests.ReturnsOkResult_Deserialized.verified.txt
  • src/App/backend/test/Altinn.App.Api.Tests/Controllers/InstancesController_GetTests.ReturnsOkResult_Raw.verified.txt
  • src/App/backend/test/Altinn.App.Api.Tests/Controllers/InstancesController_PostNewInstanceTests.PostNewInstance_Simplified_SelfIdentifiedUser.verified.txt
  • src/App/backend/test/Altinn.App.Api.Tests/Controllers/InstancesController_PostNewInstanceTests.PostNewInstance_Simplified_ServiceOwner.verified.txt
  • src/App/backend/test/Altinn.App.Api.Tests/Controllers/InstancesController_PostNewInstanceTests.PostNewInstance_Simplified_SystemUser.verified.txt
  • src/App/backend/test/Altinn.App.Api.Tests/Controllers/InstancesController_PostNewInstanceTests.PostNewInstance_Simplified_User.verified.txt
  • src/App/backend/test/Altinn.App.Api.Tests/OpenApi/OpenApiSpecChangeDetection.SaveJsonSwagger.verified.json
  • src/App/backend/test/Altinn.App.Api.Tests/PublicApiTests.PublicApi_ShouldNotChange_Unintentionally.verified.txt
  • src/App/backend/test/Altinn.App.Core.Tests/PublicApiTests.PublicApi_ShouldNotChange_Unintentionally.verified.txt
  • src/App/frontend/src/components/wrappers/ProcessWrapper.tsx
  • src/App/frontend/src/core/queries/instance/index.ts
  • src/App/frontend/src/core/queries/instance/instance.queries.ts
  • src/App/frontend/src/features/devtools/components/PermissionsEditor/PermissionsEditor.tsx
  • src/App/frontend/src/features/expressions/shared-functions.test.tsx
  • src/App/frontend/src/features/formData/FormDataWriteStateMachine.tsx
  • src/App/frontend/src/features/instance/InstanceContext.tsx
  • src/App/frontend/src/features/instance/instanceUtils.ts
  • src/App/frontend/src/features/instance/useProcessNext.tsx
  • src/App/frontend/src/features/instance/useProcessQuery.ts
  • src/App/frontend/src/features/instantiate/containers/InstantiateContainer.tsx
  • src/App/frontend/src/features/pdf/PDFWrapper.test.tsx
  • src/App/frontend/src/features/process/feedback/Feedback.tsx
  • src/App/frontend/src/features/receipt/ReceiptContainer.test.tsx
  • src/App/frontend/src/layout/CustomButton/CustomButtonComponent.test.tsx
  • src/App/frontend/src/queries/queries.ts
  • src/App/frontend/src/queries/types.ts
  • src/App/frontend/src/router.tsx
  • src/App/frontend/src/routes/instance/instance.loader.ts
  • src/App/frontend/src/routes/task/task.loader.ts
  • src/App/frontend/src/setupTests.ts
  • src/App/frontend/src/types/shared.ts
  • src/App/frontend/src/utils/layout/all.test.tsx
  • src/App/frontend/src/utils/urls/appUrlHelper.test.ts
  • src/App/frontend/src/utils/urls/appUrlHelper.ts
  • src/Runtime/workflow-engine-app/AGENTS.md
  • src/Runtime/workflow-engine-app/README.md
  • src/Runtime/workflow-engine/.claude/skills/docker/SKILL.md
  • src/Runtime/workflow-engine/.claude/skills/format/SKILL.md
  • src/Runtime/workflow-engine/.claude/skills/k6/SKILL.md
  • src/Runtime/workflow-engine/README.md
  • src/Runtime/workflow-engine/docs/db-connections-notes.md
  • src/Runtime/workflow-engine/docs/presentation-technical.md
  • src/Runtime/workflow-engine/docs/proposals/recurring-workflows.md
  • src/Runtime/workflow-engine/docs/proposals/workflow-cancel-restart.md
  • src/Runtime/workflow-engine/docs/technical-guide.md
  • src/Runtime/workflow-engine/src/WorkflowEngine.Core/wwwroot/AGENTS.md
  • src/Runtime/workflow-engine/src/WorkflowEngine.Core/wwwroot/DASHBOARD_SPEC.md
  • src/Runtime/workflow-engine/src/WorkflowEngine.Core/wwwroot/app.js
  • src/Runtime/workflow-engine/src/WorkflowEngine.Core/wwwroot/modules/core/state.js
  • src/Runtime/workflow-engine/src/WorkflowEngine.Core/wwwroot/modules/features/filters.js
  • src/Runtime/workflow-engine/src/WorkflowEngine.Core/wwwroot/modules/features/modal.js
  • src/Runtime/workflow-engine/src/WorkflowEngine.Core/wwwroot/modules/features/url.js
  • src/Runtime/workflow-engine/src/WorkflowEngine.Core/wwwroot/modules/shared/dropdown.js
  • src/Runtime/workflow-engine/src/WorkflowEngine.Core/wwwroot/style.css
💤 Files with no reviewable changes (5)
  • src/App/frontend/src/utils/layout/all.test.tsx
  • src/App/frontend/src/queries/queries.ts
  • src/App/frontend/src/utils/urls/appUrlHelper.test.ts
  • src/App/frontend/src/utils/urls/appUrlHelper.ts
  • src/App/frontend/src/routes/instance/instance.loader.ts

Comment on lines +647 to +648
var processState = await _processStateEnricher.Enrich(instance, instance.Process, User);
var dto = InstanceResponse.From(instance, party, processState);
Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue | 🟠 Major

🧩 Analysis chain

🏁 Script executed:

#!/bin/bash
# Description: Check if WithOnlyAccessibleDataElements is used consistently across instance creation endpoints
# Expected: Understand if PostSimplified intentionally skips this check or if it's an oversight

rg -n "WithOnlyAccessibleDataElements" --type=cs -C 3

Repository: Altinn/altinn-studio

Length of output: 5097


🏁 Script executed:

# Read the context around lines 647-648 to see the method signature and surrounding code
sed -n '640,655p' src/App/backend/src/Altinn.App.Api/Controllers/InstancesController.cs

Repository: Altinn/altinn-studio

Length of output: 723


🏁 Script executed:

# Search for PostSimplified method definition to understand its purpose
rg -n "PostSimplified" --type=cs -B 5 -A 15 src/App/backend/src/Altinn.App.Api/Controllers/InstancesController.cs

Repository: Altinn/altinn-studio

Length of output: 944


🏁 Script executed:

# Read the complete PostSimplified method starting from line 464
sed -n '464,650p' src/App/backend/src/Altinn.App.Api/Controllers/InstancesController.cs

Repository: Altinn/altinn-studio

Length of output: 7098


🏁 Script executed:

# Check for any comments or documentation in PostSimplified or around it
sed -n '450,470p' src/App/backend/src/Altinn.App.Api/Controllers/InstancesController.cs

Repository: Altinn/altinn-studio

Length of output: 1104


🏁 Script executed:

# Search for any other references to PostSimplified that might explain design intent
rg -n "PostSimplified" --type=cs src/App/backend/src/Altinn.App.Api/

Repository: Altinn/altinn-studio

Length of output: 207


🏁 Script executed:

# Find and read the regular Post method documentation and signature
rg -n "public async Task<ActionResult<InstanceResponse>> Post\(" --type=cs -B 20 -A 5 src/App/backend/src/Altinn.App.Api/Controllers/InstancesController.cs | head -50

Repository: Altinn/altinn-studio

Length of output: 1580


Apply WithOnlyAccessibleDataElements to PostSimplified for consistency with other instance endpoints.

The Post endpoint applies instance.WithOnlyAccessibleDataElements(_dataElementAccessChecker) before constructing the response (line 419), as do the Get endpoint and DataController. The PostSimplified method should apply the same filtering to ensure data elements are restricted consistently across all instance-creation endpoints. Currently, lines 647–648 construct the response directly from the unfiltered instance.

🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@src/App/backend/src/Altinn.App.Api/Controllers/InstancesController.cs` around
lines 647 - 648, PostSimplified constructs the response from the unfiltered
instance; apply the same data-element filtering used elsewhere by calling
instance.WithOnlyAccessibleDataElements(_dataElementAccessChecker) before
building the DTO. Locate PostSimplified where processState is obtained via
_processStateEnricher.Enrich and replace (or wrap) the instance passed to
InstanceResponse.From(...) with the result of
instance.WithOnlyAccessibleDataElements(_dataElementAccessChecker) so the
response uses the filtered instance consistent with Post, Get, and
DataController.

Comment on lines +149 to 153
AppProcessState appProcessState = await _processStateEnricher.Enrich(
instance,
result.ProcessStateChange?.NewProcessState
result.ProcessStateChange?.NewProcessState,
User
);
Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue | 🟡 Minor

🧩 Analysis chain

🏁 Script executed:

#!/bin/bash
# Description: Check the ProcessChangeResult type to understand if ProcessStateChange can be null when Success is true

# Search for ProcessChangeResult class definition
ast-grep --pattern $'class ProcessChangeResult {
  $$$
}'

# Also check if there's nullability annotation on ProcessStateChange property
rg -n "ProcessStateChange" --type cs -C3 | head -80

Repository: Altinn/altinn-studio

Length of output: 7856


🏁 Script executed:

sed -n '140,160p' src/App/backend/src/Altinn.App.Api/Controllers/ProcessController.cs

Repository: Altinn/altinn-studio

Length of output: 895


🏁 Script executed:

sed -n '275,295p' src/App/backend/src/Altinn.App.Api/Controllers/ProcessController.cs

Repository: Altinn/altinn-studio

Length of output: 740


Remove unnecessary null-conditional operators from lines 147 and 151.

Both StartProcess and NextElement methods check result.Success before accessing ProcessStateChange, and the ProcessChangeResult type uses [MemberNotNullWhen(true, nameof(ProcessStateChange))] to guarantee non-null access when Success is true.

  • Line 147: result.ProcessStateChange?.Events should be result.ProcessStateChange.Events
  • Line 151: result.ProcessStateChange?.NewProcessState should be result.ProcessStateChange.NewProcessState

Line 287 correctly follows this pattern without the null-conditional operator. The current inconsistency obscures the actual contract and reduces code clarity.

🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@src/App/backend/src/Altinn.App.Api/Controllers/ProcessController.cs` around
lines 149 - 153, Remove the unnecessary null-conditional operators when
accessing ProcessChangeResult.ProcessStateChange in StartProcess and
NextElement: replace result.ProcessStateChange?.Events with
result.ProcessStateChange.Events and replace
result.ProcessStateChange?.NewProcessState with
result.ProcessStateChange.NewProcessState, because the method checks
result.Success and the type uses [MemberNotNullWhen(true,
nameof(ProcessStateChange))]; update the accesses in the StartProcess and
NextElement methods (and mirror the pattern used at the other occurrence) so the
code consistently reflects the non-null contract.

Comment on lines +46 to +55
var actions = new List<AltinnAction> { new("read"), new("write") };
actions.AddRange(
processTask.ExtensionElements?.TaskExtension?.AltinnActions ?? new List<AltinnAction>()
);
var authDecisions = await _authorization.AuthorizeActions(instance, user, actions);
appProcessState.CurrentTask.Actions = authDecisions
.Where(a => a.ActionType == ActionType.ProcessAction)
.ToDictionary(a => a.Id, a => a.Authorized);
appProcessState.CurrentTask.HasReadAccess = authDecisions.Single(a => a.Id == "read").Authorized;
appProcessState.CurrentTask.HasWriteAccess = authDecisions.Single(a => a.Id == "write").Authorized;
Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue | 🟠 Major

🧩 Analysis chain

🏁 Script executed:

cat -n src/App/backend/src/Altinn.App.Core/Internal/Process/ProcessStateEnricher.cs | head -100

Repository: Altinn/altinn-studio

Length of output: 4300


🏁 Script executed:

# Check the file size first
wc -l src/App/backend/src/Altinn.App.Core/Internal/Process/ProcessStateEnricher.cs

Repository: Altinn/altinn-studio

Length of output: 143


🏁 Script executed:

# Search for how actions are handled elsewhere in the codebase
rg "ToDictionary.*ActionType|AltinnActions" --type cs -A 2 -B 2

Repository: Altinn/altinn-studio

Length of output: 8709


🏁 Script executed:

# Look for any existing validation or deduplication of action IDs
rg "action.*id|duplicate|distinct" --type cs -i | grep -i action

Repository: Altinn/altinn-studio

Length of output: 50376


🏁 Script executed:

# Find the IAuthorizationService interface and AuthorizeActions method
rg "interface IAuthorizationService|AuthorizeActions" --type cs -A 3 | head -80

Repository: Altinn/altinn-studio

Length of output: 8774


🏁 Script executed:

# Check test files to see if they test duplicate action scenarios
rg "ProcessStateEnricher" --type cs -A 20 | head -100

Repository: Altinn/altinn-studio

Length of output: 11591


🏁 Script executed:

# Look at the AuthorizeActions implementation
fd "IAuthorizationService.cs|AuthorizationService.cs" --type f

Repository: Altinn/altinn-studio

Length of output: 469


🏁 Script executed:

# Check what MultiDecisionHelper does - seems to handle actions
cat -n src/App/backend/src/Altinn.App.Core/Helpers/MultiDecisionHelper.cs | head -150

Repository: Altinn/altinn-studio

Length of output: 6646


🏁 Script executed:

# Look at the full AuthorizationService.AuthorizeActions implementation
cat -n src/App/backend/src/Altinn.App.Core/Internal/Auth/AuthorizationService.cs | grep -A 40 "public async Task<List<UserAction>> AuthorizeActions"

Repository: Altinn/altinn-studio

Length of output: 1699


🏁 Script executed:

# Check the IAuthorizationService interface to see the exact return type
cat -n src/App/backend/src/Altinn.App.Core/Internal/Auth/IAuthorizationService.cs | grep -A 10 "AuthorizeActions"

Repository: Altinn/altinn-studio

Length of output: 194


🏁 Script executed:

# Check if there are any tests that verify duplicate action handling
rg "read.*write|duplicate.*action" --type cs src/App/backend/test | head -40

Repository: Altinn/altinn-studio

Length of output: 3811


🏁 Script executed:

# Look for test files for ProcessStateEnricher
fd "ProcessStateEnricher.*Test" --type f

Repository: Altinn/altinn-studio

Length of output: 46


🏁 Script executed:

# Look for any tests that might test ProcessStateEnricher
rg "ProcessStateEnricher" --type cs -l

Repository: Altinn/altinn-studio

Length of output: 440


🏁 Script executed:

# Check if there's any deduplication logic in the authorization flow
rg "Distinct|distinct|HashSet|RemoveDuplicates" --type cs src/App/backend/src/Altinn.App.Core/Internal | head -20

Repository: Altinn/altinn-studio

Length of output: 765


🏁 Script executed:

# Check the UserAction class to see its structure
fd "UserAction.cs" --type f -x cat -n {}

Repository: Altinn/altinn-studio

Length of output: 20346


🏁 Script executed:

# Check test data to see how tasks define their actions
rg "AltinnActions.*=.*new|new.*AltinnAction" --type cs src/App/backend/test -A 1 | head -80

Repository: Altinn/altinn-studio

Length of output: 5721


🏁 Script executed:

# Look at actual BPMN or process definition examples to see if read/write can be defined by apps
fd "*.bpmn" --type f | head -5

Repository: Altinn/altinn-studio

Length of output: 349


🏁 Script executed:

# Check if there are any integration tests that exercise ProcessStateEnricher
rg "ProcessStateEnricher|CurrentTask.*Actions" --type cs src/App/backend/test -B 2 -A 8 | head -100

Repository: Altinn/altinn-studio

Length of output: 1547


🏁 Script executed:

# Look at InstancesControllerFixture test to understand how ProcessStateEnricher is tested
cat -n src/App/backend/test/Altinn.App.Api.Tests/Controllers/InstancesControllerFixture.cs | grep -A 30 "ProcessStateEnricher"

Repository: Altinn/altinn-studio

Length of output: 1264


🏁 Script executed:

# Check if there are unit tests specifically for ProcessStateEnricher
rg "ProcessStateEnricher" --type cs src/App/backend/test -B 5 -A 15 | head -150

Repository: Altinn/altinn-studio

Length of output: 2956


🏁 Script executed:

# Examine the AltinnAction class to confirm its properties
cat -n src/App/backend/src/Altinn.App.Core/Internal/Process/Elements/AltinnExtensionProperties/AltinnAction.cs

Repository: Altinn/altinn-studio

Length of output: 2360


🏁 Script executed:

# Confirm that authDecisions is a Dictionary<string, bool> and thus would also fail with duplicate keys
cat -n src/App/backend/src/Altinn.App.Core/Internal/Auth/IAuthorizationClient.cs | grep -A 3 "AuthorizeActions"

Repository: Altinn/altinn-studio

Length of output: 295


🏁 Script executed:

# Check if there's any deduplication or validation before calling AuthorizeActions in ProcessStateEnricher
cat -n src/App/backend/src/Altinn.App.Core/Internal/Process/ProcessStateEnricher.cs | sed -n '40,60p'

Repository: Altinn/altinn-studio

Length of output: 1521


Deduplicating action IDs prevents runtime exceptions in permission mapping.

If a task's BPMN extension defines read, write, or any action ID already in the default set, the combined actions list contains duplicates. When authDecisions (a List<UserAction>) is passed to ToDictionary(a => a.Id, ...) on line 53, duplicate keys cause an ArgumentException, resulting in a 500 error on process state requests. Deduplicating before authorisation ensures each action ID appears exactly once, allowing both ToDictionary and the Single() calls on lines 54–55 to execute safely.

🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@src/App/backend/src/Altinn.App.Core/Internal/Process/ProcessStateEnricher.cs`
around lines 46 - 55, The actions list built in ProcessStateEnricher (the local
variable actions that seeds new AltinnAction("read"), new("write") and then adds
processTask.ExtensionElements?.TaskExtension?.AltinnActions) can contain
duplicate Ids which causes ToDictionary and Single calls to throw; fix by
deduplicating action IDs before calling
_authorization.AuthorizeActions(instance, user, actions) (e.g. collapse
AltinnActions into the seeded list by Id so each action.Id is unique), then pass
the deduplicated list to _authorization.AuthorizeActions and continue assigning
appProcessState.CurrentTask.Actions, HasReadAccess and HasWriteAccess from the
returned authDecisions.

Comment on lines +277 to +281
authorizationClientMock
.Setup(x => x.AuthorizeActions(It.IsAny<Instance>(), It.IsAny<ClaimsPrincipal>(), It.IsAny<List<string>>()))
.ReturnsAsync(
(Instance _, ClaimsPrincipal _, List<string> actions) => actions.ToDictionary(a => a, _ => true)
);
Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue | 🟡 Minor

🧩 Analysis chain

🏁 Script executed:

wc -l src/App/backend/test/Altinn.App.Api.Tests/Controllers/DataController_RequiredActionTests.cs

Repository: Altinn/altinn-studio

Length of output: 159


🏁 Script executed:

sed -n '260,295n' src/App/backend/test/Altinn.App.Api.Tests/Controllers/DataController_RequiredActionTests.cs

Repository: Altinn/altinn-studio

Length of output: 46


🏁 Script executed:

# Find the test method containing these lines to get full context
rg -A 50 -B 20 "AuthorizeActions" src/App/backend/test/Altinn.App.Api.Tests/Controllers/DataController_RequiredActionTests.cs | head -100

Repository: Altinn/altinn-studio

Length of output: 1295


🏁 Script executed:

sed -n '200,285n' src/App/backend/test/Altinn.App.Api.Tests/Controllers/DataController_RequiredActionTests.cs

Repository: Altinn/altinn-studio

Length of output: 46


🏁 Script executed:

# Let's read the entire file to understand the test structure
cat -n src/App/backend/test/Altinn.App.Api.Tests/Controllers/DataController_RequiredActionTests.cs

Repository: Altinn/altinn-studio

Length of output: 13548


🏁 Script executed:

# Find if AuthorizeActions is actually called in production code
rg "AuthorizeActions" src/App/backend --type cs -g "!test" -A 3 -B 3

Repository: Altinn/altinn-studio

Length of output: 8385


🏁 Script executed:

# Check if there are any other test files that test AuthorizeActions consistency
rg "AuthorizeActions" src/App/backend/test --type cs -l

Repository: Altinn/altinn-studio

Length of output: 418


🏁 Script executed:

# Check how AuthorizeActions is mocked in AuthorizationMock.cs
cat -n src/App/backend/test/Altinn.App.Api.Tests/Mocks/AuthorizationMock.cs

Repository: Altinn/altinn-studio

Length of output: 3365


Align the batched authorisation mock with the single-action path.

The AuthorizeActions mock currently returns true for all actions unconditionally, whereas AuthorizeAction respects the hasOrgAuthorization parameter for customReadAction and customWriteAction. This inconsistency contradicts the mock pattern used elsewhere (e.g., AuthorizationMock.cs) and can hide regressions as production code in ProcessStateEnricher.cs actively uses the batched API to authorise process task actions.

Suggested adjustment
         authorizationClientMock
             .Setup(x => x.AuthorizeActions(It.IsAny<Instance>(), It.IsAny<ClaimsPrincipal>(), It.IsAny<List<string>>()))
             .ReturnsAsync(
-                (Instance _, ClaimsPrincipal _, List<string> actions) => actions.ToDictionary(a => a, _ => true)
+                (Instance _, ClaimsPrincipal _, List<string> actions) =>
+                    actions.ToDictionary(
+                        action => action,
+                        action => action switch
+                        {
+                            "customReadAction" => hasOrgAuthorization,
+                            "customWriteAction" => hasOrgAuthorization,
+                            _ => true,
+                        }
+                    )
             );
📝 Committable suggestion

‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.

Suggested change
authorizationClientMock
.Setup(x => x.AuthorizeActions(It.IsAny<Instance>(), It.IsAny<ClaimsPrincipal>(), It.IsAny<List<string>>()))
.ReturnsAsync(
(Instance _, ClaimsPrincipal _, List<string> actions) => actions.ToDictionary(a => a, _ => true)
);
authorizationClientMock
.Setup(x => x.AuthorizeActions(It.IsAny<Instance>(), It.IsAny<ClaimsPrincipal>(), It.IsAny<List<string>>()))
.ReturnsAsync(
(Instance _, ClaimsPrincipal _, List<string> actions) =>
actions.ToDictionary(
action => action,
action => action switch
{
"customReadAction" => hasOrgAuthorization,
"customWriteAction" => hasOrgAuthorization,
_ => true,
}
)
);
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In
`@src/App/backend/test/Altinn.App.Api.Tests/Controllers/DataController_RequiredActionTests.cs`
around lines 277 - 281, The batched authorization mock on
authorizationClientMock for AuthorizeActions currently grants true for all
actions; update it to mirror the single-action logic used by AuthorizeAction by
inspecting the incoming actions list and returning hasOrgAuthorization-dependent
results for customReadAction and customWriteAction (i.e., consult the same
hasOrgAuthorization flag used by AuthorizeAction and return true/false
accordingly), so ProcessStateEnricher.cs and other callers using
AuthorizeActions get consistent behavior with the single-action path.

Comment on lines +128 to 167
jest.mocked(InstanceApi.getInstance).mockImplementation(async () => ({
...getInstanceDataMock(),
process: {
started: '2024-01-03T06:52:49.716640678Z',
ended: null,
validated: null,
flowType: 'CompleteCurrentMoveToNext',
endEvent: null,
startEvent: '2024-01-03T06:52:49.716640678Z',
currentTask: {
userActions: [
{
id: 'read',
authorized: true,
type: 'ProcessAction',
},
{
id: 'write',
authorized: true,
type: 'ProcessAction',
},
{
id: 'complete',
authorized: false,
type: 'ProcessAction',
},
...(actionAuthorization ?? []),
],
read: true,
write: true,
flow: 2,
started: '2024-01-03T06:37:22.7573522Z',
elementId: 'Task_1',
elementType: 'Task',
name: 'Utfylling',
altinnTaskType: 'data',
ended: null,
validated: null,
flowType: 'CompleteCurrentMoveToNext',
},
},
}));
Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue | 🟡 Minor

🧩 Analysis chain

🏁 Script executed:

#!/bin/bash
# Check if flowType is defined in the ITask interface or related types
rg -n 'flowType' --type=ts src/App/frontend/src/types/

Repository: Altinn/altinn-studio

Length of output: 46


🏁 Script executed:

#!/bin/bash
# Search for flowType in the entire frontend codebase
rg -n 'flowType' src/App/frontend/ --type=ts --type=tsx

Repository: Altinn/altinn-studio

Length of output: 91


🏁 Script executed:

#!/bin/bash
# Find and examine the ITask interface definition
fd -e ts -e tsx 'shared' src/App/frontend/ | head -20

Repository: Altinn/altinn-studio

Length of output: 515


🏁 Script executed:

#!/bin/bash
# Look for the ITask interface definition
rg -n 'interface ITask|type ITask' src/App/frontend/ --type=ts --type=tsx -A 15

Repository: Altinn/altinn-studio

Length of output: 91


🏁 Script executed:

#!/bin/bash
# Read the shared.ts file to find ITask interface
cat -n src/App/frontend/src/types/shared.ts | head -100

Repository: Altinn/altinn-studio

Length of output: 3055


🏁 Script executed:

#!/bin/bash
# Search for flowType without type filter
rg -n 'flowType' src/App/frontend/

Repository: Altinn/altinn-studio

Length of output: 186


🏁 Script executed:

#!/bin/bash
# Check the test file around line 164
sed -n '150,170p' src/App/frontend/src/layout/CustomButton/CustomButtonComponent.test.tsx

Repository: Altinn/altinn-studio

Length of output: 568


🏁 Script executed:

#!/bin/bash
# Continue reading shared.ts to find ITask and IProcess interfaces
sed -n '100,300p' src/App/frontend/src/types/shared.ts

Repository: Altinn/altinn-studio

Length of output: 4978


🏁 Script executed:

#!/bin/bash
# Search for IProcess interface
rg -n 'interface IProcess|type IProcess' src/App/frontend/

Repository: Altinn/altinn-studio

Length of output: 271


🏁 Script executed:

#!/bin/bash
# Check if there are any TypeScript configuration files that might affect strict checking
find src/App/frontend -maxdepth 2 -name 'tsconfig*.json' -o -name '.eslintignore' | head -10

Repository: Altinn/altinn-studio

Length of output: 172


🏁 Script executed:

#!/bin/bash
# Look for any recent changes or TODOs related to flowType or task properties
rg -n 'flowType|TODO|FIXME' src/App/frontend/src/layout/CustomButton/CustomButtonComponent.test.tsx -B 2 -A 2

Repository: Altinn/altinn-studio

Length of output: 192


Remove flowType property from the mock or add it to the ITask interface.

The mock object includes flowType: 'CompleteCurrentMoveToNext' (line 164), but this property is not defined in the ITask interface at src/App/frontend/src/types/shared.ts. Either remove this property from the mock if it is not needed, or add it to the interface if it is a valid task property.

🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@src/App/frontend/src/layout/CustomButton/CustomButtonComponent.test.tsx`
around lines 128 - 167, The test mock in CustomButtonComponent.test.tsx sets
currentTask.flowType = 'CompleteCurrentMoveToNext', but that property isn't
declared on the ITask type; either remove flowType from the mock or add it to
the ITask interface in src/App/frontend/src/types/shared.ts. Locate the mock
inside the jest.mocked(InstanceApi.getInstance).mockImplementation block and
either delete the flowType line if it’s not required for assertions, or add
flowType?: string (or a proper union type like 'CompleteCurrentMoveToNext') to
the ITask definition so the mock conforms to the interface.

Comment on lines +8 to +14
return async function loader({ params }: LoaderFunctionArgs) {
const { instanceOwnerPartyId, instanceGuid } = params;
const instanceId = instanceOwnerPartyId && instanceGuid ? `${instanceOwnerPartyId}/${instanceGuid}` : undefined;

// Fire-and-forget: warm the cache without blocking route rendering.
// Instance and process data should already be cached from the parent instance loader.
if (instanceOwnerPartyId && instanceGuid) {
prefetchInstanceData(queryClient, { instanceOwnerPartyId, instanceGuid });
}
if (instanceId) {
queryClient.prefetchQuery(processQueries.processState(instanceId));
await prefetchInstanceData(queryClient, { instanceOwnerPartyId, instanceGuid });
Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue | 🟠 Major

🧩 Analysis chain

🏁 Script executed:

#!/bin/bash
set -euo pipefail

echo "1) task loader implementation"
fd 'task.loader.ts$' src/App/frontend -x sh -c 'nl -ba "$1" | sed -n "7,24p"' sh {}

echo
echo "2) prefetchInstanceData definition and nearby logic"
rg -n --type=ts -C5 '\bprefetchInstanceData\b' src/App/frontend/src/core/queries/instance

echo
echo "3) Check whether instance prefetch helpers catch errors internally"
rg -n --type=ts -C4 '\b(prefetchQuery|fetchQuery)\b|\btry\b|\bcatch\b' src/App/frontend/src/core/queries/instance

echo
echo "4) Route error boundary wiring (impact if loader rejects)"
rg -n --type=ts -C3 '\berrorElement\b|\btaskLoader\b' src/App/frontend/src/routes

Repository: Altinn/altinn-studio

Length of output: 4661


🏁 Script executed:

#!/bin/bash
set -euo pipefail

echo "=== 1) Complete task loader file ==="
cat -n src/App/frontend/src/routes/task/task.loader.ts

echo
echo "=== 2) Check for errorElement in route configuration ==="
rg -n --type=ts 'taskLoader|errorElement' src/App/frontend/src/routes -B2 -A2

echo
echo "=== 3) TanStack Query usage - check instanceDataQuery definition ==="
rg -n --type=ts 'instanceDataQuery' src/App/frontend/src/core/queries/instance -B2 -A5

echo
echo "=== 4) Check queryOptions usage pattern ==="
rg -n --type=ts 'queryOptions' src/App/frontend/src/core/queries/instance

Repository: Altinn/altinn-studio

Length of output: 4241


🏁 Script executed:

#!/bin/bash
set -euo pipefail

echo "=== Route configuration using taskLoader ==="
rg -n 'taskLoader' src/App/frontend/src/routes -B3 -A3 --type=ts

echo
echo "=== Check for parent route error boundary ==="
rg -n 'errorElement' src/App/frontend/src/routes -B2 -A2 --type=ts

Repository: Altinn/altinn-studio

Length of output: 754


🏁 Script executed:

#!/bin/bash
set -euo pipefail

echo "=== Find all imports of taskLoader ==="
rg -n 'from.*task\.loader|import.*taskLoader' src/App/frontend --type=ts --type=tsx

echo
echo "=== Look for route configuration files ==="
fd -e ts -e tsx 'route' src/App/frontend/src/routes | head -20

echo
echo "=== Check main route config or App routes ==="
fd -e ts -e tsx '(routes|App)' src/App/frontend/src | grep -E '(routes\.|App\.route|main.*route)' | head -10

Repository: Altinn/altinn-studio

Length of output: 147


🏁 Script executed:

#!/bin/bash
set -euo pipefail

echo "=== Find all imports of taskLoader ==="
rg -n 'taskLoader' src/App/frontend --type=typescript

echo
echo "=== Look for route configuration or index files ==="
find src/App/frontend/src/routes -name '*.ts' -o -name '*.tsx' | head -20

echo
echo "=== Search for Router or route setup in main App files ==="
find src/App/frontend/src -maxdepth 2 -name '*[Rr]oute*' -o -name 'App*'

Repository: Altinn/altinn-studio

Length of output: 154


🏁 Script executed:

#!/bin/bash
set -euo pipefail

echo "=== Find all references to taskLoader ==="
rg 'taskLoader' src/App/frontend

echo
echo "=== List route directory structure ==="
find src/App/frontend/src/routes -type f | head -30

Repository: Altinn/altinn-studio

Length of output: 1129


🏁 Script executed:

#!/bin/bash
set -euo pipefail

echo "=== Router configuration around taskLoader ==="
rg -n -B5 -A5 'taskLoader' src/App/frontend/src/router.tsx

echo
echo "=== Check for errorElement in route configuration ==="
rg -n -B2 -A2 'errorElement' src/App/frontend/src/router.tsx

echo
echo "=== Check task route file ==="
cat -n src/App/frontend/src/routes/task/task.route.tsx | head -50

Repository: Altinn/altinn-studio

Length of output: 1432


🏁 Script executed:

#!/bin/bash
set -euo pipefochip

echo "=== Full router.tsx to check errorElement anywhere ==="
cat -n src/App/frontend/src/router.tsx | grep -i 'error'

echo
echo "=== Check task.route.tsx for ErrorBoundary ==="
cat src/App/frontend/src/routes/task/task.route.tsx

echo
echo "=== Verify prefetchInstanceData can reject ==="
cat -n src/App/frontend/src/core/queries/instance/instance.queries.ts | sed -n '19,35p'

Repository: Altinn/altinn-studio

Length of output: 119


🏁 Script executed:

#!/bin/bash
set -euo pipefail

echo "=== Full router.tsx to check errorElement anywhere ==="
cat -n src/App/frontend/src/router.tsx | grep -i 'error'

echo
echo "=== Check task.route.tsx for ErrorBoundary ==="
cat src/App/frontend/src/routes/task/task.route.tsx

echo
echo "=== Verify prefetchInstanceData can reject ==="
cat -n src/App/frontend/src/core/queries/instance/instance.queries.ts | sed -n '19,35p'

Repository: Altinn/altinn-studio

Length of output: 118


🏁 Script executed:

#!/bin/bash
set -euo pipefail

echo "=== Check task.route.tsx ==="
cat src/App/frontend/src/routes/task/task.route.tsx

echo
echo "=== Check full router.tsx for errorElement (using rg with no filters) ==="
rg -i 'errorElement' src/App/frontend/src/router.tsx || echo "No errorElement found"

echo
echo "=== Verify instance data query definition and retry behaviour ==="
sed -n '19,35p' src/App/frontend/src/core/queries/instance/instance.queries.ts

Repository: Altinn/altinn-studio

Length of output: 1314


Loader comment and implementation are inconsistent; blocking prefetch can fail navigation

Line 14 uses await making the prefetch blocking, while lines 11–12 describe "fire-and-forget" non-blocking behaviour. prefetchInstanceData returns queryClient.prefetchQuery() which can reject if the fetch fails (even after retries), but the loader has no error handling. With no error boundary in the route configuration, a rejected prefetch will break navigation instead of gracefully skipping the warm-up.

Restore best-effort behaviour by removing the await:

Suggested fix
-    // Fire-and-forget: warm the cache without blocking route rendering.
-    // Instance and process data should already be cached from the parent instance loader.
+    // Best-effort warm-up: do not block route rendering.
+    // Instance and process data should already be cached from the parent instance loader.
     if (instanceOwnerPartyId && instanceGuid) {
-      await prefetchInstanceData(queryClient, { instanceOwnerPartyId, instanceGuid });
+      void prefetchInstanceData(queryClient, { instanceOwnerPartyId, instanceGuid }).catch(() => {
+        // Ignore warm-up failures; downstream queries can still fetch as needed.
+      });
     }
📝 Committable suggestion

‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.

Suggested change
return async function loader({ params }: LoaderFunctionArgs) {
const { instanceOwnerPartyId, instanceGuid } = params;
const instanceId = instanceOwnerPartyId && instanceGuid ? `${instanceOwnerPartyId}/${instanceGuid}` : undefined;
// Fire-and-forget: warm the cache without blocking route rendering.
// Instance and process data should already be cached from the parent instance loader.
if (instanceOwnerPartyId && instanceGuid) {
prefetchInstanceData(queryClient, { instanceOwnerPartyId, instanceGuid });
}
if (instanceId) {
queryClient.prefetchQuery(processQueries.processState(instanceId));
await prefetchInstanceData(queryClient, { instanceOwnerPartyId, instanceGuid });
return async function loader({ params }: LoaderFunctionArgs) {
const { instanceOwnerPartyId, instanceGuid } = params;
// Best-effort warm-up: do not block route rendering.
// Instance and process data should already be cached from the parent instance loader.
if (instanceOwnerPartyId && instanceGuid) {
void prefetchInstanceData(queryClient, { instanceOwnerPartyId, instanceGuid }).catch(() => {
// Ignore warm-up failures; downstream queries can still fetch as needed.
});
}
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@src/App/frontend/src/routes/task/task.loader.ts` around lines 8 - 14, The
loader is incorrectly awaiting prefetchInstanceData which contradicts the
"fire-and-forget" comment and can cause navigation failures if
queryClient.prefetchQuery rejects; change the loader so it calls
prefetchInstanceData(instanceOwnerPartyId, instanceGuid) without await and
ensure the returned promise is swallowed (e.g., call it without awaiting and
attach a noop .catch to prevent rejections from bubbling) so warm-up remains
best-effort; update references in the loader function and ensure
prefetchInstanceData/queryClient.prefetchQuery errors are not allowed to reject
the loader.

| Service task crashes (PDF, signing, payment) | In-memory changes lost. Instance stays at previous state. |
| Storage API call times out | State unknown. May or may not have persisted. No idempotency. |
| Pod killed mid-processing | Instance lock held for 5 minutes. Data state unclear. |
| Timeout during auto-loop iteration N | Iterations 1..N-1 persisted. Iteration N partially executed. |
Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue | 🟡 Minor

Fix punctuation in range notation for slide readability.

Line 93 uses 1..N-1, which reads as accidental double punctuation in prose. Use a clearer range format (for example 1 to N-1 or 1–(N-1)).

✏️ Proposed text fix
-| Timeout during auto-loop iteration N         | Iterations 1..N-1 persisted. Iteration N partially executed.  |
+| Timeout during auto-loop iteration N         | Iterations 1 to N-1 persisted. Iteration N partially executed. |
📝 Committable suggestion

‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.

Suggested change
| Timeout during auto-loop iteration N | Iterations 1..N-1 persisted. Iteration N partially executed. |
| Timeout during auto-loop iteration N | Iterations 1 to N-1 persisted. Iteration N partially executed. |
🧰 Tools
🪛 LanguageTool

[typographical] ~93-~93: Two consecutive dots
Context: ...-loop iteration N | Iterations 1..N-1 persisted. Iteration N partially exe...

(DOUBLE_PUNCTUATION)

🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@src/Runtime/workflow-engine/docs/presentation-technical.md` at line 93, The
prose in the table cell for the row "Timeout during auto-loop iteration N" uses
the range notation "Iterations 1..N-1 persisted. Iteration N partially
executed." which looks like accidental double punctuation; update that phrase to
a clearer range format such as "Iterations 1 to N-1 persisted. Iteration N
partially executed." (or "Iterations 1–(N-1) persisted...") so the slide reads
cleanly—change the text in the table cell containing "Iterations 1..N-1
persisted. Iteration N partially executed." accordingly.

color: var(--text);
line-height: 1.5;
word-break: break-word;
}
Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue | 🟡 Minor

Replace deprecated CSS property.

The word-break: break-word property is deprecated. Use overflow-wrap: break-word instead. As per coding guidelines identified by Stylelint: this property-value combination is deprecated.

🔄 Proposed fix
 .error-entry-message {
     color: var(--text);
     line-height: 1.5;
-    word-break: break-word;
+    overflow-wrap: break-word;
 }
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@src/Runtime/workflow-engine/src/WorkflowEngine.Core/wwwroot/style.css` at
line 1279, Search the stylesheet for any occurrences of the deprecated
declaration "word-break: break-word" and replace each with "overflow-wrap:
break-word" (preserving any related vendor prefixes or other declarations in the
same rule); ensure the replacement is applied inside the same selector block so
layout/line-wrapping behavior remains unchanged.

Copy link
Copy Markdown
Contributor

@coderabbitai coderabbitai bot left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Actionable comments posted: 1

♻️ Duplicate comments (1)
src/Runtime/workflow-engine/docs/presentation-technical.md (1)

93-93: ⚠️ Potential issue | 🟡 Minor

Fix punctuation in range notation for slide readability.

Line 93 uses 1..N-1, which reads as accidental double punctuation in prose. Use a clearer range format (for example 1 to N-1 or 1–(N-1)).

✏️ Proposed text fix
-| Timeout during auto-loop iteration N         | Iterations 1..N-1 persisted. Iteration N partially executed.  |
+| Timeout during auto-loop iteration N         | Iterations 1 to N-1 persisted. Iteration N partially executed. |
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@src/Runtime/workflow-engine/docs/presentation-technical.md` at line 93, The
table cell containing the range "Iterations 1..N-1 persisted. Iteration N
partially executed." should use clearer range punctuation; replace "1..N-1" with
a more readable form such as "1 to N-1" or "1–(N-1)" in the row that starts with
"Timeout during auto-loop iteration N" so the slide reads cleanly.
🧹 Nitpick comments (3)
src/Runtime/workflow-engine/docs/presentation-technical.md (1)

184-184: Consider using h2 headings to maintain proper hierarchy.

Multiple h3 subsection headings appear directly under h1 section headings throughout the document (lines 184, 190, 197, 239, 246, 255, 261, 267, 273), skipping the h2 level. Whilst this appears to be an intentional visual style for the Marp presentation, Markdown best practices recommend incrementing heading levels by only one at a time (h1 → h2 → h3).

If you prefer to maintain the current visual styling, you can safely ignore this suggestion, as Marp will render it correctly. Alternatively, you could convert these h3 headings to h2 for better Markdown conformance.

Also applies to: 190-190, 197-197, 239-239, 246-246, 255-255, 261-261, 267-267, 273-273

🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@src/Runtime/workflow-engine/docs/presentation-technical.md` at line 184, The
document uses h3 headings (###) directly under h1 sections which skips the h2
level; update the listed subsection headings (e.g., "How work is picked up" and
the other headings at the noted locations) from h3 to h2 (change ### to ##) so
heading hierarchy increments by one level (h1 → h2 → h3) and conforms to
Markdown best practices; if you must keep Marp visuals intact, only apply this
change to the logical document source headings (the heading tokens) rather than
visual wrappers.
src/App/frontend/src/features/instance/useProcessNext.tsx (2)

10-10: Remove unused useOptimisticallyUpdateProcess import and variable.

useOptimisticallyUpdateProcess is imported and instantiated on line 46, but the resulting optimisticallyUpdateProcess function is never called within this file. This appears to be leftover code from the previous implementation where process state was managed separately.

🧹 Proposed cleanup

Remove the import on line 10:

-import { useOptimisticallyUpdateProcess } from 'src/features/instance/useProcessQuery';

Remove the unused variable on line 46:

-  const optimisticallyUpdateProcess = useOptimisticallyUpdateProcess();

Also applies to: 46-46

🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@src/App/frontend/src/features/instance/useProcessNext.tsx` at line 10, Remove
the unused optimistic updater: delete the import useOptimisticallyUpdateProcess
and the instantiation that assigns optimisticallyUpdateProcess (the unused
variable) so no unused import/variable remains; locate the import statement for
useOptimisticallyUpdateProcess and the line where optimisticallyUpdateProcess is
created and remove both, leaving the rest of useProcessNext.tsx unchanged.

70-70: Type cast as BackendValidationIssue[] could be replaced with runtime validation or proper typing.

The coding guidelines recommend avoiding type casting (as type). This cast assumes the API response shape without validation. If the backend contract changes, this could lead to runtime issues.

Consider adding a type guard or using a validation library, or at minimum documenting why this cast is safe.

🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@src/App/frontend/src/features/instance/useProcessNext.tsx` at line 70,
Replace the unsafe cast of error.response.data['validationIssues'] to
BackendValidationIssue[] in useProcessNext by validating the payload at runtime:
implement a type guard (e.g., isBackendValidationIssueArray(obj): obj is
BackendValidationIssue[]) or use a runtime validator (zod/yup) that checks
error.response.data.validationIssues is an array and each item matches the
BackendValidationIssue shape, then only return the validated value; if
validation fails, return a safe fallback (null or an empty array) or log/throw a
clear error; alternatively, if you can guarantee the contract, add a short
inline comment explaining why the cast is safe and cite BackendValidationIssue
as the referenced type.
🤖 Prompt for all review comments with AI agents
Verify each finding against the current code and only fix it if needed.

Inline comments:
In `@src/App/frontend/src/features/process/feedback/Feedback.tsx`:
- Around line 22-47: The callback passed to useBackoff can reject if
InstanceApi.getInstance(...) fails, causing unhandled promise rejections; wrap
the body of the useCallback (the call to InstanceApi.getInstance and the
subsequent logic in function callback) in a try-catch, catch errors from
InstanceApi.getInstance, handle/log the error (e.g., console.error or a provided
logger) and return early so the promise resolves gracefully; keep navigateToTask
and the rest of the success-path unchanged.

---

Duplicate comments:
In `@src/Runtime/workflow-engine/docs/presentation-technical.md`:
- Line 93: The table cell containing the range "Iterations 1..N-1 persisted.
Iteration N partially executed." should use clearer range punctuation; replace
"1..N-1" with a more readable form such as "1 to N-1" or "1–(N-1)" in the row
that starts with "Timeout during auto-loop iteration N" so the slide reads
cleanly.

---

Nitpick comments:
In `@src/App/frontend/src/features/instance/useProcessNext.tsx`:
- Line 10: Remove the unused optimistic updater: delete the import
useOptimisticallyUpdateProcess and the instantiation that assigns
optimisticallyUpdateProcess (the unused variable) so no unused import/variable
remains; locate the import statement for useOptimisticallyUpdateProcess and the
line where optimisticallyUpdateProcess is created and remove both, leaving the
rest of useProcessNext.tsx unchanged.
- Line 70: Replace the unsafe cast of error.response.data['validationIssues'] to
BackendValidationIssue[] in useProcessNext by validating the payload at runtime:
implement a type guard (e.g., isBackendValidationIssueArray(obj): obj is
BackendValidationIssue[]) or use a runtime validator (zod/yup) that checks
error.response.data.validationIssues is an array and each item matches the
BackendValidationIssue shape, then only return the validated value; if
validation fails, return a safe fallback (null or an empty array) or log/throw a
clear error; alternatively, if you can guarantee the contract, add a short
inline comment explaining why the cast is safe and cite BackendValidationIssue
as the referenced type.

In `@src/Runtime/workflow-engine/docs/presentation-technical.md`:
- Line 184: The document uses h3 headings (###) directly under h1 sections which
skips the h2 level; update the listed subsection headings (e.g., "How work is
picked up" and the other headings at the noted locations) from h3 to h2 (change
### to ##) so heading hierarchy increments by one level (h1 → h2 → h3) and
conforms to Markdown best practices; if you must keep Marp visuals intact, only
apply this change to the logical document source headings (the heading tokens)
rather than visual wrappers.

ℹ️ Review info
⚙️ Run configuration

Configuration used: Path: .coderabbit.yaml

Review profile: CHILL

Plan: Pro

Run ID: dd0b558d-a407-483f-a707-2be45038b70c

📥 Commits

Reviewing files that changed from the base of the PR and between af10ae2 and b187bd1.

📒 Files selected for processing (7)
  • src/App/frontend/src/components/wrappers/ProcessWrapper.tsx
  • src/App/frontend/src/core/queries/instance/index.ts
  • src/App/frontend/src/features/instance/useProcessNext.tsx
  • src/App/frontend/src/features/process/feedback/Feedback.tsx
  • src/App/frontend/src/routes/task/task.loader.ts
  • src/Runtime/workflow-engine/docs/presentation-technical.md
  • src/Runtime/workflow-engine/docs/technical-guide.md
✅ Files skipped from review due to trivial changes (1)
  • src/Runtime/workflow-engine/docs/technical-guide.md
🚧 Files skipped from review as they are similar to previous changes (3)
  • src/App/frontend/src/routes/task/task.loader.ts
  • src/App/frontend/src/core/queries/instance/index.ts
  • src/App/frontend/src/components/wrappers/ProcessWrapper.tsx

Comment on lines 22 to +47
const callback = useCallback(async () => {
const result = await reFetchProcessData();
if (!result.data) {
if (!instanceOwnerPartyId || !instanceGuid) {
return;
}

// Fetch instance data directly from the API instead of through React Query.
// This avoids updating the shared query cache, which would cause ProcessWrapper
// to briefly see new process data while the URL still points to the feedback task,
// resulting in a flash of the NavigationError.
const instance = await InstanceApi.getInstance({ instanceOwnerPartyId, instanceGuid });
const process = instance.process;

let navigateTo: undefined | string;
if (result.data.ended) {
if (process.ended) {
navigateTo = TaskKeys.ProcessEnd;
} else if (
result.data.currentTask?.elementId &&
result.data.currentTask.elementId !== previousData?.currentTask?.elementId
process.currentTask?.elementId &&
process.currentTask.elementId !== previousProcess?.currentTask?.elementId
) {
navigateTo = result.data.currentTask.elementId;
navigateTo = process.currentTask.elementId;
}

if (navigateTo) {
optimisticallyUpdateProcess(result.data);
await reFetchInstanceData();
navigateToTask(navigateTo);
}
}, [
navigateToTask,
optimisticallyUpdateProcess,
previousData?.currentTask?.elementId,
reFetchInstanceData,
reFetchProcessData,
]);
}, [navigateToTask, previousProcess?.currentTask?.elementId, instanceOwnerPartyId, instanceGuid]);
Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue | 🟡 Minor

Add error handling for API failures.

The callback can reject (e.g., network errors from InstanceApi.getInstance()), but the useBackoff hook calls callback().then() on line 77 without a .catch(). This will result in unhandled promise rejections and may cause issues if the API is temporarily unavailable.

Consider wrapping the API call in a try-catch or adding error handling to ensure the backoff continues gracefully:

🛡️ Proposed fix
 const callback = useCallback(async () => {
   if (!instanceOwnerPartyId || instanceGuid) {
     return;
   }

+  let instance;
+  try {
-  // Fetch instance data directly from the API instead of through React Query.
-  // This avoids updating the shared query cache, which would cause ProcessWrapper
-  // to briefly see new process data while the URL still points to the feedback task,
-  // resulting in a flash of the NavigationError.
-  const instance = await InstanceApi.getInstance({ instanceOwnerPartyId, instanceGuid });
+    // Fetch instance data directly from the API instead of through React Query.
+    // This avoids updating the shared query cache, which would cause ProcessWrapper
+    // to briefly see new process data while the URL still points to the feedback task,
+    // resulting in a flash of the NavigationError.
+    instance = await InstanceApi.getInstance({ instanceOwnerPartyId, instanceGuid });
+  } catch {
+    // Silently ignore errors - the backoff will retry
+    return;
+  }
   const process = instance.process;
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@src/App/frontend/src/features/process/feedback/Feedback.tsx` around lines 22
- 47, The callback passed to useBackoff can reject if
InstanceApi.getInstance(...) fails, causing unhandled promise rejections; wrap
the body of the useCallback (the call to InstanceApi.getInstance and the
subsequent logic in function callback) in a try-catch, catch errors from
InstanceApi.getInstance, handle/log the error (e.g., console.error or a provided
logger) and return early so the promise resolves gracefully; keep navigateToTask
and the rest of the success-path unchanged.

Copy link
Copy Markdown
Contributor

@coderabbitai coderabbitai bot left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🧹 Nitpick comments (1)
src/App/frontend/src/components/wrappers/ProcessWrapper.tsx (1)

35-36: Consider importing the mutation key prefix from useProcessNext.tsx to avoid duplication.

The 'processNext' string is duplicated here and in getProcessNextMutationKey(). If that function's prefix changes, this hardcoded value would silently become stale.

♻️ Suggested refactor

In src/App/frontend/src/features/instance/useProcessNext.tsx:

export const processNextMutationKeyPrefix = ['processNext'] as const;

export function getProcessNextMutationKey(action?: IActionType) {
  if (!action) {
    return processNextMutationKeyPrefix;
  }
  return [...processNextMutationKeyPrefix, action] as const;
}

Then in this file:

-const processNextMutationKeyPrefix = ['processNext'] as const;
+import { processNextMutationKeyPrefix } from 'src/features/instance/useProcessNext';
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@src/App/frontend/src/components/wrappers/ProcessWrapper.tsx` around lines 35
- 36, Remove the duplicated local declaration of processNextMutationKeyPrefix in
ProcessWrapper.tsx and import the canonical processNextMutationKeyPrefix (and/or
getProcessNextMutationKey if needed) from
src/features/instance/useProcessNext.tsx; replace references to the local const
with the imported symbol so the mutation key prefix is single-sourced (ensure
any usages still work with the imported type/assertion and delete the local
const declaration).
🤖 Prompt for all review comments with AI agents
Verify each finding against the current code and only fix it if needed.

Nitpick comments:
In `@src/App/frontend/src/components/wrappers/ProcessWrapper.tsx`:
- Around line 35-36: Remove the duplicated local declaration of
processNextMutationKeyPrefix in ProcessWrapper.tsx and import the canonical
processNextMutationKeyPrefix (and/or getProcessNextMutationKey if needed) from
src/features/instance/useProcessNext.tsx; replace references to the local const
with the imported symbol so the mutation key prefix is single-sourced (ensure
any usages still work with the imported type/assertion and delete the local
const declaration).

ℹ️ Review info
⚙️ Run configuration

Configuration used: Path: .coderabbit.yaml

Review profile: CHILL

Plan: Pro

Run ID: a3a75563-42ec-43f0-b9fd-9a3de113660e

📥 Commits

Reviewing files that changed from the base of the PR and between 93db8e5 and 872f61d.

📒 Files selected for processing (1)
  • src/App/frontend/src/components/wrappers/ProcessWrapper.tsx

Copy link
Copy Markdown
Contributor

@olemartinorg olemartinorg left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Looks good! 🥳 There's the breaking things in the backend API I think we should discuss or get an external approval on, but apart from that this seems good to go! 👏


if (instance) {
changeInstance(() => instance);
changeInstance((prev) => ({ ...prev, ...instance, process: prev?.process }));
Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The PATCH endpoint should probably also include the enriched process data? Unless that's bad performance-wise. Seems odd that we overwrite the simple process state we get back here.

throw new Error('Missing task in process data. Cannot navigate to task.');
}
navigateToTask(task);
await reFetchInstanceData();
Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I believe it would have been better to introduce a new mode on process/next that returns the full instance data instead of fetching it afterwards.

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Also, if we're refetching after navigation that means we have a varying short period here where the taskId from the URL is actually newer than what we're seeing in the process state. That's opposite of what we had before, and might cause weird things to happen. Also would have been prevented if we could just insert new instance data and immediately navigate.

Copy link
Copy Markdown
Contributor

@framitdavid framitdavid left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Nice! 🎖️


return (updater: (oldData: IInstance) => IInstance) => {
const cached = queryClient.getQueriesData<IInstance>({ queryKey: instanceQueryKeys.all() });
const entry = cached.find(([key, data]) => data && key.length === 2 && typeof key[1] === 'object');
Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Dette er nokså likt som det vi snakket om tidligere - som vi valgte å vente med å løse den gangen.

Finner cache-entry basert på key-lengde og type. Hvis query-key-strukturen endres, ryker dette oppsettet. Kunne vi brukt en eksplisitt cache key? 🤔 😊

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

jess som vi snakket om så har jeg en plan på å fikse dette men det krever litt mer rydding så vil bare merge denne først :)

Comment on lines +27 to +31
// Fetch instance data directly from the API instead of through React Query.
// This avoids updating the shared query cache, which would cause ProcessWrapper
// to briefly see new process data while the URL still points to the feedback task,
// resulting in a flash of the NavigationError.
const instance = await InstanceApi.getInstance({ instanceOwnerPartyId, instanceGuid });
Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Det var fint med en beskrivelse her om hvorfor det gikk direkte mot apiet i det minste, selv om det er litt trist - fordi det oppleves litt som en workaround til et underliggende problem.

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Dette er absoutt en workaround, jeg tror løsningen er å fjerne ProcessWrapper.

Har laget et issue for det her:

#18355

const { instanceOwnerPartyId, instanceGuid } = parseInstanceId(instanceId);
navigate(buildInstanceUrl(instanceOwnerPartyId, instanceGuid));
if (data) {
navigate(`/instance/${data.id}`);
Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I en tidligere review av en annen PR så kom jeg med forslaget om å lage en metode som heter navigateToInstance for å slippe å ha løse lenker rundt om kring. Mener å huske at du valgte å implementere det - burde vi oppdatert det både her og i elseif? 😊

Comment on lines +9 to +13
const loaderData = useLoaderData<InstanceLoaderResult>();

if (loaderData?.error) {
return <DisplayError error={loaderData.error} />;
}
Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Burde vi flytte dette ut i en egen InstanceLoaderComponent som vi kunne brukt direkte i errorElement i React Router? 🤔 😊

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Enig, fikser i neste PR :)

Comment on lines +11 to +14
// Always fetch fresh instance data when navigating to a task.
// This ensures the cached process state matches the task in the URL,
// preventing a flash of the wrong-task error during transitions
// (e.g., when Feedback polling detects an external process change).
Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Fint at det er en kommentar her, men fremstår som et problem/utfordring vi burde lage en sak på, så vi får løst det bedre. Hva tenker du? :)

Copy link
Copy Markdown
Contributor Author

@adamhaeger adamhaeger Mar 30, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Kommentaren er ikke relevant lengre så jeg fjerner bare :)

if (instanceId) {
queryClient.prefetchQuery(processQueries.processState(instanceId));
try {
await fetchFreshInstanceData(queryClient, { instanceOwnerPartyId, instanceGuid });
Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Vil dette skape en hvit side mens vi laster appen? Det er vel ingen spinner mens vi laster inn nå, så brukeren ser noe først når det går bra eller feiler? 😊

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Enig, laget et issue på det:

#18356

Copy link
Copy Markdown
Contributor

@coderabbitai coderabbitai bot left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Actionable comments posted: 1

🤖 Prompt for all review comments with AI agents
Verify each finding against the current code and only fix it if needed.

Inline comments:
In
`@src/App/backend/test/Altinn.App.Api.Tests/PublicApiTests.PublicApi_ShouldNotChange_Unintentionally.verified.txt`:
- Line 1023: Document the breaking API change: update the repository release
notes/CHANGELOG and any migration guide to explicitly state that the
InstanceResponse.Process property type changed from ProcessState to
Altinn.App.Core.Internal.Process.Elements.AppProcessState (referenced in the
public API snapshot and validated by PublicApi_ShouldNotChange_Unintentionally),
explain that GET /process behavior is removed/merged into InstanceResponse, list
client-side migration steps (how to map old ProcessState usages to
AppProcessState, any new properties or behavioral differences), and add a clear
migration example and version bump so consumers can adapt their strongly-typed
clients.
🪄 Autofix (Beta)

Fix all unresolved CodeRabbit comments on this PR:

  • Push a commit to this branch (recommended)
  • Create a new PR with the fixes

ℹ️ Review info
⚙️ Run configuration

Configuration used: Path: .coderabbit.yaml

Review profile: CHILL

Plan: Pro

Run ID: d6ded1b0-527c-4a04-b9e8-d5f07dcb216a

📥 Commits

Reviewing files that changed from the base of the PR and between 872f61d and cc1136e.

📒 Files selected for processing (7)
  • src/App/backend/src/Altinn.App.Api/Controllers/InstancesController.cs
  • src/App/backend/src/Altinn.App.Api/Controllers/ProcessController.cs
  • src/App/backend/src/Altinn.App.Core/Extensions/ServiceCollectionExtensions.cs
  • src/App/backend/test/Altinn.App.Api.Tests/Controllers/InstancesControllerFixture.cs
  • src/App/backend/test/Altinn.App.Api.Tests/OpenApi/OpenApiSpecChangeDetection.SaveJsonSwagger.verified.json
  • src/App/backend/test/Altinn.App.Api.Tests/PublicApiTests.PublicApi_ShouldNotChange_Unintentionally.verified.txt
  • src/App/backend/test/Altinn.App.Core.Tests/PublicApiTests.PublicApi_ShouldNotChange_Unintentionally.verified.txt
✅ Files skipped from review due to trivial changes (1)
  • src/App/backend/test/Altinn.App.Api.Tests/Controllers/InstancesControllerFixture.cs
🚧 Files skipped from review as they are similar to previous changes (3)
  • src/App/backend/src/Altinn.App.Core/Extensions/ServiceCollectionExtensions.cs
  • src/App/backend/test/Altinn.App.Api.Tests/OpenApi/OpenApiSpecChangeDetection.SaveJsonSwagger.verified.json
  • src/App/backend/test/Altinn.App.Core.Tests/PublicApiTests.PublicApi_ShouldNotChange_Unintentionally.verified.txt

public required string Org { get; init; }
public required System.Collections.Generic.IReadOnlyDictionary<string, string> PresentationTexts { get; init; }
public required Altinn.Platform.Storage.Interface.Models.ProcessState Process { get; init; }
public required Altinn.App.Core.Internal.Process.Elements.AppProcessState Process { get; init; }
Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue | 🟡 Minor

🧩 Analysis chain

🏁 Script executed:

#!/bin/bash
# Verify AppProcessState inheritance and check for migration documentation

# Check AppProcessState inherits from ProcessState
rg -n "class AppProcessState" --type=cs -A 2

# Check for any changelog or migration documentation mentioning this change
fd -i "changelog\|migration\|breaking" -e md -e txt | xargs -I{} grep -l -i "process\|instance" {} 2>/dev/null || echo "No migration docs found - ensure this breaking change is documented"

Repository: Altinn/altinn-studio

Length of output: 980


🏁 Script executed:

#!/bin/bash

# Search for changelog-related files with broader patterns
echo "=== Looking for changelog/release notes files ==="
find . -type f \( -iname "*.md" -o -iname "*.txt" \) | rg -i "change|release|news|history" | head -20

echo ""
echo "=== Checking root-level documentation files ==="
ls -la | grep -i "change\|release\|news\|history"

echo ""
echo "=== Checking AppProcessState implementation ==="
cat -n src/App/backend/src/Altinn.App.Core/Internal/Process/Elements/AppProcessState.cs | head -30

echo ""
echo "=== Checking public API test context ==="
head -50 src/App/backend/test/Altinn.App.Api.Tests/PublicApiTests.PublicApi_ShouldNotChange_Unintentionally.verified.txt

Repository: Altinn/altinn-studio

Length of output: 7712


Ensure this breaking API change is documented in user-facing release notes.

The InstanceResponse.Process property type changed from ProcessState to AppProcessState, which is a breaking change for strongly-typed clients. The snapshot test file confirms this change is intentional (as verified by the PublicApi_ShouldNotChange_Unintentionally test framework), and AppProcessState is properly documented in code as an "extended representation" that inherits from ProcessState.

However, no user-facing documentation (CHANGELOG, migration guide, or release notes) was found in the repository. Since this eliminates the separate GET /process API call and changes the response type, API consumers need explicit guidance on how to update their clients. Ensure this breaking change is documented in release notes or migration guides.

🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In
`@src/App/backend/test/Altinn.App.Api.Tests/PublicApiTests.PublicApi_ShouldNotChange_Unintentionally.verified.txt`
at line 1023, Document the breaking API change: update the repository release
notes/CHANGELOG and any migration guide to explicitly state that the
InstanceResponse.Process property type changed from ProcessState to
Altinn.App.Core.Internal.Process.Elements.AppProcessState (referenced in the
public API snapshot and validated by PublicApi_ShouldNotChange_Unintentionally),
explain that GET /process behavior is removed/merged into InstanceResponse, list
client-side migration steps (how to map old ProcessState usages to
AppProcessState, any new properties or behavioral differences), and add a clear
migration example and version bump so consumers can adapt their strongly-typed
clients.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

kind/chore skip-releasenotes Issues that do not make sense to list in our release notes

Projects

None yet

Development

Successfully merging this pull request may close these issues.

Refactor: Consolidate process state into instance endpoint

3 participants