Skip to content

Added composite command support and test coverage#639

Open
msivasubramaniaan wants to merge 47 commits intoche-incubator:mainfrom
msivasubramaniaan:feat-add-composite-command-support
Open

Added composite command support and test coverage#639
msivasubramaniaan wants to merge 47 commits intoche-incubator:mainfrom
msivasubramaniaan:feat-add-composite-command-support

Conversation

@msivasubramaniaan
Copy link
Copy Markdown
Collaborator

@msivasubramaniaan msivasubramaniaan commented Jan 28, 2026

What does this PR do?

This PR supports to run the composite commands in terminal

What issues does this PR fix?

eclipse-che/che#21859
eclipse-che/che#23726
eclipse-che/che#23709
eclipse-che/che#23725

How to test this PR?

Added workspace which has all commands of devfile. Run those commands from devfile.yaml.

Does this PR contain changes that override default upstream Code-OSS behavior?

  • the PR contains changes in the code folder (you can skip it if your changes are placed in a che extension )
  • the corresponding items were added to the CHANGELOG.md file
  • rules for automatic git rebase were added to the .rebase folder

Summary by CodeRabbit

  • New Features

    • Composite devfile commands: sequential or parallel execution across components with validation, flattening, cancellation and failure handling
  • Tests

    • Comprehensive Jest suites, mocks, and TS/Jest configs covering composite execution, task creation, env handling, shell-preservation, failure semantics, naming, and edge cases
  • Chores

    • Build images now run extension tests during image build; added test tooling and package scripts; updated copyright year ranges

@github-actions
Copy link
Copy Markdown

github-actions bot commented Jan 28, 2026

Click here to review and test in web IDE: Contribute

