Clarify unprepared artifact scopes

This commit is contained in:
Haitao Pan 2026-05-11 17:23:56 +08:00
parent 0ee969ea16
commit 5c686f95b2
3 changed files with 133 additions and 14 deletions

View File

@ -76,13 +76,19 @@ export async function exportXWorkmateArtifacts(input) {
const artifactScope = requestedArtifactScope || expectedArtifactScope;
const scopeRoot = resolveScopeRoot(workspaceRoot, artifactScope);
const scopeKind = "task";
const candidates = await collectCandidates({
scanRoot: scopeRoot,
relativeRoot: scopeRoot,
sinceUnixMs,
skipTaskScopeRoot: false,
warnings,
});
const scopePrepared = await directoryExists(scopeRoot);
const candidates = scopePrepared
? await collectCandidates({
scanRoot: scopeRoot,
relativeRoot: scopeRoot,
sinceUnixMs,
skipTaskScopeRoot: false,
warnings,
})
: [];
if (!scopePrepared) {
warnings.push("artifact scope is not prepared");
}
candidates.sort((left, right) => {
if (right.mtimeMs !== left.mtimeMs) {
return right.mtimeMs - left.mtimeMs;
@ -397,6 +403,15 @@ function safeTaskSessionScope(value) {
}
return scope;
}
async function directoryExists(absolutePath) {
try {
const stat = await fs.stat(absolutePath);
return stat.isDirectory();
}
catch {
return false;
}
}
function safeArtifactRefRunScope(value) {
try {
return safeTaskArtifactScope(value);

View File

@ -7,6 +7,11 @@ import plugin from "./index.js";
import { prepareXWorkmateArtifacts } from "./src/exportArtifacts.js";
type GatewayMethodHandler = Parameters<OpenClawPluginApi["registerGatewayMethod"]>[1];
type GatewayMethodResponse = {
ok: boolean;
payload?: Record<string, unknown>;
error?: { code?: string; message?: string };
};
describe("plugin registration", () => {
it("declares registered agent tools in the manifest contract", () => {
@ -50,6 +55,68 @@ describe("plugin registration", () => {
});
});
it("executes registered gateway methods against the current task scope", async () => {
const root = await fs.promises.mkdtemp(path.join(os.tmpdir(), "tmp-openclaw-multi-session-gateway-"));
const methods = new Map<string, GatewayMethodHandler>();
const api = {
config: {},
pluginConfig: { workspaceDir: root },
registerGatewayMethod: (method: string, handler: GatewayMethodHandler) => {
methods.set(method, handler);
},
registerTool: () => undefined,
} as unknown as OpenClawPluginApi;
plugin.register(api);
const prepared = await callGatewayMethod(methods, "xworkmate.artifacts.prepare", {
sessionKey: "thread-main",
runId: "turn-1",
});
expect(prepared.ok).toBe(true);
expect(prepared.payload?.artifactScope).toBe("tasks/thread-main/turn-1");
const artifactDirectory = String(prepared.payload?.artifactDirectory);
const emptyExport = await callGatewayMethod(methods, "xworkmate.artifacts.export", {
sessionKey: "thread-main",
runId: "turn-1",
artifactScope: prepared.payload?.artifactScope,
});
expect(emptyExport.ok).toBe(true);
expect(emptyExport.payload?.artifacts).toEqual([]);
expect(emptyExport.payload?.warnings).toEqual([]);
await fs.promises.mkdir(path.join(artifactDirectory, "reports"), { recursive: true });
await fs.promises.writeFile(path.join(artifactDirectory, "reports", "final.md"), "final");
const listed = await callGatewayMethod(methods, "xworkmate.artifacts.list", {
sessionKey: "thread-main",
runId: "turn-1",
artifactScope: prepared.payload?.artifactScope,
});
expect(listed.ok).toBe(true);
expect(listed.payload?.artifacts).toMatchObject([{ relativePath: "reports/final.md" }]);
const listedArtifacts = listed.payload?.artifacts as Array<Record<string, unknown>>;
expect(listedArtifacts[0]).not.toHaveProperty("content");
const read = await callGatewayMethod(methods, "xworkmate.artifacts.read", {
sessionKey: "thread-main",
runId: "turn-1",
artifactScope: prepared.payload?.artifactScope,
relativePath: "reports/final.md",
});
expect(read.ok).toBe(true);
expect(read.payload?.artifacts).toMatchObject([{ relativePath: "reports/final.md", encoding: "base64" }]);
const unprepared = await callGatewayMethod(methods, "xworkmate.artifacts.export", {
sessionKey: "thread-main",
runId: "turn-unprepared",
});
expect(unprepared.ok).toBe(true);
expect(unprepared.payload?.artifacts).toEqual([]);
expect(unprepared.payload?.warnings).toEqual(["artifact scope is not prepared"]);
});
it("does not invent default session or run ids for the optional agent tool", async () => {
const tools: Array<{ tool: unknown; options: unknown }> = [];
const api = {
@ -120,3 +187,25 @@ describe("plugin registration", () => {
expect(result.content[0]?.text).not.toContain("global.txt");
});
});
async function callGatewayMethod(
methods: Map<string, GatewayMethodHandler>,
method: string,
params: Record<string, unknown>,
): Promise<GatewayMethodResponse> {
const handler = methods.get(method);
if (!handler) {
throw new Error(`missing gateway method ${method}`);
}
let response: GatewayMethodResponse | undefined;
await handler({
params,
respond: (ok: boolean, payload?: Record<string, unknown>, error?: GatewayMethodResponse["error"]) => {
response = { ok, payload, error };
},
} as Parameters<GatewayMethodHandler>[0]);
if (!response) {
throw new Error(`gateway method ${method} did not respond`);
}
return response;
}

View File

@ -157,13 +157,19 @@ export async function exportXWorkmateArtifacts(input: ExportInput): Promise<XWor
const artifactScope = requestedArtifactScope || expectedArtifactScope;
const scopeRoot = resolveScopeRoot(workspaceRoot, artifactScope);
const scopeKind: XWorkmateArtifactScopeKind = "task";
const candidates = await collectCandidates({
scanRoot: scopeRoot,
relativeRoot: scopeRoot,
sinceUnixMs,
skipTaskScopeRoot: false,
warnings,
});
const scopePrepared = await directoryExists(scopeRoot);
const candidates = scopePrepared
? await collectCandidates({
scanRoot: scopeRoot,
relativeRoot: scopeRoot,
sinceUnixMs,
skipTaskScopeRoot: false,
warnings,
})
: [];
if (!scopePrepared) {
warnings.push("artifact scope is not prepared");
}
candidates.sort((left, right) => {
if (right.mtimeMs !== left.mtimeMs) {
@ -523,6 +529,15 @@ function safeTaskSessionScope(value: unknown): string {
return scope;
}
async function directoryExists(absolutePath: string): Promise<boolean> {
try {
const stat = await fs.stat(absolutePath);
return stat.isDirectory();
} catch {
return false;
}
}
function safeArtifactRefRunScope(value: unknown): string {
try {
return safeTaskArtifactScope(value);