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

437 lines
15 KiB
TypeScript

/**
* Validates that the produced first-run scheduled-task pack is shape-valid
* per the W1-A `ScheduledTask` contract: required fields are present, every
* trigger.kind is in the registered set, every priority is in the registered
* set, every `respectsGlobalPause` is set, every task has an `idempotencyKey`
* (so replay upserts), and that the customize finalizer's channel-validation
* fallback path produces an in_app channel + warning.
*/
import { afterEach, describe, expect, it } from "vitest";
import {
createChannelRegistry,
registerDefaultChannelPack,
} from "../src/lifeops/channels/index.ts";
import type { ConnectorContribution } from "../src/lifeops/connectors/contract.ts";
import {
createConnectorRegistry,
registerConnectorRegistry,
} from "../src/lifeops/connectors/registry.ts";
import { installFirstRunChannelInspector } from "../src/lifeops/first-run/channel-inspector.ts";
import { buildDefaultsPack } from "../src/lifeops/first-run/defaults.ts";
import {
parseCategories,
parseRelationships,
parseTimeWindow,
parseTimezone,
setChannelInspector,
validateChannel,
} from "../src/lifeops/first-run/questions.ts";
import { FirstRunService } from "../src/lifeops/first-run/service.ts";
import {
createFirstRunStateStore,
createOwnerFactStore,
} from "../src/lifeops/first-run/state.ts";
import type {
ScheduledTask,
ScheduledTaskInput,
} from "../src/lifeops/wave1-types.ts";
import { createMinimalRuntimeStub } from "./first-run-helpers.ts";
afterEach(() => setChannelInspector(null));
const VALID_TRIGGER_KINDS = new Set([
"once",
"cron",
"interval",
"relative_to_anchor",
"during_window",
"event",
"manual",
"after_task",
]);
const VALID_PRIORITIES = new Set(["low", "medium", "high"]);
const VALID_KINDS = new Set([
"reminder",
"checkin",
"followup",
"approval",
"recap",
"watcher",
"output",
"custom",
]);
/**
* A runner that mirrors the production runner's idempotency contract: if a
* task with the same `idempotencyKey` already exists in the live store it is
* returned as-is (no duplicate). The store is a plain Map the test can delete
* from to model the user removing a default. This is exactly the surface the
* seeded-defaults marker must guard: the runner alone would happily re-create a
* deleted default; the marker is what prevents it.
*/
function makeTrackingRunner(tasks: Map<string, ScheduledTask>) {
let counter = 0;
return {
async schedule(input: ScheduledTaskInput): Promise<ScheduledTask> {
if (input.idempotencyKey) {
const existing = [...tasks.values()].find(
(t) => t.idempotencyKey === input.idempotencyKey,
);
if (existing) return existing;
}
counter += 1;
const task: ScheduledTask = {
...input,
taskId: `track-${counter}`,
state: { status: "scheduled", followupCount: 0 },
};
tasks.set(task.taskId, task);
return task;
},
};
}
function asScheduledTask(input: ScheduledTaskInput): ScheduledTaskInput {
expect(VALID_KINDS.has(input.kind)).toBe(true);
expect(VALID_PRIORITIES.has(input.priority)).toBe(true);
expect(VALID_TRIGGER_KINDS.has(input.trigger.kind)).toBe(true);
expect(typeof input.respectsGlobalPause).toBe("boolean");
expect(typeof input.idempotencyKey).toBe("string");
expect(typeof input.promptInstructions).toBe("string");
expect(input.promptInstructions.length).toBeGreaterThan(0);
expect(input.source).toBe("first_run");
return input;
}
function makeConnectedTelegramConnector(): ConnectorContribution {
return {
kind: "telegram",
capabilities: ["telegram.send"],
modes: ["local"],
describe: { label: "Telegram" },
async start() {},
async disconnect() {},
async verify() {
return true;
},
async status() {
return {
state: "ok",
observedAt: "2026-07-06T00:00:00.000Z",
};
},
};
}
describe("first-run config validation", () => {
it("buildDefaultsPack emits six shape-valid ScheduledTask inputs", () => {
const pack = buildDefaultsPack({
morningWindow: { startLocal: "06:30", endLocal: "11:30" },
timezone: "America/Los_Angeles",
agentId: "agent-1",
channel: "in_app",
});
expect(pack.length).toBe(6);
pack.forEach(asScheduledTask);
// Specific slot assertions
const slots = new Set(
pack.map((p) => (p.metadata?.slot ?? null) as string | null),
);
expect(slots).toEqual(
new Set([
"gm",
"gn",
"checkin",
"morningBrief",
"weeklyReview",
"localBackup",
]),
);
const checkin = pack.find((p) => p.metadata?.slot === "checkin");
expect(checkin?.completionCheck?.kind).toBe("user_replied_within");
const morningBrief = pack.find((p) => p.metadata?.slot === "morningBrief");
expect(morningBrief?.trigger.kind).toBe("relative_to_anchor");
if (morningBrief?.trigger.kind === "relative_to_anchor") {
expect(morningBrief.trigger.anchorKey).toBe("wake.confirmed");
}
// The weekly-review starter ships PAUSED: a manual trigger means it exists
// and is owner-visible but never fires on its own.
const weeklyReview = pack.find((p) => p.metadata?.slot === "weeklyReview");
expect(weeklyReview?.trigger.kind).toBe("manual");
expect(weeklyReview?.metadata?.pausedByDefault).toBe(true);
expect(weeklyReview?.ownerVisible).toBe(true);
const localBackup = pack.find((p) => p.metadata?.slot === "localBackup");
expect(localBackup?.kind).toBe("output");
expect(localBackup?.trigger.kind).toBe("cron");
if (localBackup?.trigger.kind === "cron") {
expect(localBackup.trigger.expression).toBe("0 */6 * * *");
expect(localBackup.trigger.tz).toBe("America/Los_Angeles");
}
expect(localBackup?.metadata?.systemOperation).toBe("agent.localBackup");
expect(localBackup?.respectsGlobalPause).toBe(false);
expect(localBackup?.executionProfile).toBe("bg-heavy-fgs");
});
it("parseTimezone / parseTimeWindow accept valid input and reject garbage", () => {
expect(parseTimezone("America/New_York")).toBe("America/New_York");
expect(parseTimezone("")).toBe(null);
expect(parseTimeWindow({ startLocal: "06:00", endLocal: "11:00" })).toEqual(
{ startLocal: "06:00", endLocal: "11:00" },
);
expect(parseTimeWindow({ startLocal: "11:00", endLocal: "06:00" })).toBe(
null,
);
expect(parseTimeWindow({ startLocal: "25:00", endLocal: "30:00" })).toBe(
null,
);
});
it("parseCategories filters to the allowed set", () => {
expect(
parseCategories([
"sleep tracking",
"ALIENS",
"follow-ups",
" inbox triage ",
]),
).toEqual(["sleep tracking", "follow-ups", "inbox triage"]);
});
it("parseRelationships shapes user input and bounds at 5 entries", () => {
const result = parseRelationships(
Array.from({ length: 8 }, (_, i) => ({
name: `Person ${i}`,
cadenceDays: i + 1,
})),
);
expect(result?.length).toBe(5);
});
it("validateChannel falls back to in_app + warning for unconnected channels", async () => {
const runtime = createMinimalRuntimeStub();
const result = await validateChannel("telegram", runtime);
expect(result.fallbackToInApp).toBe(true);
expect(result.warning).toMatch(/fall back/i);
});
it("validateChannel passes a connected channel through cleanly", async () => {
setChannelInspector({
isRegistered: () => true,
isConnected: () => true,
});
const runtime = createMinimalRuntimeStub();
const result = await validateChannel("telegram", runtime);
expect(result.fallbackToInApp).toBe(false);
expect(result.warning).toBeUndefined();
setChannelInspector(null);
});
it("validates a connected Telegram channel through the production registry inspector", async () => {
const runtime = createMinimalRuntimeStub();
const connectorRegistry = createConnectorRegistry();
connectorRegistry.register(makeConnectedTelegramConnector());
registerConnectorRegistry(runtime, connectorRegistry);
const channelRegistry = createChannelRegistry();
registerDefaultChannelPack(channelRegistry, runtime);
installFirstRunChannelInspector(runtime, channelRegistry);
await expect(validateChannel("telegram", runtime)).resolves.toEqual({
channel: "telegram",
registered: true,
connected: true,
fallbackToInApp: false,
});
});
it("keeps production channel inspectors isolated per runtime", async () => {
const connectedRuntime = createMinimalRuntimeStub();
const connectorRegistry = createConnectorRegistry();
connectorRegistry.register(makeConnectedTelegramConnector());
registerConnectorRegistry(connectedRuntime, connectorRegistry);
const connectedChannelRegistry = createChannelRegistry();
registerDefaultChannelPack(connectedChannelRegistry, connectedRuntime);
installFirstRunChannelInspector(connectedRuntime, connectedChannelRegistry);
const disconnectedRuntime = createMinimalRuntimeStub();
const disconnectedChannelRegistry = createChannelRegistry();
registerDefaultChannelPack(
disconnectedChannelRegistry,
disconnectedRuntime,
);
installFirstRunChannelInspector(
disconnectedRuntime,
disconnectedChannelRegistry,
);
await expect(
validateChannel("telegram", connectedRuntime),
).resolves.toEqual({
channel: "telegram",
registered: true,
connected: true,
fallbackToInApp: false,
});
await expect(
validateChannel("telegram", disconnectedRuntime),
).resolves.toMatchObject({
channel: "telegram",
registered: true,
connected: false,
fallbackToInApp: true,
});
});
it("rejects an unregistered channel with the right warning", async () => {
const runtime = createMinimalRuntimeStub();
const result = await validateChannel("morse_code", runtime);
expect(result.channel).toBe("in_app");
expect(result.fallbackToInApp).toBe(true);
expect(result.warning).toMatch(/not registered/i);
});
it("seedDefaultPackOnBoot seeds on an already-initialized runtime that never ran first-run", async () => {
const runtime = createMinimalRuntimeStub();
const tasks = new Map<string, ScheduledTask>();
const service = new FirstRunService(runtime, {
runner: makeTrackingRunner(tasks),
});
// No first-run was ever performed (the lifecycle store is `pending`),
// yet boot seeding still materializes the full default pack.
const result = await service.seedDefaultPackOnBoot();
expect(result.seeded.length).toBe(6);
expect(result.skipped.length).toBe(0);
expect(tasks.size).toBe(6);
const slots = new Set(
[...tasks.values()].map((t) => t.metadata?.slot as string),
);
expect(slots).toEqual(
new Set([
"gm",
"gn",
"checkin",
"morningBrief",
"weeklyReview",
"localBackup",
]),
);
// The weekly-review starter stays paused (manual trigger, never fires
// on its own).
const weekly = [...tasks.values()].find(
(t) => t.metadata?.slot === "weeklyReview",
);
expect(weekly?.trigger.kind).toBe("manual");
expect(weekly?.metadata?.pausedByDefault).toBe(true);
});
it("seedDefaultPackOnBoot is idempotent across two boots — no duplicates", async () => {
const runtime = createMinimalRuntimeStub();
const tasks = new Map<string, ScheduledTask>();
const first = await new FirstRunService(runtime, {
runner: makeTrackingRunner(tasks),
}).seedDefaultPackOnBoot();
expect(first.seeded.length).toBe(6);
// Second boot (fresh service instance, same persistent runtime cache):
// every key is already in the seeded marker, so nothing is re-created.
const second = await new FirstRunService(runtime, {
runner: makeTrackingRunner(tasks),
}).seedDefaultPackOnBoot();
expect(second.seeded.length).toBe(0);
expect(second.skipped.length).toBe(6);
expect(tasks.size).toBe(6);
});
it("seedDefaultPackOnBoot respects user deletion — a deleted default is not recreated", async () => {
const runtime = createMinimalRuntimeStub();
const tasks = new Map<string, ScheduledTask>();
await new FirstRunService(runtime, {
runner: makeTrackingRunner(tasks),
}).seedDefaultPackOnBoot();
expect(tasks.size).toBe(6);
// User deletes the weekly-review default out from under the runner.
const weeklyKey = "lifeops:first-run:default:weekly-review";
const weekly = [...tasks.values()].find(
(t) => t.idempotencyKey === weeklyKey,
);
if (!weekly) throw new Error("expected weekly-review default to be seeded");
tasks.delete(weekly.taskId);
expect(tasks.size).toBe(5);
// Next boot must NOT resurrect it — the seeded marker still records the
// key, so the boot seeder skips it.
const reboot = await new FirstRunService(runtime, {
runner: makeTrackingRunner(tasks),
}).seedDefaultPackOnBoot();
expect(reboot.seeded.length).toBe(0);
expect(reboot.skipped).toContain(weeklyKey);
expect(tasks.size).toBe(5);
expect(
[...tasks.values()].some((t) => t.idempotencyKey === weeklyKey),
).toBe(false);
});
it("first-run then boot does not double-seed (shared per-key marker)", async () => {
const runtime = createMinimalRuntimeStub();
const tasks = new Map<string, ScheduledTask>();
// Fresh install runs first-run defaults, which seeds the pack and records
// the marker.
const firstRun = new FirstRunService(runtime, {
runner: makeTrackingRunner(tasks),
});
await firstRun.runDefaultsPath({ wakeTime: "6:30am" });
expect(tasks.size).toBe(6);
// A subsequent boot seeder sees every key already seeded → no double-seed.
const boot = await new FirstRunService(runtime, {
runner: makeTrackingRunner(tasks),
}).seedDefaultPackOnBoot();
expect(boot.seeded.length).toBe(0);
expect(boot.skipped.length).toBe(6);
expect(tasks.size).toBe(6);
});
it("FirstRunService produces shape-valid tasks via the in-memory runner", async () => {
const runtime = createMinimalRuntimeStub();
const stateStore = createFirstRunStateStore(runtime);
const factStore = createOwnerFactStore(runtime);
const recorded: ScheduledTask[] = [];
const service = new FirstRunService(runtime, {
stateStore,
factStore,
runner: {
async schedule(input) {
asScheduledTask(input);
const task: ScheduledTask = {
...input,
taskId: `t-${recorded.length}`,
state: { status: "scheduled", followupCount: 0 },
};
recorded.push(task);
return task;
},
},
});
// First call without a wake time returns the question.
const ask = await service.runDefaultsPath({});
expect(ask.status).toBe("needs_more_input");
expect(ask.awaitingQuestion).toBe("wakeTime");
const done = await service.runDefaultsPath({ wakeTime: "6:30am" });
expect(done.status).toBe("ok");
expect(done.scheduledTasks.length).toBe(6);
expect(recorded.length).toBe(6);
expect(done.facts.morningWindow?.startLocal).toBe("06:30");
});
});