diff --git a/docs/TEST_INTEGRITY.md b/docs/TEST_INTEGRITY.md index e6e5832..c426613 100644 --- a/docs/TEST_INTEGRITY.md +++ b/docs/TEST_INTEGRITY.md @@ -2268,3 +2268,25 @@ test_integrity_note: did_relax_any_assertion: false if_true_explain_spec_basis: "" ``` +# First live scheduler pilot: native systemd parser (2026-09-30) + +```yaml +test_integrity_note: + change_type: new_tests + scenarios: + - id: SCN-systemd-native-unit + given: "A generated dispatcher with a repository path containing spaces, percent and quote characters" + when: "The real Linux systemd-analyze parser verifies the emitted service" + then: "The service parses successfully and WorkingDirectory remains an absolute scalar path" + observable_outcomes: + - "Actual native parser exit status and generated unit bytes" + determinism_controls: + - "Temporary project path alias and unit file; /usr/bin/true command; verify-only, no service activation" + anti_cheat_rationale: + prevents: + - "Mocked systemctl success masking invalid unit syntax" + - "Treating quoted ExecStart syntax as valid for scalar WorkingDirectory" + relaxation: + did_relax_any_assertion: false + if_true_explain_spec_basis: "" +``` diff --git a/src/schedules/launcher.mjs b/src/schedules/launcher.mjs index d3a3222..d59e9e6 100644 --- a/src/schedules/launcher.mjs +++ b/src/schedules/launcher.mjs @@ -15,6 +15,18 @@ function unitQuote(value) { return `"${String(value).replaceAll('\\', '\\\\').replaceAll('"', '\\"')}"`; } +// WorkingDirectory is a scalar path, not an ExecStart-style quoted word. +// C-escape special path bytes and escape systemd specifiers without adding quotes. +function unitPath(value) { + return [...String(value)].map(character => { + if (character === '%') return '%%'; + if (character.charCodeAt(0) <= 0x20 || character === '\\' || character === '"') { + return `\\x${character.charCodeAt(0).toString(16).padStart(2, '0')}`; + } + return character; + }).join(''); +} + function configPath(repositoryRoot) { return join(repositoryRoot, '.torch', 'torch.yaml'); } @@ -39,7 +51,7 @@ function render({ repositoryRoot, projectId, command, digest, env }) { ? `Environment=${unitQuote(`XDG_DATA_HOME=${resolve(env.XDG_DATA_HOME)}`)}\n` : ''; const service = [ '[Unit]', `Description=TORCH system schedules for ${projectId}`, '', '[Service]', 'Type=oneshot', - `WorkingDirectory=${unitQuote(repositoryRoot)}`, environment.trimEnd(), + `WorkingDirectory=${unitPath(repositoryRoot)}`, environment.trimEnd(), `ExecStart=${args.map(unitQuote).join(' ')}`, '', ].filter((line) => line !== '').join('\n') + '\n'; const timer = [ diff --git a/test/fleet/schedules.test.mjs b/test/fleet/schedules.test.mjs index 536ae04..77e8159 100644 --- a/test/fleet/schedules.test.mjs +++ b/test/fleet/schedules.test.mjs @@ -1,6 +1,6 @@ import assert from 'node:assert/strict'; import { execFileSync, spawnSync } from 'node:child_process'; -import { existsSync, mkdtempSync, readFileSync, writeFileSync } from 'node:fs'; +import { existsSync, mkdtempSync, readFileSync, symlinkSync, writeFileSync } from 'node:fs'; import { tmpdir } from 'node:os'; import { join } from 'node:path'; import test from 'node:test'; @@ -16,6 +16,25 @@ import { loadProjectConfig, validateProjectConfig } from '../../src/kernel/confi import { CheckService } from '../../src/checks/service.mjs'; import { IntegrationService } from '../../src/integration/service.mjs'; import { createWorktrees } from '../../src/kernel/worktrees.mjs'; + +test('SCN-systemd-native-unit: generated dispatcher is accepted by the real systemd parser', (t) => { + if (process.platform !== 'linux') return t.skip('Native systemd qualification is Linux-specific'); + const context = fixture(); + const alias = join(mkdtempSync(join(tmpdir(), 'torch-unit-path-')), 'project space % "quoted"'); + symlinkSync(context.root, alias, 'dir'); + const launcher = new ScheduleLauncherService({ repositoryRoot: alias, env: context.env, command: ['/usr/bin/true'] }); + const plan = launcher.plan(); + const service = plan.files.find(file => file.name.endsWith('.service')); + const directory = mkdtempSync(join(tmpdir(), 'torch-systemd-verify-')); + const path = join(directory, service.name); + writeFileSync(path, service.content); + assert.match(service.content, /WorkingDirectory=\//); + assert.ok(!service.content.includes('WorkingDirectory="')); + assert.ok(service.content.includes('project\\x20space\\x20%%\\x20\\x22quoted\\x22')); + const checked = spawnSync('systemd-analyze', ['--user', 'verify', path], { encoding: 'utf8' }); + assert.equal(checked.error, undefined, checked.error?.message); + assert.equal(checked.status, 0, checked.stderr); +}); import { createRuntimeAdapterRegistry } from '../../src/adapters/registry.mjs'; import { planAreaUp } from '../../src/runtime/lifecycle.mjs'; import { observeProject } from '../../src/observability/snapshot.mjs';