mirror of
https://github.com/openclaw/openclaw.git
synced 2026-08-08 02:52:15 +00:00
fix(ci): align scheduled e2e checks with sqlite sessions
This commit is contained in:
@@ -1197,7 +1197,7 @@ jobs:
|
||||
export OPENCLAW_DOCKER_ALL_TIMINGS_FILE=".artifacts/docker-tests/release-${DOCKER_E2E_CHUNK}-timings.json"
|
||||
export OPENCLAW_DOCKER_ALL_PNPM_COMMAND="$(command -v pnpm)"
|
||||
if [[ "${{ steps.plan.outputs.needs_live_image }}" == "1" ]]; then
|
||||
OPENCLAW_DOCKER_BUILD_ON_MISSING=1 OPENCLAW_LIVE_DOCKER_REPO_ROOT="$GITHUB_WORKSPACE" bash .release-harness/scripts/test-live-build-docker.sh
|
||||
OPENCLAW_SKIP_DOCKER_BUILD=0 OPENCLAW_LIVE_DOCKER_REPO_ROOT="$GITHUB_WORKSPACE" bash .release-harness/scripts/test-live-build-docker.sh
|
||||
fi
|
||||
|
||||
node .release-harness/scripts/test-docker-all.mjs
|
||||
@@ -1525,7 +1525,7 @@ jobs:
|
||||
export OPENCLAW_DOCKER_ALL_TIMINGS_FILE=".artifacts/docker-tests/targeted-${ARTIFACT_SUFFIX}-timings.json"
|
||||
export OPENCLAW_DOCKER_ALL_PNPM_COMMAND="$(command -v pnpm)"
|
||||
if [[ "${{ steps.plan.outputs.needs_live_image }}" == "1" ]]; then
|
||||
OPENCLAW_DOCKER_BUILD_ON_MISSING=1 OPENCLAW_LIVE_DOCKER_REPO_ROOT="$GITHUB_WORKSPACE" bash .release-harness/scripts/test-live-build-docker.sh
|
||||
OPENCLAW_SKIP_DOCKER_BUILD=0 OPENCLAW_LIVE_DOCKER_REPO_ROOT="$GITHUB_WORKSPACE" bash .release-harness/scripts/test-live-build-docker.sh
|
||||
fi
|
||||
export OPENCLAW_DOCKER_ALL_BUILD=0
|
||||
|
||||
@@ -1717,7 +1717,7 @@ jobs:
|
||||
export OPENCLAW_DOCKER_ALL_TIMINGS_FILE=".artifacts/docker-tests/release-openwebui-timings.json"
|
||||
export OPENCLAW_DOCKER_ALL_PNPM_COMMAND="$(command -v pnpm)"
|
||||
if [[ "${{ steps.plan.outputs.needs_live_image }}" == "1" ]]; then
|
||||
OPENCLAW_DOCKER_BUILD_ON_MISSING=1 OPENCLAW_LIVE_DOCKER_REPO_ROOT="$GITHUB_WORKSPACE" bash .release-harness/scripts/test-live-build-docker.sh
|
||||
OPENCLAW_SKIP_DOCKER_BUILD=0 OPENCLAW_LIVE_DOCKER_REPO_ROOT="$GITHUB_WORKSPACE" bash .release-harness/scripts/test-live-build-docker.sh
|
||||
fi
|
||||
|
||||
node .release-harness/scripts/test-docker-all.mjs
|
||||
|
||||
@@ -1,6 +1,7 @@
|
||||
// Assertions for live plugin tool E2E scenarios.
|
||||
import fs from "node:fs";
|
||||
import path from "node:path";
|
||||
import { DatabaseSync } from "node:sqlite";
|
||||
import { extractAgentReplyTexts } from "../agent-turn-output.mjs";
|
||||
import { readPluginInstallRecords } from "../plugin-index-sqlite.mjs";
|
||||
import { readTextFileTail, tailText } from "../text-file-utils.mjs";
|
||||
@@ -33,6 +34,7 @@ const AGENT_OUTPUT_MAX_BYTES = readPositiveIntEnv(
|
||||
1024 * 1024,
|
||||
);
|
||||
const SESSION_FILE_LIST_LIMIT = 20;
|
||||
const LIVE_PLUGIN_TOOL_SESSION_ID = "live-plugin-tool";
|
||||
const SESSION_SCAN_MAX_ENTRIES = readPositiveIntEnv(
|
||||
"OPENCLAW_LIVE_PLUGIN_TOOL_SESSION_SCAN_MAX_ENTRIES",
|
||||
50_000,
|
||||
@@ -373,6 +375,41 @@ function scanSessionTranscripts(sessionsDir, toolName, expected) {
|
||||
return { checkedFiles, filesChecked, found: false, missingDir: false };
|
||||
}
|
||||
|
||||
function scanSqliteSessionTranscript(databasePath, sessionId, toolName, expected) {
|
||||
if (!fs.existsSync(databasePath)) {
|
||||
return { eventsChecked: 0, found: false };
|
||||
}
|
||||
const database = new DatabaseSync(databasePath, { readOnly: true });
|
||||
try {
|
||||
const table = database
|
||||
.prepare("SELECT name FROM sqlite_master WHERE type = 'table' AND name = 'transcript_events'")
|
||||
.get();
|
||||
if (!table) {
|
||||
return { eventsChecked: 0, found: false };
|
||||
}
|
||||
const rows = database
|
||||
.prepare("SELECT event_json FROM transcript_events WHERE session_id = ? ORDER BY seq LIMIT ?")
|
||||
.all(sessionId, SESSION_SCAN_MAX_ENTRIES + 1);
|
||||
if (rows.length > SESSION_SCAN_MAX_ENTRIES) {
|
||||
throw new Error(`session transcript scan exceeded ${SESSION_SCAN_MAX_ENTRIES} SQLite events`);
|
||||
}
|
||||
|
||||
const tracker = createToolEvidenceTracker(toolName, expected);
|
||||
for (const row of rows) {
|
||||
if (typeof row.event_json !== "string") {
|
||||
continue;
|
||||
}
|
||||
const message = transcriptMessageFromLine(row.event_json);
|
||||
if (message && tracker.recordMessage(message)) {
|
||||
return { eventsChecked: rows.length, found: true };
|
||||
}
|
||||
}
|
||||
return { eventsChecked: rows.length, found: false };
|
||||
} finally {
|
||||
database.close();
|
||||
}
|
||||
}
|
||||
|
||||
function realPathMaybe(filePath) {
|
||||
try {
|
||||
return fs.realpathSync(filePath);
|
||||
@@ -610,13 +647,22 @@ function assertAgentTurn() {
|
||||
`live agent reply did not contain tool slug ${expected}:\nstdout tail=${tailText(stdout, ERROR_DETAIL_TAIL_BYTES)}\nstderr tail=${stderrTail}`,
|
||||
);
|
||||
}
|
||||
const sessionsDir = path.join(stateDir(), "agents", "main", "sessions");
|
||||
const scan = scanSessionTranscripts(sessionsDir, toolName, expected);
|
||||
if (!scan.found) {
|
||||
const checkedFiles = scan.checkedFiles.length > 0 ? scan.checkedFiles.join(", ") : "<none>";
|
||||
const missingDir = scan.missingDir ? " sessions directory was missing." : "";
|
||||
const agentStateDir = path.join(stateDir(), "agents", "main");
|
||||
const sqliteScan = scanSqliteSessionTranscript(
|
||||
path.join(agentStateDir, "agent", "openclaw-agent.sqlite"),
|
||||
LIVE_PLUGIN_TOOL_SESSION_ID,
|
||||
toolName,
|
||||
expected,
|
||||
);
|
||||
const fileScan = sqliteScan.found
|
||||
? { checkedFiles: [], filesChecked: 0, found: false, missingDir: false }
|
||||
: scanSessionTranscripts(path.join(agentStateDir, "sessions"), toolName, expected);
|
||||
if (!sqliteScan.found && !fileScan.found) {
|
||||
const checkedFiles =
|
||||
fileScan.checkedFiles.length > 0 ? fileScan.checkedFiles.join(", ") : "<none>";
|
||||
const missingDir = fileScan.missingDir ? " sessions directory was missing." : "";
|
||||
throw new Error(
|
||||
`session transcript did not show ${toolName} returning ${expected}; missing causal tool-result evidence after checking ${scan.filesChecked} jsonl file(s): ${checkedFiles}.${missingDir}`,
|
||||
`session transcript did not show ${toolName} returning ${expected}; missing causal tool-result evidence after checking ${sqliteScan.eventsChecked} SQLite event(s) and ${fileScan.filesChecked} jsonl file(s): ${checkedFiles}.${missingDir}`,
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -478,20 +478,11 @@ function assertSessionMetadataMigrated(stateDir) {
|
||||
const legacyStorePath = path.join(stateDir, "sessions", "sessions.json");
|
||||
const agentSessionsDir = path.join(stateDir, "agents", "main", "sessions");
|
||||
const targetStorePath = path.join(agentSessionsDir, "sessions.json");
|
||||
const dbPath = path.join(stateDir, "agents", "main", "agent", "openclaw-agent.sqlite");
|
||||
assert(
|
||||
!fs.existsSync(legacyStorePath),
|
||||
`legacy sessions.json survived migration: ${legacyStorePath}`,
|
||||
);
|
||||
for (const sessionId of [
|
||||
LEGACY_SESSION_MAIN_ID,
|
||||
LEGACY_SESSION_DIRECT_ID,
|
||||
LEGACY_SESSION_GROUP_ID,
|
||||
]) {
|
||||
assert(
|
||||
fs.existsSync(path.join(agentSessionsDir, `${sessionId}.jsonl`)),
|
||||
`legacy session transcript was not moved for ${sessionId}`,
|
||||
);
|
||||
}
|
||||
|
||||
const store = readMigratedSessionStore(stateDir, targetStorePath);
|
||||
const main = store["agent:main:main"];
|
||||
@@ -500,18 +491,44 @@ function assertSessionMetadataMigrated(stateDir) {
|
||||
assert(main?.sessionId === LEGACY_SESSION_MAIN_ID, "main legacy session row missing");
|
||||
assert(direct?.sessionId === LEGACY_SESSION_DIRECT_ID, "direct legacy session row missing");
|
||||
assert(group?.sessionId === LEGACY_SESSION_GROUP_ID, "channel legacy session row missing");
|
||||
assert(
|
||||
main?.sessionFile === path.join(agentSessionsDir, `${LEGACY_SESSION_MAIN_ID}.jsonl`),
|
||||
"main legacy session row still points at the old sessions directory",
|
||||
);
|
||||
assert(
|
||||
direct?.sessionFile === path.join(agentSessionsDir, `${LEGACY_SESSION_DIRECT_ID}.jsonl`),
|
||||
"direct legacy session row still points at the old sessions directory",
|
||||
);
|
||||
assert(
|
||||
group?.sessionFile === path.join(agentSessionsDir, `${LEGACY_SESSION_GROUP_ID}.jsonl`),
|
||||
"channel legacy session row still points at the old sessions directory",
|
||||
);
|
||||
const migratedSessionIds = [
|
||||
LEGACY_SESSION_MAIN_ID,
|
||||
LEGACY_SESSION_DIRECT_ID,
|
||||
LEGACY_SESSION_GROUP_ID,
|
||||
];
|
||||
if (fs.existsSync(dbPath)) {
|
||||
const db = new DatabaseSync(dbPath, { readOnly: true });
|
||||
try {
|
||||
const count = db.prepare(
|
||||
"SELECT COUNT(*) AS count FROM transcript_events WHERE session_id = ?",
|
||||
);
|
||||
for (const sessionId of migratedSessionIds) {
|
||||
const row = count.get(sessionId);
|
||||
assert(
|
||||
Number(row?.count ?? 0) > 0,
|
||||
`legacy session transcript was not imported for ${sessionId}`,
|
||||
);
|
||||
}
|
||||
} finally {
|
||||
db.close();
|
||||
}
|
||||
} else {
|
||||
for (const [sessionId, entry] of [
|
||||
[LEGACY_SESSION_MAIN_ID, main],
|
||||
[LEGACY_SESSION_DIRECT_ID, direct],
|
||||
[LEGACY_SESSION_GROUP_ID, group],
|
||||
]) {
|
||||
const expectedPath = path.join(agentSessionsDir, `${sessionId}.jsonl`);
|
||||
assert(
|
||||
fs.existsSync(expectedPath),
|
||||
`legacy session transcript was not moved for ${sessionId}`,
|
||||
);
|
||||
assert(
|
||||
entry?.sessionFile === expectedPath,
|
||||
`legacy session row still points at the old sessions directory for ${sessionId}`,
|
||||
);
|
||||
}
|
||||
}
|
||||
assert(
|
||||
main.skillsSnapshot?.prompt === "legacy prompt survives as metadata",
|
||||
"legacy session metadata prompt was not preserved",
|
||||
@@ -533,15 +550,28 @@ function readMigratedSessionStore(stateDir, targetStorePath) {
|
||||
let db;
|
||||
try {
|
||||
db = new DatabaseSync(dbPath, { readOnly: true });
|
||||
const rows = db
|
||||
.prepare("SELECT key, value_json FROM cache_entries WHERE scope = ?")
|
||||
.all("session_entries");
|
||||
const hasSessionEntries = db
|
||||
.prepare("SELECT name FROM sqlite_master WHERE type = 'table' AND name = 'session_entries'")
|
||||
.get();
|
||||
const rows = hasSessionEntries
|
||||
? db
|
||||
.prepare(
|
||||
`SELECT se.session_key AS key, sr.session_id, se.entry_json AS value_json
|
||||
FROM session_entries AS se
|
||||
INNER JOIN session_routes AS sr ON sr.session_key = se.session_key`,
|
||||
)
|
||||
.all()
|
||||
: db
|
||||
.prepare("SELECT key, value_json FROM cache_entries WHERE scope = ?")
|
||||
.all("session_entries");
|
||||
const store = {};
|
||||
for (const row of rows) {
|
||||
if (typeof row?.key !== "string" || typeof row?.value_json !== "string") {
|
||||
continue;
|
||||
}
|
||||
store[row.key] = JSON.parse(row.value_json);
|
||||
const entry = JSON.parse(row.value_json);
|
||||
store[row.key] =
|
||||
typeof row.session_id === "string" ? { ...entry, sessionId: row.session_id } : entry;
|
||||
}
|
||||
return store;
|
||||
} finally {
|
||||
|
||||
@@ -150,7 +150,7 @@ openclaw_e2e_enable_openclaw_cli_timeout
|
||||
fixture_dir="$(mktemp -d /tmp/openclaw-live-plugin-tool.XXXXXX)"
|
||||
plugin_dir="$fixture_dir/package"
|
||||
mkdir -p "$plugin_dir"
|
||||
node scripts/e2e/lib/live-plugin-tool/assertions.mjs write-fixture "$plugin_dir"
|
||||
node --disable-warning=ExperimentalWarning scripts/e2e/lib/live-plugin-tool/assertions.mjs write-fixture "$plugin_dir"
|
||||
(cd "$plugin_dir" && npm pack --pack-destination "$fixture_dir" --silent) \
|
||||
>/tmp/openclaw-live-plugin-tool-pack.log 2>&1
|
||||
plugin_tgzs=()
|
||||
@@ -166,11 +166,11 @@ plugin_tgz="${plugin_tgzs[0]}"
|
||||
|
||||
echo "Installing fixture plugin from npm-pack: $plugin_tgz"
|
||||
openclaw plugins install "npm-pack:$plugin_tgz" --force >/tmp/openclaw-plugin-install.log 2>&1
|
||||
node scripts/e2e/lib/live-plugin-tool/assertions.mjs configure
|
||||
node --disable-warning=ExperimentalWarning scripts/e2e/lib/live-plugin-tool/assertions.mjs configure
|
||||
openclaw plugins enable "$PLUGIN_ID" >/tmp/openclaw-plugin-enable.log 2>&1
|
||||
openclaw plugins list --json >/tmp/openclaw-plugins-list.json
|
||||
openclaw plugins inspect "$PLUGIN_ID" --runtime --json >/tmp/openclaw-plugin-inspect.json
|
||||
node scripts/e2e/lib/live-plugin-tool/assertions.mjs assert-installed
|
||||
node --disable-warning=ExperimentalWarning scripts/e2e/lib/live-plugin-tool/assertions.mjs assert-installed
|
||||
|
||||
echo "Running live OpenAI agent turn that must call $TOOL_NAME..."
|
||||
openclaw agent --local \
|
||||
@@ -182,7 +182,7 @@ openclaw agent --local \
|
||||
--timeout "${OPENCLAW_LIVE_PLUGIN_TOOL_TIMEOUT_SECONDS:-300}" \
|
||||
--json >/tmp/openclaw-agent.json 2>/tmp/openclaw-agent.err
|
||||
|
||||
node scripts/e2e/lib/live-plugin-tool/assertions.mjs assert-agent-turn
|
||||
node --disable-warning=ExperimentalWarning scripts/e2e/lib/live-plugin-tool/assertions.mjs assert-agent-turn
|
||||
|
||||
echo "Live plugin tool Docker E2E passed"
|
||||
EOF
|
||||
|
||||
@@ -5,7 +5,9 @@ import { spawnSync } from "node:child_process";
|
||||
import fs from "node:fs/promises";
|
||||
import os from "node:os";
|
||||
import path from "node:path";
|
||||
import { DatabaseSync } from "node:sqlite";
|
||||
import { SessionManager } from "openclaw/plugin-sdk/agent-sessions";
|
||||
import { readSessionTranscriptEvents } from "openclaw/plugin-sdk/session-transcript-runtime";
|
||||
import {
|
||||
buildRuntimeContextCustomMessage,
|
||||
resolveRuntimeContextPromptParts,
|
||||
@@ -32,14 +34,6 @@ function setEnvValue(key: string, value: string): void {
|
||||
Reflect.set(process.env, key, value);
|
||||
}
|
||||
|
||||
async function readJsonl(filePath: string): Promise<TranscriptEntry[]> {
|
||||
const raw = await fs.readFile(filePath, "utf-8");
|
||||
return raw
|
||||
.split(/\r?\n/)
|
||||
.filter(Boolean)
|
||||
.map((line) => JSON.parse(line) as TranscriptEntry);
|
||||
}
|
||||
|
||||
function messageText(content: unknown): string {
|
||||
if (typeof content === "string") {
|
||||
return content;
|
||||
@@ -91,7 +85,7 @@ async function verifyRuntimeContextTranscriptShape(root: string) {
|
||||
timestamp: Date.now() + 1,
|
||||
});
|
||||
|
||||
const entries = await readJsonl(sessionFile);
|
||||
const entries = sessionManager.getEntries() as TranscriptEntry[];
|
||||
const customEntry = entries.find((entry) => entry.type === "custom_message");
|
||||
assert(!customEntry, "runtime custom message should not be persisted without its user turn");
|
||||
assert(
|
||||
@@ -218,7 +212,25 @@ async function verifyDoctorRepair(root: string) {
|
||||
result.status === 0,
|
||||
`doctor --fix failed\nstdout:\n${result.stdout}\nstderr:\n${result.stderr}`,
|
||||
);
|
||||
const entries = await readJsonl(sessionFile);
|
||||
const databasePath = path.join(stateDir, "agents", "main", "agent", "openclaw-agent.sqlite");
|
||||
const database = new DatabaseSync(databasePath, { readOnly: true });
|
||||
let migratedSessionId: string | undefined;
|
||||
try {
|
||||
const row = database
|
||||
.prepare("SELECT session_id FROM session_routes WHERE session_key = ?")
|
||||
.get("agent:main:qa:docker-runtime-context");
|
||||
if (typeof row?.session_id === "string") {
|
||||
migratedSessionId = row.session_id;
|
||||
}
|
||||
} finally {
|
||||
database.close();
|
||||
}
|
||||
assert(migratedSessionId, "doctor did not migrate session");
|
||||
const entries = (await readSessionTranscriptEvents({
|
||||
agentId: "main",
|
||||
sessionId: migratedSessionId,
|
||||
sessionKey: "agent:main:qa:docker-runtime-context",
|
||||
})) as TranscriptEntry[];
|
||||
const ids = entries.map((entryValue) => (entryValue as { id?: string }).id).filter(Boolean);
|
||||
assert(
|
||||
JSON.stringify(ids) ===
|
||||
|
||||
@@ -3,6 +3,7 @@ import { spawnSync } from "node:child_process";
|
||||
import { mkdirSync, mkdtempSync, readFileSync, rmSync, writeFileSync } from "node:fs";
|
||||
import { tmpdir } from "node:os";
|
||||
import path from "node:path";
|
||||
import { DatabaseSync } from "node:sqlite";
|
||||
import { describe, expect, it } from "vitest";
|
||||
|
||||
const ASSERTIONS_SCRIPT = "scripts/e2e/lib/live-plugin-tool/assertions.mjs";
|
||||
@@ -68,7 +69,7 @@ describe("live plugin tool assertions", () => {
|
||||
OPENCLAW_LIVE_PLUGIN_TOOL_TIMEOUT_SECONDS: "240",
|
||||
});
|
||||
|
||||
expect(result.status).toBe(0);
|
||||
expect(result.status, result.stderr).toBe(0);
|
||||
const config = JSON.parse(readFileSync(path.join(root, "state", "openclaw.json"), "utf8"));
|
||||
expect(config.models.providers.openai.timeoutSeconds).toBe(240);
|
||||
expect(config.agents.defaults.timeoutSeconds).toBe(240);
|
||||
@@ -115,7 +116,75 @@ describe("live plugin tool assertions", () => {
|
||||
|
||||
const result = runAssertion(root);
|
||||
|
||||
expect(result.status).toBe(0);
|
||||
expect(result.status, result.stderr).toBe(0);
|
||||
expect(result.stderr).toBe("");
|
||||
} finally {
|
||||
rmSync(root, { force: true, recursive: true });
|
||||
}
|
||||
});
|
||||
|
||||
it("reads causal tool evidence from the canonical SQLite transcript", () => {
|
||||
const root = mkdtempSync(path.join(tmpdir(), "openclaw-live-plugin-tool-"));
|
||||
const databasePath = path.join(
|
||||
root,
|
||||
"state",
|
||||
"agents",
|
||||
"main",
|
||||
"agent",
|
||||
"openclaw-agent.sqlite",
|
||||
);
|
||||
|
||||
try {
|
||||
writeJson(path.join(root, "agent.json"), {
|
||||
payloads: [{ text: "live-plugin-slug" }],
|
||||
});
|
||||
mkdirSync(path.dirname(databasePath), { recursive: true });
|
||||
const database = new DatabaseSync(databasePath);
|
||||
try {
|
||||
database.exec(`
|
||||
CREATE TABLE transcript_events (
|
||||
session_id TEXT NOT NULL,
|
||||
seq INTEGER NOT NULL,
|
||||
event_json TEXT NOT NULL
|
||||
)
|
||||
`);
|
||||
const insert = database.prepare(
|
||||
"INSERT INTO transcript_events (session_id, seq, event_json) VALUES (?, ?, ?)",
|
||||
);
|
||||
insert.run(
|
||||
"live-plugin-tool",
|
||||
1,
|
||||
JSON.stringify({
|
||||
message: {
|
||||
role: "assistant",
|
||||
content: [
|
||||
{
|
||||
type: "tool_use",
|
||||
id: "call-live-plugin-tool",
|
||||
name: "e2e_slug_probe",
|
||||
},
|
||||
],
|
||||
},
|
||||
}),
|
||||
);
|
||||
insert.run(
|
||||
"live-plugin-tool",
|
||||
2,
|
||||
JSON.stringify({
|
||||
message: {
|
||||
role: "tool",
|
||||
tool_call_id: "call-live-plugin-tool",
|
||||
content: "live-plugin-slug",
|
||||
},
|
||||
}),
|
||||
);
|
||||
} finally {
|
||||
database.close();
|
||||
}
|
||||
|
||||
const result = runAssertion(root);
|
||||
|
||||
expect(result.status, result.stderr).toBe(0);
|
||||
expect(result.stderr).toBe("");
|
||||
} finally {
|
||||
rmSync(root, { force: true, recursive: true });
|
||||
@@ -162,7 +231,7 @@ describe("live plugin tool assertions", () => {
|
||||
|
||||
expect(result.status).not.toBe(0);
|
||||
expect(result.stderr).toContain("session transcript did not show");
|
||||
expect(result.stderr).toContain("after checking 2 jsonl file(s)");
|
||||
expect(result.stderr).toContain("0 SQLite event(s) and 2 jsonl file(s)");
|
||||
} finally {
|
||||
rmSync(root, { force: true, recursive: true });
|
||||
}
|
||||
@@ -322,7 +391,7 @@ describe("live plugin tool assertions", () => {
|
||||
|
||||
expect(result.status).not.toBe(0);
|
||||
expect(result.stderr).toContain("session transcript did not show");
|
||||
expect(result.stderr).toContain("after checking 1 jsonl file(s)");
|
||||
expect(result.stderr).toContain("0 SQLite event(s) and 1 jsonl file(s)");
|
||||
expect(result.stderr).toContain("session.jsonl");
|
||||
expect(result.stderr).not.toContain("DO_NOT_DUMP_SESSION_CONTENT");
|
||||
} finally {
|
||||
|
||||
@@ -188,6 +188,23 @@ function expectReadOnlyPackagePermission(workflowJob: WorkflowJob): void {
|
||||
}
|
||||
|
||||
describe("release validation no-push transport", () => {
|
||||
it("builds planned live images locally without entering pull fallback", () => {
|
||||
const workflow = readWorkflow(LIVE_E2E);
|
||||
for (const jobName of [
|
||||
"validate_docker_e2e",
|
||||
"validate_docker_lanes",
|
||||
"validate_docker_openwebui",
|
||||
]) {
|
||||
const job = workflow.jobs?.[jobName];
|
||||
const runStep = job?.steps?.find((candidate) =>
|
||||
candidate.run?.includes("test-live-build-docker.sh"),
|
||||
);
|
||||
|
||||
expect(runStep?.run, jobName).toContain("OPENCLAW_SKIP_DOCKER_BUILD=0");
|
||||
expect(runStep?.run, jobName).not.toContain("OPENCLAW_DOCKER_BUILD_ON_MISSING=1");
|
||||
}
|
||||
});
|
||||
|
||||
it("keeps every local reusable-workflow permission request within its caller ceiling", () => {
|
||||
const readOnlyCalls = [
|
||||
[PLUGIN_PRERELEASE, "plugin-prerelease-docker-suite"],
|
||||
|
||||
@@ -15,79 +15,88 @@ function writeJson(path: string, value: unknown): void {
|
||||
function writeMigratedSessionState(stateDir: string): void {
|
||||
const agentSessionsDir = join(stateDir, "agents", "main", "sessions");
|
||||
const agentDbDir = join(stateDir, "agents", "main", "agent");
|
||||
const mainSessionFile = join(agentSessionsDir, "upgrade-main-session.jsonl");
|
||||
const directSessionFile = join(agentSessionsDir, "upgrade-direct-session.jsonl");
|
||||
const groupSessionFile = join(agentSessionsDir, "upgrade-group-session.jsonl");
|
||||
mkdirSync(agentSessionsDir, { recursive: true });
|
||||
mkdirSync(agentDbDir, { recursive: true });
|
||||
writeFileSync(mainSessionFile, '{"type":"main"}\n');
|
||||
writeFileSync(directSessionFile, '{"type":"direct"}\n');
|
||||
writeFileSync(groupSessionFile, '{"type":"group"}\n');
|
||||
writeJson(join(agentSessionsDir, "sessions.json"), {
|
||||
"agent:main:main": {
|
||||
sessionFile: mainSessionFile,
|
||||
sessionId: "upgrade-main-session",
|
||||
skillsSnapshot: {
|
||||
prompt: "legacy prompt survives as metadata",
|
||||
},
|
||||
},
|
||||
"agent:main:+15551234567": {
|
||||
sessionFile: directSessionFile,
|
||||
sessionId: "upgrade-direct-session",
|
||||
},
|
||||
"agent:main:slack:channel:cupgrade": {
|
||||
sessionFile: groupSessionFile,
|
||||
sessionId: "upgrade-group-session",
|
||||
},
|
||||
});
|
||||
|
||||
const db = new DatabaseSync(join(agentDbDir, "openclaw-agent.sqlite"));
|
||||
try {
|
||||
db.exec(`
|
||||
CREATE TABLE IF NOT EXISTS cache_entries (
|
||||
scope TEXT NOT NULL,
|
||||
key TEXT NOT NULL,
|
||||
value_json TEXT,
|
||||
blob BLOB,
|
||||
expires_at INTEGER,
|
||||
CREATE TABLE sessions (
|
||||
session_id TEXT PRIMARY KEY,
|
||||
session_key TEXT NOT NULL,
|
||||
created_at INTEGER NOT NULL,
|
||||
updated_at INTEGER NOT NULL
|
||||
);
|
||||
CREATE TABLE session_routes (
|
||||
session_key TEXT PRIMARY KEY,
|
||||
session_id TEXT NOT NULL,
|
||||
updated_at INTEGER NOT NULL,
|
||||
PRIMARY KEY (scope, key)
|
||||
FOREIGN KEY (session_id) REFERENCES sessions(session_id) ON DELETE CASCADE
|
||||
);
|
||||
CREATE TABLE session_entries (
|
||||
session_key TEXT PRIMARY KEY,
|
||||
session_id TEXT NOT NULL,
|
||||
entry_json TEXT NOT NULL,
|
||||
updated_at INTEGER NOT NULL,
|
||||
FOREIGN KEY (session_id) REFERENCES sessions(session_id) ON DELETE CASCADE
|
||||
);
|
||||
CREATE TABLE transcript_events (
|
||||
session_id TEXT NOT NULL,
|
||||
seq INTEGER NOT NULL,
|
||||
event_json TEXT NOT NULL,
|
||||
created_at INTEGER NOT NULL,
|
||||
PRIMARY KEY (session_id, seq),
|
||||
FOREIGN KEY (session_id) REFERENCES sessions(session_id) ON DELETE CASCADE
|
||||
);
|
||||
`);
|
||||
const insert = db.prepare(`
|
||||
INSERT INTO cache_entries (scope, key, value_json, updated_at)
|
||||
const insertSession = db.prepare(`
|
||||
INSERT INTO sessions (session_id, session_key, created_at, updated_at)
|
||||
VALUES (?, ?, ?, ?)
|
||||
`);
|
||||
insert.run(
|
||||
"session_entries",
|
||||
"agent:main:main",
|
||||
JSON.stringify({
|
||||
sessionFile: mainSessionFile,
|
||||
sessionId: "upgrade-main-session",
|
||||
skillsSnapshot: {
|
||||
prompt: "legacy prompt survives as metadata",
|
||||
const insertRoute = db.prepare(`
|
||||
INSERT INTO session_routes (session_key, session_id, updated_at)
|
||||
VALUES (?, ?, ?)
|
||||
`);
|
||||
const insertEntry = db.prepare(`
|
||||
INSERT INTO session_entries (session_key, session_id, entry_json, updated_at)
|
||||
VALUES (?, ?, ?, ?)
|
||||
`);
|
||||
const insertTranscript = db.prepare(`
|
||||
INSERT INTO transcript_events (session_id, seq, event_json, created_at)
|
||||
VALUES (?, ?, ?, ?)
|
||||
`);
|
||||
const migratedSessions = [
|
||||
{
|
||||
entry: {
|
||||
skillsSnapshot: {
|
||||
prompt: "legacy prompt survives as metadata",
|
||||
},
|
||||
},
|
||||
}),
|
||||
1710000000000,
|
||||
);
|
||||
insert.run(
|
||||
"session_entries",
|
||||
"agent:main:+15551234567",
|
||||
JSON.stringify({
|
||||
sessionFile: directSessionFile,
|
||||
sessionId: "upgrade-main-session",
|
||||
sessionKey: "agent:main:main",
|
||||
},
|
||||
{
|
||||
entry: {},
|
||||
sessionId: "upgrade-direct-session",
|
||||
}),
|
||||
1710000000100,
|
||||
);
|
||||
insert.run(
|
||||
"session_entries",
|
||||
"agent:main:slack:channel:cupgrade",
|
||||
JSON.stringify({
|
||||
sessionFile: groupSessionFile,
|
||||
sessionKey: "agent:main:+15551234567",
|
||||
},
|
||||
{
|
||||
entry: {},
|
||||
sessionId: "upgrade-group-session",
|
||||
}),
|
||||
1710000000200,
|
||||
);
|
||||
sessionKey: "agent:main:slack:channel:cupgrade",
|
||||
},
|
||||
];
|
||||
for (const { entry, sessionId, sessionKey } of migratedSessions) {
|
||||
insertSession.run(sessionId, sessionKey, 1710000000000, 1710000000000);
|
||||
insertRoute.run(sessionKey, sessionId, 1710000000000);
|
||||
insertEntry.run(sessionKey, sessionId, JSON.stringify(entry), 1710000000000);
|
||||
insertTranscript.run(
|
||||
sessionId,
|
||||
1,
|
||||
JSON.stringify({ type: "session", id: sessionId }),
|
||||
1710000000000,
|
||||
);
|
||||
}
|
||||
} finally {
|
||||
db.close();
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user