// Tests for the `--auto` opt-OUT marker (issue #779, harness jwulf/c8ctl-plugin-nano#235). The single
// agentic-task signal the harness `--auto` reconciliation scans is ``; this marker — `` inside an agent task's extensionElements — is the explicit escape hatch that
// EXCLUDES a task from `--auto` so it is served only by a worker that explicitly subscribes. Mirrors
// agent-marker.test.ts: assert the marker's shape/placement, and guard that any opted-out task in the
// deployed models is itself a real (externally-marked) agent task, so the opt-out can't drift onto a
// non-agent element.
import { readFileSync, readdirSync } from "node:fs";
import { test } from "node:test";
import { dirname, join, relative } from "node:path";
import { fileURLToPath } from "node:url";
import { assert, assertEquals } from "#test-assert";
import {
agentTaskTypesMissingExternalMarker,
agentTaskTypesOptedOutMissingExternalMarker,
agentTaskTypesOptedOutOfAuto,
MALFORMED_OPTOUT_LABEL,
} from "./job-types.ts";
const RESOURCES_DIR = join(dirname(fileURLToPath(import.meta.url)), "../../../resources");
// urban deploys `resources/` recursively (every file at ANY depth), so an opted-out task in a `.bpmn`
// added under any resources subdirectory — not just `resources/processes` — would still deploy. Root
// the walk at the `resources/` convention root (mirroring the deploy contract) so a deployed BPMN
// placed elsewhere under `resources/` cannot bypass this guard.
function bpmnFiles(): string[] {
const walk = (dir: string): string[] =>
readdirSync(dir, { withFileTypes: true }).flatMap((entry) => {
const full = join(dir, entry.name);
if (entry.isDirectory()) return walk(full);
return entry.name.endsWith(".bpmn") ? [relative(RESOURCES_DIR, full)] : [];
});
return walk(RESOURCES_DIR).sort();
}
test("agentTaskTypesOptedOutOfAuto flags a task carrying the value=\"false\" opt-out property", () => {
const xml = `
`;
assertEquals(agentTaskTypesOptedOutOfAuto(xml), ["senior:special"]);
});
test("agentTaskTypesOptedOutOfAuto tolerates reversed attribute order (name/value swapped)", () => {
const xml = `
`;
assertEquals(agentTaskTypesOptedOutOfAuto(xml), ["senior:special"]);
});
test("agentTaskTypesOptedOutOfAuto ignores a task without the marker and one with a non-false value", () => {
const xml = `
`;
assertEquals(agentTaskTypesOptedOutOfAuto(xml), []);
});
test("agentTaskTypesOptedOutOfAuto ignores an unrelated property named the same-ish", () => {
const xml = `
`;
assertEquals(agentTaskTypesOptedOutOfAuto(xml), []);
});
test("agentTaskTypesOptedOutOfAuto ignores a bare opt-out property OUTSIDE the wrapper (placement contract)", () => {
// The property carries the exact name/value, but it sits directly under
// rather than inside the wrapper the engine honours — so the engine ignores it
// and it is NOT an active opt-out. Both the reader and the drift guard must treat it as absent.
const xml = `
`;
assertEquals(agentTaskTypesOptedOutOfAuto(xml), []);
assertEquals(agentTaskTypesOptedOutMissingExternalMarker(xml), []);
});
test("agentTaskTypesMissingExternalMarker flags a prompt-bearing task whose external marker sits OUTSIDE extensionElements (placement contract)", () => {
// The task is a real prompt-bearing agent task, but its marker is a sibling
// of rather than inside extensionElements, so the engine/harness ignores it — the
// task is effectively unmarked. A whole-block scan would see the marker "somewhere" and wrongly
// pass; the placement-scoped scan flags the drift.
const xml = `
`;
assertEquals(agentTaskTypesMissingExternalMarker(xml), ["senior:special"]);
});
test("agentTaskTypesOptedOutMissingExternalMarker flags an opt-out on a block lacking the external marker", () => {
// A host task (no external marker, no prompt link) that carries the opt-out is authoring drift:
// the block-level check catches it even though `agentTaskTypesMissingExternalMarker` (prompt-only)
// never reports it.
const xml = `
`;
assertEquals(agentTaskTypesMissingExternalMarker(xml), []);
assertEquals(agentTaskTypesOptedOutMissingExternalMarker(xml), ["pr.finalize"]);
});
test("agentTaskTypesOptedOutMissingExternalMarker passes an opt-out on an externally-marked agent task", () => {
const xml = `
`;
assertEquals(agentTaskTypesOptedOutMissingExternalMarker(xml), []);
});
test("agentTaskTypesOptedOutMissingExternalMarker checks each block independently (a marked sibling does not cover a drifted opt-out)", () => {
// Two tasks share the `senior:special` type: one is a proper externally-marked agent task, the
// other opts out but lacks the marker. A deduplicated cross-task comparison would miss this; the
// per-block check flags the unmarked one.
const xml = `
`;
assertEquals(agentTaskTypesOptedOutMissingExternalMarker(xml), ["senior:special"]);
});
test("agentTaskTypesOptedOutMissingExternalMarker flags an opt-out whose external marker sits OUTSIDE extensionElements (placement contract)", () => {
// The opt-out is correctly placed inside extensionElements, but the external marker is out of place
// (a sibling of , not inside extensionElements) so the engine ignores it — the block
// is therefore NOT a real marked agent task. A whole-block scan would see the marker "somewhere" and
// wrongly pass; the placement-scoped scan flags the drift.
const xml = `
`;
assertEquals(agentTaskTypesOptedOutMissingExternalMarker(xml), ["senior:special"]);
});
test("agentTaskTypesOptedOutMissingExternalMarker surfaces an opt-out on a block with a missing/empty taskDefinition type", () => {
// An opt-out on an unmarked block whose type is empty cannot be a real agent
// task, so it is still drift — surfaced under the sentinel rather than silently skipped.
const xml = `
`;
assertEquals(agentTaskTypesOptedOutMissingExternalMarker(xml), [MALFORMED_OPTOUT_LABEL]);
});
test("agentTaskTypesOptedOutMissingExternalMarker surfaces a MARKED opt-out block with a missing/empty taskDefinition type", () => {
// The block carries BOTH the external marker AND the opt-out, but its type
// is empty — so it still cannot be a real agent task. The malformed check must run BEFORE the
// external-marker short-circuit, or the marker would wrongly let this typeless opt-out pass.
const xml = `
`;
assertEquals(agentTaskTypesOptedOutMissingExternalMarker(xml), [MALFORMED_OPTOUT_LABEL]);
});
test("GUARD: the deploy-scan walks from the resources/ convention root (not just resources/processes)", () => {
// Regression for the deploy-by-convention coverage gap: urban deploys `resources/` recursively, so
// the guard must root its BPMN walk at `resources/` — a `.bpmn` added under any other resources
// subdirectory must still be scanned. Assert the walk actually reaches the process models AND that
// every returned path is relative to the resources root (carries its subdirectory segment), so a
// future refactor that narrows the root back to `resources/processes` is caught here.
const files = bpmnFiles();
assert(files.length > 0, "expected the resources/ walk to discover deployed BPMN models");
assert(
files.some((f) => f.startsWith("processes/")),
`expected resources-root-relative paths (e.g. "processes/…"); got ${JSON.stringify(files.slice(0, 3))}`,
);
});
test("GUARD: every deployed opted-out task is itself an externally-marked agent task", () => {
for (const file of bpmnFiles()) {
const xml = readFileSync(join(RESOURCES_DIR, file), "utf8");
// Drive the guard DIRECTLY from the block-level, placement-scoped helper rather than gating on
// `agentTaskTypesOptedOutOfAuto` (which skips missing/empty task-definition types, so a typeless
// opt-out would never reach the check). The helper scans every service task itself, surfaces a
// malformed typeless opt-out under the sentinel, and checks the external marker on the SAME
// block's extensionElements — so a typeless opt-out, an out-of-place property, or a marker that
// drifted onto a non-agent element all fail CI here.
const drifted = agentTaskTypesOptedOutMissingExternalMarker(xml);
assertEquals(
drifted,
[],
`${file}: task(s) ${JSON.stringify(drifted)} opt out of --auto but lack on the same block — an opt-out belongs only on a real agent task`,
);
}
});