Skip to content

Support service task being first element after StartEvent.#1667

Open
bjorntore wants to merge 10 commits intomainfrom
feat/auto-run-service-task-on-instantiate
Open

Support service task being first element after StartEvent.#1667
bjorntore wants to merge 10 commits intomainfrom
feat/auto-run-service-task-on-instantiate

Conversation

@bjorntore
Copy link
Contributor

@bjorntore bjorntore commented Feb 19, 2026

If a service task is the first element after StartEvent, run process next automatically to execute it and move to the next element. Forward prefill to first user task.

Description

Related Issue(s)

  • #{issue number}

Verification

  • 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)
  • All tests run green

Documentation

  • User documentation is updated with a separate linked PR in altinn-studio-docs. (if applicable)

Summary by CodeRabbit

  • New Features

    • Prefill now propagates during instantiation; initial service tasks can auto-run so instances progress to user tasks automatically.
  • Bug Fixes

    • Process start failures now return Conflict with error details to avoid silent mismatches.
  • Improvements

    • Unified start workflow replaces prior side-effect paths; process-start results replace local instance state and tracing includes a distinct execution step.
  • Tests

    • Added tests, fixtures, app data and configs for service-task-first, auto-run service tasks, and prefill behavior.
  • Documentation

    • Added guideline discouraging unjustified use of the null-forgiveness operator.

@bjorntore bjorntore requested a review from HauklandJ February 19, 2026 14:54
@bjorntore
Copy link
Contributor Author

/publish

@bjorntore bjorntore added kind/feature-request New feature or request feature Label Pull requests with new features. Used when generation releasenotes backport-ignore This PR is a new feature and should not be cherry-picked onto release branches labels Feb 19, 2026
@bjorntore bjorntore self-assigned this Feb 19, 2026
@github-actions
Copy link

github-actions bot commented Feb 19, 2026

@bjorntore bjorntore moved this to 🔎 Review in Team Studio Feb 19, 2026
@bjorntore bjorntore added the squad/flyt Issues that belongs to the named squad. label Feb 19, 2026
@coderabbitai
Copy link
Contributor

coderabbitai bot commented Feb 19, 2026

📝 Walkthrough

Walkthrough

Replaces ad-hoc event handling with a new IProcessEngine.Start(...) workflow that accepts user, prefill, language and CancellationToken, returns a ProcessChangeResult with MutatedInstance, auto-runs initial service tasks, and updates controllers, engine, models, and tests to use Start for process initiation and storage updates.

Changes

Cohort / File(s) Summary
Process engine interface & implementation
src/Altinn.App.Core/Internal/Process/Interfaces/IProcessEngine.cs, src/Altinn.App.Core/Internal/Process/ProcessEngine.cs
Adds Start(Instance, ProcessStateChange, ClaimsPrincipal, Dictionary<string,string>? prefill = null, string? language = null, CancellationToken ct = default) to the interface and implements it in ProcessEngine. Propagates prefill through Next/MoveToNext, auto-runs initial service tasks, and updates HandleMoveToNext signature.
Controllers
src/Altinn.App.Api/Controllers/InstancesController.cs, src/Altinn.App.Api/Controllers/ProcessController.cs
Replaces prior HandleEventsAndUpdateStorage flows with _processEngine.Start(...) calls in Post, PostSimplified, CopyInstance, and process start paths; on Start failure returns Conflict/error, on success uses returned MutatedInstance.
Process models
src/Altinn.App.Core/Models/Process/ProcessNextRequest.cs, src/Altinn.App.Core/Models/Process/ProcessChangeResult.cs
Adds Prefill property to ProcessNextRequest. Adds [MemberNotNullWhen(true, nameof(MutatedInstance))] to ProcessChangeResult.Success.
Tests — controller fixtures & copy tests
test/Altinn.App.Api.Tests/Controllers/InstancesControllerFixture.cs, test/Altinn.App.Api.Tests/Controllers/InstancesController_CopyInstanceTests.cs
Mocks updated to expect Start(...) and return ProcessChangeResult; HttpContext.RequestAborted mocked to CancellationToken.None; tests adjusted to new Start signature and new failure cases added.
Tests — new service-task-first app & tests
test/Altinn.App.Api.Tests/Process/ServiceTasks/ServiceTaskFirstTests.cs, test/Altinn.App.Api.Tests/Data/apps/ttd/service-task-first/...
Adds a test app (BPMN, metadata, policy, appsettings) and ServiceTaskFirstTests that validate auto-execution of a service task and prefill propagation; injects a test IServiceTask.
Public API & snapshots
test/Altinn.App.Core.Tests/PublicApiTests.PublicApi_ShouldNotChange_Unintentionally.verified.txt, multiple *.verified.txt snapshot files
Public API snapshot updated for new Start method and Prefill additions; verification snapshots updated to include a new Process.StartExecution activity entry alongside Process.Start.
Telemetry
src/Altinn.App.Core/Features/Telemetry/Telemetry.Processes.cs
Adds StartProcessStartExecutionActivity helper and emits StartExecution activity in relevant flows.
Misc — docs & config
AGENTS.md, *.csproj, test app config files
Adds a guideline line to AGENTS.md; new test app config/appsettings and manifest/test project changes included.

Estimated code review effort

🎯 4 (Complex) | ⏱️ ~45 minutes

🚥 Pre-merge checks | ✅ 2 | ❌ 1

❌ Failed checks (1 warning)

Check name Status Explanation Resolution
Docstring Coverage ⚠️ Warning Docstring coverage is 34.48% which is insufficient. The required threshold is 80.00%. Write docstrings for the functions missing them to satisfy the coverage threshold.
✅ Passed checks (2 passed)
Check name Status Explanation
Title check ✅ Passed The title accurately describes the main change: supporting automatic execution of service tasks when they are the first element after a StartEvent, with forwarding of prefill data.
Description Check ✅ Passed Check skipped - CodeRabbit’s high-level summary is enabled.

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

✨ Finishing Touches
  • 📝 Generate docstrings (stacked PR)
  • 📝 Generate docstrings (commit on current branch)
🧪 Generate unit tests (beta)
  • Create PR with unit tests
  • Post copyable unit tests in a comment
  • Commit unit tests in branch feat/auto-run-service-task-on-instantiate

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.

Copy link
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: 5

🧹 Nitpick comments (2)
src/Altinn.App.Core/Internal/Process/ProcessEngine.cs (1)

153-169: Missing telemetry instrumentation for the new Start method.

Other entry points like GenerateProcessStartEvents and Next use _telemetry?.Start*Activity(...). The Start method is a new public-facing operation on the process engine and should have its own activity span for observability. As per coding guidelines, comprehensive telemetry instrumentation should be included in feature implementations using OpenTelemetry.

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

In `@src/Altinn.App.Core/Internal/Process/ProcessEngine.cs` around lines 153 -
169, The Start method lacks OpenTelemetry instrumentation; add a telemetry
activity at the top of Start (similar to GenerateProcessStartEvents/Next) by
calling the telemetry helper (e.g. _telemetry?.StartProcessStartActivity(...) or
the existing _telemetry?.Start*Activity API) and wrap the method body in the
activity's scope (dispose/stop the activity after the awaited operations).
Ensure the activity encompasses the call to HandleEventsAndUpdateStorage and the
subsequent IsServiceTask check and return, and pass relevant context (instance,
user, language/prefill) to the telemetry call so traces correlate with the Start
operation.
test/Altinn.App.Api.Tests/Controllers/InstancesController_CopyInstanceTests.cs (1)

