Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
39 changes: 39 additions & 0 deletions src/lib/onboard/machine/handlers/sandbox.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -549,6 +549,45 @@ describe("handleSandboxState", () => {
expect(getSession().messagingPlan?.credentialBindings[0]?.credentialHash).toBe(newHash);
});

it("refreshes credential hashes when restoring a registry plan for rebuild resume", async () => {
const oldHash = hashCredential("telegram-token-a");
const newHash = hashCredential("telegram-token-b");
const registryPlan = withTelegramCredentialHash(
makeMinimalPlan("my-assistant", "openclaw", ["telegram"]),
oldHash,
);
const session = createSession({ sandboxName: "my-assistant", messagingPlan: registryPlan });
const getRecordedMessagingChannelsForResume = vi.fn(() => ["telegram"]);
const writePlanToEnv = vi.fn();
const { deps, calls, getSession } = createDeps({
getRecordedMessagingChannelsForResume,
writePlanToEnv,
readMessagingPlanFromEnv: () => null,
getRegistrySandboxMessagingPlan: () => registryPlan,
});

await withEnv("TELEGRAM_BOT_TOKEN", "telegram-token-b", async () => {
await handleSandboxState({
...baseOptions(deps, session),
resume: true,
sandboxName: "my-assistant",
});
});

expect(calls.setupMessaging).not.toHaveBeenCalled();
expect(writePlanToEnv).toHaveBeenCalledWith(
expect.objectContaining({
credentialBindings: [
expect.objectContaining({
providerEnvKey: "TELEGRAM_BOT_TOKEN",
credentialHash: newHash,
}),
],
}),
);
expect(getSession().messagingPlan?.credentialBindings[0]?.credentialHash).toBe(newHash);
});

it("preserves an empty env-staged rebuild plan instead of rediscovering token-backed channels", async () => {
const emptyRebuildPlan = makeMinimalPlan("my-assistant");
const session = createSession({ sandboxName: "my-assistant", messagingPlan: emptyRebuildPlan });
Expand Down
64 changes: 64 additions & 0 deletions test/e2e-scenario/live/onboard-resume.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -60,10 +60,22 @@ interface SessionStateComplete {
>;
}

interface MutableSessionState extends Record<string, unknown> {
status?: string;
resumable?: boolean;
}

function readSession<T>(file: string): T {
return JSON.parse(fs.readFileSync(file, "utf8")) as T;
}

function markSessionInProgress(file: string): void {
const session = readSession<MutableSessionState>(file);
session.status = "in_progress";
session.resumable = true;
fs.writeFileSync(file, JSON.stringify(session, null, 2), "utf8");
}

function interruptedSessionSummary(session: SessionStateInterrupted): Record<string, unknown> {
return {
status: session.status,
Expand Down Expand Up @@ -338,5 +350,57 @@ test.skipIf(!shouldRunLiveE2EScenarios())(
expect(fs.existsSync(REGISTRY_FILE)).toBe(true);
const registry = JSON.parse(fs.readFileSync(REGISTRY_FILE, "utf8")) as unknown;
expect(containsExactJsonToken(registry, SANDBOX_NAME)).toBe(true);

// ──────────────────────────────────────────────────────────────────
// Phase 3.5: implicit resume — a plain `onboard` auto-detects an
// in_progress session, and `--fresh` suppresses that auto-resume.
// ──────────────────────────────────────────────────────────────────
markSessionInProgress(SESSION_FILE);
const implicitResumeRun = await host.command(
"node",
[CLI_ENTRYPOINT, "onboard", "--non-interactive"],
{
artifactName: "phase-3-5-onboard-implicit-resume",
env: {
...buildAvailabilityProbeEnv(),
NEMOCLAW_SANDBOX_NAME: SANDBOX_NAME,
NEMOCLAW_POLICY_MODE: "skip",
NEMOCLAW_ACCEPT_THIRD_PARTY_SOFTWARE: "1",
},
redactionValues: [apiKey],
timeoutMs: ONBOARD_TIMEOUT_MS,
},
);
const implicitResumeText = `${implicitResumeRun.stdout}\n${implicitResumeRun.stderr}`;
expect(implicitResumeRun.exitCode, implicitResumeText).toBe(0);
expect(implicitResumeText).toContain("(resume mode)");
expect(
implicitResumeText.includes("[resume] Skipping") ||
implicitResumeText.includes("[reuse] Skipping"),
implicitResumeText,
).toBe(true);

markSessionInProgress(SESSION_FILE);
const freshRun = await host.command(
"node",
[CLI_ENTRYPOINT, "onboard", "--fresh", "--non-interactive"],
{
artifactName: "phase-3-5-onboard-fresh-suppresses-resume",
env: {
...buildAvailabilityProbeEnv(),
NEMOCLAW_SANDBOX_NAME: SANDBOX_NAME,
NEMOCLAW_POLICY_MODE: "skip",
NEMOCLAW_ACCEPT_THIRD_PARTY_SOFTWARE: "1",
NEMOCLAW_E2E_FAILURE_INJECTION: "1",
NEMOCLAW_E2E_FORCE_FAIL_AT_STEP: "preflight",
},
redactionValues: [apiKey],
timeoutMs: ONBOARD_TIMEOUT_MS,
},
);
const freshText = `${freshRun.stdout}\n${freshRun.stderr}`;
expect(freshRun.exitCode, freshText).not.toBe(0);
expect(freshText).toContain("[e2e] Forced onboarding failure at step 'preflight'.");
expect(freshText).not.toContain("(resume mode)");
},
);
12 changes: 9 additions & 3 deletions test/e2e-scenario/live/rebuild-hermes.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -6,15 +6,14 @@ import fs from "node:fs";
import os from "node:os";
import path from "node:path";
import { setTimeout as sleep } from "node:timers/promises";

import { shellQuote } from "../../../src/lib/core/shell-quote";
import { buildAvailabilityProbeEnv } from "../fixtures/availability-env.ts";
import { resultText, type HostCliClient } from "../fixtures/clients/index.ts";
import { type HostCliClient, resultText } from "../fixtures/clients/index.ts";
import { validateSandboxName } from "../fixtures/clients/sandbox.ts";
import { expect, test } from "../fixtures/e2e-test.ts";
import { shouldRunLiveE2EScenarios } from "../fixtures/live-project-gate.ts";
import { listCredentialLeakPaths } from "../fixtures/phases/state-validation.ts";
import type { ShellProbeResult } from "../fixtures/shell-probe.ts";
import { shellQuote } from "../../../src/lib/core/shell-quote";

// Direct Vitest replacement coverage for test/e2e/test-rebuild-hermes.sh.
// The migrated scope is the legacy non-interactive shell regression: install.sh,
Expand Down Expand Up @@ -304,6 +303,13 @@ function seedRegistryAndSession(): SessionArtifactSummary {
agentVersion: OLD_HERMES_REGISTRY_VERSION,
messaging: { schemaVersion: 1, plan: messagingPlan },
};
expect(
Object.prototype.hasOwnProperty.call(
registry.sandboxes[SANDBOX_NAME],
"providerCredentialHashes",
),
"legacy providerCredentialHashes must stay out of the curated rebuild registry; credential fingerprints live on messaging plan bindings",
).toBe(false);
registry.defaultSandbox = SANDBOX_NAME;
writeJsonFile(REGISTRY_FILE, registry);

Expand Down