Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
11 changes: 11 additions & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
@@ -1,5 +1,16 @@
# Changelog

## 3.0.1

- Include the conventional `tests/opencloud.e2e.js` source in deterministic
bundles, validate its bounded test contract locally, and expose its immutable
SHA-256 metadata.
- Require exact-revision external E2E evidence during `app dev verify`, with an
optional bounded parallelism override, so a missing specification fails
closed instead of producing a legacy receipt.
- Generate new projects with a deliberately failing external E2E starter that
agents must replace with the app's primary outcomes before promotion.

## 3.0.0

- Vendor the hard-cut OpenCloud browser SDK 2.0.0 and final schema-2 manifest
Expand Down
13 changes: 8 additions & 5 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -12,12 +12,12 @@ offline source bundle, but cannot connect to or deploy through OpenCloud.

## Install a pinned release

OpenCloud application skills pin an exact CLI release. To install `v3.0.0` in
OpenCloud application skills pin an exact CLI release. To install `v3.0.1` in
an isolated task directory:

```bash
OPENCLOUD_CLI_VERSION="v3.0.0"
OPENCLOUD_CLI_PACKAGE="opencloud-cli-3.0.0.tgz"
OPENCLOUD_CLI_VERSION="v3.0.1"
OPENCLOUD_CLI_PACKAGE="opencloud-cli-3.0.1.tgz"
OPENCLOUD_CLI_DIR="$(mktemp -d)"

curl -fsSLo "$OPENCLOUD_CLI_DIR/$OPENCLOUD_CLI_PACKAGE" \
Expand Down Expand Up @@ -169,7 +169,7 @@ Use the stable capability preview and isolated migration-replayed database befor
--id "$ITEM_ID" --values '{"title":"Updated preview item"}'
"$OPENCLOUD_CLI" app dev invoke . function-name --body '{"example":true}'
"$OPENCLOUD_CLI" app dev requests .
"$OPENCLOUD_CLI" app dev verify .
"$OPENCLOUD_CLI" app dev verify . --parallelism 5
"$OPENCLOUD_CLI" app dev promote . --idempotency-key "$IDEMPOTENCY_KEY"
"$OPENCLOUD_CLI" app dev receipts .
"$OPENCLOUD_CLI" app dev evidence .
Expand All @@ -182,7 +182,10 @@ configured required values remain unavailable and optional values may be
absent. Functions
imported from `@opencloud/server` remain dormant until `app dev invoke` or a
deliberate preview interaction calls them. Exact-revision verification requires
every declared Function to have a successful explicit invocation.
every declared Function to have a successful explicit invocation and runs the
immutable `tests/opencloud.e2e.js` specification. The conventional test source
stays outside `frontend.directory`, is included in the deterministic artifact,
and must use only the bounded `@opencloud/test` UI fixtures.

`app dev promote` is the completion path: it deploys only the verified receipt,
follows the durable production operation, runs feature-aware production
Expand Down
4 changes: 2 additions & 2 deletions package-lock.json

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

2 changes: 1 addition & 1 deletion package.json
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
{
"name": "@opencloud/cli",
"version": "3.0.0",
"version": "3.0.1",
"description": "Versioned command-line client for building, deploying, and verifying OpenCloud applications",
"type": "module",
"bin": {
Expand Down
185 changes: 184 additions & 1 deletion src/bundle.test.ts
Original file line number Diff line number Diff line change
@@ -1,9 +1,14 @@
import { createHash } from "node:crypto";
import { mkdtemp, mkdir, rm, symlink, writeFile } from "node:fs/promises";
import os from "node:os";
import path from "node:path";
import { afterEach, describe, expect, it } from "vitest";
import * as tar from "tar";
import { buildBundle } from "./bundle.js";
import {
buildBundle,
OPEN_CLOUD_E2E_TEST_MAX_BYTES,
OPEN_CLOUD_E2E_TEST_PATH,
} from "./bundle.js";

const temporary: string[] = [];

Expand Down Expand Up @@ -330,3 +335,181 @@ functions:
);
});
});

describe("conventional OpenCloud E2E tests", () => {
const validE2eSource = `import { expect, test } from "@opencloud/test";

