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
416 lines
13 KiB
TypeScript
416 lines
13 KiB
TypeScript
/**
|
|
* LifeOps screen-time integration tests against a real PGLite runtime.
|
|
*
|
|
* Exercises LifeOpsService screen-time recording, daily aggregation, summary,
|
|
* and the SCREEN_TIME action handler end-to-end. No SQL mocks, no LLM.
|
|
*/
|
|
|
|
import type { AgentRuntime, IAgentRuntime } from "@elizaos/core";
|
|
import { afterAll, beforeAll, describe, expect, it } from "vitest";
|
|
import {
|
|
createRealTestRuntime,
|
|
type RealTestRuntimeResult,
|
|
} from "../../../packages/test/helpers/real-runtime.ts";
|
|
import { screenTimeAction } from "../src/actions/screen-time.js";
|
|
import { insertActivityEvent } from "../src/activity-profile/activity-tracker-repo.js";
|
|
import { LifeOpsRepository } from "../src/lifeops/repository.js";
|
|
import { LifeOpsService } from "../src/lifeops/service.js";
|
|
|
|
const AGENT_ID = "lifeops-screentime-agent";
|
|
const DAY_MS = 24 * 60 * 60_000;
|
|
|
|
function makeMessage(runtime: IAgentRuntime, text: string) {
|
|
return {
|
|
id: `msg-${Math.random()}` as string,
|
|
entityId: runtime.agentId,
|
|
roomId: runtime.agentId,
|
|
content: { text },
|
|
};
|
|
}
|
|
|
|
function requireScreenTimeHandler() {
|
|
const handler = screenTimeAction.handler;
|
|
if (!handler) {
|
|
throw new Error("SCREEN_TIME action handler is required");
|
|
}
|
|
return handler;
|
|
}
|
|
|
|
describe("screen-time handler — real PGLite", () => {
|
|
let runtime: AgentRuntime;
|
|
let service: LifeOpsService;
|
|
let testResult: RealTestRuntimeResult;
|
|
|
|
beforeAll(async () => {
|
|
testResult = await createRealTestRuntime({ characterName: AGENT_ID });
|
|
runtime = testResult.runtime;
|
|
await LifeOpsRepository.bootstrapSchema(runtime);
|
|
service = new LifeOpsService(runtime);
|
|
}, 180_000);
|
|
|
|
afterAll(async () => {
|
|
await testResult?.cleanup();
|
|
});
|
|
|
|
it("recordScreenTimeEvent inserts a session", async () => {
|
|
const session = await service.recordScreenTimeEvent({
|
|
source: "app",
|
|
identifier: "com.apple.Safari",
|
|
displayName: "Safari",
|
|
startAt: new Date(Date.now() - 600_000).toISOString(),
|
|
endAt: new Date().toISOString(),
|
|
durationSeconds: 600,
|
|
metadata: {},
|
|
});
|
|
expect(session.id).toBeTruthy();
|
|
expect(session.durationSeconds).toBe(600);
|
|
});
|
|
|
|
it("aggregateDailyForDate rolls sessions into daily totals", async () => {
|
|
// Use a fixed historical date so this test does not collide with the
|
|
// session inserted above, and so the daily total is deterministic.
|
|
const dateBase = new Date("2025-01-15T00:00:00.000Z");
|
|
const date = dateBase.toISOString().slice(0, 10);
|
|
await service.recordScreenTimeEvent({
|
|
source: "app",
|
|
identifier: "com.apple.SafariAggTest",
|
|
displayName: "SafariAggTest",
|
|
startAt: new Date(dateBase.getTime() + 3_600_000).toISOString(),
|
|
endAt: new Date(dateBase.getTime() + 4_200_000).toISOString(),
|
|
durationSeconds: 600,
|
|
metadata: {},
|
|
});
|
|
await service.recordScreenTimeEvent({
|
|
source: "app",
|
|
identifier: "com.apple.SafariAggTest",
|
|
displayName: "SafariAggTest",
|
|
startAt: new Date(dateBase.getTime() + 7_200_000).toISOString(),
|
|
endAt: new Date(dateBase.getTime() + 7_800_000).toISOString(),
|
|
durationSeconds: 600,
|
|
metadata: {},
|
|
});
|
|
await service.aggregateDailyForDate(date);
|
|
const daily = await service.getScreenTimeDaily({ date });
|
|
const safari = daily.find(
|
|
(d) => d.identifier === "com.apple.SafariAggTest",
|
|
);
|
|
if (!safari) {
|
|
throw new Error("Expected Safari aggregate row");
|
|
}
|
|
expect(safari.totalSeconds).toBeGreaterThanOrEqual(1200);
|
|
expect(safari.sessionCount).toBeGreaterThanOrEqual(2);
|
|
});
|
|
|
|
it("getScreenTimeSummary returns top apps in descending order", async () => {
|
|
const baseMs = Date.now() - 3 * 3_600_000;
|
|
await insertActivityEvent(runtime, {
|
|
agentId: String(runtime.agentId),
|
|
observedAt: new Date(baseMs).toISOString(),
|
|
eventKind: "activate",
|
|
bundleId: "com.summary.SafariX",
|
|
appName: "SafariX",
|
|
windowTitle: null,
|
|
});
|
|
await insertActivityEvent(runtime, {
|
|
agentId: String(runtime.agentId),
|
|
observedAt: new Date(baseMs + 600_000).toISOString(),
|
|
eventKind: "activate",
|
|
bundleId: "com.summary.ChromeX",
|
|
appName: "ChromeX",
|
|
windowTitle: null,
|
|
});
|
|
await insertActivityEvent(runtime, {
|
|
agentId: String(runtime.agentId),
|
|
observedAt: new Date(baseMs + 900_000).toISOString(),
|
|
eventKind: "activate",
|
|
bundleId: "com.summary.VSCodeX",
|
|
appName: "VSCodeX",
|
|
windowTitle: null,
|
|
});
|
|
await insertActivityEvent(runtime, {
|
|
agentId: String(runtime.agentId),
|
|
observedAt: new Date(baseMs + 2_100_000).toISOString(),
|
|
eventKind: "deactivate",
|
|
bundleId: "com.summary.VSCodeX",
|
|
appName: "VSCodeX",
|
|
windowTitle: null,
|
|
});
|
|
const since = new Date(baseMs - 60_000).toISOString();
|
|
const until = new Date().toISOString();
|
|
const summary = await service.getScreenTimeSummary({
|
|
since,
|
|
until,
|
|
source: "app",
|
|
topN: 2,
|
|
});
|
|
const summaryIds = summary.items.map((i) => i.identifier);
|
|
expect(summaryIds).toContain("com.summary.VSCodeX");
|
|
// VSCode (1200) should rank above Chrome (300); top-2 must include VSCode first.
|
|
expect(summary.items[0].identifier).toBe("com.summary.VSCodeX");
|
|
expect(summary.items.length).toBe(2);
|
|
});
|
|
|
|
it("excludes OS login surfaces from app screen-time summaries", async () => {
|
|
const startAt = new Date("2025-01-16T01:00:00.000Z");
|
|
const endAt = new Date("2025-01-16T01:30:00.000Z");
|
|
await service.recordScreenTimeEvent({
|
|
source: "app",
|
|
identifier: "com.apple.loginwindow",
|
|
displayName: "loginwindow",
|
|
startAt: startAt.toISOString(),
|
|
endAt: endAt.toISOString(),
|
|
durationSeconds: 30 * 60,
|
|
metadata: { platform: "darwin" },
|
|
});
|
|
await service.recordScreenTimeEvent({
|
|
source: "app",
|
|
identifier: "com.summary.RealEditor",
|
|
displayName: "RealEditor",
|
|
startAt: new Date("2025-01-16T02:00:00.000Z").toISOString(),
|
|
endAt: new Date("2025-01-16T02:20:00.000Z").toISOString(),
|
|
durationSeconds: 20 * 60,
|
|
metadata: {},
|
|
});
|
|
|
|
const summary = await service.getScreenTimeSummary({
|
|
since: "2025-01-16T00:00:00.000Z",
|
|
until: "2025-01-16T03:00:00.000Z",
|
|
source: "app",
|
|
topN: 10,
|
|
});
|
|
|
|
expect(summary.items.map((item) => item.identifier)).not.toContain(
|
|
"com.apple.loginwindow",
|
|
);
|
|
expect(summary.items.map((item) => item.identifier)).toContain(
|
|
"com.summary.RealEditor",
|
|
);
|
|
});
|
|
|
|
it("getScreenTimeWeeklyAverageByApp returns structured per-day averages", async () => {
|
|
const weekStart = Date.parse("2025-02-03T00:00:00.000Z");
|
|
const weekEnd = weekStart + 7 * DAY_MS;
|
|
|
|
for (let day = 0; day < 7; day += 1) {
|
|
const dayStart = weekStart + day * DAY_MS;
|
|
await service.recordScreenTimeEvent({
|
|
source: "app",
|
|
identifier: "com.weekly.VSCode",
|
|
displayName: "VS Code",
|
|
startAt: new Date(dayStart + 30 * 60_000).toISOString(),
|
|
endAt: new Date(dayStart + 90 * 60_000).toISOString(),
|
|
durationSeconds: 60 * 60,
|
|
metadata: {},
|
|
});
|
|
await service.recordScreenTimeEvent({
|
|
source: "app",
|
|
identifier: "com.weekly.Safari",
|
|
displayName: "Safari",
|
|
startAt: new Date(dayStart + 150 * 60_000).toISOString(),
|
|
endAt: new Date(dayStart + 180 * 60_000).toISOString(),
|
|
durationSeconds: 30 * 60,
|
|
metadata: {},
|
|
});
|
|
}
|
|
|
|
const weeklyAverage = await service.getScreenTimeWeeklyAverageByApp({
|
|
since: new Date(weekStart).toISOString(),
|
|
until: new Date(weekEnd).toISOString(),
|
|
daysInWindow: 7,
|
|
topN: 10,
|
|
});
|
|
|
|
expect(weeklyAverage.daysInWindow).toBe(7);
|
|
expect(weeklyAverage.totalSeconds).toBe(7 * 90 * 60);
|
|
const vscode = weeklyAverage.items.find(
|
|
(item) => item.identifier === "com.weekly.VSCode",
|
|
);
|
|
const safari = weeklyAverage.items.find(
|
|
(item) => item.identifier === "com.weekly.Safari",
|
|
);
|
|
expect(vscode?.averageSecondsPerDay).toBe(60 * 60);
|
|
expect(vscode?.averageMinutesPerDay).toBe(60);
|
|
expect(safari?.averageSecondsPerDay).toBe(30 * 60);
|
|
expect(safari?.averageMinutesPerDay).toBe(30);
|
|
});
|
|
|
|
it("SCREEN_TIME weekly_average_by_app returns structured action data", async () => {
|
|
const now = Date.now();
|
|
const weekStart = now - 7 * DAY_MS;
|
|
|
|
for (let day = 0; day < 7; day += 1) {
|
|
const dayStart = weekStart + day * DAY_MS;
|
|
await service.recordScreenTimeEvent({
|
|
source: "app",
|
|
identifier: "com.action.VSCode",
|
|
displayName: "VS Code",
|
|
startAt: new Date(dayStart + 15 * 60_000).toISOString(),
|
|
endAt: new Date(dayStart + 75 * 60_000).toISOString(),
|
|
durationSeconds: 60 * 60,
|
|
metadata: {},
|
|
});
|
|
}
|
|
|
|
const result = await requireScreenTimeHandler()(
|
|
runtime,
|
|
makeMessage(runtime, "What's my weekly average per app?") as never,
|
|
undefined,
|
|
{
|
|
parameters: {
|
|
subaction: "weekly_average_by_app",
|
|
sinceDays: 7,
|
|
},
|
|
},
|
|
async () => undefined,
|
|
);
|
|
|
|
expect(result.success).toBe(true);
|
|
const data = result.data as
|
|
| {
|
|
subaction?: string;
|
|
weeklyAverage?: {
|
|
daysInWindow?: number;
|
|
items?: Array<{
|
|
identifier?: string;
|
|
averageSecondsPerDay?: number;
|
|
}>;
|
|
};
|
|
}
|
|
| undefined;
|
|
expect(data?.subaction).toBe("weekly_average_by_app");
|
|
const weeklyAverage = data?.weeklyAverage;
|
|
expect(weeklyAverage?.daysInWindow).toBe(7);
|
|
expect(
|
|
weeklyAverage?.items?.find(
|
|
(item) => item.identifier === "com.action.VSCode",
|
|
)?.averageSecondsPerDay,
|
|
).toBe(60 * 60);
|
|
});
|
|
|
|
it("syncBrowserState persists website focus windows into screen time summaries", async () => {
|
|
const startAt = new Date(Date.now() - 10 * 60_000).toISOString();
|
|
const endAt = new Date(Date.now() - 6 * 60_000).toISOString();
|
|
|
|
await service.updateBrowserSettings({
|
|
enabled: true,
|
|
allowBrowserControl: true,
|
|
});
|
|
|
|
await service.syncBrowserState({
|
|
companion: {
|
|
browser: "chrome",
|
|
profileId: "screen-time-profile",
|
|
label: "LifeOps Browser",
|
|
connectionState: "connected",
|
|
lastSeenAt: startAt,
|
|
permissions: {
|
|
tabs: true,
|
|
scripting: true,
|
|
activeTab: true,
|
|
allOrigins: true,
|
|
grantedOrigins: ["https://github.com"],
|
|
incognitoEnabled: false,
|
|
},
|
|
},
|
|
tabs: [
|
|
{
|
|
browser: "chrome",
|
|
profileId: "screen-time-profile",
|
|
windowId: "window-1",
|
|
tabId: "tab-1",
|
|
url: "https://github.com/elizaos/elizaos",
|
|
title: "elizaOS",
|
|
activeInWindow: true,
|
|
focusedWindow: true,
|
|
focusedActive: true,
|
|
lastSeenAt: startAt,
|
|
lastFocusedAt: startAt,
|
|
},
|
|
],
|
|
pageContexts: [],
|
|
});
|
|
|
|
await service.syncBrowserState({
|
|
companion: {
|
|
browser: "chrome",
|
|
profileId: "screen-time-profile",
|
|
label: "LifeOps Browser",
|
|
connectionState: "connected",
|
|
lastSeenAt: endAt,
|
|
permissions: {
|
|
tabs: true,
|
|
scripting: true,
|
|
activeTab: true,
|
|
allOrigins: true,
|
|
grantedOrigins: ["https://github.com"],
|
|
incognitoEnabled: false,
|
|
},
|
|
},
|
|
tabs: [
|
|
{
|
|
browser: "chrome",
|
|
profileId: "screen-time-profile",
|
|
windowId: "window-1",
|
|
tabId: "tab-2",
|
|
url: "https://news.ycombinator.com",
|
|
title: "Hacker News",
|
|
activeInWindow: true,
|
|
focusedWindow: true,
|
|
focusedActive: true,
|
|
lastSeenAt: endAt,
|
|
lastFocusedAt: endAt,
|
|
},
|
|
],
|
|
pageContexts: [],
|
|
});
|
|
|
|
const summary = await service.getScreenTimeSummary({
|
|
since: new Date(Date.now() - 30 * 60_000).toISOString(),
|
|
until: new Date().toISOString(),
|
|
source: "website",
|
|
topN: 5,
|
|
});
|
|
|
|
expect(summary.items).toEqual(
|
|
expect.arrayContaining([
|
|
expect.objectContaining({
|
|
source: "website",
|
|
identifier: "github.com",
|
|
}),
|
|
]),
|
|
);
|
|
});
|
|
|
|
it("screenTimeAction today handler returns text and data", async () => {
|
|
const today = new Date().toISOString().slice(0, 10);
|
|
const result = await requireScreenTimeHandler()(
|
|
runtime,
|
|
makeMessage(runtime, "screen time today") as never,
|
|
undefined,
|
|
{ parameters: { subaction: "today", date: today } } as never,
|
|
async () => {},
|
|
);
|
|
expect(result?.success).toBe(true);
|
|
expect(typeof (result as { text?: string }).text).toBe("string");
|
|
const data = (result as { data?: { date?: string } }).data;
|
|
expect(data?.date).toBe(today);
|
|
});
|
|
|
|
it("screenTimeAction summary handler returns ranked items", async () => {
|
|
const result = await requireScreenTimeHandler()(
|
|
runtime,
|
|
makeMessage(runtime, "screen time summary") as never,
|
|
undefined,
|
|
{ parameters: { subaction: "summary", sinceDays: 7 } } as never,
|
|
async () => {},
|
|
);
|
|
expect(result?.success).toBe(true);
|
|
const data = (
|
|
result as {
|
|
data?: { summary?: { items: unknown[]; totalSeconds: number } };
|
|
}
|
|
).data;
|
|
expect(Array.isArray(data?.summary?.items)).toBe(true);
|
|
});
|
|
});
|