Repair workspace authoring: typed RPC inputs, automatic preservation, and template releases
Use checked input contracts and protobuf JSON for CLI roundtrips; generate Web Studio platform inputs; reject invalid constructors before allocation and filter Createable eligibility. Preserve compatible storage without no-op migrations, report activation readiness, and validate migration coverage before maintenance. Follow authored package declarations during scaffold refresh and queue source capture. Add a real local TODO check/activate/create/place/edit acceptance, publish updated protocol and SDK dependencies, and make verified template default selection explicit.
This commit is contained in:
+23
-1
@@ -1,7 +1,7 @@
|
||||
import assert from "node:assert/strict";
|
||||
import { test } from "node:test";
|
||||
import { capabilityId as id, valueType, validateWorkspaceRevision } from "../src/capability-model/index.js";
|
||||
import { contentDigest, planEvolution, runtimeContracts, storageContracts } from "../src/capability-model/evolution.js";
|
||||
import { contentDigest, planEvolution, runtimeContracts, storageContracts, storageChangeRequiresMigration } from "../src/capability-model/evolution.js";
|
||||
import { capabilityFixtureSource, capabilityResourceSources, compileCapabilityFixture, makeValidCapabilityWorkspace } from "./fixtures/capability-model.js";
|
||||
|
||||
test("QX carries stable conformance IDs and implementation semantic majors", () => {
|
||||
@@ -68,6 +68,7 @@ test("storage defaults and ownership changes invalidate consumers without requir
|
||||
if (slot.kind === "state") slot.defaultValue = "Different default";
|
||||
const report = planEvolution(before, after, { allowLegacy: true });
|
||||
assert.equal(report.storageChanges.length, 1);
|
||||
assert.deepEqual(report.migrationRequired, []);
|
||||
assert.equal(report.runtimeActions[0]!.action, "replace");
|
||||
assert.deepEqual(report.reviews, []);
|
||||
assert.notDeepEqual(storageContracts(before), storageContracts(after));
|
||||
@@ -96,3 +97,24 @@ test("evolution enrollment is explicit and never erases legacy ownership", () =>
|
||||
assert.equal(runtimeContracts(workspace).length, 1);
|
||||
assert.throws(() => planEvolution(workspace, { ...workspace, workspaceId: id.workspace("other") }), /different workspace/);
|
||||
});
|
||||
|
||||
test("automatic preservation distinguishes additions and defaults from incompatible storage", () => {
|
||||
const workspace = makeValidCapabilityWorkspace();
|
||||
const before = storageContracts(workspace).find(entry => entry.kind === "state")!;
|
||||
const next = structuredClone(before);
|
||||
const value = next.definition as Record<string, unknown>;
|
||||
value.defaultValue = {displayName: "important user data"};
|
||||
assert.equal(storageChangeRequiresMigration(before, next, new Set()), false);
|
||||
next.ownerId = "another-owner";
|
||||
assert.equal(storageChangeRequiresMigration(before, next, new Set()), true);
|
||||
assert.equal(storageChangeRequiresMigration(before, undefined, new Set()), true);
|
||||
delete value.defaultValue;
|
||||
assert.equal(storageChangeRequiresMigration(undefined, next, new Set([value.attachedTo as string])), true);
|
||||
assert.equal(storageChangeRequiresMigration(undefined, next, new Set()), false);
|
||||
const slot = workspace.sharedAttachments.find(entry => entry.kind === "state")!;
|
||||
if (slot.kind !== "state") throw new Error("fixture slot");
|
||||
slot.defaultValue = {displayName: "one"};
|
||||
const first = storageContracts(workspace);
|
||||
slot.defaultValue = {displayName: "two"};
|
||||
assert.notDeepEqual(storageContracts(workspace), first, "user data must not be stripped as schema metadata");
|
||||
});
|
||||
|
||||
@@ -11,9 +11,14 @@ test("authoring lock excludes concurrent mutations and survives owner death", as
|
||||
const root = await mkdtemp(path.join(os.tmpdir(), "qx-lock-test-"));
|
||||
context.after(() => rm(root, {recursive: true, force: true}));
|
||||
const filename = path.join(root, "lock");
|
||||
const events: string[] = [];
|
||||
let queued: Promise<void>;
|
||||
await withFileLock(filename, async () => {
|
||||
await assert.rejects(withFileLock(filename, async () => assert.fail("concurrent mutation")), /Another authoring command/);
|
||||
queued = withFileLock(filename, async () => {events.push("second");});
|
||||
events.push("first");
|
||||
});
|
||||
await queued!;
|
||||
assert.deepEqual(events, ["first", "second"]);
|
||||
const module = new URL("../src/capability-language/file-lock.js", import.meta.url).href;
|
||||
const owner = spawn(process.execPath, ["--input-type=module", "-e",
|
||||
`import {withFileLock} from ${JSON.stringify(module)}; await withFileLock(${JSON.stringify(filename)}, async () => {process.stdout.write('ready'); await new Promise(() => {});});`],
|
||||
@@ -23,11 +28,7 @@ test("authoring lock excludes concurrent mutations and survives owner death", as
|
||||
const exited = once(owner, "exit");
|
||||
owner.kill("SIGKILL");
|
||||
await exited;
|
||||
// EOF release happens in the helper; wait a bounded amount for scheduling.
|
||||
let acquired = false;
|
||||
for (let attempt = 0; attempt < 30 && !acquired; attempt++) {
|
||||
try { await withFileLock(filename, async () => {acquired = true;}); }
|
||||
catch (error) { if (!/Another authoring command/.test(String(error))) throw error; }
|
||||
}
|
||||
await withFileLock(filename, async () => {acquired = true;});
|
||||
assert.equal(acquired, true);
|
||||
});
|
||||
|
||||
@@ -55,6 +55,12 @@ test("package/function/migration scaffolds register implementations and refresh
|
||||
await execFile("nix-instantiate", ["--parse", path.join(packageRoot, "flake.nix")]);
|
||||
}
|
||||
await assert.rejects(() => apply("function", {...base, name: "play", id: "export:play"}), /unique/);
|
||||
const declarations = path.join(root, base.directory, "package.qx");
|
||||
await fs.writeFile(declarations, (await fs.readFile(declarations, "utf8")).replace(/}\s*$/, ' function authored id "export:authored" : unit -> unit;\n}\n'));
|
||||
await apply("refresh", base);
|
||||
assert.match(await fs.readFile(path.join(root, base.directory, "src/server.ts"), "utf8"), /"authored":/);
|
||||
assert.match(await fs.readFile(path.join(root, base.directory, "src/impl/authored.ts"), "utf8"), /Implement authored/);
|
||||
assert.equal(await fs.readFile(filename, "utf8"), edited);
|
||||
await assert.rejects(() => planStructure(root, {kind: "package", source, resourceRoot: base.directory, validation: "syntax", files: [{
|
||||
file: `${base.directory}/package.qx`, edits: [{operation: "replace", target: {kind: "packageResourceDecl", id: "package:chess"},
|
||||
source: 'package Other id "package:other" revision "package:other@1" {}'}],
|
||||
|
||||
Reference in New Issue
Block a user