// 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`, ); } });