385-405: Consider extracting the repeated Start mock setup into a helper method.

The identical IProcessEngine.Start mock setup (.Setup + .ReturnsAsync lambda) is duplicated verbatim across five test methods (lines 385–405, 517–538, 729–750, 940–961, 1158–1179). A small helper would reduce boilerplate and make future signature changes a single-point fix.

Also, System.Security.Claims.ClaimsPrincipal is fully qualified in every occurrence. Adding using System.Security.Claims; at the top would clean this up.

Example helper
// At the top of the file
using System.Security.Claims;

// Helper method in the test class
private static void SetupProcessEngineStart(InstancesControllerFixture fixture)
{
    fixture
        .Mock<IProcessEngine>()
        .Setup(p =>
            p.Start(
                It.IsAny<Instance>(),
                It.IsAny<List<InstanceEvent>>(),
                It.IsAny<ClaimsPrincipal>(),
                It.IsAny<Dictionary<string, string>>(),
                It.IsAny<string>(),
                It.IsAny<CancellationToken>()
            )
        )
        .ReturnsAsync(
            (
                Instance inst,
                List<InstanceEvent>? _,
                ClaimsPrincipal _,
                Dictionary<string, string>? _,
                string? _,
                CancellationToken _
            ) => new ProcessChangeResult { Success = true, MutatedInstance = inst }
        );
}
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In
`@test/Altinn.App.Api.Tests/Controllers/InstancesController_CopyInstanceTests.cs`
around lines 385 - 405, Extract the duplicated IProcessEngine.Start mock in
InstancesController_CopyInstanceTests into a single helper method (e.g.,
SetupProcessEngineStart) that accepts the test fixture and performs the
.Mock<IProcessEngine>().Setup(...Start(...)) and .ReturnsAsync(...) logic;
replace the five verbatim setups with calls to this helper. Also add using
System.Security.Claims; to the top of the test file and use ClaimsPrincipal in
the Setup/ReturnsAsync signatures instead of the fully-qualified
System.Security.Claims.ClaimsPrincipal to simplify the mock lambda parameters
and make future signature changes a single-point edit.
🤖 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/Altinn.App.Api/Controllers/InstancesController.cs`:
- Around line 385-395: The use of the null-forgiving operator on
ProcessChangeResult.MutatedInstance is unsafe; update the Sites in
InstancesController (where you call _processEngine.Start and then use
startProcessResult.MutatedInstance) to explicitly guard against null instead of
using "!" — check startProcessResult.Success and also verify
startProcessResult.MutatedInstance is not null before assigning/using it,
returning an appropriate error (e.g., Conflict or InternalServerError) if it is
null; alternatively, if you prefer a type-level fix, add
[MemberNotNullWhen(true, nameof(MutatedInstance))] to the
ProcessChangeResult.Success property (and ensure all code paths set
MutatedInstance when Success is true, including the non-service-task Start path)
so the compiler can trust the invariant.

In `@src/Altinn.App.Api/Controllers/ProcessController.cs`:
- Around line 150-163: startProcessResult.MutatedInstance is being dereferenced
with a null-forgiving operator; instead mirror the fix from InstancesController
by validating MutatedInstance after a successful Start call and handling the
unexpected null safely. After calling _processEngine.Start(...) and confirming
startProcessResult.Success, check if startProcessResult.MutatedInstance is null
and return an appropriate error response (e.g., Conflict or BadRequest) rather
than using !; then assign instance = startProcessResult.MutatedInstance and
continue to call ConvertAndAuthorizeActions(instance, instance.Process).
- Around line 156-159: The code path in ProcessController.Start currently
returns Conflict(startProcessResult.ErrorMessage) for all failures, which forces
a 409 regardless of the actual ErrorType; change the failure return to use
GetResultForError(startProcessResult) so the controller maps Start/Next errors
(Internal, Unauthorized, etc.) to the correct HTTP status codes. Locate the
failing branch that checks if (!startProcessResult.Success) and replace the
Conflict(...) return with a call to GetResultForError(startProcessResult),
keeping the same startProcessResult object to preserve the error message and
type mapping.

In `@src/Altinn.App.Core/Internal/Process/ProcessEngine.cs`:
- Around line 153-183: The Start method returns new
ProcessChangeResult(instance) with Success = true but never sets
ProcessStateChange, violating the [MemberNotNullWhen(true,
nameof(ProcessStateChange))] contract; modify Start to capture the result/state
produced by HandleEventsAndUpdateStorage (or the state mutation it produces) and
assign it to the ProcessStateChange property on the returned ProcessChangeResult
when Success is true (e.g., store the change/result from
HandleEventsAndUpdateStorage and use it to populate ProcessStateChange before
returning), keeping existing behavior for IsServiceTask and the Next call
intact.

In
`@test/Altinn.App.Api.Tests/Controllers/InstancesController_CopyInstanceTests.cs`:
- Around line 387-394: The mock setup for IProcessEngine.Start uses non-nullable
matchers but the real method signature has nullable parameters
(List<InstanceEvent>?, Dictionary<string,string>?, string?); update the mock
call in the test to use nullable-compatible matchers—replace
It.IsAny<List<InstanceEvent>>(), It.IsAny<Dictionary<string,string>>(),
It.IsAny<string>() with It.IsAny<List<InstanceEvent>?>(),
It.IsAny<Dictionary<string,string>?>(), It.IsAny<string?>() so the mocked
invocation matches the actual Start signature and VerifyAll() can correctly
validate the call.

---

Duplicate comments:
In `@src/Altinn.App.Api/Controllers/InstancesController.cs`:
- Around line 610-620: The code uses the null-forgiving operator on
startProcessResult.MutatedInstance (after calling _processEngine.Start) which
can propagate a null-forgiving pipeline failure like in the Post method; update
the handling of startProcessResult to check that
startProcessResult.MutatedInstance is not null before assigning to instance (or
return an error/Conflict if it is null) instead of using the ! operator,
mirroring the fix applied to the Post method so _processEngine.Start,
startProcessResult, and instance are safely handled.
- Around line 742-751: The code currently assigns
startProcessResult.MutatedInstance! to targetInstance using a null-forgiving
operator which can cause a pipeline null-forgiving failure; instead, check that
startProcessResult.MutatedInstance is not null before assignment (e.g., if
(!startProcessResult.MutatedInstance.HasValue or == null) return Conflict or
appropriate error), and only set targetInstance =
startProcessResult.MutatedInstance when it is non-null—apply the same
null-check/early-return pattern used in the Post method around
_processEngine.Start/startProcessResult to avoid the null-forgiving operator.

---

Nitpick comments:
In `@src/Altinn.App.Core/Internal/Process/ProcessEngine.cs`:
- Around line 153-169: The Start method lacks OpenTelemetry instrumentation; add
a telemetry activity at the top of Start (similar to
GenerateProcessStartEvents/Next) by calling the telemetry helper (e.g.
_telemetry?.StartProcessStartActivity(...) or the existing
_telemetry?.Start*Activity API) and wrap the method body in the activity's scope
(dispose/stop the activity after the awaited operations). Ensure the activity
encompasses the call to HandleEventsAndUpdateStorage and the subsequent
IsServiceTask check and return, and pass relevant context (instance, user,
language/prefill) to the telemetry call so traces correlate with the Start
operation.

In
`@test/Altinn.App.Api.Tests/Controllers/InstancesController_CopyInstanceTests.cs`:
- Around line 385-405: Extract the duplicated IProcessEngine.Start mock in
InstancesController_CopyInstanceTests into a single helper method (e.g.,
SetupProcessEngineStart) that accepts the test fixture and performs the
.Mock<IProcessEngine>().Setup(...Start(...)) and .ReturnsAsync(...) logic;
replace the five verbatim setups with calls to this helper. Also add using
System.Security.Claims; to the top of the test file and use ClaimsPrincipal in
the Setup/ReturnsAsync signatures instead of the fully-qualified
System.Security.Claims.ClaimsPrincipal to simplify the mock lambda parameters
and make future signature changes a single-point edit.

Copy link
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 `@test/Altinn.App.Api.Tests/Process/ServiceTasks/ServiceTaskFirstTests.cs`:
- Line 8: Replace FluentAssertions usage in the ServiceTaskFirstTests class with
xUnit asserts: remove the using FluentAssertions; and change expressions like
.Should().BeTrue() -> Assert.True(...), .Should().BeFalse() ->
Assert.False(...), .Should().Be(expected) -> Assert.Equal(expected, actual),
.Should().NotBeNull() -> Assert.NotNull(...), .Should().HaveCount(n) ->
Assert.Equal(n, collection.Count), and similar conversions inside each test
method; update any chained calls to capture the actual value passed as the first
xUnit parameter and ensure test methods in ServiceTaskFirstTests compile with
xUnit Assert methods.

Copy link
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

Caution

Some comments are outside the diff and can’t be posted inline due to platform limitations.

⚠️ Outside diff range comments (1)
src/Altinn.App.Core/Models/Process/ProcessChangeResult.cs (1)

10-10: 🛠️ Refactor suggestion | 🟠 Major

Seal ProcessChangeResult.

The class is a concrete result model with no intended inheritance. Per coding guidelines: "Use sealed for classes unless inheritance is considered a valid use-case."

♻️ Proposed fix
-public class ProcessChangeResult
+public sealed class ProcessChangeResult

As per coding guidelines: "Use sealed for classes unless inheritance is considered a valid use-case."

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

In `@src/Altinn.App.Core/Models/Process/ProcessChangeResult.cs` at line 10, The
ProcessChangeResult class is a concrete result model that should be sealed;
update its declaration to add the sealed modifier on the class definition
(ProcessChangeResult) so it cannot be inherited, leaving the rest of the class
unchanged.
🧹 Nitpick comments (1)
src/Altinn.App.Core/Models/Process/ProcessChangeResult.cs (1)

58-61: Constructor does not enforce Success = true — consider setting it in-body.

The constructor initialises MutatedInstance but leaves Success = false (the bool default). Callers must remember to append { Success = true } via object initialiser, which is an easy-to-miss footgun. Because init properties can be assigned from within a constructor of the same type, setting Success = true directly in the body encodes the invariant explicitly:

♻️ Proposed fix
 internal ProcessChangeResult(Instance mutatedInstance)
 {
+    Success = true;
     MutatedInstance = mutatedInstance;
 }
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@src/Altinn.App.Core/Models/Process/ProcessChangeResult.cs` around lines 58 -
61, The constructor ProcessChangeResult(Instance mutatedInstance) currently only
sets MutatedInstance and leaves the Success property at its default false;
modify the constructor body to also assign Success = true so the invariant is
enforced at construction time (i.e., set Success within the
ProcessChangeResult(Instance) constructor alongside MutatedInstance) to avoid
requiring callers to use an object initializer to mark success.
🤖 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/Altinn.App.Core/Models/Process/ProcessChangeResult.cs`:
- Around line 15-18: The MemberNotNullWhen contract is currently invalid because
callers create ProcessChangeResult instances with Success=true while leaving
ProcessStateChange or MutatedInstance null; fix by making ProcessChangeResult
sealed, change the internal constructor ProcessChangeResult(Instance
mutatedInstance) to assign MutatedInstance and set Success = true (so callers
can simply return new ProcessChangeResult(instance)), and update any
factory/return sites that use "new ProcessChangeResult { Success = true }" to
instead call the appropriate constructor or provide both required non-null
properties; alternatively remove the [MemberNotNullWhen(true,
nameof(MutatedInstance))] attribute if you prefer to keep current
patterns—ensure the final contract guarantees that when Success == true both
ProcessStateChange and MutatedInstance are non-null.

---

Outside diff comments:
In `@src/Altinn.App.Core/Models/Process/ProcessChangeResult.cs`:
- Line 10: The ProcessChangeResult class is a concrete result model that should
be sealed; update its declaration to add the sealed modifier on the class
definition (ProcessChangeResult) so it cannot be inherited, leaving the rest of
the class unchanged.

---

Nitpick comments:
In `@src/Altinn.App.Core/Models/Process/ProcessChangeResult.cs`:
- Around line 58-61: The constructor ProcessChangeResult(Instance
mutatedInstance) currently only sets MutatedInstance and leaves the Success
property at its default false; modify the constructor body to also assign
Success = true so the invariant is enforced at construction time (i.e., set
Success within the ProcessChangeResult(Instance) constructor alongside
MutatedInstance) to avoid requiring callers to use an object initializer to mark
success.

Copy link
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/Altinn.App.Api/Controllers/ProcessController.cs (1)

150-163: Null-forgiving operator removed — MutatedInstance is now correctly narrowed by [MemberNotNullWhen].

The ProcessStateChange is properly threaded through, and MutatedInstance is safely accessed after the Success check without !. Good fix.

However, lines 156-159 still return Conflict(startProcessResult.ErrorMessage) for all failure cases. Since Start can delegate to Next (for initial service tasks), failures may include Internal or Unauthorized error types. Use GetResultForError(startProcessResult) for correct HTTP status mapping, consistent with the NextElement endpoint (line 292).

Proposed fix
             if (!startProcessResult.Success)
             {
-                return Conflict(startProcessResult.ErrorMessage);
+                return GetResultForError(startProcessResult);
             }
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@src/Altinn.App.Api/Controllers/ProcessController.cs` around lines 150 - 163,
Replace the unconditional Conflict return for start failures with the
standardized error mapper: after calling _processEngine.Start and checking if
(!startProcessResult.Success) return GetResultForError(startProcessResult)
instead of Conflict(startProcessResult.ErrorMessage); reference the
startProcessResult variable and the Start call and mirror the pattern used in
NextElement (GetResultForError) so Internal/Unauthorized are mapped correctly.
🧹 Nitpick comments (4)
src/Altinn.App.Core/Internal/Process/Interfaces/IProcessEngine.cs (1)

