921 lines
38 KiB
TypeScript
921 lines
38 KiB
TypeScript
import { describe, expect, test } from "bun:test";
|
||
import { spawnSync } from "node:child_process";
|
||
import {
|
||
mkdirSync,
|
||
mkdtempSync,
|
||
readdirSync,
|
||
readFileSync,
|
||
rmSync,
|
||
writeFileSync,
|
||
} from "node:fs";
|
||
import { resolve } from "node:path";
|
||
import { tmpdir } from "node:os";
|
||
import {
|
||
deploymentPrompts,
|
||
manualDeploymentCopy,
|
||
} from "../apps/site/src/deployment-prompts";
|
||
import {
|
||
decideUpstreamSync,
|
||
shouldRedeploy,
|
||
} from "../scripts/upstream-sync-plan.mjs";
|
||
import { repositoryWranglerConfigError } from "../scripts/wrangler-runner.mjs";
|
||
import {
|
||
edgeEverDeploymentEnvironment,
|
||
hasPlaceholderD1Binding,
|
||
shouldRunEdgeEverDeployment,
|
||
} from "../packages/wrangler/dispatch.mjs";
|
||
|
||
const repositoryRoot = resolve(import.meta.dir, "..");
|
||
const normalizeLineEndings = (value: string) => value.replace(/\r\n/g, "\n");
|
||
const readRepositoryFile = (path: string) =>
|
||
normalizeLineEndings(readFileSync(resolve(repositoryRoot, path), "utf8"));
|
||
const extractTextPrompt = (document: string, sectionHeading: string) => {
|
||
const sectionStart = document.indexOf(sectionHeading);
|
||
if (sectionStart === -1) throw new Error(`Missing deployment section: ${sectionHeading}`);
|
||
const match = document.slice(sectionStart).match(/```text\n([\s\S]*?)\n```/);
|
||
if (!match?.[1]) throw new Error(`Missing deployment prompt after: ${sectionHeading}`);
|
||
return match[1];
|
||
};
|
||
const extractSection = (document: string, sectionHeading: string) => {
|
||
const sectionStart = document.indexOf(sectionHeading);
|
||
if (sectionStart === -1) throw new Error(`Missing section: ${sectionHeading}`);
|
||
const sectionEnd = document.indexOf("\n---", sectionStart);
|
||
return document.slice(sectionStart, sectionEnd === -1 ? undefined : sectionEnd);
|
||
};
|
||
const normalizeMarkdownCopy = (value: string) =>
|
||
value.replace(/[`*]/g, "").replace(/\s+/g, " ").trim();
|
||
const runFixtureGit = (cwd: string, ...args: string[]) => {
|
||
const result = spawnSync("git", args, { cwd, encoding: "utf8" });
|
||
if (result.error) throw result.error;
|
||
if (result.status !== 0) {
|
||
throw new Error(
|
||
`git ${args.join(" ")} failed:\n${result.stdout.trim()}\n${result.stderr.trim()}`,
|
||
);
|
||
}
|
||
};
|
||
const fixtureGitStatus = (cwd: string, ...args: string[]) =>
|
||
spawnSync("git", args, { cwd, encoding: "utf8" }).status;
|
||
const writeFixtureFile = (cwd: string, path: string, content: string) => {
|
||
const absolutePath = resolve(cwd, path);
|
||
mkdirSync(resolve(absolutePath, ".."), { recursive: true });
|
||
writeFileSync(absolutePath, content);
|
||
};
|
||
const initializeSyncFixture = (cwd: string) => {
|
||
runFixtureGit(cwd, "init", "-b", "main");
|
||
runFixtureGit(cwd, "config", "user.name", "EdgeEver Test");
|
||
runFixtureGit(cwd, "config", "user.email", "edgeever-test@example.com");
|
||
runFixtureGit(cwd, "config", "core.autocrlf", "false");
|
||
};
|
||
const prepareFixtureUpstreamSync = (
|
||
cwd: string,
|
||
alignMode: "merge" | "snapshot",
|
||
targetRevision: string,
|
||
) => {
|
||
const result = spawnSync(
|
||
"node",
|
||
[resolve(repositoryRoot, "scripts/prepare-upstream-sync.mjs"), "prepare"],
|
||
{
|
||
cwd,
|
||
encoding: "utf8",
|
||
env: {
|
||
...process.env,
|
||
EDGE_SYNC_ALIGN_MODE: alignMode,
|
||
EDGE_SYNC_BASE_COMMIT: "HEAD",
|
||
EDGE_SYNC_TARGET_COMMIT: targetRevision,
|
||
},
|
||
},
|
||
);
|
||
if (result.error) throw result.error;
|
||
if (result.status !== 0) {
|
||
throw new Error(`prepare-upstream-sync failed:\n${result.stdout}\n${result.stderr}`);
|
||
}
|
||
};
|
||
|
||
describe("Cloudflare deployment entrypoints", () => {
|
||
test("all entrypoints converge on the common deployment pipeline", () => {
|
||
const packageJson = JSON.parse(readRepositoryFile("package.json"));
|
||
const scripts = packageJson.scripts as Record<string, string>;
|
||
|
||
expect(scripts.deploy).toContain("bun run build:cloudflare");
|
||
expect(scripts.deploy).toContain("EDGE_EVER_USE_EXISTING_AUTH_SECRET=true");
|
||
expect(scripts.deploy).toContain("deploy:ci");
|
||
expect(scripts["deploy:manual"]).toBe(
|
||
"export EDGE_EVER_DEPLOYMENT_TRIGGER=manual EDGE_EVER_DEPLOYMENT_METHOD=local_cli && bun run deploy:doctor && bun run build:cloudflare && bun run deploy:ci",
|
||
);
|
||
expect(scripts["deploy:ci"]).toBe(
|
||
"bun run db:migrate:remote && bun run deploy:worker && bun run deploy:verify",
|
||
);
|
||
expect(scripts["deploy:cloudflare-builds"]).toBe(
|
||
"EDGE_EVER_USE_EXISTING_AUTH_SECRET=true bun run deploy:ci",
|
||
);
|
||
expect(scripts["build:cloudflare"]).toContain("bun run build:worker");
|
||
|
||
const wranglerConfig = readRepositoryFile("wrangler.toml");
|
||
expect(wranglerConfig).toContain('main = ".wrangler/edgeever-worker/index.js"');
|
||
expect(wranglerConfig).toContain("no_bundle = true");
|
||
expect(wranglerConfig).toContain("find_additional_modules = true");
|
||
expect(wranglerConfig).toContain('globs = ["modules/*.js"]');
|
||
});
|
||
|
||
test("Cloudflare's default Wrangler command cannot bypass the deployment pipeline", () => {
|
||
const packageJson = JSON.parse(readRepositoryFile("package.json"));
|
||
const shimPackage = JSON.parse(readRepositoryFile("packages/wrangler/package.json"));
|
||
const shim = readRepositoryFile("packages/wrangler/bin/wrangler.js");
|
||
const englishGuide = readRepositoryFile("docs/deploy-cloudflare-button.md");
|
||
const chineseGuide = readRepositoryFile("docs/deploy-cloudflare-button.zh-CN.md");
|
||
|
||
expect(packageJson.devDependencies.wrangler).toBe("workspace:*");
|
||
expect(shimPackage.name).toBe("wrangler");
|
||
expect(shimPackage.dependencies["edgeever-wrangler-cli"]).toMatch(/^npm:wrangler@/);
|
||
expect(shim).toContain('["run", "deploy"]');
|
||
const placeholderConfig = 'database_id = "00000000-0000-0000-0000-000000000000"';
|
||
const legacyConfig = 'database_id = "11111111-1111-1111-1111-111111111111"';
|
||
expect(hasPlaceholderD1Binding(placeholderConfig)).toBe(true);
|
||
expect(hasPlaceholderD1Binding(legacyConfig)).toBe(false);
|
||
expect(shouldRunEdgeEverDeployment(
|
||
["deploy"],
|
||
{ WORKERS_CI: "1" },
|
||
placeholderConfig,
|
||
)).toBe(true);
|
||
expect(shouldRunEdgeEverDeployment(["deploy"], {}, placeholderConfig)).toBe(false);
|
||
expect(shouldRunEdgeEverDeployment(
|
||
["deploy"],
|
||
{ WORKERS_CI: "1" },
|
||
legacyConfig,
|
||
)).toBe(false);
|
||
expect(shouldRunEdgeEverDeployment(
|
||
["deploy"],
|
||
{ WORKERS_CI: "1", WRANGLER_CONFIG: "custom.toml" },
|
||
placeholderConfig,
|
||
)).toBe(false);
|
||
expect(shouldRunEdgeEverDeployment(
|
||
["deploy", "--config", "custom.toml"],
|
||
{ WORKERS_CI: "1" },
|
||
placeholderConfig,
|
||
)).toBe(false);
|
||
expect(shouldRunEdgeEverDeployment(
|
||
["deploy", "--env", "production"],
|
||
{ WORKERS_CI: "1" },
|
||
placeholderConfig,
|
||
)).toBe(false);
|
||
expect(shouldRunEdgeEverDeployment(
|
||
["deploy", "--name", "custom-worker"],
|
||
{ WORKERS_CI: "1" },
|
||
placeholderConfig,
|
||
)).toBe(false);
|
||
expect(shouldRunEdgeEverDeployment(
|
||
["deploy"],
|
||
{ WORKERS_CI: "1", EDGE_EVER_WRANGLER_BYPASS_SHIM: "1" },
|
||
placeholderConfig,
|
||
)).toBe(false);
|
||
expect(edgeEverDeploymentEnvironment({ WORKERS_CI: "1" }))
|
||
.toMatchObject({
|
||
EDGE_EVER_DEPLOYMENT_TRIGGER: "main_push",
|
||
EDGE_EVER_DEPLOYMENT_METHOD: "cloudflare_workers_builds_default",
|
||
});
|
||
expect(englishGuide).toContain("Deploy command: npx wrangler deploy");
|
||
expect(chineseGuide).toContain("Deploy command: npx wrangler deploy");
|
||
});
|
||
|
||
test("deployment verification lets piped diagnostics flush before exiting", () => {
|
||
const verificationScript = readRepositoryFile("scripts/verify-deployment.mjs");
|
||
|
||
expect(verificationScript).toContain("process.exitCode = 1");
|
||
expect(verificationScript).not.toContain("process.exit(1)");
|
||
});
|
||
|
||
test("online deployment declares the required authentication Secret", () => {
|
||
const example = readRepositoryFile(".dev.vars.example");
|
||
expect(example).toMatch(/^EDGE_EVER_AUTH_PASSWORD=\s*$/m);
|
||
|
||
const packageJson = JSON.parse(readRepositoryFile("package.json"));
|
||
expect(packageJson.cloudflare.bindings.EDGE_EVER_AUTH_PASSWORD.description).toBeTruthy();
|
||
|
||
const englishGuide = readRepositoryFile("docs/deploy-cloudflare-button.md");
|
||
const chineseGuide = readRepositoryFile("docs/deploy-cloudflare-button.zh-CN.md");
|
||
expect(englishGuide).toContain("Worker runtime Secret, not a Workers Builds variable");
|
||
expect(chineseGuide).toContain("Worker 运行时 Secret,不是 Workers Builds 构建变量");
|
||
});
|
||
|
||
test("Workers Builds receives configuration but never the runtime password", () => {
|
||
const setup = readRepositoryFile("scripts/cloudflare-workers-builds.mjs");
|
||
|
||
expect(setup).toContain('"AUTH_USERNAME"');
|
||
expect(setup).toContain('"AUTH_LOGIN_WINDOW_SECONDS"');
|
||
expect(setup).not.toContain('"AUTH_PASSWORD",');
|
||
expect(setup).not.toContain('"AUTH_PASSWORD_HASH",');
|
||
expect(setup).not.toContain("Missing EDGE_EVER_AUTH_PASSWORD");
|
||
expect(setup).toContain("is_secret: false");
|
||
});
|
||
|
||
test("online deployment resolves the D1 id without editing the repository config", () => {
|
||
const runner = readRepositoryFile("scripts/run-wrangler.mjs");
|
||
const wranglerConfig = readRepositoryFile("wrangler.toml");
|
||
const englishAgentDoc = readRepositoryFile("docs/agent-deploy-cloudflare.md");
|
||
const chineseAgentDoc = readRepositoryFile("docs/agent-deploy-cloudflare.zh-CN.md");
|
||
|
||
expect(wranglerConfig).toContain(
|
||
'database_id = "00000000-0000-0000-0000-000000000000"',
|
||
);
|
||
expect(runner).toContain('"d1", "list", "--json"');
|
||
expect(runner).toContain("findD1DatabaseIdByName");
|
||
expect(runner).toContain("repositoryWranglerConfigError(config, usesRepositoryConfig)");
|
||
expect(runner).not.toContain("replace the database_id placeholder");
|
||
expect(englishAgentDoc).toContain("automatically resolves the D1 UUID");
|
||
expect(chineseAgentDoc).toContain("自动查询 D1 UUID");
|
||
});
|
||
|
||
test("rejects instance-specific values in the repository Wrangler config", () => {
|
||
const repositoryConfig = readRepositoryFile("wrangler.toml");
|
||
expect(repositoryConfig).toContain('[version_metadata]\nbinding = "CF_VERSION_METADATA"');
|
||
const instanceConfigs = [
|
||
repositoryConfig.replace('name = "edgeever"', 'name = "my-notes"'),
|
||
repositoryConfig.replace("workers_dev = true", "workers_dev = false"),
|
||
repositoryConfig.replace('database_name = "edgeever"', 'database_name = "my-notes"'),
|
||
repositoryConfig.replace(
|
||
'database_id = "00000000-0000-0000-0000-000000000000"',
|
||
'database_id = "11111111-1111-1111-1111-111111111111"',
|
||
),
|
||
repositoryConfig.replace('bucket_name = "edgeever-resources"', 'bucket_name = "my-notes"'),
|
||
repositoryConfig.replace('compatibility_date = "2026-06-26"', 'compatibility_date = "2026-08-17"'),
|
||
`${repositoryConfig}\n[vars]\nEDGE_EVER_AUTH_USERNAME = "owner"\n`,
|
||
`${repositoryConfig}\n[[routes]]\npattern = "notes.example.com"\ncustom_domain = true\n`,
|
||
];
|
||
|
||
for (const instanceConfig of instanceConfigs) {
|
||
const error = repositoryWranglerConfigError(instanceConfig, true);
|
||
expect(error).toContain("Refusing instance-specific setting");
|
||
expect(error).toContain("EDGE_EVER_*");
|
||
expect(error).toContain("WRANGLER_CONFIG");
|
||
}
|
||
expect(repositoryWranglerConfigError(repositoryConfig, true)).toBeUndefined();
|
||
expect(repositoryWranglerConfigError(instanceConfigs[0], false)).toBeUndefined();
|
||
});
|
||
|
||
test("keeps D1 resolver diagnostics out of Wrangler JSON stdout", () => {
|
||
const workingDirectory = mkdtempSync(resolve(tmpdir(), "edgeever-wrangler-output-"));
|
||
const wranglerBinDirectory = resolve(
|
||
workingDirectory,
|
||
"node_modules",
|
||
"wrangler",
|
||
"bin",
|
||
);
|
||
const queryOutput = JSON.stringify([{ results: [{ name: "users" }] }]);
|
||
const inheritedEnvironment = Object.fromEntries(
|
||
["PATH", "Path", "PATHEXT", "SystemRoot", "ComSpec", "TEMP", "TMP"]
|
||
.map((name) => [name, process.env[name]])
|
||
.filter((entry): entry is [string, string] => Boolean(entry[1])),
|
||
);
|
||
const environment = {
|
||
...inheritedEnvironment,
|
||
EDGE_EVER_INSTANCE: "",
|
||
WRANGLER_CONFIG: resolve(workingDirectory, "wrangler.external.toml"),
|
||
};
|
||
|
||
try {
|
||
mkdirSync(wranglerBinDirectory, { recursive: true });
|
||
writeFileSync(
|
||
resolve(workingDirectory, "wrangler.external.toml"),
|
||
[
|
||
'name = "edgeever"',
|
||
'database_name = "edgeever"',
|
||
'database_id = "00000000-0000-0000-0000-000000000000"',
|
||
'bucket_name = "edgeever-resources"',
|
||
'preview_bucket_name = "edgeever-resources-preview"',
|
||
'migrations_dir = "migrations"',
|
||
"",
|
||
].join("\n"),
|
||
);
|
||
writeFileSync(
|
||
resolve(wranglerBinDirectory, "wrangler.js"),
|
||
[
|
||
'const args = process.argv.slice(2);',
|
||
'if (args.includes("list")) {',
|
||
' process.stdout.write(JSON.stringify([{ name: "edgeever", uuid: "11111111-1111-1111-1111-111111111111" }]));',
|
||
"} else {",
|
||
` process.stdout.write(${JSON.stringify(`${queryOutput}\n`)});`,
|
||
"}",
|
||
"",
|
||
].join("\n"),
|
||
);
|
||
|
||
const resultPath = resolve(workingDirectory, "result.json");
|
||
const runnerArguments = [
|
||
"d1",
|
||
"execute",
|
||
"DB",
|
||
"--remote",
|
||
"--command",
|
||
"SELECT name FROM sqlite_master",
|
||
"--json",
|
||
];
|
||
const harness = spawnSync(
|
||
"node",
|
||
[
|
||
"-e",
|
||
[
|
||
'const { spawnSync } = require("node:child_process");',
|
||
'const { writeFileSync } = require("node:fs");',
|
||
"const result = spawnSync(",
|
||
" process.env.EDGE_TEST_RUNTIME,",
|
||
" [process.env.EDGE_TEST_RUNNER, ...JSON.parse(process.env.EDGE_TEST_ARGUMENTS)],",
|
||
' { cwd: process.env.EDGE_TEST_CWD, encoding: "utf8", env: process.env },',
|
||
");",
|
||
"writeFileSync(process.env.EDGE_TEST_RESULT, JSON.stringify({",
|
||
" status: result.status,",
|
||
" stdout: result.stdout,",
|
||
" stderr: result.stderr,",
|
||
" error: result.error?.message,",
|
||
"}));",
|
||
].join("\n"),
|
||
],
|
||
{
|
||
env: {
|
||
...environment,
|
||
EDGE_TEST_ARGUMENTS: JSON.stringify(runnerArguments),
|
||
EDGE_TEST_CWD: workingDirectory,
|
||
EDGE_TEST_RESULT: resultPath,
|
||
EDGE_TEST_RUNNER: resolve(repositoryRoot, "scripts", "run-wrangler.mjs"),
|
||
EDGE_TEST_RUNTIME: process.execPath,
|
||
},
|
||
},
|
||
);
|
||
expect(harness.status).toBe(0);
|
||
const result = JSON.parse(readFileSync(resultPath, "utf8")) as {
|
||
error?: string;
|
||
status: number;
|
||
stderr: string;
|
||
stdout: string;
|
||
};
|
||
|
||
expect(result.error).toBeUndefined();
|
||
expect(result.status).toBe(0);
|
||
expect(result.stdout).toBe(`${queryOutput}\n`);
|
||
expect(result.stderr).toContain("[info] resolving Cloudflare D1 database id for edgeever");
|
||
expect(result.stderr).toContain("[ok] resolved D1 database edgeever");
|
||
} finally {
|
||
rmSync(workingDirectory, { force: true, recursive: true });
|
||
}
|
||
});
|
||
|
||
test("records the public Worker target reported by a CI deployment", () => {
|
||
const workingDirectory = mkdtempSync(resolve(tmpdir(), "edgeever-deployment-target-"));
|
||
const wranglerBinDirectory = resolve(
|
||
workingDirectory,
|
||
"node_modules",
|
||
"wrangler",
|
||
"bin",
|
||
);
|
||
const inheritedEnvironment = Object.fromEntries(
|
||
["PATH", "Path", "PATHEXT", "SystemRoot", "ComSpec", "TEMP", "TMP"]
|
||
.map((name) => [name, process.env[name]])
|
||
.filter((entry): entry is [string, string] => Boolean(entry[1])),
|
||
);
|
||
|
||
try {
|
||
mkdirSync(wranglerBinDirectory, { recursive: true });
|
||
writeFileSync(
|
||
resolve(workingDirectory, "wrangler.external.toml"),
|
||
[
|
||
'name = "edgeever"',
|
||
"workers_dev = true",
|
||
'database_name = "edgeever"',
|
||
'database_id = "11111111-1111-1111-1111-111111111111"',
|
||
'bucket_name = "edgeever-resources"',
|
||
'preview_bucket_name = "edgeever-resources-preview"',
|
||
'migrations_dir = "migrations"',
|
||
"",
|
||
].join("\n"),
|
||
);
|
||
writeFileSync(
|
||
resolve(wranglerBinDirectory, "wrangler.js"),
|
||
[
|
||
'if (process.argv.includes("deployments")) {',
|
||
' process.stdout.write(JSON.stringify({ versions: [{ version_id: "legacy-version", percentage: 100 }] }));',
|
||
'} else if (process.argv.includes("versions")) {',
|
||
' process.stdout.write(JSON.stringify({ resources: { bindings: [',
|
||
' { name: "RESOURCES", type: "r2_bucket", bucket_name: "legacy-user-resources" },',
|
||
' { name: "EDGE_EVER_AUTH_USERNAME", type: "plain_text", text: "legacy-owner" }',
|
||
' ] } }));',
|
||
'} else if (process.argv.includes("deploy")) {',
|
||
' process.stdout.write("Uploaded edgeever\\nDeployed edgeever triggers (0.4 sec)\\n https://edgeever.example.workers.dev\\nCurrent Version ID: version-1\\n");',
|
||
"}",
|
||
"",
|
||
].join("\n"),
|
||
);
|
||
|
||
const result = spawnSync(
|
||
process.execPath,
|
||
[resolve(repositoryRoot, "scripts", "run-wrangler.mjs"), "deploy"],
|
||
{
|
||
cwd: workingDirectory,
|
||
encoding: "utf8",
|
||
env: {
|
||
...inheritedEnvironment,
|
||
CI: "true",
|
||
EDGE_EVER_AUTH_PASSWORD: "test-password",
|
||
EDGE_EVER_INSTANCE: "",
|
||
WRANGLER_CONFIG: resolve(workingDirectory, "wrangler.external.toml"),
|
||
},
|
||
},
|
||
);
|
||
|
||
expect(result.status).toBe(0);
|
||
expect(JSON.parse(readFileSync(
|
||
resolve(workingDirectory, ".wrangler.deployment-targets.json"),
|
||
"utf8",
|
||
))).toEqual({
|
||
urls: ["https://edgeever.example.workers.dev"],
|
||
versionId: "version-1",
|
||
});
|
||
const generatedConfig = readFileSync(
|
||
resolve(workingDirectory, ".wrangler.generated.toml"),
|
||
"utf8",
|
||
);
|
||
expect(generatedConfig).toContain('bucket_name = "legacy-user-resources"');
|
||
expect(generatedConfig).toContain('EDGE_EVER_R2_BUCKET_NAME = "legacy-user-resources"');
|
||
expect(generatedConfig).toContain('EDGE_EVER_AUTH_USERNAME = "legacy-owner"');
|
||
} finally {
|
||
rmSync(workingDirectory, { force: true, recursive: true });
|
||
}
|
||
});
|
||
|
||
test("deployed repositories receive guarded daily upstream updates", () => {
|
||
const workflow = readRepositoryFile(".github/workflows/sync-edgeever-upstream.yml");
|
||
const bunConfig = readRepositoryFile("bunfig.toml");
|
||
const packageJson = JSON.parse(readRepositoryFile("package.json"));
|
||
const scripts = packageJson.scripts as Record<string, string>;
|
||
|
||
expect(workflow).toContain("github.repository != 'tianma-if/edgeever'");
|
||
expect(workflow).toContain("UPSTREAM_REPOSITORY: tianma-if/edgeever");
|
||
expect(workflow).toContain("Require a GitHub Fork");
|
||
expect(workflow).toContain(".fork");
|
||
expect(workflow).toContain("EDGE_EVER_UPDATE_CHANNEL");
|
||
expect(workflow).toContain("stable)");
|
||
expect(workflow).toContain("edge)");
|
||
expect(workflow).toContain("FORCE_REDEPLOY: ${{ github.event_name == 'workflow_dispatch' }}");
|
||
expect(workflow).not.toContain("force_redeploy:");
|
||
expect(workflow).toContain("bun run db:migrate:local");
|
||
expect(bunConfig).toContain('pathIgnorePatterns = ["tests/e2e/**"]');
|
||
expect(scripts.test).toBe("bun run test:bulk && bun run test:integration");
|
||
expect(scripts["test:bulk"]).toContain("--path-ignore-patterns='tests/e2e/**'");
|
||
expect(scripts["test:bulk"]).toContain("apps/api/src/companion-learning.test.mjs");
|
||
expect(scripts["test:bulk"]).toContain("apps/api/src/resource-upload-integration.test.mjs");
|
||
expect(scripts["test:integration"]).toBe(
|
||
"bun test apps/api/src/companion-learning.test.mjs apps/api/src/resource-upload-integration.test.mjs",
|
||
);
|
||
expect(workflow).toContain("bun run test");
|
||
expect(workflow.match(/if: steps\.upstream\.outputs\.align_mode == 'merge'/g)).toHaveLength(2);
|
||
expect(workflow).toContain("git push origin HEAD:main");
|
||
expect(workflow).not.toContain("git push --force-with-lease origin HEAD:main");
|
||
expect(workflow).not.toContain("git reset --hard");
|
||
expect(workflow).toContain("scripts/prepare-upstream-sync.mjs");
|
||
expect(workflow).toContain("':(exclude).github/workflows/**'");
|
||
expect(workflow).toContain("planner_path");
|
||
expect(workflow).toContain('if [ "${align_mode}" = "reset" ]');
|
||
expect(workflow).toContain("Refusing to publish an update that changes the downstream updater");
|
||
expect(workflow).toContain("content_matches_target");
|
||
expect(workflow).toContain("already_on_target");
|
||
expect(workflow).toContain("fork_mode=mirror");
|
||
expect(workflow).toContain("GITHUB_STEP_SUMMARY");
|
||
expect(workflow).toContain("name: Sync Fork and trigger deployment");
|
||
expect(workflow).toContain("name: Report result / 输出结果");
|
||
expect(workflow).toContain("::notice title=Manual redeploy / 手动重新部署");
|
||
expect(workflow).toContain("PUBLISH_OUTCOME: ${{ steps.publish.outcome }}");
|
||
expect(workflow).toContain("DEPLOY_TRIGGER_OUTCOME: ${{ steps.deploy.outcome }}");
|
||
expect(workflow).toContain("| Git publish / Git 发布 |");
|
||
expect(workflow).toContain("| Deployment trigger / 部署触发 |");
|
||
expect(workflow).toContain("| Live deployment / 线上部署 |");
|
||
expect(workflow).toContain("Not verified by this workflow / 本工作流未验证");
|
||
expect(workflow).toContain("EDGE_EVER_CLOUDFLARE_DEPLOY_HOOK_URL");
|
||
expect(workflow).toContain("EDGE_EVER_PRESERVE_FORK_CHANGES");
|
||
expect(workflow).toContain("PRESERVE_FORK_CHANGES");
|
||
expect(workflow).not.toContain("upstream_merge_base");
|
||
expect(workflow).not.toContain("local_app_changes");
|
||
expect(workflow).toContain("scripts/upstream-sync-plan.mjs");
|
||
expect(workflow).toContain("Prefer this workflow over GitHub **Sync fork**");
|
||
});
|
||
|
||
test("forks skip every workflow job except upstream updates", () => {
|
||
const workflowsDirectory = resolve(repositoryRoot, ".github/workflows");
|
||
const workflowFiles = readdirSync(workflowsDirectory)
|
||
.filter((file) => /\.ya?ml$/.test(file))
|
||
.filter((file) => file !== "sync-edgeever-upstream.yml");
|
||
|
||
for (const file of workflowFiles) {
|
||
const workflow = readRepositoryFile(`.github/workflows/${file}`);
|
||
const jobs = workflow.slice(workflow.indexOf("\njobs:\n") + "\njobs:\n".length);
|
||
const jobStarts = [...jobs.matchAll(/^ ([A-Za-z0-9_-]+):\s*$/gm)];
|
||
|
||
expect(jobStarts.length).toBeGreaterThan(0);
|
||
for (const [index, match] of jobStarts.entries()) {
|
||
const nextJob = jobStarts[index + 1];
|
||
const job = jobs.slice(match.index, nextJob?.index);
|
||
expect(job).toContain("github.repository == 'tianma-if/edgeever'");
|
||
}
|
||
}
|
||
});
|
||
|
||
test("republishes an already-aligned deploy mirror when redeploy is requested", () => {
|
||
expect(
|
||
decideUpstreamSync({
|
||
contentMatchesTarget: true,
|
||
forceRedeploy: true,
|
||
headEqualsTarget: true,
|
||
headIsAncestorOfTarget: true,
|
||
preserveForkChanges: false,
|
||
targetIsAncestorOfHead: true,
|
||
}),
|
||
).toEqual({
|
||
alignMode: "none",
|
||
reason: "already_on_target",
|
||
republishOnly: true,
|
||
updateRequired: false,
|
||
});
|
||
});
|
||
|
||
test("manual dispatch redeploys through an older Fork workflow", () => {
|
||
expect(shouldRedeploy({
|
||
eventName: "workflow_dispatch",
|
||
forceRedeploy: false,
|
||
})).toBe(true);
|
||
expect(shouldRedeploy({
|
||
eventName: "schedule",
|
||
forceRedeploy: false,
|
||
})).toBe(false);
|
||
});
|
||
|
||
test("snapshots a deploy mirror that moved ahead of the stable release", () => {
|
||
expect(
|
||
decideUpstreamSync({
|
||
contentMatchesTarget: false,
|
||
forceRedeploy: false,
|
||
headEqualsTarget: false,
|
||
headIsAncestorOfTarget: false,
|
||
preserveForkChanges: false,
|
||
targetIsAncestorOfHead: true,
|
||
}),
|
||
).toEqual({
|
||
alignMode: "snapshot",
|
||
reason: "deploy_mirror_ahead",
|
||
republishOnly: false,
|
||
updateRequired: true,
|
||
});
|
||
});
|
||
|
||
test("preserves changes only when a customized fork explicitly opts in", () => {
|
||
expect(
|
||
decideUpstreamSync({
|
||
contentMatchesTarget: false,
|
||
forceRedeploy: false,
|
||
headEqualsTarget: false,
|
||
headIsAncestorOfTarget: false,
|
||
preserveForkChanges: true,
|
||
targetIsAncestorOfHead: true,
|
||
}),
|
||
).toEqual({
|
||
alignMode: "none",
|
||
reason: "customized_contains_target",
|
||
republishOnly: false,
|
||
updateRequired: false,
|
||
});
|
||
});
|
||
|
||
test("merges a diverged customized fork only after explicit opt-in", () => {
|
||
expect(
|
||
decideUpstreamSync({
|
||
contentMatchesTarget: false,
|
||
forceRedeploy: false,
|
||
headEqualsTarget: false,
|
||
headIsAncestorOfTarget: false,
|
||
preserveForkChanges: true,
|
||
targetIsAncestorOfHead: false,
|
||
}),
|
||
).toEqual({
|
||
alignMode: "merge",
|
||
reason: "customized_merge",
|
||
republishOnly: false,
|
||
updateRequired: true,
|
||
});
|
||
});
|
||
|
||
test("prepares a linear product snapshot without changing downstream workflows", () => {
|
||
const workingDirectory = mkdtempSync(resolve(tmpdir(), "edgeever-upstream-snapshot-"));
|
||
|
||
try {
|
||
initializeSyncFixture(workingDirectory);
|
||
writeFixtureFile(workingDirectory, "app.txt", "base\n");
|
||
writeFixtureFile(workingDirectory, "removed.txt", "remove me\n");
|
||
writeFixtureFile(
|
||
workingDirectory,
|
||
".github/workflows/sync-edgeever-upstream.yml",
|
||
"name: downstream base\n",
|
||
);
|
||
writeFixtureFile(
|
||
workingDirectory,
|
||
"scripts/prepare-upstream-sync.mjs",
|
||
"// downstream prepare helper\n",
|
||
);
|
||
writeFixtureFile(
|
||
workingDirectory,
|
||
"scripts/upstream-sync-plan.mjs",
|
||
"// downstream planner\n",
|
||
);
|
||
runFixtureGit(workingDirectory, "add", ".");
|
||
runFixtureGit(workingDirectory, "commit", "-m", "base");
|
||
|
||
runFixtureGit(workingDirectory, "checkout", "-b", "upstream");
|
||
writeFixtureFile(workingDirectory, "app.txt", "upstream target\n");
|
||
writeFixtureFile(workingDirectory, "added.txt", "added upstream\n");
|
||
writeFixtureFile(
|
||
workingDirectory,
|
||
".github/workflows/sync-edgeever-upstream.yml",
|
||
"name: upstream changed\n",
|
||
);
|
||
writeFixtureFile(
|
||
workingDirectory,
|
||
".github/workflows/windows-test-signing.yml",
|
||
"name: official only\n",
|
||
);
|
||
writeFixtureFile(
|
||
workingDirectory,
|
||
"scripts/prepare-upstream-sync.mjs",
|
||
"// upstream prepare helper\n",
|
||
);
|
||
writeFixtureFile(
|
||
workingDirectory,
|
||
"scripts/upstream-sync-plan.mjs",
|
||
"// upstream planner\n",
|
||
);
|
||
rmSync(resolve(workingDirectory, "removed.txt"));
|
||
runFixtureGit(workingDirectory, "add", "-A");
|
||
runFixtureGit(workingDirectory, "commit", "-m", "upstream target");
|
||
|
||
runFixtureGit(workingDirectory, "checkout", "-b", "downstream", "main");
|
||
writeFixtureFile(workingDirectory, "app.txt", "stale downstream product\n");
|
||
writeFixtureFile(
|
||
workingDirectory,
|
||
".github/workflows/sync-edgeever-upstream.yml",
|
||
"name: downstream updater\n",
|
||
);
|
||
runFixtureGit(workingDirectory, "add", ".");
|
||
runFixtureGit(workingDirectory, "commit", "-m", "downstream state");
|
||
runFixtureGit(workingDirectory, "tag", "downstream-base");
|
||
|
||
prepareFixtureUpstreamSync(workingDirectory, "snapshot", "upstream");
|
||
|
||
expect(readFileSync(resolve(workingDirectory, "app.txt"), "utf8")).toBe(
|
||
"upstream target\n",
|
||
);
|
||
expect(readFileSync(resolve(workingDirectory, "added.txt"), "utf8")).toBe(
|
||
"added upstream\n",
|
||
);
|
||
expect(readFileSync(resolve(
|
||
workingDirectory,
|
||
".github/workflows/sync-edgeever-upstream.yml",
|
||
), "utf8")).toBe("name: downstream updater\n");
|
||
expect(readFileSync(resolve(
|
||
workingDirectory,
|
||
"scripts/prepare-upstream-sync.mjs",
|
||
), "utf8")).toBe("// downstream prepare helper\n");
|
||
expect(readFileSync(resolve(
|
||
workingDirectory,
|
||
"scripts/upstream-sync-plan.mjs",
|
||
), "utf8")).toBe("// downstream planner\n");
|
||
expect(readdirSync(resolve(workingDirectory, ".github/workflows"))).toEqual([
|
||
"sync-edgeever-upstream.yml",
|
||
]);
|
||
expect(fixtureGitStatus(
|
||
workingDirectory,
|
||
"diff",
|
||
"--cached",
|
||
"--quiet",
|
||
"--",
|
||
".github/workflows",
|
||
"scripts/prepare-upstream-sync.mjs",
|
||
"scripts/upstream-sync-plan.mjs",
|
||
)).toBe(0);
|
||
|
||
runFixtureGit(workingDirectory, "commit", "-m", "sync upstream product");
|
||
expect(fixtureGitStatus(workingDirectory, "diff", "--quiet", "downstream-base", "HEAD^"))
|
||
.toBe(0);
|
||
expect(fixtureGitStatus(workingDirectory, "rev-parse", "--verify", "HEAD^2"))
|
||
.not.toBe(0);
|
||
expect(fixtureGitStatus(
|
||
workingDirectory,
|
||
"diff",
|
||
"--quiet",
|
||
"upstream",
|
||
"HEAD",
|
||
"--",
|
||
".",
|
||
":(exclude).github/workflows/**",
|
||
":(exclude)scripts/prepare-upstream-sync.mjs",
|
||
":(exclude)scripts/upstream-sync-plan.mjs",
|
||
)).toBe(0);
|
||
expect(fixtureGitStatus(
|
||
workingDirectory,
|
||
"diff",
|
||
"--quiet",
|
||
"downstream-base",
|
||
"HEAD",
|
||
"--",
|
||
".github/workflows",
|
||
"scripts/prepare-upstream-sync.mjs",
|
||
"scripts/upstream-sync-plan.mjs",
|
||
)).toBe(0);
|
||
} finally {
|
||
rmSync(workingDirectory, { force: true, recursive: true });
|
||
}
|
||
});
|
||
|
||
test("flattens customized merges and preserves their downstream workflows", () => {
|
||
const workingDirectory = mkdtempSync(resolve(tmpdir(), "edgeever-upstream-merge-"));
|
||
|
||
try {
|
||
initializeSyncFixture(workingDirectory);
|
||
writeFixtureFile(workingDirectory, "upstream.txt", "base\n");
|
||
writeFixtureFile(workingDirectory, "custom.txt", "base\n");
|
||
writeFixtureFile(
|
||
workingDirectory,
|
||
".github/workflows/sync-edgeever-upstream.yml",
|
||
"name: base\n",
|
||
);
|
||
runFixtureGit(workingDirectory, "add", ".");
|
||
runFixtureGit(workingDirectory, "commit", "-m", "base");
|
||
|
||
runFixtureGit(workingDirectory, "checkout", "-b", "upstream");
|
||
writeFixtureFile(workingDirectory, "upstream.txt", "upstream target\n");
|
||
writeFixtureFile(
|
||
workingDirectory,
|
||
".github/workflows/sync-edgeever-upstream.yml",
|
||
"name: upstream workflow\n",
|
||
);
|
||
runFixtureGit(workingDirectory, "add", ".");
|
||
runFixtureGit(workingDirectory, "commit", "-m", "upstream target");
|
||
runFixtureGit(workingDirectory, "tag", "upstream-v1");
|
||
|
||
runFixtureGit(workingDirectory, "checkout", "-b", "downstream", "main");
|
||
writeFixtureFile(workingDirectory, "custom.txt", "downstream customization\n");
|
||
writeFixtureFile(
|
||
workingDirectory,
|
||
".github/workflows/sync-edgeever-upstream.yml",
|
||
"name: downstream updater\n",
|
||
);
|
||
runFixtureGit(workingDirectory, "add", ".");
|
||
runFixtureGit(workingDirectory, "commit", "-m", "customized downstream");
|
||
runFixtureGit(workingDirectory, "tag", "downstream-base");
|
||
|
||
prepareFixtureUpstreamSync(workingDirectory, "merge", "upstream-v1");
|
||
|
||
expect(readFileSync(resolve(workingDirectory, "upstream.txt"), "utf8")).toBe(
|
||
"upstream target\n",
|
||
);
|
||
expect(readFileSync(resolve(workingDirectory, "custom.txt"), "utf8")).toBe(
|
||
"downstream customization\n",
|
||
);
|
||
expect(readFileSync(resolve(
|
||
workingDirectory,
|
||
".github/workflows/sync-edgeever-upstream.yml",
|
||
), "utf8")).toBe("name: downstream updater\n");
|
||
|
||
runFixtureGit(
|
||
workingDirectory,
|
||
"commit",
|
||
"-m",
|
||
"sync customized fork",
|
||
"-m",
|
||
"EdgeEver-Upstream-Commit: upstream-v1",
|
||
);
|
||
expect(fixtureGitStatus(workingDirectory, "diff", "--quiet", "downstream-base", "HEAD^"))
|
||
.toBe(0);
|
||
expect(fixtureGitStatus(workingDirectory, "rev-parse", "--verify", "HEAD^2"))
|
||
.not.toBe(0);
|
||
expect(fixtureGitStatus(
|
||
workingDirectory,
|
||
"diff",
|
||
"--quiet",
|
||
"downstream-base",
|
||
"HEAD",
|
||
"--",
|
||
".github/workflows",
|
||
)).toBe(0);
|
||
|
||
runFixtureGit(workingDirectory, "checkout", "upstream");
|
||
writeFixtureFile(workingDirectory, "upstream.txt", "upstream target v2\n");
|
||
writeFixtureFile(
|
||
workingDirectory,
|
||
".github/workflows/sync-edgeever-upstream.yml",
|
||
"name: upstream workflow v2\n",
|
||
);
|
||
runFixtureGit(workingDirectory, "add", ".");
|
||
runFixtureGit(workingDirectory, "commit", "-m", "upstream target v2");
|
||
runFixtureGit(workingDirectory, "tag", "upstream-v2");
|
||
runFixtureGit(workingDirectory, "checkout", "downstream");
|
||
prepareFixtureUpstreamSync(workingDirectory, "merge", "upstream-v2");
|
||
|
||
expect(readFileSync(resolve(workingDirectory, "upstream.txt"), "utf8")).toBe(
|
||
"upstream target v2\n",
|
||
);
|
||
expect(readFileSync(resolve(workingDirectory, "custom.txt"), "utf8")).toBe(
|
||
"downstream customization\n",
|
||
);
|
||
expect(readFileSync(resolve(
|
||
workingDirectory,
|
||
".github/workflows/sync-edgeever-upstream.yml",
|
||
), "utf8")).toBe("name: downstream updater\n");
|
||
runFixtureGit(
|
||
workingDirectory,
|
||
"commit",
|
||
"-m",
|
||
"sync customized fork v2",
|
||
"-m",
|
||
"EdgeEver-Upstream-Commit: upstream-v2",
|
||
);
|
||
expect(fixtureGitStatus(workingDirectory, "rev-parse", "--verify", "HEAD^2"))
|
||
.not.toBe(0);
|
||
} finally {
|
||
rmSync(workingDirectory, { force: true, recursive: true });
|
||
}
|
||
});
|
||
|
||
test("public deployment documentation exposes only Fork and Agent paths", () => {
|
||
const englishReadme = readRepositoryFile("README.md");
|
||
const chineseReadme = readRepositoryFile("README.zh-CN.md");
|
||
const traditionalChineseReadme = readRepositoryFile("README.zh-TW.md");
|
||
const japaneseReadme = readRepositoryFile("README.ja.md");
|
||
|
||
expect(englishReadme).not.toContain("deploy.workers.cloudflare.com");
|
||
expect(englishReadme).not.toContain("Option C: Manual Deployment");
|
||
expect(englishReadme).toContain("Fork https://github.com/tianma-if/edgeever");
|
||
expect(chineseReadme).not.toContain("deploy.workers.cloudflare.com");
|
||
expect(chineseReadme).not.toContain("方案 C:手动部署");
|
||
expect(chineseReadme).toContain("Fork https://github.com/tianma-if/edgeever");
|
||
expect(traditionalChineseReadme).not.toContain("deploy.workers.cloudflare.com");
|
||
expect(traditionalChineseReadme).not.toContain("方案 C:手動部署");
|
||
expect(traditionalChineseReadme).toContain("Fork https://github.com/tianma-if/edgeever");
|
||
expect(japaneseReadme).not.toContain("deploy.workers.cloudflare.com");
|
||
expect(japaneseReadme).not.toContain("方案 C:手動導入");
|
||
expect(japaneseReadme).toContain("Fork https://github.com/tianma-if/edgeever");
|
||
});
|
||
|
||
test("product site deployment prompts mirror the root READMEs", () => {
|
||
const englishReadme = readRepositoryFile("README.md");
|
||
const chineseReadme = readRepositoryFile("README.zh-CN.md");
|
||
const siteDeploymentComponent = readRepositoryFile(
|
||
"apps/site/src/components/agent-install-tabs.astro",
|
||
);
|
||
|
||
expect(deploymentPrompts["en-US"]).toBe(
|
||
extractTextPrompt(englishReadme, "### Option A: Deploy with an AI Agent (Recommended)"),
|
||
);
|
||
expect(deploymentPrompts["zh-CN"]).toBe(
|
||
extractTextPrompt(chineseReadme, "### 方案一:AI Agent 一键部署(推荐)"),
|
||
);
|
||
expect(siteDeploymentComponent).toContain('deploymentPrompts["en-US"]');
|
||
expect(siteDeploymentComponent).toContain('deploymentPrompts["zh-CN"]');
|
||
expect(siteDeploymentComponent).toContain('manualDeploymentCopy["en-US"]');
|
||
expect(siteDeploymentComponent).toContain('manualDeploymentCopy["zh-CN"]');
|
||
expect(siteDeploymentComponent).toContain("UnionPay");
|
||
expect(siteDeploymentComponent).toContain("银联(UnionPay)");
|
||
expect(siteDeploymentComponent).toContain("free storage allowance");
|
||
expect(siteDeploymentComponent).toContain("免费存储额度");
|
||
expect(siteDeploymentComponent).not.toContain("dual-currency credit card");
|
||
expect(siteDeploymentComponent).not.toContain("双币信用卡");
|
||
expect(siteDeploymentComponent).not.toContain("China Merchants Bank");
|
||
expect(siteDeploymentComponent).not.toContain("招商和浦发");
|
||
|
||
for (const [locale, readme, heading, separator] of [
|
||
["en-US", englishReadme, "### Option B: Manual Online Deployment", ": "],
|
||
["zh-CN", chineseReadme, "### 方案二:手动在线部署", ":"],
|
||
] as const) {
|
||
const section = normalizeMarkdownCopy(extractSection(readme, heading));
|
||
const manualCopy = manualDeploymentCopy[locale];
|
||
expect(section).toContain(manualCopy.intro);
|
||
manualCopy.steps.forEach((step, index) => {
|
||
expect(section).toContain(
|
||
normalizeMarkdownCopy(`${index + 1}. ${step.title}${separator}${step.body}`),
|
||
);
|
||
});
|
||
}
|
||
});
|
||
|
||
test("AI Agent deployment remains fully online", () => {
|
||
const englishAgentDoc = readRepositoryFile("docs/agent-deploy-cloudflare.md");
|
||
const chineseAgentDoc = readRepositoryFile("docs/agent-deploy-cloudflare.zh-CN.md");
|
||
|
||
expect(englishAgentDoc).toContain("Workers & Pages");
|
||
expect(englishAgentDoc).toContain("Update deployed EdgeEver");
|
||
expect(englishAgentDoc).toContain("deployment mirror by default");
|
||
expect(englishAgentDoc).toContain("EDGE_EVER_PRESERVE_FORK_CHANGES");
|
||
expect(englishAgentDoc).not.toContain("bun run deploy:manual");
|
||
expect(englishAgentDoc).not.toContain("deploy:setup");
|
||
expect(englishAgentDoc).not.toContain(".env.local");
|
||
expect(chineseAgentDoc).toContain("Workers & Pages");
|
||
expect(chineseAgentDoc).toContain("默认作为部署镜像");
|
||
expect(chineseAgentDoc).toContain("EDGE_EVER_PRESERVE_FORK_CHANGES");
|
||
expect(chineseAgentDoc).not.toContain("bun run deploy:manual");
|
||
expect(chineseAgentDoc).not.toContain("bun run deploy:manual");
|
||
});
|
||
});
|