"peerDependencies": {
"jsep": "^0.4.0||^1.0.0"
"node_modules/@emnapi/core": {
"version": "1.8.1",
Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

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

FYI
The license check is failing

Image

@github-actions
Copy link
Copy Markdown


// Single-component → join commands
const joiner = parallel ? " & " : " && ";
let compositeCmd = execs.map((e) => e.commandLine).join(joiner);
Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

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

@RomanNikitenko , is string manipulation the only way to do this ? Using compound tasks on the vscode side still isn't possible ?

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

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

I investigated that area more than 2 years ago, so I asked @msivasubramaniaan to double check that option when he started to work on the issue.
@msivasubramaniaan could you provide result of your investigation?

Copy link
Copy Markdown
Collaborator Author

Choose a reason for hiding this comment

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

I double-checked this again.

At the moment, yes — string manipulation is still the only practical way to achieve this.

VS Code does not support dynamically creating or chaining compound tasks programmatically from an extension. Compound tasks must be statically defined in tasks.json, and there’s no API to generate or invoke a compound task on the fly based on resolved components or runtime data.

Because of that limitation, joining commands using && / & (depending on parallel vs sequential execution) remains the only viable solution when the task needs to be assembled dynamically.

So the approach of building a composite command string is the only way

Copy link
Copy Markdown
Collaborator

@RomanNikitenko RomanNikitenko left a comment

Choose a reason for hiding this comment

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

@msivasubramaniaan
I tried to test the functionality using your samples
I run component-parallel-demo command:

Image

My understanding - it should run signal-frontend in the frontend container and signal-backend in the backend container.

Actual behaviour:

Moreover, the dependent commands just hanging without any output when I run them separately:

Image

Could you clarify:

  • what is expected behaviour there?
  • is it possible to detect which component is used for your test commands?
  • If commands are in the running state - why composite command reports that Composite multi-component execution completed?

thanks in advance!

@RomanNikitenko
Copy link
Copy Markdown
Collaborator

@msivasubramaniaan
I've prepared own sample and tested parallel and sequential uses cases for 2 components, I can confirm - it does not work, there is a bug - no output for both dependent commands and it's unclear if the commands are executed at all:

Screenshot 2026-02-02 at 23 17 53

@RomanNikitenko
Copy link
Copy Markdown
Collaborator

@msivasubramaniaan
I've detected one more bug : a command label should be displayed instead of id:

Screenshot 2026-02-02 at 23 29 09 Screenshot 2026-02-02 at 23 34 23

@msivasubramaniaan msivasubramaniaan marked this pull request as draft February 5, 2026 11:44
@RomanNikitenko
Copy link
Copy Markdown
Collaborator

RomanNikitenko commented Feb 5, 2026

@msivasubramaniaan
I see you've requested a new review, but image for testing is not ready.
Please let me know when you test all use cases we discussed and regressions are fixed.
Thanks!

@github-actions
Copy link
Copy Markdown

github-actions bot commented Feb 5, 2026

@github-actions
Copy link
Copy Markdown

github-actions bot commented Feb 5, 2026

@github-actions
Copy link
Copy Markdown

github-actions bot commented Feb 5, 2026

@github-actions
Copy link
Copy Markdown

github-actions bot commented Feb 5, 2026

@github-actions
Copy link
Copy Markdown

1 similar comment
@github-actions
Copy link
Copy Markdown

@msivasubramaniaan
Copy link
Copy Markdown
Collaborator Author

msivasubramaniaan commented Mar 2, 2026

Hello @RomanNikitenko
As discussed completed all the feedbacks other than #639 (comment). AFAIK VS Code requires a task to have an execution to run, so I just created a task and close it immediately. If any other suggestion I am glad to receive it.

@github-actions
Copy link
Copy Markdown

github-actions bot commented Mar 9, 2026

}

private sanitize(cmd: string) {
return cmd.replace(/(?:\s*(?:&&|\|\||[|;&]))+\s*$/, "").trim();
Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

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

@msivasubramaniaan
I guess this logic changes behavior from background/non-blocking to foreground/blocking, which is a semantic regression.
Like: cmd & becomes cmd

Could you double check it using logs or another way?

I prepared simple commands that show a background command blocks next command

BG_command.mov

Copy link
Copy Markdown
Collaborator

@RomanNikitenko RomanNikitenko Mar 12, 2026

Choose a reason for hiding this comment

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

@msivasubramaniaan
For example, we use background command to run machine-exec in the che-code: https://github.com/che-incubator/che-code/blob/main/build/scripts/entrypoint-volume.sh#L74

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

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

I used the following commands for testing:

- id: bg-command
    exec:
      label: "Background Command"
      component: ubi9-tools
      commandLine: "sh -c 'echo Start Background Task; sleep 5; echo Complete Background Task' &"
  - id: after-bg-command
    exec:
      label: "NEXT Task after BG Command"
      component: ubi9-tools
      commandLine: "sh -c 'sleep 1; echo NEXT Task after BG TASK'"
  - id: bg-in-composite
    composite:
      label: "Background operator stripped"
      commands:
        - bg-command
        - after-bg-command
      parallel: false

@RomanNikitenko
Copy link
Copy Markdown
Collaborator

@msivasubramaniaan
I detected another bug, it's related to resolving workingDir.

On the video you can see that:

  • resolving workingDir works correctly when I run separate tasks
  • there is a bug when I run the same tasks within a composite task
Working_Dir_bug.mp4

@coderabbitai
Copy link
Copy Markdown

coderabbitai bot commented Mar 19, 2026

No actionable comments were generated in the recent review. 🎉

ℹ️ Recent review info
⚙️ Run configuration

Configuration used: Path: .coderabbit.yaml

Review profile: CHILL

Plan: Pro

Run ID: 4f852627-095c-4527-99a0-9b13330a07c8

📥 Commits

Reviewing files that changed from the base of the PR and between 9963c38 and 1905f9d.

📒 Files selected for processing (2)
  • code/extensions/che-commands/tests/compositeTaskBuilder.test.ts
  • code/extensions/che-commands/tests/mocks.ts
✅ Files skipped from review due to trivial changes (1)
  • code/extensions/che-commands/tests/compositeTaskBuilder.test.ts
🚧 Files skipped from review as they are similar to previous changes (1)
  • code/extensions/che-commands/tests/mocks.ts

📝 Walkthrough

Walkthrough

Adds Jest test infrastructure and multiple test suites for the che-commands extension, including VS Code and terminal mocks and a ts-jest configuration. Introduces a new CompositeTaskBuilder that validates and flattens devfile composite commands and produces vscode.Tasks supporting both parallel (via child task execution) and sequential (via an aggregator pseudoterminal running per-step PTYs) execution semantics. Updates DevfileTaskProvider to delegate composite commands to the builder and adjusts env escaping. Adds npm test steps to several Dockerfiles and updates copyright year ranges.

Sequence Diagram(s)

sequenceDiagram
    participant Client
    participant TaskProvider
    participant CompositeBuilder
    participant VSCodeAPI
    participant TerminalExtAPI
    participant Component

    Client->>TaskProvider: provideTasks(devfile)
    activate TaskProvider
    TaskProvider->>CompositeBuilder: build(compositeCommand, allCommands)
    activate CompositeBuilder
    CompositeBuilder->>CompositeBuilder: validate() & flatten()
    alt parallel composite
        CompositeBuilder->>VSCodeAPI: create CustomExecution parent task
        VSCodeAPI-->>Client: expose parent task
        Client->>VSCodeAPI: execute parent task
        VSCodeAPI->>VSCodeAPI: parent.open() triggers child tasks
        VSCodeAPI->>VSCodeAPI: vscode.tasks.executeTask(childTask) per step
        VSCodeAPI->>TerminalExtAPI: start child execs (via tasks)
        TerminalExtAPI->>Component: run each step concurrently
        Component-->>TerminalExtAPI: outputs / exits
    else sequential composite
        CompositeBuilder->>VSCodeAPI: create CustomExecution aggregator task
        VSCodeAPI-->>Client: expose aggregator task
        Client->>VSCodeAPI: execute aggregator
        VSCodeAPI->>TerminalExtAPI: getMachineExecPTY(step) for step1
        TerminalExtAPI->>Component: run step1
        Component-->>TerminalExtAPI: outputs / exit code
        TerminalExtAPI->>VSCodeAPI: stream output to aggregator PTY
        loop next steps
            VSCodeAPI->>TerminalExtAPI: getMachineExecPTY(nextStep)
            TerminalExtAPI->>Component: run nextStep
            Component-->>TerminalExtAPI: outputs / exit code
        end
        alt step fails (non-zero)
            TerminalExtAPI->>VSCodeAPI: aggregator close -> stop remaining steps
        end
    end
    deactivate CompositeBuilder
    TaskProvider-->>Client: tasks list
    deactivate TaskProvider
Loading

Estimated code review effort

🎯 4 (Complex) | ⏱️ ~45 minutes

Suggested reviewers

  • rgrunber
  • azatsarynnyy
  • vitaliy-guliy
  • RomanNikitenko
🚥 Pre-merge checks | ✅ 3
✅ Passed checks (3 passed)
Check name Status Explanation
Description Check ✅ Passed Check skipped - CodeRabbit’s high-level summary is enabled.
Title check ✅ Passed The title accurately describes the main changes: adding composite command support with test coverage. It follows imperative mood and is under 72 characters.
Rebase Rules For Upstream Changes ✅ Passed PR changes only Che-only extensions in code/extensions/che-commands/ and build/dockerfiles/, both excluded from rebase rules scope.

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

✨ Finishing Touches
🧪 Generate unit tests (beta)
  • Create PR with unit tests

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

@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 (1)
code/extensions/che-commands/src/compositeTaskBuilder.ts (1)

328-330: ⚠️ Potential issue | 🟠 Major

Background operator & at end of command is stripped, changing semantics.

The sanitize() regex removes trailing & which converts background commands to foreground. For example, cmd & becomes cmd. This was flagged in past reviews - a command intended to run in background will now block.

🐛 Proposed fix: only strip joiners when combining commands
-	private sanitize(cmd: string) {
-		return cmd.replace(/(?:\s*(?:&&|\|\||[|;&]))+\s*$/, "").trim();
+	private sanitize(cmd: string) {
+		// Only strip trailing sequential joiners (&&, ||, ;) - preserve background &
+		return cmd.replace(/(?:\s*(?:&&|\|\||;))+\s*$/, "").trim();
 	}

Note: This preserves standalone & for backgrounding while still removing trailing &&, ||, and ; that are meant as joiners.

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

In `@code/extensions/che-commands/src/compositeTaskBuilder.ts` around lines 328 -
330, sanitize currently strips a trailing single ampersand (background operator)
because the regex includes '&' in the character class; update the sanitize
function so it only strips command joiners (&&, ||, |, ;) but preserves a
standalone '&' at the end; specifically, replace the current regex in sanitize
with one that does not include '&' in the single-character class (e.g., remove
'&' from [|;&] and keep matching multi-char operators like && and ||) so
sanitize(cmd: string) returns the trimmed string but leaves a trailing single
'&' intact.
🧹 Nitpick comments (6)
code/extensions/che-commands/package.json (1)

82-82: Missing trailing newline at end of file.

Most editors and linters expect a trailing newline. Consider adding one for consistency.

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

In `@code/extensions/che-commands/package.json` at line 82, The file
code/extensions/che-commands/package.json is missing a trailing newline after
the final closing brace; open that package.json and add a single newline
character at the end of file so the file ends with "\n" (ensure the final "}"
remains intact), save the file and include the change in your commit so
linters/editors recognize the EOF newline.
code/extensions/che-commands/src/compositeTaskBuilder.ts (1)

187-213: Consider extracting duplicate cancellation logic.

The close() and handleInput('\x03') handlers share nearly identical cleanup logic. Extract to a helper for maintainability.

♻️ Suggested refactor
+			const cancelExecution = () => {
+				isCancelled = true;
+				for (const p of activePtys) {
+					try {
+						p.handleInput?.("\x03");
+						p.close?.();
+					} catch {}
+				}
+				activePtys.clear();
+				closeEmitter.fire(130);
+			};
+
 			const aggregator: vscode.Pseudoterminal = {
 				// ...
 				close: () => {
-					isCancelled = true;
-					for (const p of activePtys) {
-						try {
-							p.handleInput?.("\x03");
-							p.close?.();
-						} catch {}
-					}
-					activePtys.clear();
-					closeEmitter.fire(130);
+					cancelExecution();
 				},
 				handleInput: (data: string) => {
 					if (data === "\x03") {
-						isCancelled = true;
-						for (const p of activePtys) {
-							p.handleInput?.("\x03");
-							p.close?.();
-						}
-						activePtys.clear();
-						closeEmitter.fire(130);
+						cancelExecution();
 					}
 				},
 			};
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@code/extensions/che-commands/src/compositeTaskBuilder.ts` around lines 187 -
213, The cancellation/cleanup logic duplicated in the compositeTaskBuilder
object's close() and handleInput(data) when data === "\x03" should be extracted
into a single helper function (e.g., cancelAndCleanup or performCancellation) to
avoid repetition: move the body that sets isCancelled = true, iterates
activePtys calling p.handleInput?.("\x03") and p.close?.(), clears activePtys,
and fires closeEmitter.fire(130) into that helper, then call that helper from
both close and handleInput branches; update references to activePtys,
isCancelled, and closeEmitter inside the new helper so behavior remains
identical.
code/extensions/che-commands/tests/__mocks__/vscode.ts (1)

30-55: Consider adding TaskRevealKind, TaskPanelKind, and tasks to the mock.

The compositeTaskBuilder.ts uses vscode.TaskRevealKind, vscode.TaskPanelKind, and vscode.tasks.executeTask, which are currently mocked inline in tests. Adding them here would improve consistency and reduce test-level mocking.

♻️ Optional additions to the mock
 export const TaskScope = {
 	Workspace: 1,
 };
+
+export const TaskRevealKind = {
+	Always: 1,
+	Silent: 2,
+	Never: 3,
+};
+
+export const TaskPanelKind = {
+	Shared: 1,
+	Dedicated: 2,
+	New: 3,
+};
+
+export const tasks = {
+	executeTask: async () => ({ terminate: () => {} }),
+};
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@code/extensions/che-commands/tests/__mocks__/vscode.ts` around lines 30 - 55,
The mock is missing TaskRevealKind, TaskPanelKind, and vscode.tasks.executeTask
used by compositeTaskBuilder.ts; add TaskRevealKind and TaskPanelKind
enums/objects (e.g., values like Always/Hidden/never and
Shared/Dedicated/Replace) alongside the existing Task and TaskScope, and add a
tasks object exposing an executeTask method that returns a Promise (can resolve
to undefined) and a registerTaskProvider stub; ensure the symbols
TaskRevealKind, TaskPanelKind, and tasks.executeTask are exported from the mock
so tests can use them consistently.
code/extensions/che-commands/tests/mocks.ts (1)

45-47: close() always fires exit code 0, ignoring configured exitCode.

When close() is called directly (e.g., during cancellation), it fires 0 instead of the configured exit code. This may not matter for current tests but could cause confusion if tests expect close() to respect configured exit codes.

♻️ Consider using configured exitCode or a parameter
-	close() {
-		for (const l of this.closeListeners) l(0);
+	close(code?: number) {
+		for (const l of this.closeListeners) l(code ?? this.exitCode);
 	}
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@code/extensions/che-commands/tests/mocks.ts` around lines 45 - 47, The
close() method currently always invokes closeListeners with 0; update it to use
the configured exit code (e.g., this.exitCode) or accept an optional parameter
so it calls each listener with the real exit code instead of hardcoding 0;
modify the close() signature and callers (or default to this.exitCode) and
ensure closeListeners is invoked with that exit code value so cancellation paths
and tests receive the correct code (refer to close(), closeListeners and any
exitCode property/field).
code/extensions/che-commands/tests/taskProvider.test.ts (1)

143-166: Cleanup process.env.TEST_DIR after test to avoid pollution.

The test sets process.env.TEST_DIR without cleanup, which could affect other tests.

♻️ Suggested fix
 describe("Exec working directory expansion", () => {
+	const originalTestDir = process.env.TEST_DIR;
+
+	afterEach(() => {
+		if (originalTestDir === undefined) {
+			delete process.env.TEST_DIR;
+		} else {
+			process.env.TEST_DIR = originalTestDir;
+		}
+	});
+
 	test("expands ${VAR} placeholders from process.env", async () => {
 		process.env.TEST_DIR = "/tmp/demo";
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@code/extensions/che-commands/tests/taskProvider.test.ts` around lines 143 -
166, The test "expands ${VAR} placeholders from process.env" sets
process.env.TEST_DIR but never restores it; either restore the original value or
delete process.env.TEST_DIR after the test to avoid pollution. Update the test
(or add an afterEach) around the test that calls
provide/runFirst/MockTerminalAPI so you capture the previous value of
process.env.TEST_DIR, run the assertions, and then set process.env.TEST_DIR back
to the previous value (or delete it) in a finally/cleanup step to ensure other
tests aren’t affected.
code/extensions/che-commands/tests/compositeTaskBuilder.test.ts (1)

117-153: Test isolation: restore mocked vscode properties after test.

The test mutates the global vscode mock (TaskRevealKind, TaskPanelKind, tasks) without cleanup. This can pollute subsequent tests in the same suite.

♻️ Suggested fix using beforeEach/afterEach
+describe("Composite — cross component execution", () => {
+	let originalTaskRevealKind: any;
+	let originalTaskPanelKind: any;
+	let originalTasks: any;
+
+	beforeEach(() => {
+		originalTaskRevealKind = (vscode as any).TaskRevealKind;
+		originalTaskPanelKind = (vscode as any).TaskPanelKind;
+		originalTasks = (vscode as any).tasks;
+	});
+
+	afterEach(() => {
+		(vscode as any).TaskRevealKind = originalTaskRevealKind;
+		(vscode as any).TaskPanelKind = originalTaskPanelKind;
+		(vscode as any).tasks = originalTasks;
+	});
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@code/extensions/che-commands/tests/compositeTaskBuilder.test.ts` around lines
117 - 153, The test "parallel composite executes all components and opens only
child terminals" mutates global vscode properties (TaskRevealKind,
TaskPanelKind, tasks) and should restore them to avoid polluting other tests;
modify the test file to save the original values of (vscode as
any).TaskRevealKind, (vscode as any).TaskPanelKind and (vscode as any).tasks
before mutating and restore them in an afterEach (or restore at the end of the
test), or move the setup into a beforeEach and the teardown into afterEach so
provide, MockTerminalAPI and runByName behavior remains isolated; locate the
test by its name and update the surrounding describe block to include
beforeEach/afterEach that capture/restore those symbols.
🤖 Prompt for all review comments with AI agents
Verify each finding against the current code and only fix it if needed.

Inline comments:
In `@code/extensions/che-commands/jest.config.js`:
- Around line 19-23: The current jest config uses the deprecated
globals['ts-jest'] and likely a preset; replace this by removing the preset and
moving the ts-jest settings into the transform option: remove the globals object
(and any preset entry), add a transform mapping for the TypeScript files that
references ts-jest and passes the tsconfig.jest.json path (preserving the same
tsconfig). Update any references to globals or preset so Jest uses transform
with ts-jest instead of globals['ts-jest'].

In `@code/extensions/che-commands/tsconfig.json`:
- Line 10: Remove test-specific types and include patterns from the main
tsconfig: delete "jest" from the "types" array and remove the "tests/**/*.ts"
(or any test globs) from "include" in the existing tsconfig.json; instead ensure
the dedicated tsconfig.jest.json contains those entries and is used for test
runs. Locate the "types": ["jest"] reference and any "tests/**/*.ts" include
entries in the tsconfig.json and remove them, and verify tsconfig.jest.json
contains the test-specific "types" and "include" so test files and Jest types
are only declared there.

---

Duplicate comments:
In `@code/extensions/che-commands/src/compositeTaskBuilder.ts`:
- Around line 328-330: sanitize currently strips a trailing single ampersand
(background operator) because the regex includes '&' in the character class;
update the sanitize function so it only strips command joiners (&&, ||, |, ;)
but preserves a standalone '&' at the end; specifically, replace the current
regex in sanitize with one that does not include '&' in the single-character
class (e.g., remove '&' from [|;&] and keep matching multi-char operators like
&& and ||) so sanitize(cmd: string) returns the trimmed string but leaves a
trailing single '&' intact.

---

Nitpick comments:
In `@code/extensions/che-commands/package.json`:
- Line 82: The file code/extensions/che-commands/package.json is missing a
trailing newline after the final closing brace; open that package.json and add a
single newline character at the end of file so the file ends with "\n" (ensure
the final "}" remains intact), save the file and include the change in your
commit so linters/editors recognize the EOF newline.

In `@code/extensions/che-commands/src/compositeTaskBuilder.ts`:
- Around line 187-213: The cancellation/cleanup logic duplicated in the
compositeTaskBuilder object's close() and handleInput(data) when data === "\x03"
should be extracted into a single helper function (e.g., cancelAndCleanup or
performCancellation) to avoid repetition: move the body that sets isCancelled =
true, iterates activePtys calling p.handleInput?.("\x03") and p.close?.(),
clears activePtys, and fires closeEmitter.fire(130) into that helper, then call
that helper from both close and handleInput branches; update references to
activePtys, isCancelled, and closeEmitter inside the new helper so behavior
remains identical.

In `@code/extensions/che-commands/tests/__mocks__/vscode.ts`:
- Around line 30-55: The mock is missing TaskRevealKind, TaskPanelKind, and
vscode.tasks.executeTask used by compositeTaskBuilder.ts; add TaskRevealKind and
TaskPanelKind enums/objects (e.g., values like Always/Hidden/never and
Shared/Dedicated/Replace) alongside the existing Task and TaskScope, and add a
tasks object exposing an executeTask method that returns a Promise (can resolve
to undefined) and a registerTaskProvider stub; ensure the symbols
TaskRevealKind, TaskPanelKind, and tasks.executeTask are exported from the mock
so tests can use them consistently.

In `@code/extensions/che-commands/tests/compositeTaskBuilder.test.ts`:
- Around line 117-153: The test "parallel composite executes all components and
opens only child terminals" mutates global vscode properties (TaskRevealKind,
TaskPanelKind, tasks) and should restore them to avoid polluting other tests;
modify the test file to save the original values of (vscode as
any).TaskRevealKind, (vscode as any).TaskPanelKind and (vscode as any).tasks
before mutating and restore them in an afterEach (or restore at the end of the
test), or move the setup into a beforeEach and the teardown into afterEach so
provide, MockTerminalAPI and runByName behavior remains isolated; locate the
test by its name and update the surrounding describe block to include
beforeEach/afterEach that capture/restore those symbols.

In `@code/extensions/che-commands/tests/mocks.ts`:
- Around line 45-47: The close() method currently always invokes closeListeners
with 0; update it to use the configured exit code (e.g., this.exitCode) or
accept an optional parameter so it calls each listener with the real exit code
instead of hardcoding 0; modify the close() signature and callers (or default to
this.exitCode) and ensure closeListeners is invoked with that exit code value so
cancellation paths and tests receive the correct code (refer to close(),
closeListeners and any exitCode property/field).

In `@code/extensions/che-commands/tests/taskProvider.test.ts`:
- Around line 143-166: The test "expands ${VAR} placeholders from process.env"
sets process.env.TEST_DIR but never restores it; either restore the original
value or delete process.env.TEST_DIR after the test to avoid pollution. Update
the test (or add an afterEach) around the test that calls
provide/runFirst/MockTerminalAPI so you capture the previous value of
process.env.TEST_DIR, run the assertions, and then set process.env.TEST_DIR back
to the previous value (or delete it) in a finally/cleanup step to ensure other
tests aren’t affected.

ℹ️ Review info
⚙️ Run configuration

Configuration used: defaults

Review profile: CHILL

Plan: Pro

Run ID: c09384de-6585-4a52-b536-95dd5db8d961

📥 Commits

Reviewing files that changed from the base of the PR and between 4bab97c and 8d6f68f.

⛔ Files ignored due to path filters (1)
  • code/extensions/che-commands/package-lock.json is excluded by !**/package-lock.json
📒 Files selected for processing (13)
  • build/dockerfiles/linux-libc-ubi8.Dockerfile
  • build/dockerfiles/linux-libc-ubi9.Dockerfile
  • build/dockerfiles/linux-musl.Dockerfile
  • code/extensions/che-commands/jest.config.js
  • code/extensions/che-commands/package.json
  • code/extensions/che-commands/src/compositeTaskBuilder.ts
  • code/extensions/che-commands/src/taskProvider.ts
  • code/extensions/che-commands/tests/__mocks__/vscode.ts
  • code/extensions/che-commands/tests/compositeTaskBuilder.test.ts
  • code/extensions/che-commands/tests/mocks.ts
  • code/extensions/che-commands/tests/taskProvider.test.ts
  • code/extensions/che-commands/tsconfig.jest.json
  • code/extensions/che-commands/tsconfig.json

Comment thread code/extensions/che-commands/jest.config.js Outdated
"declaration": true,
"types": [
"node",
"jest",
Copy link
Copy Markdown

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 test files are compiled to the out directory during build
# and if the webpack config processes tsconfig.json

echo "=== Checking webpack config usage of tsconfig.json ==="
rg -n "tsconfig.json" code/extensions/shared.webpack.config.mjs code/extensions/mangle-loader.js

echo ""
echo "=== Checking if tests directory exists and what it contains ==="
fd -t f . code/extensions/che-commands/tests 2>/dev/null | head -20

echo ""
echo "=== Checking gulp compile task for che-commands ==="
rg -n "che-commands" code/build/gulpfile.extensions.js 2>/dev/null || echo "No specific gulp task found"

Repository: che-incubator/che-code

Length of output: 758


🏁 Script executed:

#!/bin/bash

echo "=== Checking for tsconfig.jest.json ==="
find code/extensions/che-commands -name "tsconfig.jest.json" -o -name "tsconfig.*.json" | head -20

echo ""
echo "=== Examining shared.webpack.config.mjs to understand tsconfig usage ==="
sed -n '40,160p' code/extensions/shared.webpack.config.mjs

echo ""
echo "=== Checking current tsconfig.json include array ==="
cat code/extensions/che-commands/tsconfig.json | grep -A 10 '"include"'

Repository: che-incubator/che-code

Length of output: 4833


🏁 Script executed:

#!/bin/bash

echo "=== Checking tsLoaderOptions definition ==="
rg -B 5 "tsLoaderOptions" code/extensions/shared.webpack.config.mjs | head -30

echo ""
echo "=== Examining mangle-loader.js usage of configFile ==="
cat code/extensions/mangle-loader.js | head -50

echo ""
echo "=== Checking if tsconfig.jest.json extends main tsconfig ==="
cat code/extensions/che-commands/tsconfig.jest.json

Repository: che-incubator/che-code

Length of output: 2606


Test files and Jest types should not be in the main tsconfig.json.

While the CopyWebpackPlugin prevents test files from being included in the production output, including "jest" in types and "tests/**/*.ts" in the main tsconfig still pollutes the production type-checking environment. The dedicated tsconfig.jest.json already exists and should be the single source of truth for all test-related configuration.

Proposed fix
 	"compilerOptions": {
 		"outDir": "./out",
 		"experimentalDecorators": true,
 		"emitDecoratorMetadata": true,
-		"declaration": true,
 		"types": [
 			"node",
-			"jest",
 			"reflect-metadata"
 		]
 	},
 	"include": [
 		"src/**/*",
-		"tests/**/*.ts",
 		"../../src/vscode-dts/vscode.d.ts"
 	]
📝 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
"jest",
"compilerOptions": {
"outDir": "./out",
"experimentalDecorators": true,
"emitDecoratorMetadata": true,
"types": [
"node",
"reflect-metadata"
]
},
"include": [
"src/**/*",
"../../src/vscode-dts/vscode.d.ts"
]
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@code/extensions/che-commands/tsconfig.json` at line 10, Remove test-specific
types and include patterns from the main tsconfig: delete "jest" from the
"types" array and remove the "tests/**/*.ts" (or any test globs) from "include"
in the existing tsconfig.json; instead ensure the dedicated tsconfig.jest.json
contains those entries and is used for test runs. Locate the "types": ["jest"]
reference and any "tests/**/*.ts" include entries in the tsconfig.json and
remove them, and verify tsconfig.jest.json contains the test-specific "types"
and "include" so test files and Jest types are only declared there.

@github-actions
Copy link
Copy Markdown

Copy link
Copy Markdown

@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

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

Inline comments:
In `@code/extensions/che-commands/jest.config.js`:
- Around line 14-28: Replace the multiple named exports (testEnvironment, roots,
collectCoverage, transform, moduleNameMapper, collectCoverageFrom) with a single
exported Jest configuration object (e.g., module.exports = { ... } or export
default { ... }) that contains those keys and values; keep the existing values
(including the '^.+\\.tsx?$' transform using ts-jest and its tsconfig setting,
the moduleNameMapper for '^vscode$', and the collectCoverageFrom array) but
remove the individual export const declarations and return them as properties of
one config object.

In `@code/extensions/che-commands/src/compositeTaskBuilder.ts`:
- Around line 43-45: The code currently turns an empty/unresolved composite into
a no-op echo task by returning this.echoTask(`Composite ${command.id} resolved
empty`); instead of skipping it, change the logic to return undefined (or null)
when execs.length === 0 so DevfileTaskProvider.computeTasks() can filter it out;
update the same pattern found around lines where the alternate branch returns an
echo task (referencing this.echoTask and the composite task building function in
compositeTaskBuilder.ts) and ensure callers expect a possibly undefined task
rather than a placeholder echo.
- Around line 88-90: The composite PTY emits duplicate close events because both
the child PTY exit handler and the final open() success path call
closeEmitter.fire; modify the CustomExecution created in compositeTaskBuilder.ts
so closeEmitter.fire is invoked exactly once by adding a single shared "closed"
flag (or using an atomic/once guard) checked/updated before firing; update the
open() finalization path and any child PTY exit/CTRL+C handlers (the places that
currently call closeEmitter.fire with exit code 130 and the final success close)
to consult this guard and only fire when it transitions from false to true, and
ensure any related disposables/listeners clear or ignore further close attempts.
- Around line 61-67: Composite exec commands are bypassing the normal standalone
path (createCheTask) and call getMachineExecPTY directly, causing env to be
dropped, sanitize() to be skipped and workingDir to differ; update the composite
branches that build exec tasks (places that use cmd.exec.commandLine and call
getMachineExecPTY) to instead construct tasks via createCheTask(...) passing
through cmd.exec.env and resolved workingDir (use cmd.exec.workingDir ||
"${PROJECT_SOURCE}"), and ensure the commandLine is run through sanitize() (or
the same sanitizer used by createCheTask) so composite, sequential and parallel
branches behave identically; apply the same change to the other
composite-building occurrences that mirror this pattern.
- Around line 190-207: The composite PTY currently fires closeEmitter.fire(0)
immediately after launching children; change it to capture the TaskExecution
returned by vscode.tasks.executeTask(childTask) inside the open() of the
CustomExecution (where execs.forEach and buildExecTask(...) are invoked), track
those executions and wait for their termination via
vscode.tasks.onDidEndTaskProcess or vscode.tasks.onDidEndTask, and only call
closeEmitter.fire(0) when all tracked child TaskExecution instances have
completed; also wire parent close() to cancel/terminate any still-running
TaskExecution handles to support proper cancellation and cleanup.

ℹ️ Review info
⚙️ Run configuration

Configuration used: defaults

Review profile: CHILL

Plan: Pro

Run ID: 8456d5f7-14d6-434c-a3c9-ddc4e2bce433

📥 Commits

Reviewing files that changed from the base of the PR and between 8d6f68f and 008820f.

📒 Files selected for processing (3)
  • code/extensions/che-commands/jest.config.js
  • code/extensions/che-commands/src/compositeTaskBuilder.ts
  • code/extensions/che-commands/tests/compositeTaskBuilder.test.ts

Comment on lines +14 to +28
export const testEnvironment = 'node';
export const roots = ['<rootDir>/tests'];
export const collectCoverage = true;
export const transform = {
'^.+\\.tsx?$': ['ts-jest', {
tsconfig: 'tsconfig.json' }],
};
export const moduleNameMapper = {
'^vscode$': '<rootDir>/tests/__mocks__/vscode.ts'
};
export const collectCoverageFrom = [
"src/**/*.ts",
"!src/**/*.d.ts",
"!tests/**",
] No newline at end of file
Copy link
Copy Markdown

Choose a reason for hiding this comment

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

⚠️ Potential issue | 🔴 Critical

🧩 Analysis chain

🌐 Web query:

In Jest's official configuration docs, should jest.config.js export the configuration object itself (module.exportsorexport default) instead of named exports?

💡 Result:

Yes, in Jest's official configuration docs, jest.config.js should export the configuration object itself using module.exports = config (for CommonJS JavaScript) or export default config (for ES modules or TypeScript), rather than named exports. The docs explicitly state: "The configuration file should simply export an object," and provide examples using these default export patterns.

Citations:


Export the Jest configuration as a single object, not named exports.

The jest.config.js file currently exports individual named bindings (export const testEnvironment, export const roots, etc.), but Jest requires the configuration to be exported as a single object via module.exports or export default. The runner will fail to load these settings before tests run.

Proposed fix
 //@ts-check
-export const testEnvironment = 'node';
-export const roots = ['<rootDir>/tests'];
-export const collectCoverage = true;
-export const transform = {
-  '^.+\\.tsx?$': ['ts-jest', { 
-    tsconfig: 'tsconfig.json' }],
-};
-export const moduleNameMapper = {
-    '^vscode$': '<rootDir>/tests/__mocks__/vscode.ts'
-};
-export const collectCoverageFrom = [
-    "src/**/*.ts",
-    "!src/**/*.d.ts",
-    "!tests/**",
-]
+/** `@type` {import('jest').Config} */
+module.exports = {
+  testEnvironment: 'node',
+  roots: ['<rootDir>/tests'],
+  collectCoverage: true,
+  transform: {
+    '^.+\\.tsx?$': ['ts-jest', { tsconfig: 'tsconfig.json' }],
+  },
+  moduleNameMapper: {
+    '^vscode$': '<rootDir>/tests/__mocks__/vscode.ts',
+  },
+  collectCoverageFrom: [
+    'src/**/*.ts',
+    '!src/**/*.d.ts',
+    '!tests/**',
+  ],
+};
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@code/extensions/che-commands/jest.config.js` around lines 14 - 28, Replace
the multiple named exports (testEnvironment, roots, collectCoverage, transform,
moduleNameMapper, collectCoverageFrom) with a single exported Jest configuration
object (e.g., module.exports = { ... } or export default { ... }) that contains
those keys and values; keep the existing values (including the '^.+\\.tsx?$'
transform using ts-jest and its tsconfig setting, the moduleNameMapper for
'^vscode$', and the collectCoverageFrom array) but remove the individual export
const declarations and return them as properties of one config object.

Comment on lines +43 to +45
if (!execs.length) {
return this.echoTask(`Composite ${command.id} resolved empty`);
}
Copy link
Copy Markdown

Choose a reason for hiding this comment

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

⚠️ Potential issue | 🟠 Major

Skip empty composites instead of surfacing a fake echo task.

Line 44 turns an unresolved/empty composite into a runnable task. Because DevfileTaskProvider.computeTasks() only filters undefined, that no-op still shows up in the task list and the “invalid composites are not exposed” behavior can pass just because the task was renamed.

💡 Proposed fix
-		if (!execs.length) {
-			return this.echoTask(`Composite ${command.id} resolved empty`);
-		}
+		if (!execs.length) {
+			this.channel.appendLine(`Skipping composite ${command.id}: resolved empty`);
+			return undefined;
+		}

Also applies to: 291-299

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

In `@code/extensions/che-commands/src/compositeTaskBuilder.ts` around lines 43 -
45, The code currently turns an empty/unresolved composite into a no-op echo
task by returning this.echoTask(`Composite ${command.id} resolved empty`);
instead of skipping it, change the logic to return undefined (or null) when
execs.length === 0 so DevfileTaskProvider.computeTasks() can filter it out;
update the same pattern found around lines where the alternate branch returns an
echo task (referencing this.echoTask and the composite task building function in
compositeTaskBuilder.ts) and ensure callers expect a possibly undefined task
rather than a placeholder echo.

Comment thread code/extensions/che-commands/src/compositeTaskBuilder.ts
Comment on lines +88 to +90
const execution = new vscode.CustomExecution(async () => {
const writeEmitter = new vscode.EventEmitter<string>();
const closeEmitter = new vscode.EventEmitter<number>();
Copy link
Copy Markdown

Choose a reason for hiding this comment

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

⚠️ Potential issue | 🟠 Major

Close the sequential PTY exactly once.

When Ctrl+C or close() emits 130, open() can still fall through to the final success close after the active child PTY exits. That produces duplicate close events and can make a cancelled composite look successful.

💡 Proposed fix
 		const execution = new vscode.CustomExecution(async () => {
 			const writeEmitter = new vscode.EventEmitter<string>();
 			const closeEmitter = new vscode.EventEmitter<number>();
+			let closed = false;
+			const finalize = (exitCode: number) => {
+				if (closed) return;
+				closed = true;
+				closeEmitter.fire(exitCode);
+			};

 			const aggregator: vscode.Pseudoterminal = {
 				onDidWrite: writeEmitter.event,
 				onDidClose: closeEmitter.event,
@@
 					for (const e of execs) {
 						if (isCancelled) break;
 						const exitCode: number = await run(e);
 						if(exitCode !== 0) {
 							isCancelled = true;
-							closeEmitter.fire(exitCode);
+							finalize(exitCode);
 							return;
 						}
 					}
-					closeEmitter.fire(0);
+					finalize(isCancelled ? 130 : 0);
 				},
@@
 					activePtys.clear();
-					closeEmitter.fire(130);
+					finalize(130);
 				},
@@
 						activePtys.clear();
-						closeEmitter.fire(130);
+						finalize(130);
 					}
 				},

Also applies to: 125-148, 151-162

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

In `@code/extensions/che-commands/src/compositeTaskBuilder.ts` around lines 88 -
90, The composite PTY emits duplicate close events because both the child PTY
exit handler and the final open() success path call closeEmitter.fire; modify
the CustomExecution created in compositeTaskBuilder.ts so closeEmitter.fire is
invoked exactly once by adding a single shared "closed" flag (or using an
atomic/once guard) checked/updated before firing; update the open() finalization
path and any child PTY exit/CTRL+C handlers (the places that currently call
closeEmitter.fire with exit code 130 and the final success close) to consult
this guard and only fire when it transitions from false to true, and ensure any
related disposables/listeners clear or ignore further close attempts.

Comment on lines +190 to +207
const execution = new vscode.CustomExecution(async () => {
const closeEmitter = new vscode.EventEmitter<number>();

const pty: vscode.Pseudoterminal = {
onDidWrite: new vscode.EventEmitter<string>().event,
onDidClose: closeEmitter.event,

open: () => {
execs.forEach((e, index) => {
const childTask = this.buildExecTask(e, index === 0);
vscode.tasks.executeTask(childTask);
});

closeEmitter.fire(0);
},

close: () => closeEmitter.fire(0),
handleInput: () => {},
Copy link
Copy Markdown

Choose a reason for hiding this comment

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

⚠️ Potential issue | 🔴 Critical

🧩 Analysis chain

🌐 Web query:

In the VS Code Tasks API, what is the recommended way for a CustomExecutiontask to wait for and terminate child tasks started viavscode.tasks.executeTask?

💡 Result:

The recommended way for a CustomExecution task to wait for and terminate child tasks started via vscode.tasks.executeTask is to use the global task events and taskExecutions list within the Pseudoterminal implementation. To wait for child tasks: - Call vscode.tasks.executeTask(task) to get the TaskExecution. - Use vscode.tasks.onDidEndTask or onDidEndTaskProcess with a Promise and disposable listener to wait until the specific child task ends, matching by task properties like source, name, definition, or group (noting historical bugs with direct === comparison). Example: const childExecution = await vscode.tasks.executeTask(childTask); return new Promise((resolve, reject) => { const disposable = vscode.tasks.onDidEndTask(e => { if (matchesTask(e.execution.task, childTask)) { // implement matcher disposable.dispose; if (e.execution.exitCode === 0) resolve; else reject; } }); }); To terminate child tasks: - Access vscode.tasks.taskExecutions (array of active TaskExecution). - Iterate and call terminate on matching TaskExecution instances. - Handle in Pseudoterminal.close method for task cancellation. Example: close { for (const exec of vscode.tasks.taskExecutions) { if (matchesTask(exec.task, expectedChildTask)) { exec.terminate; } } this.closeEmitter.fire(0); // or handle appropriately } This leverages the Tasks API events and TaskExecution.terminate method, as shown in StackOverflow examples and official API docs. CustomExecution manages its own logic via Pseudoterminal, so track child executions explicitly if needed. No official docs show executeTask directly in samples, but API supports it fully.

Citations:


Wait for child task completion before closing the composite.

vscode.tasks.executeTask() returns a TaskExecution handle required to track and terminate the child task. The current code discards this handle and fires close(0) immediately after launching children, so the composite reports success before any child finishes. This also breaks parent-side cancellation—the launched tasks cannot be terminated.

Capture the returned TaskExecution from each executeTask() call, listen for task completion using vscode.tasks.onDidEndTask or onDidEndTaskProcess, and close the parent PTY only after all children finish. (VS Code Task Provider API, TaskExecution reference)

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

In `@code/extensions/che-commands/src/compositeTaskBuilder.ts` around lines 190 -
207, The composite PTY currently fires closeEmitter.fire(0) immediately after
launching children; change it to capture the TaskExecution returned by
vscode.tasks.executeTask(childTask) inside the open() of the CustomExecution
(where execs.forEach and buildExecTask(...) are invoked), track those executions
and wait for their termination via vscode.tasks.onDidEndTaskProcess or
vscode.tasks.onDidEndTask, and only call closeEmitter.fire(0) when all tracked
child TaskExecution instances have completed; also wire parent close() to
cancel/terminate any still-running TaskExecution handles to support proper
cancellation and cleanup.

@github-actions
Copy link
Copy Markdown

@msivasubramaniaan
Copy link
Copy Markdown
Collaborator Author

@RomanNikitenko
I have addressed the review comments which you provide, Here are the references of the fixes:

image image

Copy link
Copy Markdown
Collaborator

@RomanNikitenko RomanNikitenko left a comment

Choose a reason for hiding this comment

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

@msivasubramaniaan
I tested commands with env variables, for example go stack uses this approach: https://github.com/devfile/registry/blob/7c358cd8ef31c59111fac73808eaab7b37afd19c/stacks/go/2.6.0/devfile.yaml#L66-L73

It works for regular commands,
but
env variables are not resolved for composite commands

resolving_env_var_bug.mov

@github-actions
Copy link
Copy Markdown

github-actions bot commented Apr 7, 2026

Copy link
Copy Markdown

@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 (3)
code/extensions/che-commands/src/compositeTaskBuilder.ts (3)

126-149: ⚠️ Potential issue | 🟠 Major

Close the sequential aggregator exactly once.

The failure path, close(), Ctrl+C handler, and final success path can all emit onDidClose. If cancellation happens while the active child PTY is still exiting, open() can still fall through to closeEmitter.fire(0) and flip a cancelled composite to success. Add a shared once-guard around every close emission.

Suggested fix
 		const execution = new vscode.CustomExecution(async () => {
 			const writeEmitter = new vscode.EventEmitter<string>();
 			const closeEmitter = new vscode.EventEmitter<number>();
+			let closed = false;
+			const finalize = (exitCode: number) => {
+				if (closed) return;
+				closed = true;
+				closeEmitter.fire(exitCode);
+			};

@@
 						const exitCode: number = await run(e);
 						if(exitCode !== 0) {
 							isCancelled = true;
-							closeEmitter.fire(exitCode);
+							finalize(exitCode);
 							return;
 						}
 					}
-					closeEmitter.fire(0);
+					finalize(isCancelled ? 130 : 0);
 				},
@@
 					activePtys.clear();
-					closeEmitter.fire(130);
+					finalize(130);
 				},
@@
 						activePtys.clear();
-						closeEmitter.fire(130);
+						finalize(130);
 					}
 				},

Also applies to: 152-163

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

In `@code/extensions/che-commands/src/compositeTaskBuilder.ts` around lines 126 -
149, The closeEmitter.fire can be called from multiple places (open() success
path, open() failure path, and close() / Ctrl+C handler) which allows onDidClose
to fire more than once; introduce a shared once-guard (e.g. a boolean like
hasClosed or a single-use function) visible to the entire CompositeTaskBuilder
scope and use it inside close(), the sequential open() loop where
closeEmitter.fire(0) is called, and the error path where
closeEmitter.fire(exitCode) is called so that any call to closeEmitter.fire(...)
first checks/sets the guard and only emits once; update all occurrences
(including the other block around lines 152-163) to use that guard and remove
any duplicate emissions.

97-104: ⚠️ Potential issue | 🟠 Major

env is silently ignored for composite execs.

Both call sites pass e.env into getMachineExecPTY(...), but that API only accepts (containerName, command, workdir) in code/extensions/che-terminal/src/extension.ts:56-66. So composite leaves still bypass the standalone createCheTask(..., env) path in code/extensions/che-commands/src/taskProvider.ts:49-62, and their environment can differ from the same command run outside a composite. Route composite execs through the same builder as standalone tasks, or extend the terminal API explicitly.

#!/bin/bash
set -euo pipefail

echo "== getMachineExecPTY contract =="
rg -n -C2 'getMachineExecPTY\s*\(' code/extensions/che-terminal/src/extension.ts

echo
echo "== compositeTaskBuilder call sites =="
rg -n -C2 'getMachineExecPTY\s*\(' code/extensions/che-commands/src/compositeTaskBuilder.ts

echo
echo "== standalone exec path =="
rg -n -C2 'createCheTask\s*\(' code/extensions/che-commands/src/taskProvider.ts

Also applies to: 242-259

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

In `@code/extensions/che-commands/src/compositeTaskBuilder.ts` around lines 97 -
104, The compositeTaskBuilder's run(ResolvedExec) currently calls
terminalExtAPI.getMachineExecPTY(component, sanitize(command), workdir, env) but
getMachineExecPTY does not accept env, so the exec's environment is silently
dropped; either route composite execs through the same standalone task builder
by invoking the existing createCheTask(...) path used in taskProvider (so the
env is applied consistently), or extend terminalExtAPI.getMachineExecPTY to
accept an env parameter and propagate it into the PTY creation; update all
occurrences of getMachineExecPTY used by composite tasks (including the other
occurrence referenced) to use the chosen approach and ensure ResolvedExec.env is
honored.

198-207: ⚠️ Potential issue | 🔴 Critical

Wait for child tasks before reporting parallel composite success.

open() launches each child via vscode.tasks.executeTask(...) and then immediately closes the parent with exit code 0. close() also returns success without terminating any started TaskExecutions. That lets the composite finish successfully while children are still running, and Ctrl+C / Kill has nothing to cancel. Track the returned executions, wait for their end events, and terminate outstanding ones from close().

In the VS Code Tasks API, when a CustomExecution task starts child tasks with vscode.tasks.executeTask(...), what is the recommended way to wait for those child tasks to finish and terminate them when the parent task is cancelled?
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@code/extensions/che-commands/src/compositeTaskBuilder.ts` around lines 198 -
207, The parent composite currently fires success immediately because open()
calls vscode.tasks.executeTask(...) for each exec in execs but does not wait for
their TaskExecution completions and close() does not terminate running children;
change open() to collect the TaskExecution results (the Thenables returned by
vscode.tasks.executeTask) into an array, create Promises that resolve when
vscode.tasks.onDidEndTask detects each TaskExecution has ended (matching the
TaskExecution object from executeTask), and only fire closeEmitter.fire(0) after
Promise.all of those completions; update close() to iterate remaining unresolved
TaskExecution objects and call their terminate() (or the Thenable’s resolved
TaskExecution.terminate()) to cancel outstanding children; use buildExecTask to
create tasks as before and keep tracking by index or by the TaskExecution
objects returned.
🤖 Prompt for all review comments with AI agents
Verify each finding against the current code and only fix it if needed.

Duplicate comments:
In `@code/extensions/che-commands/src/compositeTaskBuilder.ts`:
- Around line 126-149: The closeEmitter.fire can be called from multiple places
(open() success path, open() failure path, and close() / Ctrl+C handler) which
allows onDidClose to fire more than once; introduce a shared once-guard (e.g. a
boolean like hasClosed or a single-use function) visible to the entire
CompositeTaskBuilder scope and use it inside close(), the sequential open() loop
where closeEmitter.fire(0) is called, and the error path where
closeEmitter.fire(exitCode) is called so that any call to closeEmitter.fire(...)
first checks/sets the guard and only emits once; update all occurrences
(including the other block around lines 152-163) to use that guard and remove
any duplicate emissions.
- Around line 97-104: The compositeTaskBuilder's run(ResolvedExec) currently
calls terminalExtAPI.getMachineExecPTY(component, sanitize(command), workdir,
env) but getMachineExecPTY does not accept env, so the exec's environment is
silently dropped; either route composite execs through the same standalone task
builder by invoking the existing createCheTask(...) path used in taskProvider
(so the env is applied consistently), or extend terminalExtAPI.getMachineExecPTY
to accept an env parameter and propagate it into the PTY creation; update all
occurrences of getMachineExecPTY used by composite tasks (including the other
occurrence referenced) to use the chosen approach and ensure ResolvedExec.env is
honored.
- Around line 198-207: The parent composite currently fires success immediately
because open() calls vscode.tasks.executeTask(...) for each exec in execs but
does not wait for their TaskExecution completions and close() does not terminate
running children; change open() to collect the TaskExecution results (the
Thenables returned by vscode.tasks.executeTask) into an array, create Promises
that resolve when vscode.tasks.onDidEndTask detects each TaskExecution has ended
(matching the TaskExecution object from executeTask), and only fire
closeEmitter.fire(0) after Promise.all of those completions; update close() to
iterate remaining unresolved TaskExecution objects and call their terminate()
(or the Thenable’s resolved TaskExecution.terminate()) to cancel outstanding
children; use buildExecTask to create tasks as before and keep tracking by index
or by the TaskExecution objects returned.

ℹ️ Review info
⚙️ Run configuration

Configuration used: Path: .coderabbit.yaml

Review profile: CHILL

Plan: Pro

Run ID: 5b9a372c-bde7-402a-ba13-9c239aaf4ef1

📥 Commits

Reviewing files that changed from the base of the PR and between 008820f and 9963c38.

📒 Files selected for processing (1)
  • code/extensions/che-commands/src/compositeTaskBuilder.ts

@github-actions
Copy link
Copy Markdown

1 similar comment
@github-actions
Copy link
Copy Markdown

@github-actions
Copy link
Copy Markdown

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

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

3 participants