import { afterEach, beforeEach, describe, expect, test } from "bun:test"; import fs from "fs"; import os from "os"; import path from "path"; const RUN_MANUAL = process.env.KIT_REMOTE_MANU_TEST === "1"; const manualDescribe = RUN_MANUAL ? describe : describe.skip; const manualTest = RUN_MANUAL ? test : test.skip; let Remote; if (RUN_MANUAL) { require("../src/init"); Remote = require("../src/remote"); } async function createRemote() { const remote = new Remote(); await remote.init(); return remote; } function writeFixtureProject(tempDir, version, content, options = {}) { const projectId = options.id || "myapp"; const group = options.group === undefined ? "/modules" : options.group; const projectDir = path.resolve(tempDir, `fixture-${projectId}-${version || "auto"}-${Math.random().toString(16).slice(2)}`); fs.mkdirSync(projectDir, { recursive: true }); const filePath = path.resolve(projectDir, "myapp.txt"); fs.writeFileSync(filePath, content, "utf-8"); fs.writeFileSync(path.resolve(projectDir, "meta.json"), JSON.stringify({ id: projectId, group, version, desc: "test app", official_url: "https://example.test/myapp", dist: [ { path: filePath, os: "any", platform: "any", filename: "myapp.txt", type: "data" } ] }, null, 4), "utf-8"); return projectDir; } function normalizeRemotePath(value) { if (!value || value === "/") { return "/"; } return path.posix.normalize("/" + String(value).replace(/\\/g, "/")).replace(/\/+$/g, "") || "/"; } function remoteProjectPath(group, projectId) { const normalizedGroup = group === undefined ? "/modules" : normalizeRemotePath(group); return normalizeRemotePath(path.posix.join(normalizedGroup, projectId)); } async function deleteRemotePath(remote, remotePath) { try { if (remote.client && remote.client.deleteFile && await remote.client.exists(remotePath)) { await remote.client.deleteFile(remotePath); } } catch (e) { console.warn(`cleanup remote failed ${remotePath}: ${e.message}`); } } let tempDir; let localStore; let remote; let runId; let counter; let projectIds; let remoteRoots; beforeEach(async () => { if (!RUN_MANUAL) { return; } tempDir = fs.mkdtempSync(path.resolve(os.tmpdir(), "kit-remote-manu-test-")); localStore = $sConfig.getLocalStorePath(); remote = await createRemote(); runId = `${Date.now()}-${Math.random().toString(16).slice(2)}`; counter = 0; projectIds = []; remoteRoots = []; }); afterEach(async () => { if (!RUN_MANUAL) { return; } for (const remoteRoot of remoteRoots.reverse()) { await deleteRemotePath(remote, remoteRoot); } for (const projectId of projectIds) { fs.rmSync(path.resolve(localStore, "modules", projectId), { recursive: true, force: true }); } fs.rmSync(tempDir, { recursive: true, force: true }); }); function nextProjectId() { const projectId = `myapp-${runId}-${++counter}`.toLowerCase(); projectIds.push(projectId); return projectId; } function trackRemote(group, projectId) { const root = remoteProjectPath(group, projectId); remoteRoots.push(root); return root; } manualDescribe("remote module public API manual webdav", () => { manualTest("1-1 exposes documented public methods", async () => { expect(typeof remote.check).toBe("function"); expect(typeof remote.publish).toBe("function"); expect(typeof remote.install).toBe("function"); expect(typeof remote.list).toBe("function"); expect(typeof remote.use).toBe("function"); expect(typeof remote.rollback).toBe("function"); expect(typeof remote.remove).toBe("function"); }); manualTest("1-2 check validates an available webdav client", async () => { expect(await remote.check()).toBe(true); }); manualTest("1-3 publish stores files under default group /modules/id", async () => { const projectId = nextProjectId(); trackRemote("/modules", projectId); const projectDir = writeFixtureProject(tempDir, "1.0", "hello v1", { id: projectId }); await remote.publish(projectDir); const metadata = JSON.parse(await remote.client.getFileContents(`/modules/${projectId}/metadata.json`, { format: "text" })); const version = JSON.parse(await remote.client.getFileContents(`/modules/${projectId}/1.0/version.json`, { format: "text" })); const artifact = await remote.client.getFileContents(`/modules/${projectId}/1.0/any/any/myapp.txt`, { format: "text" }); expect(metadata.project_id).toBe(projectId); expect(metadata.group).toBe("/modules"); expect(metadata.version).toBe("1.0"); expect(metadata.versions).toBeUndefined(); expect(metadata.latest_version).toBeUndefined(); expect(version.artifacts[0].path).toBe("any/any/myapp.txt"); expect(artifact).toBe("hello v1"); }, 30000); manualTest("1-4 publish supports custom nested group", async () => { const projectId = nextProjectId(); const group = `/kit-manual-test/${runId}/docker/net`; trackRemote(group, projectId); await remote.publish(writeFixtureProject(tempDir, "2.0", "docker net", { id: projectId, group })); const metadata = JSON.parse(await remote.client.getFileContents(`${group}/${projectId}/metadata.json`, { format: "text" })); const artifact = await remote.client.getFileContents(`${group}/${projectId}/2.0/any/any/myapp.txt`, { format: "text" }); expect(metadata.group).toBe(group); expect(metadata.version).toBe("2.0"); expect(artifact).toBe("docker net"); }, 30000); manualTest("1-5 empty group stores project at repository root", async () => { const projectId = nextProjectId(); trackRemote("", projectId); await remote.publish(writeFixtureProject(tempDir, "1.0", "root group", { id: projectId, group: "" })); const metadata = JSON.parse(await remote.client.getFileContents(`/${projectId}/metadata.json`, { format: "text" })); expect(metadata.group).toBe("/"); expect(await remote.client.getFileContents(`/${projectId}/1.0/any/any/myapp.txt`, { format: "text" })).toBe("root group"); }, 30000); manualTest("1-6 explicit version fails when remote version already exists", async () => { const projectId = nextProjectId(); trackRemote("/modules", projectId); await remote.publish(writeFixtureProject(tempDir, "1.0", "hello v1", { id: projectId })); await expect(remote.publish(writeFixtureProject(tempDir, "1.0", "hello again", { id: projectId }))).rejects.toThrow("远程版本已存在"); }, 30000); manualTest("1-7 blank version increments remote version tail or starts at 1.0", async () => { const projectId = nextProjectId(); trackRemote("/modules", projectId); await remote.publish(writeFixtureProject(tempDir, "", "first auto", { id: projectId })); let metadata = JSON.parse(await remote.client.getFileContents(`/modules/${projectId}/metadata.json`, { format: "text" })); expect(metadata.version).toBe("1.0"); await remote.publish(writeFixtureProject(tempDir, "", "second auto", { id: projectId })); metadata = JSON.parse(await remote.client.getFileContents(`/modules/${projectId}/metadata.json`, { format: "text" })); expect(metadata.version).toBe("1.1"); expect(await remote.client.getFileContents(`/modules/${projectId}/1.1/any/any/myapp.txt`, { format: "text" })).toBe("second auto"); }, 30000); manualTest("1-8 install downloads latest version from default group", async () => { const projectId = nextProjectId(); trackRemote("/modules", projectId); await remote.publish(writeFixtureProject(tempDir, "1.0", "hello v1", { id: projectId })); const installed = await remote.install(projectId); const moduleRoot = path.resolve(localStore, "modules", projectId); const installJson = JSON.parse(fs.readFileSync(path.resolve(moduleRoot, ".meta", "install.json"), "utf-8")); const installedFile = fs.readFileSync(path.resolve(moduleRoot, "versions", "1.0", "any", "any", "myapp.txt"), "utf-8"); expect(installed).toBe("1.0"); expect(installedFile).toBe("hello v1"); expect(installJson.current_version).toBe("1.0"); expect(installJson.installed_versions).toContain("1.0"); expect(fs.existsSync(path.resolve(moduleRoot, "current"))).toBe(true); }, 30000); manualTest("1-9 install can target a custom group path", async () => { const projectId = nextProjectId(); const group = `/kit-manual-test/${runId}/docker/net`; trackRemote(group, projectId); await remote.publish(writeFixtureProject(tempDir, "1.0", "docker install", { id: projectId, group })); const installed = await remote.install(`${group}/${projectId}`); const installedFile = fs.readFileSync(path.resolve(localStore, "modules", projectId, "versions", "1.0", "any", "any", "myapp.txt"), "utf-8"); expect(installed).toBe("1.0"); expect(installedFile).toBe("docker install"); }, 30000); manualTest("1-10 list use rollback and remove manage installed versions", async () => { const projectId = nextProjectId(); trackRemote("/modules", projectId); await remote.publish(writeFixtureProject(tempDir, "1.0", "hello v1", { id: projectId })); await remote.install(projectId); await remote.publish(writeFixtureProject(tempDir, "2.0", "hello v2", { id: projectId })); await remote.install(projectId); expect(await remote.list(projectId)).toEqual(["1.0", "2.0"]); await remote.rollback(projectId); let installJson = JSON.parse(fs.readFileSync(path.resolve(localStore, "modules", projectId, ".meta", "install.json"), "utf-8")); expect(installJson.current_version).toBe("1.0"); await remote.use(`${projectId}@2.0`); installJson = JSON.parse(fs.readFileSync(path.resolve(localStore, "modules", projectId, ".meta", "install.json"), "utf-8")); expect(installJson.current_version).toBe("2.0"); await remote.remove(`${projectId}@1.0`); installJson = JSON.parse(fs.readFileSync(path.resolve(localStore, "modules", projectId, ".meta", "install.json"), "utf-8")); expect(installJson.installed_versions).toEqual(["2.0"]); expect(fs.existsSync(path.resolve(localStore, "modules", projectId, "versions", "1.0"))).toBe(false); }, 30000); }); if (!RUN_MANUAL) { describe("remote manual webdav tests", () => { test.skip("set KIT_REMOTE_MANU_TEST=1 and run bun test test/remote_manu_test.js", () => {}); }); }