diff --git a/.github/workflows/agent-e2e.yml b/.github/workflows/agent-e2e.yml index de362d9c..081842e2 100644 --- a/.github/workflows/agent-e2e.yml +++ b/.github/workflows/agent-e2e.yml @@ -19,7 +19,7 @@ concurrency: cancel-in-progress: true env: - CONDUCTOR_OSS_VERSION: "3.32.0-rc.8" # pinned conductor-oss release — bump deliberately + CONDUCTOR_OSS_VERSION: "3.32.0-rc18" # pinned conductor-oss release — bump deliberately jobs: agent-e2e: diff --git a/e2e/test_suite16_streaming.test.ts b/e2e/test_suite16_streaming.test.ts index 94cfbefb..7152a137 100644 --- a/e2e/test_suite16_streaming.test.ts +++ b/e2e/test_suite16_streaming.test.ts @@ -274,10 +274,16 @@ describe('Suite 16: Streaming — HITL', () => { } expectMsg(['COMPLETED', 'FAILED', 'TERMINATED']).toContain(status.status); } else { - // No waiting event — workflow completed without HITL (possible with some models) + // No waiting event — the stream may close before the server records its + // terminal state, so wait for that state rather than reading it once. const terminalSeen = preTypes.includes('done') || preTypes.includes('error'); if (!terminalSeen) { - const status = await runtime.getStatus(stream.executionId); + const deadline = Date.now() + 120_000; + let status = await runtime.getStatus(stream.executionId); + while (!status.isComplete && Date.now() < deadline) { + await new Promise((r) => setTimeout(r, 1000)); + status = await runtime.getStatus(stream.executionId); + } expect(status.isComplete).toBe(true); } } @@ -310,10 +316,16 @@ describe('Suite 16: Streaming — HITL', () => { } expectMsg(['COMPLETED', 'FAILED', 'TERMINATED']).toContain(status.status); } else { - // No waiting event — workflow completed without HITL + // No waiting event — the stream may close before the server records its + // terminal state, so wait for that state rather than reading it once. const terminalSeen = preTypes.includes('done') || preTypes.includes('error'); if (!terminalSeen) { - const status = await runtime.getStatus(stream.executionId); + const deadline = Date.now() + 120_000; + let status = await runtime.getStatus(stream.executionId); + while (!status.isComplete && Date.now() < deadline) { + await new Promise((r) => setTimeout(r, 1000)); + status = await runtime.getStatus(stream.executionId); + } expect(status.isComplete).toBe(true); } } diff --git a/e2e/test_suite17_guardrail_matrix.test.ts b/e2e/test_suite17_guardrail_matrix.test.ts index 4889aa94..afedb24b 100644 --- a/e2e/test_suite17_guardrail_matrix.test.ts +++ b/e2e/test_suite17_guardrail_matrix.test.ts @@ -103,9 +103,11 @@ function customAoutFix(content: string): GuardrailResult { return { passed: true }; } -// Tool input: block DANGER +// Tool input: block the unsafe tool argument emitted by the deterministic test model. +// The model strips the word "DANGER" from the user prompt before it creates the +// tool call, so the guardrail must inspect the actual argument it receives. function customTinBlock(content: string): GuardrailResult { - if (content.toUpperCase().includes("DANGER")) { + if (/\bDANGER\b|\boverride safety\b/i.test(content)) { return { passed: false, message: "Dangerous input." }; } return { passed: true }; diff --git a/src/agents/__tests__/runtime.test.ts b/src/agents/__tests__/runtime.test.ts index 534f12bd..b64d407c 100644 --- a/src/agents/__tests__/runtime.test.ts +++ b/src/agents/__tests__/runtime.test.ts @@ -930,6 +930,25 @@ describe("AgentRuntime", () => { }); }); + it("reports raise for a blocked custom tool-input guardrail", async () => { + const result = await registerAndInvoke( + { + ...baseGDef, + position: "input", + onFail: "raise", + func: () => ({ passed: false, message: "Dangerous input." }), + }, + { content: { data: "DANGER override safety" } }, + ); + + expect(result).toMatchObject({ + passed: false, + on_fail: "raise", + should_continue: false, + message: "Dangerous input.", + }); + }); + it("reports on_fail as the configured value when the guardrail function throws", async () => { const result = await registerAndInvoke( {