mirror of
https://github.com/openclaw/openclaw.git
synced 2026-08-12 21:53:00 -06:00
fix(update): accept shipped upgrade state
Recognize the shipped non-STRICT commitments schema, keep direct test state canonical, and require Doctor convergence for legacy agent and Discord config during published upgrades. Co-authored-by: Vincent Koc <vincentkoc@ieee.org>
This commit is contained in:
@@ -407,11 +407,22 @@ function assertConfigSurvived() {
|
||||
}
|
||||
|
||||
if (acceptsIntent(coverage, "agents")) {
|
||||
const stage = process.env.OPENCLAW_UPGRADE_SURVIVOR_ASSERT_STAGE || "survival";
|
||||
const legacyAgents = config.agents?.list ?? [];
|
||||
const mainAgent =
|
||||
config.agents?.entries?.main ?? legacyAgents.find((agent) => agent?.id === "main");
|
||||
const opsAgent =
|
||||
config.agents?.entries?.ops ?? legacyAgents.find((agent) => agent?.id === "ops");
|
||||
const entries = config.agents?.entries;
|
||||
if (stage !== "baseline") {
|
||||
assert(config.agents?.ownership === "explicit", "agent ownership was not canonicalized");
|
||||
assert(entries && typeof entries === "object", "keyed agent entries missing after Doctor");
|
||||
assert(!Object.hasOwn(config.agents ?? {}, "list"), "legacy agent list survived Doctor");
|
||||
for (const entry of Object.values(entries ?? {})) {
|
||||
assert(
|
||||
!Object.hasOwn(entry ?? {}, "default"),
|
||||
"legacy agent default marker survived Doctor",
|
||||
);
|
||||
}
|
||||
}
|
||||
const mainAgent = entries?.main ?? legacyAgents.find((agent) => agent?.id === "main");
|
||||
const opsAgent = entries?.ops ?? legacyAgents.find((agent) => agent?.id === "ops");
|
||||
assert(mainAgent, "main agent missing");
|
||||
assert(opsAgent, "ops agent missing");
|
||||
if (hasCoverage(coverage)) {
|
||||
@@ -467,10 +478,23 @@ function assertConfigSurvived() {
|
||||
}
|
||||
|
||||
if (acceptsIntent(coverage, "discord-channel")) {
|
||||
const stage = process.env.OPENCLAW_UPGRADE_SURVIVOR_ASSERT_STAGE || "survival";
|
||||
const discord = config.channels?.discord;
|
||||
assert(discord?.enabled === true, "discord enabled flag changed");
|
||||
const discordAllowFrom = discord.allowFrom ?? discord.dm?.allowFrom;
|
||||
const discordDmPolicy = discord.dmPolicy ?? discord.dm?.policy;
|
||||
const discordAllowFrom =
|
||||
stage === "baseline" ? (discord.allowFrom ?? discord.dm?.allowFrom) : discord.allowFrom;
|
||||
const discordDmPolicy =
|
||||
stage === "baseline" ? (discord.dmPolicy ?? discord.dm?.policy) : discord.dmPolicy;
|
||||
if (stage !== "baseline") {
|
||||
assert(
|
||||
!Object.hasOwn(discord.dm ?? {}, "policy"),
|
||||
"legacy Discord DM policy survived Doctor",
|
||||
);
|
||||
assert(
|
||||
!Object.hasOwn(discord.dm ?? {}, "allowFrom"),
|
||||
"legacy Discord allowFrom survived Doctor",
|
||||
);
|
||||
}
|
||||
assert(discordDmPolicy === "allowlist", "discord DM policy changed");
|
||||
assert(
|
||||
Array.isArray(discordAllowFrom) && discordAllowFrom.includes("111111111111111111"),
|
||||
|
||||
@@ -127,6 +127,7 @@ function scenarioConfig(scenario: string, options: TestStateOptions = {}) {
|
||||
},
|
||||
},
|
||||
agents: {
|
||||
ownership: "explicit",
|
||||
defaults: {
|
||||
model: {
|
||||
primary: "openai/gpt-5.6-luna",
|
||||
@@ -136,7 +137,6 @@ function scenarioConfig(scenario: string, options: TestStateOptions = {}) {
|
||||
},
|
||||
entries: {
|
||||
main: {
|
||||
default: true,
|
||||
name: "Main",
|
||||
workspace: "~/workspace",
|
||||
model: {
|
||||
@@ -176,10 +176,8 @@ function scenarioConfig(scenario: string, options: TestStateOptions = {}) {
|
||||
discord: {
|
||||
enabled: true,
|
||||
token: { source: "env", provider: "default", id: "DISCORD_BOT_TOKEN" },
|
||||
dm: {
|
||||
policy: "allowlist",
|
||||
allowFrom: ["111111111111111111"],
|
||||
},
|
||||
dmPolicy: "allowlist",
|
||||
allowFrom: ["111111111111111111"],
|
||||
groupPolicy: "allowlist",
|
||||
guilds: {
|
||||
"222222222222222222": {
|
||||
@@ -472,6 +470,7 @@ OPENCLAW_TEST_STATE_JSON
|
||||
}
|
||||
},
|
||||
"agents": {
|
||||
"ownership": "explicit",
|
||||
"defaults": {
|
||||
"model": {
|
||||
"primary": "openai/gpt-5.6-luna"
|
||||
@@ -481,10 +480,8 @@ OPENCLAW_TEST_STATE_JSON
|
||||
"memory"
|
||||
]
|
||||
},
|
||||
"list": [
|
||||
{
|
||||
"id": "main",
|
||||
"default": true,
|
||||
"entries": {
|
||||
"main": {
|
||||
"name": "Main",
|
||||
"workspace": "~/workspace",
|
||||
"model": {
|
||||
@@ -496,8 +493,7 @@ OPENCLAW_TEST_STATE_JSON
|
||||
],
|
||||
"contextTokens": 64000
|
||||
},
|
||||
{
|
||||
"id": "ops",
|
||||
"ops": {
|
||||
"name": "Ops",
|
||||
"workspace": "~/workspace/ops",
|
||||
"model": {
|
||||
@@ -505,7 +501,7 @@ OPENCLAW_TEST_STATE_JSON
|
||||
},
|
||||
"fastModeDefault": true
|
||||
}
|
||||
]
|
||||
}
|
||||
},
|
||||
"skills": {
|
||||
"allowBundled": [
|
||||
@@ -545,12 +541,10 @@ OPENCLAW_TEST_STATE_JSON
|
||||
"provider": "default",
|
||||
"id": "DISCORD_BOT_TOKEN"
|
||||
},
|
||||
"dm": {
|
||||
"policy": "allowlist",
|
||||
"allowFrom": [
|
||||
"111111111111111111"
|
||||
]
|
||||
},
|
||||
"dmPolicy": "allowlist",
|
||||
"allowFrom": [
|
||||
"111111111111111111"
|
||||
],
|
||||
"groupPolicy": "allowlist",
|
||||
"guilds": {
|
||||
"222222222222222222": {
|
||||
|
||||
@@ -1723,13 +1723,10 @@ describe("doctor config flow", () => {
|
||||
expect(result.cfg.agents).not.toHaveProperty("list");
|
||||
});
|
||||
|
||||
it("materializes ambient roles for a multi-agent configured default", async () => {
|
||||
it("materializes ambient roles for the shipped multi-agent list default", async () => {
|
||||
const rawConfig = {
|
||||
agents: {
|
||||
entries: {
|
||||
ops: { default: true },
|
||||
research: {},
|
||||
},
|
||||
list: [{ id: "main", default: true }, { id: "ops" }],
|
||||
},
|
||||
channels: { telegram: { enabled: true } },
|
||||
talk: { provider: "test" },
|
||||
@@ -1744,16 +1741,25 @@ describe("doctor config flow", () => {
|
||||
|
||||
expect(result.shouldWriteConfig).toBe(true);
|
||||
expect(result.cfg.bindings).toEqual([
|
||||
{ agentId: "ops", match: { channel: "telegram", accountId: "*" } },
|
||||
{ agentId: "main", match: { channel: "telegram", accountId: "*" } },
|
||||
]);
|
||||
expect(result.cfg.agents?.defaults).toMatchObject({
|
||||
heartbeat: { agentId: "ops" },
|
||||
systemAgent: { agentId: "ops" },
|
||||
authInheritance: { agentId: "ops" },
|
||||
heartbeat: { agentId: "main" },
|
||||
systemAgent: { agentId: "main" },
|
||||
});
|
||||
expect(result.cfg.talk).toMatchObject({ provider: "test", agentId: "ops" });
|
||||
expect(result.cfg.agents?.entries?.ops).not.toHaveProperty("default");
|
||||
expect(result.cfg.agents?.defaults?.authInheritance).toBeUndefined();
|
||||
expect(result.cfg.talk).toMatchObject({ provider: "test", agentId: "main" });
|
||||
expect(result.cfg.agents).not.toHaveProperty("list");
|
||||
expect(result.cfg.agents?.entries?.main).not.toHaveProperty("default");
|
||||
expect(result.cfg.agents?.ownership).toBe("explicit");
|
||||
|
||||
const secondRun = await runDoctorConfigWithInput({
|
||||
config: result.cfg,
|
||||
parsedConfig: result.cfg,
|
||||
repair: true,
|
||||
run: loadAndMaybeMigrateDoctorConfig,
|
||||
});
|
||||
expect(secondRun.shouldWriteConfig).toBe(false);
|
||||
});
|
||||
|
||||
it("preserves shared all-agent heartbeat enrollment during materialization", async () => {
|
||||
|
||||
@@ -81,7 +81,7 @@ CREATE INDEX idx_commitments_agent_sent
|
||||
ON commitments(agent_id, status, sent_at_ms, session_key);
|
||||
`;
|
||||
|
||||
const ADDITIVE_RETIRED_COMMITMENTS_SCHEMA_SQL = `
|
||||
const SHIPPED_RETIRED_COMMITMENTS_SCHEMA_SQL = `
|
||||
CREATE TABLE commitments (
|
||||
id TEXT NOT NULL PRIMARY KEY,
|
||||
agent_id TEXT NOT NULL,
|
||||
@@ -91,22 +91,22 @@ CREATE TABLE commitments (
|
||||
recipient_id TEXT,
|
||||
thread_id TEXT,
|
||||
sender_id TEXT,
|
||||
kind TEXT NOT NULL DEFAULT 'followup',
|
||||
sensitivity TEXT NOT NULL DEFAULT 'normal',
|
||||
source TEXT NOT NULL DEFAULT 'unknown',
|
||||
kind TEXT NOT NULL,
|
||||
sensitivity TEXT NOT NULL,
|
||||
source TEXT NOT NULL,
|
||||
status TEXT NOT NULL,
|
||||
reason TEXT NOT NULL DEFAULT '',
|
||||
suggested_text TEXT NOT NULL DEFAULT '',
|
||||
dedupe_key TEXT NOT NULL DEFAULT '',
|
||||
confidence REAL NOT NULL DEFAULT 0,
|
||||
reason TEXT NOT NULL,
|
||||
suggested_text TEXT NOT NULL,
|
||||
dedupe_key TEXT NOT NULL,
|
||||
confidence REAL NOT NULL,
|
||||
due_earliest_ms INTEGER NOT NULL,
|
||||
due_latest_ms INTEGER NOT NULL,
|
||||
due_timezone TEXT NOT NULL DEFAULT 'UTC',
|
||||
due_timezone TEXT NOT NULL,
|
||||
source_message_id TEXT,
|
||||
source_run_id TEXT,
|
||||
created_at_ms INTEGER NOT NULL DEFAULT 0,
|
||||
created_at_ms INTEGER NOT NULL,
|
||||
updated_at_ms INTEGER NOT NULL,
|
||||
attempts INTEGER NOT NULL DEFAULT 0,
|
||||
attempts INTEGER NOT NULL,
|
||||
last_attempt_at_ms INTEGER,
|
||||
sent_at_ms INTEGER,
|
||||
dismissed_at_ms INTEGER,
|
||||
@@ -118,12 +118,8 @@ CREATE INDEX idx_commitments_scope_due
|
||||
ON commitments(agent_id, session_key, status, due_earliest_ms, due_latest_ms);
|
||||
CREATE INDEX idx_commitments_status_due
|
||||
ON commitments(status, due_earliest_ms, due_latest_ms);
|
||||
CREATE INDEX idx_commitments_agent_due
|
||||
ON commitments(agent_id, status, due_earliest_ms, due_latest_ms, session_key);
|
||||
CREATE INDEX idx_commitments_scope_dedupe
|
||||
ON commitments(agent_id, session_key, channel, dedupe_key, status);
|
||||
CREATE INDEX idx_commitments_agent_sent
|
||||
ON commitments(agent_id, status, sent_at_ms, session_key);
|
||||
`;
|
||||
|
||||
const RETIRED_COMMITMENTS_INDEX_NAMES = [
|
||||
@@ -178,11 +174,6 @@ const RETIRED_COMMITMENTS_SCHEMA_COMPATIBILITY: SqliteSchemaCompatibility = {
|
||||
allowedMissingIndexes: RETIRED_COMMITMENTS_INDEX_NAMES,
|
||||
};
|
||||
|
||||
const ADDITIVE_RETIRED_COMMITMENTS_SCHEMA_COMPATIBILITY: SqliteSchemaCompatibility = {
|
||||
allowedMissingColumns: RETIRED_COMMITMENTS_ADDITIVE_COLUMNS,
|
||||
allowedMissingIndexes: RETIRED_COMMITMENTS_INDEX_NAMES,
|
||||
};
|
||||
|
||||
function hasSupportedRetiredCommitmentsSchema(
|
||||
db: DatabaseSync,
|
||||
schemaSql: string,
|
||||
@@ -233,9 +224,9 @@ function hasRecognizedRetiredCommitmentsSchema(db: DatabaseSync): boolean {
|
||||
) ||
|
||||
hasSupportedRetiredCommitmentsSchema(
|
||||
db,
|
||||
ADDITIVE_RETIRED_COMMITMENTS_SCHEMA_SQL,
|
||||
SHIPPED_RETIRED_COMMITMENTS_SCHEMA_SQL,
|
||||
RETIRED_COMMITMENTS_INDEX_NAMES,
|
||||
ADDITIVE_RETIRED_COMMITMENTS_SCHEMA_COMPATIBILITY,
|
||||
RETIRED_COMMITMENTS_SCHEMA_COMPATIBILITY,
|
||||
)
|
||||
);
|
||||
}
|
||||
|
||||
@@ -318,6 +318,63 @@ function seedAdditiveV6CommitmentSchema(database: DatabaseSync): void {
|
||||
markStateDatabaseAsV6(database);
|
||||
}
|
||||
|
||||
function seedV2026_7_1_2CommitmentSchema(database: DatabaseSync): void {
|
||||
database.exec(`
|
||||
CREATE TABLE commitments (
|
||||
id TEXT NOT NULL PRIMARY KEY,
|
||||
agent_id TEXT NOT NULL,
|
||||
session_key TEXT NOT NULL,
|
||||
channel TEXT NOT NULL,
|
||||
account_id TEXT,
|
||||
recipient_id TEXT,
|
||||
thread_id TEXT,
|
||||
sender_id TEXT,
|
||||
kind TEXT NOT NULL,
|
||||
sensitivity TEXT NOT NULL,
|
||||
source TEXT NOT NULL,
|
||||
status TEXT NOT NULL,
|
||||
reason TEXT NOT NULL,
|
||||
suggested_text TEXT NOT NULL,
|
||||
dedupe_key TEXT NOT NULL,
|
||||
confidence REAL NOT NULL,
|
||||
due_earliest_ms INTEGER NOT NULL,
|
||||
due_latest_ms INTEGER NOT NULL,
|
||||
due_timezone TEXT NOT NULL,
|
||||
source_message_id TEXT,
|
||||
source_run_id TEXT,
|
||||
created_at_ms INTEGER NOT NULL,
|
||||
updated_at_ms INTEGER NOT NULL,
|
||||
attempts INTEGER NOT NULL,
|
||||
last_attempt_at_ms INTEGER,
|
||||
sent_at_ms INTEGER,
|
||||
dismissed_at_ms INTEGER,
|
||||
snoozed_until_ms INTEGER,
|
||||
expired_at_ms INTEGER,
|
||||
record_json TEXT NOT NULL
|
||||
);
|
||||
CREATE INDEX idx_commitments_scope_due
|
||||
ON commitments(agent_id, session_key, status, due_earliest_ms, due_latest_ms);
|
||||
CREATE INDEX idx_commitments_status_due
|
||||
ON commitments(status, due_earliest_ms, due_latest_ms);
|
||||
CREATE INDEX idx_commitments_scope_dedupe
|
||||
ON commitments(agent_id, session_key, channel, dedupe_key, status);
|
||||
INSERT INTO commitments (
|
||||
id, agent_id, session_key, channel, kind, sensitivity, source, status,
|
||||
reason, suggested_text, dedupe_key, confidence, due_earliest_ms,
|
||||
due_latest_ms, due_timezone, created_at_ms, updated_at_ms, attempts, record_json
|
||||
) VALUES (
|
||||
'released-commitment', 'main', 'agent:main:main', 'telegram', 'followup',
|
||||
'normal', 'message', 'pending', 'inert', 'follow up', 'released-dedupe',
|
||||
1.0, 10, 20, 'UTC', 1, 1, 0, '{}'
|
||||
);
|
||||
INSERT INTO state_leases (
|
||||
scope, lease_key, owner, expires_at, heartbeat_at, payload_json, created_at, updated_at
|
||||
) VALUES ('test', 'released-preserved-lease', 'migration-test', 100, 50, '{}', 1, 2);
|
||||
PRAGMA user_version = 1;
|
||||
UPDATE schema_meta SET schema_version = 1 WHERE meta_key = 'primary';
|
||||
`);
|
||||
}
|
||||
|
||||
function seedPartiallyAdditiveV6CommitmentSchema(database: DatabaseSync): void {
|
||||
database.exec(`
|
||||
CREATE TABLE commitments (
|
||||
@@ -1583,6 +1640,79 @@ describe("openclaw state database", () => {
|
||||
},
|
||||
);
|
||||
|
||||
it.each(["runtime open", "doctor repair"] as const)(
|
||||
"retires the shipped v2026.7.1-2 commitments layout through %s",
|
||||
(migrationPath) => {
|
||||
const stateDir = createTempStateDir();
|
||||
const options = { env: { OPENCLAW_STATE_DIR: stateDir } };
|
||||
const databasePath = materializeCurrentStateDatabase(stateDir);
|
||||
const { DatabaseSync } = requireNodeSqlite();
|
||||
const released = new DatabaseSync(databasePath);
|
||||
seedV2026_7_1_2CommitmentSchema(released);
|
||||
expect(readSqliteNumberPragma(released, "user_version")).toBe(1);
|
||||
expect(
|
||||
released.prepare("SELECT schema_version FROM schema_meta WHERE meta_key = 'primary'").get(),
|
||||
).toEqual({ schema_version: 1 });
|
||||
expect(
|
||||
released.prepare("SELECT strict FROM pragma_table_list WHERE name = 'commitments'").get(),
|
||||
).toEqual({ strict: 0 });
|
||||
expect(
|
||||
released
|
||||
.prepare(
|
||||
`SELECT name
|
||||
FROM sqlite_schema
|
||||
WHERE type = 'index'
|
||||
AND tbl_name = 'commitments'
|
||||
AND sql IS NOT NULL
|
||||
ORDER BY name`,
|
||||
)
|
||||
.all(),
|
||||
).toEqual([
|
||||
{ name: "idx_commitments_scope_dedupe" },
|
||||
{ name: "idx_commitments_scope_due" },
|
||||
{ name: "idx_commitments_status_due" },
|
||||
]);
|
||||
released.close();
|
||||
|
||||
if (migrationPath === "doctor repair") {
|
||||
const result = repairOpenClawStateDatabaseSchema(options);
|
||||
expect(result.warnings).toEqual([]);
|
||||
expect(result.changes).toContain("Retired shared state commitments table and indexes");
|
||||
}
|
||||
const migrated = openOpenClawStateDatabase(options);
|
||||
expect(readSqliteNumberPragma(migrated.db, "user_version")).toBe(7);
|
||||
expect(
|
||||
migrated.db
|
||||
.prepare("SELECT schema_version FROM schema_meta WHERE meta_key = 'primary'")
|
||||
.get(),
|
||||
).toEqual({ schema_version: 7 });
|
||||
for (const name of RETIRED_COMMITMENT_SCHEMA_OBJECTS) {
|
||||
expect(
|
||||
migrated.db.prepare("SELECT name FROM sqlite_schema WHERE name = ?").get(name),
|
||||
).toBeUndefined();
|
||||
}
|
||||
expect(
|
||||
migrated.db
|
||||
.prepare(
|
||||
`SELECT scope, lease_key, owner, expires_at, heartbeat_at, payload_json,
|
||||
created_at, updated_at
|
||||
FROM state_leases
|
||||
WHERE scope = 'test' AND lease_key = 'released-preserved-lease'`,
|
||||
)
|
||||
.get(),
|
||||
).toEqual({
|
||||
scope: "test",
|
||||
lease_key: "released-preserved-lease",
|
||||
owner: "migration-test",
|
||||
expires_at: 100,
|
||||
heartbeat_at: 50,
|
||||
payload_json: "{}",
|
||||
created_at: 1,
|
||||
updated_at: 2,
|
||||
});
|
||||
},
|
||||
);
|
||||
|
||||
it.each(["runtime open", "doctor repair"] as const)(
|
||||
"retires a partially additive v6 commitments layout through %s",
|
||||
(migrationPath) => {
|
||||
|
||||
@@ -233,11 +233,17 @@ describe("scripts/lib/openclaw-test-state", () => {
|
||||
source: "env",
|
||||
},
|
||||
});
|
||||
expect(payload.config.agents.ownership).toBe("explicit");
|
||||
expect(Object.keys(payload.config.agents.entries)).toEqual(["main", "ops"]);
|
||||
expect(payload.config.agents).not.toHaveProperty("list");
|
||||
for (const agent of Object.values(payload.config.agents.entries)) {
|
||||
expect(agent).not.toHaveProperty("default");
|
||||
}
|
||||
expect(payload.config.channels.discord.enabled).toBe(true);
|
||||
expect(payload.config.channels.discord.dm).toStrictEqual({
|
||||
allowFrom: ["111111111111111111"],
|
||||
policy: "allowlist",
|
||||
});
|
||||
expect(payload.config.channels.discord.dmPolicy).toBe("allowlist");
|
||||
expect(payload.config.channels.discord.allowFrom).toEqual(["111111111111111111"]);
|
||||
expect(payload.config.channels.discord.dm?.policy).toBeUndefined();
|
||||
expect(payload.config.channels.discord.dm?.allowFrom).toBeUndefined();
|
||||
expect(payload.config.channels.telegram.enabled).toBe(true);
|
||||
expect(payload.config.channels.whatsapp.enabled).toBe(true);
|
||||
} finally {
|
||||
@@ -273,6 +279,22 @@ describe("scripts/lib/openclaw-test-state", () => {
|
||||
expect(payload.secretKey).toMatch(secretKeyPattern);
|
||||
expect(payload.config).toStrictEqual({});
|
||||
|
||||
const upgradeProbe = await execFileAsync("bash", [
|
||||
"-lc",
|
||||
`${cleanupTestStateHomeTrap()}; export OPENCLAW_TEST_STATE_TMPDIR=${shellQuote(path.join(tempRoot, "upgrade-function-tmp"))}; source ${shellQuote(snippetFile)}; openclaw_test_state_create "upgrade case" upgrade-survivor; node -e 'const fs=require("node:fs"); process.stdout.write(fs.readFileSync(process.env.OPENCLAW_CONFIG_PATH,"utf8"));'`,
|
||||
]);
|
||||
const upgradeConfig = JSON.parse(upgradeProbe.stdout);
|
||||
expect(upgradeConfig.agents.ownership).toBe("explicit");
|
||||
expect(Object.keys(upgradeConfig.agents.entries)).toEqual(["main", "ops"]);
|
||||
expect(upgradeConfig.agents).not.toHaveProperty("list");
|
||||
for (const agent of Object.values(upgradeConfig.agents.entries)) {
|
||||
expect(agent).not.toHaveProperty("default");
|
||||
}
|
||||
expect(upgradeConfig.channels.discord.dmPolicy).toBe("allowlist");
|
||||
expect(upgradeConfig.channels.discord.allowFrom).toEqual(["111111111111111111"]);
|
||||
expect(upgradeConfig.channels.discord.dm?.policy).toBeUndefined();
|
||||
expect(upgradeConfig.channels.discord.dm?.allowFrom).toBeUndefined();
|
||||
|
||||
const trailingTmpDir = path.join(tempRoot, "function-trailing-tmp");
|
||||
const trailingProbe = await execFileAsync("bash", [
|
||||
"-lc",
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
// Upgrade Survivor Assertions tests cover upgrade survivor assertions script behavior.
|
||||
import { execFileSync } from "node:child_process";
|
||||
import { execFileSync, spawnSync } from "node:child_process";
|
||||
import { mkdtempSync, mkdirSync, readFileSync, rmSync, writeFileSync } from "node:fs";
|
||||
import { tmpdir } from "node:os";
|
||||
import { join } from "node:path";
|
||||
@@ -449,6 +449,118 @@ describe("upgrade survivor assertions", () => {
|
||||
}
|
||||
});
|
||||
|
||||
it("requires canonical agent and Discord config after Doctor", () => {
|
||||
const root = mkdtempSync(join(tmpdir(), "openclaw-upgrade-survivor-config-stage-"));
|
||||
try {
|
||||
const configPath = join(root, "openclaw.json");
|
||||
const coveragePath = join(root, "coverage.json");
|
||||
writeJson(coveragePath, {
|
||||
acceptedIntents: ["agents", "discord-channel"],
|
||||
skippedIntents: [],
|
||||
});
|
||||
const discord = {
|
||||
enabled: true,
|
||||
groupPolicy: "allowlist",
|
||||
guilds: {
|
||||
"222222222222222222": {
|
||||
channels: { "333333333333333333": { requireMention: true } },
|
||||
},
|
||||
},
|
||||
threadBindings: { idleHours: 72 },
|
||||
};
|
||||
const legacy = {
|
||||
agents: {
|
||||
defaults: { contextTokens: 64000 },
|
||||
list: [
|
||||
{ id: "main", default: true, contextTokens: 64000 },
|
||||
{ id: "ops", fastModeDefault: true },
|
||||
],
|
||||
},
|
||||
channels: {
|
||||
discord: {
|
||||
...discord,
|
||||
dm: { policy: "allowlist", allowFrom: ["111111111111111111"] },
|
||||
},
|
||||
},
|
||||
};
|
||||
const canonical = {
|
||||
agents: {
|
||||
ownership: "explicit",
|
||||
defaults: { contextTokens: 64000 },
|
||||
entries: {
|
||||
main: { contextTokens: 64000 },
|
||||
ops: { fastModeDefault: true },
|
||||
},
|
||||
},
|
||||
channels: {
|
||||
discord: {
|
||||
...discord,
|
||||
dmPolicy: "allowlist",
|
||||
allowFrom: ["111111111111111111"],
|
||||
},
|
||||
},
|
||||
};
|
||||
const run = (config: unknown, stage: "baseline" | "survival") => {
|
||||
writeJson(configPath, config);
|
||||
return spawnSync(process.execPath, [ASSERTIONS_PATH, "assert-config"], {
|
||||
encoding: "utf8",
|
||||
env: {
|
||||
...process.env,
|
||||
OPENCLAW_CONFIG_PATH: configPath,
|
||||
OPENCLAW_UPGRADE_SURVIVOR_ASSERT_STAGE: stage,
|
||||
OPENCLAW_UPGRADE_SURVIVOR_CONFIG_COVERAGE_JSON: coveragePath,
|
||||
OPENCLAW_UPGRADE_SURVIVOR_SCENARIO: "base",
|
||||
},
|
||||
});
|
||||
};
|
||||
|
||||
expect(run(legacy, "baseline").status).toBe(0);
|
||||
expect(run(legacy, "survival").status).not.toBe(0);
|
||||
expect(run(canonical, "survival").status).toBe(0);
|
||||
|
||||
expect(
|
||||
run(
|
||||
{
|
||||
...canonical,
|
||||
agents: {
|
||||
...canonical.agents,
|
||||
entries: {
|
||||
...canonical.agents.entries,
|
||||
main: { ...canonical.agents.entries.main, default: true },
|
||||
},
|
||||
},
|
||||
},
|
||||
"survival",
|
||||
).status,
|
||||
).not.toBe(0);
|
||||
|
||||
const {
|
||||
allowFrom: _allowFrom,
|
||||
dmPolicy: _dmPolicy,
|
||||
...discordWithoutCanonicalDm
|
||||
} = canonical.channels.discord;
|
||||
expect(
|
||||
run(
|
||||
{
|
||||
...canonical,
|
||||
channels: {
|
||||
discord: {
|
||||
...discordWithoutCanonicalDm,
|
||||
dm: {
|
||||
policy: "allowlist",
|
||||
allowFrom: ["111111111111111111"],
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
"survival",
|
||||
).status,
|
||||
).not.toBe(0);
|
||||
} finally {
|
||||
rmSync(root, { force: true, recursive: true });
|
||||
}
|
||||
});
|
||||
|
||||
it("accepts the ACPX OpenClaw tools bridge scenario during seed", () => {
|
||||
const root = mkdtempSync(join(tmpdir(), "openclaw-upgrade-survivor-acpx-"));
|
||||
try {
|
||||
|
||||
@@ -146,6 +146,21 @@ describe("upgrade survivor config recipe command resolution", () => {
|
||||
]);
|
||||
});
|
||||
|
||||
it("pins the shipped v2026.7.1-2 agent and Discord aliases", () => {
|
||||
const recipeRoot = join(process.cwd(), "scripts/e2e/lib/upgrade-survivor/config-recipe");
|
||||
const agents = JSON.parse(readFileSync(join(recipeRoot, "agents.json"), "utf8"));
|
||||
const discord = JSON.parse(readFileSync(join(recipeRoot, "channels-discord.json"), "utf8"));
|
||||
|
||||
expect(agents.list.map((agent: { id: string }) => agent.id)).toEqual(["main", "ops"]);
|
||||
expect(agents.list[0]).toMatchObject({ id: "main", default: true });
|
||||
expect(agents).not.toHaveProperty("entries");
|
||||
expect(discord.dm).toEqual({
|
||||
policy: "allowlist",
|
||||
allowFrom: ["111111111111111111"],
|
||||
});
|
||||
expect(discord).not.toHaveProperty("dmPolicy");
|
||||
});
|
||||
|
||||
it("inserts scenario config before final validation", () => {
|
||||
const steps = resolveUpgradeSurvivorConfigSteps("feishu-channel");
|
||||
expect(steps.find((step) => step.id === "channels-discord")).toBeDefined();
|
||||
|
||||
Reference in New Issue
Block a user