mirror of
https://github.com/openclaw/openclaw.git
synced 2026-10-03 09:39:25 +00:00
test(scripts): remove low-value tests (batch d152) (#163549)
* test(scripts): deslop s1015 tests * test(scripts): deslop s1023 tests * test(scripts): deslop s1026 tests * test(scripts): deslop s1027 tests * test(scripts): deslop s1020 tests * test(scripts): deslop s1031 tests * test(scripts): deslop s1038 tests * test(scripts): deslop s1025 tests * test(scripts): deslop s1028 tests * test(scripts): deslop s1037 tests * test(scripts): retain rollout ancestry coverage
This commit is contained in:
parent
779c5aae5e
commit
65cb5ab547
47 changed files with 668 additions and 1749 deletions
|
|
@ -26,7 +26,7 @@ type OxlintDiagnostic = {
|
|||
help?: string;
|
||||
};
|
||||
|
||||
export function compareLineCapViolations(
|
||||
function compareLineCapViolations(
|
||||
head: ReadonlyMap<string, LineCapViolation>,
|
||||
base: ReadonlyMap<string, LineCapViolation>,
|
||||
renames: readonly { from: string; to: string }[] = [],
|
||||
|
|
|
|||
|
|
@ -2,7 +2,7 @@ import { execFileSync } from "node:child_process";
|
|||
import fs from "node:fs";
|
||||
import path from "node:path";
|
||||
import { afterEach, beforeEach, describe, expect, it, vi } from "vitest";
|
||||
import { compareLineCapViolations, main } from "../../scripts/check-line-cap-ratchet.mts";
|
||||
import { main } from "../../scripts/check-line-cap-ratchet.mts";
|
||||
import { useAutoCleanupTempDirTracker } from "../helpers/temp-dir.js";
|
||||
|
||||
const tempDirs = useAutoCleanupTempDirTracker(afterEach);
|
||||
|
|
@ -102,18 +102,6 @@ describe("line-cap growth ratchet", () => {
|
|||
expect(errors).not.toHaveBeenCalled();
|
||||
});
|
||||
|
||||
it.each([
|
||||
{ label: "over-cap shrinking", before: 705, after: 703, fails: false },
|
||||
{ label: "over-cap growing", before: 705, after: 706, fails: true },
|
||||
{ label: "newly over-cap", before: 700, after: 701, fails: true },
|
||||
{ label: "under-cap growth", before: 698, after: 700, fails: false },
|
||||
{ label: "unchanged over-cap", before: 705, after: 705, fails: false },
|
||||
])("$label", ({ before, after, fails }) => {
|
||||
const violations = (count: number) =>
|
||||
new Map(count > 700 ? [["src/file.ts", { count, cap: 700 }]] : []);
|
||||
expect(compareLineCapViolations(violations(after), violations(before)).length > 0).toBe(fails);
|
||||
});
|
||||
|
||||
it.skipIf(process.platform === "win32")("preserves native filenames beginning with file:", () => {
|
||||
vi.stubEnv("TERMINAL_EMULATOR", "");
|
||||
const root = fixture();
|
||||
|
|
|
|||
|
|
@ -300,29 +300,16 @@ describe("bundled browser MCP package", () => {
|
|||
change: "modify",
|
||||
error: "bundled chrome-devtools-mcp must be ESM version 1.10.1",
|
||||
},
|
||||
...[
|
||||
"build/src/TextSnapshot.js",
|
||||
"build/src/McpPage.js",
|
||||
"build/src/third_party/index.js",
|
||||
"build/src/OPENCLAW_PATCH_NOTICE.md",
|
||||
].map((file) => ({
|
||||
file,
|
||||
{
|
||||
file: "build/src/TextSnapshot.js",
|
||||
change: "modify",
|
||||
error: `unpatched or changed runtime entry ${file}`,
|
||||
})),
|
||||
...[
|
||||
MCP_CLI,
|
||||
"build/src/bin/chrome-devtools-mcp-main.js",
|
||||
"build/src/third_party/devtools-formatter-worker.js",
|
||||
"build/src/third_party/devtools-heap-snapshot-worker.js",
|
||||
"build/src/third_party/lighthouse-devtools-mcp-bundle.js",
|
||||
"LICENSE",
|
||||
"build/src/third_party/THIRD_PARTY_NOTICES",
|
||||
].map((file) => ({
|
||||
file,
|
||||
error: "unpatched or changed runtime entry build/src/TextSnapshot.js",
|
||||
},
|
||||
{
|
||||
file: MCP_CLI,
|
||||
change: "remove",
|
||||
error: `missing required runtime entry ${file}`,
|
||||
})),
|
||||
error: `missing required runtime entry ${MCP_CLI}`,
|
||||
},
|
||||
{
|
||||
file: "build/src/third_party/issue-descriptions",
|
||||
change: "remove",
|
||||
|
|
|
|||
|
|
@ -12,42 +12,14 @@ import {
|
|||
const tempDirs = useAutoCleanupTempDirTracker(afterEach);
|
||||
|
||||
describe.skipIf(process.platform === "win32")("architecture CI Go memory environment", () => {
|
||||
it.each([
|
||||
{
|
||||
name: "defaults",
|
||||
gogc: undefined,
|
||||
limit: undefined,
|
||||
expectedGc: "30",
|
||||
expectedLimit: "3GiB",
|
||||
exit: 0,
|
||||
},
|
||||
{
|
||||
name: "caller overrides",
|
||||
gogc: "70",
|
||||
limit: "8GiB",
|
||||
expectedGc: "70",
|
||||
expectedLimit: "8GiB",
|
||||
exit: 0,
|
||||
},
|
||||
{
|
||||
name: "partial override",
|
||||
gogc: "off",
|
||||
limit: undefined,
|
||||
expectedGc: "off",
|
||||
expectedLimit: "3GiB",
|
||||
exit: 0,
|
||||
},
|
||||
{
|
||||
name: "blocking command failure",
|
||||
gogc: undefined,
|
||||
limit: undefined,
|
||||
expectedGc: "30",
|
||||
expectedLimit: "3GiB",
|
||||
exit: 17,
|
||||
},
|
||||
it.each<[string, string | undefined, string | undefined, string, string, number]>([
|
||||
["defaults", undefined, undefined, "30", "3GiB", 0],
|
||||
["caller overrides", "70", "8GiB", "70", "8GiB", 0],
|
||||
["partial override", "off", undefined, "off", "3GiB", 0],
|
||||
["blocking command failure", undefined, undefined, "30", "3GiB", 17],
|
||||
])(
|
||||
"passes $name to the architecture command",
|
||||
({ gogc, limit, expectedGc, expectedLimit, exit }) => {
|
||||
"passes %s to the architecture command",
|
||||
(_name, gogc, limit, expectedGc, expectedLimit, exit) => {
|
||||
const script = readCiWorkflow().jobs["check-additional-shard"].steps.find(
|
||||
(step: WorkflowStep) => step.name === "Run additional check shard",
|
||||
)?.run;
|
||||
|
|
|
|||
|
|
@ -21,6 +21,16 @@ import { useAutoCleanupTempDirTracker } from "../helpers/temp-dir.js";
|
|||
|
||||
const tempDirs = useAutoCleanupTempDirTracker(afterEach);
|
||||
|
||||
function fixture(prefix: string) {
|
||||
const cwd = tempDirs.make(prefix);
|
||||
const write = (file: string, source: string) => {
|
||||
const target = path.join(cwd, file);
|
||||
mkdirSync(path.dirname(target), { recursive: true });
|
||||
writeFileSync(target, source);
|
||||
};
|
||||
return { cwd, write };
|
||||
}
|
||||
|
||||
vi.mock("../../scripts/test-projects.test-support.mts", async (importOriginal) => {
|
||||
const original =
|
||||
await importOriginal<typeof import("../../scripts/test-projects.test-support.mts")>();
|
||||
|
|
@ -39,12 +49,7 @@ vi.mock("../../scripts/test-projects.test-support.mts", async (importOriginal) =
|
|||
|
||||
describe("CI changed lint", () => {
|
||||
it("includes transitive and aliased type consumers without widening to their packages", async () => {
|
||||
const cwd = tempDirs.make("ci-file-lint-");
|
||||
const write = (file: string, content: string) => {
|
||||
const target = path.join(cwd, file);
|
||||
mkdirSync(path.dirname(target), { recursive: true });
|
||||
writeFileSync(target, content);
|
||||
};
|
||||
const { cwd, write } = fixture("ci-file-lint-");
|
||||
write("pnpm-workspace.yaml", "packages:\n - .\n - ui\n - packages/*\n - extensions/*\n");
|
||||
for (const root of [
|
||||
".",
|
||||
|
|
@ -145,23 +150,17 @@ describe("CI changed lint", () => {
|
|||
])(
|
||||
"retains full lint when an affected consumer augments globals through %s",
|
||||
async (file, source) => {
|
||||
const cwd = tempDirs.make("ci-ambient-consumer-");
|
||||
mkdirSync(path.join(cwd, "src"));
|
||||
writeFileSync(path.join(cwd, "package.json"), '{"type":"module"}');
|
||||
writeFileSync(path.join(cwd, "src/value.ts"), "export type Work = () => Promise<void>;\n");
|
||||
writeFileSync(path.join(cwd, "src", file), source);
|
||||
writeFileSync(path.join(cwd, "src/reader.ts"), "window.work();\n");
|
||||
const { cwd, write } = fixture("ci-ambient-consumer-");
|
||||
write("package.json", '{"type":"module"}');
|
||||
write("src/value.ts", "export type Work = () => Promise<void>;\n");
|
||||
write(`src/${file}`, source);
|
||||
write("src/reader.ts", "window.work();\n");
|
||||
expect(await resolveChangedOxlintFileScope(["src/value.ts"], cwd)).toBeUndefined();
|
||||
},
|
||||
);
|
||||
|
||||
it("runs native rules on a changed file and unchanged consumers of its changed type", () => {
|
||||
const cwd = tempDirs.make("ci-file-lint-native-");
|
||||
const write = (file: string, source: string) => {
|
||||
const target = path.join(cwd, file);
|
||||
mkdirSync(path.dirname(target), { recursive: true });
|
||||
writeFileSync(target, source);
|
||||
};
|
||||
const { cwd, write } = fixture("ci-file-lint-native-");
|
||||
write("package.json", '{"private":true,"type":"module"}');
|
||||
write("pnpm-workspace.yaml", "packages: [.]\n");
|
||||
write(
|
||||
|
|
|
|||
|
|
@ -32,6 +32,18 @@ function selectedFiles(shards: ReturnType<typeof createChangedNodeTestShards>) {
|
|||
);
|
||||
}
|
||||
|
||||
function canonicalOwner(jobs: CompactNodeTestShard[], shardName: string) {
|
||||
const job = expectDefined(
|
||||
jobs.find((candidate) => candidate.groups.some((group) => group.shard_name === shardName)),
|
||||
`canonical job for ${shardName}`,
|
||||
);
|
||||
const group = expectDefined(
|
||||
job.groups.find((candidate) => candidate.shard_name === shardName),
|
||||
`canonical group for ${shardName}`,
|
||||
);
|
||||
return { job, group };
|
||||
}
|
||||
|
||||
it("keeps the aggressive fixed smoke within two Node rows", () => {
|
||||
let smoke: string[] = [];
|
||||
resolveChangedNodeTestTargets(["src/infra/new-unlisted-module.ts"], {
|
||||
|
|
@ -282,16 +294,7 @@ it("keeps UI and core changes with exact owners and direct consumers", () => {
|
|||
const owners = group.configs.includes("test/vitest/vitest.tooling.config.ts")
|
||||
? canonicalTooling
|
||||
: canonical;
|
||||
const ownerJob = expectDefined(
|
||||
owners.find((candidate) =>
|
||||
candidate.groups.some((owner) => owner.shard_name === group.shard_name),
|
||||
),
|
||||
`canonical UI consumer job for ${group.shard_name}`,
|
||||
);
|
||||
const owner = expectDefined(
|
||||
ownerJob.groups.find((candidate) => candidate.shard_name === group.shard_name),
|
||||
"canonical UI consumer group",
|
||||
);
|
||||
const { group: owner } = canonicalOwner(owners, group.shard_name);
|
||||
if (group.includePatterns) {
|
||||
expect(group.includePatterns.length).toBeGreaterThan(0);
|
||||
} else {
|
||||
|
|
@ -299,15 +302,9 @@ it("keeps UI and core changes with exact owners and direct consumers", () => {
|
|||
}
|
||||
expect(group.configs.every((config) => owner.configs.includes(config))).toBe(true);
|
||||
// Tooling capacity follows selected files; an excluded compiler can require a larger full job.
|
||||
const selectedJob = expectDefined(
|
||||
selectedCanonical.find((candidate) =>
|
||||
candidate.groups.some((selected) => selected.shard_name === group.shard_name),
|
||||
),
|
||||
`selected UI consumer job for ${group.shard_name}`,
|
||||
);
|
||||
const selectedGroup = expectDefined(
|
||||
selectedJob.groups.find((selected) => selected.shard_name === group.shard_name),
|
||||
"selected UI consumer group",
|
||||
const { job: selectedJob, group: selectedGroup } = canonicalOwner(
|
||||
selectedCanonical,
|
||||
group.shard_name,
|
||||
);
|
||||
for (const key of [
|
||||
"configs",
|
||||
|
|
|
|||
|
|
@ -73,15 +73,10 @@ describe("changed CI compiler graph selection", () => {
|
|||
|
||||
it.for([
|
||||
[],
|
||||
["src/missing.ts"],
|
||||
["src/types/runtime.d.ts"],
|
||||
["test/tsconfig/tsconfig.test.root.json"],
|
||||
["package.json"],
|
||||
["unclassified/module.ts"],
|
||||
["docs/plugins/sdk-subpaths.md", "ui/src/styles/chat.css"],
|
||||
[sharedType, "src/config/catalog.json"],
|
||||
["extensions/example/value.ts", "extensions/example/types.d.ts"],
|
||||
["extensions/example/value.ts", "extensions/example/tsconfig.json"],
|
||||
])("retains all compilers without discovery for uncertain input %j", async (paths) => {
|
||||
expect(selectChangedCiTsgoGraphs(paths, graphs())).toBeUndefined();
|
||||
inspectGraphs.mockRejectedValue(new Error("Full plans must not enumerate compiler inputs"));
|
||||
|
|
@ -151,14 +146,13 @@ describe("changed CI compiler graph selection", () => {
|
|||
});
|
||||
});
|
||||
|
||||
it.each(["leaf", "directory", "extension-alias", "broken", "traversal", "directory-file"])(
|
||||
it.each(["leaf", "directory", "traversal", "directory-file"])(
|
||||
"retains every compiler for a nonphysical extension input (%s)",
|
||||
async (kind) => {
|
||||
const cwd = tempDirs.make("ci-type-extension-alias-");
|
||||
mkdirSync(join(cwd, "src"));
|
||||
mkdirSync(join(cwd, "extensions", "example"), { recursive: true });
|
||||
writeFileSync(join(cwd, "src/value.ts"), "export type Value = number;\n");
|
||||
writeFileSync(join(cwd, "extensions/example/other.ts"), "export type Value = number;\n");
|
||||
const file =
|
||||
kind === "traversal"
|
||||
? "extensions/example/../../src/value.ts"
|
||||
|
|
@ -170,18 +164,7 @@ describe("changed CI compiler graph selection", () => {
|
|||
} else if (kind === "directory-file") {
|
||||
mkdirSync(join(cwd, file));
|
||||
} else if (kind !== "traversal") {
|
||||
symlinkSync(
|
||||
join(
|
||||
cwd,
|
||||
kind === "broken"
|
||||
? "src/missing.ts"
|
||||
: kind === "extension-alias"
|
||||
? "extensions/example/other.ts"
|
||||
: "src/value.ts",
|
||||
),
|
||||
join(cwd, file),
|
||||
"file",
|
||||
);
|
||||
symlinkSync(join(cwd, "src/value.ts"), join(cwd, file), "file");
|
||||
}
|
||||
inspectGraphs.mockRejectedValue(
|
||||
new Error("Aliases must retain all graphs without discovery"),
|
||||
|
|
@ -215,7 +198,7 @@ describe("changed CI compiler graph selection", () => {
|
|||
},
|
||||
);
|
||||
|
||||
it.each(["missing", "duplicate", "unexpected", "unmatched", "mixed", "ambient", "config"])(
|
||||
it.each(["missing", "duplicate", "unmatched", "mixed"])(
|
||||
"refuses incomplete or inapplicable noncore discovery (%s)",
|
||||
(kind) => {
|
||||
const file = "extensions/example/value.ts";
|
||||
|
|
@ -227,29 +210,19 @@ describe("changed CI compiler graph selection", () => {
|
|||
inventory.pop();
|
||||
} else if (kind === "duplicate") {
|
||||
inventory[inventory.length - 1] = inventory[0]!;
|
||||
} else if (kind === "unexpected") {
|
||||
inventory[0] = { config: "tsconfig.core.json", files: [file] };
|
||||
} else if (kind === "unmatched") {
|
||||
paths.push("extensions/example/other.ts");
|
||||
} else if (kind === "mixed") {
|
||||
paths.push("src/value.ts");
|
||||
} else if (kind === "ambient") {
|
||||
paths.push("extensions/example/types.d.ts");
|
||||
} else if (kind === "config") {
|
||||
paths.push("extensions/example/package.json");
|
||||
}
|
||||
expect(selectChangedCiTsgoGraphs(paths, inventory, { scope: "noncore" })).toBeUndefined();
|
||||
},
|
||||
);
|
||||
|
||||
it.each(["missing", "duplicate"])("refuses an incomplete %s compiler inventory", (kind) => {
|
||||
it("refuses an incomplete full compiler inventory", () => {
|
||||
const inventory = graphs();
|
||||
inventory[0]!.files.push(sharedType);
|
||||
if (kind === "missing") {
|
||||
inventory.pop();
|
||||
} else {
|
||||
inventory[inventory.length - 1] = inventory[0]!;
|
||||
}
|
||||
inventory.pop();
|
||||
expect(selectChangedCiTsgoGraphs([sharedType], inventory)).toBeUndefined();
|
||||
});
|
||||
});
|
||||
|
|
|
|||
|
|
@ -37,8 +37,9 @@ describe("narrow PR check families", () => {
|
|||
"packages/media-core/src/types.d.ts",
|
||||
])("keeps runtime and declaration consumers for %s", (path) => {
|
||||
const scope = resolveCiCheckFamilyScope([path]);
|
||||
expect(scope.checkTasks).not.toContain("bundled-channel-config-metadata");
|
||||
expect(scope.checkTasks).toEqual(["guards", "dependencies"]);
|
||||
expect(scope.fastTasks).toEqual([]);
|
||||
expect(scope.additionalGroups).toContain("boundaries");
|
||||
expect(scope.additionalGroups).toContain("extension-package-boundary");
|
||||
expect(scope).toMatchObject({
|
||||
baselineRatchets: true,
|
||||
|
|
@ -47,15 +48,6 @@ describe("narrow PR check families", () => {
|
|||
});
|
||||
});
|
||||
|
||||
it("keeps extension correctness checks while exact runtime tests use the Node owner", () => {
|
||||
const scope = resolveCiCheckFamilyScope(["extensions/telegram/src/send.ts"]);
|
||||
expect(scope.fastTasks).toEqual([]);
|
||||
expect(scope.checkTasks).toEqual(["guards", "dependencies"]);
|
||||
expect(scope).toMatchObject({ types: true, lint: true });
|
||||
expect(scope.additionalGroups).toContain("boundaries");
|
||||
expect(scope.additionalGroups).toContain("extension-package-boundary");
|
||||
});
|
||||
|
||||
it.each(["src/config/zod-schema.core.ts", "extensions/telegram/src/config-schema.ts"])(
|
||||
"selects bundled metadata through its schema owner for %s",
|
||||
(file) => {
|
||||
|
|
@ -70,7 +62,6 @@ describe("narrow PR check families", () => {
|
|||
"src/agents/session.test-support.ts",
|
||||
"scripts/lib/source-file-scan-cache.mts",
|
||||
"config/knip.all-exports.config.ts",
|
||||
"config/test-timeout-race-baseline.txt",
|
||||
"extensions/telegram/tsconfig.json",
|
||||
"extensions/telegram/package.json",
|
||||
"pnpm-lock.yaml",
|
||||
|
|
|
|||
|
|
@ -66,7 +66,6 @@ function sealDiagnostic(before: CompilerInputSnapshot, after: CompilerInputSnaps
|
|||
it.each([
|
||||
['{"compilerOptions":{"target":"invalid"},"include":["src/**/*.ts"]}', "TS6046"],
|
||||
['{"include":"src/**/*.ts"}', "TS5024"],
|
||||
['{"files":"src/index.ts"}', "TS5024"],
|
||||
['{"extends":"./missing.json","include":["src/**/*.ts"]}', "TS5083"],
|
||||
['{"compilerOptions": {', "TS1005"],
|
||||
])("rejects invalid native compiler configuration %s", (config, diagnostic) => {
|
||||
|
|
@ -245,12 +244,15 @@ it.each([
|
|||
["config-bytes", "base.json", '{ "compilerOptions": {"target":"ES2023","types":[]} }\n'],
|
||||
])(
|
||||
"identifies a %s rejection without exposing configuration or tool bytes",
|
||||
(category, file, bytes) => {
|
||||
async (category, file, bytes) => {
|
||||
const f = fixture();
|
||||
const before = f.snapshot();
|
||||
await before.prepare();
|
||||
f.signature(before);
|
||||
f.write(file!, bytes!);
|
||||
expect(sealDiagnostic(before, f.snapshot()).detail).toEqual({ category });
|
||||
const after = f.snapshot();
|
||||
await after.prepare();
|
||||
expect(sealDiagnostic(before, after).detail).toEqual({ category });
|
||||
},
|
||||
);
|
||||
|
||||
|
|
@ -294,16 +296,6 @@ it.each(["ascii", "unicode", "controls"])(
|
|||
},
|
||||
);
|
||||
|
||||
it("prepares the same ordered source and installed-alias namespace as synchronous readers", async () => {
|
||||
const f = fixture();
|
||||
const synchronous = f.snapshot();
|
||||
const prepared = f.snapshot();
|
||||
await prepared.prepare();
|
||||
for (const outputRoot of [undefined, path.join(f.root, "packages/local/dist")]) {
|
||||
expect(f.signature(prepared, outputRoot)).toBe(f.signature(synchronous, outputRoot));
|
||||
}
|
||||
});
|
||||
|
||||
it("ignores checkout scratch packages that disappear during preparation", async () => {
|
||||
const f = fixture();
|
||||
const original = f.signature(f.snapshot());
|
||||
|
|
@ -545,21 +537,20 @@ it("preloads sibling subtrees while the ordered visitor waits on a deeper direct
|
|||
expect(active).toBe(0);
|
||||
expect(observed.has(path.join(f.root, ".artifacts"))).toBe(false);
|
||||
expect(observed.has(path.join(f.root, ".cache/vitest"))).toBe(false);
|
||||
expect(f.signature(snapshot)).toBe(f.signature(f.snapshot()));
|
||||
const synchronous = f.snapshot();
|
||||
for (const outputRoot of [undefined, path.join(f.root, "packages/local/dist")]) {
|
||||
expect(f.signature(snapshot, outputRoot)).toBe(f.signature(synchronous, outputRoot));
|
||||
}
|
||||
});
|
||||
|
||||
it.each([
|
||||
["source addition", "src/shadow.ts", "export const shadow = 1;\n"],
|
||||
["package addition", "src/package.json", '{"type":"commonjs"}'],
|
||||
["nested workspace metadata", "packages/local/.tmp/package.json", '{"type":"commonjs"}'],
|
||||
[
|
||||
"installed package metadata",
|
||||
"packages/local/package.json",
|
||||
'{"name":"fixture-package","type":"commonjs"}',
|
||||
],
|
||||
["inherited config", "base.json", '{"compilerOptions":{"target":"ES2022","types":[]}}'],
|
||||
["generator input", "scripts/generator.mts", "export const generator = 2;\n"],
|
||||
["compiler input", "tools/compiler.js", "export const compiler = 2;\n"],
|
||||
])("retains invalidation after a %s change", async (_label, filename, bytes) => {
|
||||
const f = fixture();
|
||||
const before = f.snapshot();
|
||||
|
|
|
|||
|
|
@ -169,7 +169,6 @@ afterEach(() => {
|
|||
|
||||
describe("syncControlUiCatalogFallbackBaseline", () => {
|
||||
it("checks and writes ordered fallback bytes without rewriting a matching baseline", async () => {
|
||||
const stdout = vi.spyOn(process.stdout, "write").mockReturnValue(true);
|
||||
const expected = baseline({ "group.second": ["de"], missing: ["de", "fr"] });
|
||||
|
||||
await expect(
|
||||
|
|
@ -179,9 +178,6 @@ describe("syncControlUiCatalogFallbackBaseline", () => {
|
|||
await syncControlUiCatalogFallbackBaseline(writeOptions);
|
||||
expect(fixture.writes).toEqual([{ path: baselinePath, data: expected }]);
|
||||
expect(fixture.files.get(baselinePath)).toBe(expected);
|
||||
expect(stdout).toHaveBeenCalledWith(
|
||||
"control-ui-i18n: catalog: fallback_keys=2 fallback_pairs=3\n",
|
||||
);
|
||||
|
||||
fixture.writes.length = 0;
|
||||
await syncControlUiCatalogFallbackBaseline(writeOptions);
|
||||
|
|
@ -231,26 +227,22 @@ describe("syncControlUiCatalogFallbackBaseline", () => {
|
|||
);
|
||||
});
|
||||
|
||||
it.each([false, true])(
|
||||
"preserves the first missing/malformed memory error (allowCatalogDrift=%s)",
|
||||
async (allowCatalogDrift) => {
|
||||
fixture.files.delete(memoryPath("fr"));
|
||||
fixture.files.set(memoryPath("de"), "{");
|
||||
await expect(
|
||||
syncControlUiCatalogFallbackBaseline({ ...writeOptions, allowCatalogDrift }),
|
||||
).rejects.toThrow("ui/src/i18n/.i18n/fr.tm.jsonl does not contain fr translations");
|
||||
it("preserves the first missing/malformed memory error during scoped sync", async () => {
|
||||
fixture.files.delete(memoryPath("fr"));
|
||||
fixture.files.set(memoryPath("de"), "{");
|
||||
await expect(
|
||||
syncControlUiCatalogFallbackBaseline({ ...writeOptions, allowCatalogDrift: true }),
|
||||
).rejects.toThrow("ui/src/i18n/.i18n/fr.tm.jsonl does not contain fr translations");
|
||||
|
||||
fixture.files.set(memoryPath("fr"), "{");
|
||||
fixture.files.delete(memoryPath("de"));
|
||||
await expect(
|
||||
syncControlUiCatalogFallbackBaseline({ ...writeOptions, allowCatalogDrift }),
|
||||
).rejects.toBeInstanceOf(SyntaxError);
|
||||
expect(fixture.writes).toEqual([]);
|
||||
},
|
||||
);
|
||||
fixture.files.set(memoryPath("fr"), "{");
|
||||
fixture.files.delete(memoryPath("de"));
|
||||
await expect(
|
||||
syncControlUiCatalogFallbackBaseline({ ...writeOptions, allowCatalogDrift: true }),
|
||||
).rejects.toBeInstanceOf(SyntaxError);
|
||||
expect(fixture.writes).toEqual([]);
|
||||
});
|
||||
|
||||
it("only tolerates analyzer drift during scoped sync", async () => {
|
||||
const stdout = vi.spyOn(process.stdout, "write").mockReturnValue(true);
|
||||
fixture.loadSource.mockReturnValue({ greeting: "Hello {count}" });
|
||||
setMemory("fr", [
|
||||
row({
|
||||
|
|
@ -268,23 +260,17 @@ describe("syncControlUiCatalogFallbackBaseline", () => {
|
|||
expect(fixture.writes).toEqual([]);
|
||||
await syncControlUiCatalogFallbackBaseline({ ...writeOptions, allowCatalogDrift: true });
|
||||
expect(fixture.files.get(baselinePath)).toBe(baseline({ greeting: ["de"] }));
|
||||
expect(stdout).toHaveBeenCalledWith(
|
||||
"control-ui-i18n: catalog: tolerated_errors=1 during scoped sync\n",
|
||||
);
|
||||
});
|
||||
|
||||
it.each([false, true])(
|
||||
"rejects terminology errors before later locale errors (allowCatalogDrift=%s)",
|
||||
async (allowCatalogDrift) => {
|
||||
fixture.loadSource.mockReturnValue({ sessionsView: { subagentPrefix: "abc" } });
|
||||
setMemory("fr", [
|
||||
row({ segment_id: "sessionsView.subagentPrefix", segment_ids: [], translated: "Cron" }),
|
||||
]);
|
||||
fixture.files.set(memoryPath("de"), "{");
|
||||
await expect(
|
||||
syncControlUiCatalogFallbackBaseline({ ...writeOptions, allowCatalogDrift }),
|
||||
).rejects.toThrow("fr: sessionsView.subagentPrefix");
|
||||
expect(fixture.writes).toEqual([]);
|
||||
},
|
||||
);
|
||||
it("rejects terminology errors before later locale errors during scoped sync", async () => {
|
||||
fixture.loadSource.mockReturnValue({ sessionsView: { subagentPrefix: "abc" } });
|
||||
setMemory("fr", [
|
||||
row({ segment_id: "sessionsView.subagentPrefix", segment_ids: [], translated: "Cron" }),
|
||||
]);
|
||||
fixture.files.set(memoryPath("de"), "{");
|
||||
await expect(
|
||||
syncControlUiCatalogFallbackBaseline({ ...writeOptions, allowCatalogDrift: true }),
|
||||
).rejects.toThrow("fr: sessionsView.subagentPrefix");
|
||||
expect(fixture.writes).toEqual([]);
|
||||
});
|
||||
});
|
||||
|
|
|
|||
|
|
@ -34,52 +34,54 @@ function scan(css: string) {
|
|||
return scanIconGridFit(css, [fixture], base);
|
||||
}
|
||||
|
||||
const cramped =
|
||||
'html`<header class="toolbar"><button class="icon">${icons.refresh}</button></header>`';
|
||||
|
||||
function createAuditFixture(sheets: Record<string, string> = {}, markup = cramped) {
|
||||
const root = tempDirs.make("openclaw-icon-grid-");
|
||||
const styles = path.join(root, "ui/src/styles");
|
||||
const source = path.join(root, "ui/src/control.ts");
|
||||
fs.mkdirSync(styles, { recursive: true });
|
||||
for (const [name, css] of Object.entries({
|
||||
"base.css": base,
|
||||
"components.css": "",
|
||||
"control.css": original,
|
||||
...sheets,
|
||||
})) {
|
||||
fs.writeFileSync(path.join(styles, name), css);
|
||||
}
|
||||
fs.writeFileSync(source, markup);
|
||||
return { root, styles, source };
|
||||
}
|
||||
|
||||
describe("fixed icon-grid fit", () => {
|
||||
it("catches the shipped size override rather than flagging the safe base control", () => {
|
||||
const findings = scan(original).findings;
|
||||
expect(findings).toHaveLength(2);
|
||||
expect(findings).toEqual(
|
||||
expect.arrayContaining([
|
||||
for (const axis of ["width", "height"]) {
|
||||
expect(findings).toContainEqual(
|
||||
expect.objectContaining({
|
||||
kind: "overflow",
|
||||
axis: "width",
|
||||
axis,
|
||||
size: 26,
|
||||
padding: 12,
|
||||
border: 2,
|
||||
icon: 17,
|
||||
available: 12,
|
||||
}),
|
||||
expect.objectContaining({
|
||||
kind: "overflow",
|
||||
axis: "height",
|
||||
size: 26,
|
||||
padding: 12,
|
||||
border: 2,
|
||||
icon: 17,
|
||||
available: 12,
|
||||
}),
|
||||
]),
|
||||
);
|
||||
);
|
||||
}
|
||||
expect(scan(original + ".toolbar > button {padding:0}").findings).toEqual([]);
|
||||
expect(scan(original.replace("width:26px; height:26px;", "")).findings).toEqual([]);
|
||||
});
|
||||
|
||||
it("requires authored padding instead of trusting jsdom's zero native default", () => {
|
||||
const css = original.replace("padding:6px;", "");
|
||||
expect(scan(css).findings).toEqual([
|
||||
expect.objectContaining({
|
||||
kind: "native-padding",
|
||||
axis: "width",
|
||||
padding: null,
|
||||
available: null,
|
||||
}),
|
||||
expect.objectContaining({
|
||||
kind: "native-padding",
|
||||
axis: "height",
|
||||
padding: null,
|
||||
available: null,
|
||||
}),
|
||||
]);
|
||||
expect(scan(css).findings).toEqual(
|
||||
["width", "height"].map((axis) =>
|
||||
expect.objectContaining({ kind: "native-padding", axis, padding: null, available: null }),
|
||||
),
|
||||
);
|
||||
expect(scan(css + ".icon {padding:0}").findings).toEqual([]);
|
||||
});
|
||||
|
||||
|
|
@ -115,7 +117,6 @@ describe("fixed icon-grid fit", () => {
|
|||
".toolbar .icon {width:40px!important}.icon {width:24px!important}",
|
||||
],
|
||||
["always-applicable media correction", "@media all {.icon {padding:0}}"],
|
||||
["supports correction", "@supports (display:grid) {.icon {padding:0}}"],
|
||||
["nested-only padding", ".icon {&:hover {padding:0}}"],
|
||||
])("defers %s rather than inventing a resolved geometry", (_name, correction) => {
|
||||
const result = scan(original + correction);
|
||||
|
|
@ -123,9 +124,9 @@ describe("fixed icon-grid fit", () => {
|
|||
expect(result.unresolved).toBeGreaterThan(0);
|
||||
});
|
||||
|
||||
it.each(["hover", "focus-visible", "active"])("defers top-level %s geometry", (state) => {
|
||||
it("defers top-level hover geometry", () => {
|
||||
const result = scan(
|
||||
original + ".toolbar > button {padding:0}.toolbar > button:" + state + " {padding:8px}",
|
||||
original + ".toolbar > button {padding:0}.toolbar > button:hover {padding:8px}",
|
||||
);
|
||||
expect(result.findings).toHaveLength(0);
|
||||
expect(result.checked).toBe(0);
|
||||
|
|
@ -133,14 +134,8 @@ describe("fixed icon-grid fit", () => {
|
|||
});
|
||||
|
||||
it("defers cross-sheet ancestor, tag, ID, attribute, and SVG overrides", () => {
|
||||
const root = tempDirs.make("openclaw-icon-grid-cross-sheet-");
|
||||
const styles = path.join(root, "ui/src/styles");
|
||||
fs.mkdirSync(styles, { recursive: true });
|
||||
fs.writeFileSync(path.join(styles, "base.css"), base);
|
||||
fs.writeFileSync(path.join(styles, "components.css"), "");
|
||||
fs.writeFileSync(path.join(styles, "control.css"), original);
|
||||
fs.writeFileSync(
|
||||
path.join(root, "ui/src/control.ts"),
|
||||
const { root, styles } = createAuditFixture(
|
||||
{},
|
||||
'html`<header class="toolbar"><button class="icon" id="action" title="Preview">${icons.refresh}</button></header>`',
|
||||
);
|
||||
for (const correction of [
|
||||
|
|
@ -249,17 +244,7 @@ describe("fixed icon-grid fit", () => {
|
|||
});
|
||||
|
||||
it("audits current source and shared styles on each manual invocation", () => {
|
||||
const root = tempDirs.make("openclaw-icon-grid-");
|
||||
const styles = path.join(root, "ui/src/styles");
|
||||
fs.mkdirSync(styles, { recursive: true });
|
||||
fs.writeFileSync(path.join(styles, "base.css"), base);
|
||||
fs.writeFileSync(path.join(styles, "components.css"), "");
|
||||
const source = path.join(root, "ui/src/control.ts");
|
||||
fs.writeFileSync(
|
||||
source,
|
||||
'html`<header class="toolbar"><button class="icon">${icons.refresh}</button></header>`',
|
||||
);
|
||||
fs.writeFileSync(path.join(styles, "control.css"), original);
|
||||
const { root, styles, source } = createAuditFixture();
|
||||
const audit = () => auditIconButtons(root, ["ui/src/styles/control.css"]);
|
||||
const first = audit();
|
||||
expect(first.findings).toHaveLength(2);
|
||||
|
|
@ -267,8 +252,6 @@ describe("fixed icon-grid fit", () => {
|
|||
fs.writeFileSync(source, 'html`<button class="icon">${icons.refresh}</button>`');
|
||||
expect(audit().findings).toHaveLength(0);
|
||||
const added = path.join(root, "ui/src/added.ts");
|
||||
const cramped =
|
||||
'html`<header class="toolbar"><button class="icon">${icons.refresh}</button></header>`';
|
||||
fs.writeFileSync(added, cramped);
|
||||
expect(audit().findings).toHaveLength(2);
|
||||
fs.unlinkSync(added);
|
||||
|
|
@ -288,18 +271,11 @@ describe("fixed icon-grid fit", () => {
|
|||
});
|
||||
|
||||
it("does not append the base sheet again after component overrides", () => {
|
||||
const root = tempDirs.make("openclaw-icon-grid-order-");
|
||||
const styles = path.join(root, "ui/src/styles");
|
||||
fs.mkdirSync(styles, { recursive: true });
|
||||
fs.writeFileSync(path.join(styles, "base.css"), base + original);
|
||||
fs.writeFileSync(
|
||||
path.join(styles, "components.css"),
|
||||
".toolbar > button {width:32px;height:32px}",
|
||||
);
|
||||
fs.writeFileSync(
|
||||
path.join(root, "ui/src/control.ts"),
|
||||
'html`<header class="toolbar"><button class="icon">${icons.refresh}</button></header>`',
|
||||
);
|
||||
const { root } = createAuditFixture({
|
||||
"base.css": base + original,
|
||||
"components.css": ".toolbar > button {width:32px;height:32px}",
|
||||
"control.css": "",
|
||||
});
|
||||
const result = auditIconButtons(root, ["ui/src/styles/base.css"]);
|
||||
expect(result.findings).toHaveLength(0);
|
||||
expect(result.checkedAxes).toBe(2);
|
||||
|
|
|
|||
|
|
@ -23,19 +23,6 @@ describe("Control UI plugin discovery preview", () => {
|
|||
const result = buildPluginDiscoveryCategoriesMock();
|
||||
|
||||
expect(Value.Check(PluginsCatalogCategoriesResultSchema, result)).toBe(true);
|
||||
expect(result.categories.map((category) => category.slug)).toEqual([
|
||||
"channels",
|
||||
"models",
|
||||
"memory",
|
||||
"context",
|
||||
"voice",
|
||||
"media",
|
||||
"web",
|
||||
"tools",
|
||||
"runtime",
|
||||
"gateway",
|
||||
"security",
|
||||
"other",
|
||||
]);
|
||||
expect(result.categories.length).toBeGreaterThan(0);
|
||||
});
|
||||
});
|
||||
|
|
|
|||
|
|
@ -97,10 +97,8 @@ fi
|
|||
["shared setup action", "fresh"],
|
||||
["default hydration", "legacy"],
|
||||
["GitHub hydration", "legacy"],
|
||||
["default hydration", "unknown"],
|
||||
["GitHub hydration", "unknown"],
|
||||
["default hydration", "unknown-newline"],
|
||||
["default hydration", "fallback"],
|
||||
["default hydration", "fallback-dangling"],
|
||||
["default hydration", "configured-fallback"],
|
||||
["default hydration", "unknown-fallback"],
|
||||
|
|
@ -116,7 +114,6 @@ fi
|
|||
const store = path.join(root, "store");
|
||||
const runnerTemp = path.join(root, "runner");
|
||||
const usesFallback = [
|
||||
"fallback",
|
||||
"fallback-dangling",
|
||||
"configured-fallback",
|
||||
"unknown-fallback",
|
||||
|
|
@ -303,26 +300,7 @@ fi
|
|||
rmSync(path.join(workspace, "node_modules"), { recursive: true, force: true });
|
||||
symlinkSync(linkedModules, path.join(workspace, "node_modules"));
|
||||
|
||||
const handoff = path.join(env.HOME!, ".crabbox/actions/hydration-proof.env");
|
||||
const exports = `${handoff}.sh`;
|
||||
const legacyExports =
|
||||
job === "hydrate-github"
|
||||
? `export PNPM_CONFIG_MODULES_DIR=${shellQuote(externalModules)}\nexport PNPM_CONFIG_VIRTUAL_STORE_DIR=${shellQuote(path.join(externalRoot, "virtual-store"))}\n`
|
||||
: `export CRABBOX_PNPM_MODULES_DIR=${shellQuote(externalModules)}\n`;
|
||||
write(
|
||||
root,
|
||||
path.relative(root, handoff),
|
||||
`WORKSPACE=${workspace}\nRUN_ID=fixture\nJOB=${job}\nENV_FILE=${exports}\nSERVICES_FILE=${handoff.replace(/\.env$/u, ".services")}\nREADY_AT=2026-09-14T00:00:00Z\n`,
|
||||
);
|
||||
write(
|
||||
root,
|
||||
path.relative(root, exports),
|
||||
`export CI=true\nexport GITHUB_WORKSPACE=${shellQuote(workspace)}\nexport GITHUB_RUN_ID=fixture\nexport PNPM_CONFIG_STORE_DIR=${shellQuote(legacyStore)}\n${usesFallback ? `export XDG_CACHE_HOME=${shellQuote(cacheRoot)}\n` : ""}${legacyExports}`,
|
||||
);
|
||||
// Released Crabbox clears both native handoff markers before starting rehydration.
|
||||
rmSync(handoff);
|
||||
rmSync(exports);
|
||||
expect(existsSync(handoff) || existsSync(exports)).toBe(false);
|
||||
if (initialState === "fallback-dangling") {
|
||||
// Native rehydration recreates the same lease's runner root before workflow steps.
|
||||
rmSync(runnerTemp, { recursive: true });
|
||||
|
|
|
|||
|
|
@ -247,51 +247,37 @@ describe.skipIf(process.platform === "win32")("persistent Crabbox source capsule
|
|||
}
|
||||
});
|
||||
|
||||
it.each(["empty", "changed source"])(
|
||||
"reuses unchanged mirror files after a same-ref %s commit and seals the new witness",
|
||||
(change) => {
|
||||
const f = fixture();
|
||||
const first = f.prepare();
|
||||
const stable = fileIdentity(join(first.directory, "stable.txt"));
|
||||
first.cleanup();
|
||||
if (change === "changed source") {
|
||||
writeFileSync(join(f.repository, "change.txt"), "committed newer bytes\r\n");
|
||||
f.git(f.repository, "add", "change.txt");
|
||||
}
|
||||
f.git(
|
||||
f.repository,
|
||||
"-c",
|
||||
"commit.gpgsign=false",
|
||||
"commit",
|
||||
"--quiet",
|
||||
"--allow-empty",
|
||||
"-m",
|
||||
"next head",
|
||||
it("reuses unchanged mirror files after a same-ref source commit and seals the new witness", () => {
|
||||
const f = fixture();
|
||||
const first = f.prepare();
|
||||
const stable = fileIdentity(join(first.directory, "stable.txt"));
|
||||
first.cleanup();
|
||||
writeFileSync(join(f.repository, "change.txt"), "committed newer bytes\r\n");
|
||||
f.git(f.repository, "add", "change.txt");
|
||||
f.git(f.repository, "-c", "commit.gpgsign=false", "commit", "--quiet", "-m", "next head");
|
||||
const head = f.git(f.repository, "rev-parse", "HEAD");
|
||||
expect(head).not.toBe(first.sourceSha);
|
||||
const next = f.prepare();
|
||||
try {
|
||||
expect(next.directory).toBe(first.directory);
|
||||
expect(fileIdentity(join(next.directory, "stable.txt"))).toEqual(stable);
|
||||
expect(next.sourceSha).toBe(head);
|
||||
expect(readFileSync(join(next.directory, "change.txt"), "utf8")).toBe(
|
||||
"committed newer bytes\r\n",
|
||||
);
|
||||
const head = f.git(f.repository, "rev-parse", "HEAD");
|
||||
expect(head).not.toBe(first.sourceSha);
|
||||
const next = f.prepare();
|
||||
try {
|
||||
expect(next.directory).toBe(first.directory);
|
||||
expect(fileIdentity(join(next.directory, "stable.txt"))).toEqual(stable);
|
||||
expect(next.sourceSha).toBe(head);
|
||||
expect(readFileSync(join(next.directory, "change.txt"), "utf8")).toBe(
|
||||
change === "changed source" ? "committed newer bytes\r\n" : "original bytes\n",
|
||||
);
|
||||
const receipt: unknown = JSON.parse(
|
||||
readFileSync(join(next.staging.root, "staging.json"), "utf8"),
|
||||
);
|
||||
expect(receipt).toHaveProperty("witness", {
|
||||
gitDir: f.git(f.repository, "rev-parse", "--path-format=absolute", "--git-common-dir"),
|
||||
ref: "refs/heads/main",
|
||||
commit: head,
|
||||
});
|
||||
f.expectColdEquivalent(next);
|
||||
} finally {
|
||||
next.cleanup();
|
||||
}
|
||||
},
|
||||
);
|
||||
const receipt: unknown = JSON.parse(
|
||||
readFileSync(join(next.staging.root, "staging.json"), "utf8"),
|
||||
);
|
||||
expect(receipt).toHaveProperty("witness", {
|
||||
gitDir: f.git(f.repository, "rev-parse", "--path-format=absolute", "--git-common-dir"),
|
||||
ref: "refs/heads/main",
|
||||
commit: head,
|
||||
});
|
||||
f.expectColdEquivalent(next);
|
||||
} finally {
|
||||
next.cleanup();
|
||||
}
|
||||
});
|
||||
|
||||
it.each(["ref", "Git directory"])(
|
||||
"rebuilds the mirror when its retained source %s changes",
|
||||
|
|
|
|||
|
|
@ -133,23 +133,9 @@ it.runIf(process.platform === "linux")(
|
|||
{ command: "busctl", args: ["--machine", "testuser@", ...versionArgs] },
|
||||
{ command: "systemctl", args: ["--system", "is-system-running"] },
|
||||
]);
|
||||
console.info("original Doctor argv:", JSON.stringify(invocations[0]?.args));
|
||||
},
|
||||
);
|
||||
|
||||
it.runIf(process.platform === "linux")("still rejects an unavailable synthetic bus", async () => {
|
||||
const env = scenarioEnvironment();
|
||||
vi.mocked(execFileUtf8).mockResolvedValue({
|
||||
code: 1,
|
||||
termination: "exit",
|
||||
stdout: "",
|
||||
stderr: "Failed to connect to bus: No such file or directory",
|
||||
});
|
||||
await expect(resolveSystemdUserTransport(env)).rejects.toMatchObject({
|
||||
reason: "systemd-user-bus-unavailable",
|
||||
});
|
||||
});
|
||||
|
||||
it("keeps machine scope, auto-start, and foreign-manager probes outside the shim contract", () => {
|
||||
const env = scenarioEnvironment();
|
||||
for (const args of [
|
||||
|
|
|
|||
|
|
@ -20,7 +20,6 @@ describe("extended-stable live publication eligibility", () => {
|
|||
});
|
||||
|
||||
it.skipIf(process.platform === "win32").each([
|
||||
{ mainVersion: "2026.8.1", expectedStatus: 42, expectedError: "" },
|
||||
{ mainVersion: "2026.9.1", expectedStatus: 42, expectedError: "" },
|
||||
{
|
||||
mainVersion: "2026.10.1",
|
||||
|
|
|
|||
|
|
@ -38,7 +38,7 @@ describe("cold-import resource observations", () => {
|
|||
it.each([
|
||||
"not a resource record",
|
||||
RESOURCE_MARKER + "{",
|
||||
...[undefined, 0, -1, 1.5].map(
|
||||
...[undefined, 0, 1.5].map(
|
||||
(pid) =>
|
||||
RESOURCE_MARKER + JSON.stringify({ ...observation, pid, userCpuUs: 1, systemCpuUs: 0 }),
|
||||
),
|
||||
|
|
|
|||
|
|
@ -679,7 +679,6 @@ describe("publication status real CLI", () => {
|
|||
"duplicate-name",
|
||||
"duplicate-id",
|
||||
"count-gap",
|
||||
"attempt-limit",
|
||||
])("rejects independently observed %s", async (kind) => {
|
||||
const result = await runPublicationCli(publicationFixture(), undefined, (responses) => {
|
||||
const prefix = `repos/${REPOSITORY}/actions/`;
|
||||
|
|
@ -711,32 +710,11 @@ describe("publication status real CLI", () => {
|
|||
if (kind === "count-gap") {
|
||||
list.total_count++;
|
||||
}
|
||||
if (kind === "attempt-limit") {
|
||||
Object.assign(responses[`${prefix}runs/101`] as object, { run_attempt: 100000000 });
|
||||
}
|
||||
});
|
||||
expect(result.status, result.stdout).toBe(1);
|
||||
expect(JSON.parse(result.stdout).publication.collection.complete).toBe(false);
|
||||
});
|
||||
|
||||
it.each(["88", "77", "101"])(
|
||||
"refuses advancing run %s without restarting observation",
|
||||
async (runId) => {
|
||||
const result = await runPublicationCli(publicationFixture(), undefined, (responses) => {
|
||||
const path = `repos/${REPOSITORY}/actions/runs/${runId}`;
|
||||
const before = responses[path] as { run_attempt: number };
|
||||
responses[path] = {
|
||||
sequence: [before, { ...before, run_attempt: before.run_attempt + 1 }],
|
||||
};
|
||||
});
|
||||
expect(result.status).toBe(1);
|
||||
expect(JSON.parse(result.stdout).publication.collection.error).toBe("attempt-changed");
|
||||
expect(
|
||||
result.calls.filter((call) => call.includes(`repos/${REPOSITORY}/actions/runs/${runId}`)),
|
||||
).toHaveLength(2);
|
||||
},
|
||||
);
|
||||
|
||||
it.each(["missing", "expired", "legacy-only", "unsupported", "incomplete-link"])(
|
||||
"keeps authenticated historical %s unknown, not failed publication",
|
||||
async (kind) => {
|
||||
|
|
@ -773,24 +751,21 @@ describe("publication status real CLI", () => {
|
|||
},
|
||||
);
|
||||
|
||||
it.each(["403 quota", "404 unavailable", "network timeout"])(
|
||||
"classifies %s as unavailable, never absence",
|
||||
async (failure) => {
|
||||
const result = await runPublicationCli(publicationFixture(), undefined, (responses) => {
|
||||
responses[`repos/${REPOSITORY}/actions/runs/88/artifacts?per_page=100&page=1`] = {
|
||||
failure: `${failure}: /private/fixture/credential synthetic-secret`,
|
||||
};
|
||||
});
|
||||
expect(result.status).toBe(1);
|
||||
expect(JSON.parse(result.stdout).publication.collection).toEqual({
|
||||
complete: false,
|
||||
error: "transport",
|
||||
});
|
||||
expect(result.stdout + result.stderr).not.toMatch(
|
||||
/synthetic-secret|private\/fixture|quota|404 unavailable/u,
|
||||
);
|
||||
},
|
||||
);
|
||||
it("classifies failed artifact reads as unavailable, never absence", async () => {
|
||||
const result = await runPublicationCli(publicationFixture(), undefined, (responses) => {
|
||||
responses[`repos/${REPOSITORY}/actions/runs/88/artifacts?per_page=100&page=1`] = {
|
||||
failure: "404 unavailable: /private/fixture/credential synthetic-secret",
|
||||
};
|
||||
});
|
||||
expect(result.status).toBe(1);
|
||||
expect(JSON.parse(result.stdout).publication.collection).toEqual({
|
||||
complete: false,
|
||||
error: "transport",
|
||||
});
|
||||
expect(result.stdout + result.stderr).not.toMatch(
|
||||
/synthetic-secret|private\/fixture|quota|404 unavailable/u,
|
||||
);
|
||||
});
|
||||
|
||||
it("retains partial package successes and truncation without declaring a complete observation", async () => {
|
||||
const fixture = publicationFixture();
|
||||
|
|
@ -973,6 +948,9 @@ describe("publication status real CLI", () => {
|
|||
expect(result.status).toBe(1);
|
||||
expect(JSON.parse(result.stdout).publication.collection.error).toBe("attempt-changed");
|
||||
expect(JSON.parse(result.stdout).publication.relationship.status).toBe("verified");
|
||||
expect(
|
||||
result.calls.filter((call) => call.includes(`repos/${REPOSITORY}/actions/runs/101`)),
|
||||
).toHaveLength(2);
|
||||
});
|
||||
|
||||
it.each(["transport", "workflow", "attempt-limit"])(
|
||||
|
|
@ -1077,6 +1055,10 @@ describe("publication status real CLI", () => {
|
|||
});
|
||||
expect(result.status).toBe(1);
|
||||
expect(JSON.parse(result.stdout).publication.relationship.status).toBe("invalid");
|
||||
expect(JSON.parse(result.stdout).publication.collection.error).toBe("attempt-changed");
|
||||
expect(
|
||||
result.calls.filter((call) => call.includes(`repos/${REPOSITORY}/actions/runs/${runId}`)),
|
||||
).toHaveLength(2);
|
||||
},
|
||||
);
|
||||
|
||||
|
|
@ -1317,11 +1299,11 @@ describe("publication status real CLI", () => {
|
|||
},
|
||||
);
|
||||
|
||||
it.each(["traversal", "unexpected-entry", "expanded", "truncated", "corrupt", "duplicate-entry"])(
|
||||
it.each(["unexpected-entry", "expanded"])(
|
||||
"refuses %s archives before projecting diagnostics",
|
||||
async (kind) => {
|
||||
const fixture = publicationFixture();
|
||||
const result = await runPublicationCli(fixture, undefined, async (responses, artifact) => {
|
||||
const result = await runPublicationCli(fixture, undefined, async (_responses, artifact) => {
|
||||
await artifact(
|
||||
3,
|
||||
fixture.publisher,
|
||||
|
|
@ -1329,41 +1311,14 @@ describe("publication status real CLI", () => {
|
|||
DIAGNOSTIC_FILE,
|
||||
fixture.diagnostic,
|
||||
(zip) => {
|
||||
if (kind === "traversal") {
|
||||
zip.file("../escape.json", "{}");
|
||||
}
|
||||
if (kind === "unexpected-entry") {
|
||||
zip.file("extra.json", "{}");
|
||||
}
|
||||
if (kind === "expanded") {
|
||||
zip.file(DIAGNOSTIC_FILE, " ".repeat(128 * 1024 + 1));
|
||||
}
|
||||
if (kind === "duplicate-entry") {
|
||||
zip.file("x".repeat(DIAGNOSTIC_FILE.length), "{}");
|
||||
}
|
||||
},
|
||||
);
|
||||
const archive = responses[`repos/${REPOSITORY}/actions/artifacts/3/zip`] as {
|
||||
binary: string;
|
||||
};
|
||||
let bytes = Buffer.from(archive.binary, "base64");
|
||||
if (kind === "truncated") {
|
||||
bytes = bytes.subarray(0, -10);
|
||||
}
|
||||
if (kind === "corrupt") {
|
||||
bytes[0] = 0;
|
||||
}
|
||||
if (kind === "duplicate-entry") {
|
||||
const needle = Buffer.from("x".repeat(DIAGNOSTIC_FILE.length));
|
||||
for (let offset = bytes.indexOf(needle); offset !== -1; offset = bytes.indexOf(needle)) {
|
||||
bytes.set(Buffer.from(DIAGNOSTIC_FILE), offset);
|
||||
}
|
||||
}
|
||||
archive.binary = bytes.toString("base64");
|
||||
Object.assign(responses[`repos/${REPOSITORY}/actions/artifacts/3`] as object, {
|
||||
size_in_bytes: bytes.length,
|
||||
digest: `sha256:${createHash("sha256").update(bytes).digest("hex")}`,
|
||||
});
|
||||
});
|
||||
expect(result.status).toBe(1);
|
||||
expect(JSON.parse(result.stdout).publication.verification.state).toBe("unknown");
|
||||
|
|
@ -1418,7 +1373,6 @@ describe("publication status real CLI", () => {
|
|||
});
|
||||
|
||||
it.each([
|
||||
[1, true],
|
||||
[100, true],
|
||||
[100, false],
|
||||
] as const)(
|
||||
|
|
|
|||
|
|
@ -123,15 +123,6 @@ describe("FreeBSD CLI runtime installation", () => {
|
|||
},
|
||||
);
|
||||
|
||||
it("refuses private Node recovery before linking or changing packages", () => {
|
||||
const { bin, prefix } = fixture();
|
||||
const result = install(bin, prefix, "NODE_ONLY=1");
|
||||
expect(result.status).toBe(1);
|
||||
expect(result.stdout).toContain("Private Node.js recovery is unavailable on FreeBSD");
|
||||
expect(result.stdout).not.toContain("unexpected");
|
||||
expect(existsSync(prefix)).toBe(false);
|
||||
});
|
||||
|
||||
it("explains how to install missing Git without invoking pkg", () => {
|
||||
const result = run(`
|
||||
uname() { printf 'FreeBSD\\n'; }
|
||||
|
|
@ -146,11 +137,14 @@ describe("FreeBSD CLI runtime installation", () => {
|
|||
});
|
||||
|
||||
describe("FreeBSD source-install admission", () => {
|
||||
it.each(
|
||||
["--install-method git", "--method git", "--git", "--github", "--npm --git", ""].flatMap(
|
||||
(args) => [false, true].map((json) => ({ args, json })),
|
||||
),
|
||||
)("refuses $args before installation side effects (JSON: $json)", ({ args, json }) => {
|
||||
it.each([
|
||||
{ args: "--install-method git", json: false },
|
||||
{ args: "--method git", json: true },
|
||||
{ args: "--git", json: false },
|
||||
{ args: "--github", json: true },
|
||||
{ args: "--npm --git", json: true },
|
||||
{ args: "", json: false },
|
||||
])("refuses $args before installation side effects (JSON: $json)", ({ args, json }) => {
|
||||
const { root, prefix } = fixture();
|
||||
const oldRuntime = join(root, "old-runtime");
|
||||
const checkout = join(root, "checkout");
|
||||
|
|
@ -209,11 +203,7 @@ describe("FreeBSD source-install admission", () => {
|
|||
|
||||
it.each([
|
||||
["freebsd", "--git --npm"],
|
||||
["freebsd", "--github --install-method npm"],
|
||||
["freebsd", "--method npm"],
|
||||
["linux", "--git"],
|
||||
["darwin", "--git"],
|
||||
["linux", "--npm"],
|
||||
["darwin", "--npm"],
|
||||
])("keeps %s %s on its selected install route", (os, args) => {
|
||||
const result = run(
|
||||
|
|
|
|||
|
|
@ -336,37 +336,6 @@ try {
|
|||
`,
|
||||
].join("\n"),
|
||||
},
|
||||
{
|
||||
name: "openclaw-native-command-exit",
|
||||
source: [
|
||||
scriptWithoutEntryPoint,
|
||||
"",
|
||||
"function Get-OpenClawCommandPath { return (Get-Process -Id $PID).Path }",
|
||||
"$caught = $false",
|
||||
"try {",
|
||||
" Invoke-OpenClawCommand -NoLogo -NoProfile -Command 'exit 17'",
|
||||
"} catch {",
|
||||
" if ($_.Exception.Message -notmatch 'failed with exit code 17') { throw }",
|
||||
" $caught = $true",
|
||||
"}",
|
||||
"if (-not $caught) { throw 'nonzero native exit was accepted' }",
|
||||
"",
|
||||
].join("\n"),
|
||||
},
|
||||
{
|
||||
name: "doctor-failure-output",
|
||||
source: [
|
||||
scriptWithoutEntryPoint,
|
||||
"",
|
||||
"function Invoke-OpenClawCommand { throw 'doctor failed' }",
|
||||
"$output = @(Run-Doctor *>&1 | ForEach-Object { $_.ToString() })",
|
||||
'$text = $output -join "`n"',
|
||||
"if ($text -match 'Migration complete') { throw 'doctor failure reported success' }",
|
||||
"if ($text -notmatch 'Migration failed') { throw \"missing error: $text\" }",
|
||||
"if ($output[-1] -ne $false) { throw 'doctor failure did not propagate' }",
|
||||
"",
|
||||
].join("\n"),
|
||||
},
|
||||
{
|
||||
name: "npm-lifecycle-policy",
|
||||
source: [
|
||||
|
|
@ -894,31 +863,6 @@ try {
|
|||
"",
|
||||
].join("\n"),
|
||||
},
|
||||
{
|
||||
name: "package-manager-node-validation-failure",
|
||||
source: [
|
||||
scriptWithoutEntryPoint,
|
||||
"",
|
||||
"function Get-Command {",
|
||||
" [CmdletBinding()]",
|
||||
" param([string]$Name)",
|
||||
" if ($Name -eq 'choco') { return $true }",
|
||||
" return $null",
|
||||
"}",
|
||||
"filter Out-Host { }",
|
||||
"function choco {",
|
||||
" $global:LASTEXITCODE = 0",
|
||||
" Write-Output 'Chocolatey output'",
|
||||
"}",
|
||||
"$script:portableCalled = $false",
|
||||
"function Install-PortableNode { $script:portableCalled = $true }",
|
||||
"function Check-Node { return $script:portableCalled }",
|
||||
"$result = @(Install-Node)",
|
||||
'if ($result.Count -ne 1 -or $result[0] -ne $true) { throw "Install-Node returned $result" }',
|
||||
"if (-not $script:portableCalled) { throw 'Portable Node fallback was not attempted' }",
|
||||
"",
|
||||
].join("\n"),
|
||||
},
|
||||
{
|
||||
name: "package-manager-node-command-failures",
|
||||
source: [
|
||||
|
|
@ -1798,7 +1742,6 @@ try {
|
|||
expectBatchedPowerShellCase("winget-node-delayed-path");
|
||||
expectBatchedPowerShellCase("chocolatey-node-upgrade");
|
||||
expectBatchedPowerShellCase("scoop-node-update");
|
||||
expectBatchedPowerShellCase("package-manager-node-validation-failure");
|
||||
});
|
||||
|
||||
runIfPowerShell("recovers from package-manager failures and preserves installer refusal", () => {
|
||||
|
|
@ -2090,19 +2033,12 @@ describe("install.ps1 stale Winget repair", () => {
|
|||
repair: true,
|
||||
success: true,
|
||||
},
|
||||
{
|
||||
name: "accepts a normal successful install without repair",
|
||||
installExit: 0,
|
||||
afterInstall: "healthy",
|
||||
success: true,
|
||||
},
|
||||
{
|
||||
name: "accepts a healthy no-upgrade result without repair",
|
||||
afterInstall: "healthy",
|
||||
success: true,
|
||||
},
|
||||
{ name: "does not repair generic Winget failure", installExit: 1 },
|
||||
{ name: "does not repair another HRESULT", installExit: -1978335188 },
|
||||
{ name: "does not repair successful install with missing Node", installExit: 0 },
|
||||
{
|
||||
name: "recovers unsupported repair through Chocolatey",
|
||||
|
|
@ -2125,13 +2061,6 @@ describe("install.ps1 stale Winget repair", () => {
|
|||
fallback: "portable",
|
||||
success: true,
|
||||
},
|
||||
{
|
||||
name: "rejects unusable Chocolatey fallback after failed repair",
|
||||
repairExit: 1,
|
||||
repair: true,
|
||||
fallback: "choco",
|
||||
afterFallback: "old-sqlite",
|
||||
},
|
||||
{
|
||||
name: "rejects unusable portable fallback after failed repair",
|
||||
repairExit: 1,
|
||||
|
|
@ -2139,24 +2068,6 @@ describe("install.ps1 stale Winget repair", () => {
|
|||
fallback: "portable",
|
||||
afterFallback: "text",
|
||||
},
|
||||
{
|
||||
name: "recovers a generic Winget failure through portable Node",
|
||||
installExit: 1,
|
||||
fallback: "portable",
|
||||
success: true,
|
||||
},
|
||||
{
|
||||
name: "recovers a thrown Winget invocation through portable Node",
|
||||
installThrows: true,
|
||||
fallback: "portable",
|
||||
success: true,
|
||||
},
|
||||
{
|
||||
name: "recovers a generic Winget failure through the next package manager",
|
||||
installExit: 1,
|
||||
fallback: "choco",
|
||||
success: true,
|
||||
},
|
||||
{
|
||||
name: "rejects failed repair even if Node becomes healthy",
|
||||
repairExit: 1,
|
||||
|
|
@ -2164,8 +2075,6 @@ describe("install.ps1 stale Winget repair", () => {
|
|||
repair: true,
|
||||
},
|
||||
{ name: "rejects repair that leaves Node missing", repair: true },
|
||||
{ name: "rejects old Node after repair", afterRepair: "old-node", repair: true },
|
||||
{ name: "rejects old SQLite after repair", afterRepair: "old-sqlite", repair: true },
|
||||
...["text", "blob", "json", "probe-error"].map((capability) => ({
|
||||
name: `rejects broken SQLite ${capability} after repair`,
|
||||
afterRepair: capability,
|
||||
|
|
@ -2186,7 +2095,6 @@ describe("install.ps1 stale Winget repair", () => {
|
|||
afterRepair: "missing",
|
||||
repair: false,
|
||||
success: false,
|
||||
installThrows: false,
|
||||
fallback: "none",
|
||||
afterFallback: "healthy",
|
||||
...testCase,
|
||||
|
|
@ -2237,7 +2145,6 @@ function Get-Command {
|
|||
function Invoke-FixtureNode {
|
||||
$global:LASTEXITCODE = 0
|
||||
if ($args[0] -eq '-v') {
|
||||
if ($global:State -eq 'old-node') { return 'v22.15.0' }
|
||||
return 'v26.1.0'
|
||||
}
|
||||
$probe = @($input) -join [Environment]::NewLine
|
||||
|
|
@ -2253,7 +2160,6 @@ function winget {
|
|||
$global:Events.Add($args[0])
|
||||
$global:WingetCalls.Add(@($args))
|
||||
if ($args[0] -eq 'install') {
|
||||
if ($case.installThrows) { throw 'fixture Winget invocation failed' }
|
||||
$global:LASTEXITCODE = $case.installExit
|
||||
$global:PendingState = $case.afterInstall
|
||||
} elseif ($args[0] -eq 'repair') {
|
||||
|
|
@ -2347,8 +2253,7 @@ Write-Output ('RESULT:' + (@{ direct = $direct; main = $main; healthy = $healthy
|
|||
expect(run.calls).toEqual(options.repair ? [installArgs, repairArgs] : [installArgs]);
|
||||
const repaired =
|
||||
options.repair && options.repairExit === 0 && options.afterRepair === "healthy";
|
||||
const fallbackExpected =
|
||||
!repaired && (options.installThrows || options.afterInstall !== "healthy");
|
||||
const fallbackExpected = !repaired && options.afterInstall !== "healthy";
|
||||
const fallbacks: string[] = [];
|
||||
if (fallbackExpected) {
|
||||
if (options.fallback === "choco") {
|
||||
|
|
|
|||
|
|
@ -71,40 +71,21 @@ describe.each([
|
|||
expect(stdout).toHaveBeenCalledWith(expect.stringContaining("Usage:"));
|
||||
expect(scan).not.toHaveBeenCalled();
|
||||
});
|
||||
});
|
||||
|
||||
describe("shared inventory argument validation", () => {
|
||||
it.each([
|
||||
{ argv: ["--limit"], error: "--limit expects a non-negative integer" },
|
||||
...[
|
||||
"",
|
||||
" ",
|
||||
" 1",
|
||||
"1 ",
|
||||
"+1",
|
||||
"-1",
|
||||
"1.0",
|
||||
"1e3",
|
||||
"0x10",
|
||||
"١",
|
||||
"1",
|
||||
"9007199254740992",
|
||||
"9".repeat(400),
|
||||
"--",
|
||||
"-h",
|
||||
].map((value) => ({
|
||||
...["-1", "9007199254740992", "--"].map((value) => ({
|
||||
argv: ["--limit", value],
|
||||
error: "--limit expects a non-negative integer",
|
||||
})),
|
||||
{ argv: ["--repo-root"], error: "--repo-root expects a path" },
|
||||
...["", "-", "-h", "--"].map((value) => ({
|
||||
argv: ["--repo-root", value],
|
||||
error: "--repo-root expects a path",
|
||||
{ argv: ["--repo-root", "-h"], error: "--repo-root expects a path" },
|
||||
...["--limit=1", "--repo-root=repo", "--json=true"].map((arg) => ({
|
||||
argv: [arg],
|
||||
error: `Unknown argument: ${arg}`,
|
||||
})),
|
||||
...["--limit=1", "--repo-root=repo", "--json=true", "--unknown", "positional", ""].map(
|
||||
(arg) => ({
|
||||
argv: [arg],
|
||||
error: `Unknown argument: ${arg}`,
|
||||
}),
|
||||
),
|
||||
{ argv: ["--limit", "1", "--limit", "bad"], error: "--limit expects a non-negative integer" },
|
||||
{ argv: ["--limit", "bad", "--limit", "1"], error: "--limit expects a non-negative integer" },
|
||||
{ argv: ["--repo-root", "valid", "--repo-root", ""], error: "--repo-root expects a path" },
|
||||
|
|
@ -115,7 +96,7 @@ describe.each([
|
|||
const scan = vi.spyOn(fileUtils, "listRepoFilesSync");
|
||||
const stdout = vi.spyOn(process.stdout, "write").mockReturnValue(true);
|
||||
for (const args of [argv, ["--help", ...argv], [...argv, "--help"]]) {
|
||||
expect(() => main(args)).toThrow(new Error(error));
|
||||
expect(() => runEnvReport(args)).toThrow(new Error(error));
|
||||
}
|
||||
expect(stdout).not.toHaveBeenCalled();
|
||||
expect(scan).not.toHaveBeenCalled();
|
||||
|
|
|
|||
|
|
@ -108,7 +108,6 @@ describe("iOS release test identity", () => {
|
|||
["skipped", { result: "Skipped" }],
|
||||
["failed", { result: "Failed" }],
|
||||
["failed child", { children: [{ nodeType: "Test Case Run", result: "Failed" }] }],
|
||||
["wrong class", { nodeIdentifier: "OtherTests/testLiveGatewayPairChatAndRelaunch()" }],
|
||||
[
|
||||
"retry to green",
|
||||
{
|
||||
|
|
@ -204,7 +203,6 @@ describe("sampled simulator-tree footprint", () => {
|
|||
it.each([
|
||||
{},
|
||||
{ ...sample, bytes: 0 },
|
||||
{ ...sample, bytes: -1 },
|
||||
{ ...sample, bytes: "1024" },
|
||||
{ ...sample, processes: 0 },
|
||||
{ ...sample, cpu: Number.NaN },
|
||||
|
|
@ -234,7 +232,7 @@ describe("sampled simulator-tree footprint", () => {
|
|||
|
||||
function fixture(
|
||||
options: {
|
||||
fail?: "prepare" | "test" | "reader" | "cleanup";
|
||||
fail?: "test" | "cleanup";
|
||||
measure?: boolean;
|
||||
invalidMeasurement?: boolean;
|
||||
cancel?: boolean;
|
||||
|
|
@ -262,9 +260,6 @@ function fixture(
|
|||
prepare: async () => {
|
||||
trace.push(`prepare:${index}`);
|
||||
time += 100;
|
||||
if (index === 1 && options.fail === "prepare") {
|
||||
throw new Error("private preparation diagnostics");
|
||||
}
|
||||
},
|
||||
test: async (test: TestIdentity) => {
|
||||
trace.push(`test:${index}:${test}`);
|
||||
|
|
@ -278,11 +273,7 @@ function fixture(
|
|||
if (options.cancel) {
|
||||
abort.abort();
|
||||
}
|
||||
if (
|
||||
index === 1 &&
|
||||
(options.fail === "test" ||
|
||||
(options.fail === "reader" && test === IOS_RELEASE_TESTS[1]))
|
||||
) {
|
||||
if (index === 1 && options.fail === "test") {
|
||||
throw Object.assign(new Error("private timeout diagnostics"), { code: "ETIMEDOUT" });
|
||||
}
|
||||
return result(test);
|
||||
|
|
@ -361,45 +352,11 @@ describe("fresh trial ownership", () => {
|
|||
}
|
||||
expect(JSON.stringify(report)).not.toContain("private");
|
||||
});
|
||||
it.each(["test", "reader"] as const)(
|
||||
"fails the arm after its %s failure without repeating either test",
|
||||
async (fail) => {
|
||||
const { deps, trace } = fixture({ fail });
|
||||
const report = await runTrials("stock", deps);
|
||||
expect(report.trials).toHaveLength(1);
|
||||
expect(report.trials[0]).toMatchObject({
|
||||
status: "failed",
|
||||
errors: ["test-timeout"],
|
||||
tests:
|
||||
fail === "test"
|
||||
? [{ test: IOS_RELEASE_TESTS[0], status: "failed" }]
|
||||
: [
|
||||
{ test: IOS_RELEASE_TESTS[0], status: "passed" },
|
||||
{ test: IOS_RELEASE_TESTS[1], status: "failed" },
|
||||
],
|
||||
});
|
||||
expect(trace.filter((entry) => entry.startsWith("test:"))).toEqual(
|
||||
(fail === "test" ? [IOS_RELEASE_TESTS[0]] : IOS_RELEASE_TESTS).map(
|
||||
(test) => `test:1:${test}`,
|
||||
),
|
||||
);
|
||||
expect(trace.at(-1)).toBe("cleanup:1");
|
||||
},
|
||||
);
|
||||
it("refuses comparison without a meter", async () => {
|
||||
const { deps } = fixture();
|
||||
await expect(runTrials("compare", deps)).rejects.toThrow("comparison-meter-required");
|
||||
expect(deps.create).not.toHaveBeenCalled();
|
||||
});
|
||||
it("does not retry failed preparation or start its test/meter", async () => {
|
||||
const { deps, trace } = fixture({ fail: "prepare" });
|
||||
const report = await runTrials("stock", deps);
|
||||
expect(report.trials[0]?.errors).toEqual(["preparation-failed"]);
|
||||
expect(trace.filter((entry) => entry === "prepare:1")).toHaveLength(1);
|
||||
expect(trace.some((entry) => entry.startsWith("test:"))).toBe(false);
|
||||
expect(report.trials[0]?.tests).toEqual([]);
|
||||
expect(trace).toContain("cleanup:1");
|
||||
});
|
||||
it("joins the collector and fails incomplete measurement without discarding the trial", async () => {
|
||||
const { deps, trace } = fixture({ measure: true, invalidMeasurement: true });
|
||||
const report = await runTrials("stock", deps);
|
||||
|
|
@ -521,9 +478,7 @@ describe("release qualification workflow authority", () => {
|
|||
});
|
||||
it.each([
|
||||
["manual current revision", {}, true],
|
||||
["CI current revision", { caller: "ci" }, true],
|
||||
["manual arbitrary target", { target: "b".repeat(40) }, false],
|
||||
["CI arbitrary target", { caller: "ci", target: "b".repeat(40) }, false],
|
||||
["invalid SHA", { target: "main" }, false],
|
||||
["invalid mode", { mode: "unknown" }, false],
|
||||
])("checks %s before checkout", (_name, options, admitted) => {
|
||||
|
|
@ -533,7 +488,6 @@ describe("release qualification workflow authority", () => {
|
|||
const target = "target" in options ? options.target : sha;
|
||||
const repository = "openclaw/openclaw";
|
||||
const ref = "refs/heads/main";
|
||||
const caller = "caller" in options ? options.caller : "ios-release-e2e";
|
||||
const first = workflow.jobs.qualify.steps[0];
|
||||
expect(first.id).toBe("start");
|
||||
const execution = spawnSync("/bin/bash", ["-c", first.run], {
|
||||
|
|
@ -546,7 +500,7 @@ describe("release qualification workflow authority", () => {
|
|||
GITHUB_SHA: sha,
|
||||
GITHUB_REPOSITORY: repository,
|
||||
GITHUB_REF: ref,
|
||||
GITHUB_WORKFLOW_REF: `${repository}/.github/workflows/${caller}.yml@${ref}`,
|
||||
GITHUB_WORKFLOW_REF: `${repository}/.github/workflows/ios-release-e2e.yml@${ref}`,
|
||||
GITHUB_EVENT_NAME: "workflow_dispatch",
|
||||
TARGET_SHA: target,
|
||||
E2E_MODE: "mode" in options ? options.mode : "stock",
|
||||
|
|
@ -637,7 +591,6 @@ describe("release qualification workflow authority", () => {
|
|||
["full", "a".repeat(40), true],
|
||||
["full", "b".repeat(40), false],
|
||||
["main", "a".repeat(40), false],
|
||||
["main", "b".repeat(40), false],
|
||||
])(
|
||||
"selects %s-tier target %s for required native qualification: %s",
|
||||
(tier, target, selected) => {
|
||||
|
|
@ -671,15 +624,12 @@ describe("release qualification workflow authority", () => {
|
|||
|
||||
describe("native command adapter", () => {
|
||||
it.each([
|
||||
"success",
|
||||
"dirty-tracked",
|
||||
"dirty-untracked",
|
||||
"source-late-dirty",
|
||||
"source-late-head-change",
|
||||
"different-xcode",
|
||||
"different-xcode-build",
|
||||
"invalid-xcode-output",
|
||||
"different-runtime",
|
||||
"newest-compatible-runtime",
|
||||
"unavailable-runtime",
|
||||
"unsupported-runtime-device",
|
||||
|
|
@ -709,8 +659,6 @@ describe("native command adapter", () => {
|
|||
"reader-failure",
|
||||
"fixture-exit",
|
||||
"gateway-exit",
|
||||
"missing-first",
|
||||
"missing-second",
|
||||
"missing-relaunch",
|
||||
"provider-duplicate",
|
||||
"provider-out-of-order",
|
||||
|
|
@ -718,7 +666,6 @@ describe("native command adapter", () => {
|
|||
"test-timeout-output",
|
||||
"reply-failure-evidence",
|
||||
"reply-failure-history-error",
|
||||
"reply-failure-submission",
|
||||
"reply-failure-source-only",
|
||||
"reply-failure-app-log-error",
|
||||
])("owns admission, build, test and cleanup for %s", async (scenario) => {
|
||||
|
|
@ -951,11 +898,9 @@ describe("native command adapter", () => {
|
|||
stdout.write(
|
||||
scenario === "different-xcode"
|
||||
? "Xcode 26.6\nBuild version 17F113\n"
|
||||
: scenario === "different-xcode-build"
|
||||
? "Xcode 27.0\nBuild version 27A000\n"
|
||||
: scenario === "invalid-xcode-output"
|
||||
? "unrecognized toolchain\n"
|
||||
: "Xcode 27.0\nBuild version 27A266a\n",
|
||||
: scenario === "invalid-xcode-output"
|
||||
? "unrecognized toolchain\n"
|
||||
: "Xcode 27.0\nBuild version 27A266a\n",
|
||||
);
|
||||
} else if (args.includes("--print-path")) {
|
||||
stdout.write(`${developerDir}\n`);
|
||||
|
|
@ -964,13 +909,11 @@ describe("native command adapter", () => {
|
|||
} else if (args.includes("runtimes")) {
|
||||
const runtime = {
|
||||
isAvailable: scenario !== "unavailable-runtime",
|
||||
version: scenario === "different-runtime" ? "27.0" : "26.5",
|
||||
version: "26.5",
|
||||
identifier:
|
||||
scenario === "different-runtime"
|
||||
? "com.apple.CoreSimulator.SimRuntime.iOS-27-0"
|
||||
: scenario === "non-ios-runtime"
|
||||
? "com.apple.CoreSimulator.SimRuntime.watchOS-26-5"
|
||||
: "com.apple.CoreSimulator.SimRuntime.iOS-26-5",
|
||||
scenario === "non-ios-runtime"
|
||||
? "com.apple.CoreSimulator.SimRuntime.watchOS-26-5"
|
||||
: "com.apple.CoreSimulator.SimRuntime.iOS-26-5",
|
||||
supportedArchitectures:
|
||||
scenario === "unsupported-runtime-architecture" ? ["x86_64"] : ["arm64"],
|
||||
supportedDeviceTypes: [
|
||||
|
|
@ -1014,11 +957,9 @@ describe("native command adapter", () => {
|
|||
expect(lifecycle).toContain("setup-status-ready");
|
||||
lifecycle.push("simulator-create");
|
||||
expect(args.at(-1)).toBe(
|
||||
scenario === "different-runtime"
|
||||
? "com.apple.CoreSimulator.SimRuntime.iOS-27-0"
|
||||
: scenario === "newest-compatible-runtime"
|
||||
? "com.apple.CoreSimulator.SimRuntime.iOS-26-10"
|
||||
: "com.apple.CoreSimulator.SimRuntime.iOS-26-5",
|
||||
scenario === "newest-compatible-runtime"
|
||||
? "com.apple.CoreSimulator.SimRuntime.iOS-26-10"
|
||||
: "com.apple.CoreSimulator.SimRuntime.iOS-26-5",
|
||||
);
|
||||
if (scenario === "gateway-exit-during-create") {
|
||||
gatewayChild.exitCode = 17;
|
||||
|
|
@ -1181,7 +1122,7 @@ describe("native command adapter", () => {
|
|||
const failureMessage =
|
||||
scenario === "reply-failure-source-only"
|
||||
? ""
|
||||
: `IOS_RELEASE_CHAT_FAILURE relaunch ${scenario === "reply-failure-submission" ? "submission" : "reply"} draft=false keyboard=true reply=false writing=false jump=true foreground=true input=true transcript=true send=false`;
|
||||
: "IOS_RELEASE_CHAT_FAILURE relaunch reply draft=false keyboard=true reply=false writing=false jump=true foreground=true input=true transcript=true send=false";
|
||||
stdout.write(
|
||||
"Test Case '-[OpenClawUITests.OpenClawSnapshotUITests testLiveGatewayPairChatAndRelaunch]' started.\n" +
|
||||
`/private/checkout/OpenClawSnapshotUITests.swift:1913: error: private ${failureMessage}\n` +
|
||||
|
|
@ -1338,24 +1279,12 @@ describe("native command adapter", () => {
|
|||
const native = await admission;
|
||||
expect(proof).toMatchObject({
|
||||
xcode: scenario === "different-xcode" ? "26.6" : "27.0",
|
||||
xcodeBuild:
|
||||
scenario === "different-xcode"
|
||||
? "17F113"
|
||||
: scenario === "different-xcode-build"
|
||||
? "27A000"
|
||||
: "27A266a",
|
||||
runtime:
|
||||
scenario === "different-runtime"
|
||||
? "27.0"
|
||||
: scenario === "newest-compatible-runtime"
|
||||
? "26.10"
|
||||
: "26.5",
|
||||
xcodeBuild: scenario === "different-xcode" ? "17F113" : "27A266a",
|
||||
runtime: scenario === "newest-compatible-runtime" ? "26.10" : "26.5",
|
||||
runtimeIdentifier:
|
||||
scenario === "different-runtime"
|
||||
? "com.apple.CoreSimulator.SimRuntime.iOS-27-0"
|
||||
: scenario === "newest-compatible-runtime"
|
||||
? "com.apple.CoreSimulator.SimRuntime.iOS-26-10"
|
||||
: "com.apple.CoreSimulator.SimRuntime.iOS-26-5",
|
||||
scenario === "newest-compatible-runtime"
|
||||
? "com.apple.CoreSimulator.SimRuntime.iOS-26-10"
|
||||
: "com.apple.CoreSimulator.SimRuntime.iOS-26-5",
|
||||
});
|
||||
try {
|
||||
if (scenario === "native-build-only") {
|
||||
|
|
@ -1426,7 +1355,7 @@ describe("native command adapter", () => {
|
|||
? []
|
||||
: [
|
||||
"chat-stage:relaunch",
|
||||
`chat-checkpoint:${scenario === "reply-failure-submission" ? "submission" : "reply"}`,
|
||||
"chat-checkpoint:reply",
|
||||
"chat-draft-retained:false",
|
||||
"chat-keyboard:true",
|
||||
"chat-reply-present:false",
|
||||
|
|
|
|||
|
|
@ -81,7 +81,7 @@ describe("Kitchen Sink resource phase receipts", () => {
|
|||
expect(phase.processCpuMsPerCompletedOperation).toBeCloseTo(1.21 / 20);
|
||||
});
|
||||
|
||||
it.each([0, 1, 3])(
|
||||
it.each([0, 3])(
|
||||
"retains partial split counts when original operation %i fails",
|
||||
async (failedIndex) => {
|
||||
let step = 0;
|
||||
|
|
@ -183,49 +183,46 @@ describe("Kitchen Sink resource phase receipts", () => {
|
|||
},
|
||||
);
|
||||
|
||||
it.each(invalidSamples)(
|
||||
"preserves the first receipt and all completions on a %s final sample",
|
||||
async (_name, invalidSample) => {
|
||||
const before = snapshot(1);
|
||||
const midpoint = snapshot(2, 90);
|
||||
const sample = vi
|
||||
.fn()
|
||||
.mockResolvedValueOnce(before)
|
||||
.mockResolvedValueOnce(midpoint)
|
||||
.mockImplementationOnce(invalidSample);
|
||||
const run = vi.fn(async () => {});
|
||||
const phase = await measureResourceOperations({
|
||||
name: "plugin-tool",
|
||||
count: 20,
|
||||
splitFirst: true,
|
||||
sample,
|
||||
run,
|
||||
});
|
||||
expect(run).toHaveBeenCalledTimes(20);
|
||||
expect(sample).toHaveBeenCalledTimes(3);
|
||||
expect(phase).toMatchObject({
|
||||
status: "failed",
|
||||
operations: { attempted: 20, completed: 20, failed: 0 },
|
||||
after: null,
|
||||
cpu: null,
|
||||
});
|
||||
expect(phase.before).toBe(before);
|
||||
expect(phase.breakdown![0]).toEqual(
|
||||
summarizeResourcePhase("plugin-tool-first", before, midpoint, {
|
||||
attempted: 1,
|
||||
completed: 1,
|
||||
failed: 0,
|
||||
}),
|
||||
);
|
||||
expect(phase.breakdown![1]).toMatchObject({
|
||||
name: "plugin-tool-warm",
|
||||
status: "failed",
|
||||
operations: { attempted: 19, completed: 19, failed: 0 },
|
||||
after: null,
|
||||
cpu: null,
|
||||
});
|
||||
},
|
||||
);
|
||||
it("preserves the first receipt and all completions on a missing final sample", async () => {
|
||||
const before = snapshot(1);
|
||||
const midpoint = snapshot(2, 90);
|
||||
const sample = vi
|
||||
.fn()
|
||||
.mockResolvedValueOnce(before)
|
||||
.mockResolvedValueOnce(midpoint)
|
||||
.mockRejectedValueOnce(new Error("sample unavailable"));
|
||||
const run = vi.fn(async () => {});
|
||||
const phase = await measureResourceOperations({
|
||||
name: "plugin-tool",
|
||||
count: 20,
|
||||
splitFirst: true,
|
||||
sample,
|
||||
run,
|
||||
});
|
||||
expect(run).toHaveBeenCalledTimes(20);
|
||||
expect(sample).toHaveBeenCalledTimes(3);
|
||||
expect(phase).toMatchObject({
|
||||
status: "failed",
|
||||
operations: { attempted: 20, completed: 20, failed: 0 },
|
||||
after: null,
|
||||
cpu: null,
|
||||
});
|
||||
expect(phase.before).toBe(before);
|
||||
expect(phase.breakdown![0]).toEqual(
|
||||
summarizeResourcePhase("plugin-tool-first", before, midpoint, {
|
||||
attempted: 1,
|
||||
completed: 1,
|
||||
failed: 0,
|
||||
}),
|
||||
);
|
||||
expect(phase.breakdown![1]).toMatchObject({
|
||||
name: "plugin-tool-warm",
|
||||
status: "failed",
|
||||
operations: { attempted: 19, completed: 19, failed: 0 },
|
||||
after: null,
|
||||
cpu: null,
|
||||
});
|
||||
});
|
||||
|
||||
it("counts only asserted responses and stops on the first failure without retrying", async () => {
|
||||
const sample = vi
|
||||
|
|
@ -327,16 +324,6 @@ describe("Kitchen Sink resource phase receipts", () => {
|
|||
).toEqual([]);
|
||||
});
|
||||
|
||||
it("rejects mixed process identities and unavailable memory rather than reporting zero", () => {
|
||||
const ops = { attempted: 0, completed: 0, failed: 0 };
|
||||
expect(() =>
|
||||
summarizeResourcePhase("idle", snapshot(1), { ...snapshot(2), pid: 999 }, ops),
|
||||
).toThrow("one process");
|
||||
const invalid = snapshot(2);
|
||||
invalid.memory.rss = Number.NaN;
|
||||
expect(() => summarizeResourcePhase("idle", snapshot(1), invalid, ops)).toThrow("invalid rss");
|
||||
});
|
||||
|
||||
it("retains signed resource changes and rejects missing observations", () => {
|
||||
const before = { ...snapshot(1), activeResources: { Timeout: 2, TCPServerWrap: 1 } };
|
||||
const after = { ...snapshot(2), activeResources: { Timeout: 1, Immediate: 1 } };
|
||||
|
|
|
|||
|
|
@ -1028,27 +1028,20 @@ it("accepts a successful completed publisher for finalization without a Linux ch
|
|||
|
||||
it("rejects retired Tideclaw alpha finalization before writes", () => {
|
||||
const f = fixture("tideclaw/alpha/2026-09-13-0400Z");
|
||||
const tag = "v2026.9.4-alpha.1";
|
||||
f.addDraft(tag, true);
|
||||
const result = f.run("finalize-core", tag, "false");
|
||||
const alphaTag = "v2026.9.4-alpha.1";
|
||||
f.addDraft(alphaTag, true);
|
||||
const result = f.run("finalize-core", alphaTag, "false");
|
||||
expect(result.status, result.stderr).toBe(1);
|
||||
expect(result.stderr).toContain("Alpha releases are retired;");
|
||||
expect(f.mutations()).toEqual([]);
|
||||
});
|
||||
|
||||
it.each([
|
||||
{ ref: "unreviewed/branch", tag: "v2026.9.4-alpha.1", latest: "false" },
|
||||
{ ref: "tideclaw/alpha/2026-09-13-0400Z", tag: nextTag, latest: "false" },
|
||||
{ ref: "tideclaw/alpha/2026-09-13-0400Z", tag: "v2026.9.4-alpha.1", latest: "true" },
|
||||
])(
|
||||
"rejects unapproved branch finalization $ref/$tag/$latest",
|
||||
({ ref, tag: selectedTag, latest }) => {
|
||||
const f = fixture(ref);
|
||||
f.addDraft(selectedTag, selectedTag.includes("-alpha."));
|
||||
expect(f.run("finalize-core", selectedTag, latest).status).toBe(1);
|
||||
expect(f.mutations()).toEqual([]);
|
||||
},
|
||||
);
|
||||
it("rejects a retired alpha workflow even when finalizing a stable tag", () => {
|
||||
const f = fixture("tideclaw/alpha/2026-09-13-0400Z");
|
||||
f.addDraft(nextTag);
|
||||
failed(f.run("finalize-core", nextTag, "false"), "Alpha releases are retired;");
|
||||
expect(f.mutations()).toEqual([]);
|
||||
});
|
||||
|
||||
it("publishes opt-in desktop metadata only after successful Linux and legacy readback", () => {
|
||||
const f = fixture(toolingRef, true);
|
||||
|
|
@ -1485,7 +1478,7 @@ it("refuses mirroring before canonical initialization without creating a release
|
|||
expect(f.mutations()).toEqual([]);
|
||||
});
|
||||
|
||||
it.each(["v2026.9.3-alpha.1", "v2026.9.3-beta.1", "v2026.6.33"])(
|
||||
it.each(["v2026.9.3-beta.1", "v2026.6.33"])(
|
||||
"rejects non-regular target %s before touching GitHub",
|
||||
(releaseTag) => {
|
||||
const f = fixture();
|
||||
|
|
@ -1556,7 +1549,6 @@ it.each([null, tag])(
|
|||
|
||||
it.each([
|
||||
{ releaseTag: nextTag, prerelease: false },
|
||||
{ releaseTag: "v2026.6.33", prerelease: false },
|
||||
{ releaseTag: "v2026.8.35", prerelease: false },
|
||||
{ releaseTag: "v2026.9.4-beta.1", prerelease: true },
|
||||
])("honors explicit non-latest finalization of $releaseTag", ({ releaseTag, prerelease }) => {
|
||||
|
|
|
|||
|
|
@ -46,7 +46,7 @@ type Remote = {
|
|||
requestReadRejected?: boolean;
|
||||
uploadBehavior?: "accepted-error" | "rejected-error" | "deleted-error" | "success-noop";
|
||||
dispatchRejected?: boolean;
|
||||
dispatchResponse?: "missing-id" | "null-id";
|
||||
missingDispatchId?: boolean;
|
||||
toolingStatus?: string;
|
||||
parentAttempt?: number;
|
||||
currentRun?: ActionRun;
|
||||
|
|
@ -175,8 +175,7 @@ if (args[0] === 'api') {
|
|||
(state.requestRuns ||= []).unshift({id: 456, head_sha: '${toolingSha}', head_branch: request.ref,
|
||||
event: 'workflow_dispatch', status: 'queued', conclusion: null,
|
||||
display_title: 'Linux App Release Request [' + request.inputs.tag + '] desktop=' + request.inputs['desktop-test-bundles']});
|
||||
result({...(state.dispatchResponse === 'missing-id' ? {} :
|
||||
{workflow_run_id: state.dispatchResponse === 'null-id' ? null : 456}),
|
||||
result({...(state.missingDispatchId ? {} : {workflow_run_id: 456}),
|
||||
html_url: 'https://github.com/${repository}/actions/runs/456'});
|
||||
} else if (endpoint?.startsWith('repos/${repository}/commits/')) {
|
||||
result('${sourceSha}');
|
||||
|
|
@ -380,7 +379,7 @@ process.stdout.write('${sourceSha}\\trefs/tags/v2026.9.4\\n');
|
|||
};
|
||||
}
|
||||
|
||||
it.each(["2026.9.3", "2026.9.4", "2026.9.5"])(
|
||||
it.each(["2026.9.3", "2026.9.5"])(
|
||||
"preserves an equal or newer valid target manifest byte-for-byte (%s)",
|
||||
(version) => {
|
||||
const remote = fixture();
|
||||
|
|
@ -418,7 +417,6 @@ it.each(["missing-manifest", "missing-release"])(
|
|||
|
||||
it.each([
|
||||
"cross-repository",
|
||||
"wrong-version-url",
|
||||
"empty-signature",
|
||||
"invalid-base64",
|
||||
"invalid-json",
|
||||
|
|
@ -436,9 +434,6 @@ it.each([
|
|||
if (kind === "cross-repository") {
|
||||
platform.url = platform.url.replace(repository, "foreign/repository");
|
||||
}
|
||||
if (kind === "wrong-version-url") {
|
||||
platform.url = platform.url.replaceAll("2026.9.3", "2026.9.2");
|
||||
}
|
||||
if (kind === "empty-signature") {
|
||||
platform.signature = "";
|
||||
}
|
||||
|
|
@ -565,7 +560,7 @@ it("preserves identity CLI behavior when no writer tuple is requested", () => {
|
|||
});
|
||||
|
||||
it.each([
|
||||
...writerCliFields.map(([name, value]) => ({ name, arguments: [name, value] })),
|
||||
{ name: "incomplete writer tuple", arguments: ["--writer-run-id", "200"] },
|
||||
{ name: "missing writer value", arguments: ["--writer-run-id"] },
|
||||
])("refuses partial identity CLI writer arguments: $name", (scenario) => {
|
||||
const remote = fixture();
|
||||
|
|
@ -811,10 +806,7 @@ it.each(["absent", "complete", "partial", "propagation"])(
|
|||
);
|
||||
|
||||
it("reports a refused Linux workflow dispatch without a success receipt or retry", () => {
|
||||
const remote = fixture({ dispatchRejected: true });
|
||||
remote.update((state) => {
|
||||
state.releases["v2026.9.4"] = release("2026.9.4", { assets: [], manifest: undefined });
|
||||
});
|
||||
const remote = pendingLinuxFixture({ dispatchRejected: true });
|
||||
const result = remote.dispatch();
|
||||
expect(result.status).toBe(1);
|
||||
expect(result.evidence.state).toBe("dispatch-unconfirmed");
|
||||
|
|
@ -847,7 +839,7 @@ function linuxRequest(overrides: Partial<ActionRun> = {}): ActionRun {
|
|||
};
|
||||
}
|
||||
|
||||
it.each(["requested", "waiting", "pending", "queued", "in_progress", "completed"])(
|
||||
it.each(["queued", "completed"])(
|
||||
"reuses a manual same-tag Linux request in %s state without another build request",
|
||||
(status) => {
|
||||
const remote = pendingLinuxFixture({
|
||||
|
|
@ -894,7 +886,6 @@ it("finds a desktop-inclusive Linux request beyond the first history page", () =
|
|||
|
||||
it.each([
|
||||
{ reason: "failed", overrides: { conclusion: "failure" } },
|
||||
{ reason: "cancelled", overrides: { conclusion: "cancelled" } },
|
||||
{ reason: "another branch", overrides: { head_branch: "feature" } },
|
||||
{ reason: "another event", overrides: { event: "push" } },
|
||||
{
|
||||
|
|
@ -919,10 +910,7 @@ it("refuses another Linux request when request history cannot be read", () => {
|
|||
});
|
||||
|
||||
it("refuses a moved tag even when the dispatch caller suppresses shell errexit", () => {
|
||||
const remote = fixture();
|
||||
remote.update((state) => {
|
||||
state.releases["v2026.9.4"] = release("2026.9.4", { assets: [], manifest: undefined });
|
||||
});
|
||||
const remote = pendingLinuxFixture();
|
||||
const result = remote.dispatch("c".repeat(40));
|
||||
expect(result.status).toBe(1);
|
||||
expect(result.stderr).toContain("Release tag v2026.9.4 moved");
|
||||
|
|
@ -933,20 +921,14 @@ it("refuses a moved tag even when the dispatch caller suppresses shell errexit",
|
|||
).toEqual([]);
|
||||
});
|
||||
|
||||
it.each(["missing-id", "null-id"] as const)(
|
||||
"does not confirm or repeat an accepted workflow dispatch with %s",
|
||||
(dispatchResponse) => {
|
||||
const remote = fixture({ dispatchResponse });
|
||||
remote.update((state) => {
|
||||
state.releases["v2026.9.4"] = release("2026.9.4", { assets: [], manifest: undefined });
|
||||
});
|
||||
const result = remote.dispatch();
|
||||
expect(result.status).toBe(1);
|
||||
expect(result.evidence.state).toBe("dispatch-unconfirmed");
|
||||
expect(remote.read().requests).toHaveLength(1);
|
||||
expect(remote.read().calls.filter((args) => args[0] === "run")).toEqual([]);
|
||||
expect(
|
||||
remote.read().calls.filter((args) => args.some((arg) => arg.endsWith("/dispatches"))),
|
||||
).toHaveLength(1);
|
||||
},
|
||||
);
|
||||
it("does not confirm or repeat an accepted workflow dispatch without a run ID", () => {
|
||||
const remote = pendingLinuxFixture({ missingDispatchId: true });
|
||||
const result = remote.dispatch();
|
||||
expect(result.status).toBe(1);
|
||||
expect(result.evidence.state).toBe("dispatch-unconfirmed");
|
||||
expect(remote.read().requests).toHaveLength(1);
|
||||
expect(remote.read().calls.filter((args) => args[0] === "run")).toEqual([]);
|
||||
expect(
|
||||
remote.read().calls.filter((args) => args.some((arg) => arg.endsWith("/dispatches"))),
|
||||
).toHaveLength(1);
|
||||
});
|
||||
|
|
|
|||
|
|
@ -208,7 +208,6 @@ it.each([false, true])(
|
|||
it.each([
|
||||
["win32", true, true],
|
||||
["win32", false, true],
|
||||
["darwin", true, true],
|
||||
["darwin", false, true],
|
||||
["win32", true, false],
|
||||
] as const)(
|
||||
|
|
@ -297,12 +296,7 @@ it.each([
|
|||
if (terminates && closes) {
|
||||
expect(descendantOutput.destroyed).toBe(true);
|
||||
owner.assertReleased();
|
||||
// POSIX strict normal-exit policy still reports unexpected group survivors.
|
||||
if (platform === "win32") {
|
||||
expect(outcome).toBe(0);
|
||||
} else {
|
||||
expect(outcome).toMatchObject({ processTreeState: "terminated" });
|
||||
}
|
||||
expect(outcome).toBe(0);
|
||||
} else {
|
||||
expect(hasUnjoinedWork(outcome)).toBe(true);
|
||||
expect(outcome).toMatchObject({ code: "EPROCESSGROUP_CLEANUP_FAILED" });
|
||||
|
|
|
|||
|
|
@ -1,4 +1,4 @@
|
|||
import { execFileSync, spawnSync } from "node:child_process";
|
||||
import { spawnSync } from "node:child_process";
|
||||
import { randomUUID } from "node:crypto";
|
||||
import { mkdirSync, readdirSync, realpathSync, readFileSync, lstatSync } from "node:fs";
|
||||
import path from "node:path";
|
||||
|
|
@ -73,23 +73,6 @@ const tempDirs = useAutoCleanupTempDirTracker(afterEach);
|
|||
|
||||
afterEach(() => vi.restoreAllMocks());
|
||||
|
||||
it("loads the worker compiler with native Node before preparing artifacts", () => {
|
||||
const output = execFileSync(
|
||||
process.execPath,
|
||||
[
|
||||
"--input-type=module",
|
||||
"--eval",
|
||||
`
|
||||
await import("./scripts/lib/vitest-worker-compiler.mts");
|
||||
console.log("native worker compiler import verified");
|
||||
`,
|
||||
],
|
||||
{ encoding: "utf8", timeout: 30_000 },
|
||||
);
|
||||
|
||||
expect(output.trim()).toBe("native worker compiler import verified");
|
||||
});
|
||||
|
||||
it.each(
|
||||
(["managed", "package"] as const).filter(
|
||||
(kind) => kind === "managed" || process.platform !== "win32",
|
||||
|
|
@ -246,9 +229,6 @@ it.each(
|
|||
"createManagedHandoffLeaseStore",
|
||||
"resolveUpdateRestartNoticeMeta",
|
||||
"shouldPublishUpdateRestartNotice",
|
||||
"extractSqliteTableSchema",
|
||||
"readRestartSentinelRowSync",
|
||||
"writeRestartSentinelRowIfRevisionSync",
|
||||
]) {
|
||||
assert.equal(typeof runtime[name], "function", name);
|
||||
}
|
||||
|
|
@ -330,12 +310,7 @@ it.each(
|
|||
expect(stale.error).toBeUndefined();
|
||||
expect(stale.status).toBe(1);
|
||||
expect(stale.stderr).toContain("different operation");
|
||||
const after = snapshot();
|
||||
expect(after).toHaveLength(before.length);
|
||||
for (const [index, original] of before.entries()) {
|
||||
expect(after[index]!.ino).toBe(original.ino);
|
||||
expect(after[index]!.bytes.equals(original.bytes)).toBe(true);
|
||||
}
|
||||
expect(snapshot()).toEqual(before);
|
||||
}
|
||||
// The replacement's temporary command is deliberately one-phase, never
|
||||
// another locator for the next operation after its helper has moved.
|
||||
|
|
|
|||
|
|
@ -755,16 +755,6 @@ describe("mobile release CI tools", () => {
|
|||
expect(JSON.stringify(steps)).not.toContain("candidate/");
|
||||
|
||||
const tooling = steps[toolingIndex]?.run ?? "";
|
||||
expect(tooling).toContain('apt_source="/etc/apt/sources.list.d/ubuntu.sources"');
|
||||
expect(tooling).toContain(
|
||||
'apt_source_parts="$RUNNER_TEMP/openclaw-android-apt-sourceparts-disabled"',
|
||||
);
|
||||
expect(tooling).toContain('test -s "$apt_source"');
|
||||
expect(tooling).toContain('[[ -e "$apt_source_parts" || -L "$apt_source_parts" ]]');
|
||||
expect(tooling).toContain('/usr/bin/apt-get "${apt_options[@]}" update');
|
||||
expect(tooling).toMatch(
|
||||
/\/usr\/bin\/apt-get "\$\{apt_options\[@\]\}" install \\\n\s+-y --no-install-recommends imagemagick/u,
|
||||
);
|
||||
expect(tooling).toContain(
|
||||
'test "$(git -C "$trusted_root" rev-parse HEAD)" = "$GITHUB_WORKFLOW_SHA"',
|
||||
);
|
||||
|
|
@ -955,18 +945,10 @@ describe("mobile release CI tools", () => {
|
|||
expect(diagnostic).toContain(
|
||||
'emulator_args=(-avd "$AVD_NAME" -no-window -no-audio -no-boot-anim -verbose -show-kernel)',
|
||||
);
|
||||
expect(diagnostic).toContain("capture_accel_check() {");
|
||||
expect(diagnostic).toContain("accel_check_timeout_seconds=10");
|
||||
expect(diagnostic).toContain('emulator -accel-check >"$accel_raw" 2>&1 &');
|
||||
expect(diagnostic).toContain(
|
||||
'head -c 16384 "$accel_raw" >"$DIAGNOSTIC_DIR/emulator-accel-check.txt"',
|
||||
);
|
||||
expect(diagnostic).toContain(
|
||||
'printf \'exit_status=%s\\n\' "$accel_status" >>"$DIAGNOSTIC_DIR/emulator-accel-check.txt"',
|
||||
);
|
||||
expect(diagnostic).toContain(
|
||||
'printf \'timed_out=%s\\n\' "$accel_timed_out" >>"$DIAGNOSTIC_DIR/emulator-accel-check.txt"',
|
||||
);
|
||||
expect(diagnostic).toContain("sample_owned_qemu() {");
|
||||
expect(diagnostic).toContain(
|
||||
'printf \'\\n[%s] owned_emulator_pid=%s\\n\' "$(date -u +%Y-%m-%dT%H:%M:%SZ)" "$emulator_pid"',
|
||||
|
|
@ -1021,18 +1003,15 @@ describe("mobile release CI tools", () => {
|
|||
expect(timedOutAccel.output).toContain("exit_status=124");
|
||||
expect(timedOutAccel.output).toContain("timed_out=true");
|
||||
|
||||
expect(diagnostic).toContain("observe_after_readiness_timeout() {");
|
||||
expect(diagnostic).toContain("final_cold_boot_observation_seconds=900");
|
||||
expect(diagnostic).toContain("probe_timeout_seconds=5");
|
||||
expect(diagnostic).toContain("final_snapshot_lead_seconds=15");
|
||||
expect(diagnostic).toContain("snapshot_properties_max_bytes=65536");
|
||||
expect(diagnostic).toContain("snapshot_logcat_max_bytes=262144");
|
||||
expect(diagnostic).toContain("capture_cold_boot_snapshot() {");
|
||||
expect(diagnostic).toContain(
|
||||
"emulator_observation_deadline=$((emulator_launch_seconds + final_cold_boot_observation_seconds))",
|
||||
);
|
||||
expect(diagnostic).not.toContain("post_deadline_observation_seconds");
|
||||
expect(diagnostic).toContain("fail_after_readiness_timeout() {");
|
||||
const observationFunctionStart = diagnostic.indexOf("run_bounded_probe() {");
|
||||
const observationFunctionEnd = diagnostic.indexOf(
|
||||
"\n\nfail_after_readiness_timeout()",
|
||||
|
|
@ -1127,34 +1106,11 @@ describe("mobile release CI tools", () => {
|
|||
};
|
||||
};
|
||||
|
||||
const lateReady = await runPostDeadlineObservation(`#!/bin/bash
|
||||
set -euo pipefail
|
||||
if [[ "\${1:-}" == "devices" ]]; then
|
||||
printf 'List of devices attached\\nemulator-5554\\tdevice product:sdk model:sdk\\n'
|
||||
elif [[ "\${1:-}" == "-s" && "\${3:-}" == "shell" ]]; then
|
||||
printf '1\\n'
|
||||
elif [[ "\${1:-}" == "-s" && "\${3:-}" == "emu" ]]; then
|
||||
printf '%s\\nOK\\n' "\${AVD_NAME:?}"
|
||||
fi
|
||||
`);
|
||||
expect(lateReady.result.status).toBe(1);
|
||||
expect(lateReady.result.stderr).toContain("::error::latched readiness failure");
|
||||
expect(lateReady.observations).toContain("late_adb_online_at=");
|
||||
expect(lateReady.observations).toContain("late_boot_completed_at=");
|
||||
expect(lateReady.observations).toContain("observation_stop=late-boot-completed");
|
||||
expect(lateReady.snapshots).toEqual(["first-online"]);
|
||||
expect(
|
||||
fs.readFileSync(
|
||||
path.join(lateReady.snapshotsRoot, "first-online", "boot-properties.txt"),
|
||||
"utf8",
|
||||
),
|
||||
).toContain("probe_exit_status=0");
|
||||
|
||||
const lateReadyNearCeilingFunctions = observationFunctions.replace(
|
||||
"final_snapshot_lead_seconds=4",
|
||||
"final_snapshot_lead_seconds=60",
|
||||
);
|
||||
const lateReadyNearCeiling = await runPostDeadlineObservation(
|
||||
const lateReady = await runPostDeadlineObservation(
|
||||
`#!/bin/bash
|
||||
set -euo pipefail
|
||||
if [[ "\${1:-}" == "devices" ]]; then
|
||||
|
|
@ -1167,10 +1123,18 @@ fi
|
|||
`,
|
||||
{ functions: lateReadyNearCeilingFunctions },
|
||||
);
|
||||
expect(lateReadyNearCeiling.result.status).toBe(1);
|
||||
expect(lateReadyNearCeiling.observations).toContain("late_boot_completed_at=");
|
||||
expect(lateReadyNearCeiling.observations).toContain("observation_stop=late-boot-completed");
|
||||
expect(lateReadyNearCeiling.snapshots).toEqual(["first-online"]);
|
||||
expect(lateReady.result.status).toBe(1);
|
||||
expect(lateReady.result.stderr).toContain("::error::latched readiness failure");
|
||||
expect(lateReady.observations).toContain("late_adb_online_at=");
|
||||
expect(lateReady.observations).toContain("late_boot_completed_at=");
|
||||
expect(lateReady.observations).toContain("observation_stop=late-boot-completed");
|
||||
expect(lateReady.snapshots).toEqual(["first-online"]);
|
||||
expect(
|
||||
fs.readFileSync(
|
||||
path.join(lateReady.snapshotsRoot, "first-online", "boot-properties.txt"),
|
||||
"utf8",
|
||||
),
|
||||
).toContain("probe_exit_status=0");
|
||||
|
||||
const [failedBootProbeResult, boundedSnapshotsResult] = await Promise.allSettled([
|
||||
runPostDeadlineObservation(
|
||||
|
|
@ -1256,21 +1220,6 @@ fi
|
|||
expect(changedDevice.observations).toContain("observation_stop=unexpected-device-change");
|
||||
expect(changedDevice.snapshots).toEqual([]);
|
||||
|
||||
const capped = await runPostDeadlineObservation(
|
||||
`#!/bin/bash
|
||||
set -euo pipefail
|
||||
if [[ "\${1:-}" == "devices" ]]; then
|
||||
printf 'List of devices attached\\n\\n'
|
||||
fi
|
||||
`,
|
||||
{ deadlineSeconds: 2 },
|
||||
);
|
||||
expect(capped.result.status).toBe(1);
|
||||
expect(capped.elapsedSeconds).toBe(2);
|
||||
expect(capped.result.stderr).toContain("::error::latched readiness failure");
|
||||
expect(capped.observations).toContain("observation_cap_seconds=900");
|
||||
expect(capped.observations).toContain("observation_stop=observation-cap-reached");
|
||||
|
||||
const absoluteCap = await runPostDeadlineObservation(
|
||||
`#!/bin/bash
|
||||
set -euo pipefail
|
||||
|
|
@ -1282,6 +1231,8 @@ fi
|
|||
);
|
||||
expect(absoluteCap.result.status).toBe(1);
|
||||
expect(absoluteCap.elapsedSeconds).toBe(3);
|
||||
expect(absoluteCap.result.stderr).toContain("::error::latched readiness failure");
|
||||
expect(absoluteCap.observations).toContain("observation_cap_seconds=900");
|
||||
expect(absoluteCap.durationMs).toBeLessThan(5_000);
|
||||
expect(absoluteCap.observations).toContain("observation_stop=observation-cap-reached");
|
||||
|
||||
|
|
@ -1454,7 +1405,6 @@ fi
|
|||
expect(diagnostic).toContain("adb devices -l");
|
||||
expect(diagnostic).toContain('>>"$DIAGNOSTIC_DIR/adb-observations.log" 2>&1');
|
||||
expect(diagnostic).toContain('ps -p "$emulator_pid"');
|
||||
expect(diagnostic).toContain('kill "$emulator_pid"');
|
||||
expect(diagnostic).toContain("adb kill-server");
|
||||
expect(diagnostic).toContain("trap cleanup EXIT");
|
||||
expect(diagnostic).toMatch(
|
||||
|
|
@ -1906,8 +1856,6 @@ fi
|
|||
"bundle:_4.0.21_ exec fastlane ios signing_check",
|
||||
"probe:root-cwd",
|
||||
]);
|
||||
expect(signingProof).toContain("source ./scripts/lib/ios-fastlane.sh");
|
||||
expect(signingProof).toContain("(cd apps/ios && run_ios_fastlane ios signing_check)");
|
||||
|
||||
const failedCheck = runSigningProof({ FIXTURE_FAIL_CHECK: "1" });
|
||||
expect(failedCheck.result.status).not.toBe(0);
|
||||
|
|
|
|||
|
|
@ -179,104 +179,90 @@ describe("generated mobile store notes", () => {
|
|||
await expect(generateMobileReleaseNotes(f)).rejects.toThrow("different production baseline");
|
||||
});
|
||||
|
||||
it.each(["legacy", "v2"])(
|
||||
"keeps phone and Wear baselines and text separate with %s source records",
|
||||
async (scheme) => {
|
||||
const f = fixture("android");
|
||||
const sourceRef =
|
||||
"refs/openclaw/mobile-releases/android/v2/2026.7.3/0/1/2026070449-2026070450";
|
||||
const wearBase = f.sourceSha;
|
||||
git(
|
||||
f.rootDir,
|
||||
"update-ref",
|
||||
"refs/openclaw/mobile-releases/android/2026.7.3-2026070302",
|
||||
wearBase,
|
||||
);
|
||||
f.plan.releaseNotesBaselines[1] = {
|
||||
audience: "wear",
|
||||
version: "2026.7.3",
|
||||
build: "2026070352",
|
||||
};
|
||||
const wearFile = "apps/android/wear/src/main/java/Watch.kt";
|
||||
f.write(wearFile, 'val label = "Start talking"\n');
|
||||
git(f.rootDir, "add", wearFile);
|
||||
git(f.rootDir, "commit", "-m", "Clarify watch voice action");
|
||||
f.sourceSha = git(f.rootDir, "rev-parse", "HEAD");
|
||||
f.plan.sourceSha = f.sourceSha;
|
||||
fs.writeFileSync(f.planPath, JSON.stringify(f.plan));
|
||||
if (scheme === "v2") {
|
||||
git(f.rootDir, "update-ref", sourceRef, f.base);
|
||||
f.plan.releaseNotesBaselines[0] = {
|
||||
audience: "phone",
|
||||
version: "2026.7.30",
|
||||
build: "2026070449",
|
||||
};
|
||||
fs.writeFileSync(
|
||||
f.planPath,
|
||||
JSON.stringify({
|
||||
...f.plan,
|
||||
releaseNotesBaselines: [
|
||||
{ ...f.plan.releaseNotesBaselines[0], sourceRef },
|
||||
f.plan.releaseNotesBaselines[1],
|
||||
],
|
||||
}),
|
||||
);
|
||||
}
|
||||
accept([{ file: f.file, focus: ["label"] }]);
|
||||
accept(
|
||||
[{ file: wearFile, focus: ["label"] }],
|
||||
[{ text: "Clearer watch voice controls.", evidenceIds: ["e1"] }],
|
||||
);
|
||||
const saved = await generateMobileReleaseNotes(f);
|
||||
const wearInventory = JSON.parse(api.parse.mock.calls[3]![0].input);
|
||||
expect(wearInventory.files).toEqual(
|
||||
expect.arrayContaining([expect.objectContaining({ file: wearFile })]),
|
||||
);
|
||||
expect(wearInventory.files).not.toEqual(
|
||||
expect.arrayContaining([expect.objectContaining({ file: f.file })]),
|
||||
);
|
||||
for (const index of [2, 5]) {
|
||||
const review = JSON.parse(api.parse.mock.calls[index]![0].input);
|
||||
expect(review.evidence).toEqual(
|
||||
expect.arrayContaining([
|
||||
expect.objectContaining({
|
||||
file: "apps/android/app/src/play/java/ai/openclaw/app/SensitiveFeatureConfig.kt",
|
||||
kind: "context",
|
||||
patch: expect.stringContaining("smsEnabled = false"),
|
||||
}),
|
||||
]),
|
||||
);
|
||||
}
|
||||
expect(
|
||||
saved.entries.map((entry) => [
|
||||
entry.audience,
|
||||
entry.baseline.build,
|
||||
entry.baseline.sourceSha,
|
||||
entry.text,
|
||||
]),
|
||||
).toEqual([
|
||||
[
|
||||
"phone",
|
||||
scheme === "v2" ? "2026070449" : "2026070301",
|
||||
f.base,
|
||||
"- Clearer labels when sending messages.",
|
||||
it("keeps v2 phone and legacy Wear baselines and text separate", async () => {
|
||||
const f = fixture("android");
|
||||
const sourceRef = "refs/openclaw/mobile-releases/android/v2/2026.7.3/0/1/2026070449-2026070450";
|
||||
const wearBase = f.sourceSha;
|
||||
git(
|
||||
f.rootDir,
|
||||
"update-ref",
|
||||
"refs/openclaw/mobile-releases/android/2026.7.3-2026070302",
|
||||
wearBase,
|
||||
);
|
||||
f.plan.releaseNotesBaselines[1] = {
|
||||
audience: "wear",
|
||||
version: "2026.7.3",
|
||||
build: "2026070352",
|
||||
};
|
||||
const wearFile = "apps/android/wear/src/main/java/Watch.kt";
|
||||
f.write(wearFile, 'val label = "Start talking"\n');
|
||||
git(f.rootDir, "add", wearFile);
|
||||
git(f.rootDir, "commit", "-m", "Clarify watch voice action");
|
||||
f.sourceSha = git(f.rootDir, "rev-parse", "HEAD");
|
||||
f.plan.sourceSha = f.sourceSha;
|
||||
git(f.rootDir, "update-ref", sourceRef, f.base);
|
||||
f.plan.releaseNotesBaselines[0] = {
|
||||
audience: "phone",
|
||||
version: "2026.7.30",
|
||||
build: "2026070449",
|
||||
};
|
||||
fs.writeFileSync(
|
||||
f.planPath,
|
||||
JSON.stringify({
|
||||
...f.plan,
|
||||
releaseNotesBaselines: [
|
||||
{ ...f.plan.releaseNotesBaselines[0], sourceRef },
|
||||
f.plan.releaseNotesBaselines[1],
|
||||
],
|
||||
["wear", "2026070352", wearBase, "- Clearer watch voice controls."],
|
||||
]);
|
||||
vi.stubEnv("OPENAI_API_KEY", "");
|
||||
expect(await generateMobileReleaseNotes(f)).toEqual(saved);
|
||||
if (scheme === "v2") {
|
||||
fs.rmSync(f.outputPath);
|
||||
vi.stubEnv("OPENAI_API_KEY", "synthetic-key");
|
||||
const plan = JSON.parse(fs.readFileSync(f.planPath, "utf8"));
|
||||
plan.releaseNotesBaselines[0].build = "2026070450";
|
||||
fs.writeFileSync(f.planPath, JSON.stringify(plan));
|
||||
await expect(generateMobileReleaseNotes(f)).rejects.toThrow(
|
||||
"does not match its recorded store identity",
|
||||
);
|
||||
}
|
||||
},
|
||||
);
|
||||
}),
|
||||
);
|
||||
accept([{ file: f.file, focus: ["label"] }]);
|
||||
accept(
|
||||
[{ file: wearFile, focus: ["label"] }],
|
||||
[{ text: "Clearer watch voice controls.", evidenceIds: ["e1"] }],
|
||||
);
|
||||
const saved = await generateMobileReleaseNotes(f);
|
||||
const wearInventory = JSON.parse(api.parse.mock.calls[3]![0].input);
|
||||
expect(wearInventory.files).toEqual(
|
||||
expect.arrayContaining([expect.objectContaining({ file: wearFile })]),
|
||||
);
|
||||
expect(wearInventory.files).not.toEqual(
|
||||
expect.arrayContaining([expect.objectContaining({ file: f.file })]),
|
||||
);
|
||||
for (const index of [2, 5]) {
|
||||
const review = JSON.parse(api.parse.mock.calls[index]![0].input);
|
||||
expect(review.evidence).toEqual(
|
||||
expect.arrayContaining([
|
||||
expect.objectContaining({
|
||||
file: "apps/android/app/src/play/java/ai/openclaw/app/SensitiveFeatureConfig.kt",
|
||||
kind: "context",
|
||||
patch: expect.stringContaining("smsEnabled = false"),
|
||||
}),
|
||||
]),
|
||||
);
|
||||
}
|
||||
expect(
|
||||
saved.entries.map((entry) => [
|
||||
entry.audience,
|
||||
entry.baseline.build,
|
||||
entry.baseline.sourceSha,
|
||||
entry.text,
|
||||
]),
|
||||
).toEqual([
|
||||
["phone", "2026070449", f.base, "- Clearer labels when sending messages."],
|
||||
["wear", "2026070352", wearBase, "- Clearer watch voice controls."],
|
||||
]);
|
||||
vi.stubEnv("OPENAI_API_KEY", "");
|
||||
expect(await generateMobileReleaseNotes(f)).toEqual(saved);
|
||||
fs.rmSync(f.outputPath);
|
||||
vi.stubEnv("OPENAI_API_KEY", "synthetic-key");
|
||||
const plan = JSON.parse(fs.readFileSync(f.planPath, "utf8"));
|
||||
plan.releaseNotesBaselines[0].build = "2026070450";
|
||||
fs.writeFileSync(f.planPath, JSON.stringify(plan));
|
||||
await expect(generateMobileReleaseNotes(f)).rejects.toThrow(
|
||||
"does not match its recorded store identity",
|
||||
);
|
||||
});
|
||||
|
||||
it("does not invent notes for a fully reverted app change", async () => {
|
||||
const f = fixture();
|
||||
|
|
|
|||
|
|
@ -55,13 +55,6 @@ describe("installed-package execution identity proof", () => {
|
|||
expect(existsSync(path.join(state, "state/openclaw.sqlite"))).toBe(false);
|
||||
});
|
||||
|
||||
it("accepts the persisted run id when it differs from the caller session id", () => {
|
||||
const result = projection();
|
||||
expect(
|
||||
assertIdentityProjection(result, JSON.stringify(result.identity.context), [privateMarker]),
|
||||
).toBe("execution-1");
|
||||
});
|
||||
|
||||
it.each([
|
||||
[
|
||||
"missing identity",
|
||||
|
|
@ -109,15 +102,10 @@ describe("installed-package execution identity proof", () => {
|
|||
).toThrow();
|
||||
});
|
||||
|
||||
it.each(["export", "storage", "receipt"])("rejects a private canary in %s", (where) => {
|
||||
it.each(["export", "storage"])("rejects a private canary in %s", (where) => {
|
||||
const result = projection();
|
||||
const stored = JSON.stringify(result.identity.context);
|
||||
const exported =
|
||||
where === "export"
|
||||
? { ...result, prompt: privateMarker }
|
||||
: where === "receipt"
|
||||
? { ...result, decisions: [{ body: privateMarker }] }
|
||||
: result;
|
||||
const exported = where === "export" ? { ...result, prompt: privateMarker } : result;
|
||||
expect(() =>
|
||||
assertIdentityProjection(exported, where === "storage" ? stored + privateMarker : stored, [
|
||||
privateMarker,
|
||||
|
|
@ -152,7 +140,7 @@ describe("installed-package execution identity proof", () => {
|
|||
insert.run(JSON.stringify(before.identity.context));
|
||||
writeFileSync(beforePath, JSON.stringify(before));
|
||||
writeFileSync(afterPath, JSON.stringify(before));
|
||||
expect(verify().status).toBe(0);
|
||||
expect(verify()).toMatchObject({ status: 0, stdout: "execution-1" });
|
||||
const after = projection();
|
||||
after.identity.context.runtimeInstance.runtimeRef = domainRef.replace(/b/gu, "c");
|
||||
db.prepare("UPDATE execution_identity_contexts SET context_json = ?").run(
|
||||
|
|
|
|||
|
|
@ -8,13 +8,9 @@ const helper = "scripts/e2e/lib/onboard/first-agent-flow.sh";
|
|||
describe.skipIf(process.platform === "win32")("guided first-agent prompt handshake", () => {
|
||||
it.each([
|
||||
["legacy", "plain", "provider", 5],
|
||||
["legacy", "fragmented", "provider", 5],
|
||||
["team", "plain", "provider", 6],
|
||||
["team", "fragmented", "provider", 6],
|
||||
["legacy", "plain", "configured", 4],
|
||||
["legacy", "fragmented", "configured", 4],
|
||||
["team", "plain", "configured", 5],
|
||||
["team", "fragmented", "configured", 5],
|
||||
] as const)(
|
||||
"drives the real guided sender through %s %s %s prompts",
|
||||
(layout, rendering, modelPrompt, count) => {
|
||||
|
|
|
|||
|
|
@ -292,12 +292,8 @@ describe("OpenClaw performance Crabbox boundary", () => {
|
|||
},
|
||||
);
|
||||
|
||||
it.each([
|
||||
{ name: "IPv6 rule readback", fault: "6:-C" },
|
||||
{ name: "IPv4 reachable metadata", family: 4, code: 0, output: "401" },
|
||||
{ name: "IPv6 reachable metadata", family: 6, code: 0, output: "401" },
|
||||
])("blocks runner handoff without its own metadata denial: $name", (options) => {
|
||||
const run = prepareSut({ ...options, runner: true });
|
||||
it("blocks runner handoff when metadata is reachable", () => {
|
||||
const run = prepareSut({ runner: true, family: 6, code: 0, output: "401" });
|
||||
expect(run.result.error).toBeUndefined();
|
||||
expect(run.result.status, run.result.stderr).not.toBe(0);
|
||||
expect(run.handoff).toBe(false);
|
||||
|
|
@ -310,21 +306,19 @@ describe("OpenClaw performance Crabbox boundary", () => {
|
|||
fault: `missing:${tool}`,
|
||||
})),
|
||||
...["http", "ipv6", "version"].map((fault) => ({ name: `curl ${fault}`, fault })),
|
||||
...[4, 6].flatMap((family) =>
|
||||
["401", "404"].map((output) => ({
|
||||
name: `IPv${family} HTTP ${output}`,
|
||||
family,
|
||||
output,
|
||||
code: 0,
|
||||
})),
|
||||
),
|
||||
...[0, 1, 2, 3, 4, 5, 6, 22, 23, 26, 28, 52, 55, 56, 126, 127, 143].map((code) => ({
|
||||
...[4, 6].map((family) => ({
|
||||
name: `IPv${family} HTTP 401`,
|
||||
family,
|
||||
output: "401",
|
||||
code: 0,
|
||||
})),
|
||||
...[0, 22, 28, 127].map((code) => ({
|
||||
name: `IPv6 curl exit ${code}`,
|
||||
code,
|
||||
})),
|
||||
{ name: "IPv4 timeout", family: 4, code: 28 },
|
||||
{ name: "HTTP response despite connection failure", output: "401", code: 7 },
|
||||
...["", "00", "000000", "000\n401", " 000", "000\n"].map((output) => ({
|
||||
...["", "000\n401", "000\n"].map((output) => ({
|
||||
name: `malformed status ${JSON.stringify(output)}`,
|
||||
output,
|
||||
})),
|
||||
|
|
@ -446,7 +440,6 @@ collect_diagnostics "$SOURCE" "$DESTINATION" "$SUBTREE"
|
|||
|
||||
it.each([
|
||||
{ name: "success", expected: 0 },
|
||||
{ name: "advisory matrix 17", matrixExit: 17, expected: 0 },
|
||||
{ name: "adapted gated matrix 17", matrixExit: 17, gated: true, adapted: true, expected: 0 },
|
||||
{ name: "rejected gated matrix 17", matrixExit: 17, gated: true, expected: 17 },
|
||||
{ name: "setup failure", setupExit: 23, expected: 23 },
|
||||
|
|
@ -823,75 +816,58 @@ fi
|
|||
it.each([
|
||||
{
|
||||
name: "advisory BLOCKED",
|
||||
gated: false,
|
||||
sutExit: 17,
|
||||
records: true,
|
||||
planFilter: "scenario:probe",
|
||||
expected: 0,
|
||||
},
|
||||
{
|
||||
name: "unadaptable gated BLOCKED",
|
||||
gated: true,
|
||||
sutExit: 17,
|
||||
records: true,
|
||||
planFilter: "scenario:probe",
|
||||
expected: 17,
|
||||
},
|
||||
{
|
||||
name: "missing requested records",
|
||||
gated: false,
|
||||
sutExit: 0,
|
||||
records: false,
|
||||
planFilter: "scenario:probe",
|
||||
expected: 1,
|
||||
},
|
||||
{
|
||||
name: "wrong plan filters",
|
||||
gated: false,
|
||||
sutExit: 0,
|
||||
records: true,
|
||||
planFilter: "scenario:wrong",
|
||||
expected: 1,
|
||||
},
|
||||
{
|
||||
name: "ambiguous full reports",
|
||||
gated: false,
|
||||
sutExit: 0,
|
||||
records: true,
|
||||
planFilter: "scenario:probe",
|
||||
ambiguous: true,
|
||||
expected: 1,
|
||||
},
|
||||
{
|
||||
name: "custom Kova diagnostics",
|
||||
gated: false,
|
||||
sutExit: 0,
|
||||
records: true,
|
||||
planFilter: "scenario:probe",
|
||||
admitted: false,
|
||||
expected: 0,
|
||||
},
|
||||
{
|
||||
name: "custom Kova cannot approve a gate",
|
||||
gated: true,
|
||||
sutExit: 0,
|
||||
records: true,
|
||||
planFilter: "scenario:probe",
|
||||
admitted: false,
|
||||
expected: 1,
|
||||
},
|
||||
{
|
||||
name: "custom Kova invalid evidence",
|
||||
gated: false,
|
||||
sutExit: 0,
|
||||
records: false,
|
||||
planFilter: "scenario:probe",
|
||||
admitted: false,
|
||||
expected: 1,
|
||||
},
|
||||
])(
|
||||
"enforces native Kova evidence and gate semantics: $name",
|
||||
({ gated, sutExit, records, planFilter, expected, admitted = true, ambiguous = false }) => {
|
||||
({
|
||||
gated = false,
|
||||
sutExit = 0,
|
||||
records = true,
|
||||
planFilter = "scenario:probe",
|
||||
expected,
|
||||
admitted = true,
|
||||
ambiguous = false,
|
||||
}) => {
|
||||
const root = tempDirs.make("openclaw-performance-kova-contract-");
|
||||
const openclaw = join(root, "openclaw");
|
||||
mkdirSync(join(openclaw, ".artifacts/kova/reports/mock-provider"), { recursive: true });
|
||||
|
|
@ -1051,10 +1027,8 @@ validate_kova mock-provider "$ROOT" diagnostic 1 scenario:probe - "$GATED" "$HEL
|
|||
);
|
||||
chmodSync(crabbox, 0o755);
|
||||
|
||||
for (let attempt = 0; attempt < 2; attempt += 1) {
|
||||
const result = spawnSync("bash", [SCRIPT, "confirm-stop", crabbox, "cbx_0123456789ab"]);
|
||||
expect(result.status).toBe(status);
|
||||
}
|
||||
const result = spawnSync("bash", [SCRIPT, "confirm-stop", crabbox, "cbx_0123456789ab"]);
|
||||
expect(result.status).toBe(status);
|
||||
},
|
||||
);
|
||||
|
||||
|
|
|
|||
|
|
@ -157,7 +157,6 @@ describe("release readiness contract", () => {
|
|||
["retired soak waiver", { stable_soak_waiver: "2026.9.2 approved" }],
|
||||
["retired lane waiver", { lane_waiver: "2026.9.2 approved" }],
|
||||
["moving source", { tag: "main" }],
|
||||
["missing source", { tag: "" }],
|
||||
["wrong beta channel", { npm_dist_tag: "latest" }],
|
||||
["alpha owner", { tag: "v2026.9.2-alpha.1", npm_dist_tag: "alpha" }],
|
||||
["extended-stable owner", { tag: "v2026.9.33", npm_dist_tag: "latest" }],
|
||||
|
|
@ -1844,28 +1843,25 @@ describe("release preparation recovery", () => {
|
|||
expect(existsSync(fixture.env.GITHUB_OUTPUT)).toBe(false);
|
||||
});
|
||||
|
||||
it.each([1, 2])(
|
||||
"adopts exact identified preparations without dispatch on attempt %s",
|
||||
async (attempt) => {
|
||||
const fixture = await preparationFixture();
|
||||
const request = preparationRequest();
|
||||
const result = fixture.prepare(request, attempt);
|
||||
expect(result.status, result.stderr).toBe(0);
|
||||
expect(JSON.parse(readFileSync(fixture.requestPath, "utf8"))).toEqual(request);
|
||||
expect(fixture.trace().filter((entry) => entry.args?.includes("POST"))).toEqual([]);
|
||||
expect(
|
||||
fixture
|
||||
.trace()
|
||||
.filter((entry) => /\/actions\/runs\/(?:300|400)$/u.test(entry.args?.[1] ?? "")),
|
||||
).toEqual([
|
||||
{ event: "gh", args: ["api", `repos/${REPOSITORY}/actions/runs/300`] },
|
||||
{ event: "gh", args: ["api", `repos/${REPOSITORY}/actions/runs/400`] },
|
||||
]);
|
||||
expect(readFileSync(fixture.env.GITHUB_OUTPUT, "utf8")).toContain(
|
||||
`request=${JSON.stringify(request)}\n`,
|
||||
);
|
||||
},
|
||||
);
|
||||
it("adopts exact identified preparations without dispatch on a rerun", async () => {
|
||||
const fixture = await preparationFixture();
|
||||
const request = preparationRequest();
|
||||
const result = fixture.prepare(request, 2);
|
||||
expect(result.status, result.stderr).toBe(0);
|
||||
expect(JSON.parse(readFileSync(fixture.requestPath, "utf8"))).toEqual(request);
|
||||
expect(fixture.trace().filter((entry) => entry.args?.includes("POST"))).toEqual([]);
|
||||
expect(
|
||||
fixture
|
||||
.trace()
|
||||
.filter((entry) => /\/actions\/runs\/(?:300|400)$/u.test(entry.args?.[1] ?? "")),
|
||||
).toEqual([
|
||||
{ event: "gh", args: ["api", `repos/${REPOSITORY}/actions/runs/300`] },
|
||||
{ event: "gh", args: ["api", `repos/${REPOSITORY}/actions/runs/400`] },
|
||||
]);
|
||||
expect(readFileSync(fixture.env.GITHUB_OUTPUT, "utf8")).toContain(
|
||||
`request=${JSON.stringify(request)}\n`,
|
||||
);
|
||||
});
|
||||
|
||||
it.each([
|
||||
["schema", { schema: "different" }],
|
||||
|
|
@ -1893,9 +1889,6 @@ describe("release preparation recovery", () => {
|
|||
|
||||
it.each([
|
||||
["workflow", { path: ".github/workflows/openclaw-release-publish.yml" }],
|
||||
["event", { event: "push" }],
|
||||
["protected ref", { head_branch: "main" }],
|
||||
["tooling SHA", { head_sha: "c".repeat(40) }],
|
||||
["attempt", { run_attempt: 0 }],
|
||||
])("rejects a recovered producer with the wrong %s", async (_label, preparationProducer) => {
|
||||
const fixture = await preparationFixture({ preparationProducer });
|
||||
|
|
@ -1952,7 +1945,6 @@ process.exitCode = 1;
|
|||
] as const)(
|
||||
"preserves sealed inputs with resume override %s (sealed=%s)",
|
||||
(resumeRunId, sealedResumeRunId) => {
|
||||
const fixture = finalizationFixture();
|
||||
const ready = readyRelease();
|
||||
ready.inputs = validateReleaseButtonInputs(
|
||||
inputs({
|
||||
|
|
@ -1967,38 +1959,9 @@ process.exitCode = 1;
|
|||
...(sealedResumeRunId ? { openclaw_npm_resume_run_id: sealedResumeRunId } : {}),
|
||||
}),
|
||||
);
|
||||
writeFixtureFile(
|
||||
fixture.scripts,
|
||||
"lib/actions-artifact-archive.mjs",
|
||||
`
|
||||
export { readBoundedRegularFile } from ${JSON.stringify(pathToFileURL(resolve("scripts/lib/actions-artifact-archive.mjs")).href)};
|
||||
export async function downloadActionsArtifactArchive() { return { archiveBytes: Buffer.from('verified fixture archive') }; }
|
||||
export function inspectActionsArtifactZipWithPolicy() {
|
||||
return new Map([['release-ready.json', Buffer.from(${JSON.stringify(JSON.stringify(ready))})]]);
|
||||
}
|
||||
`,
|
||||
);
|
||||
const artifact = {
|
||||
...descriptor("npm"),
|
||||
workflowPath: ".github/workflows/openclaw-release-prepare.yml",
|
||||
artifactName: readyArtifactName(SOURCE_SHA, 300, 1),
|
||||
};
|
||||
const workflow = parse(
|
||||
readFileSync(".github/workflows/openclaw-release-promote.yml", "utf8"),
|
||||
);
|
||||
const dispatch = workflow.jobs.publish.steps.find(
|
||||
(step: { id?: string }) => step.id === "dispatch",
|
||||
);
|
||||
const result = spawnSync("bash", ["-c", dispatch.run], {
|
||||
cwd: dirname(fixture.scripts),
|
||||
env: {
|
||||
...fixture.env,
|
||||
PREPARED_ARTIFACT: JSON.stringify(artifact),
|
||||
OPENCLAW_NPM_RESUME_RUN_ID: resumeRunId,
|
||||
},
|
||||
encoding: "utf8",
|
||||
timeout: 10_000,
|
||||
});
|
||||
const fixture = publicationFixture({}, ready);
|
||||
const { workflow } = fixture;
|
||||
const result = fixture.publish({ OPENCLAW_NPM_RESUME_RUN_ID: resumeRunId });
|
||||
expect(result.status, result.stderr).toBe(0);
|
||||
const outputs = Object.fromEntries(
|
||||
readFileSync(fixture.env.GITHUB_OUTPUT, "utf8")
|
||||
|
|
@ -2063,7 +2026,7 @@ process.exitCode = 1;
|
|||
},
|
||||
);
|
||||
|
||||
it.each(["0", "-1", "1.5", " 800", "9007199254740992"])(
|
||||
it.each(["0", " 800", "9007199254740992"])(
|
||||
"rejects malformed resume run %s without dispatching publication",
|
||||
(resumeRunId) => {
|
||||
const fixture = finalizationFixture();
|
||||
|
|
@ -2133,7 +2096,6 @@ process.exitCode = 1;
|
|||
|
||||
it.each([
|
||||
["v2026.9.2-beta.1", "beta", true],
|
||||
["v2026.9.2", "beta", false],
|
||||
["v2026.9.2", "latest", false],
|
||||
] as const)(
|
||||
"refuses parent activation of %s on %s with a mismatched draft classification",
|
||||
|
|
@ -2168,12 +2130,10 @@ process.exitCode = 1;
|
|||
describe("prepared Windows handoff", () => {
|
||||
it.each([
|
||||
["stable on beta", "v2026.9.2", "beta", "success", true, true, false, true],
|
||||
["stable on latest", "v2026.9.2", "latest", "success", true, true, false, true],
|
||||
["absent", "v2026.9.2", "beta", "success", false, false, false, false],
|
||||
["incomplete", "v2026.9.2", "beta", "success", true, false, false, true],
|
||||
["beta", "v2026.9.2-beta.1", "beta", "success", true, true, false, false],
|
||||
["failed activation", "v2026.9.2", "beta", "failure", true, true, false, false],
|
||||
["skipped activation", "v2026.9.2", "beta", "skipped", true, true, false, false],
|
||||
["dispatch failed", "v2026.9.2", "beta", "success", true, true, true, true],
|
||||
] as const)(
|
||||
"uses the frozen optional selection after activation: %s",
|
||||
|
|
|
|||
|
|
@ -127,8 +127,6 @@ describePosix("native merge outcome with real Git and supervised lock recovery",
|
|||
|
||||
it.each([
|
||||
{ auto: false, mergeStateStatus: "CLEAN", route: "immediate" },
|
||||
{ auto: true, mergeStateStatus: "CLEAN", route: "immediate" },
|
||||
{ auto: true, mergeStateStatus: "BEHIND", route: "auto" },
|
||||
{ auto: true, mergeStateStatus: "BLOCKED", route: "auto" },
|
||||
{ auto: false, mergeStateStatus: "CLEAN", route: "immediate", statusFirst: true },
|
||||
])(
|
||||
|
|
@ -209,18 +207,14 @@ describePosix("native merge outcome with real Git and supervised lock recovery",
|
|||
});
|
||||
|
||||
it.each([
|
||||
"persistent UNKNOWN",
|
||||
"persistent UNKNOWN mergeable",
|
||||
"persistent UNKNOWN status",
|
||||
"known mergeable reverts",
|
||||
"known status reverts",
|
||||
"known status changes",
|
||||
"invalid metadata",
|
||||
"API error",
|
||||
"PR identity",
|
||||
"head",
|
||||
"base",
|
||||
"closed",
|
||||
"merged",
|
||||
"draft",
|
||||
"auto request",
|
||||
|
|
@ -231,74 +225,31 @@ describePosix("native merge outcome with real Git and supervised lock recovery",
|
|||
"known HAS_HOOKS",
|
||||
"final UNKNOWN mergeable",
|
||||
"final UNKNOWN status",
|
||||
"final changed status",
|
||||
])("stops initial settlement without dispatch on %s", (fault) => {
|
||||
const f = fixture();
|
||||
const next = f.state();
|
||||
const { author: _author, headRefName: _headRefName, ...observedPr } = next.pr;
|
||||
const step: (typeof next.observations)[number] = {};
|
||||
switch (fault) {
|
||||
case "invalid metadata":
|
||||
step.invalid = true;
|
||||
break;
|
||||
case "API error":
|
||||
step.unavailable = true;
|
||||
break;
|
||||
case "PR identity":
|
||||
step.pr = { id: "other-pr" };
|
||||
break;
|
||||
case "head":
|
||||
step.pr = { headRefOid: f.base };
|
||||
break;
|
||||
case "base":
|
||||
step.pr = { baseRefName: "release" };
|
||||
break;
|
||||
case "closed":
|
||||
step.pr = { state: "CLOSED" };
|
||||
break;
|
||||
case "merged":
|
||||
step.main = f.commit(f.tree("after\n"), [f.base]);
|
||||
step.pr = { state: "MERGED", mergeCommit: { oid: step.main } };
|
||||
break;
|
||||
case "draft":
|
||||
step.pr = { isDraft: true };
|
||||
break;
|
||||
case "auto request":
|
||||
step.pr = { autoMergeRequest: { mergeMethod: "SQUASH" } };
|
||||
break;
|
||||
case "queue policy":
|
||||
step.pr = { isMergeQueueEnabled: true };
|
||||
break;
|
||||
case "queue membership":
|
||||
step.pr = { isInMergeQueue: true };
|
||||
break;
|
||||
case "invalid receipt":
|
||||
step.pr = { mergeCommit: { oid: f.head } };
|
||||
break;
|
||||
case "conflicting":
|
||||
step.pr = { mergeable: "CONFLICTING", mergeStateStatus: "DIRTY" };
|
||||
break;
|
||||
case "known HAS_HOOKS":
|
||||
step.pr = { mergeable: "MERGEABLE", mergeStateStatus: "HAS_HOOKS" };
|
||||
break;
|
||||
case "known mergeable reverts":
|
||||
step.pr = { mergeable: "UNKNOWN" };
|
||||
break;
|
||||
case "known status reverts":
|
||||
step.pr = { mergeStateStatus: "UNKNOWN" };
|
||||
break;
|
||||
case "known status changes":
|
||||
step.pr = { mergeStateStatus: "BEHIND" };
|
||||
break;
|
||||
case "final UNKNOWN mergeable":
|
||||
step.pr = { mergeable: "UNKNOWN" };
|
||||
break;
|
||||
case "final UNKNOWN status":
|
||||
step.pr = { mergeStateStatus: "UNKNOWN" };
|
||||
break;
|
||||
case "final changed status":
|
||||
step.pr = { mergeStateStatus: "BEHIND" };
|
||||
break;
|
||||
const steps: Record<string, (typeof next.observations)[number]> = {
|
||||
"invalid metadata": { invalid: true },
|
||||
"API error": { unavailable: true },
|
||||
"PR identity": { pr: { id: "other-pr" } },
|
||||
head: { pr: { headRefOid: f.base } },
|
||||
draft: { pr: { isDraft: true } },
|
||||
"auto request": { pr: { autoMergeRequest: { mergeMethod: "SQUASH" } } },
|
||||
"queue policy": { pr: { isMergeQueueEnabled: true } },
|
||||
"queue membership": { pr: { isInMergeQueue: true } },
|
||||
"invalid receipt": { pr: { mergeCommit: { oid: f.head } } },
|
||||
conflicting: { pr: { mergeable: "CONFLICTING", mergeStateStatus: "DIRTY" } },
|
||||
"known HAS_HOOKS": { pr: { mergeable: "MERGEABLE", mergeStateStatus: "HAS_HOOKS" } },
|
||||
"known mergeable reverts": { pr: { mergeable: "UNKNOWN" } },
|
||||
"known status reverts": { pr: { mergeStateStatus: "UNKNOWN" } },
|
||||
"final UNKNOWN mergeable": { pr: { mergeable: "UNKNOWN" } },
|
||||
"final UNKNOWN status": { pr: { mergeStateStatus: "UNKNOWN" } },
|
||||
};
|
||||
const step = steps[fault] ?? {};
|
||||
if (fault === "merged") {
|
||||
step.main = f.commit(f.tree("after\n"), [f.base]);
|
||||
step.pr = { state: "MERGED", mergeCommit: { oid: step.main } };
|
||||
}
|
||||
next.observations = [{ pr: unknownProjection }];
|
||||
const persistent = fault.startsWith("persistent ");
|
||||
|
|
@ -348,7 +299,7 @@ describePosix("native merge outcome with real Git and supervised lock recovery",
|
|||
expect(run.output).toContain(
|
||||
fault === "final UNKNOWN mergeable"
|
||||
? 'mergeable: observed="UNKNOWN"; expected="MERGEABLE"'
|
||||
: `mergeStateStatus: observed="${fault === "final UNKNOWN status" ? "UNKNOWN" : "BEHIND"}"; expected="CLEAN"`,
|
||||
: 'mergeStateStatus: observed="UNKNOWN"; expected="CLEAN"',
|
||||
);
|
||||
for (const [label, expected] of [
|
||||
["observation", { main: f.base, pr: observedPr, transport: "graphql" }],
|
||||
|
|
@ -448,7 +399,7 @@ describePosix("native merge outcome with real Git and supervised lock recovery",
|
|||
f.git(["merge-base", "--is-ancestor", f.head, f.record().landed]);
|
||||
});
|
||||
|
||||
it.each(["review", "ready", "checks", "pending", "existing-auto", "auto-ineligible"])(
|
||||
it.each(["review", "ready", "checks", "pending"])(
|
||||
"keeps %s admission ahead of intent",
|
||||
(gate) => {
|
||||
const f = fixture();
|
||||
|
|
@ -465,12 +416,6 @@ describePosix("native merge outcome with real Git and supervised lock recovery",
|
|||
if (gate === "pending") {
|
||||
next.gates = "pending";
|
||||
}
|
||||
if (gate === "existing-auto") {
|
||||
next.pr.autoMergeRequest = { mergeMethod: "MERGE" };
|
||||
}
|
||||
if (gate === "auto-ineligible") {
|
||||
next.pr.mergeStateStatus = "HAS_HOOKS";
|
||||
}
|
||||
f.save(next);
|
||||
const run = f.run(true);
|
||||
expect(run.status, run.output).toBe(1);
|
||||
|
|
@ -499,39 +444,23 @@ describePosix("native merge outcome with real Git and supervised lock recovery",
|
|||
expect(() => f.record()).toThrow();
|
||||
});
|
||||
|
||||
it.each([
|
||||
{
|
||||
name: "removed",
|
||||
reason: "completed review is missing.",
|
||||
comments: [
|
||||
it("revalidates removed review evidence immediately before intent", () => {
|
||||
const f = fixture();
|
||||
f.save({
|
||||
...f.state(),
|
||||
issueCommentsAfterFirst: [
|
||||
{
|
||||
id: 2,
|
||||
body: "<!-- clawsweeper-pr-ack:opened item=123 -->",
|
||||
user: { id: 274271284, login: "clawsweeper[bot]", type: "Bot" },
|
||||
},
|
||||
],
|
||||
},
|
||||
{
|
||||
name: "malformed",
|
||||
reason: "trusted review-version field values are invalid.",
|
||||
comments: [
|
||||
{
|
||||
id: 2,
|
||||
body: `<!-- clawsweeper-review-version item=123 reviewed_at=invalid sha=${"a".repeat(40)} source_revision=${"c".repeat(64)} lease_owner=github-run-2 lease_comment_id=2 v=1 -->
|
||||
|
||||
<!-- clawsweeper-review item=123 -->`,
|
||||
user: { id: 274271284, login: "clawsweeper[bot]", type: "Bot" },
|
||||
},
|
||||
],
|
||||
},
|
||||
])("revalidates $name review evidence immediately before intent", ({ comments, reason }) => {
|
||||
const f = fixture();
|
||||
f.save({ ...f.state(), issueCommentsAfterFirst: comments });
|
||||
});
|
||||
|
||||
const run = f.run();
|
||||
|
||||
expect(run.status, run.output).toBe(1);
|
||||
expect(run.output).toContain(`ClawSweeper review gate failed: ${reason}`);
|
||||
expect(run.output).toContain("ClawSweeper review gate failed: completed review is missing.");
|
||||
expect(f.state().issueCommentReads).toBe(2);
|
||||
expect(f.state().mutations).toBe(0);
|
||||
expect(() => f.record()).toThrow();
|
||||
|
|
|
|||
|
|
@ -41,7 +41,6 @@ describePosix("native auto-merge recovery", () => {
|
|||
{ mode: "pending", cancellation: "lost", absent: false },
|
||||
{ mode: "pending-error", cancellation: "success", absent: false },
|
||||
{ mode: "pending", cancellation: "success", absent: true },
|
||||
{ mode: "pending-error", cancellation: "success", absent: true },
|
||||
])(
|
||||
"retires auto before recovering a reviewed replacement (submission=$mode, cancellation=$cancellation, absent=$absent)",
|
||||
({ mode, cancellation, absent }) => {
|
||||
|
|
|
|||
|
|
@ -28,7 +28,7 @@ describePosix("native merge outcome with real Git and supervised lock recovery",
|
|||
expect(f.state().graphqlMergePayloads[0]?.expectedHeadOid).toBe(f.head);
|
||||
});
|
||||
|
||||
it.each(["tamper", "head", "queue", "merge", "review"])(
|
||||
it.each(["tamper", "head", "queue", "merge"])(
|
||||
"keeps explicit body admission closed for %s",
|
||||
(fault) => {
|
||||
const f = fixture();
|
||||
|
|
@ -44,9 +44,6 @@ describePosix("native merge outcome with real Git and supervised lock recovery",
|
|||
if (fault === "queue") {
|
||||
state.observations = [{ pr: { isMergeQueueEnabled: true } }];
|
||||
}
|
||||
if (fault === "review") {
|
||||
state.ready = false;
|
||||
}
|
||||
f.save(state);
|
||||
const result = f.run(false, f.repo, fault === "merge" ? "merge" : "squash", "", "", body);
|
||||
expect(result.status, result.output).not.toBe(0);
|
||||
|
|
@ -55,60 +52,18 @@ describePosix("native merge outcome with real Git and supervised lock recovery",
|
|||
},
|
||||
);
|
||||
|
||||
it.each([
|
||||
{
|
||||
route: "immediate",
|
||||
access: "external",
|
||||
auto: false,
|
||||
admin: false,
|
||||
queue: false,
|
||||
mergeStateStatus: "CLEAN",
|
||||
},
|
||||
{
|
||||
route: "auto",
|
||||
access: "unknown",
|
||||
auto: true,
|
||||
admin: false,
|
||||
queue: false,
|
||||
mergeStateStatus: "BEHIND",
|
||||
},
|
||||
{
|
||||
route: "queue",
|
||||
access: "external",
|
||||
auto: false,
|
||||
admin: false,
|
||||
queue: true,
|
||||
mergeStateStatus: "CLEAN",
|
||||
},
|
||||
{
|
||||
route: "admin",
|
||||
access: "unknown",
|
||||
auto: false,
|
||||
admin: true,
|
||||
queue: false,
|
||||
mergeStateStatus: "BLOCKED",
|
||||
},
|
||||
])(
|
||||
"blocks rewritten $access squash before $route intent",
|
||||
({ access, auto, admin, queue, mergeStateStatus }) => {
|
||||
const f = fixture();
|
||||
f.setPrivacyProvenance("true", access);
|
||||
f.save({
|
||||
...f.state(),
|
||||
admin,
|
||||
gates: admin ? "fail" : "pass",
|
||||
pr: { ...f.state().pr, isMergeQueueEnabled: queue, mergeStateStatus },
|
||||
});
|
||||
it.each(["external", "unknown"])("blocks rewritten %s squash before intent", (access) => {
|
||||
const f = fixture();
|
||||
f.setPrivacyProvenance("true", access);
|
||||
|
||||
const run = f.run(auto);
|
||||
const run = f.run();
|
||||
|
||||
expect(run.status, run.output).toBe(1);
|
||||
expect(run.output).toContain("maintainer-owned replacement PR");
|
||||
expect(f.state().mutations).toBe(0);
|
||||
expect(f.captures()).toEqual([]);
|
||||
expect(() => f.record()).toThrow();
|
||||
},
|
||||
);
|
||||
expect(run.status, run.output).toBe(1);
|
||||
expect(run.output).toContain("maintainer-owned replacement PR");
|
||||
expect(f.state().mutations).toBe(0);
|
||||
expect(f.captures()).toEqual([]);
|
||||
expect(() => f.record()).toThrow();
|
||||
});
|
||||
|
||||
it.each([
|
||||
{ rewrite: "true", access: "maintainer" },
|
||||
|
|
@ -126,11 +81,6 @@ describePosix("native merge outcome with real Git and supervised lock recovery",
|
|||
|
||||
it.each([
|
||||
["missing", "PREP_REPLACED_HOSTED_ANCESTRY=false\n"],
|
||||
[
|
||||
"malformed rewrite",
|
||||
"PREP_REPLACED_HOSTED_ANCESTRY=false",
|
||||
"PREP_REPLACED_HOSTED_ANCESTRY=yes",
|
||||
],
|
||||
["malformed access", "PREP_AUTHOR_ACCESS=external", "PREP_AUTHOR_ACCESS=write"],
|
||||
])("requires prepare rerun for %s squash provenance", (_label, from, to = "") => {
|
||||
const f = fixture();
|
||||
|
|
@ -145,11 +95,11 @@ describePosix("native merge outcome with real Git and supervised lock recovery",
|
|||
expect(() => f.record()).toThrow();
|
||||
});
|
||||
|
||||
it.each(["merge", "rebase"])("leaves %s mechanics independent of squash provenance", (method) => {
|
||||
it("leaves merge mechanics independent of squash provenance", () => {
|
||||
const f = fixture();
|
||||
f.setPrivacyProvenance(null, null);
|
||||
|
||||
const run = f.run(false, f.repo, method);
|
||||
const run = f.run(false, f.repo, "merge");
|
||||
|
||||
expect(run.status, run.output).toBe(0);
|
||||
expect(f.state().mutations).toBe(1);
|
||||
|
|
@ -170,14 +120,9 @@ describePosix("native merge outcome with real Git and supervised lock recovery",
|
|||
});
|
||||
|
||||
it.each([
|
||||
{ auto: false, admin: false, mergeState: "CLEAN", route: "immediate" },
|
||||
{ auto: false, admin: false, mergeState: "HAS_HOOKS", route: "immediate" },
|
||||
{ auto: false, admin: false, mergeState: "UNSTABLE", route: "immediate" },
|
||||
{ auto: false, admin: true, mergeState: "CLEAN", route: "admin" },
|
||||
{ auto: false, admin: true, mergeState: "BLOCKED", route: "admin" },
|
||||
{ auto: false, admin: true, mergeState: "BEHIND", route: "admin" },
|
||||
{ auto: true, admin: false, mergeState: "BEHIND", route: "auto" },
|
||||
{ auto: true, admin: false, mergeState: "BLOCKED", route: "auto" },
|
||||
{ auto: true, admin: false, mergeState: "CLEAN", route: "immediate", pendingGates: true },
|
||||
])(
|
||||
"submits verified attribution with pinned head for %j",
|
||||
|
|
|
|||
|
|
@ -63,13 +63,7 @@ function correctionFixture() {
|
|||
}
|
||||
|
||||
describePosix("correction authority through native merge admission", () => {
|
||||
it("merges an exactly reviewed and qualified correction while retaining original NEEDS WORK", () => {
|
||||
const f = correctionFixture();
|
||||
const result = f.run();
|
||||
expect(result.status, result.output).toBe(0);
|
||||
expect(f.state().mutations).toBe(1);
|
||||
});
|
||||
it.each(["correction-review.json", "prep-context.env", "gates.env", "prep.env", "pr-meta.env"])(
|
||||
it.each(["correction-review.json", "prep-context.env", "gates.env"])(
|
||||
"refuses changed %s after CI checks and before intent",
|
||||
(artifact) => {
|
||||
const f = correctionFixture();
|
||||
|
|
|
|||
|
|
@ -101,7 +101,6 @@ describePosix("native merge outcome with real Git and supervised lock recovery",
|
|||
});
|
||||
|
||||
it.each([
|
||||
"empty",
|
||||
"timeout",
|
||||
"extra-capture",
|
||||
"wrong-hash",
|
||||
|
|
@ -109,10 +108,7 @@ describePosix("native merge outcome with real Git and supervised lock recovery",
|
|||
"wrong-base",
|
||||
"missing-proof",
|
||||
"symlink",
|
||||
"auto",
|
||||
"queue",
|
||||
"closed",
|
||||
"pending",
|
||||
"ci-proof",
|
||||
"changed-capture",
|
||||
"no-head",
|
||||
|
|
@ -122,8 +118,8 @@ describePosix("native merge outcome with real Git and supervised lock recovery",
|
|||
const approvedHead = f.replacePreparedHead();
|
||||
const next = f.state();
|
||||
const capture = join(f.worktree, ".local/merge-output.log");
|
||||
if (fault === "empty" || fault === "timeout") {
|
||||
const text = fault === "empty" ? "" : "502 after dispatch\n";
|
||||
if (fault === "timeout") {
|
||||
const text = "502 after dispatch\n";
|
||||
writeFileSync(capture, text);
|
||||
writeFileSync(join(legacy.directory, "merge-output.log"), text);
|
||||
legacy.oid = f.git(["hash-object", "--no-filters", capture]);
|
||||
|
|
@ -151,18 +147,9 @@ describePosix("native merge outcome with real Git and supervised lock recovery",
|
|||
rmSync(capture);
|
||||
symlinkSync(join(legacy.directory, "merge-output.log"), capture);
|
||||
}
|
||||
if (fault === "auto") {
|
||||
next.pr.autoMergeRequest = { mergeMethod: "SQUASH" };
|
||||
}
|
||||
if (fault === "queue") {
|
||||
next.pr.isMergeQueueEnabled = true;
|
||||
}
|
||||
if (fault === "closed") {
|
||||
next.pr.state = "CLOSED";
|
||||
}
|
||||
if (fault === "pending") {
|
||||
next.gates = "pending";
|
||||
}
|
||||
if (fault === "ci-proof") {
|
||||
next.ciExit = 15;
|
||||
}
|
||||
|
|
|
|||
|
|
@ -45,10 +45,27 @@ const hash = (text: string) =>
|
|||
execFileSync("git", ["hash-object", "--stdin"], { input: text, encoding: "utf8" }).trim();
|
||||
const describePosix = process.platform === "win32" ? describe.skip : describe;
|
||||
|
||||
function refusalEvidence(contents: string, proof: unknown) {
|
||||
const root = temps.make("pr-refusal-evidence-");
|
||||
const directory = join(root, "evidence");
|
||||
mkdirSync(directory);
|
||||
mkdirSync(join(root, ".local"));
|
||||
writeFileSync(join(root, ".local", capture), contents);
|
||||
writeFileSync(join(directory, capture), contents);
|
||||
writeFileSync(join(directory, "qualification.json"), JSON.stringify(proof));
|
||||
return {
|
||||
root,
|
||||
directory,
|
||||
run: (attempt = record) =>
|
||||
spawnSync(node, [helper, directory, outcome, JSON.stringify(attempt)], {
|
||||
cwd: root,
|
||||
encoding: "utf8",
|
||||
}),
|
||||
};
|
||||
}
|
||||
|
||||
describePosix("operator-qualified pre-dispatch evidence", () => {
|
||||
it.each([
|
||||
"approved",
|
||||
"uninspected",
|
||||
"diagnostics-not-requested",
|
||||
"wrong-producer",
|
||||
"wrong-command",
|
||||
|
|
@ -57,20 +74,13 @@ describePosix("operator-qualified pre-dispatch evidence", () => {
|
|||
"wrong-executable",
|
||||
"missing-source",
|
||||
"extra-source",
|
||||
...Object.keys(policyTimeoutQualification.sourceSha256).map((path) => `source:${path}`),
|
||||
"policy-denial",
|
||||
"source:cmd/octopool/string_rewrites_pr.go",
|
||||
"other-class",
|
||||
"extra-text",
|
||||
"extra-newline",
|
||||
"missing-newline",
|
||||
"diagnostic",
|
||||
"child-started",
|
||||
"response-headers",
|
||||
])("bounds the inspected initial policy timeout: %s", (fault) => {
|
||||
const root = temps.make("pr-policy-timeout-evidence-");
|
||||
const directory = join(root, "evidence");
|
||||
mkdirSync(directory);
|
||||
mkdirSync(join(root, ".local"));
|
||||
const sourceSha256: Record<string, string> = { ...policyTimeoutQualification.sourceSha256 };
|
||||
if (fault.startsWith("source:")) {
|
||||
sourceSha256[fault.slice("source:".length)] = "0".repeat(64);
|
||||
|
|
@ -81,36 +91,19 @@ describePosix("operator-qualified pre-dispatch evidence", () => {
|
|||
if (fault === "extra-source") {
|
||||
sourceSha256["cmd/octopool/unknown.go"] = "0".repeat(64);
|
||||
}
|
||||
let contents = policyTimeoutCapture;
|
||||
if (fault === "policy-denial") {
|
||||
contents = refusal;
|
||||
}
|
||||
if (fault === "other-class") {
|
||||
contents = contents.replace("class=timeout", "class=server_validation");
|
||||
}
|
||||
if (fault === "extra-text") {
|
||||
contents += "mutation accepted\n";
|
||||
}
|
||||
if (fault === "extra-newline") {
|
||||
contents += "\n";
|
||||
}
|
||||
if (fault === "missing-newline") {
|
||||
contents = contents.trimEnd();
|
||||
}
|
||||
if (fault === "diagnostic" || fault === "child-started") {
|
||||
contents +=
|
||||
fault === "diagnostic"
|
||||
? diagnostic
|
||||
: diagnostic.replace("child_started=false", "child_started=true");
|
||||
}
|
||||
if (fault === "response-headers") {
|
||||
contents = contents.replace(")\n", " http_status=504)\n");
|
||||
}
|
||||
const captures: Record<string, string> = {
|
||||
"other-class": policyTimeoutCapture.replace("class=timeout", "class=server_validation"),
|
||||
"extra-newline": policyTimeoutCapture + "\n",
|
||||
"missing-newline": policyTimeoutCapture.trimEnd(),
|
||||
diagnostic: policyTimeoutCapture + diagnostic,
|
||||
"response-headers": policyTimeoutCapture.replace(")\n", " http_status=504)\n"),
|
||||
};
|
||||
const contents = captures[fault] ?? policyTimeoutCapture;
|
||||
const proof = {
|
||||
...policyTimeoutQualification,
|
||||
outcome,
|
||||
capture: hash(contents),
|
||||
inspected: fault !== "uninspected",
|
||||
inspected: true,
|
||||
diagnosticsEnabled: fault !== "diagnostics-not-requested",
|
||||
producer: fault === "wrong-producer" ? "gh" : policyTimeoutQualification.producer,
|
||||
command: fault === "wrong-command" ? "pr view" : policyTimeoutQualification.command,
|
||||
|
|
@ -121,38 +114,20 @@ describePosix("operator-qualified pre-dispatch evidence", () => {
|
|||
fault === "wrong-executable" ? "a".repeat(64) : policyTimeoutQualification.executableSha256,
|
||||
sourceSha256,
|
||||
};
|
||||
writeFileSync(join(root, ".local", capture), contents);
|
||||
writeFileSync(join(directory, capture), contents);
|
||||
writeFileSync(join(directory, "qualification.json"), JSON.stringify(proof));
|
||||
const result = spawnSync(node, [helper, directory, outcome, JSON.stringify(record)], {
|
||||
cwd: root,
|
||||
encoding: "utf8",
|
||||
});
|
||||
if (fault === "approved") {
|
||||
expect(result.status, result.stderr).toBe(0);
|
||||
expect(JSON.parse(result.stdout)).toMatchObject({
|
||||
kind: proof.kind,
|
||||
capture,
|
||||
files: { [capture]: proof.capture },
|
||||
});
|
||||
} else {
|
||||
expect(result.status, result.stderr).toBe(1);
|
||||
expect(result.stdout).toBe("");
|
||||
}
|
||||
const result = refusalEvidence(contents, proof).run();
|
||||
expect(result.status, result.stderr).toBe(1);
|
||||
expect(result.stdout).toBe("");
|
||||
});
|
||||
|
||||
it.each([
|
||||
"historical",
|
||||
"historical-0.7.1",
|
||||
"0.7.1-wrong-version",
|
||||
"0.7.1-wrong-source",
|
||||
"0.7.1-wrong-parser",
|
||||
"0.7.1-subject",
|
||||
"0.7.1-subject-equals",
|
||||
"0.7.1-altered-stderr",
|
||||
"diagnostic",
|
||||
"generic-only",
|
||||
"wrong-source",
|
||||
"changed-capture",
|
||||
"symlink",
|
||||
"accepted",
|
||||
|
|
@ -163,10 +138,6 @@ describePosix("operator-qualified pre-dispatch evidence", () => {
|
|||
"extra-evidence",
|
||||
"wrong-outcome",
|
||||
])("qualifies only intact, inspected no-dispatch evidence: %s", (fault) => {
|
||||
const root = temps.make("pr-refusal-evidence-");
|
||||
const directory = join(root, "evidence");
|
||||
mkdirSync(directory);
|
||||
mkdirSync(join(root, ".local"));
|
||||
const diagnostics = ["diagnostic", "started", "duplicate"].includes(fault);
|
||||
const historical =
|
||||
historicalRefusals[
|
||||
|
|
@ -195,9 +166,6 @@ describePosix("operator-qualified pre-dispatch evidence", () => {
|
|||
"--body-file",
|
||||
".local/merge-body.fixture",
|
||||
];
|
||||
if (fault === "0.7.1-subject") {
|
||||
args.push("--subject", "Fixture subject");
|
||||
}
|
||||
if (fault === "0.7.1-subject-equals") {
|
||||
args.push("--subject=Fixture subject");
|
||||
}
|
||||
|
|
@ -212,16 +180,14 @@ describePosix("operator-qualified pre-dispatch evidence", () => {
|
|||
version: fault === "0.7.1-wrong-version" ? "0.7.0" : historical.version,
|
||||
sourceRevision:
|
||||
fault === "0.7.1-wrong-source" ? "a".repeat(40) : historical.sourceRevision,
|
||||
parserSha256:
|
||||
fault === "wrong-source" || fault === "0.7.1-wrong-parser"
|
||||
? "a".repeat(64)
|
||||
: historical.parserSha256,
|
||||
parserSha256: fault === "0.7.1-wrong-parser" ? "a".repeat(64) : historical.parserSha256,
|
||||
args,
|
||||
}),
|
||||
};
|
||||
writeFileSync(join(root, ".local", capture), contents);
|
||||
writeFileSync(join(directory, capture), fault === "changed-capture" ? "changed\n" : contents);
|
||||
writeFileSync(join(directory, "qualification.json"), JSON.stringify(proof));
|
||||
const { root, directory, run } = refusalEvidence(contents, proof);
|
||||
if (fault === "changed-capture") {
|
||||
writeFileSync(join(directory, capture), "changed\n");
|
||||
}
|
||||
if (fault === "symlink") {
|
||||
rmSync(join(directory, capture));
|
||||
symlinkSync(join(root, ".local", capture), join(directory, capture));
|
||||
|
|
@ -237,11 +203,8 @@ describePosix("operator-qualified pre-dispatch evidence", () => {
|
|||
accepted: fault === "accepted",
|
||||
route: fault === "queue" ? "queue" : record.route,
|
||||
};
|
||||
const result = spawnSync(node, [helper, directory, outcome, JSON.stringify(attempt)], {
|
||||
cwd: root,
|
||||
encoding: "utf8",
|
||||
});
|
||||
if (fault === "historical" || fault === "historical-0.7.1" || fault === "diagnostic") {
|
||||
const result = run(attempt);
|
||||
if (fault === "historical-0.7.1" || fault === "diagnostic") {
|
||||
expect(result.status, result.stderr).toBe(0);
|
||||
expect(JSON.parse(result.stdout)).toMatchObject({
|
||||
kind: proof.kind,
|
||||
|
|
|
|||
|
|
@ -9,24 +9,9 @@ import {
|
|||
|
||||
const { fixture, outcomeRef, describePosix } = createMergeOutcomeFixtureHarness();
|
||||
|
||||
const historicalRefusals = {
|
||||
"0.6.10": {
|
||||
kind: "octopool-0.6.10-auto-refusal",
|
||||
version: "0.6.10",
|
||||
sourceRevision: "00c442d8084ad26eb5a5003f7372170e75a20c8a",
|
||||
parserSha256: "f6ff8cd7e59503f71f94fefd561b671193df11b3aac9ba0986a0dc3ba91ca32b",
|
||||
},
|
||||
"0.7.1": {
|
||||
kind: "octopool-0.7.1-missing-subject-refusal",
|
||||
version: "0.7.1",
|
||||
sourceRevision: "7ab9b348c99a7be4fdc82c75cb06ebce44e0007e",
|
||||
parserSha256: "b32cb960537f5ffa1336a7689674afba9b4a2485b05e449acd2684a251ff8970",
|
||||
},
|
||||
} as const;
|
||||
|
||||
function qualifiedAutoRefusal(
|
||||
f: ReturnType<typeof fixture>,
|
||||
version: keyof typeof historicalRefusals | "policy-timeout" = "0.6.10",
|
||||
version: "0.6.10" | "policy-timeout" = "0.6.10",
|
||||
) {
|
||||
f.save({
|
||||
...f.state(),
|
||||
|
|
@ -49,7 +34,10 @@ function qualifiedAutoRefusal(
|
|||
...(version === "policy-timeout"
|
||||
? policyTimeoutQualification
|
||||
: {
|
||||
...historicalRefusals[version],
|
||||
kind: "octopool-0.6.10-auto-refusal",
|
||||
version,
|
||||
sourceRevision: "00c442d8084ad26eb5a5003f7372170e75a20c8a",
|
||||
parserSha256: "f6ff8cd7e59503f71f94fefd561b671193df11b3aac9ba0986a0dc3ba91ca32b",
|
||||
args: [
|
||||
"pr",
|
||||
"merge",
|
||||
|
|
@ -68,14 +56,18 @@ function qualifiedAutoRefusal(
|
|||
writeFileSync(join(directory, "qualification.json"), JSON.stringify(qualification));
|
||||
f.recover();
|
||||
f.save({ ...f.state(), mode: "success", pr: { ...f.state().pr, mergeStateStatus: "CLEAN" } });
|
||||
return { outcome, directory, capture, qualification };
|
||||
return {
|
||||
outcome,
|
||||
capture,
|
||||
qualification,
|
||||
runRecovery: (replacement = "") =>
|
||||
f.run(false, f.repo, "squash", outcome, replacement, "", "", "", false, directory),
|
||||
};
|
||||
}
|
||||
|
||||
describePosix("qualified pre-dispatch merge recovery", () => {
|
||||
it.each([
|
||||
{ version: "0.6.10", replaceHead: false },
|
||||
{ version: "0.6.10", replaceHead: true },
|
||||
{ version: "0.7.1", replaceHead: true },
|
||||
{ version: "policy-timeout", replaceHead: true },
|
||||
] as const)(
|
||||
"recovers a qualified $version pre-dispatch refusal with retained evidence (replacement=$replaceHead)",
|
||||
|
|
@ -89,18 +81,7 @@ describePosix("qualified pre-dispatch merge recovery", () => {
|
|||
`PR_NUMBER=123\nGATES_MODE=github_pending\nHOSTED_GATES_TARGET_HEAD_SHA=${preparedHead}\n`,
|
||||
);
|
||||
f.save({ ...f.state(), requiredCheckName: "openclaw/ci-gate" });
|
||||
const run = f.run(
|
||||
false,
|
||||
f.repo,
|
||||
"squash",
|
||||
proof.outcome,
|
||||
replacement,
|
||||
"",
|
||||
"",
|
||||
"",
|
||||
false,
|
||||
proof.directory,
|
||||
);
|
||||
const run = proof.runRecovery(replacement);
|
||||
expect(run.status, run.output).toBe(0);
|
||||
expect(f.state().mutations).toBe(1);
|
||||
expect(f.record()).toMatchObject({
|
||||
|
|
@ -125,18 +106,7 @@ describePosix("qualified pre-dispatch merge recovery", () => {
|
|||
);
|
||||
expect(f.run().status).toBe(0);
|
||||
expect(f.state().mutations).toBe(1);
|
||||
const replay = f.run(
|
||||
false,
|
||||
f.repo,
|
||||
"squash",
|
||||
proof.outcome,
|
||||
replacement,
|
||||
"",
|
||||
"",
|
||||
"",
|
||||
false,
|
||||
proof.directory,
|
||||
);
|
||||
const replay = proof.runRecovery(replacement);
|
||||
expect(replay.status, replay.output).toBe(1);
|
||||
expect(f.state().mutations).toBe(1);
|
||||
},
|
||||
|
|
@ -176,18 +146,7 @@ describePosix("qualified pre-dispatch merge recovery", () => {
|
|||
next.duringChecks = { preparedHead: movedHead };
|
||||
}
|
||||
f.save(next);
|
||||
const run = f.run(
|
||||
false,
|
||||
f.repo,
|
||||
"squash",
|
||||
proof.outcome,
|
||||
"",
|
||||
"",
|
||||
"",
|
||||
"",
|
||||
false,
|
||||
proof.directory,
|
||||
);
|
||||
const run = proof.runRecovery();
|
||||
expect(run.error, run.output).toBeUndefined();
|
||||
expect(run.status, run.output).toBe(1);
|
||||
expect(f.state()).toMatchObject({ mutations: 0, posts: 0 });
|
||||
|
|
@ -226,18 +185,7 @@ describePosix("qualified pre-dispatch merge recovery", () => {
|
|||
next.pr.mergeStateStatus = "BEHIND";
|
||||
}
|
||||
f.save(next);
|
||||
const run = f.run(
|
||||
false,
|
||||
f.repo,
|
||||
"squash",
|
||||
proof.outcome,
|
||||
replacement,
|
||||
"",
|
||||
"",
|
||||
"",
|
||||
false,
|
||||
proof.directory,
|
||||
);
|
||||
const run = proof.runRecovery(replacement);
|
||||
expect(run.status, `${fault}: ${run.output}`).toBe(1);
|
||||
expect(f.state().mutations).toBe(0);
|
||||
expect(f.git(["rev-parse", outcomeRef])).toBe(proof.outcome);
|
||||
|
|
|
|||
|
|
@ -128,25 +128,19 @@ describe("security review rollout", () => {
|
|||
expect(result.requests).toEqual([`/repos/openclaw/openclaw/pulls/${rolloutNumber}`]);
|
||||
});
|
||||
|
||||
it.each(["2026-09-19T12:00:00Z", "2026-09-20T00:00:00Z"])(
|
||||
"enforces PRs created at or after rollout (%s), even on an old branch",
|
||||
(createdAt) => {
|
||||
const result = evaluate({ pullRequest: { ...pullRequest, created_at: createdAt } });
|
||||
expect(result.mode).toBe("enforced");
|
||||
expect(result.requests).toHaveLength(1);
|
||||
},
|
||||
);
|
||||
it("enforces PRs created at rollout, even on an old branch", () => {
|
||||
const result = evaluate({ pullRequest: { ...pullRequest, created_at: mergedAt } });
|
||||
expect(result.mode).toBe("enforced");
|
||||
expect(result.requests).toHaveLength(1);
|
||||
});
|
||||
|
||||
it.each(["behind", "diverged"])(
|
||||
"exempts an older PR whose head has not incorporated the rollout (%s)",
|
||||
(status) => {
|
||||
const result = evaluate({ comparison: { ...comparison, status } });
|
||||
expect(result.mode).toBe("grandfathered");
|
||||
expect(result.requests[1]).toBe(
|
||||
`/repos/openclaw/openclaw/compare/${mergeCommit}...${head}?per_page=1&page=2`,
|
||||
);
|
||||
},
|
||||
);
|
||||
it("exempts an older PR whose head is behind the rollout", () => {
|
||||
const result = evaluate({ comparison: { ...comparison, status: "behind" } });
|
||||
expect(result.mode).toBe("grandfathered");
|
||||
expect(result.requests[1]).toBe(
|
||||
`/repos/openclaw/openclaw/compare/${mergeCommit}...${head}?per_page=1&page=2`,
|
||||
);
|
||||
});
|
||||
|
||||
it("enforces an older PR after a rebase or merge incorporates the rollout", () => {
|
||||
const result = evaluate({
|
||||
|
|
@ -181,12 +175,10 @@ describe("security review rollout", () => {
|
|||
{ ...rollout, number: 1 },
|
||||
{ ...rollout, base: { ref: "stable", repo: { full_name: "openclaw/openclaw" } } },
|
||||
{ ...rollout, base: { ref: "main", repo: { full_name: "contributor/fork" } } },
|
||||
{ ...rollout, merged: undefined },
|
||||
{ ...rollout, merged: false },
|
||||
{ ...rollout, state: "open" },
|
||||
{ ...rollout, merged_at: null },
|
||||
{ ...rollout, merged_at: "2026-02-30T12:00:00Z" },
|
||||
{ ...rollout, merge_commit_sha: "main" },
|
||||
])("does not grant an exemption for invalid rollout metadata (%j)", (value) => {
|
||||
const result = evaluate({ rollout: value });
|
||||
expect(result.status).toBe(1);
|
||||
|
|
@ -216,12 +208,9 @@ describe("security review rollout", () => {
|
|||
expect(result.error).toContain("Cannot determine security review rollout");
|
||||
});
|
||||
|
||||
it.each(["rollout", "comparison"] as const)(
|
||||
"does not convert a GitHub %s error into an exemption",
|
||||
(apiError) => {
|
||||
const result = evaluate({ apiError });
|
||||
expect(result.status).toBe(1);
|
||||
expect(result.error).toBe("GitHub unavailable");
|
||||
},
|
||||
);
|
||||
it("does not convert a GitHub comparison error into an exemption", () => {
|
||||
const result = evaluate({ apiError: "comparison" });
|
||||
expect(result.status).toBe(1);
|
||||
expect(result.error).toBe("GitHub unavailable");
|
||||
});
|
||||
});
|
||||
|
|
|
|||
|
|
@ -63,6 +63,26 @@ const otherReview = {
|
|||
creator: { login: "github-actions[bot]", type: "Bot" },
|
||||
};
|
||||
|
||||
const lockfilePr = {
|
||||
...pr,
|
||||
changed_files: 1,
|
||||
head: { ...pr.head, repo: { id: 1, full_name: "openclaw/openclaw" } },
|
||||
};
|
||||
const lockfileContents = {
|
||||
type: "file",
|
||||
encoding: "base64",
|
||||
content: Buffer.from("lockfileVersion: '9.0'\n").toString("base64"),
|
||||
};
|
||||
const lockfileRoutes = {
|
||||
[`GET ${pullPath}/files`]: [{ filename: "pnpm-lock.yaml", status: "modified" }],
|
||||
[rolePath]: { role_name: "read" },
|
||||
[`GET /repos/openclaw/openclaw/dependency-graph/compare/${pr.base.sha}...${head}`]: [],
|
||||
[`GET /repos/openclaw/openclaw/compare/${pr.base.sha}...${head}`]: {
|
||||
base_commit: { sha: pr.base.sha },
|
||||
merge_base_commit: { sha: pr.base.sha },
|
||||
},
|
||||
};
|
||||
|
||||
function evaluate(routes: Record<string, unknown> = {}, mode = "enforce", deadline?: number) {
|
||||
const root = tempDirs.make("security-review-");
|
||||
const logPath = path.join(root, "requests.jsonl");
|
||||
|
|
@ -148,20 +168,9 @@ function evaluate(routes: Record<string, unknown> = {}, mode = "enforce", deadli
|
|||
}
|
||||
|
||||
describe("combined security review entry point", () => {
|
||||
it("recovers a temporary HTTP 500 without failing the review", () => {
|
||||
const result = evaluate({
|
||||
[`GET ${pullPath}`]: { responses: [{ httpError: 500 }, pr] },
|
||||
});
|
||||
expect(result.status, result.stderr).toBe(0);
|
||||
expect(result.waits).toEqual([1_000]);
|
||||
expect(result.combined.at(-1)).toBe("success");
|
||||
expect(result.reviews.filter((entry) => entry.body?.state === "success")).toHaveLength(2);
|
||||
});
|
||||
|
||||
it.each([
|
||||
{ route: `GET ${pullPath}`, response: pr, requestTimeout: "fetch" },
|
||||
{ route: rolePath, response: { role_name: "maintain" }, requestTimeout: "body" },
|
||||
{ route: jobsPath, response: jobs, requestTimeout: "fetch" },
|
||||
])(
|
||||
"restarts evaluation after a $requestTimeout deadline on $route",
|
||||
({ route, response, requestTimeout }) => {
|
||||
|
|
@ -213,27 +222,10 @@ describe("combined security review entry point", () => {
|
|||
it("recovers a lockfile read deadline before submitting one cleanup commit", () => {
|
||||
const result = evaluate(
|
||||
{
|
||||
[`GET ${pullPath}`]: {
|
||||
...pr,
|
||||
changed_files: 1,
|
||||
head: { ...pr.head, repo: { id: 1, full_name: "openclaw/openclaw" } },
|
||||
},
|
||||
[`GET ${pullPath}/files`]: [{ filename: "pnpm-lock.yaml", status: "modified" }],
|
||||
[rolePath]: { role_name: "read" },
|
||||
[`GET /repos/openclaw/openclaw/dependency-graph/compare/${pr.base.sha}...${head}`]: [],
|
||||
[`GET /repos/openclaw/openclaw/compare/${pr.base.sha}...${head}`]: {
|
||||
base_commit: { sha: pr.base.sha },
|
||||
merge_base_commit: { sha: pr.base.sha },
|
||||
},
|
||||
...lockfileRoutes,
|
||||
[`GET ${pullPath}`]: lockfilePr,
|
||||
[`GET /repos/openclaw/openclaw/contents/pnpm-lock.yaml?ref=${pr.base.sha}`]: {
|
||||
responses: [
|
||||
{ requestTimeout: "fetch" },
|
||||
{
|
||||
type: "file",
|
||||
encoding: "base64",
|
||||
content: Buffer.from("lockfileVersion: '9.0'\n").toString("base64"),
|
||||
},
|
||||
],
|
||||
responses: [{ requestTimeout: "fetch" }, lockfileContents],
|
||||
},
|
||||
"POST /graphql": { data: { createCommitOnBranch: { commit: { oid: "e".repeat(40) } } } },
|
||||
},
|
||||
|
|
@ -268,8 +260,6 @@ describe("combined security review entry point", () => {
|
|||
unavailable: true,
|
||||
},
|
||||
{ name: "unknown HTTP 403", response: { httpError: 403 }, unavailable: false },
|
||||
{ name: "invalid credentials", response: { httpError: 401 }, unavailable: false },
|
||||
{ name: "missing repository", response: { httpError: 404 }, unavailable: false },
|
||||
{
|
||||
name: "stale head",
|
||||
response: { errors: [{ type: "STALE_DATA", message: "Expected branch head to match" }] },
|
||||
|
|
@ -307,26 +297,15 @@ describe("combined security review entry point", () => {
|
|||
const { response, unavailable } = scenario;
|
||||
const baseReadDenied = "baseReadDenied" in scenario && scenario.baseReadDenied;
|
||||
const routes = {
|
||||
...lockfileRoutes,
|
||||
[`GET ${pullPath}`]: {
|
||||
...pr,
|
||||
changed_files: 1,
|
||||
...lockfilePr,
|
||||
maintainer_can_modify: true,
|
||||
head: { ...pr.head, repo: { id: 2, full_name: "contributor/openclaw" } },
|
||||
},
|
||||
[`GET ${pullPath}/files`]: [{ filename: "pnpm-lock.yaml", status: "modified" }],
|
||||
[rolePath]: { role_name: "read" },
|
||||
[`GET /repos/openclaw/openclaw/dependency-graph/compare/${pr.base.sha}...${head}`]: [],
|
||||
[`GET /repos/openclaw/openclaw/compare/${pr.base.sha}...${head}`]: {
|
||||
base_commit: { sha: pr.base.sha },
|
||||
merge_base_commit: { sha: pr.base.sha },
|
||||
},
|
||||
[`GET /repos/openclaw/openclaw/contents/pnpm-lock.yaml`]: baseReadDenied
|
||||
? response
|
||||
: {
|
||||
type: "file",
|
||||
encoding: "base64",
|
||||
content: Buffer.from("lockfileVersion: '9.0'\n").toString("base64"),
|
||||
},
|
||||
: lockfileContents,
|
||||
"POST /graphql": response,
|
||||
};
|
||||
const cleanup = evaluate(routes, "autoscrub");
|
||||
|
|
@ -357,34 +336,19 @@ describe("combined security review entry point", () => {
|
|||
});
|
||||
|
||||
it("preserves a failed cleanup mutation when the PR then closes", () => {
|
||||
const cleanupPr = {
|
||||
...pr,
|
||||
changed_files: 1,
|
||||
head: { ...pr.head, repo: { id: 1, full_name: "openclaw/openclaw" } },
|
||||
};
|
||||
const result = evaluate(
|
||||
{
|
||||
...lockfileRoutes,
|
||||
[`GET ${pullPath}`]: {
|
||||
responses: [
|
||||
cleanupPr,
|
||||
cleanupPr,
|
||||
cleanupPr,
|
||||
cleanupPr,
|
||||
{ ...cleanupPr, state: "closed" },
|
||||
lockfilePr,
|
||||
lockfilePr,
|
||||
lockfilePr,
|
||||
lockfilePr,
|
||||
{ ...lockfilePr, state: "closed" },
|
||||
],
|
||||
},
|
||||
[`GET ${pullPath}/files`]: [{ filename: "pnpm-lock.yaml", status: "modified" }],
|
||||
[rolePath]: { role_name: "read" },
|
||||
[`GET /repos/openclaw/openclaw/dependency-graph/compare/${pr.base.sha}...${head}`]: [],
|
||||
[`GET /repos/openclaw/openclaw/compare/${pr.base.sha}...${head}`]: {
|
||||
base_commit: { sha: pr.base.sha },
|
||||
merge_base_commit: { sha: pr.base.sha },
|
||||
},
|
||||
[`GET /repos/openclaw/openclaw/contents/pnpm-lock.yaml`]: {
|
||||
type: "file",
|
||||
encoding: "base64",
|
||||
content: Buffer.from("lockfileVersion: '9.0'\n").toString("base64"),
|
||||
},
|
||||
[`GET /repos/openclaw/openclaw/contents/pnpm-lock.yaml`]: lockfileContents,
|
||||
"POST /graphql": { httpError: 500 },
|
||||
},
|
||||
"autoscrub",
|
||||
|
|
@ -444,32 +408,6 @@ describe("combined security review entry point", () => {
|
|||
expect(result.combined).not.toContain("success");
|
||||
});
|
||||
|
||||
it("exhausts HTTP 500 retries without publishing approval", () => {
|
||||
const result = evaluate({ [`GET ${pullPath}`]: { httpError: 500 } });
|
||||
expect(result.status).toBe(1);
|
||||
expect(result.waits).toEqual([1_000, 2_000, 4_000]);
|
||||
expect(result.requests.filter((entry) => entry.path === pullPath)).toHaveLength(4);
|
||||
expect(result.stderr).toContain(`GitHub API GET ${pullPath} failed: 500`);
|
||||
expect(
|
||||
result.requests.every((entry) => entry.method === "GET" || entry.method === "WAIT"),
|
||||
).toBe(true);
|
||||
});
|
||||
|
||||
it.each([
|
||||
{ name: "partial file list", initialPr: pr, initialFiles: files.slice(0, 1) },
|
||||
{ name: "stale file count", initialPr: { ...pr, changed_files: 3 }, initialFiles: files },
|
||||
])("recovers a $name before evaluating either guard", ({ initialPr, initialFiles }) => {
|
||||
const result = evaluate({
|
||||
[`GET ${pullPath}`]: { responses: [initialPr, pr] },
|
||||
[`GET ${pullPath}/files`]: { responses: [initialFiles, files] },
|
||||
});
|
||||
expect(result.status, result.stderr).toBe(0);
|
||||
expect(result.waits).toEqual([30_000, 30_000]);
|
||||
expect(result.requests.filter((entry) => entry.path === `${pullPath}/files`)).toHaveLength(2);
|
||||
expect(result.combined.at(-1)).toBe("success");
|
||||
expect(result.reviews.filter((entry) => entry.body?.state === "success")).toHaveLength(2);
|
||||
});
|
||||
|
||||
it.each(["detect", "enforce"])(
|
||||
"recovers diff data that takes longer than seven seconds to settle in %s mode",
|
||||
(mode) => {
|
||||
|
|
@ -724,22 +662,19 @@ describe("combined security review entry point", () => {
|
|||
},
|
||||
);
|
||||
|
||||
it.each([
|
||||
{ httpError: 500 },
|
||||
{ httpError: 502 },
|
||||
{ httpError: 503 },
|
||||
{ httpError: 504 },
|
||||
{ transportError: "ECONNRESET" },
|
||||
])("restarts evaluation after a transient status publication failure: %j", (failure) => {
|
||||
const result = evaluate({ [statusPath]: { responses: [failure, {}] } });
|
||||
expect(result.status, result.stderr).toBe(0);
|
||||
expect(result.waits).toEqual([1_000]);
|
||||
const afterWait = result.requests.slice(
|
||||
result.requests.findIndex((entry) => entry.method === "WAIT") + 1,
|
||||
);
|
||||
expect(afterWait[0]).toMatchObject({ method: "GET", path: pullPath });
|
||||
expect(result.combined.at(-1)).toBe("success");
|
||||
});
|
||||
it.each([{ httpError: 500 }, { transportError: "ECONNRESET" }])(
|
||||
"restarts evaluation after a transient status publication failure: %j",
|
||||
(failure) => {
|
||||
const result = evaluate({ [statusPath]: { responses: [failure, {}] } });
|
||||
expect(result.status, result.stderr).toBe(0);
|
||||
expect(result.waits).toEqual([1_000]);
|
||||
const afterWait = result.requests.slice(
|
||||
result.requests.findIndex((entry) => entry.method === "WAIT") + 1,
|
||||
);
|
||||
expect(afterWait[0]).toMatchObject({ method: "GET", path: pullPath });
|
||||
expect(result.combined.at(-1)).toBe("success");
|
||||
},
|
||||
);
|
||||
|
||||
it.each([
|
||||
{ httpError: 429 },
|
||||
|
|
@ -1129,29 +1064,14 @@ describe("combined security review entry point", () => {
|
|||
it.each(["before cleanup", "before commit"])(
|
||||
"preserves merged lockfiles %s and still reports their missing approval",
|
||||
(phase) => {
|
||||
const cleanupPr = {
|
||||
...pr,
|
||||
changed_files: 1,
|
||||
head: { ...pr.head, repo: { id: 1, full_name: "openclaw/openclaw" } },
|
||||
};
|
||||
const merged = { ...cleanupPr, state: "closed", merged: true };
|
||||
const merged = { ...lockfilePr, state: "closed", merged: true };
|
||||
const routes = {
|
||||
...lockfileRoutes,
|
||||
[`GET ${pullPath}`]: {
|
||||
responses:
|
||||
phase === "before cleanup" ? [merged] : [cleanupPr, cleanupPr, cleanupPr, merged],
|
||||
},
|
||||
[`GET ${pullPath}/files`]: [{ filename: "pnpm-lock.yaml", status: "modified" }],
|
||||
[rolePath]: { role_name: "read" },
|
||||
[`GET /repos/openclaw/openclaw/dependency-graph/compare/${pr.base.sha}...${head}`]: [],
|
||||
[`GET /repos/openclaw/openclaw/compare/${pr.base.sha}...${head}`]: {
|
||||
base_commit: { sha: pr.base.sha },
|
||||
merge_base_commit: { sha: pr.base.sha },
|
||||
},
|
||||
[`GET /repos/openclaw/openclaw/contents/pnpm-lock.yaml`]: {
|
||||
type: "file",
|
||||
encoding: "base64",
|
||||
content: Buffer.from("lockfileVersion: '9.0'\n").toString("base64"),
|
||||
phase === "before cleanup" ? [merged] : [lockfilePr, lockfilePr, lockfilePr, merged],
|
||||
},
|
||||
[`GET /repos/openclaw/openclaw/contents/pnpm-lock.yaml`]: lockfileContents,
|
||||
};
|
||||
const cleanup = evaluate(routes, "autoscrub");
|
||||
expect(cleanup.status, cleanup.stderr).toBe(0);
|
||||
|
|
@ -1216,15 +1136,12 @@ describe("combined security review entry point", () => {
|
|||
expect(result.combined).not.toContain("success");
|
||||
});
|
||||
|
||||
it.each([undefined, "", "main"])(
|
||||
"does not treat an invalid live head %s as superseded",
|
||||
(sha) => {
|
||||
const result = evaluate({ [`GET ${pullPath}`]: { ...pr, head: { ...pr.head, sha } } });
|
||||
expect(result.status).toBe(1);
|
||||
expect(result.stdout).not.toContain("Superseded");
|
||||
expect(result.requests.some((entry) => entry.method !== "GET")).toBe(false);
|
||||
},
|
||||
);
|
||||
it.each([undefined, "main"])("does not treat an invalid live head %s as superseded", (sha) => {
|
||||
const result = evaluate({ [`GET ${pullPath}`]: { ...pr, head: { ...pr.head, sha } } });
|
||||
expect(result.status).toBe(1);
|
||||
expect(result.stdout).not.toContain("Superseded");
|
||||
expect(result.requests.some((entry) => entry.method !== "GET")).toBe(false);
|
||||
});
|
||||
|
||||
it("accepts the existing exact-head CI fallback without a manual guard run", () => {
|
||||
const fallback = {
|
||||
|
|
@ -1329,7 +1246,6 @@ describe("combined security review entry point", () => {
|
|||
|
||||
it.each([
|
||||
{ status: "completed", conclusion: "failure", expected: "failure" },
|
||||
{ status: "completed", conclusion: "cancelled", expected: "failure" },
|
||||
{ status: "in_progress", conclusion: null, expected: "pending" },
|
||||
])(
|
||||
"keeps newer substantive CI authoritative before a skipped PR workflow: $status/$conclusion",
|
||||
|
|
@ -1401,7 +1317,6 @@ describe("combined security review entry point", () => {
|
|||
});
|
||||
|
||||
it.each([
|
||||
{ field: "status", value: undefined },
|
||||
{ field: "status", value: "unknown" },
|
||||
{ field: "run_attempt", value: undefined },
|
||||
{ field: "run_attempt", value: 0 },
|
||||
|
|
@ -1419,7 +1334,7 @@ describe("combined security review entry point", () => {
|
|||
}
|
||||
});
|
||||
|
||||
it.each([0, -1, undefined, "invalid"])(
|
||||
it.each([0, undefined])(
|
||||
"rejects malformed eligible run ID %s before selecting older successful CI",
|
||||
(id) => {
|
||||
const result = evaluate({
|
||||
|
|
@ -1434,14 +1349,11 @@ describe("combined security review entry point", () => {
|
|||
},
|
||||
);
|
||||
|
||||
it.each(["failure", "cancelled", "skipped", "neutral"])(
|
||||
"does not hide a %s CI gate",
|
||||
(conclusion) => {
|
||||
const result = evaluate({ [jobsPath]: { ...jobs, jobs: [{ ...jobs.jobs[0], conclusion }] } });
|
||||
expect(result.status, result.stderr).toBe(0);
|
||||
expect(result.combined).not.toContain("success");
|
||||
},
|
||||
);
|
||||
it.each(["failure", "skipped"])("does not hide a %s CI gate", (conclusion) => {
|
||||
const result = evaluate({ [jobsPath]: { ...jobs, jobs: [{ ...jobs.jobs[0], conclusion }] } });
|
||||
expect(result.status, result.stderr).toBe(0);
|
||||
expect(result.combined).not.toContain("success");
|
||||
});
|
||||
|
||||
it("does not accept a previously successful CI attempt while a new attempt runs", () => {
|
||||
const result = evaluate({
|
||||
|
|
@ -1460,17 +1372,6 @@ describe("combined security review entry point", () => {
|
|||
expect(result.combined).toEqual(["pending", "failure"]);
|
||||
});
|
||||
|
||||
it("settles after CI completes without leaving either evaluation failed", () => {
|
||||
const waiting = evaluate({
|
||||
[runsPath]: { total_count: 1, workflow_runs: [{ ...run, status: "in_progress" }] },
|
||||
});
|
||||
const completed = evaluate();
|
||||
expect(waiting.status, waiting.stderr).toBe(0);
|
||||
expect(waiting.combined.at(-1)).toBe("pending");
|
||||
expect(completed.status, completed.stderr).toBe(0);
|
||||
expect(completed.combined.at(-1)).toBe("success");
|
||||
});
|
||||
|
||||
it.each([
|
||||
{ name: "CI API failure", routes: { [runsPath]: { httpError: 403 } } },
|
||||
{ name: "invalid CI metadata", routes: { [runsPath]: { workflow_runs: null } } },
|
||||
|
|
|
|||
|
|
@ -1,15 +1,6 @@
|
|||
import { execFileSync, spawnSync } from "node:child_process";
|
||||
import {
|
||||
cpSync,
|
||||
existsSync,
|
||||
mkdirSync,
|
||||
readFileSync,
|
||||
realpathSync,
|
||||
rmSync,
|
||||
writeFileSync,
|
||||
} from "node:fs";
|
||||
import { cpSync, existsSync, mkdirSync, readFileSync, realpathSync, writeFileSync } from "node:fs";
|
||||
import { dirname, join, resolve } from "node:path";
|
||||
import { pathToFileURL } from "node:url";
|
||||
import { runInNewContext } from "node:vm";
|
||||
import ignore from "ignore";
|
||||
import { afterEach, describe, expect, it } from "vitest";
|
||||
|
|
@ -592,26 +583,6 @@ describe("security review workflow trust boundaries", () => {
|
|||
expect(result.stderr.trim()).toBe(
|
||||
"GitHub token, event, event name, and repository are required.",
|
||||
);
|
||||
|
||||
rmSync(join(workspace, "scripts/lib/bounded-response.mjs"));
|
||||
const entryUrl = pathToFileURL(join(workspace, "scripts/github", entry)).href;
|
||||
const missingModule = spawnSync(
|
||||
process.execPath,
|
||||
[
|
||||
"--input-type=module",
|
||||
"--eval",
|
||||
`await import(${JSON.stringify(entryUrl)}).catch(error => {
|
||||
console.error(JSON.stringify({ code: error.code, message: error.message }));
|
||||
process.exitCode = 1;
|
||||
});`,
|
||||
],
|
||||
{ cwd: workspace, env: {}, encoding: "utf8" },
|
||||
);
|
||||
expect(missingModule.status).toBe(1);
|
||||
expect(JSON.parse(missingModule.stderr)).toMatchObject({
|
||||
code: "ERR_MODULE_NOT_FOUND",
|
||||
message: expect.stringContaining("bounded-response.mjs"),
|
||||
});
|
||||
});
|
||||
|
||||
it.skipIf(process.platform === "win32")(
|
||||
|
|
@ -695,16 +666,6 @@ for (const [name, target] of Object.entries(${JSON.stringify(packages)})) {
|
|||
expect(loaded.stderr.trim()).toBe(
|
||||
"GITHUB_TOKEN, GITHUB_EVENT_PATH, and GITHUB_REPOSITORY are required.",
|
||||
);
|
||||
|
||||
rmSync(join(workspace, ".github/security-review-policy.yml"));
|
||||
const missingPolicy = spawnSync(process.execPath, [probe], {
|
||||
cwd: workspace,
|
||||
env: {},
|
||||
encoding: "utf8",
|
||||
});
|
||||
expect(missingPolicy.status).toBe(1);
|
||||
expect(missingPolicy.stderr).toContain("ENOENT");
|
||||
expect(missingPolicy.stderr).toContain("security-review-policy.yml");
|
||||
},
|
||||
);
|
||||
|
||||
|
|
@ -777,14 +738,11 @@ describe("security review ownership", () => {
|
|||
it.each([
|
||||
".github/CODEOWNERS",
|
||||
"SECURITY.md",
|
||||
".github/codeql/codeql-core-auth-secrets-critical-security.yml",
|
||||
".github/codeql/openclaw-boundary/queries/managed-proxy-runtime-mutation.ql",
|
||||
".github/workflows/codeql-macos-critical-security.yml",
|
||||
".github/workflows/security-review.yml",
|
||||
".github/security-review-policy.yml",
|
||||
".github/actions/setup-security-review/action.yml",
|
||||
".github/actions/setup-security-review/package.json",
|
||||
".github/actions/setup-security-review/package-lock.json",
|
||||
"scripts/github/security-review-policy.mjs",
|
||||
"scripts/github/security-review-event.mjs",
|
||||
"scripts/github/security-review.mjs",
|
||||
|
|
|
|||
|
|
@ -29,7 +29,7 @@ describe("pinned pnpm cold bootstrap", () => {
|
|||
for (const name of archives) {
|
||||
fs.copyFileSync(path.join(f.registry, name), path.join(f.image, name));
|
||||
}
|
||||
for (const source of ["image", "store", "registry"]) {
|
||||
for (const [index, source] of ["image", "store", "registry"].entries()) {
|
||||
if (source === "store") {
|
||||
for (const name of archives) {
|
||||
fs.writeFileSync(path.join(f.image, name), "corrupt image");
|
||||
|
|
@ -46,10 +46,13 @@ describe("pinned pnpm cold bootstrap", () => {
|
|||
expect(
|
||||
fs.readFileSync(path.join(root, "node_modules/@pnpm/exe.linux-x64/pnpm"), "utf8"),
|
||||
).toBe("native-fixture\n");
|
||||
expect(JSON.parse(fs.readFileSync(path.join(root, ".corepack"), "utf8")).hash).toBe(
|
||||
f.spec.split("+")[1],
|
||||
);
|
||||
expect(JSON.parse(fs.readFileSync(path.join(root, ".corepack"), "utf8"))).toEqual({
|
||||
locator: { name: "pnpm", reference: f.spec.slice(5) },
|
||||
bin: { pnpm: "./bin/pnpm.mjs", pnpx: "./bin/pnpx.mjs" },
|
||||
hash: f.spec.split("+")[1],
|
||||
});
|
||||
expect(fs.existsSync(f.calls)).toBe(source === "registry");
|
||||
expect(fs.readdirSync(f.runner)).toHaveLength(index + 1);
|
||||
for (const name of archives) {
|
||||
expect(fs.readFileSync(path.join(f.store, "toolchain", name))).toEqual(
|
||||
fs.readFileSync(path.join(f.registry, name)),
|
||||
|
|
@ -151,27 +154,6 @@ describe("pinned pnpm cold bootstrap", () => {
|
|||
});
|
||||
});
|
||||
|
||||
it("downloads authenticated registry archives when both the store and image are empty", async ({
|
||||
command,
|
||||
}) => {
|
||||
const f = createPnpmArchiveFixture(command);
|
||||
const result = await f.run();
|
||||
expect(result.status, result.stderr).toBe(0);
|
||||
expect(result.stdout.trim()).not.toBe("");
|
||||
const root = path.join(result.stdout.trim(), "v1/pnpm/12.5.1");
|
||||
expect(fs.readFileSync(path.join(root, "pnpm"), "utf8")).toBe("wrapper-fixture\n");
|
||||
expect(fs.readFileSync(path.join(root, "node_modules/@pnpm/exe.linux-x64/pnpm"), "utf8")).toBe(
|
||||
"native-fixture\n",
|
||||
);
|
||||
expect(JSON.parse(fs.readFileSync(path.join(root, ".corepack"), "utf8"))).toEqual({
|
||||
locator: { name: "pnpm", reference: f.spec.slice(5) },
|
||||
bin: { pnpm: "./bin/pnpm.mjs", pnpx: "./bin/pnpx.mjs" },
|
||||
hash: f.spec.split("+")[1],
|
||||
});
|
||||
expect(fs.readFileSync(f.calls, "utf8").trim().split("\n")).toHaveLength(2);
|
||||
expect(fs.readdirSync(f.runner)).toHaveLength(1);
|
||||
});
|
||||
|
||||
it("uses authenticated image bytes without making a network request", async ({ command }) => {
|
||||
const f = createPnpmArchiveFixture(command);
|
||||
for (const name of fs.readdirSync(f.registry)) {
|
||||
|
|
|
|||
|
|
@ -192,8 +192,6 @@ describe.skipIf(process.platform === "win32")("iOS simulator preparation", () =>
|
|||
{ env: { OPENCLAW_CI_SIMSLIM_BINARY: "/missing-simslim" } },
|
||||
{ args: [] },
|
||||
{ args: ["booted"] },
|
||||
{ args: ["all"] },
|
||||
{ args: [simulatorId, "extra"] },
|
||||
])("rejects invalid admission before any tool call: %j", (options) => {
|
||||
const { result, commands } = runFixture("ios-simulator-prepare.sh", options);
|
||||
expect(result.status).not.toBe(0);
|
||||
|
|
|
|||
|
|
@ -32,26 +32,6 @@ describe("SQLite reliability worker messages", () => {
|
|||
vi.useRealTimers();
|
||||
});
|
||||
|
||||
it("listens before sending, ignores other messages, and preserves other listeners", async () => {
|
||||
const child = new ChildProcess();
|
||||
const observed: unknown[] = [];
|
||||
const observe = (message: unknown) => observed.push(message);
|
||||
child.on("message", observe);
|
||||
const ready = waitForReady(child, {
|
||||
action: () => {
|
||||
child.emit("message", "unrelated");
|
||||
expect(child.listenerCount("message")).toBe(2);
|
||||
child.emit("message", "ready");
|
||||
},
|
||||
});
|
||||
|
||||
await expect(ready).resolves.toBe("ready");
|
||||
expect(observed).toEqual(["unrelated", "ready"]);
|
||||
expect(child.listeners("message")).toEqual([observe]);
|
||||
child.off("message", observe);
|
||||
expectWaitCleanedUp(child);
|
||||
});
|
||||
|
||||
it("rejects child errors without replacing the original error", async () => {
|
||||
const child = new ChildProcess();
|
||||
const error = new Error("IPC failed");
|
||||
|
|
@ -75,25 +55,23 @@ describe("SQLite reliability worker messages", () => {
|
|||
expectWaitCleanedUp(child);
|
||||
});
|
||||
|
||||
it.each([30_000, 120_000])(
|
||||
"honors a %i ms timeout and reads final diagnostics",
|
||||
async (timeoutMs) => {
|
||||
const child = new ChildProcess();
|
||||
let stderr = "before";
|
||||
const ready = waitForReady(child, {
|
||||
timeoutMs,
|
||||
timeoutMessage: () => `timeout: ${stderr}`,
|
||||
});
|
||||
const rejected = expect(ready).rejects.toThrow("timeout: last stderr");
|
||||
await vi.advanceTimersByTimeAsync(timeoutMs - 1);
|
||||
expect(child.listenerCount("message")).toBe(1);
|
||||
stderr = "last stderr";
|
||||
await vi.advanceTimersByTimeAsync(1);
|
||||
it("honors the configured timeout and reads final diagnostics", async () => {
|
||||
const timeoutMs = 120_000;
|
||||
const child = new ChildProcess();
|
||||
let stderr = "before";
|
||||
const ready = waitForReady(child, {
|
||||
timeoutMs,
|
||||
timeoutMessage: () => `timeout: ${stderr}`,
|
||||
});
|
||||
const rejected = expect(ready).rejects.toThrow("timeout: last stderr");
|
||||
await vi.advanceTimersByTimeAsync(timeoutMs - 1);
|
||||
expect(child.listenerCount("message")).toBe(1);
|
||||
stderr = "last stderr";
|
||||
await vi.advanceTimersByTimeAsync(1);
|
||||
|
||||
await rejected;
|
||||
expectWaitCleanedUp(child);
|
||||
},
|
||||
);
|
||||
await rejected;
|
||||
expectWaitCleanedUp(child);
|
||||
});
|
||||
|
||||
it.each(["action", "matches"] as const)("cleans up when %s throws", async (source) => {
|
||||
const child = new ChildProcess();
|
||||
|
|
@ -123,19 +101,28 @@ describe("SQLite reliability worker messages", () => {
|
|||
expectWaitCleanedUp(child);
|
||||
});
|
||||
|
||||
it("returns the requested writer payload from a synchronous action reply", async () => {
|
||||
it("waits for the synchronous writer reply while preserving other listeners", async () => {
|
||||
const child = new ChildProcess();
|
||||
const observed: unknown[] = [];
|
||||
const observe = (message: unknown) => observed.push(message);
|
||||
child.on("message", observe);
|
||||
const payload = { kind: "result", batchesCommitted: 2, rowsCommitted: 16 };
|
||||
const result = await waitForWriterMessage(
|
||||
{ child, stderr: [], stopped: false },
|
||||
"result",
|
||||
() => {
|
||||
child.emit("message", { kind: "ready" });
|
||||
child.emit("message", { kind: "result", batchesCommitted: 2, rowsCommitted: 16 });
|
||||
expect(child.listenerCount("message")).toBe(2);
|
||||
child.emit("message", payload);
|
||||
},
|
||||
);
|
||||
|
||||
expect(result.batchesCommitted).toBe(2);
|
||||
expect(result.rowsCommitted).toBe(16);
|
||||
expect(result).toBe(payload);
|
||||
expect(observed).toEqual([{ kind: "ready" }, payload]);
|
||||
expect(child.listeners("message")).toEqual([observe]);
|
||||
child.off("message", observe);
|
||||
expectWaitCleanedUp(child);
|
||||
});
|
||||
});
|
||||
|
|
|
|||
|
|
@ -16,20 +16,19 @@ const { createTempDir } = createScriptTestHarness();
|
|||
const chunk = previousReleaseInventory.releases[0]!.chunks.find((entry) =>
|
||||
/-[A-Za-z0-9_-]{8}\.m?js$/.test(entry.path),
|
||||
)!;
|
||||
const externalSources = [
|
||||
{ kind: "named export", source: 'export { helper as NAME } from "external-package";' },
|
||||
{ kind: "namespace export", source: 'export * as NAME from "external-package";' },
|
||||
{
|
||||
kind: "namespace import",
|
||||
source: 'import * as NAME from "external-package"; export { NAME };',
|
||||
},
|
||||
];
|
||||
const namedExport = {
|
||||
kind: "named export",
|
||||
source: 'export { helper as NAME } from "external-package";',
|
||||
};
|
||||
const namespaceImport = {
|
||||
kind: "namespace import",
|
||||
source: 'import * as NAME from "external-package"; export { NAME };',
|
||||
};
|
||||
|
||||
it.each([
|
||||
'import { safePath as helper } from "@openclaw/fs-safe/path"; export { helper };',
|
||||
'import helper from "external-package"; export { helper };',
|
||||
'export { safePath as helper } from "@openclaw/fs-safe/path";',
|
||||
'import { basename as helper } from "node:path"; export { helper };',
|
||||
'export * as helper from "external-package";',
|
||||
'import * as helper from "external-package"; export { helper };',
|
||||
])("keeps published updater bridges usable beside %s", async (external) => {
|
||||
|
|
@ -51,7 +50,7 @@ it.each([
|
|||
}
|
||||
});
|
||||
|
||||
it.each(externalSources)(
|
||||
it.each([namedExport, namespaceImport])(
|
||||
"refuses a required updater implementation replaced with an external $kind",
|
||||
({ source: template }) => {
|
||||
const root = createTempDir("update-compat-required-external-");
|
||||
|
|
@ -71,7 +70,10 @@ it.each(externalSources)(
|
|||
},
|
||||
);
|
||||
|
||||
it.each(externalSources)("refuses to omit a published external $kind", ({ source }) => {
|
||||
it.each([
|
||||
namedExport,
|
||||
{ kind: "namespace export", source: 'export * as NAME from "external-package";' },
|
||||
])("refuses to omit a published external $kind", ({ source }) => {
|
||||
const root = createTempDir("update-compat-published-external-");
|
||||
fs.mkdirSync(path.join(root, "dist"));
|
||||
fs.writeFileSync(
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue