426e9eeabd
Voice Workbench / headless workbench (mocked backends) (push) Has been cancelled
Voice Workbench / real acoustic lane (nightly, provisioned only) (push) Has been cancelled
ci / test (push) Has been cancelled
ci / lint-and-format (push) Has been cancelled
ci / build (push) Has been cancelled
ci / dev-startup (push) Has been cancelled
gitleaks / gitleaks (push) Has been cancelled
Markdown Links / Relative Markdown Links (push) Has been cancelled
Quality (Extended) / Homepage Build (PR smoke) (push) Has been cancelled
Quality (Extended) / Comment-only diff guard (push) Has been cancelled
Quality (Extended) / Format + Type Safety Ratchet (push) Has been cancelled
Quality (Extended) / Develop Gate (secret scan + UI determinism) (push) Has been cancelled
Quality (Extended) / Develop Gate (lint) (push) Has been cancelled
Chat shell gestures / Chat shell gesture + parity e2e (push) Has been cancelled
Cloud Gateway Discord / Test (push) Has been cancelled
Benchmark Bridge Tests / benchmark (bunx @biomejs/biome check packages/lifeops-bench/src, benchmark-lint) (push) Has been cancelled
Benchmark Bridge Tests / benchmark (bunx vitest run --config packages/lifeops-bench/vitest.config.ts --root packages/lifeops-bench --passWithNoTests, benchmark-tests) (push) Has been cancelled
Build Agent Image / build-and-push (push) Has been cancelled
Dev Smoke / bun run dev onboarding chat (push) Has been cancelled
Dev Smoke / Vite HMR dependency-level smoke (push) Has been cancelled
Electrobun Submodule Guard / electrobun gitlink is fetchable (push) Has been cancelled
Publish @elizaos/example-code / check_npm (push) Has been cancelled
Publish @elizaos/example-code / publish_npm (push) Has been cancelled
Publish @elizaos/plugin-elizacloud / verify_version (push) Has been cancelled
Publish @elizaos/plugin-elizacloud / publish_npm (push) Has been cancelled
Sandbox Live Smoke / Sandbox live smoke (push) Has been cancelled
Snap Build & Test / Build Snap (amd64) (push) Has been cancelled
Snap Build & Test / Build Snap (arm64) (push) Has been cancelled
Test Packaging / elizaos CLI global-install smoke (node + bun) (push) Has been cancelled
Cloud Gateway Webhook / Test (push) Has been cancelled
Cloud Tests / lint-and-types (push) Has been cancelled
Cloud Tests / unit-tests (push) Has been cancelled
Cloud Tests / integration-tests (push) Has been cancelled
Cloud Tests / e2e-tests (push) Has been cancelled
CodeQL Advanced / Analyze (javascript-typescript) (push) Has been cancelled
Deploy Apps Worker (Product 2) / Determine environment (push) Has been cancelled
Deploy Apps Worker (Product 2) / Deploy apps worker to apps-control host (${{ needs.determine-env.outputs.environment }}) (push) Has been cancelled
Deploy Eliza Provisioning Worker / Determine environment (push) Has been cancelled
Deploy Eliza Provisioning Worker / Deploy worker to Hetzner host (${{ needs.determine-env.outputs.environment }} @ ${{ needs.determine-env.outputs.deployment_sha }}) (push) Has been cancelled
Dev Smoke / Classify changed paths (push) Has been cancelled
supply-chain / sbom (push) Has been cancelled
supply-chain / vulnerability-scan (push) Has been cancelled
Build, Push & Deploy to Phala Cloud / build-and-push (push) Has been cancelled
Test Packaging / Validate Packaging Configs (push) Has been cancelled
Test Packaging / Build & Test PyPI Package (push) Has been cancelled
Test Packaging / PyPI on Python ${{ matrix.python }} (push) Has been cancelled
Test Packaging / Pack & Test JS Tarballs (push) Has been cancelled
UI Fixture E2E / ui-fixture-e2e (push) Has been cancelled
UI Fixture E2E / fixture-e2e (push) Has been cancelled
UI Story Gate / story-gate (push) Has been cancelled
vault-ci / test (macos-latest) (push) Has been cancelled
vault-ci / test (ubuntu-latest) (push) Has been cancelled
vault-ci / test (windows-latest) (push) Has been cancelled
vault-ci / app-core wiring tests (push) Has been cancelled
verify-patches / verify patches/CHECKSUMS.sha256 (push) Has been cancelled
Voice Benchmark Smoke / voice-emotion fixture smoke (push) Has been cancelled
Voice Benchmark Smoke / voiceagentbench fixture smoke (push) Has been cancelled
Voice Benchmark Smoke / voicebench-quality unit smoke (push) Has been cancelled
Voice Benchmark Smoke / voicebench TypeScript unit (no audio) (push) Has been cancelled
Voice Benchmark Smoke / voice bench smoke summary (push) Has been cancelled
Windows CI / windows ([bun run --cwd packages/app-core test bun run --cwd packages/elizaos test bun run --cwd packages/cloud/shared test], app-and-cli) (push) Has been cancelled
Windows CI / windows ([bun run --cwd packages/scenario-runner test bun run --cwd packages/vault test bun run --cwd packages/security test bun run --cwd plugins/plugin-coding-tools test], framework-packages) (push) Has been cancelled
Windows CI / windows ([bun run --cwd plugins/plugin-elizacloud test bun run --cwd plugins/plugin-discord test bun run --cwd plugins/plugin-anthropic test bun run --cwd plugins/plugin-openai test bun run --cwd plugins/plugin-app-control test bun run --cwd plugins/pl… (push) Has been cancelled
Windows CI / windows ([node packages/scripts/run-turbo.mjs run build --filter=@elizaos/core --filter=@elizaos/shared --filter=@elizaos/agent --concurrency=4 node packages/scripts/run-bash-linux-only.mjs scripts/verify-riscv64-buildpaths.sh node packages/scripts/run… (push) Has been cancelled
Windows CI / windows ([node packages/scripts/run-turbo.mjs run typecheck --filter=@elizaos/core --filter=@elizaos/shared --filter=@elizaos/cloud-shared --concurrency=4 bun run --cwd packages/core test bun run --cwd packages/shared test], core-runtime, 75) (push) Has been cancelled
317 lines
9.9 KiB
TypeScript
317 lines
9.9 KiB
TypeScript
/**
|
|
* Tests for the two-phase confirm state machine (readStructuredConfirmation and the conflicting-target/amount/domain guards). Pure — planner-boolean parsing, never prose. No SDK.
|
|
*/
|
|
import { describe, expect, it } from "bun:test";
|
|
import {
|
|
buildConnectorCta,
|
|
CONFIRM_TTL_MS,
|
|
confirmationPrompt,
|
|
confirmReferenceMatchesTarget,
|
|
conflictingConfirmAmount,
|
|
conflictingConfirmDomain,
|
|
conflictingConfirmTarget,
|
|
pendingExpired,
|
|
readStructuredConfirmation,
|
|
} from "../src/safety.ts";
|
|
|
|
const TARGET = {
|
|
name: "Acme Bot",
|
|
id: "11111111-2222-3333-4444-555555555555",
|
|
aliases: ["acme-bot"],
|
|
};
|
|
|
|
describe("readStructuredConfirmation", () => {
|
|
it("accepts only structured boolean-ish confirmation fields", () => {
|
|
expect(readStructuredConfirmation({ confirm: true })).toBe(true);
|
|
expect(readStructuredConfirmation({ confirm: false })).toBe(false);
|
|
expect(readStructuredConfirmation({ confirmed: "1" })).toBe(true);
|
|
expect(readStructuredConfirmation({ confirm: "0" })).toBe(false);
|
|
});
|
|
|
|
it("reads the confirmation from the real planner path (options.parameters)", () => {
|
|
// The runtime nests validated action parameters under options.parameters
|
|
// (execute-planned-tool-call.ts). Reading only the top level made confirm
|
|
// invisible on every real turn, so the destructive action could never fire.
|
|
expect(readStructuredConfirmation({ parameters: { confirm: true } })).toBe(
|
|
true,
|
|
);
|
|
expect(readStructuredConfirmation({ parameters: { confirm: false } })).toBe(
|
|
false,
|
|
);
|
|
expect(readStructuredConfirmation({ parameters: { confirmed: "1" } })).toBe(
|
|
true,
|
|
);
|
|
expect(readStructuredConfirmation({ parameters: {} })).toBe(null);
|
|
// The nested (planner-extracted) value is authoritative over any top-level.
|
|
expect(
|
|
readStructuredConfirmation({
|
|
parameters: { confirm: false },
|
|
confirm: true,
|
|
}),
|
|
).toBe(false);
|
|
// Prose is still never treated as confirmation, even when nested.
|
|
expect(readStructuredConfirmation({ parameters: { confirm: "yes" } })).toBe(
|
|
null,
|
|
);
|
|
});
|
|
|
|
it("does not infer confirmation from user prose", () => {
|
|
expect(readStructuredConfirmation(undefined)).toBe(null);
|
|
expect(readStructuredConfirmation({ text: "yes delete Acme Bot" })).toBe(
|
|
null,
|
|
);
|
|
expect(readStructuredConfirmation({ confirm: "yes" })).toBe(null);
|
|
expect(
|
|
readStructuredConfirmation({ confirm: "delete Acme Bot — yes" }),
|
|
).toBe(null);
|
|
});
|
|
});
|
|
|
|
describe("confirmReferenceMatchesTarget (frozen-target guard)", () => {
|
|
it("matches the frozen target by id, exact name, alias, and partial name", () => {
|
|
expect(confirmReferenceMatchesTarget(TARGET.id, TARGET)).toBe(true);
|
|
expect(confirmReferenceMatchesTarget("Acme Bot", TARGET)).toBe(true);
|
|
expect(confirmReferenceMatchesTarget("acme bot", TARGET)).toBe(true);
|
|
expect(confirmReferenceMatchesTarget("acme-bot", TARGET)).toBe(true);
|
|
// Partial names of the SAME target must never read as a switch.
|
|
expect(confirmReferenceMatchesTarget("Acme", TARGET)).toBe(true);
|
|
// A longer phrase containing the frozen name still matches.
|
|
expect(confirmReferenceMatchesTarget("the Acme Bot app", TARGET)).toBe(
|
|
true,
|
|
);
|
|
});
|
|
|
|
it("treats generic filler as a non-reference (never blocks)", () => {
|
|
expect(confirmReferenceMatchesTarget("my app", TARGET)).toBe(true);
|
|
expect(confirmReferenceMatchesTarget("the app", TARGET)).toBe(true);
|
|
expect(confirmReferenceMatchesTarget("it", TARGET)).toBe(true);
|
|
});
|
|
|
|
it("rejects a clearly different target", () => {
|
|
expect(confirmReferenceMatchesTarget("Beta Dashboard", TARGET)).toBe(false);
|
|
expect(
|
|
confirmReferenceMatchesTarget(
|
|
"99999999-8888-7777-6666-555555555555",
|
|
TARGET,
|
|
),
|
|
).toBe(false);
|
|
});
|
|
});
|
|
|
|
describe("conflictingConfirmTarget", () => {
|
|
it("returns null on a bare confirm (no reference sent)", () => {
|
|
expect(conflictingConfirmTarget({ confirm: true }, TARGET)).toBe(null);
|
|
expect(
|
|
conflictingConfirmTarget({ parameters: { confirm: true } }, TARGET),
|
|
).toBe(null);
|
|
expect(conflictingConfirmTarget(undefined, TARGET)).toBe(null);
|
|
});
|
|
|
|
it("returns null when the confirm turn re-names the SAME target", () => {
|
|
expect(
|
|
conflictingConfirmTarget(
|
|
{ parameters: { confirm: true, appName: "acme" } },
|
|
TARGET,
|
|
),
|
|
).toBe(null);
|
|
});
|
|
|
|
it("returns the conflicting reference when a DIFFERENT target is named (nested planner path)", () => {
|
|
expect(
|
|
conflictingConfirmTarget(
|
|
{ parameters: { confirm: true, appName: "Beta Dashboard" } },
|
|
TARGET,
|
|
),
|
|
).toBe("Beta Dashboard");
|
|
expect(
|
|
conflictingConfirmTarget({ confirm: true, appName: "Beta" }, TARGET),
|
|
).toBe("Beta");
|
|
});
|
|
|
|
it("honors custom reference keys (influencer bookings)", () => {
|
|
expect(
|
|
conflictingConfirmTarget(
|
|
{ parameters: { confirm: true, influencer: "Bob Creator" } },
|
|
{ name: "Alice Creator", id: "profile-1" },
|
|
["profileId", "influencer"],
|
|
),
|
|
).toBe("Bob Creator");
|
|
expect(
|
|
conflictingConfirmTarget(
|
|
{ parameters: { confirm: true, profileId: "profile-1" } },
|
|
{ name: "Alice Creator", id: "profile-1" },
|
|
["profileId", "influencer"],
|
|
),
|
|
).toBe(null);
|
|
});
|
|
});
|
|
|
|
describe("conflictingConfirmAmount", () => {
|
|
it("returns null on a bare confirm or a matching amount", () => {
|
|
expect(conflictingConfirmAmount({ confirm: true }, 100)).toBe(null);
|
|
expect(conflictingConfirmAmount({ parameters: { amount: 100 } }, 100)).toBe(
|
|
null,
|
|
);
|
|
expect(
|
|
conflictingConfirmAmount({ parameters: { amount: "100" } }, 100),
|
|
).toBe(null);
|
|
expect(
|
|
conflictingConfirmAmount({ parameters: { amount: "$100" } }, 100),
|
|
).toBe(null);
|
|
});
|
|
|
|
it("returns the conflicting amount when the confirm turn names a different one", () => {
|
|
expect(conflictingConfirmAmount({ parameters: { amount: 50 } }, 100)).toBe(
|
|
50,
|
|
);
|
|
expect(conflictingConfirmAmount({ amount: "50" }, 100)).toBe(50);
|
|
});
|
|
|
|
it("ignores prose amounts (never guesses)", () => {
|
|
expect(
|
|
conflictingConfirmAmount({ parameters: { amount: "fifty bucks" } }, 100),
|
|
).toBe(null);
|
|
});
|
|
});
|
|
|
|
describe("conflictingConfirmDomain", () => {
|
|
it("returns null on a bare confirm or the same domain", () => {
|
|
expect(conflictingConfirmDomain({ confirm: true }, "yourbrand.com")).toBe(
|
|
null,
|
|
);
|
|
expect(
|
|
conflictingConfirmDomain(
|
|
{ parameters: { domain: "yourbrand.com" } },
|
|
"yourbrand.com",
|
|
),
|
|
).toBe(null);
|
|
expect(
|
|
conflictingConfirmDomain(
|
|
{ parameters: { domain: "WWW.YourBrand.com" } },
|
|
"yourbrand.com",
|
|
),
|
|
).toBe(null);
|
|
});
|
|
|
|
it("domains compare exactly: a substring domain is a DIFFERENT domain", () => {
|
|
expect(
|
|
conflictingConfirmDomain(
|
|
{ parameters: { domain: "brand.com" } },
|
|
"yourbrand.com",
|
|
),
|
|
).toBe("brand.com");
|
|
expect(
|
|
conflictingConfirmDomain(
|
|
{ parameters: { domain: "other.io" } },
|
|
"yourbrand.com",
|
|
),
|
|
).toBe("other.io");
|
|
});
|
|
|
|
it("ignores values that don't look like a domain", () => {
|
|
expect(
|
|
conflictingConfirmDomain(
|
|
{ parameters: { domain: "the domain" } },
|
|
"yourbrand.com",
|
|
),
|
|
).toBe(null);
|
|
});
|
|
});
|
|
|
|
describe("confirmationPrompt", () => {
|
|
it("names the target, what is destroyed, and the exact token", () => {
|
|
const prompt = confirmationPrompt(TARGET, [
|
|
"its running container",
|
|
"its tenant database",
|
|
]);
|
|
expect(prompt).toContain("Acme Bot");
|
|
expect(prompt).toContain(TARGET.id);
|
|
expect(prompt).toContain("its running container");
|
|
expect(prompt).toContain("its tenant database");
|
|
expect(prompt.toLowerCase()).toContain("can't be undone");
|
|
expect(prompt).toContain("confirm delete Acme Bot");
|
|
});
|
|
});
|
|
|
|
describe("buildConnectorCta", () => {
|
|
it("builds a neutral {label,url,kind} for an https URL", () => {
|
|
const cta = buildConnectorCta(
|
|
"Withdraw",
|
|
"https://x.test/withdraw",
|
|
"button",
|
|
);
|
|
expect(cta).toEqual({
|
|
label: "Withdraw",
|
|
url: "https://x.test/withdraw",
|
|
kind: "button",
|
|
});
|
|
});
|
|
|
|
it("rejects non-http(s) URLs (no creds/money smuggling)", () => {
|
|
expect(() => buildConnectorCta("x", "javascript:alert(1)")).toThrow();
|
|
expect(() => buildConnectorCta("x", "not a url")).toThrow();
|
|
});
|
|
});
|
|
|
|
describe("pendingExpired (shared confirm TTL)", () => {
|
|
const base = {
|
|
taskId: "task-1",
|
|
metadata: {
|
|
roomId: "room-1",
|
|
action: "BOOK_INFLUENCER" as const,
|
|
appId: "inf_1",
|
|
appName: "Nova",
|
|
amount: 200,
|
|
},
|
|
};
|
|
|
|
it("a fresh pending is not expired", () => {
|
|
const pending = {
|
|
...base,
|
|
metadata: {
|
|
...base.metadata,
|
|
intentCreatedAt: new Date().toISOString(),
|
|
},
|
|
};
|
|
expect(pendingExpired(pending)).toBe(false);
|
|
});
|
|
|
|
it("a pending older than CONFIRM_TTL_MS is expired", () => {
|
|
const pending = {
|
|
...base,
|
|
metadata: {
|
|
...base.metadata,
|
|
intentCreatedAt: new Date(
|
|
Date.now() - CONFIRM_TTL_MS - 1000,
|
|
).toISOString(),
|
|
},
|
|
};
|
|
expect(pendingExpired(pending)).toBe(true);
|
|
});
|
|
|
|
it("a pending with no/invalid intentCreatedAt never expires (no timestamp to age)", () => {
|
|
expect(pendingExpired(base)).toBe(false);
|
|
expect(
|
|
pendingExpired({
|
|
...base,
|
|
metadata: { ...base.metadata, intentCreatedAt: "not-a-date" },
|
|
}),
|
|
).toBe(false);
|
|
});
|
|
|
|
it("a BUY_APP_DOMAIN recovery retry never expires (no new charge at stake)", () => {
|
|
const pending = {
|
|
...base,
|
|
metadata: {
|
|
...base.metadata,
|
|
action: "BUY_APP_DOMAIN" as const,
|
|
recovery: true,
|
|
intentCreatedAt: new Date(
|
|
Date.now() - CONFIRM_TTL_MS * 10,
|
|
).toISOString(),
|
|
},
|
|
};
|
|
expect(pendingExpired(pending)).toBe(false);
|
|
});
|
|
});
|