Files
openclaw/src/cli/claws-cli.project.test.ts
T
Gio Della-Libera 7422222788 feat(claws): add local project author lifecycle (#117037)
* feat(claws): add local project lifecycle

* fix(claws): harden local project lifecycle

* fix(claws): harden project source selection

Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
Copilot-Session: 3cdcdb00-ade8-4e61-85a7-8151b35f216a

* fix(claws): preserve project archive paths

Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
Copilot-Session: db3a73db-01ff-428e-a451-b7b710cd5085

* fix(claws): harden author lifecycle

Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
Copilot-Session: db3a73db-01ff-428e-a451-b7b710cd5085

* chore(claws): normalize authoring test mode

Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
Copilot-Session: db3a73db-01ff-428e-a451-b7b710cd5085

* fix(claws): normalize archive path spellings

Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
Copilot-Session: db3a73db-01ff-428e-a451-b7b710cd5085

* fix(claws): exclude repository-local sources

Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
Copilot-Session: db3a73db-01ff-428e-a451-b7b710cd5085

* fix(claws): satisfy CI contracts

Co-authored-by: Copilot 223556219+Copilot@users.noreply.github.com

Copilot-Session: db3a73db-01ff-428e-a451-b7b710cd5085

* chore: record CI fix attribution

Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
Copilot-Session: db3a73db-01ff-428e-a451-b7b710cd5085

* fix(claws): reject excluded manifest links

Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
Copilot-Session: db3a73db-01ff-428e-a451-b7b710cd5085

---------

Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
Copilot-Session: 3cdcdb00-ade8-4e61-85a7-8151b35f216a
Copilot-Session: db3a73db-01ff-428e-a451-b7b710cd5085
2026-08-09 03:30:21 -07:00

132 lines
4.7 KiB
TypeScript

import { join } from "node:path";
import { afterEach, beforeEach, describe, expect, it, vi } from "vitest";
import { useAutoCleanupTempDirTracker } from "../../test/helpers/temp-dir.js";
const mocks = vi.hoisted(() => {
const payloads: unknown[] = [];
return {
payloads,
runtime: {
log: vi.fn(),
error: vi.fn(),
writeJson: vi.fn((value: unknown) => payloads.push(value)),
writeStdout: vi.fn(),
exit: vi.fn((code: number) => {
throw new Error(`__exit__:${code}`);
}),
},
};
});
vi.mock("../runtime.js", async () => ({
...(await vi.importActual<typeof import("../runtime.js")>("../runtime.js")),
defaultRuntime: mocks.runtime,
writeRuntimeJson: (runtime: typeof mocks.runtime, value: unknown) => runtime.writeJson(value),
}));
vi.mock("../config/config.js", async () => ({
...(await vi.importActual<typeof import("../config/config.js")>("../config/config.js")),
readConfigFileSnapshot: async () => ({
exists: true,
valid: true,
issues: [],
warnings: [],
legacyIssues: [],
path: "/tmp/openclaw.json",
raw: {},
sourceConfig: {},
resolved: {},
}),
}));
const { runClawsBuildCommand, runClawsCreateCommand, runClawsDevCommand, runClawsValidateCommand } =
await import("./claws-cli.project.js");
const tempDirs = useAutoCleanupTempDirTracker(afterEach);
describe("Claw project CLI", () => {
beforeEach(() => {
vi.stubEnv("OPENCLAW_EXPERIMENTAL_CLAWS", "1");
mocks.payloads.length = 0;
});
it("runs create, validate, build, and offline dev against the built artifact", async () => {
const root = join(tempDirs.make("openclaw-claw-author-"), "author-flow");
const artifact = join(tempDirs.make("openclaw-claw-author-output-"), "author-flow.tgz");
await runClawsCreateCommand(root, { json: true });
await runClawsValidateCommand(root, { json: true });
await runClawsBuildCommand(root, { out: artifact, json: true });
await runClawsDevCommand(root, {
agentId: "author-flow-preview",
workspace: join(root, "preview-workspace"),
json: true,
});
const payloads = mocks.payloads as Array<Record<string, unknown>>;
expect(payloads.map((payload) => payload.schemaVersion)).toEqual([
"openclaw.clawProject.v1",
"openclaw.clawProject.v1",
"openclaw.clawBuild.v1",
"openclaw.clawDev.v1",
]);
expect(payloads[1]).toMatchObject({ excludedPaths: [] });
expect(payloads[2]).toMatchObject({ excludedPaths: [] });
const dev = payloads[3] as { mutationAllowed: boolean; offline: boolean; plan: ClawPlan };
expect(dev).toMatchObject({ mutationAllowed: false, offline: true });
expect(dev.plan).toMatchObject({ mutationAllowed: false, blockers: [] });
expect(dev.plan.claw).toMatchObject({
integrityKind: "artifact",
integrity: (payloads[2] as { integrity: string }).integrity,
});
expect(dev.plan.claw.packageRoot).toBe(
`claw-artifact:${(payloads[2] as { integrity: string }).integrity}`,
);
});
it("emits stable dev plans without deleted extraction paths", async () => {
const root = join(tempDirs.make("openclaw-claw-dev-stable-"), "stable-dev");
await runClawsCreateCommand(root, { json: true });
const options = {
agentId: "stable-dev-preview",
workspace: join(root, "preview-workspace"),
json: true,
};
await runClawsDevCommand(root, options);
await runClawsDevCommand(root, options);
const payloads = mocks.payloads as Array<Record<string, unknown>>;
const first = payloads[1] as { plan: ClawPlan };
const second = payloads[2] as { plan: ClawPlan };
expect(first.plan).toEqual(second.plan);
expect(first.plan.planIntegrity).toBe(second.plan.planIntegrity);
expect(JSON.stringify(first.plan)).not.toContain("openclaw-claw-artifact-");
expect(first.plan.claw.packageRoot).toMatch(/^claw-artifact:sha256:/u);
});
it("uses command-specific schemas for build and dev failures", async () => {
const missing = join(tempDirs.make("openclaw-claw-errors-"), "missing");
await expect(
runClawsBuildCommand(missing, {
out: join(tempDirs.make("openclaw-claw-error-output-"), "missing.tgz"),
json: true,
}),
).rejects.toThrow("__exit__:1");
await expect(runClawsDevCommand(missing, { json: true })).rejects.toThrow("__exit__:1");
const payloads = mocks.payloads as Array<Record<string, unknown>>;
expect(payloads).toMatchObject([
{ schemaVersion: "openclaw.clawBuild.v1", ok: false },
{ schemaVersion: "openclaw.clawDev.v1", ok: false },
]);
});
});
type ClawPlan = {
mutationAllowed: boolean;
planIntegrity: string;
blockers: unknown[];
claw: { integrityKind: string; integrity: string; packageRoot: string };
};