refactor: share expected artifact dir normalization
This commit is contained in:
parent
bec9567f13
commit
0682fbf7cf
1
dist/src/expectedArtifactDirs.d.ts
vendored
Normal file
1
dist/src/expectedArtifactDirs.d.ts
vendored
Normal file
@ -0,0 +1 @@
|
||||
export declare function normalizeExpectedArtifactDirs(value: unknown): string[];
|
||||
33
dist/src/expectedArtifactDirs.js
vendored
Normal file
33
dist/src/expectedArtifactDirs.js
vendored
Normal file
@ -0,0 +1,33 @@
|
||||
function optionalString(value) {
|
||||
return typeof value === "string" ? value.trim() : "";
|
||||
}
|
||||
function safeExpectedArtifactDir(value) {
|
||||
const relativePath = optionalString(value);
|
||||
if (!relativePath) {
|
||||
return "";
|
||||
}
|
||||
if (/^[A-Za-z]:[\\/]/u.test(relativePath) || relativePath.startsWith("/") || relativePath.includes("\0")) {
|
||||
throw new Error("expectedArtifactDir must stay inside the workspace");
|
||||
}
|
||||
const normalized = relativePath.split(/[\\/]/).filter(Boolean).join("/");
|
||||
if (!normalized || normalized.split("/").some((part) => part === ".." || part === ".")) {
|
||||
throw new Error("expectedArtifactDir must stay inside the workspace");
|
||||
}
|
||||
return normalized.endsWith("/") ? normalized : `${normalized}/`;
|
||||
}
|
||||
export function normalizeExpectedArtifactDirs(value) {
|
||||
if (!Array.isArray(value)) {
|
||||
return [];
|
||||
}
|
||||
const seen = new Set();
|
||||
const result = [];
|
||||
for (const entry of value) {
|
||||
const normalized = safeExpectedArtifactDir(entry);
|
||||
if (!normalized || seen.has(normalized)) {
|
||||
continue;
|
||||
}
|
||||
seen.add(normalized);
|
||||
result.push(normalized);
|
||||
}
|
||||
return result;
|
||||
}
|
||||
14
dist/src/exportArtifacts.d.ts
vendored
14
dist/src/exportArtifacts.d.ts
vendored
@ -1,4 +1,4 @@
|
||||
export type XWorkmateArtifact = {
|
||||
type XWorkmateArtifact = {
|
||||
relativePath: string;
|
||||
label: string;
|
||||
contentType: string;
|
||||
@ -10,8 +10,8 @@ export type XWorkmateArtifact = {
|
||||
encoding?: "base64";
|
||||
content?: string;
|
||||
};
|
||||
export type XWorkmateArtifactScopeKind = "task";
|
||||
export type XWorkmateArtifactExport = {
|
||||
type XWorkmateArtifactScopeKind = "task";
|
||||
type XWorkmateArtifactExport = {
|
||||
runId: string;
|
||||
sessionKey: string;
|
||||
remoteWorkingDirectory: string;
|
||||
@ -25,7 +25,7 @@ export type XWorkmateArtifactExport = {
|
||||
constraintSatisfied: boolean;
|
||||
missingRequiredExtensions: string[];
|
||||
};
|
||||
export type XWorkmateArtifactPrepare = {
|
||||
type XWorkmateArtifactPrepare = {
|
||||
runId: string;
|
||||
sessionKey: string;
|
||||
remoteWorkingDirectory: string;
|
||||
@ -38,11 +38,11 @@ export type XWorkmateArtifactPrepare = {
|
||||
expectedArtifactDirs: string[];
|
||||
expectedArtifactDirStatus: XWorkmateExpectedArtifactDirStatus[];
|
||||
};
|
||||
export type XWorkmateExpectedArtifactDirStatus = {
|
||||
type XWorkmateExpectedArtifactDirStatus = {
|
||||
relativePath: string;
|
||||
exists: boolean;
|
||||
};
|
||||
export type XWorkmateArtifactSnapshot = {
|
||||
type XWorkmateArtifactSnapshot = {
|
||||
runId: string;
|
||||
sessionKey: string;
|
||||
remoteWorkingDirectory: string;
|
||||
@ -68,8 +68,6 @@ export declare function prepareXWorkmateArtifacts(input: ExportInput): Promise<X
|
||||
export declare function collectAndSnapshotXWorkmateArtifacts(input: ExportInput): Promise<XWorkmateArtifactSnapshot>;
|
||||
export declare function exportXWorkmateArtifacts(input: ExportInput): Promise<XWorkmateArtifactExport>;
|
||||
export declare function readXWorkmateArtifact(input: ReadInput): Promise<XWorkmateArtifactExport>;
|
||||
export declare function normalizeExpectedArtifactDirs(value: unknown): string[];
|
||||
export declare function normalizeRequiredExtensions(value: unknown): string[];
|
||||
export declare function formatArtifactManifestMarkdown(input: {
|
||||
remoteWorkingDirectory: string;
|
||||
artifactScope?: string;
|
||||
|
||||
20
dist/src/exportArtifacts.js
vendored
20
dist/src/exportArtifacts.js
vendored
@ -2,6 +2,7 @@ import { createHash, createHmac, randomBytes, timingSafeEqual } from "node:crypt
|
||||
import fs from "node:fs/promises";
|
||||
import os from "node:os";
|
||||
import path from "node:path";
|
||||
import { normalizeExpectedArtifactDirs } from "./expectedArtifactDirs.js";
|
||||
const DEFAULT_MAX_FILES = 64;
|
||||
const DEFAULT_MAX_INLINE_BYTES = 10 * 1024 * 1024;
|
||||
const TASK_SCOPE_ROOT = "tasks";
|
||||
@ -369,24 +370,7 @@ export async function readXWorkmateArtifact(input) {
|
||||
};
|
||||
return result;
|
||||
}
|
||||
export function normalizeExpectedArtifactDirs(value) {
|
||||
if (!Array.isArray(value)) {
|
||||
return [];
|
||||
}
|
||||
const seen = new Set();
|
||||
const result = [];
|
||||
for (const entry of value) {
|
||||
const normalized = safeInputRelativePath(entry, "expectedArtifactDir");
|
||||
const withSlash = normalized.endsWith("/") ? normalized : `${normalized}/`;
|
||||
if (seen.has(withSlash)) {
|
||||
continue;
|
||||
}
|
||||
seen.add(withSlash);
|
||||
result.push(withSlash);
|
||||
}
|
||||
return result;
|
||||
}
|
||||
export function normalizeRequiredExtensions(value) {
|
||||
function normalizeRequiredExtensions(value) {
|
||||
if (!Array.isArray(value)) {
|
||||
return [];
|
||||
}
|
||||
|
||||
12
dist/src/taskState.d.ts
vendored
12
dist/src/taskState.d.ts
vendored
@ -1,5 +1,4 @@
|
||||
import type { OpenClawPluginApi } from "openclaw/plugin-sdk/core";
|
||||
export declare const XWORKMATE_PLUGIN_ID = "openclaw-multi-session-plugins";
|
||||
export declare const XWORKMATE_SESSION_EXTENSION_NAMESPACE = "xworkmate.sessionMapping";
|
||||
export type XWorkmateTaskMetadataV1 = {
|
||||
schemaVersion: 1;
|
||||
@ -35,17 +34,6 @@ export declare function recordXWorkmateSessionMapping(input: {
|
||||
artifactScope?: string;
|
||||
source?: XWorkmateSessionMappingSource;
|
||||
}): Promise<XWorkmateSessionMappingV1>;
|
||||
export declare function normalizeXWorkmateTaskMetadataV1(input: Record<string, unknown>): XWorkmateTaskMetadataV1;
|
||||
export declare function normalizeExpectedArtifactDirs(value: unknown): string[];
|
||||
export declare function upsertXWorkmateSessionMapping(api: OpenClawPluginApi, input: {
|
||||
metadata: XWorkmateTaskMetadataV1;
|
||||
openclawSessionKey: string;
|
||||
source: XWorkmateSessionMappingSource;
|
||||
}): Promise<XWorkmateSessionMappingV1>;
|
||||
export declare function readXWorkmateSessionMapping(api: OpenClawPluginApi, lookup: {
|
||||
appThreadKey?: string;
|
||||
openclawSessionKey?: string;
|
||||
}): Promise<XWorkmateSessionMappingV1 | undefined>;
|
||||
export declare function getXWorkmateTaskSnapshot(input: {
|
||||
api: OpenClawPluginApi;
|
||||
params: Record<string, unknown>;
|
||||
|
||||
31
dist/src/taskState.js
vendored
31
dist/src/taskState.js
vendored
@ -1,5 +1,6 @@
|
||||
import { exportXWorkmateArtifacts } from "./exportArtifacts.js";
|
||||
export const XWORKMATE_PLUGIN_ID = "openclaw-multi-session-plugins";
|
||||
import { normalizeExpectedArtifactDirs } from "./expectedArtifactDirs.js";
|
||||
const XWORKMATE_PLUGIN_ID = "openclaw-multi-session-plugins";
|
||||
export const XWORKMATE_SESSION_EXTENSION_NAMESPACE = "xworkmate.sessionMapping";
|
||||
export function registerXWorkmateSessionExtension(api) {
|
||||
const registerExtension = api.session?.state?.registerSessionExtension ?? api.registerSessionExtension;
|
||||
@ -28,7 +29,7 @@ export async function recordXWorkmateSessionMapping(input) {
|
||||
source: input.source ?? "bridge_prepare",
|
||||
});
|
||||
}
|
||||
export function normalizeXWorkmateTaskMetadataV1(input) {
|
||||
function normalizeXWorkmateTaskMetadataV1(input) {
|
||||
const envelope = asRecord(input.xworkmate) ?? asRecord(input.xworkmateMetadata) ?? input;
|
||||
const schemaVersion = Number(envelope.schemaVersion ?? 1);
|
||||
if (schemaVersion !== 1) {
|
||||
@ -46,29 +47,7 @@ export function normalizeXWorkmateTaskMetadataV1(input) {
|
||||
createdAt,
|
||||
});
|
||||
}
|
||||
export function normalizeExpectedArtifactDirs(value) {
|
||||
if (!Array.isArray(value)) {
|
||||
return [];
|
||||
}
|
||||
const seen = new Set();
|
||||
const result = [];
|
||||
for (const entry of value) {
|
||||
const text = optionalString(entry).replaceAll("\\", "/").replace(/^\.\/+/u, "");
|
||||
if (!text || seen.has(text)) {
|
||||
continue;
|
||||
}
|
||||
if (text.startsWith("/") || /^[A-Za-z]:\//u.test(text) || text.split("/").includes("..")) {
|
||||
throw new Error("expectedArtifactDirs must be relative paths without traversal");
|
||||
}
|
||||
const normalized = text.endsWith("/") ? text : `${text}/`;
|
||||
if (!seen.has(normalized)) {
|
||||
seen.add(normalized);
|
||||
result.push(normalized);
|
||||
}
|
||||
}
|
||||
return result;
|
||||
}
|
||||
export async function upsertXWorkmateSessionMapping(api, input) {
|
||||
async function upsertXWorkmateSessionMapping(api, input) {
|
||||
const patchSessionEntry = resolvePatchSessionEntry(api);
|
||||
if (!patchSessionEntry) {
|
||||
throw new Error("OpenClaw runtime session patch API is unavailable");
|
||||
@ -114,7 +93,7 @@ export async function upsertXWorkmateSessionMapping(api, input) {
|
||||
}
|
||||
return mapping;
|
||||
}
|
||||
export async function readXWorkmateSessionMapping(api, lookup) {
|
||||
async function readXWorkmateSessionMapping(api, lookup) {
|
||||
const getSessionEntry = resolveGetSessionEntry(api);
|
||||
if (!getSessionEntry) {
|
||||
return undefined;
|
||||
|
||||
35
src/expectedArtifactDirs.ts
Normal file
35
src/expectedArtifactDirs.ts
Normal file
@ -0,0 +1,35 @@
|
||||
function optionalString(value: unknown): string {
|
||||
return typeof value === "string" ? value.trim() : "";
|
||||
}
|
||||
|
||||
function safeExpectedArtifactDir(value: unknown): string {
|
||||
const relativePath = optionalString(value);
|
||||
if (!relativePath) {
|
||||
return "";
|
||||
}
|
||||
if (/^[A-Za-z]:[\\/]/u.test(relativePath) || relativePath.startsWith("/") || relativePath.includes("\0")) {
|
||||
throw new Error("expectedArtifactDir must stay inside the workspace");
|
||||
}
|
||||
const normalized = relativePath.split(/[\\/]/).filter(Boolean).join("/");
|
||||
if (!normalized || normalized.split("/").some((part) => part === ".." || part === ".")) {
|
||||
throw new Error("expectedArtifactDir must stay inside the workspace");
|
||||
}
|
||||
return normalized.endsWith("/") ? normalized : `${normalized}/`;
|
||||
}
|
||||
|
||||
export function normalizeExpectedArtifactDirs(value: unknown): string[] {
|
||||
if (!Array.isArray(value)) {
|
||||
return [];
|
||||
}
|
||||
const seen = new Set<string>();
|
||||
const result: string[] = [];
|
||||
for (const entry of value) {
|
||||
const normalized = safeExpectedArtifactDir(entry);
|
||||
if (!normalized || seen.has(normalized)) {
|
||||
continue;
|
||||
}
|
||||
seen.add(normalized);
|
||||
result.push(normalized);
|
||||
}
|
||||
return result;
|
||||
}
|
||||
@ -2,6 +2,7 @@ import { createHash, createHmac, randomBytes, timingSafeEqual } from "node:crypt
|
||||
import fs from "node:fs/promises";
|
||||
import os from "node:os";
|
||||
import path from "node:path";
|
||||
import { normalizeExpectedArtifactDirs } from "./expectedArtifactDirs.js";
|
||||
|
||||
const DEFAULT_MAX_FILES = 64;
|
||||
const DEFAULT_MAX_INLINE_BYTES = 10 * 1024 * 1024;
|
||||
@ -20,7 +21,7 @@ const SKIPPED_DIRS = new Set([
|
||||
"node_modules",
|
||||
]);
|
||||
|
||||
export type XWorkmateArtifact = {
|
||||
type XWorkmateArtifact = {
|
||||
relativePath: string;
|
||||
label: string;
|
||||
contentType: string;
|
||||
@ -33,9 +34,9 @@ export type XWorkmateArtifact = {
|
||||
content?: string;
|
||||
};
|
||||
|
||||
export type XWorkmateArtifactScopeKind = "task";
|
||||
type XWorkmateArtifactScopeKind = "task";
|
||||
|
||||
export type XWorkmateArtifactExport = {
|
||||
type XWorkmateArtifactExport = {
|
||||
runId: string;
|
||||
sessionKey: string;
|
||||
remoteWorkingDirectory: string;
|
||||
@ -50,7 +51,7 @@ export type XWorkmateArtifactExport = {
|
||||
missingRequiredExtensions: string[];
|
||||
};
|
||||
|
||||
export type XWorkmateArtifactPrepare = {
|
||||
type XWorkmateArtifactPrepare = {
|
||||
runId: string;
|
||||
sessionKey: string;
|
||||
remoteWorkingDirectory: string;
|
||||
@ -64,12 +65,12 @@ export type XWorkmateArtifactPrepare = {
|
||||
expectedArtifactDirStatus: XWorkmateExpectedArtifactDirStatus[];
|
||||
};
|
||||
|
||||
export type XWorkmateExpectedArtifactDirStatus = {
|
||||
type XWorkmateExpectedArtifactDirStatus = {
|
||||
relativePath: string;
|
||||
exists: boolean;
|
||||
};
|
||||
|
||||
export type XWorkmateArtifactSnapshot = {
|
||||
type XWorkmateArtifactSnapshot = {
|
||||
runId: string;
|
||||
sessionKey: string;
|
||||
remoteWorkingDirectory: string;
|
||||
@ -491,25 +492,7 @@ export async function readXWorkmateArtifact(input: ReadInput): Promise<XWorkmate
|
||||
return result;
|
||||
}
|
||||
|
||||
export function normalizeExpectedArtifactDirs(value: unknown): string[] {
|
||||
if (!Array.isArray(value)) {
|
||||
return [];
|
||||
}
|
||||
const seen = new Set<string>();
|
||||
const result: string[] = [];
|
||||
for (const entry of value) {
|
||||
const normalized = safeInputRelativePath(entry, "expectedArtifactDir");
|
||||
const withSlash = normalized.endsWith("/") ? normalized : `${normalized}/`;
|
||||
if (seen.has(withSlash)) {
|
||||
continue;
|
||||
}
|
||||
seen.add(withSlash);
|
||||
result.push(withSlash);
|
||||
}
|
||||
return result;
|
||||
}
|
||||
|
||||
export function normalizeRequiredExtensions(value: unknown): string[] {
|
||||
function normalizeRequiredExtensions(value: unknown): string[] {
|
||||
if (!Array.isArray(value)) {
|
||||
return [];
|
||||
}
|
||||
|
||||
@ -3,14 +3,13 @@ import os from "node:os";
|
||||
import path from "node:path";
|
||||
import { describe, expect, it } from "vitest";
|
||||
import {
|
||||
XWORKMATE_PLUGIN_ID,
|
||||
XWORKMATE_SESSION_EXTENSION_NAMESPACE,
|
||||
getXWorkmateTaskSnapshot,
|
||||
normalizeXWorkmateTaskMetadataV1,
|
||||
recordXWorkmateSessionMapping,
|
||||
readXWorkmateSessionMapping,
|
||||
} from "./taskState.js";
|
||||
|
||||
const XWORKMATE_PLUGIN_ID = "openclaw-multi-session-plugins";
|
||||
|
||||
function createApiFixture(tasks: Record<string, unknown> = {}, pluginConfig: Record<string, unknown> = {}) {
|
||||
const sessions = new Map<string, any>();
|
||||
const api = {
|
||||
@ -66,14 +65,19 @@ async function createWorkspaceFixture() {
|
||||
}
|
||||
|
||||
describe("xworkmate task state mapping", () => {
|
||||
it("requires typed appThreadKey metadata", () => {
|
||||
expect(() =>
|
||||
normalizeXWorkmateTaskMetadataV1({
|
||||
schemaVersion: 1,
|
||||
sessionKey: "draft:legacy",
|
||||
expectedArtifactDirs: ["artifacts/"],
|
||||
it("requires typed appThreadKey metadata", async () => {
|
||||
const { api } = createApiFixture();
|
||||
|
||||
await expect(
|
||||
recordXWorkmateSessionMapping({
|
||||
api,
|
||||
params: {
|
||||
schemaVersion: 1,
|
||||
sessionKey: "draft:legacy",
|
||||
expectedArtifactDirs: ["artifacts/"],
|
||||
},
|
||||
}),
|
||||
).toThrow("appThreadKey required");
|
||||
).rejects.toThrow("appThreadKey required");
|
||||
});
|
||||
|
||||
it("writes a durable pluginExtensions mapping without deriving the OpenClaw key", async () => {
|
||||
@ -273,7 +277,13 @@ describe("xworkmate task state mapping", () => {
|
||||
});
|
||||
|
||||
it("can read mapping by appThreadKey from pluginExtensions", async () => {
|
||||
const { api } = createApiFixture();
|
||||
const { api } = createApiFixture({
|
||||
"draft:lookup:run-1": {
|
||||
taskId: "task-1",
|
||||
runId: "run-1",
|
||||
status: "succeeded",
|
||||
},
|
||||
});
|
||||
await recordXWorkmateSessionMapping({
|
||||
api,
|
||||
params: {
|
||||
@ -284,7 +294,17 @@ describe("xworkmate task state mapping", () => {
|
||||
},
|
||||
});
|
||||
|
||||
await expect(readXWorkmateSessionMapping(api, { appThreadKey: "draft:lookup" })).resolves.toMatchObject({
|
||||
await expect(
|
||||
getXWorkmateTaskSnapshot({
|
||||
api,
|
||||
params: {
|
||||
appThreadKey: "draft:lookup",
|
||||
runId: "run-1",
|
||||
includeArtifacts: false,
|
||||
},
|
||||
}),
|
||||
).resolves.toMatchObject({
|
||||
success: true,
|
||||
appThreadKey: "draft:lookup",
|
||||
openclawSessionKey: "draft:lookup",
|
||||
});
|
||||
|
||||
@ -1,7 +1,8 @@
|
||||
import type { OpenClawPluginApi } from "openclaw/plugin-sdk/core";
|
||||
import { exportXWorkmateArtifacts } from "./exportArtifacts.js";
|
||||
import { normalizeExpectedArtifactDirs } from "./expectedArtifactDirs.js";
|
||||
|
||||
export const XWORKMATE_PLUGIN_ID = "openclaw-multi-session-plugins";
|
||||
const XWORKMATE_PLUGIN_ID = "openclaw-multi-session-plugins";
|
||||
export const XWORKMATE_SESSION_EXTENSION_NAMESPACE = "xworkmate.sessionMapping";
|
||||
|
||||
export type XWorkmateTaskMetadataV1 = {
|
||||
@ -101,7 +102,7 @@ export async function recordXWorkmateSessionMapping(input: {
|
||||
});
|
||||
}
|
||||
|
||||
export function normalizeXWorkmateTaskMetadataV1(input: Record<string, unknown>): XWorkmateTaskMetadataV1 {
|
||||
function normalizeXWorkmateTaskMetadataV1(input: Record<string, unknown>): XWorkmateTaskMetadataV1 {
|
||||
const envelope = asRecord(input.xworkmate) ?? asRecord(input.xworkmateMetadata) ?? input;
|
||||
const schemaVersion = Number(envelope.schemaVersion ?? 1);
|
||||
if (schemaVersion !== 1) {
|
||||
@ -120,30 +121,7 @@ export function normalizeXWorkmateTaskMetadataV1(input: Record<string, unknown>)
|
||||
}) as XWorkmateTaskMetadataV1;
|
||||
}
|
||||
|
||||
export function normalizeExpectedArtifactDirs(value: unknown): string[] {
|
||||
if (!Array.isArray(value)) {
|
||||
return [];
|
||||
}
|
||||
const seen = new Set<string>();
|
||||
const result: string[] = [];
|
||||
for (const entry of value) {
|
||||
const text = optionalString(entry).replaceAll("\\", "/").replace(/^\.\/+/u, "");
|
||||
if (!text || seen.has(text)) {
|
||||
continue;
|
||||
}
|
||||
if (text.startsWith("/") || /^[A-Za-z]:\//u.test(text) || text.split("/").includes("..")) {
|
||||
throw new Error("expectedArtifactDirs must be relative paths without traversal");
|
||||
}
|
||||
const normalized = text.endsWith("/") ? text : `${text}/`;
|
||||
if (!seen.has(normalized)) {
|
||||
seen.add(normalized);
|
||||
result.push(normalized);
|
||||
}
|
||||
}
|
||||
return result;
|
||||
}
|
||||
|
||||
export async function upsertXWorkmateSessionMapping(
|
||||
async function upsertXWorkmateSessionMapping(
|
||||
api: OpenClawPluginApi,
|
||||
input: {
|
||||
metadata: XWorkmateTaskMetadataV1;
|
||||
@ -198,7 +176,7 @@ export async function upsertXWorkmateSessionMapping(
|
||||
return mapping;
|
||||
}
|
||||
|
||||
export async function readXWorkmateSessionMapping(
|
||||
async function readXWorkmateSessionMapping(
|
||||
api: OpenClawPluginApi,
|
||||
lookup: {
|
||||
appThreadKey?: string;
|
||||
|
||||
Loading…
Reference in New Issue
Block a user