40-51: Add <param> documentation tags to the new Start method.

The existing HandleEventsAndUpdateStorage has <param> tags for each parameter, but the new Start method only has a summary. The parameters (instance, processStateChange, user, prefill, language, ct) are non-obvious and deserve inline documentation, especially processStateChange whose doc comment currently only loosely references GenerateProcessStartEvents without clarifying that it expects the non-null ProcessStateChange member from a successful ProcessChangeResult.

📝 Proposed doc additions
     /// <summary>
     /// Dispatches process start events to storage and auto-runs any initial service tasks.
     /// Call after instance creation and data storage, with the result from <see cref="GenerateProcessStartEvents"/>.
     /// </summary>
+    /// <param name="instance">The newly created instance.</param>
+    /// <param name="processStateChange">
+    /// The non-null <see cref="ProcessStateChange"/> from a successful <see cref="GenerateProcessStartEvents"/> result.
+    /// </param>
+    /// <param name="user">The authenticated principal initiating the process.</param>
+    /// <param name="prefill">Optional prefill data forwarded to the first user task.</param>
+    /// <param name="language">Optional language hint for the process execution.</param>
+    /// <param name="ct">Cancellation token.</param>
     Task<ProcessChangeResult> Start(
         Instance instance,
         ProcessStateChange processStateChange,
         ClaimsPrincipal user,
         Dictionary<string, string>? prefill = null,
         string? language = null,
         CancellationToken ct = default
     );
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@src/Altinn.App.Core/Internal/Process/Interfaces/IProcessEngine.cs` around
lines 40 - 51, Add XML <param> documentation for the Start method describing
each parameter: describe Instance instance (the instance to start),
ProcessStateChange processStateChange (must be the non-null ProcessStateChange
returned from a successful GenerateProcessStartEvents / ProcessChangeResult),
ClaimsPrincipal user (the caller/actor), Dictionary<string,string>? prefill
(optional prefill key/value data), string? language (optional language code),
and CancellationToken ct (cancellation token); keep the existing summary and
mirror the style used on HandleEventsAndUpdateStorage so consumers understand
the required non-null processStateChange contract and purpose of each parameter.
src/Altinn.App.Core/Internal/Process/ProcessEngine.cs (3)

225-233: Prefill is forwarded on every loop iteration — verify this is intentional.

Unlike Action and ActionOnBehalfOf (which are cleared after the first iteration at lines 229-230), Prefill is passed through on every iteration of the do-while loop. This is currently safe because the loop only continues while IsServiceTask is true, and HandleMoveToNext (line 799) nullifies prefill for service-task destinations. However, the asymmetry with Action/ActionOnBehalfOf could confuse future maintainers.

Consider adding a brief comment explaining why Prefill isn't cleared, or clearing it after it's been applied (i.e., after the first non-service-task iteration, which is also the last iteration).

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

In `@src/Altinn.App.Core/Internal/Process/ProcessEngine.cs` around lines 225 -
233, The code constructs a ProcessNextRequest with Prefill left set for every
loop iteration while Action and ActionOnBehalfOf are cleared after the first
iteration; either add a short explanatory comment near the ProcessNextRequest
construction referencing why Prefill must persist across service-task iterations
(mention IsServiceTask and HandleMoveToNext) or change behavior to mirror
Action/ActionOnBehalfOf by clearing Prefill after its first use (i.e., set
Prefill = null after the first non-service-task iteration or when firstIteration
becomes false) so future maintainers understand the intent and symmetry with
Action/ActionOnBehalfOf.

784-803: Prefill-gating logic in HandleMoveToNext is correct but subtly depends on mutation ordering.

Line 799 calls IsServiceTask(instance) after MoveProcessStateToNextAndGenerateEvents (line 790) has already mutated instance.Process in-place to point to the next element. So this correctly checks whether the destination task is a service task. This is the right behavior (skip prefill for service tasks, apply it for user tasks).

A brief inline comment clarifying that instance.Process already reflects the next element at this point would help readability, since HandleMoveToNext doesn't make this mutation ordering obvious.

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

In `@src/Altinn.App.Core/Internal/Process/ProcessEngine.cs` around lines 784 -
803, Add a short inline comment after the call to
MoveProcessStateToNextAndGenerateEvents in HandleMoveToNext to state that
MoveProcessStateToNextAndGenerateEvents mutates instance.Process to point to the
destination element, so the subsequent IsServiceTask(instance) check is
intentionally inspecting the next task (destination) to decide whether to apply
prefill; reference these symbols: HandleMoveToNext,
MoveProcessStateToNextAndGenerateEvents, instance.Process, and IsServiceTask.

153-183: Start method correctly threads ProcessStateChange and handles service-task auto-continuation.

The previous review's concern about ProcessChangeResult contract violation (Success = true without ProcessStateChange) is resolved — line 169 now properly sets ProcessStateChange = processStateChange.

One observation: this new public method lacks its own telemetry Activity span, unlike other public entry points (GenerateProcessStartEvents at line 86, Next at line 190). The downstream calls are instrumented, but having a top-level span for Start would improve trace visibility. As per coding guidelines, "Comprehensive telemetry instrumentation should be included in feature implementations using OpenTelemetry".

🔭 Suggested telemetry addition
 public async Task<ProcessChangeResult> Start(
     Instance instance,
     ProcessStateChange processStateChange,
     ClaimsPrincipal user,
     Dictionary<string, string>? prefill = null,
     string? language = null,
     CancellationToken ct = default
 )
 {
+    using var activity = _telemetry?.StartProcessStartActivity(instance);
+
     // HandleEvents mutates instance in-place; we discard the DispatchToStorage
     // return (same as callers did before this method existed).
     bool isServiceTask = IsServiceTask(instance);
     await HandleEventsAndUpdateStorage(instance, isServiceTask ? null : prefill, processStateChange.Events);

     if (!isServiceTask)
-        return new ProcessChangeResult(instance) { Success = true, ProcessStateChange = processStateChange };
+    {
+        var result = new ProcessChangeResult(instance) { Success = true, ProcessStateChange = processStateChange };
+        activity?.SetProcessChangeResult(result);
+        return result;
+    }

     // Auto-run through initial service tasks, forwarding prefill to the first user task
     return await Next(
         new ProcessNextRequest
         {
             Instance = instance,
             User = user,
             Action = null,
             Language = language,
             Prefill = prefill,
         },
         ct
     );
 }
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@src/Altinn.App.Core/Internal/Process/ProcessEngine.cs` around lines 153 -
183, Add a top-level OpenTelemetry Activity/span around the Start method to
match other public entry points (like GenerateProcessStartEvents and Next):
create and start an Activity (e.g., "ProcessEngine.Start") at the start of
Start, ensure it is disposed/stopped after the method completes (wrapping the
existing logic including HandleEventsAndUpdateStorage and the conditional Next
call), and attach relevant attributes (instance id, isServiceTask flag,
language) and exceptions to the span so the Start call is visible in traces and
errors are recorded.
🤖 Prompt for all review comments with AI agents
Verify each finding against the current code and only fix it if needed.

