mirror of
https://github.com/nexu-io/open-design.git
synced 2026-09-20 06:15:06 +08:00
8ab7c3d5f1
* feat(standalone): add shell-neutral exact lifecycle * feat(terminal): add native exact cold-start carrier * test(terminal): exercise mutable exact channel heads * feat(standalone): model synced blobs and Shell updates * feat(terminal): exercise the complete Shell lifecycle * feat(release): add the independent exact Terminal line * test(exact): run the full local Terminal release line * feat(terminal): harden exact Sidecar lifecycle * feat(standalone): prove exact update transactions * refactor(standalone): separate shell lifecycle dimensions * refactor(e2e): decouple terminal exact drivers * refactor(terminal): isolate exact sidecar fixture host * feat(terminal): gate handoff on physical retirement * refactor(release): gate exact activation on public acceptance * test(terminal): formalize guarded lifecycle algebra * fix(terminal): guard shell installer handoff * feat(terminal): add exact bootloader handoff * feat(terminal): integrate exact sidecar lifecycle * refactor(exact): rename validation channels * test(terminal): harden exact release acceptance * fix(postinstall): build standalone dependencies first Generated-By: looper 0.11.2 (runner=fixer, agent=codex) * fix(ci): build the native Terminal scene Generated-By: looper 0.11.2 (runner=fixer, agent=codex) * test(ci): cover exact convergence handoff routing Generated-By: looper 0.11.2 (runner=fixer, agent=codex) * fix(exact): isolate terminal validation from main CI * fix(terminal): complete shell update confirmation Generated-By: looper 0.11.2 (runner=fixer, agent=codex) * fix(exact): bind acceptance to installed manifest Generated-By: looper 0.11.2 (runner=fixer, agent=codex) * fix(standalone): block occupied shell installs Generated-By: looper 0.11.2 (runner=fixer, agent=codex) --------- Co-authored-by: Looper <looper@noreply.github.com>
128 lines
5.2 KiB
TypeScript
128 lines
5.2 KiB
TypeScript
import { copyFileSync, mkdtempSync, readFileSync, rmSync } from "node:fs";
|
|
import { tmpdir } from "node:os";
|
|
import { join } from "node:path";
|
|
import { pathToFileURL } from "node:url";
|
|
|
|
import { afterEach, describe, expect, it, vi } from "vitest";
|
|
|
|
import {
|
|
FossilHandoffHost,
|
|
createStandaloneGenerationBinding,
|
|
sha256Hex,
|
|
type GenerationRecord,
|
|
type StandaloneGenerationHandoff,
|
|
type StandaloneHandoffRequest,
|
|
type StandaloneRuntimeHandle,
|
|
type StandaloneRuntimeStatus,
|
|
} from "@open-design/standalone";
|
|
|
|
import { repoRoot } from "./helpers.js";
|
|
|
|
const roots: string[] = [];
|
|
afterEach(() => { for (const root of roots.splice(0)) rmSync(root, { recursive: true, force: true }); });
|
|
|
|
const shell = Object.freeze({
|
|
type: "terminal",
|
|
version: "0.1.0",
|
|
buildHash: "b".repeat(64),
|
|
digest: "c".repeat(64),
|
|
});
|
|
|
|
function generation(path: string, digest: string, id = "a".repeat(64)): GenerationRecord {
|
|
return {
|
|
schemaVersion: 4,
|
|
id,
|
|
channel: "somechan",
|
|
releaseVersion: "0.1.0-somechan.1",
|
|
standaloneVersion: "0.1.0",
|
|
sourceCommit: "d".repeat(40),
|
|
minimumShellVersions: { terminal: "0.1.0", electron: "1.0.0" },
|
|
launcher: { protocol: "standalone-launcher-v1", resourceId: "standalone-launcher", blobSha256: digest, entrypoint: "launcher.mjs", path },
|
|
resources: {
|
|
"standalone-launcher": {
|
|
component: "standalone.launcher",
|
|
blobSha256: digest,
|
|
entrypoint: "launcher.mjs",
|
|
materialization: { type: "file", entrypoint: "launcher.mjs" },
|
|
mediaType: "text/javascript",
|
|
path,
|
|
size: readFileSync(path).byteLength,
|
|
sync: true,
|
|
},
|
|
},
|
|
};
|
|
}
|
|
|
|
function runtime(initial: StandaloneHandoffRequest): StandaloneRuntimeHandle {
|
|
let state: StandaloneRuntimeStatus["state"] = "running";
|
|
let resolveTerminal!: (status: StandaloneRuntimeStatus) => void;
|
|
const terminal = new Promise<StandaloneRuntimeStatus>((resolve) => { resolveTerminal = resolve; });
|
|
const status = (): StandaloneRuntimeStatus => ({
|
|
bindingDigest: initial.binding.digest,
|
|
generationId: initial.binding.generationId,
|
|
instanceId: "terminal-fake-sidecar",
|
|
references: state === "running" ? 1 : 0,
|
|
state,
|
|
});
|
|
return {
|
|
readStatus: async () => status(),
|
|
invoke: async (command) => ({
|
|
requestId: command.requestId,
|
|
attachmentId: command.attachmentId,
|
|
bindingDigest: initial.binding.digest,
|
|
outcome: "accepted",
|
|
output: { command: command.command },
|
|
}),
|
|
close: async () => {
|
|
state = "stopped";
|
|
const stopped = status();
|
|
resolveTerminal(stopped);
|
|
return stopped;
|
|
},
|
|
waitForTerminal: async () => terminal,
|
|
};
|
|
}
|
|
|
|
describe("Terminal bootloader handoff host", () => {
|
|
it("loads the materialized generation launcher once and covers Electron-shaped attachments and progress callbacks", async () => {
|
|
const root = mkdtempSync(join(tmpdir(), "terminal-bootloader-handoff-")); roots.push(root);
|
|
const launcherPath = join(root, "launcher.mjs");
|
|
copyFileSync(join(repoRoot, "packages/standalone/dist/index.mjs"), launcherPath);
|
|
const launcherDigest = sha256Hex(readFileSync(launcherPath));
|
|
const binding = createStandaloneGenerationBinding(generation(launcherPath, launcherDigest), { channel: "somechan", namespace: "shared" });
|
|
const starts = vi.fn(async (initial: StandaloneHandoffRequest) => runtime(initial));
|
|
const imports = vi.fn(async (selected): Promise<StandaloneGenerationHandoff> => {
|
|
expect(selected.launcher.path).toBe(launcherPath);
|
|
expect(sha256Hex(readFileSync(selected.launcher.path))).toBe(selected.launcher.blobSha256);
|
|
const module = await import(pathToFileURL(selected.launcher.path).href) as typeof import("@open-design/standalone");
|
|
return module.createStandaloneGenerationBootloader(starts);
|
|
});
|
|
const host = new FossilHandoffHost(imports);
|
|
const progress: string[] = [];
|
|
const request = (id: string, type: "terminal" | "electron", version: string): StandaloneHandoffRequest => ({
|
|
binding,
|
|
attachment: { id, shell: { ...shell, type, version } },
|
|
capabilities: {
|
|
invoke: async (value) => {
|
|
progress.push(`${value.attachmentId}:${value.capability}`);
|
|
return { requestId: value.requestId, attachmentId: value.attachmentId, bindingDigest: value.bindingDigest, outcome: "accepted", output: { completed: 1, total: 1 } };
|
|
},
|
|
},
|
|
});
|
|
const terminal = await host.handoff(request("terminal", "terminal", "0.1.0"));
|
|
const electron = await host.handoff(request("electron", "electron", "1.0.0"));
|
|
expect(imports).toHaveBeenCalledTimes(1);
|
|
expect(starts).toHaveBeenCalledTimes(1);
|
|
const bodyRequest = starts.mock.calls[0]![0];
|
|
await expect(bodyRequest.capabilities.invoke({
|
|
requestId: "progress",
|
|
attachmentId: "electron",
|
|
bindingDigest: binding.digest,
|
|
capability: "cold-start.progress",
|
|
})).resolves.toMatchObject({ outcome: "accepted", output: { completed: 1, total: 1 } });
|
|
expect(progress).toEqual(["electron:cold-start.progress"]);
|
|
await expect(terminal.close()).resolves.toMatchObject({ state: "stopped", references: 1 });
|
|
await expect(electron.close()).resolves.toMatchObject({ state: "stopped", references: 0 });
|
|
});
|
|
});
|