test("creates a record through the UI", async ({ ownerPage }) => {
await ownerPage.getByRole("button", { name: "Add item" }).click();
await expect(ownerPage.getByRole("status")).toHaveText("Item added");
});
`;

it("keeps legacy bundles unchanged when the conventional spec is absent", async () => {
const directory = await e2eFixture();

const bundle = await buildBundle(directory);

expect(bundle.e2eTest).toBeUndefined();
expect(bundle.files).not.toContain(OPEN_CLOUD_E2E_TEST_PATH);
expect(bundle.sourceFiles).not.toContain(OPEN_CLOUD_E2E_TEST_PATH);
});

it("includes a valid conventional spec and exposes bounded immutable metadata", async () => {
const directory = await e2eFixture(validE2eSource);

const first = await buildBundle(directory);
const second = await buildBundle(directory);

expect(first.files).toContain(OPEN_CLOUD_E2E_TEST_PATH);
expect(first.sourceFiles).toContain(OPEN_CLOUD_E2E_TEST_PATH);
expect(first.e2eTest).toEqual({
path: OPEN_CLOUD_E2E_TEST_PATH,
source: validE2eSource,
sha256: createHash("sha256").update(validE2eSource).digest("hex"),
});
expect(second.sha256).toBe(first.sha256);
expect(second.archive).toEqual(first.archive);
expect(second.e2eTest).toEqual(first.e2eTest);
});

it.each([".", "tests"])(
"rejects an E2E spec inside public frontend directory %s",
async (frontendDirectory) => {
const directory = await e2eFixture(validE2eSource);
await writeManifest(
directory,
`
frontend:
directory: ${frontendDirectory}
spa: true
runtime:
sdk:
version: 2.0.0
`,
);

await expect(buildBundle(directory)).rejects.toThrow(
/must stay outside frontend\.directory/,
);
},
);

it("rejects a spec over the bounded source size", async () => {
const oversized = `${validE2eSource}\n/*${"x".repeat(OPEN_CLOUD_E2E_TEST_MAX_BYTES)}*/`;
const directory = await e2eFixture(oversized);

await expect(buildBundle(directory)).rejects.toThrow(
new RegExp(`exceeds ${OPEN_CLOUD_E2E_TEST_MAX_BYTES} bytes`),
);
});

it.each([
[
"a package subpath",
'import { test } from "@opencloud/test/fixtures";\ntest("flow", async () => {});\n',
],
[
"an unrelated package",
'import { test } from "@playwright/test";\ntest("flow", async () => {});\n',
],
[
"a default import",
'import test from "@opencloud/test";\ntest("flow", async () => {});\n',
],
[
"a second import",
'import { test, expect } from "@opencloud/test";\nimport value from "./helper.js";\ntest("flow", async () => expect(value));\n',
],
])("rejects %s instead of the exact test import", async (_label, source) => {
const directory = await e2eFixture(source);

await expect(buildBundle(directory)).rejects.toThrow(
/exactly one named import from "@opencloud\/test"/,
);
});

it("requires the named test import", async () => {
const directory = await e2eFixture(
'import { expect } from "@opencloud/test";\nexpect(true).toBe(true);\n',
);

await expect(buildBundle(directory)).rejects.toThrow(
/must import exactly test and expect from "@opencloud\/test"/,
);
});

it("requires at least one test declaration", async () => {
const directory = await e2eFixture(
'import { test, expect } from "@opencloud/test";\ntest.describe("items", () => expect(items));\n',
);

await expect(buildBundle(directory)).rejects.toThrow(
/must declare at least one test/,
);
});

it.each([
["test.skip", "test.skip"],
["test.only", "test.only"],
["test.describe.skip", "test.describe.skip"],
["test.describe.only", "test.describe.only"],
])("rejects %s", async (_label, call) => {
const directory = await e2eFixture(
`import { test, expect } from "@opencloud/test";\n${call}("flow", async () => expect(true));\ntest("required", async () => expect(true));\n`,
);

await expect(buildBundle(directory)).rejects.toThrow(
/cannot use skip or only/,
);
});

it.each([
["fetch", "await fetch('/items')"],
["XMLHttpRequest", "new XMLHttpRequest()"],
["browser evaluation", "await ownerPage.evaluate(() => true)"],
[
"locator evaluation",
"await ownerPage.locator('main').evaluateAll(() => [])",
],
["network routing", "await ownerPage.route('**/*', () => {})"],
["direct navigation", "await ownerPage.goto('https://example.test')"],
["request context", "await request.get('/items')"],
["backend route", 'const backend = "/rest/v1/items"'],
])("rejects direct %s access", async (_label, expression) => {
const directory = await e2eFixture(
`import { test, expect } from "@opencloud/test";\ntest("flow", async ({ ownerPage, request }) => { ${expression}; expect(ownerPage); });\n`,
);

await expect(buildBundle(directory)).rejects.toThrow(
/cannot (?:use|access)/,
);
});
});

async function e2eFixture(source?: string): Promise<string> {
const directory = await temporaryDirectory();
await mkdir(path.join(directory, "frontend"), { recursive: true });
await Promise.all([
writeManifest(
directory,
`
frontend:
directory: frontend
spa: true
runtime:
sdk:
version: 2.0.0
`,
),
writeFile(
path.join(directory, "frontend/index.html"),
"<!doctype html><title>Test</title>",
),
]);
if (source !== undefined) {
await mkdir(path.join(directory, "tests"), { recursive: true });
await writeFile(path.join(directory, OPEN_CLOUD_E2E_TEST_PATH), source);
}
return directory;
}
Loading
Loading