Inline comments:
In
`@test/Altinn.App.Api.Tests/Controllers/InstancesController_CopyInstanceTests.cs`:
- Around line 396-405: The mock-returned ProcessChangeResult from the mocked
Start call in InstancesController_CopyInstanceTests.cs sets Success = true and
MutatedInstance = inst but omits ProcessStateChange, which violates the
[MemberNotNullWhen(true, nameof(ProcessStateChange))] contract; update each
mocked .ReturnsAsync for Start to also set a non-null ProcessStateChange (e.g.,
a minimal ProcessStateChange instance/placeholder) whenever Success is true so
the contract holds for ProcessChangeResult, keeping MutatedInstance as-is and
applying this fix to all five test methods that mock Start.

---

Duplicate comments:
In `@src/Altinn.App.Api/Controllers/ProcessController.cs`:
- Around line 150-163: Replace the unconditional Conflict return for start
failures with the standardized error mapper: after calling _processEngine.Start
and checking if (!startProcessResult.Success) return
GetResultForError(startProcessResult) instead of
Conflict(startProcessResult.ErrorMessage); reference the startProcessResult
variable and the Start call and mirror the pattern used in NextElement
(GetResultForError) so Internal/Unauthorized are mapped correctly.

---

Nitpick comments:
In `@src/Altinn.App.Core/Internal/Process/Interfaces/IProcessEngine.cs`:
- Around line 40-51: Add XML <param> documentation for the Start method
describing each parameter: describe Instance instance (the instance to start),
ProcessStateChange processStateChange (must be the non-null ProcessStateChange
returned from a successful GenerateProcessStartEvents / ProcessChangeResult),
ClaimsPrincipal user (the caller/actor), Dictionary<string,string>? prefill
(optional prefill key/value data), string? language (optional language code),
and CancellationToken ct (cancellation token); keep the existing summary and
mirror the style used on HandleEventsAndUpdateStorage so consumers understand
the required non-null processStateChange contract and purpose of each parameter.

