diff --git a/CHAGELOG.md b/CHAGELOG.md index 140f57281..977b69806 100644 --- a/CHAGELOG.md +++ b/CHAGELOG.md @@ -15,6 +15,8 @@ Keep proposed and unmerged changes out of released sections. ## [Unreleased] +- Proposed web terminal integration-test repair ([#1135](https://github.com/BitterSecurity/Decepticon/pull/1135)): wait for the rendered session/composer and verify real PTY draft persistence plus no agent-run submission, instead of waiting forever for the retired `connected` footer label. Keep all timeouts and the actual terminal/server flow. No product behavior change or release. + - Proposed workflow declared-edge commit validation ([#1131](https://github.com/BitterSecurity/Decepticon/pull/1131)): reject OPPLAN commits whose executable leaves declare no DAG edge (blocked_by, any_of or required_fact_ids) into stages named by the workflow's `depends_on` closure. The proof gate's reachable-leaf rule can never be satisfied for such edge-less plans, so they committed successfully and then blocked every objective transition and specialist dispatch with `workflow prerequisite stage incomplete on dependency path: ` — observed live on 2.2.0 where an edge-less red-team plan completed its scope objective and then could not start the discovery stage, could not repair itself through `commit_opplan` (completed objectives are immutable) and burned the recursion budget in a rejection loop. The gate's proof semantics are unchanged; the failure now surfaces at commit time with an actionable message naming the objective, its stage and the missing prerequisite stages. - Proposed step-budget async middleware fix ([#1130](https://github.com/BitterSecurity/Decepticon/pull/1130)): add `awrap_model_call` to `StepBudgetMiddleware` with a shared `_budget_note` helper so the sync and async paths stay identical. On 2.2.0, any engagement run invoked asynchronously (LangGraph background runs) crashed in the `model` node with `NotImplementedError: Asynchronous implementation of awrap_model_call is not available`, because `decepticon/middleware/step_budget.py` defined only the sync `wrap_model_call` and langchain's `AgentMiddleware` base raises on the missing async variant. Verified against a live 2.2.0 stack: an MCP `start_engagement` run failed in ~1.2 s before the first model call; with the patch the same call reaches the model and runs normally. Add async unit tests mirroring the sync budget cases (warn at 80%, hard stop at 100%, silent under budget, sync/async parity). diff --git a/clients/web/src/lib/terminal-cli-boot.test.ts b/clients/web/src/lib/terminal-cli-boot.test.ts index 9f41f4df0..152c712f0 100644 --- a/clients/web/src/lib/terminal-cli-boot.test.ts +++ b/clients/web/src/lib/terminal-cli-boot.test.ts @@ -11,7 +11,7 @@ import { fileURLToPath } from "node:url"; import { stripVTControlCharacters } from "node:util"; import { WebSocket } from "ws"; -test("web terminal boots the real CLI and renders its connected composer", { timeout: 120_000 }, async () => { +test("web terminal boots the real CLI and persists PTY input without starting a run", { timeout: 120_000 }, async () => { const root = await mkdtemp(join(tmpdir(), "terminal-cli-boot-")); await mkdir(join(root, "workspace", "loader-test", "plan"), { recursive: true }); await writeFile(join(root, "workspace", "loader-test", "plan", "roe.json"), JSON.stringify({ machine_enforcement: { in_scope: ["example.com"] } })); @@ -60,10 +60,10 @@ test("web terminal boots the real CLI and renders its connected composer", { tim socket!.on("message", data => { output += data.toString(); const plain = stripVTControlCharacters(output); - if (plain.includes("connected") && plain.includes("Enter send")) { clearTimeout(timer); resolve(plain); } + if (plain.includes("loader-test") && plain.includes("Enter send")) { clearTimeout(timer); resolve(plain); } }); socket!.once("error", error => { clearTimeout(timer); reject(error); }); - socket!.once("close", () => { clearTimeout(timer); reject(new Error(stripVTControlCharacters(output).slice(-1200))); }); + socket!.once("close", (code, reason) => { clearTimeout(timer); reject(new Error(`Terminal closed (${code}: ${reason.toString()}): ${stripVTControlCharacters(output).slice(-1200) || serverOutput}`)); }); }); assert.match(text, /loader-test/); assert.doesNotMatch(text, /SyntaxError|Unexpected token|EACCES/);