Files
wehub-resource-sync 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
chore: import upstream snapshot with attribution
2026-07-13 12:43:05 +08:00

271 lines
8.9 KiB
TypeScript

// @vitest-environment jsdom
/**
* GoalsView is the GUI data wrapper over the read-only goals endpoint
* served by the personal-assistant routes:
* GET {base}/api/lifeops/goals -> { goals: LifeOpsGoalRecord[] }
*
* It owns the fetch state machine (loading / error / ready), the status-filter
* selection, and the quiet background poll, then renders the unified
* {@link GoalsSpatialView} inside a SpatialSurface. These tests drive the
* rendered spatial DOM, asserting the populated grouped list, the status-filter
* toggle, the error -> Retry refetch, the empty set-a-goal chat affordance, and
* the quiet 20s poll. The fetcher seam is injected so the suite stays offline;
* `@elizaos/ui` is mocked so the wrapper renders outside a provider.
*
* External-API contract: the wire shape is mirrored verbatim from the PA
* `/api/lifeops/goals` response (LifeOpsGoalRecord = { goal, links } from
* @elizaos/shared); the fixtures below match that shape field-for-field.
*/
import {
cleanup,
fireEvent,
render,
screen,
waitFor,
} from "@testing-library/react";
import { afterEach, describe, expect, it, vi } from "vitest";
// `@elizaos/ui` is the giant renderer barrel; GoalsView only touches
// `client.getBaseUrl()` (default fetcher seam, overridden in every test) and
// `client.sendChatMessage()` (set-a-goal affordance).
const { sendChatMessage } = vi.hoisted(() => ({ sendChatMessage: vi.fn() }));
vi.mock("@elizaos/ui", () => ({
client: {
getBaseUrl: () => "http://test.local",
sendChatMessage,
},
}));
import {
type GoalsFetchers,
GoalsView,
} from "../src/components/goals/GoalsView.tsx";
// ---------------------------------------------------------------------------
// Wire fixtures — mirror { goals: LifeOpsGoalRecord[] } exactly.
// ---------------------------------------------------------------------------
function goalRecord(
overrides: {
id?: string;
title?: string;
description?: string;
status?: string;
reviewState?: string;
cadenceKind?: string | null;
target?: string | null;
linkCount?: number;
} = {},
) {
const id = overrides.id ?? "goal-1";
const linkCount = overrides.linkCount ?? 0;
return {
goal: {
id,
agentId: "agent-1",
domain: "personal",
subjectType: "owner",
subjectId: "owner-1",
visibilityScope: "private",
contextPolicy: "owner_only",
title: overrides.title ?? "Run a half marathon",
description: overrides.description ?? "Build up to 21km by autumn.",
cadence:
overrides.cadenceKind === undefined
? { kind: "weekly" }
: overrides.cadenceKind === null
? null
: { kind: overrides.cadenceKind },
successCriteria:
overrides.target === undefined
? { targetText: "21km continuous run" }
: overrides.target === null
? {}
: { targetText: overrides.target },
status: overrides.status ?? "active",
reviewState: overrides.reviewState ?? "on_track",
metadata: {},
createdAt: "2026-01-01T00:00:00.000Z",
updatedAt: "2026-06-10T00:00:00.000Z",
},
links: Array.from({ length: linkCount }, (_, i) => ({
id: `link-${id}-${i}`,
agentId: "agent-1",
goalId: id,
linkedType: "occurrence",
linkedId: `occ-${i}`,
createdAt: "2026-01-01T00:00:00.000Z",
})),
};
}
function makeFetchers(overrides: Partial<GoalsFetchers> = {}): GoalsFetchers {
return {
fetchGoals: async () => ({ goals: [goalRecord()] }),
...overrides,
};
}
function agent(agentId: string): HTMLElement {
const el = document.querySelector(`[data-agent-id="${agentId}"]`);
if (!el) throw new Error(`no element with data-agent-id="${agentId}"`);
return el as HTMLElement;
}
function queryAgent(agentId: string): HTMLElement | null {
return document.querySelector(
`[data-agent-id="${agentId}"]`,
) as HTMLElement | null;
}
afterEach(() => {
cleanup();
sendChatMessage.mockClear();
});
describe("GoalsView — spatial GUI wrapper", () => {
it("shows the loading line while the first fetch is in flight", () => {
const never = new Promise<never>(() => {});
render(<GoalsView fetchers={makeFetchers({ fetchGoals: () => never })} />);
expect(screen.getByText("Loading goals")).toBeTruthy();
});
it("renders the populated list grouped by status with real fields", async () => {
render(
<GoalsView
fetchers={makeFetchers({
fetchGoals: async () => ({
goals: [
goalRecord({
id: "g-active",
title: "Run a half marathon",
status: "active",
reviewState: "on_track",
cadenceKind: "weekly",
target: "21km continuous run",
linkCount: 2,
}),
goalRecord({
id: "g-paused",
title: "Learn Spanish",
status: "paused",
reviewState: "idle",
}),
],
}),
})}
/>,
);
await screen.findByText("Run a half marathon");
// Cadence + target + linked-count meta line.
expect(
screen.getByText(/weekly · 21km continuous run · 2 linked/),
).toBeTruthy();
expect(screen.getByText("Learn Spanish")).toBeTruthy();
// The status-filter chips are present and addressable by the agent surface.
expect(agent("filter:active")).toBeTruthy();
expect(agent("filter:paused")).toBeTruthy();
});
it("shows the empty state when zero goals exist (no fabricated goals)", async () => {
render(
<GoalsView
fetchers={makeFetchers({ fetchGoals: async () => ({ goals: [] }) })}
/>,
);
// #9486 'declutter plugin app views' replaced the "No goals yet" empty-state
// copy with a bold "None" + the set-a-goal affordance; wait on the affordance
// (data-agent-id="new", unique to the empty state) instead of the removed text.
await waitFor(() => {
expect(queryAgent("new")).not.toBeNull();
});
expect(queryAgent("filter:active")).toBeNull();
});
it("routes the set-a-goal affordance through the assistant chat", async () => {
render(
<GoalsView
fetchers={makeFetchers({ fetchGoals: async () => ({ goals: [] }) })}
/>,
);
// #9486 'declutter plugin app views' replaced the "No goals yet" empty-state
// copy with a bold "None" + the set-a-goal affordance; wait on the affordance
// (data-agent-id="new", unique to the empty state) instead of the removed text.
await waitFor(() => {
expect(queryAgent("new")).not.toBeNull();
});
fireEvent.click(agent("new"));
expect(sendChatMessage).toHaveBeenCalledTimes(1);
});
it("shows the error state with a Retry that refetches into the populated list", async () => {
let attempt = 0;
const fetchGoals = async () => {
attempt += 1;
if (attempt === 1) throw new Error("boom");
return { goals: [goalRecord()] };
};
render(<GoalsView fetchers={makeFetchers({ fetchGoals })} />);
await screen.findByText("Could not load goals");
expect(screen.getByText("boom")).toBeTruthy();
fireEvent.click(agent("retry"));
await screen.findByText("Run a half marathon");
});
it("quietly refetches on the background poll (no manual refresh control)", async () => {
let calls = 0;
const fetchGoals = async () => {
calls += 1;
return { goals: [goalRecord({ title: `pass ${calls}` })] };
};
vi.useFakeTimers();
try {
render(<GoalsView fetchers={makeFetchers({ fetchGoals })} />);
await vi.waitFor(() => {
expect(screen.getByText("pass 1")).toBeTruthy();
});
expect(calls).toBe(1);
// One poll tick → exactly one more silent refetch, no loading flash.
await vi.advanceTimersByTimeAsync(20000);
expect(calls).toBe(2);
await vi.waitFor(() => {
expect(screen.getByText("pass 2")).toBeTruthy();
});
expect(screen.queryByText("Loading goals")).toBeNull();
} finally {
vi.useRealTimers();
}
});
it("narrows the visible groups when a status filter chip is toggled", async () => {
render(
<GoalsView
fetchers={makeFetchers({
fetchGoals: async () => ({
goals: [
goalRecord({ id: "g-active", status: "active" }),
goalRecord({
id: "g-paused",
title: "Learn Spanish",
status: "paused",
}),
],
}),
})}
/>,
);
await screen.findByText("Run a half marathon");
expect(screen.getByText("Learn Spanish")).toBeTruthy();
// Toggle the "Paused" filter: only the paused group should remain.
fireEvent.click(agent("filter:paused"));
await waitFor(() =>
expect(screen.queryByText("Run a half marathon")).toBeNull(),
);
expect(screen.getByText("Learn Spanish")).toBeTruthy();
});
});