In `@src/Altinn.App.Core/Internal/Process/ProcessEngine.cs`:
- Around line 225-233: The code constructs a ProcessNextRequest with Prefill
left set for every loop iteration while Action and ActionOnBehalfOf are cleared
after the first iteration; either add a short explanatory comment near the
ProcessNextRequest construction referencing why Prefill must persist across
service-task iterations (mention IsServiceTask and HandleMoveToNext) or change
behavior to mirror Action/ActionOnBehalfOf by clearing Prefill after its first
use (i.e., set Prefill = null after the first non-service-task iteration or when
firstIteration becomes false) so future maintainers understand the intent and
symmetry with Action/ActionOnBehalfOf.
- Around line 784-803: Add a short inline comment after the call to
MoveProcessStateToNextAndGenerateEvents in HandleMoveToNext to state that
MoveProcessStateToNextAndGenerateEvents mutates instance.Process to point to the
destination element, so the subsequent IsServiceTask(instance) check is
intentionally inspecting the next task (destination) to decide whether to apply
prefill; reference these symbols: HandleMoveToNext,
MoveProcessStateToNextAndGenerateEvents, instance.Process, and IsServiceTask.
- Around line 153-183: Add a top-level OpenTelemetry Activity/span around the
Start method to match other public entry points (like GenerateProcessStartEvents
and Next): create and start an Activity (e.g., "ProcessEngine.Start") at the
start of Start, ensure it is disposed/stopped after the method completes
(wrapping the existing logic including HandleEventsAndUpdateStorage and the
conditional Next call), and attach relevant attributes (instance id,
isServiceTask flag, language) and exceptions to the span so the Start call is
visible in traces and errors are recorded.
ℹ️ Review info

Configuration used: Organization UI

Review profile: CHILL

Plan: Pro

📥 Commits

Reviewing files that changed from the base of the PR and between bb98491 and f251b35.

📒 Files selected for processing (7)
  • AGENTS.md
  • src/Altinn.App.Api/Controllers/InstancesController.cs
  • src/Altinn.App.Api/Controllers/ProcessController.cs
  • src/Altinn.App.Core/Internal/Process/Interfaces/IProcessEngine.cs
  • src/Altinn.App.Core/Internal/Process/ProcessEngine.cs
  • test/Altinn.App.Api.Tests/Controllers/InstancesController_CopyInstanceTests.cs
  • test/Altinn.App.Core.Tests/PublicApiTests.PublicApi_ShouldNotChange_Unintentionally.verified.txt
🚧 Files skipped from review as they are similar to previous changes (1)
  • src/Altinn.App.Api/Controllers/InstancesController.cs

Copy link
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.

♻️ Duplicate comments (1)
test/Altinn.App.Api.Tests/Controllers/InstancesController_CopyInstanceTests.cs (1)

387-411: Start mock updated to new 6-parameter signature with ProcessStateChange properly populated in all return values.

The previously flagged missing ProcessStateChange in the Start mock return has been fixed — ProcessStateChange = new() is now present in all five test methods, satisfying the [MemberNotNullWhen] contract.

