mirror of
https://github.com/openclaw/openclaw.git
synced 2026-09-29 17:19:00 +08:00
* fix: recognize verified team admins as channel owners
Resolve channel owner authority from SQLite profile identity links and current login grants, with immediate revocation across deferred work. Keep Gateway and bundled plugins on one native SDK graph and replace the related process-global authority registries with instance-owned capabilities.
* fix: preserve channel owner authority through deferred work
Authorize Discord commands before ACP preparation and retain the original live owner through backend effects and updater handoffs. Normalize direct notice recipients through channel contracts. Complete native test-loader and instance-runtime fixture coverage, including final-effect and revocation regressions.
* fix(plugins): retain host SDK access in captured workers
Link captured plugins to the selected host package for worker isolates, preserving native SDK identity through retained generations and recovery. Align CI fixtures with instance-owned runtimes and join owned asynchronous work in teardown and Telegram buffering tests.
Validation: native worker regression fails before the fix and passes across native/legacy and source/dist hosts; 325 core tests, the full 3121-test Slack suite, targeted channel tests, protocol generation, Android lint, changed checks, and independent review pass.
* fix(runtime): keep snapshot cleanup inside owned directories
Treat captured SDK host-package links as removable leaves, preserving ownership records until snapshot data is gone. Preserve sanitized readiness subprocess failures and exercise Doctor through its complete isolated runtime on clean installations.
* fix(plugins): keep lazy runtime ownership metadata local
Preserve deferred Gateway facets with instance-owned proxy metadata and retain redacted readiness failure diagnostics on the current subprocess result owner.
* fix(auth): retain live owner authority through command effects
Carry the admitted administrator assertion through command dispatch, ACP controls and metadata commits, config and allowlist writes, plugin consent and installation, MCP mutation, and restart preparation. Preserve accepted-operation settlement and condition restart acknowledgement cleanup on its owned revision. Prove allowed administrators, forbidden senders, revocation and reassignment through real handler and persistence boundaries.
* refactor(restart): require owned revisions for sentinel cleanup
Remove the unused unconditional clear facade and storage branch. Keep revision-floor migration, durable failure reporting, and updater consumption proof on the canonical conditional-clear operation.
* test(auth): align owner regressions with fixture lint contracts
* refactor(auth): simplify channel owner and runtime authority
Resolve linked channel administration from the current Team role policy, retaining identity-grant fallback only for roleless installations. Consolidate Gateway generation state into its lifecycle owner, simplify Discord native routing and remove redundant loader and ingress state. Preserve current-authority checks before writes and required cleanup after accepted operations.
* test(auth): compare public generation state values
* refactor(auth): keep authority fixtures and handoff types with their owners
Extract coherent fixture builders and internal updater types to keep large files from growing. Correct the task-identifier test import to its codec owner and remove the unused internal route-policy export. Preserve all runtime behavior, assertions, deadlines and revision-owned sentinel cleanup.
* fix(channels): preserve native conversation scope in ingress authority
* fix(imessage): bind ingress after reply ID mapping
* fix(test): preserve scoped filesystem and channel admission contracts
* test(fleet): share stopped container state fixture
* test(fleet): type stop mock against the container contract
* fix(auth): retain current owner authority through deferred effects
* refactor(auth): keep authority fixtures and helpers with their owners
* fix(ci): remove duplicate database worker test entry
Retain the existing worker.runtime test entry so compact CI planning includes every storage-state test once. The duplicate introduced in d2c8c34af2 made preflight reject all split timing generations for this owner.
Reproduced the exact preflight error before the repair. Hybrid, GitHub, and Blacksmith planning now preserve all 660 unique storage-state files. The 217 planner/config tests, selected changed-file checks, formatting, and diff checks pass.
* refactor(auth): prepare profile authority in SQLite workers
Move channel identity and affected profile writes onto the existing worker owners, with current authority at commit and explicit rollback recovery. Bind native Telegram commands to verified ingress and retain shared-owner administration.
* fix(auth): preserve owner checks and released ingress callers
Forward Telegram authority through configured backend preparation and retain released ingress helper provenance through the existing plugin instance owner. Keep identity result types in the leaf contract and repair worker-aware test routing and fixtures without weakening policy assertions.
* fix: correct ingress names and database test ownership
Keep supported SDK ingress adapters while distinguishing internal policy operations. Assign broker-dependent HTTP suites only to the Gateway fork owner, preserve sorted test discovery, reuse chat registration fixtures, and declare the dynamically loaded Telegram test entry.
* test(cli): use prepared runtime for MCP probe exit
Exercise the real CLI entrypoint instead of compiling source-backed SQLite workers inside the command deadline. Keep the 30-second deadline and the exit, JSON, and named diagnostic checks.
* test(codex): check native worker termination at teardown
Observe native Worker thread IDs after fixture cleanup instead of equating
thread exit with asynchronous resource-destroy notification. Keep the
allocation assertion and existing cleanup; do not wait for idle retirement.
The three-case file passes in 48.495s. Omitting only its harness disk-worker
drain fails immediately on a live thread in 30.715s. A Node 24.19 control
observes threadId -1 while the async destroy notification is still pending.
Managed review is clean through P2. The earlier CI worker identity remains
unproven; this is a test-contract repair, not a production leak claim.
* test: settle identity fixtures and route database cleanup
Wait for actual GitHub metadata entry and settle both identity requests on failure. Run the session-store consumer in the existing database fork owner so native retirement can use the host broker.
* test: settle admin fixtures at their owning boundaries
Await canonical asynchronous MCP OAuth reads and the existing Gateway attachment completion. Keep avatar work independently gated and preserve all permission assertions. Move ingress and callback fixture helpers into the existing support owner to keep the health suite within its line-growth limit.
* test(worker): share the compiled SDK graph in crash fixtures
151 lines
5.4 KiB
TypeScript
151 lines
5.4 KiB
TypeScript
import fs from "node:fs";
|
|
import { createRequire } from "node:module";
|
|
import path, { sep } from "node:path";
|
|
import { setImmediate as nextTurn } from "node:timers/promises";
|
|
import { afterEach, expect, it, vi } from "vitest";
|
|
import { createDeferred } from "../helpers/promise.js";
|
|
import { useAutoCleanupTempDirTracker } from "../helpers/temp-dir.js";
|
|
|
|
const tempDirs = useAutoCleanupTempDirTracker(afterEach);
|
|
const nativeRequire = createRequire(import.meta.url);
|
|
|
|
// Vitest publishes the active mocker. Gate its real resolver to exercise the
|
|
// installed dependency's fetch boundary, rather than emulating its queue.
|
|
// oxlint-disable-next-line eslint/no-underscore-dangle -- Vitest owns this exact published global name.
|
|
declare const __vitest_mocker__: {
|
|
resolveId: (id: string, importer?: string) => Promise<unknown>;
|
|
resolveMocks: () => Promise<void>;
|
|
moduleRunner: {
|
|
vitestOptions: {
|
|
transport: { fetchModule: (...args: unknown[]) => Promise<unknown> };
|
|
};
|
|
};
|
|
};
|
|
|
|
afterEach(async () => {
|
|
vi.doUnmock("node:path");
|
|
vi.doUnmock("node:os");
|
|
await __vitest_mocker__.resolveMocks();
|
|
vi.resetModules();
|
|
});
|
|
|
|
it("loads one native source graph while preserving Vitest module mocks", async () => {
|
|
const root = tempDirs.make("openclaw-vitest-native-source-");
|
|
fs.writeFileSync(path.join(root, "package.json"), '{"type":"module"}');
|
|
fs.writeFileSync(
|
|
path.join(root, "leaf.ts"),
|
|
"export enum State { Waiting, Ready }\nexport const token = {};",
|
|
);
|
|
fs.writeFileSync(
|
|
path.join(root, "sdk.ts"),
|
|
'import { State, token } from "./leaf.js"; export { token }; export const state = State.Ready;',
|
|
);
|
|
const probePath = path.join(root, "probe.cjs");
|
|
fs.writeFileSync(
|
|
probePath,
|
|
`module.exports = async () => {
|
|
const required = require("./sdk.ts");
|
|
const imported = await import("./sdk.ts");
|
|
return { sameOwner: required.token === imported.token, state: required.state };
|
|
};`,
|
|
);
|
|
vi.doMock("node:path", () => ({ sep: "mocked" }));
|
|
expect((await import("node:path")).sep).toBe("mocked");
|
|
const probe: unknown = nativeRequire(probePath);
|
|
if (typeof probe !== "function") {
|
|
throw new Error("Native source fixture did not export its probe");
|
|
}
|
|
expect(await probe()).toEqual({ sameOwner: true, state: 1 });
|
|
expect((await import("node:path")).sep).toBe("mocked");
|
|
});
|
|
|
|
it("waits for an in-flight unmock before another import reads its registry", async () => {
|
|
vi.doMock("node:path", () => ({ sep: "mocked" }));
|
|
expect((await import("node:path")).sep).toBe("mocked");
|
|
const mocker = __vitest_mocker__;
|
|
const transport = mocker.moduleRunner.vitestOptions.transport;
|
|
const resolveId = mocker.resolveId;
|
|
const fetchModule = transport.fetchModule;
|
|
const resolving = createDeferred();
|
|
const release = createDeferred();
|
|
const fetching = createDeferred();
|
|
let observeFetch = false;
|
|
mocker.resolveId = async (id, importer) => {
|
|
if (id === "node:path") {
|
|
resolving.resolve();
|
|
await release.promise;
|
|
}
|
|
return resolveId.call(mocker, id, importer);
|
|
};
|
|
transport.fetchModule = (...args) => {
|
|
const result = fetchModule.apply(transport, args);
|
|
if (observeFetch && args[0] === "node:path") {
|
|
fetching.resolve();
|
|
}
|
|
return result;
|
|
};
|
|
vi.doUnmock("node:path");
|
|
const first = import("node:os");
|
|
let second: Promise<string> | undefined;
|
|
try {
|
|
await resolving.promise;
|
|
observeFetch = true;
|
|
let completed = false;
|
|
second = import("node:path").then((module) => {
|
|
completed = true;
|
|
return module.sep;
|
|
});
|
|
await fetching.promise;
|
|
await nextTurn();
|
|
expect.soft(completed).toBe(false);
|
|
} finally {
|
|
release.resolve();
|
|
await Promise.allSettled([first, second]);
|
|
mocker.resolveId = resolveId;
|
|
transport.fetchModule = fetchModule;
|
|
}
|
|
await expect(first).resolves.toBeDefined();
|
|
await expect(second).resolves.toBe(sep);
|
|
});
|
|
|
|
it("allows a mock factory to import actual exports and register another mock", async () => {
|
|
vi.doMock("node:path", async () => {
|
|
const actual = await vi.importActual<typeof import("node:path")>("node:path");
|
|
vi.doMock("node:os", () => ({ type: () => "nested" }));
|
|
const nested = await import("node:os");
|
|
return { sep: `${actual.sep}:${nested.type()}` };
|
|
});
|
|
expect((await import("node:path")).sep).toBe(`${sep}:nested`);
|
|
});
|
|
|
|
it("reports a failed resolution without poisoning the next caller's mock", async () => {
|
|
const mocker = __vitest_mocker__;
|
|
const resolveId = mocker.resolveId;
|
|
const resolving = createDeferred();
|
|
const release = createDeferred();
|
|
const failure = new Error("synthetic mock resolution failure");
|
|
mocker.resolveId = async (id, importer) => {
|
|
if (id === "node:path") {
|
|
resolving.resolve();
|
|
await release.promise;
|
|
throw failure;
|
|
}
|
|
return resolveId.call(mocker, id, importer);
|
|
};
|
|
vi.doMock("node:path", () => ({ sep: "unresolved" }));
|
|
const first = import("node:path").catch((error: unknown) => error);
|
|
let second: Promise<typeof import("node:os")> | undefined;
|
|
try {
|
|
await resolving.promise;
|
|
vi.doMock("node:os", () => ({ type: () => "recovered" }));
|
|
second = import("node:os");
|
|
release.resolve();
|
|
expect(await first).toMatchObject({ name: "Error", message: failure.message });
|
|
expect((await second).type()).toBe("recovered");
|
|
} finally {
|
|
release.resolve();
|
|
await Promise.allSettled([first, second]);
|
|
mocker.resolveId = resolveId;
|
|
}
|
|
});
|