diff --git a/scripts/e2e/lib/upgrade-survivor/assertions.mjs b/scripts/e2e/lib/upgrade-survivor/assertions.mjs index 52b03fc56509..6bab6c8a452c 100644 --- a/scripts/e2e/lib/upgrade-survivor/assertions.mjs +++ b/scripts/e2e/lib/upgrade-survivor/assertions.mjs @@ -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"), diff --git a/scripts/lib/openclaw-test-state.mts b/scripts/lib/openclaw-test-state.mts index 601538f9974a..0b2606e80ad7 100644 --- a/scripts/lib/openclaw-test-state.mts +++ b/scripts/lib/openclaw-test-state.mts @@ -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": { diff --git a/src/commands/doctor-config-flow.test.ts b/src/commands/doctor-config-flow.test.ts index 0e7fe446bee9..1fadabb9aec5 100644 --- a/src/commands/doctor-config-flow.test.ts +++ b/src/commands/doctor-config-flow.test.ts @@ -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 () => { diff --git a/src/state/openclaw-state-db-schema-repair.ts b/src/state/openclaw-state-db-schema-repair.ts index 50cd744d4ebe..c3265b556dbe 100644 --- a/src/state/openclaw-state-db-schema-repair.ts +++ b/src/state/openclaw-state-db-schema-repair.ts @@ -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, ) ); } diff --git a/src/state/openclaw-state-db.test.ts b/src/state/openclaw-state-db.test.ts index b3ef401050d4..2476f5f6e96b 100644 --- a/src/state/openclaw-state-db.test.ts +++ b/src/state/openclaw-state-db.test.ts @@ -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) => { diff --git a/test/scripts/openclaw-test-state.test.ts b/test/scripts/openclaw-test-state.test.ts index 070a11daeff2..2c6165edb7a6 100644 --- a/test/scripts/openclaw-test-state.test.ts +++ b/test/scripts/openclaw-test-state.test.ts @@ -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", diff --git a/test/scripts/upgrade-survivor-assertions.test.ts b/test/scripts/upgrade-survivor-assertions.test.ts index 24d33e2df9c4..af149e3837f0 100644 --- a/test/scripts/upgrade-survivor-assertions.test.ts +++ b/test/scripts/upgrade-survivor-assertions.test.ts @@ -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 { diff --git a/test/scripts/upgrade-survivor-config-recipe.test.ts b/test/scripts/upgrade-survivor-config-recipe.test.ts index 7a0a1ae03f9d..4cb2ff621eb3 100644 --- a/test/scripts/upgrade-survivor-config-recipe.test.ts +++ b/test/scripts/upgrade-survivor-config-recipe.test.ts @@ -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();