|
1 | 1 | import fs from "node:fs/promises"; |
2 | 2 | import os from "node:os"; |
3 | 3 | import path from "node:path"; |
4 | | -import { describe, expect, it } from "vitest"; |
| 4 | +import { beforeEach, describe, expect, it, vi } from "vitest"; |
5 | 5 | import { |
6 | | - deriveScheduledTaskRuntimeStatus, |
7 | 6 | parseSchtasksQuery, |
8 | 7 | readScheduledTaskCommand, |
| 8 | + readScheduledTaskRuntime, |
9 | 9 | resolveTaskScriptPath, |
10 | 10 | } from "./schtasks.js"; |
11 | 11 |
|
| 12 | +const schtasksResponses = vi.hoisted( |
| 13 | + (): Array<{ code: number; stdout: string; stderr: string }> => [], |
| 14 | +); |
| 15 | + |
| 16 | +vi.mock("./schtasks-exec.js", () => ({ |
| 17 | + execSchtasks: async () => schtasksResponses.shift() ?? { code: 0, stdout: "", stderr: "" }, |
| 18 | +})); |
| 19 | + |
| 20 | +beforeEach(() => { |
| 21 | + schtasksResponses.length = 0; |
| 22 | +}); |
| 23 | + |
12 | 24 | describe("schtasks runtime parsing", () => { |
13 | 25 | it.each(["Ready", "Running"])("parses %s status", (status) => { |
14 | 26 | const output = [ |
@@ -40,83 +52,85 @@ describe("schtasks runtime parsing", () => { |
40 | 52 | }); |
41 | 53 |
|
42 | 54 | describe("scheduled task runtime derivation", () => { |
43 | | - it("treats Running + 0x41301 as running", () => { |
44 | | - expect( |
45 | | - deriveScheduledTaskRuntimeStatus({ |
46 | | - status: "Running", |
47 | | - lastRunResult: "0x41301", |
48 | | - }), |
49 | | - ).toEqual({ status: "running" }); |
| 55 | + async function readRuntimeFromQueryOutput(output: string) { |
| 56 | + schtasksResponses.push( |
| 57 | + { code: 0, stdout: "", stderr: "" }, |
| 58 | + { code: 0, stdout: output, stderr: "" }, |
| 59 | + ); |
| 60 | + return await readScheduledTaskRuntime({ |
| 61 | + USERPROFILE: "C:\\Users\\test", |
| 62 | + OPENCLAW_PROFILE: "default", |
| 63 | + }); |
| 64 | + } |
| 65 | + |
| 66 | + function taskQueryOutput(lines: string[]): string { |
| 67 | + return [ |
| 68 | + "TaskName: \\OpenClaw Gateway", |
| 69 | + "Last Run Time: 1/8/2026 1:23:45 AM", |
| 70 | + ...lines, |
| 71 | + "", |
| 72 | + ].join("\r\n"); |
| 73 | + } |
| 74 | + |
| 75 | + it("treats Running + 0x41301 as running", async () => { |
| 76 | + await expect( |
| 77 | + readRuntimeFromQueryOutput(taskQueryOutput(["Status: Running", "Last Run Result: 0x41301"])), |
| 78 | + ).resolves.toMatchObject({ status: "running" }); |
50 | 79 | }); |
51 | 80 |
|
52 | | - it("treats Running + decimal 267009 as running", () => { |
53 | | - expect( |
54 | | - deriveScheduledTaskRuntimeStatus({ |
55 | | - status: "Running", |
56 | | - lastRunResult: "267009", |
57 | | - }), |
58 | | - ).toEqual({ status: "running" }); |
| 81 | + it("treats Running + decimal 267009 as running", async () => { |
| 82 | + await expect( |
| 83 | + readRuntimeFromQueryOutput(taskQueryOutput(["Status: Running", "Last Run Result: 267009"])), |
| 84 | + ).resolves.toMatchObject({ status: "running" }); |
59 | 85 | }); |
60 | 86 |
|
61 | | - it("treats Running without numeric result as unknown", () => { |
62 | | - expect( |
63 | | - deriveScheduledTaskRuntimeStatus({ |
64 | | - status: "Running", |
65 | | - }), |
66 | | - ).toEqual({ |
| 87 | + it("treats Running without numeric result as unknown", async () => { |
| 88 | + await expect( |
| 89 | + readRuntimeFromQueryOutput(taskQueryOutput(["Status: Running"])), |
| 90 | + ).resolves.toMatchObject({ |
67 | 91 | status: "unknown", |
68 | 92 | detail: "Task status is locale-dependent and no numeric Last Run Result was available.", |
69 | 93 | }); |
70 | 94 | }); |
71 | 95 |
|
72 | | - it("treats non-running result codes as stopped", () => { |
73 | | - expect( |
74 | | - deriveScheduledTaskRuntimeStatus({ |
75 | | - status: "Running", |
76 | | - lastRunResult: "0x0", |
77 | | - }), |
78 | | - ).toEqual({ |
| 96 | + it("treats non-running result codes as stopped", async () => { |
| 97 | + await expect( |
| 98 | + readRuntimeFromQueryOutput(taskQueryOutput(["Status: Running", "Last Run Result: 0x0"])), |
| 99 | + ).resolves.toMatchObject({ |
79 | 100 | status: "stopped", |
80 | 101 | detail: "Task Last Run Result=0x0; treating as not running.", |
81 | 102 | }); |
82 | 103 | }); |
83 | 104 |
|
84 | | - it("detects running via result code when status is localized (German)", () => { |
85 | | - expect( |
86 | | - deriveScheduledTaskRuntimeStatus({ |
87 | | - status: "Wird ausgeführt", |
88 | | - lastRunResult: "0x41301", |
89 | | - }), |
90 | | - ).toEqual({ status: "running" }); |
| 105 | + it("detects running via result code when status is localized (German)", async () => { |
| 106 | + await expect( |
| 107 | + readRuntimeFromQueryOutput( |
| 108 | + taskQueryOutput(["Status: Wird ausgeführt", "Last Run Result: 0x41301"]), |
| 109 | + ), |
| 110 | + ).resolves.toMatchObject({ status: "running" }); |
91 | 111 | }); |
92 | 112 |
|
93 | | - it("detects running via result code when status is localized (French)", () => { |
94 | | - expect( |
95 | | - deriveScheduledTaskRuntimeStatus({ |
96 | | - status: "En cours", |
97 | | - lastRunResult: "267009", |
98 | | - }), |
99 | | - ).toEqual({ status: "running" }); |
| 113 | + it("detects running via result code when status is localized (French)", async () => { |
| 114 | + await expect( |
| 115 | + readRuntimeFromQueryOutput(taskQueryOutput(["Status: En cours", "Last Run Result: 267009"])), |
| 116 | + ).resolves.toMatchObject({ status: "running" }); |
100 | 117 | }); |
101 | 118 |
|
102 | | - it("treats localized status as stopped when result code is not a running code", () => { |
103 | | - expect( |
104 | | - deriveScheduledTaskRuntimeStatus({ |
105 | | - status: "Wird ausgeführt", |
106 | | - lastRunResult: "0x0", |
107 | | - }), |
108 | | - ).toEqual({ |
| 119 | + it("treats localized status as stopped when result code is not a running code", async () => { |
| 120 | + await expect( |
| 121 | + readRuntimeFromQueryOutput( |
| 122 | + taskQueryOutput(["Status: Wird ausgeführt", "Last Run Result: 0x0"]), |
| 123 | + ), |
| 124 | + ).resolves.toMatchObject({ |
109 | 125 | status: "stopped", |
110 | 126 | detail: "Task Last Run Result=0x0; treating as not running.", |
111 | 127 | }); |
112 | 128 | }); |
113 | 129 |
|
114 | | - it("treats localized status without result code as unknown", () => { |
115 | | - expect( |
116 | | - deriveScheduledTaskRuntimeStatus({ |
117 | | - status: "Wird ausgeführt", |
118 | | - }), |
119 | | - ).toEqual({ |
| 130 | + it("treats localized status without result code as unknown", async () => { |
| 131 | + await expect( |
| 132 | + readRuntimeFromQueryOutput(taskQueryOutput(["Status: Wird ausgeführt"])), |
| 133 | + ).resolves.toMatchObject({ |
120 | 134 | status: "unknown", |
121 | 135 | detail: "Task status is locale-dependent and no numeric Last Run Result was available.", |
122 | 136 | }); |
@@ -149,9 +163,32 @@ describe("resolveTaskScriptPath", () => { |
149 | 163 | env: { HOME: "/home/test", OPENCLAW_PROFILE: "default" }, |
150 | 164 | expected: path.join("/home/test", ".openclaw", "gateway.cmd"), |
151 | 165 | }, |
| 166 | + { |
| 167 | + name: "uses a custom task script file name inside the state directory", |
| 168 | + env: { |
| 169 | + USERPROFILE: "C:\\Users\\test", |
| 170 | + OPENCLAW_TASK_SCRIPT_NAME: "gateway-node.cmd", |
| 171 | + }, |
| 172 | + expected: path.join("C:\\Users\\test", ".openclaw", "gateway-node.cmd"), |
| 173 | + }, |
152 | 174 | ])("$name", ({ env, expected }) => { |
153 | 175 | expect(resolveTaskScriptPath(env)).toBe(expected); |
154 | 176 | }); |
| 177 | + |
| 178 | + it.each([ |
| 179 | + "../gateway.cmd", |
| 180 | + "..\\gateway.cmd", |
| 181 | + "nested/gateway.cmd", |
| 182 | + "nested\\gateway.cmd", |
| 183 | + "gateway..cmd", |
| 184 | + ])("rejects non-file task script name %s", (scriptName) => { |
| 185 | + expect(() => |
| 186 | + resolveTaskScriptPath({ |
| 187 | + USERPROFILE: "C:\\Users\\test", |
| 188 | + OPENCLAW_TASK_SCRIPT_NAME: scriptName, |
| 189 | + }), |
| 190 | + ).toThrow("OPENCLAW_TASK_SCRIPT_NAME must be a file name only"); |
| 191 | + }); |
155 | 192 | }); |
156 | 193 |
|
157 | 194 | describe("readScheduledTaskCommand", () => { |
|
0 commit comments