mirror of
https://github.com/openclaw/openclaw.git
synced 2026-10-03 01:29:56 +00:00
* fix(slack): link finished progress cards to the visible work session Slack progress cards linked "Open in OpenClaw" to the conversation's main session even when the turn spawned a visible work session, so operators on groupScope "main" landed on /chat/<agent> instead of the child doing the work. Carry the child's canonical URL from its producer: accepted visible spawn receipts keep sessionUrl and label across the embedded, Codex app-server, and CLI runners; the settled run reports them once through a new optional GetReplyOptions.onVisibleWorkSessions before final delivery; Slack's native task card and Block Kit card render "Open work session" for one child, labeled links in acceptance order (max five) for several, and keep the conversation link when none was spawned. No request-time session discovery. To satisfy the line-cap ratchet, the visible-spawn TypeBox schema moves to sessions-spawn-visible.schema.ts and the new cases live in sibling test files with shared test-support fixtures. * fix(slack): match only generated session-link action IDs The link-only acknowledgement matched any action_id starting with openclaw:session_link, so an unrelated button such as openclaw:session_linked was acknowledged and dropped instead of reaching plugin or agent dispatch. Accept only the bare ID or a numeric suffix, as the renderer emits. * test(ci): accept measured command ceilings beside startup health The PR's 771st database-worker file changes compact placement and exposes a contradiction: capacity checks accept measured command groups' two-worker ceiling, but the startup-health sibling check expected no ceiling. Share the complete measured-generation lookup between both assertions and recognize the existing command policy without changing planner, packing, worker, or job-level policies. Move the unchanged tooling placement helpers to the sibling support module, reducing the test's counted lines from 7536 to 7524.
177 lines
6.5 KiB
TypeScript
177 lines
6.5 KiB
TypeScript
import { existsSync, globSync, readdirSync } from "node:fs";
|
|
import { isAbsolute, join, relative, resolve } from "node:path";
|
|
import { expect } from "vitest";
|
|
import type { CompactNodeTestShard } from "../../scripts/lib/ci-node-test-plan.mts";
|
|
import {
|
|
isReleaseOnlyRuntimeTestFile,
|
|
RELEASE_ONLY_RUNTIME_TEST_FILES,
|
|
} from "../../scripts/lib/ci-proof-test-inventory.mts";
|
|
import { parseCompactSplitTimingKey } from "../../scripts/lib/vitest-shard-metadata.mts";
|
|
import { listGitTrackedFiles, sortRepoPaths, toRepoPath } from "../../src/test-utils/repo-files.js";
|
|
import { createCliVitestConfig } from "../vitest/vitest.cli.config.ts";
|
|
|
|
type VitestTestConfig = {
|
|
dir?: string;
|
|
exclude?: string[];
|
|
include?: string[];
|
|
};
|
|
|
|
type VitestConfig = {
|
|
test?: VitestTestConfig;
|
|
};
|
|
|
|
export function isNumberedToolingGroup(group: { shard_name: string }) {
|
|
return /^core-tooling-\d+(?:-hosted-\d+)?$/u.test(group.shard_name);
|
|
}
|
|
export function nonToolingPlacement(plan: CompactNodeTestShard[]) {
|
|
return plan
|
|
.flatMap((job) => {
|
|
const groups = job.groups
|
|
.filter((group) => !isNumberedToolingGroup(group))
|
|
.map((group) => group.shard_name)
|
|
.toSorted();
|
|
return groups.length === 0
|
|
? []
|
|
: [
|
|
{
|
|
groups,
|
|
planConcurrency: job.planConcurrency,
|
|
pretestBuildMode: job.pretestBuildMode,
|
|
requiresDist: job.requiresDist,
|
|
runner: job.runner,
|
|
},
|
|
];
|
|
})
|
|
.toSorted((a, b) => a.groups.join("\0").localeCompare(b.groups.join("\0")));
|
|
}
|
|
|
|
export function listTestFiles(rootDir: string): string[] {
|
|
const gitFiles = listGitTrackedFiles({ pathspecs: rootDir });
|
|
expect(gitFiles).not.toBeNull();
|
|
if (gitFiles) {
|
|
return gitFiles.filter((line) => line.endsWith(".test.ts"));
|
|
}
|
|
|
|
if (!existsSync(rootDir)) {
|
|
return [];
|
|
}
|
|
|
|
const files: string[] = [];
|
|
const visit = (dir: string) => {
|
|
for (const entry of readdirSync(dir, { withFileTypes: true })) {
|
|
const path = join(dir, entry.name);
|
|
if (entry.isDirectory()) {
|
|
visit(path);
|
|
} else if (entry.isFile() && entry.name.endsWith(".test.ts")) {
|
|
files.push(toRepoPath(path));
|
|
}
|
|
}
|
|
};
|
|
|
|
visit(rootDir);
|
|
return sortRepoPaths(files);
|
|
}
|
|
|
|
export function listMatchedTestFiles(config: VitestConfig): string[] {
|
|
const testConfig = config.test ?? {};
|
|
const cwd = testConfig.dir ? resolve(testConfig.dir) : process.cwd();
|
|
const exclude = (testConfig.exclude ?? []).map((pattern) =>
|
|
isAbsolute(pattern) ? toRepoPath(relative(cwd, pattern)) : toRepoPath(pattern),
|
|
);
|
|
// Unit-fast ownership contributes hundreds of literal file exclusions.
|
|
// Compare those exactly after discovery; keep directory/glob pruning native.
|
|
const exactFiles = new Set(exclude.filter((pattern) => /^[\w/.-]+\.test\.ts$/u.test(pattern)));
|
|
return globSync(testConfig.include ?? [], {
|
|
cwd,
|
|
exclude: exclude.filter((pattern) => !exactFiles.has(pattern)),
|
|
})
|
|
.filter((file) => !exactFiles.has(toRepoPath(relative(cwd, resolve(cwd, file)))))
|
|
.map((file) => toRepoPath(relative(process.cwd(), resolve(cwd, file))))
|
|
.toSorted((a, b) => a.localeCompare(b));
|
|
}
|
|
|
|
export function expectRuntimeReleaseInventory({
|
|
before,
|
|
after,
|
|
reducedOwners,
|
|
compactMode,
|
|
}: {
|
|
before: CompactNodeTestShard[];
|
|
after: CompactNodeTestShard[];
|
|
reducedOwners: string[];
|
|
compactMode: "push" | "pull-request";
|
|
}) {
|
|
const cliFiles = listMatchedTestFiles(createCliVitestConfig({}));
|
|
const groupFiles = (group: CompactNodeTestShard["groups"][number]) =>
|
|
group.includePatterns ?? (group.shard_name === "agentic-cli" ? cliFiles : []);
|
|
const files = (plan: CompactNodeTestShard[]) =>
|
|
plan.flatMap((shard) => shard.groups.flatMap(groupFiles));
|
|
const beforeFiles = files(before);
|
|
const afterFiles = files(after);
|
|
const timingLineage = (key: string) => {
|
|
const lineage = [key];
|
|
for (let split = parseCompactSplitTimingKey(key); split;) {
|
|
const parent = split.parentShardName;
|
|
lineage.push(parent);
|
|
split = parseCompactSplitTimingKey(parent);
|
|
}
|
|
return lineage;
|
|
};
|
|
const fullTimingParents = new Set(
|
|
before
|
|
.flatMap((shard) =>
|
|
shard.groups.flatMap((group) => timingLineage(group.timing_key ?? group.shard_name)),
|
|
)
|
|
// The comparison plan can already omit tooling from mixed owners.
|
|
.filter((parent) => !parent.startsWith("changed-")),
|
|
);
|
|
expect(beforeFiles.filter((file) => !afterFiles.includes(file)).toSorted()).toEqual(
|
|
RELEASE_ONLY_RUNTIME_TEST_FILES.filter(
|
|
(file) => compactMode === "pull-request" || !file.startsWith("test/scripts/"),
|
|
).toSorted(),
|
|
);
|
|
expect(afterFiles.filter((file) => !beforeFiles.includes(file))).toEqual([]);
|
|
expect(afterFiles).toContain("src/config/config-startup-corpus.test.ts");
|
|
for (const owner of reducedOwners) {
|
|
const owns = (group: { shard_name: string }) =>
|
|
group.shard_name === owner || group.shard_name.startsWith(`${owner}-hosted-`);
|
|
const reduced = after.flatMap((shard) => shard.groups).filter(owns);
|
|
const retainedFiles = before
|
|
.flatMap((shard) => shard.groups)
|
|
.filter(owns)
|
|
.flatMap(groupFiles)
|
|
.filter((file) => !isReleaseOnlyRuntimeTestFile(file));
|
|
expect(reduced.flatMap((group) => group.includePatterns ?? []).toSorted(), owner).toEqual(
|
|
retainedFiles.toSorted(),
|
|
);
|
|
if (retainedFiles.length === 0) {
|
|
expect(reduced, owner).toEqual([]);
|
|
}
|
|
if (owner === "agentic-cli") {
|
|
const full = before.flatMap((shard) => shard.groups).find(owns);
|
|
expect(full, "full CLI owner").toBeDefined();
|
|
for (const group of reduced) {
|
|
expect(group.shard_name).toMatch(/^agentic-cli(?:-hosted-\d+)?$/u);
|
|
expect(group.env).toEqual(full?.env);
|
|
expect(group.fallbackMaxWorkers).toBe(full?.fallbackMaxWorkers);
|
|
const job = after.find((shard) => shard.groups.includes(group));
|
|
expect(job?.planConcurrency).toBe(1);
|
|
}
|
|
}
|
|
for (const group of reduced) {
|
|
expect(group.timing_key, "reduced runtime timing identity").toBeTypeOf("string");
|
|
const timingKey = group.timing_key!;
|
|
const lineage = timingLineage(timingKey);
|
|
for (const parent of lineage) {
|
|
expect(fullTimingParents.has(parent), `${owner}: ${parent}`).toBe(false);
|
|
}
|
|
const timingParent = lineage.at(-1)!;
|
|
expect(
|
|
timingParent.replace(
|
|
/(?:#workers-4|#file-parallel-(?:2|8)|-parallel(?:-2)?(?:-native-serial)?(?:-stripes)?)$/u,
|
|
"",
|
|
),
|
|
).toBe(`changed-${owner}`);
|
|
}
|
|
}
|
|
}
|