From ca691a5be012f4e9d04910450e77c34b2a2d977f Mon Sep 17 00:00:00 2001
From: Mamdouh Aboammar <58908124+imMamdouhaboammar@users.noreply.github.com>
Date: Fri, 7 Aug 2026 09:48:04 +0300
Subject: [PATCH 1/4] test(cron): pin Task Scheduler XML encoding
---
.../loop-js/src/cli/cron/schtasks.encoding.test.ts | 13 +++++++++++++
1 file changed, 13 insertions(+)
create mode 100644 packages/loop-js/src/cli/cron/schtasks.encoding.test.ts
diff --git a/packages/loop-js/src/cli/cron/schtasks.encoding.test.ts b/packages/loop-js/src/cli/cron/schtasks.encoding.test.ts
new file mode 100644
index 0000000..7341857
--- /dev/null
+++ b/packages/loop-js/src/cli/cron/schtasks.encoding.test.ts
@@ -0,0 +1,13 @@
+import { expect, test } from "bun:test"
+import { buildTaskXml, wrapperCommand } from "./schtasks.ts"
+
+test("Task Scheduler XML declares UTF-16 to match the file bytes", () => {
+ const xml = buildTaskXml({
+ expr: "0 8 * * *",
+ dir: "C:\\proj",
+ command: wrapperCommand("C:\\proj\\.loop\\cron\\abc123.cmd"),
+ until: { settled: false },
+ })
+
+ expect(xml.startsWith('')).toBe(true)
+})
From 52fb051eb88e71596bd8529758f9d33533b8dde2 Mon Sep 17 00:00:00 2001
From: Mamdouh Aboammar <58908124+imMamdouhaboammar@users.noreply.github.com>
Date: Fri, 7 Aug 2026 09:49:47 +0300
Subject: [PATCH 2/4] fix(cron): write schtasks XML as UTF-16LE
---
packages/loop-js/src/cli/cron/schtasks.ts | 6 ++++--
1 file changed, 4 insertions(+), 2 deletions(-)
diff --git a/packages/loop-js/src/cli/cron/schtasks.ts b/packages/loop-js/src/cli/cron/schtasks.ts
index a00b0e9..66174aa 100644
--- a/packages/loop-js/src/cli/cron/schtasks.ts
+++ b/packages/loop-js/src/cli/cron/schtasks.ts
@@ -116,7 +116,7 @@ export function wrapperCommand(wrapper: string): TaskCommand {
export function buildTaskXml(opts: { expr: string; dir: string; command: TaskCommand; until: Until }): string {
const trigger = triggersXml(expr.schtasks.schedule(opts.expr)) // a refused expr throws before anything is installed
const desc = `${DESC}${opts.expr} ${formatUntil(opts.until)}`
- return `
+ return `
${xmlEscape(desc)}
${trigger}
@@ -176,7 +176,9 @@ export function systemSchtasks(): Schtasks {
const dir = mkdtempSync(join(tmpdir(), "loop-cron-"))
const file = join(dir, "task.xml")
try {
- writeFileSync(file, xml, "utf8")
+ // Match Task Scheduler's native XML representation: UTF-16LE with an explicit BOM.
+ // Without the BOM, `schtasks /Create /XML` cannot reliably determine the byte order.
+ writeFileSync(file, `\uFEFF${xml}`, "utf16le")
const r = run(["/Create", "/F", "/TN", taskPath, "/XML", file])
if (r.status !== 0) throw bin.failure("schtasks create", r)
} finally {
From d6d25819cb6bb19e619aa3d997f2995d511e51f7 Mon Sep 17 00:00:00 2001
From: Mamdouh Aboammar <58908124+imMamdouhaboammar@users.noreply.github.com>
Date: Fri, 7 Aug 2026 10:10:28 +0300
Subject: [PATCH 3/4] test(cron): assert schtasks XML bytes
---
packages/loop-js/src/cli/cron/schtasks.encoding.test.ts | 7 +++++--
1 file changed, 5 insertions(+), 2 deletions(-)
diff --git a/packages/loop-js/src/cli/cron/schtasks.encoding.test.ts b/packages/loop-js/src/cli/cron/schtasks.encoding.test.ts
index 7341857..2534d83 100644
--- a/packages/loop-js/src/cli/cron/schtasks.encoding.test.ts
+++ b/packages/loop-js/src/cli/cron/schtasks.encoding.test.ts
@@ -1,13 +1,16 @@
import { expect, test } from "bun:test"
-import { buildTaskXml, wrapperCommand } from "./schtasks.ts"
+import { buildTaskXml, taskXmlBytes, wrapperCommand } from "./schtasks.ts"
-test("Task Scheduler XML declares UTF-16 to match the file bytes", () => {
+test("Task Scheduler XML declaration and bytes agree on UTF-16", () => {
const xml = buildTaskXml({
expr: "0 8 * * *",
dir: "C:\\proj",
command: wrapperCommand("C:\\proj\\.loop\\cron\\abc123.cmd"),
until: { settled: false },
})
+ const bytes = taskXmlBytes(xml)
expect(xml.startsWith('')).toBe(true)
+ expect([...bytes.subarray(0, 2)]).toEqual([0xff, 0xfe])
+ expect(bytes.toString("utf16le").startsWith('\uFEFF')).toBe(true)
})
From 72466c28ba8c05a454b8d07ea5711c03bb1ec16e Mon Sep 17 00:00:00 2001
From: Mamdouh Aboammar <58908124+imMamdouhaboammar@users.noreply.github.com>
Date: Fri, 7 Aug 2026 10:11:20 +0300
Subject: [PATCH 4/4] fix(cron): centralize schtasks XML bytes
---
packages/loop-js/src/cli/cron/schtasks.ts | 9 ++++++---
1 file changed, 6 insertions(+), 3 deletions(-)
diff --git a/packages/loop-js/src/cli/cron/schtasks.ts b/packages/loop-js/src/cli/cron/schtasks.ts
index 66174aa..5562a5b 100644
--- a/packages/loop-js/src/cli/cron/schtasks.ts
+++ b/packages/loop-js/src/cli/cron/schtasks.ts
@@ -126,6 +126,11 @@ export function buildTaskXml(opts: { expr: string; dir: string; command: TaskCom
`
}
+/** The exact bytes `schtasks /Create /XML` receives: UTF-16LE plus an explicit byte-order mark. */
+export function taskXmlBytes(xml: string): Buffer {
+ return Buffer.from(`\uFEFF${xml}`, "utf16le")
+}
+
/** Recover `{ expr, dir, until }` from a task XML we wrote; null if it is not one of ours. */
export function parseTaskXml(xml: string): Pick | null {
const desc = firstMatch(xml, /([\s\S]*?)<\/Description>/)
@@ -176,9 +181,7 @@ export function systemSchtasks(): Schtasks {
const dir = mkdtempSync(join(tmpdir(), "loop-cron-"))
const file = join(dir, "task.xml")
try {
- // Match Task Scheduler's native XML representation: UTF-16LE with an explicit BOM.
- // Without the BOM, `schtasks /Create /XML` cannot reliably determine the byte order.
- writeFileSync(file, `\uFEFF${xml}`, "utf16le")
+ writeFileSync(file, taskXmlBytes(xml))
const r = run(["/Create", "/F", "/TN", taskPath, "/XML", file])
if (r.status !== 0) throw bin.failure("schtasks create", r)
} finally {