mirror of
https://github.com/openclaw/openclaw.git
synced 2026-05-06 06:20:43 +00:00
test: trim boundary and fixture hotspots
This commit is contained in:
@@ -167,20 +167,16 @@ function expectShellPayloadApprovalDenied(params: {
|
||||
if (process.platform === "win32") {
|
||||
return;
|
||||
}
|
||||
const tmp = fs.mkdtempSync(path.join(os.tmpdir(), params.tmpPrefix));
|
||||
try {
|
||||
const scriptPath = path.join(tmp, params.fileName);
|
||||
fs.writeFileSync(scriptPath, params.body);
|
||||
fs.chmodSync(scriptPath, 0o755);
|
||||
const prepared = buildSystemRunApprovalPlan({
|
||||
command: ["/bin/sh", "-lc", scriptPath],
|
||||
rawCommand: scriptPath,
|
||||
cwd: tmp,
|
||||
});
|
||||
expect(prepared).toEqual(DENIED_RUNTIME_APPROVAL);
|
||||
} finally {
|
||||
fs.rmSync(tmp, { recursive: true, force: true });
|
||||
}
|
||||
const tmp = createFixtureDir(params.tmpPrefix);
|
||||
const scriptPath = path.join(tmp, params.fileName);
|
||||
fs.writeFileSync(scriptPath, params.body);
|
||||
fs.chmodSync(scriptPath, 0o755);
|
||||
const prepared = buildSystemRunApprovalPlan({
|
||||
command: ["/bin/sh", "-lc", scriptPath],
|
||||
rawCommand: scriptPath,
|
||||
cwd: tmp,
|
||||
});
|
||||
expect(prepared).toEqual(DENIED_RUNTIME_APPROVAL);
|
||||
}
|
||||
|
||||
function expectMutableFileOperandApprovalPlan(fixture: ScriptOperandFixture, cwd: string) {
|
||||
@@ -478,7 +474,7 @@ describe("hardenApprovedExecutionPaths", () => {
|
||||
];
|
||||
|
||||
it.runIf(process.platform !== "win32").each(cases)("$name", (testCase) => {
|
||||
const tmp = fs.mkdtempSync(path.join(os.tmpdir(), "openclaw-approval-hardening-"));
|
||||
const tmp = createFixtureDir("openclaw-approval-hardening-");
|
||||
const oldPath = process.env.PATH;
|
||||
let pathToken: PathTokenSetup | null = null;
|
||||
if (testCase.withPathToken) {
|
||||
@@ -534,7 +530,6 @@ describe("hardenApprovedExecutionPaths", () => {
|
||||
process.env.PATH = oldPath;
|
||||
}
|
||||
}
|
||||
fs.rmSync(tmp, { recursive: true, force: true });
|
||||
}
|
||||
});
|
||||
|
||||
@@ -847,49 +842,41 @@ describe("hardenApprovedExecutionPaths", () => {
|
||||
if (process.platform === "win32") {
|
||||
return;
|
||||
}
|
||||
const tmp = fs.mkdtempSync(path.join(os.tmpdir(), "openclaw-shell-relative-binary-binding-"));
|
||||
try {
|
||||
const binaryPath = resolveNativeBinaryFixturePath();
|
||||
const relativeBinaryPath = path.join(tmp, "tool");
|
||||
fs.copyFileSync(binaryPath, relativeBinaryPath);
|
||||
fs.chmodSync(relativeBinaryPath, 0o755);
|
||||
const prepared = buildSystemRunApprovalPlan({
|
||||
command: ["/bin/sh", "-lc", "./tool"],
|
||||
rawCommand: "./tool",
|
||||
cwd: tmp,
|
||||
});
|
||||
expect(prepared).toEqual(DENIED_RUNTIME_APPROVAL);
|
||||
} finally {
|
||||
fs.rmSync(tmp, { recursive: true, force: true });
|
||||
}
|
||||
const tmp = createFixtureDir("openclaw-shell-relative-binary-binding-");
|
||||
const binaryPath = resolveNativeBinaryFixturePath();
|
||||
const relativeBinaryPath = path.join(tmp, "tool");
|
||||
fs.copyFileSync(binaryPath, relativeBinaryPath);
|
||||
fs.chmodSync(relativeBinaryPath, 0o755);
|
||||
const prepared = buildSystemRunApprovalPlan({
|
||||
command: ["/bin/sh", "-lc", "./tool"],
|
||||
rawCommand: "./tool",
|
||||
cwd: tmp,
|
||||
});
|
||||
expect(prepared).toEqual(DENIED_RUNTIME_APPROVAL);
|
||||
});
|
||||
|
||||
it("keeps fail-closed behavior for writable absolute native-binary shell payloads", () => {
|
||||
if (process.platform === "win32") {
|
||||
return;
|
||||
}
|
||||
const tmp = fs.mkdtempSync(path.join(os.tmpdir(), "openclaw-shell-absolute-binary-binding-"));
|
||||
try {
|
||||
const binaryPath = resolveNativeBinaryFixturePath();
|
||||
const copiedBinaryPath = path.join(tmp, "tool");
|
||||
fs.copyFileSync(binaryPath, copiedBinaryPath);
|
||||
fs.chmodSync(copiedBinaryPath, 0o755);
|
||||
const prepared = buildSystemRunApprovalPlan({
|
||||
command: ["/bin/sh", "-lc", copiedBinaryPath],
|
||||
rawCommand: copiedBinaryPath,
|
||||
cwd: tmp,
|
||||
});
|
||||
expect(prepared).toEqual(DENIED_RUNTIME_APPROVAL);
|
||||
} finally {
|
||||
fs.rmSync(tmp, { recursive: true, force: true });
|
||||
}
|
||||
const tmp = createFixtureDir("openclaw-shell-absolute-binary-binding-");
|
||||
const binaryPath = resolveNativeBinaryFixturePath();
|
||||
const copiedBinaryPath = path.join(tmp, "tool");
|
||||
fs.copyFileSync(binaryPath, copiedBinaryPath);
|
||||
fs.chmodSync(copiedBinaryPath, 0o755);
|
||||
const prepared = buildSystemRunApprovalPlan({
|
||||
command: ["/bin/sh", "-lc", copiedBinaryPath],
|
||||
rawCommand: copiedBinaryPath,
|
||||
cwd: tmp,
|
||||
});
|
||||
expect(prepared).toEqual(DENIED_RUNTIME_APPROVAL);
|
||||
});
|
||||
|
||||
it("keeps fail-closed behavior for owner-controlled read-only absolute binaries", () => {
|
||||
if (process.platform === "win32") {
|
||||
return;
|
||||
}
|
||||
const tmp = fs.mkdtempSync(path.join(os.tmpdir(), "openclaw-shell-owned-readonly-binding-"));
|
||||
const tmp = createFixtureDir("openclaw-shell-owned-readonly-binding-");
|
||||
const binaryPath = path.join(tmp, "tool");
|
||||
try {
|
||||
fs.copyFileSync(resolveNativeBinaryFixturePath(), binaryPath);
|
||||
@@ -903,7 +890,6 @@ describe("hardenApprovedExecutionPaths", () => {
|
||||
expect(prepared).toEqual(DENIED_RUNTIME_APPROVAL);
|
||||
} finally {
|
||||
fs.chmodSync(tmp, 0o755);
|
||||
fs.rmSync(tmp, { recursive: true, force: true });
|
||||
}
|
||||
});
|
||||
|
||||
@@ -911,7 +897,7 @@ describe("hardenApprovedExecutionPaths", () => {
|
||||
if (process.platform === "win32") {
|
||||
return;
|
||||
}
|
||||
const tmp = fs.mkdtempSync(path.join(os.tmpdir(), "openclaw-shell-symlink-binary-binding-"));
|
||||
const tmp = createFixtureDir("openclaw-shell-symlink-binary-binding-");
|
||||
const stableDir = path.join(tmp, "stable");
|
||||
const mutableDir = path.join(tmp, "mutable");
|
||||
try {
|
||||
@@ -932,7 +918,6 @@ describe("hardenApprovedExecutionPaths", () => {
|
||||
expect(prepared).toEqual(DENIED_RUNTIME_APPROVAL);
|
||||
} finally {
|
||||
fs.chmodSync(stableDir, 0o755);
|
||||
fs.rmSync(tmp, { recursive: true, force: true });
|
||||
}
|
||||
});
|
||||
|
||||
@@ -972,35 +957,31 @@ describe("hardenApprovedExecutionPaths", () => {
|
||||
if (process.platform === "win32") {
|
||||
return;
|
||||
}
|
||||
const tmp = fs.mkdtempSync(path.join(os.tmpdir(), "openclaw-shell-race-binding-"));
|
||||
try {
|
||||
const scriptPath = path.join(tmp, "run.sh");
|
||||
fs.writeFileSync(scriptPath, "#!/bin/sh\necho SAFE\n");
|
||||
fs.chmodSync(scriptPath, 0o755);
|
||||
const realStatSync = fs.statSync;
|
||||
let targetStatCalls = 0;
|
||||
const statSyncSpy = vi.spyOn(fs, "statSync").mockImplementation((pathLike, options) => {
|
||||
const targetPath = typeof pathLike === "string" ? pathLike : pathLike.toString();
|
||||
if (targetPath === scriptPath) {
|
||||
targetStatCalls += 1;
|
||||
if (targetStatCalls === 2) {
|
||||
return realStatSync(tmp, options);
|
||||
}
|
||||
const tmp = createFixtureDir("openclaw-shell-race-binding-");
|
||||
const scriptPath = path.join(tmp, "run.sh");
|
||||
fs.writeFileSync(scriptPath, "#!/bin/sh\necho SAFE\n");
|
||||
fs.chmodSync(scriptPath, 0o755);
|
||||
const realStatSync = fs.statSync;
|
||||
let targetStatCalls = 0;
|
||||
const statSyncSpy = vi.spyOn(fs, "statSync").mockImplementation((pathLike, options) => {
|
||||
const targetPath = typeof pathLike === "string" ? pathLike : pathLike.toString();
|
||||
if (targetPath === scriptPath) {
|
||||
targetStatCalls += 1;
|
||||
if (targetStatCalls === 2) {
|
||||
return realStatSync(tmp, options);
|
||||
}
|
||||
return realStatSync(pathLike, options);
|
||||
});
|
||||
try {
|
||||
const prepared = buildSystemRunApprovalPlan({
|
||||
command: ["/bin/sh", "-lc", scriptPath],
|
||||
rawCommand: scriptPath,
|
||||
cwd: tmp,
|
||||
});
|
||||
expect(prepared).toEqual(DENIED_RUNTIME_APPROVAL);
|
||||
} finally {
|
||||
statSyncSpy.mockRestore();
|
||||
}
|
||||
return realStatSync(pathLike, options);
|
||||
});
|
||||
try {
|
||||
const prepared = buildSystemRunApprovalPlan({
|
||||
command: ["/bin/sh", "-lc", scriptPath],
|
||||
rawCommand: scriptPath,
|
||||
cwd: tmp,
|
||||
});
|
||||
expect(prepared).toEqual(DENIED_RUNTIME_APPROVAL);
|
||||
} finally {
|
||||
fs.rmSync(tmp, { recursive: true, force: true });
|
||||
statSyncSpy.mockRestore();
|
||||
}
|
||||
});
|
||||
|
||||
@@ -1008,13 +989,9 @@ describe("hardenApprovedExecutionPaths", () => {
|
||||
withFakeRuntimeBin({
|
||||
binName: testCase.binName,
|
||||
run: () => {
|
||||
const tmp = fs.mkdtempSync(path.join(os.tmpdir(), testCase.tmpPrefix));
|
||||
try {
|
||||
testCase.setup?.(tmp);
|
||||
expectRuntimeApprovalDenied(testCase.command, tmp);
|
||||
} finally {
|
||||
fs.rmSync(tmp, { recursive: true, force: true });
|
||||
}
|
||||
const tmp = createFixtureDir(testCase.tmpPrefix);
|
||||
testCase.setup?.(tmp);
|
||||
expectRuntimeApprovalDenied(testCase.command, tmp);
|
||||
},
|
||||
});
|
||||
});
|
||||
@@ -1062,19 +1039,15 @@ describe("hardenApprovedExecutionPaths", () => {
|
||||
withFakeRuntimeBins({
|
||||
binNames: ["pnpm", "tsx"],
|
||||
run: () => {
|
||||
const tmp = fs.mkdtempSync(path.join(os.tmpdir(), "openclaw-pnpm-dlx-shell-mode-"));
|
||||
try {
|
||||
fs.writeFileSync(path.join(tmp, "run.ts"), 'console.log("SAFE");\n');
|
||||
expect(
|
||||
resolveMutableFileOperandSnapshotSync({
|
||||
argv: ["pnpm", "dlx", "--shell-mode", "tsx ./run.ts"],
|
||||
cwd: tmp,
|
||||
shellCommand: null,
|
||||
}),
|
||||
).toEqual({ ok: true, snapshot: null });
|
||||
} finally {
|
||||
fs.rmSync(tmp, { recursive: true, force: true });
|
||||
}
|
||||
const tmp = createFixtureDir("openclaw-pnpm-dlx-shell-mode-");
|
||||
fs.writeFileSync(path.join(tmp, "run.ts"), 'console.log("SAFE");\n');
|
||||
expect(
|
||||
resolveMutableFileOperandSnapshotSync({
|
||||
argv: ["pnpm", "dlx", "--shell-mode", "tsx ./run.ts"],
|
||||
cwd: tmp,
|
||||
shellCommand: null,
|
||||
}),
|
||||
).toEqual({ ok: true, snapshot: null });
|
||||
},
|
||||
});
|
||||
});
|
||||
@@ -1083,12 +1056,8 @@ describe("hardenApprovedExecutionPaths", () => {
|
||||
withFakeRuntimeBin({
|
||||
binName: "pnpm",
|
||||
run: () => {
|
||||
const tmp = fs.mkdtempSync(path.join(os.tmpdir(), "openclaw-pnpm-dlx-package-bin-"));
|
||||
try {
|
||||
expectApprovalPlanWithoutMutableOperand(["pnpm", "dlx", "cowsay", "hello"], tmp);
|
||||
} finally {
|
||||
fs.rmSync(tmp, { recursive: true, force: true });
|
||||
}
|
||||
const tmp = createFixtureDir("openclaw-pnpm-dlx-package-bin-");
|
||||
expectApprovalPlanWithoutMutableOperand(["pnpm", "dlx", "cowsay", "hello"], tmp);
|
||||
},
|
||||
});
|
||||
});
|
||||
@@ -1097,14 +1066,8 @@ describe("hardenApprovedExecutionPaths", () => {
|
||||
withFakeRuntimeBin({
|
||||
binName: "pnpm",
|
||||
run: () => {
|
||||
const tmp = fs.mkdtempSync(
|
||||
path.join(os.tmpdir(), "openclaw-pnpm-dlx-package-runtime-token-"),
|
||||
);
|
||||
try {
|
||||
expectApprovalPlanWithoutMutableOperand(["pnpm", "dlx", "cowsay", "node"], tmp);
|
||||
} finally {
|
||||
fs.rmSync(tmp, { recursive: true, force: true });
|
||||
}
|
||||
const tmp = createFixtureDir("openclaw-pnpm-dlx-package-runtime-token-");
|
||||
expectApprovalPlanWithoutMutableOperand(["pnpm", "dlx", "cowsay", "node"], tmp);
|
||||
},
|
||||
});
|
||||
});
|
||||
@@ -1113,14 +1076,8 @@ describe("hardenApprovedExecutionPaths", () => {
|
||||
withFakeRuntimeBin({
|
||||
binName: "pnpm",
|
||||
run: () => {
|
||||
const tmp = fs.mkdtempSync(
|
||||
path.join(os.tmpdir(), "openclaw-pnpm-dlx-package-runtime-token-multi-"),
|
||||
);
|
||||
try {
|
||||
expectApprovalPlanWithoutMutableOperand(["pnpm", "dlx", "cowsay", "node", "hello"], tmp);
|
||||
} finally {
|
||||
fs.rmSync(tmp, { recursive: true, force: true });
|
||||
}
|
||||
const tmp = createFixtureDir("openclaw-pnpm-dlx-package-runtime-token-multi-");
|
||||
expectApprovalPlanWithoutMutableOperand(["pnpm", "dlx", "cowsay", "node", "hello"], tmp);
|
||||
},
|
||||
});
|
||||
});
|
||||
@@ -1129,14 +1086,10 @@ describe("hardenApprovedExecutionPaths", () => {
|
||||
withFakeRuntimeBins({
|
||||
binNames: ["pnpm", "eslint"],
|
||||
run: () => {
|
||||
const tmp = fs.mkdtempSync(path.join(os.tmpdir(), "openclaw-pnpm-dlx-package-file-"));
|
||||
try {
|
||||
fs.mkdirSync(path.join(tmp, "src"), { recursive: true });
|
||||
fs.writeFileSync(path.join(tmp, "src", "index.ts"), 'console.log("SAFE");\n');
|
||||
expectApprovalPlanWithoutMutableOperand(["pnpm", "dlx", "eslint", "src/index.ts"], tmp);
|
||||
} finally {
|
||||
fs.rmSync(tmp, { recursive: true, force: true });
|
||||
}
|
||||
const tmp = createFixtureDir("openclaw-pnpm-dlx-package-file-");
|
||||
fs.mkdirSync(path.join(tmp, "src"), { recursive: true });
|
||||
fs.writeFileSync(path.join(tmp, "src", "index.ts"), 'console.log("SAFE");\n');
|
||||
expectApprovalPlanWithoutMutableOperand(["pnpm", "dlx", "eslint", "src/index.ts"], tmp);
|
||||
},
|
||||
});
|
||||
});
|
||||
@@ -1145,16 +1098,9 @@ describe("hardenApprovedExecutionPaths", () => {
|
||||
withFakeRuntimeBin({
|
||||
binName: "pnpm",
|
||||
run: () => {
|
||||
const tmp = fs.mkdtempSync(path.join(os.tmpdir(), "openclaw-pnpm-dlx-package-data-tail-"));
|
||||
try {
|
||||
fs.writeFileSync(path.join(tmp, "run.ts"), 'console.log("SAFE");\n');
|
||||
expectApprovalPlanWithoutMutableOperand(
|
||||
["pnpm", "dlx", "cowsay", "tsx", "./run.ts"],
|
||||
tmp,
|
||||
);
|
||||
} finally {
|
||||
fs.rmSync(tmp, { recursive: true, force: true });
|
||||
}
|
||||
const tmp = createFixtureDir("openclaw-pnpm-dlx-package-data-tail-");
|
||||
fs.writeFileSync(path.join(tmp, "run.ts"), 'console.log("SAFE");\n');
|
||||
expectApprovalPlanWithoutMutableOperand(["pnpm", "dlx", "cowsay", "tsx", "./run.ts"], tmp);
|
||||
},
|
||||
});
|
||||
});
|
||||
@@ -1183,26 +1129,22 @@ describe("hardenApprovedExecutionPaths", () => {
|
||||
});
|
||||
|
||||
it("captures the real shell script operand after value-taking shell flags", () => {
|
||||
const tmp = fs.mkdtempSync(path.join(os.tmpdir(), "openclaw-shell-option-value-"));
|
||||
try {
|
||||
const scriptPath = path.join(tmp, "run.sh");
|
||||
fs.writeFileSync(scriptPath, "#!/bin/sh\necho SAFE\n");
|
||||
fs.writeFileSync(path.join(tmp, "errexit"), "decoy\n");
|
||||
const snapshot = resolveMutableFileOperandSnapshotSync({
|
||||
argv: ["/bin/bash", "-o", "errexit", "./run.sh"],
|
||||
cwd: tmp,
|
||||
shellCommand: null,
|
||||
});
|
||||
expect(snapshot).toEqual({
|
||||
ok: true,
|
||||
snapshot: {
|
||||
argvIndex: 3,
|
||||
path: fs.realpathSync(scriptPath),
|
||||
sha256: expect.any(String),
|
||||
},
|
||||
});
|
||||
} finally {
|
||||
fs.rmSync(tmp, { recursive: true, force: true });
|
||||
}
|
||||
const tmp = createFixtureDir("openclaw-shell-option-value-");
|
||||
const scriptPath = path.join(tmp, "run.sh");
|
||||
fs.writeFileSync(scriptPath, "#!/bin/sh\necho SAFE\n");
|
||||
fs.writeFileSync(path.join(tmp, "errexit"), "decoy\n");
|
||||
const snapshot = resolveMutableFileOperandSnapshotSync({
|
||||
argv: ["/bin/bash", "-o", "errexit", "./run.sh"],
|
||||
cwd: tmp,
|
||||
shellCommand: null,
|
||||
});
|
||||
expect(snapshot).toEqual({
|
||||
ok: true,
|
||||
snapshot: {
|
||||
argvIndex: 3,
|
||||
path: fs.realpathSync(scriptPath),
|
||||
sha256: expect.any(String),
|
||||
},
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
@@ -237,16 +237,7 @@ describe("plugin activation boundary", () => {
|
||||
]);
|
||||
|
||||
loadBundledPluginPublicSurfaceModuleSync.mockReset();
|
||||
const { getSessionBindingService } =
|
||||
await import("./infra/outbound/session-binding-service.js");
|
||||
|
||||
await expect(browser.closeTrackedBrowserTabsForSessions({ sessionKeys: [] })).resolves.toBe(0);
|
||||
await expect(
|
||||
getSessionBindingService().unbind({
|
||||
targetSessionKey: "agent:main:test",
|
||||
reason: "session-reset",
|
||||
}),
|
||||
).resolves.toEqual([]);
|
||||
expect(loadBundledPluginPublicSurfaceModuleSync).not.toHaveBeenCalled();
|
||||
});
|
||||
});
|
||||
|
||||
@@ -477,31 +477,6 @@ describe("chat view", () => {
|
||||
expect(container.textContent).not.toContain("Stop");
|
||||
});
|
||||
|
||||
it("shows sender labels from sanitized gateway messages instead of generic You", () => {
|
||||
const container = document.createElement("div");
|
||||
render(
|
||||
renderChat(
|
||||
createProps({
|
||||
messages: [
|
||||
{
|
||||
role: "user",
|
||||
content: "hello from topic",
|
||||
senderLabel: "Iris",
|
||||
timestamp: 1000,
|
||||
},
|
||||
],
|
||||
}),
|
||||
),
|
||||
container,
|
||||
);
|
||||
|
||||
const senderLabels = Array.from(container.querySelectorAll(".chat-sender-name")).map((node) =>
|
||||
node.textContent?.trim(),
|
||||
);
|
||||
expect(senderLabels).toContain("Iris");
|
||||
expect(senderLabels).not.toContain("You");
|
||||
});
|
||||
|
||||
it("keeps consecutive user messages from different senders in separate groups", () => {
|
||||
const container = document.createElement("div");
|
||||
render(
|
||||
@@ -533,6 +508,7 @@ describe("chat view", () => {
|
||||
);
|
||||
expect(senderLabels).toContain("Iris");
|
||||
expect(senderLabels).toContain("Joaquin De Rojas");
|
||||
expect(senderLabels).not.toContain("You");
|
||||
});
|
||||
|
||||
it("positions delete confirm by message side", () => {
|
||||
@@ -776,36 +752,7 @@ describe("chat view", () => {
|
||||
expect(container.textContent).toContain('"childSessionKey": "agent:test:subagent:abc123"');
|
||||
});
|
||||
|
||||
it("renders [embed] shortcodes inside the assistant bubble", () => {
|
||||
const container = document.createElement("div");
|
||||
render(
|
||||
renderChat(
|
||||
createProps({
|
||||
showToolCalls: false,
|
||||
messages: [
|
||||
{
|
||||
id: "assistant-anki-inline",
|
||||
role: "assistant",
|
||||
content: [
|
||||
{
|
||||
type: "text",
|
||||
text: 'Still the same current card.\n[embed ref="cv_shortcode" title="Shortcode view" /]',
|
||||
},
|
||||
],
|
||||
timestamp: Date.now(),
|
||||
},
|
||||
],
|
||||
}),
|
||||
),
|
||||
container,
|
||||
);
|
||||
|
||||
expect(container.querySelector(".chat-tool-card__preview-frame")).not.toBeNull();
|
||||
expect(container.textContent).toContain("Still the same current card.");
|
||||
expect(container.textContent).toContain("Shortcode view");
|
||||
});
|
||||
|
||||
it("renders canvas-only assistant bubbles", () => {
|
||||
it("renders canvas-only [embed] shortcodes inside the assistant bubble", () => {
|
||||
const container = document.createElement("div");
|
||||
render(
|
||||
renderChat(
|
||||
@@ -815,7 +762,12 @@ describe("chat view", () => {
|
||||
{
|
||||
id: "assistant-canvas-only",
|
||||
role: "assistant",
|
||||
content: [{ type: "text", text: '[embed ref="cv_tictactoe" title="Tic-Tac-Toe" /]' }],
|
||||
content: [
|
||||
{
|
||||
type: "text",
|
||||
text: '[embed ref="cv_tictactoe" title="Tic-Tac-Toe" /]',
|
||||
},
|
||||
],
|
||||
timestamp: Date.now(),
|
||||
},
|
||||
],
|
||||
|
||||
Reference in New Issue
Block a user