The non-nullable type matchers for the nullable Dictionary<string, string>? and string? parameters (It.IsAny<Dictionary<string, string>>(), It.IsAny<string>()) are functionally equivalent to their nullable counterparts at Moq runtime (C# nullable reference annotations are compile-time only). For consistency with the actual interface signature, consider using nullable variants.

✨ Optional: align matchers with nullable interface signature
-                    It.IsAny<Dictionary<string, string>>(),
-                    It.IsAny<string>(),
+                    It.IsAny<Dictionary<string, string>?>(),
+                    It.IsAny<string?>(),

Apply the same diff to all five Start mock setups (lines 391–392, 530–531, 748–749, 965–966, 1189–1190).

Also applies to: 526-549, 744-768, 961-985, 1185-1209

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

In
`@test/Altinn.App.Api.Tests/Controllers/InstancesController_CopyInstanceTests.cs`
around lines 387 - 411, The Start mock setups should use nullable matchers to
match the interface signature and consistently populate ProcessStateChange in
the returned ProcessChangeResult; update each Start(...) ReturnsAsync lambda
(the five occurrences that return ProcessChangeResult) to keep
ProcessStateChange = new() and change the non-nullable It.IsAny matchers to
their nullable equivalents (e.g., It.IsAny<Dictionary<string,string>?>() and
It.IsAny<string?>()) so the mock signature matches the Start method's nullable
parameters across all Start setups referenced in the tests.
🤖 Prompt for all review comments with AI agents
Verify each finding against the current code and only fix it if needed.

Duplicate comments:
In
`@test/Altinn.App.Api.Tests/Controllers/InstancesController_CopyInstanceTests.cs`:
- Around line 387-411: The Start mock setups should use nullable matchers to
match the interface signature and consistently populate ProcessStateChange in
the returned ProcessChangeResult; update each Start(...) ReturnsAsync lambda
(the five occurrences that return ProcessChangeResult) to keep
ProcessStateChange = new() and change the non-nullable It.IsAny matchers to
their nullable equivalents (e.g., It.IsAny<Dictionary<string,string>?>() and
It.IsAny<string?>()) so the mock signature matches the Start method's nullable
parameters across all Start setups referenced in the tests.
ℹ️ Review info

Configuration used: Organization UI

Review profile: CHILL

Plan: Pro

📥 Commits

Reviewing files that changed from the base of the PR and between f251b35 and a205638.

📒 Files selected for processing (3)
  • src/Altinn.App.Api/Controllers/ProcessController.cs
  • test/Altinn.App.Api.Tests/Controllers/InstancesController_CopyInstanceTests.cs
  • test/Altinn.App.Api.Tests/Process/ServiceTasks/ServiceTaskFirstTests.cs
🚧 Files skipped from review as they are similar to previous changes (1)
  • test/Altinn.App.Api.Tests/Process/ServiceTasks/ServiceTaskFirstTests.cs

Copy link
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: 2

♻️ Duplicate comments (2)
test/Altinn.App.Api.Tests/Controllers/InstancesController_CopyInstanceTests.cs (1)

384-411: Mock setup correctly matches the updated IProcessEngine.Start signature.

The callback lambda parameters properly use nullable annotations (Dictionary<string, string>?, string?), and ProcessChangeResult now includes both MutatedInstance and ProcessStateChange.

Note: The past review flagged using It.IsAny<Dictionary<string, string>>() instead of nullable-annotated matchers. In practice, Moq's It.IsAny<T>() will match null values for reference types since nullable reference types are a compile-time feature only. The mock will function correctly.

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

In
`@test/Altinn.App.Api.Tests/Controllers/InstancesController_CopyInstanceTests.cs`
around lines 384 - 411, The mock setup for IProcessEngine.Start is
correct—ensure the callback lambda parameters use nullable annotations matching
the updated Start signature (e.g., Dictionary<string, string>?, string?), keep
the It.IsAny<T>() matchers (they will accept null at runtime), and return a
ProcessChangeResult that populates both MutatedInstance and ProcessStateChange
as shown; no code change required other than keeping these nullable parameter
types and the two result properties in the ReturnsAsync lambda for the Start
mock.
src/Altinn.App.Api/Controllers/InstancesController.cs (1)

748-763: Same early-check issue as PostSimplified.
Apply the same ordering here—return on startResult.Success == false before creating/copying the instance.

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

In `@src/Altinn.App.Api/Controllers/InstancesController.cs` around lines 748 -
763, The code calls _processEngine.Start and mutates targetInstance before
checking startResult.Success; reorder the logic to return early when
startResult.Success is false (i.e., keep the existing "if (!startResult.Success)
return Conflict(...)" immediately after computing startResult and before any
creation/copying of the instance or calling _processEngine.Start), then only
call _processEngine.Start and assign targetInstance =
startProcessResult.MutatedInstance after confirming startResult succeeded;
adjust the block around startResult, _processEngine.Start, startProcessResult,
and targetInstance to follow this ordering.
🧹 Nitpick comments (1)
test/Altinn.App.Api.Tests/Controllers/InstancesController_CopyInstanceTests.cs (1)

1314-1391: Good test coverage for Start failure path.

This test correctly validates that the controller returns ConflictObjectResult when IProcessEngine.Start returns Success = false. The ProcessChangeResult setup is appropriate—omitting MutatedInstance and ProcessStateChange is correct since [MemberNotNullWhen(true, ...)] only requires them to be non-null when Success is true.

Minor note: Some other test methods in this file call fixture.Mock<T>().VerifyAll() and fixture.VerifyNoOtherCalls(), but this test does not. Consider adding these verification calls for consistency if the mocks are expected to be invoked exactly as set up.

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

In
`@test/Altinn.App.Api.Tests/Controllers/InstancesController_CopyInstanceTests.cs`
around lines 1314 - 1391, The test CopyInstance_StartFails_ReturnsConflict is
missing mock verification for consistency; after the Assert, call VerifyAll() on
the mocks you set up (HttpContext, IAppMetadata, IPDP, IInstanceClient,
IInstantiationValidator, IProcessEngine) to ensure expected invocations (e.g.,
fixture.Mock<HttpContext>().VerifyAll(),
fixture.Mock<IAppMetadata>().VerifyAll(), etc.), then call
fixture.VerifyNoOtherCalls() to assert no unexpected calls occurred.
🤖 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/Altinn.App.Api/Controllers/InstancesController.cs`:
- Around line 611-627: The check for processResult.Success must be moved
earlier: after the call that sets processResult (likely
GenerateProcessStartEvents) and before any instance creation/copying (methods
CreateInstance/CopyInstance). Change the flow in InstancesController.cs so that
you immediately return Conflict(processResult.ErrorMessage) if
processResult.Success is false, and only proceed to call
CreateInstance/CopyInstance and later _processEngine.Start when
processResult.Success is true; keep subsequent checks for startProcessResult
as-is. Ensure you reference the same processResult variable and preserve passing
processResult.ProcessStateChange into _processEngine.Start.
- Around line 384-395: Wrap the body of ProcessEngine.Start with an
OpenTelemetry span by calling _telemetry?.StartProcessStartActivity(instance) at
the start of the method and ensuring it is disposed/stopped around the existing
logic (including the call to Next()), so the Start flow is instrumented
end-to-end; locate the Start method in the ProcessEngine class, call
_telemetry?.StartProcessStartActivity(instance) to create the span, execute the
original logic (including Next()), and stop/dispose the activity in a finally
block to guarantee the span is ended even on error.

---

Duplicate comments:
In `@src/Altinn.App.Api/Controllers/InstancesController.cs`:
- Around line 748-763: The code calls _processEngine.Start and mutates
targetInstance before checking startResult.Success; reorder the logic to return
early when startResult.Success is false (i.e., keep the existing "if
(!startResult.Success) return Conflict(...)" immediately after computing
startResult and before any creation/copying of the instance or calling
_processEngine.Start), then only call _processEngine.Start and assign
targetInstance = startProcessResult.MutatedInstance after confirming startResult
succeeded; adjust the block around startResult, _processEngine.Start,
startProcessResult, and targetInstance to follow this ordering.

In
`@test/Altinn.App.Api.Tests/Controllers/InstancesController_CopyInstanceTests.cs`:
- Around line 384-411: The mock setup for IProcessEngine.Start is correct—ensure
the callback lambda parameters use nullable annotations matching the updated
Start signature (e.g., Dictionary<string, string>?, string?), keep the
It.IsAny<T>() matchers (they will accept null at runtime), and return a
ProcessChangeResult that populates both MutatedInstance and ProcessStateChange
as shown; no code change required other than keeping these nullable parameter
types and the two result properties in the ReturnsAsync lambda for the Start
mock.

---

Nitpick comments:
In
`@test/Altinn.App.Api.Tests/Controllers/InstancesController_CopyInstanceTests.cs`:
- Around line 1314-1391: The test CopyInstance_StartFails_ReturnsConflict is
missing mock verification for consistency; after the Assert, call VerifyAll() on
the mocks you set up (HttpContext, IAppMetadata, IPDP, IInstanceClient,
IInstantiationValidator, IProcessEngine) to ensure expected invocations (e.g.,
fixture.Mock<HttpContext>().VerifyAll(),
fixture.Mock<IAppMetadata>().VerifyAll(), etc.), then call
fixture.VerifyNoOtherCalls() to assert no unexpected calls occurred.

ℹ️ Review info

Configuration used: Organization UI

Review profile: CHILL

Plan: Pro

📥 Commits

Reviewing files that changed from the base of the PR and between a205638 and f280d66.

📒 Files selected for processing (2)
  • src/Altinn.App.Api/Controllers/InstancesController.cs
  • test/Altinn.App.Api.Tests/Controllers/InstancesController_CopyInstanceTests.cs

Copy link
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.

♻️ Duplicate comments (2)
test/Altinn.App.Api.Tests/Controllers/InstancesController_CopyInstanceTests.cs (1)

386-411: Nullable type mismatch in Start mock setup (pre-existing pattern).

It.IsAny<Dictionary<string, string>>() and It.IsAny<string>() are used as matchers for parameters typed Dictionary<string, string>? and string? in IProcessEngine.Start. The ReturnsAsync lambda already uses the correct nullable annotations (Dictionary<string, string>? _, string? _), making the Setup/ReturnsAsync inconsistent.

This pattern is repeated across all Start mock setups in the file (lines 525–550, 743–768, 960–985, 1183–1209, 1365–1383).

 p.Start(
     It.IsAny<Instance>(),
     It.IsAny<ProcessStateChange>(),
     It.IsAny<System.Security.Claims.ClaimsPrincipal>(),
-    It.IsAny<Dictionary<string, string>>(),
-    It.IsAny<string>(),
+    It.IsAny<Dictionary<string, string>?>(),
+    It.IsAny<string?>(),
     It.IsAny<CancellationToken>()
 )
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In
`@test/Altinn.App.Api.Tests/Controllers/InstancesController_CopyInstanceTests.cs`
around lines 386 - 411, The mock Setup for IProcessEngine.Start uses
non-nullable matchers It.IsAny<Dictionary<string,string>>() and
It.IsAny<string>() while the Start signature and ReturnsAsync lambda use
nullable types; update the Setup call to use nullable generic matchers
It.IsAny<Dictionary<string,string>?>() and It.IsAny<string?>() to match the
method signature. Apply the same fix to all repeated Start mock setups in this
file so the generic matcher types align with the parameter types used in the
ReturnsAsync lambdas (refer to the Start method invocation and the ReturnsAsync
lambda that returns ProcessChangeResult/MutatedInstance).
src/Altinn.App.Api/Controllers/InstancesController.cs (1)

748-762: ⚠️ Potential issue | 🟡 Minor

CopyInstance: GenerateProcessStartEvents failure check placed after CreateInstance and CopyDataFromSourceInstance.

If GenerateProcessStartEvents fails (lines 748–749), the target instance has already been created (line 742) and source data already copied (line 744). This leaves both an orphaned storage instance and copied data elements.

The null-safety rationale for placing the guard here (before accessing startResult.ProcessStateChange at line 753) is correct, but moving the check immediately after line 740 would prevent orphaned resources:

🛡️ Suggested fix
 ProcessChangeResult startResult = await _processEngine.GenerateProcessStartEvents(processStartRequest);

+if (!startResult.Success)
+    return Conflict(startResult.ErrorMessage);
+
 targetInstance = await _instanceClient.CreateInstance(org, app, targetInstance);

 await CopyDataFromSourceInstance(application, targetInstance, sourceInstance);

 targetInstance = await _instanceClient.GetInstance(targetInstance);

-if (!startResult.Success)
-    return Conflict(startResult.ErrorMessage);
-
 var startProcessResult = await _processEngine.Start(
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@src/Altinn.App.Api/Controllers/InstancesController.cs` around lines 748 -
762, In CopyInstance the result of GenerateProcessStartEvents (startResult) is
checked only after CreateInstance and CopyDataFromSourceInstance, which can
leave an orphaned instance and copied data if GenerateProcessStartEvents failed;
move the null/success guard for startResult immediately after the
GenerateProcessStartEvents call (before calling CreateInstance and
CopyDataFromSourceInstance) so you return Conflict(startResult.ErrorMessage)
early when startResult.Success is false, and only proceed to call
_processEngine.Start and assign targetInstance when startResult is valid.
🧹 Nitpick comments (2)
test/Altinn.App.Api.Tests/Controllers/InstancesController_PostNewInstanceTests.PostNewInstance_Simplified_ServiceOwner.verified.txt (1)

165-195: Both Process.Start and Process.StartExecution record identical process-change events — potential double-counting.

The Events payloads (process_StartEvent + process_StartTask, to.started, to.task.name) are identical in both spans. If StartExecution is an outer wrapper around Start, recording the same change events in both means downstream telemetry consumers (e.g., Grafana LGTM, Honeycomb) that aggregate span events will see each process change twice per instantiation. This is worth a conscious decision — if StartExecution is meant to be the authoritative carrier of these events, consider not re-emitting them on the inner Process.Start span (or vice versa).

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

In
`@test/Altinn.App.Api.Tests/Controllers/InstancesController_PostNewInstanceTests.PostNewInstance_Simplified_ServiceOwner.verified.txt`
around lines 165 - 195, The test shows duplicate process-change events emitted
on both Process.StartExecution and its inner Process.Start span; update the
span/event construction so these events are only emitted once—either emit the
process_StartEvent/process_StartTask and tags (to.started, to.task.name) only on
Process.StartExecution or add a conditional in the Process.Start event creation
to skip adding those tags when the span has a parent named
Process.StartExecution (or when HasParent is true). Locate the code that builds
Events for Process.Start and Process.StartExecution and implement the dedupe:
move the event emission to the outer Process.StartExecution span or add a
parent-check guard in Process.Start to avoid double-counting.
test/Altinn.App.Api.Tests/Controllers/InstancesController_CopyInstanceTests.cs (1)

1393-1457: CopyInstance_GenerateProcessStartEventsFails_ReturnsConflict: add fixture.VerifyNoOtherCalls() for completeness.

The test correctly sets up CreateInstance and GetInstance (confirming both are called even on a GenerateProcessStartEvents failure, which documents the ordering issue in the controller), but unlike simpler tests in this file, there is no fixture.VerifyNoOtherCalls() to guard against unexpected calls — e.g., IProcessEngine.Start being called accidentally. While Moq's loose mock would return null for Task<ProcessChangeResult> causing a NullReferenceException that would fail the test, an explicit VerifyNoOtherCalls() makes the intent clearer.

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

In
`@test/Altinn.App.Api.Tests/Controllers/InstancesController_CopyInstanceTests.cs`
around lines 1393 - 1457, Add a final verification to the test
CopyInstance_GenerateProcessStartEventsFails_ReturnsConflict to ensure no
unexpected mock calls occur: after asserting the ConflictObjectResult, call
fixture.VerifyNoOtherCalls() (using the InstancesControllerFixture instance
named fixture) so all mocked dependencies (e.g., IInstanceClient, IProcessEngine
with GenerateProcessStartEvents) have no extra interactions such as an
accidental IProcessEngine.Start invocation.
🤖 Prompt for all review comments with AI agents
Verify each finding against the current code and only fix it if needed.

Duplicate comments:
In `@src/Altinn.App.Api/Controllers/InstancesController.cs`:
- Around line 748-762: In CopyInstance the result of GenerateProcessStartEvents
(startResult) is checked only after CreateInstance and
CopyDataFromSourceInstance, which can leave an orphaned instance and copied data
if GenerateProcessStartEvents failed; move the null/success guard for
startResult immediately after the GenerateProcessStartEvents call (before
calling CreateInstance and CopyDataFromSourceInstance) so you return
Conflict(startResult.ErrorMessage) early when startResult.Success is false, and
only proceed to call _processEngine.Start and assign targetInstance when
startResult is valid.

In
`@test/Altinn.App.Api.Tests/Controllers/InstancesController_CopyInstanceTests.cs`:
- Around line 386-411: The mock Setup for IProcessEngine.Start uses non-nullable
matchers It.IsAny<Dictionary<string,string>>() and It.IsAny<string>() while the
Start signature and ReturnsAsync lambda use nullable types; update the Setup
call to use nullable generic matchers It.IsAny<Dictionary<string,string>?>() and
It.IsAny<string?>() to match the method signature. Apply the same fix to all
repeated Start mock setups in this file so the generic matcher types align with
the parameter types used in the ReturnsAsync lambdas (refer to the Start method
invocation and the ReturnsAsync lambda that returns
ProcessChangeResult/MutatedInstance).

---

Nitpick comments:
In
`@test/Altinn.App.Api.Tests/Controllers/InstancesController_CopyInstanceTests.cs`:
- Around line 1393-1457: Add a final verification to the test
CopyInstance_GenerateProcessStartEventsFails_ReturnsConflict to ensure no
unexpected mock calls occur: after asserting the ConflictObjectResult, call
fixture.VerifyNoOtherCalls() (using the InstancesControllerFixture instance
named fixture) so all mocked dependencies (e.g., IInstanceClient, IProcessEngine
with GenerateProcessStartEvents) have no extra interactions such as an
accidental IProcessEngine.Start invocation.

In
`@test/Altinn.App.Api.Tests/Controllers/InstancesController_PostNewInstanceTests.PostNewInstance_Simplified_ServiceOwner.verified.txt`:
- Around line 165-195: The test shows duplicate process-change events emitted on
both Process.StartExecution and its inner Process.Start span; update the
span/event construction so these events are only emitted once—either emit the
process_StartEvent/process_StartTask and tags (to.started, to.task.name) only on
Process.StartExecution or add a conditional in the Process.Start event creation
to skip adding those tags when the span has a parent named
Process.StartExecution (or when HasParent is true). Locate the code that builds
Events for Process.Start and Process.StartExecution and implement the dedupe:
move the event emission to the outer Process.StartExecution span or add a
parent-check guard in Process.Start to avoid double-counting.

ℹ️ Review info

Configuration used: Organization UI

Review profile: CHILL

Plan: Pro

📥 Commits

Reviewing files that changed from the base of the PR and between a205638 and ea30ffa.

📒 Files selected for processing (8)
  • src/Altinn.App.Api/Controllers/InstancesController.cs
  • src/Altinn.App.Core/Features/Telemetry/Telemetry.Processes.cs
  • src/Altinn.App.Core/Internal/Process/ProcessEngine.cs
  • test/Altinn.App.Api.Tests/Controllers/InstancesController_CopyInstanceTests.cs
  • test/Altinn.App.Api.Tests/Controllers/InstancesController_PostNewInstanceTests.PostNewInstance_Simplified_SelfIdentifiedUser.verified.txt
  • test/Altinn.App.Api.Tests/Controllers/InstancesController_PostNewInstanceTests.PostNewInstance_Simplified_ServiceOwner.verified.txt
  • test/Altinn.App.Api.Tests/Controllers/InstancesController_PostNewInstanceTests.PostNewInstance_Simplified_SystemUser.verified.txt
  • test/Altinn.App.Api.Tests/Controllers/InstancesController_PostNewInstanceTests.PostNewInstance_Simplified_User.verified.txt

Copy link
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.

♻️ Duplicate comments (1)
src/Altinn.App.Api/Controllers/InstancesController.cs (1)

738-762: ⚠️ Potential issue | 🟠 Major

Move the GenerateProcessStartEvents success guard before creating/copying the instance.

Line 748 checks startResult.Success after instance creation and copy. If the process start fails, you still create and populate an instance, leaving orphaned data.

🛠️ Proposed fix
         ProcessChangeResult startResult = await _processEngine.GenerateProcessStartEvents(processStartRequest);
+        if (!startResult.Success)
+            return Conflict(startResult.ErrorMessage);

         targetInstance = await _instanceClient.CreateInstance(org, app, targetInstance);

         await CopyDataFromSourceInstance(application, targetInstance, sourceInstance);

         targetInstance = await _instanceClient.GetInstance(targetInstance);

-        if (!startResult.Success)
-            return Conflict(startResult.ErrorMessage);
-
         var startProcessResult = await _processEngine.Start(
             targetInstance,
             startResult.ProcessStateChange,
             User,
             language: language,
             ct: HttpContext.RequestAborted
         );
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@src/Altinn.App.Api/Controllers/InstancesController.cs` around lines 738 -
762, The code calls _processEngine.GenerateProcessStartEvents (creating
ProcessStartRequest and storing startResult) but only checks startResult.Success
after creating the target instance and copying data; move the success guard so
you check startResult.Success immediately after the GenerateProcessStartEvents
call (and return Conflict(startResult.ErrorMessage) if false) before calling
_instanceClient.CreateInstance or CopyDataFromSourceInstance, ensuring you only
create/populate an instance when GenerateProcessStartEvents succeeded.
🤖 Prompt for all review comments with AI agents
Verify each finding against the current code and only fix it if needed.

Duplicate comments:
In `@src/Altinn.App.Api/Controllers/InstancesController.cs`:
- Around line 738-762: The code calls _processEngine.GenerateProcessStartEvents
(creating ProcessStartRequest and storing startResult) but only checks
startResult.Success after creating the target instance and copying data; move
the success guard so you check startResult.Success immediately after the
GenerateProcessStartEvents call (and return Conflict(startResult.ErrorMessage)
if false) before calling _instanceClient.CreateInstance or
CopyDataFromSourceInstance, ensuring you only create/populate an instance when
GenerateProcessStartEvents succeeded.

ℹ️ Review info

Configuration used: Organization UI

Review profile: CHILL

Plan: Pro

📥 Commits

Reviewing files that changed from the base of the PR and between ea30ffa and 8888c85.

📒 Files selected for processing (1)
  • src/Altinn.App.Api/Controllers/InstancesController.cs

@sonarqubecloud
Copy link

Copy link
Contributor

@HauklandJ HauklandJ 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 to me!

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

Labels

backport-ignore This PR is a new feature and should not be cherry-picked onto release branches feature Label Pull requests with new features. Used when generation releasenotes kind/feature-request New feature or request squad/flyt Issues that belongs to the named squad.

Projects

Status: 🔎 Review

Development

Successfully merging this pull request may close these issues.

2 participants