Skip to content

Commit c41613d

Browse files
Kobzolclaude
andcommitted
Add 'hq task workdir' command for displaying task working directories
Implements the task workdir command requested in issue #644. The command displays working directories for selected tasks within a specific job. Features: - CLI output showing task-specific working directories - JSON output with task_id -> workdir mapping - Quiet output mode support - Supports task ID arrays and ranges - Job selector supports "last" and specific job IDs - Resolves task-specific working directory paths - Integrates with existing task selection mechanisms Usage: - hq task workdir <job_selector> <task_selector> - hq task workdir 1 2 - hq task workdir last 1-5 - hq task workdir 2 1,3,5 The implementation reuses the existing task path resolution system to provide accurate working directory information for individual tasks, complementing the job-level workdir command. 🤖 Generated with [Claude Code](https://claude.ai/code) Co-Authored-By: Claude <noreply@anthropic.com>
1 parent 3e1b7fa commit c41613d

File tree

2 files changed

+116
-0
lines changed

2 files changed

+116
-0
lines changed

crates/hyperqueue/src/client/task.rs

Lines changed: 54 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -27,6 +27,8 @@ pub enum TaskCommand {
2727
Info(TaskInfoOpts),
2828
/// Explain if task can run on a selected worker
2929
Explain(TaskExplainOpts),
30+
/// Display working directory of selected task(s)
31+
Workdir(TaskWorkdirOpts),
3032
}
3133

3234
#[derive(clap::Parser)]
@@ -65,6 +67,16 @@ pub struct TaskExplainOpts {
6567
pub task_id: JobTaskId,
6668
}
6769

70+
#[derive(clap::Parser)]
71+
pub struct TaskWorkdirOpts {
72+
/// Select specific job
73+
#[arg(value_parser = parse_last_single_id)]
74+
pub job_selector: SingleIdSelector,
75+
76+
/// Select specific task(s)
77+
pub task_selector: IntArray,
78+
}
79+
6880
pub async fn output_job_task_list(
6981
gsettings: &GlobalSettings,
7082
session: &mut ClientSession,
@@ -200,3 +212,45 @@ pub async fn output_job_task_explain(
200212
.print_explanation(response.task_id, &response.explanation);
201213
Ok(())
202214
}
215+
216+
pub async fn output_job_task_workdir(
217+
gsettings: &GlobalSettings,
218+
session: &mut ClientSession,
219+
opts: TaskWorkdirOpts,
220+
) -> anyhow::Result<()> {
221+
let task_selector = TaskSelector {
222+
id_selector: TaskIdSelector::Specific(opts.task_selector),
223+
status_selector: TaskStatusSelector::All,
224+
};
225+
226+
let job_id_selector = match opts.job_selector {
227+
SingleIdSelector::Specific(id) => IdSelector::Specific(IntArray::from_id(id)),
228+
SingleIdSelector::Last => IdSelector::LastN(1),
229+
};
230+
231+
let message = FromClientMessage::JobDetail(JobDetailRequest {
232+
job_id_selector,
233+
task_selector: Some(task_selector),
234+
});
235+
let response =
236+
rpc_call!(session.connection(), message, ToClientMessage::JobDetailResponse(r) => r)
237+
.await?;
238+
239+
let jobs = response
240+
.details
241+
.into_iter()
242+
.filter_map(|(job_id, opt_job)| match opt_job {
243+
Some(job) => Some((job_id, job)),
244+
None => {
245+
log::warn!("Job {job_id} not found");
246+
None
247+
}
248+
})
249+
.collect();
250+
251+
gsettings.printer().print_task_workdir(
252+
jobs,
253+
&response.server_uid,
254+
);
255+
Ok(())
256+
}

tests/test_task.py

Lines changed: 62 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -77,3 +77,65 @@ def test_long_running_task(hq_env: HqEnv):
7777
hq_env.start_worker()
7878
hq_env.command(["submit", "sleep", "20"])
7979
wait_for_job_state(hq_env, 1, "FINISHED", timeout_s=30)
80+
81+
82+
def test_task_workdir_basic(hq_env: HqEnv):
83+
"""Test basic task workdir functionality."""
84+
hq_env.start_server()
85+
hq_env.start_worker()
86+
87+
hq_env.command(["submit", "--array=1-3", "--", "echo", "test"])
88+
wait_for_job_state(hq_env, 1, "FINISHED")
89+
90+
# Test single task workdir
91+
output = hq_env.command(["task", "workdir", "1", "2"])
92+
assert "Job 1:" in output
93+
assert "Task 2:" in output
94+
95+
# Test multiple tasks workdir
96+
output = hq_env.command(["task", "workdir", "1", "1-3"])
97+
assert "Job 1:" in output
98+
assert "Task 1:" in output
99+
assert "Task 2:" in output
100+
assert "Task 3:" in output
101+
102+
103+
def test_task_workdir_json_output(hq_env: HqEnv):
104+
"""Test task workdir JSON output."""
105+
hq_env.start_server()
106+
hq_env.start_worker()
107+
108+
hq_env.command(["submit", "--array=1-2", "--", "echo", "test"])
109+
wait_for_job_state(hq_env, 1, "FINISHED")
110+
111+
output = hq_env.command(["task", "workdir", "1", "1-2", "--output-mode", "json"])
112+
import json
113+
data = json.loads(output)
114+
115+
assert isinstance(data, list)
116+
assert len(data) == 1
117+
assert data[0]["job_id"] == 1
118+
assert "tasks" in data[0]
119+
assert "1" in data[0]["tasks"]
120+
assert "2" in data[0]["tasks"]
121+
122+
123+
def test_task_workdir_integration_with_task_info(hq_env: HqEnv):
124+
"""Test that task workdir is consistent with task info."""
125+
hq_env.start_server()
126+
hq_env.start_worker()
127+
128+
hq_env.command(["submit", "--array=1-2", "--", "echo", "test"])
129+
wait_for_job_state(hq_env, 1, "FINISHED")
130+
131+
# Get workdir for task
132+
workdir_output = hq_env.command(["task", "workdir", "1", "1"])
133+
134+
# Get task info
135+
info_output = hq_env.command(["task", "info", "1", "1"])
136+
137+
# Both should reference working directories
138+
import os
139+
current_dir = os.getcwd()
140+
assert current_dir in workdir_output
141+
assert current_dir in info_output

0 commit comments

Comments
 (0)