diff --git a/scripts/__tests__/run-vitest-stable-shard.test.mjs b/scripts/__tests__/run-vitest-stable-shard.test.mjs index ffe7fb2d0b7..05c8d8c1260 100644 --- a/scripts/__tests__/run-vitest-stable-shard.test.mjs +++ b/scripts/__tests__/run-vitest-stable-shard.test.mjs @@ -428,3 +428,40 @@ test("filtered collection must match the exact assigned case identities, includi assert.throws(() => assertSelectedTests(expected, actual, lineShardFile)); } }); + +// Release verification on master (release-verify.yml after #15619 and #15831) +// selects the without-chat-or-native-runner server group plus a separate +// native-runner lane, and runs the db project in its own workspaces-c lane. +// This source predates those lanes, so the groups are mirrored here so the +// stable promotion of this commit verifies under master's workflow contract. +for (const caller of ["Release", "Cloud readiness", "another caller"]) { + test(`release server, chat, and native lanes cover every server suite once under ${caller}`, () => { + const env = { GITHUB_WORKFLOW: caller }; + const full = dryRunJson(["--mode", "general", "--group", "general-server", "--shard-index", "0", "--shard-count", "1"], env); + const files = Array.from({ length: 10 }, (_, index) => dryRunJson([ + "--mode", "general", "--group", "general-server-without-chat-or-native-runner", + "--shard-index", String(index), "--shard-count", "10", + ], env)).flatMap((shard) => shard.selectedGeneralServerSuites); + const native = dryRunJson(["--mode", "general", "--group", "general-server-native-runner"], env); + const combined = [...files, chatSuitePath, ...native.selectedGeneralServerSuites]; + assert.equal(new Set(combined).size, combined.length, "lanes must not overlap"); + assert.deepEqual(combined.sort(), full.selectedGeneralServerSuites.sort()); + }); +} + +test("workspaces-c runs the db project alone and workspaces-b no longer carries it", () => { + const workspacesC = dryRunJson(["--mode", "general", "--group", "general-workspaces-c"]); + assert.deepEqual(workspacesC.workspaceProjects, ["@paperclipai/db"]); + assert.equal(workspacesC.workspacesVitestShard, null); + const workspacesB = dryRunJson(["--mode", "general", "--group", "general-workspaces-b"]); + assert.ok(!workspacesB.workspaceProjects.includes("@paperclipai/db"), "db must not run twice"); + assert.ok(workspacesB.workspaceProjects.length > 0, "workspaces-b must still run its other projects"); + const workspacesA = dryRunJson(["--mode", "general", "--group", "general-workspaces-a"]); + const all = [...workspacesA.workspaceProjects, ...workspacesB.workspaceProjects, ...workspacesC.workspaceProjects]; + assert.equal(new Set(all).size, all.length, "no project may run in two workspace lanes"); +}); + +test("shard flags are rejected for the workspaces-c group", () => { + const result = dryRun(["--mode", "general", "--group", "general-workspaces-c", "--shard-index", "0", "--shard-count", "3"]); + assert.notEqual(result.status, 0, "workspaces-c must not accept shard flags"); +}); diff --git a/scripts/run-vitest-stable.mjs b/scripts/run-vitest-stable.mjs index d98a814a12d..6231e1dff72 100644 --- a/scripts/run-vitest-stable.mjs +++ b/scripts/run-vitest-stable.mjs @@ -69,6 +69,9 @@ const generalModeName = "general"; const allModeName = "all"; const generalServerGroupName = "general-server"; const generalServerWithoutChatGroupName = "general-server-without-chat"; +// Release verification on master selects this explicit group (release-verify.yml +// after #15619) and runs the native-runner suite in its cached Runner lane. +const generalServerWithoutChatOrNativeRunnerGroupName = "general-server-without-chat-or-native-runner"; const generalChatGroupName = "general-chat"; const generalServerNativeRunnerGroupName = "general-server-native-runner"; const chatSuite = "server/src/__tests__/chat-channels.integration.test.ts"; @@ -92,14 +95,35 @@ const nativeRunnerSuiteRunsInRustCachedLane = process.env.GITHUB_WORKFLOW === pr const withoutChatExcludedSuites = nativeRunnerSuiteRunsInRustCachedLane ? [chatSuite, nativeRunnerSuite] : [chatSuite]; +const generalServerGroups = [ + generalServerGroupName, + generalServerWithoutChatGroupName, + generalServerWithoutChatOrNativeRunnerGroupName, +]; +function excludedServerSuites(groupName) { + if (groupName === generalServerWithoutChatOrNativeRunnerGroupName) return [chatSuite, nativeRunnerSuite]; + if (groupName === generalServerWithoutChatGroupName) return withoutChatExcludedSuites; + return []; +} const generalWorkspacesAGroupName = "general-workspaces-a"; const generalWorkspacesBGroupName = "general-workspaces-b"; +// The db project runs as its own lane (release-verify.yml after #15831). +const generalWorkspacesCGroupName = "general-workspaces-c"; const generalWorkspacesAProjects = ["@paperclipai/ui", "paperclipai"]; -const generalWorkspacesBProjects = nonServerProjects.filter((project) => !generalWorkspacesAProjects.includes(project)); -const generalGroupNames = [generalServerGroupName, generalWorkspacesAGroupName, generalWorkspacesBGroupName]; +const generalWorkspacesCProjects = ["@paperclipai/db"]; +const generalWorkspacesBProjects = nonServerProjects.filter( + (project) => !generalWorkspacesAProjects.includes(project) && !generalWorkspacesCProjects.includes(project), +); +const generalGroupNames = [ + generalServerGroupName, + generalWorkspacesAGroupName, + generalWorkspacesBGroupName, + generalWorkspacesCGroupName, +]; const allowedGeneralGroupNames = [ ...generalGroupNames, generalServerWithoutChatGroupName, + generalServerWithoutChatOrNativeRunnerGroupName, generalChatGroupName, generalServerNativeRunnerGroupName, ]; @@ -248,7 +272,7 @@ function parseCliOptions(argv) { const shardAllowed = mode === serializedModeName || (mode === generalModeName && - ([generalServerGroupName, generalServerWithoutChatGroupName, generalChatGroupName, generalWorkspacesAGroupName].includes(group))); + ([...generalServerGroups, generalChatGroupName, generalWorkspacesAGroupName].includes(group))); if (!shardAllowed && shardIndex !== null) { fail( "--shard-index/--shard-count are only valid with serialized mode or a shardable general server/chat/workspaces-a group.", @@ -388,14 +412,13 @@ function runGeneralGroup(routeTests, groupName, shardIndex = null, shardCount = ); return; } - if (groupName === generalServerGroupName || groupName === generalServerWithoutChatGroupName) { + if (generalServerGroups.includes(groupName)) { // In the PR workflow the without-chat group also leaves the native-runner // suite to the Rust-cached vitest lane; the full general-server group - // (local runs) keeps both. - const withoutChat = groupName === generalServerWithoutChatGroupName; - const files = withoutChat - ? generalServerTestFiles.filter((file) => !withoutChatExcludedSuites.includes(file)) - : generalServerTestFiles; + // (local runs) keeps both. The explicit without-chat-or-native-runner + // group always excludes both, for callers that run those lanes separately. + const excludedSuites = excludedServerSuites(groupName); + const files = generalServerTestFiles.filter((file) => !excludedSuites.includes(file)); if (shardCount !== null && shardCount > 1) { const shardFiles = selectGeneralServerShard( files, @@ -423,10 +446,8 @@ function runGeneralGroup(routeTests, groupName, shardIndex = null, shardCount = } const excludeRouteArgs = routeTests.flatMap((file) => ["--exclude", file.serverPath]); - if (withoutChat) { - for (const suite of withoutChatExcludedSuites) { - excludeRouteArgs.push("--exclude", suite.replace(/^server\//, "")); - } + for (const suite of excludedSuites) { + excludeRouteArgs.push("--exclude", suite.replace(/^server\//, "")); } runVitest( [ @@ -454,6 +475,11 @@ function runGeneralGroup(routeTests, groupName, shardIndex = null, shardCount = return; } + if (groupName === generalWorkspacesCGroupName) { + runProjectGroup(generalWorkspacesCProjects, groupName); + return; + } + fail(`Unknown group "${groupName}".`); } @@ -519,12 +545,10 @@ if (options.dryRun) { options.mode === generalModeName && options.group === generalServerNativeRunnerGroupName ? [nativeRunnerSuite] : options.mode === generalModeName && - [generalServerGroupName, generalServerWithoutChatGroupName].includes(options.group) && + generalServerGroups.includes(options.group) && options.shardCount !== null ? selectGeneralServerShard( - options.group === generalServerWithoutChatGroupName - ? generalServerTestFiles.filter((file) => !withoutChatExcludedSuites.includes(file)) - : generalServerTestFiles, + generalServerTestFiles.filter((file) => !excludedServerSuites(options.group).includes(file)), options.shardIndex, options.shardCount, generalServerShardDurations, @@ -535,7 +559,9 @@ if (options.dryRun) { ? generalWorkspacesAProjects : options.group === generalWorkspacesBGroupName ? generalWorkspacesBProjects - : null, + : options.group === generalWorkspacesCGroupName + ? generalWorkspacesCProjects + : null, workspacesVitestShard: options.group === generalWorkspacesAGroupName && options.shardCount !== null &&