Make workspace authoring converge through immutable Nix candidates
Coordinate registered resource edits bottom-up into retained exact remote sources. Use one Nix-owned source graph for provisional checking, template publication, explicit baseline upgrades and host activation; retain independent runtime pins. Add scoped contract inspection, historical recovery, derived worklists, crash-safe locks, named dependency adoption and plain-QX structural editing. Repair TODO ownership and template instantiation, and document the supported agent workflow. Validated with protocol and command suites, real jj/Nix convergence and cache checks, TS/React installed-command acceptance, and fresh TODO first-edit acceptance. No live deployment or public publication performed. Props projection generation and a one-command rich feature generator remain explicitly outside this delivery.
This commit is contained in:
@@ -0,0 +1,84 @@
|
||||
import test from "node:test";
|
||||
import assert from "node:assert/strict";
|
||||
import fs from "node:fs/promises";
|
||||
import os from "node:os";
|
||||
import path from "node:path";
|
||||
import { execFile as callback } from "node:child_process";
|
||||
import { promisify } from "node:util";
|
||||
import { convergeAuthoring } from "../src/capability-language/authoring-converge.js";
|
||||
import { loadQuixosLock } from "../src/resource-lock/index.js";
|
||||
const execFile = promisify(callback);
|
||||
|
||||
test("source convergence propagates nested edits and unchanged snapshots reach a fixed point", async context => {
|
||||
const temporary = await fs.mkdtemp(path.join(os.tmpdir(), "qx-converge-test-"));
|
||||
context.after(() => fs.rm(temporary, { recursive: true, force: true }));
|
||||
const workbench = path.join(temporary, "workbench"), remotes = path.join(temporary, "remotes");
|
||||
await fs.mkdir(path.join(workbench, ".quixos"), { recursive: true });
|
||||
await fs.mkdir(remotes);
|
||||
const origin = "https://convergence.example.test/";
|
||||
const previous = Object.fromEntries(["GIT_CONFIG_COUNT", "GIT_CONFIG_KEY_0", "GIT_CONFIG_VALUE_0"].map(key => [key, process.env[key]]));
|
||||
process.env.GIT_CONFIG_COUNT = "1";
|
||||
process.env.GIT_CONFIG_KEY_0 = `url.file://${remotes}/.insteadOf`;
|
||||
process.env.GIT_CONFIG_VALUE_0 = origin;
|
||||
context.after(() => { for (const [key, value] of Object.entries(previous)) { if (value === undefined) delete process.env[key]; else process.env[key] = value; } });
|
||||
const resources = [];
|
||||
let dependency = "";
|
||||
for (const [directory, kind, name] of [["resources/Base", "interface", "Base"], ["resources/Consumer", "package", "Consumer"], ["root", "workspace", "Root"]]) {
|
||||
const root = path.join(workbench, directory);
|
||||
await fs.mkdir(root, { recursive: true });
|
||||
await execFile("jj", ["git", "init", "--colocate", root]);
|
||||
await execFile("git", ["init", "--bare", path.join(remotes, name)]);
|
||||
const repository = `${origin}${name}`;
|
||||
await execFile("git", ["-C", root, "remote", "add", "origin", repository]);
|
||||
await fs.writeFile(path.join(root, "quixos.lock"), `quixos-lock version 1 { quixos source { repository "https://example.test/quixos"; commit "${"a".repeat(40)}"; } ${dependency} }`);
|
||||
await fs.writeFile(path.join(root, `${kind}.qx`), "draft");
|
||||
await execFile("jj", ["status"], { cwd: root });
|
||||
const commit = (await execFile("jj", ["--ignore-working-copy", "log", "--no-graph", "-r", "@", "-T", "commit_id"], { cwd: root })).stdout.trim();
|
||||
if (kind !== "workspace") resources.push({ directory, kind, resourceId: `${kind}:${name}`, source: { resolver: "git", repository, commit } });
|
||||
dependency = `${kind} ${name} source { repository "${repository}"; commit "${commit}"; }`;
|
||||
}
|
||||
await fs.writeFile(path.join(workbench, ".quixos/resource-graph.json"), JSON.stringify({ resources }));
|
||||
const first = await convergeAuthoring(workbench);
|
||||
assert.deepEqual(first.worklist, []);
|
||||
assert.equal(first.converged, true);
|
||||
assert.equal(first.verificationEvidence, false, "source retention never approves even syntactically invalid code");
|
||||
await fs.writeFile(path.join(workbench, "resources/Base/interface.qx"), "edited draft");
|
||||
const second = await convergeAuthoring(workbench);
|
||||
assert.deepEqual(second.worklist, []);
|
||||
assert.notEqual(second.candidate?.commit, first.candidate?.commit);
|
||||
const consumer = await loadQuixosLock(path.join(workbench, "resources/Consumer/quixos.lock"));
|
||||
assert.ok(consumer.ok);
|
||||
assert.equal(consumer.lock.resources[0].source.commit, second.retained.find(entry => entry.directory === "resources/Base")?.source.commit);
|
||||
const third = await convergeAuthoring(workbench);
|
||||
assert.deepEqual(third, second);
|
||||
const base = path.join(workbench, "resources/Base");
|
||||
const consumerRoot = path.join(workbench, "resources/Consumer");
|
||||
const goodLock = await fs.readFile(path.join(consumerRoot, "quixos.lock"), "utf8");
|
||||
await fs.writeFile(path.join(consumerRoot, "quixos.lock"), "broken draft");
|
||||
const scoped = await convergeAuthoring(workbench, "resources/Base");
|
||||
assert.deepEqual(scoped.worklist, [], "an unrelated malformed consumer cannot block a provider check");
|
||||
await fs.writeFile(path.join(base, "interface.qx"), "another edit");
|
||||
const partial = await convergeAuthoring(workbench);
|
||||
assert.equal(partial.converged, false);
|
||||
const graph = JSON.parse(await fs.readFile(path.join(workbench, ".quixos/resource-graph.json"), "utf8"));
|
||||
assert.equal(graph.resources[0].source.commit, partial.retained.find(entry => entry.directory === "resources/Base")?.source.commit);
|
||||
await fs.writeFile(path.join(consumerRoot, "quixos.lock"), goodLock);
|
||||
const resumed = await convergeAuthoring(workbench);
|
||||
assert.deepEqual(resumed.worklist, []);
|
||||
assert.deepEqual(await convergeAuthoring(workbench), resumed);
|
||||
await execFile("git", ["-C", base, "remote", "set-url", "origin", origin + "Wrong"]);
|
||||
const mismatch = await convergeAuthoring(workbench);
|
||||
assert.ok(mismatch.worklist.some(entry => entry.phase === "source" && /Origin differs/.test(entry.message)));
|
||||
await execFile("git", ["-C", base, "remote", "set-url", "origin", origin + "Base"]);
|
||||
await fs.rename(path.join(remotes, "Base"), path.join(remotes, "Base-offline"));
|
||||
const offline = await convergeAuthoring(workbench);
|
||||
assert.equal(offline.candidate, null);
|
||||
assert.ok(offline.worklist.some(entry => entry.phase === "publication"));
|
||||
await fs.rename(path.join(remotes, "Base-offline"), path.join(remotes, "Base"));
|
||||
assert.equal((await convergeAuthoring(workbench)).converged, true);
|
||||
const consumerSource = resumed.retained.find(entry => entry.directory === "resources/Consumer")!.source;
|
||||
await fs.writeFile(path.join(base, "quixos.lock"), `quixos-lock version 1 { quixos source { repository "https://example.test/quixos"; commit "${"a".repeat(40)}"; } package Consumer source { repository "${consumerSource.repository}"; commit "${consumerSource.commit}"; } }`);
|
||||
const cycle = await convergeAuthoring(workbench);
|
||||
assert.equal(cycle.candidate, null);
|
||||
assert.ok(cycle.worklist.some(entry => /Source dependency cycle/.test(entry.message)));
|
||||
});
|
||||
@@ -0,0 +1,37 @@
|
||||
import test from "node:test";
|
||||
import assert from "node:assert/strict";
|
||||
import { mkdtemp, writeFile, rm, symlink } from "node:fs/promises";
|
||||
import os from "node:os";
|
||||
import path from "node:path";
|
||||
import { execFile as callback } from "node:child_process";
|
||||
import { promisify } from "node:util";
|
||||
import { inspectAuthoringRepository } from "../src/capability-language/authoring-inspect.js";
|
||||
|
||||
const execFile = promisify(callback);
|
||||
test("inspection keeps current files and labels historical recovery without granting verification", async context => {
|
||||
const root = await mkdtemp(path.join(os.tmpdir(), "qx-inspection-test-"));
|
||||
context.after(() => rm(root, { recursive: true, force: true }));
|
||||
const git = (...args: string[]) => execFile("git", ["-C", root, ...args]);
|
||||
await git("init");
|
||||
await writeFile(path.join(root, "interface.qx"), 'interface Example id "interface:example" revision "interface:example@1" {}');
|
||||
await git("add", ".");
|
||||
await git("-c", "user.name=Test", "-c", "user.email=test@example.test", "commit", "-m", "contract");
|
||||
const commit = (await git("rev-parse", "HEAD")).stdout.trim();
|
||||
await writeFile(path.join(root, "interface.qx"), "interface broken {{{");
|
||||
await writeFile(path.join(root, "new.qx"), 'interface New id "interface:new" revision "interface:new@1" {}');
|
||||
const inspected = await inspectAuthoringRepository(root);
|
||||
assert.equal(inspected.verificationEvidence, false);
|
||||
assert.equal(inspected.resolutionChecked, false);
|
||||
assert.equal(inspected.files[0].status, "historical");
|
||||
assert.equal(inspected.files[0].revision, commit);
|
||||
assert.ok(inspected.files[0].currentErrors.length);
|
||||
assert.match(inspected.files[0].declarations[0].source, /Example/);
|
||||
assert.equal(inspected.files[1].status, "current");
|
||||
assert.match(inspected.files[1].declarations[0].source, /New/);
|
||||
assert.equal((await git("rev-parse", "HEAD")).stdout.trim(), commit);
|
||||
await symlink(path.join(root, "interface.qx"), path.join(root, "linked.qx"));
|
||||
const linked = (await inspectAuthoringRepository(root)).files.find(file => file.file === "linked.qx")!;
|
||||
assert.equal(linked.status, "unavailable");
|
||||
assert.deepEqual(linked.declarations, []);
|
||||
assert.match(JSON.stringify(linked.currentErrors), /ordinary files/);
|
||||
});
|
||||
@@ -0,0 +1,45 @@
|
||||
import test from "node:test";
|
||||
import assert from "node:assert/strict";
|
||||
import fs from "node:fs/promises";
|
||||
import os from "node:os";
|
||||
import path from "node:path";
|
||||
import {execFile as callback} from "node:child_process";
|
||||
import {promisify} from "node:util";
|
||||
import {authoringWorklist} from "../src/capability-language/authoring-worklist.js";
|
||||
import {checkRecordName} from "../src/capability-language/authoring-check.js";
|
||||
import {checkerIdentity, snapshotCommit} from "../src/capability-language/checked-build.js";
|
||||
const execFile = promisify(callback);
|
||||
|
||||
test("worklist grows and clears from current source, dependency and checker observations", async context => {
|
||||
const workbench = await fs.mkdtemp(path.join(os.tmpdir(), "qx-worklist-test-"));
|
||||
context.after(() => fs.rm(workbench, {recursive: true, force: true}));
|
||||
const root = path.join(workbench, "root"), provider = path.join(workbench, "resources/Base");
|
||||
await fs.mkdir(root); await fs.mkdir(provider, {recursive: true});
|
||||
await fs.mkdir(path.join(workbench, ".quixos/checks"), {recursive: true});
|
||||
const header = `quixos-lock version 1 { quixos source { repository "https://example.test/quixos"; commit "${"a".repeat(40)}"; }`;
|
||||
for (const directory of [root, provider]) await execFile("jj", ["git", "init", "--colocate", directory]);
|
||||
await fs.writeFile(path.join(provider, "interface.qx"), 'interface Base id "interface:base" revision "interface:base@1" {}');
|
||||
await fs.writeFile(path.join(provider, "quixos.lock"), `${header} }`);
|
||||
const baseCommit = await snapshotCommit(provider);
|
||||
await fs.writeFile(path.join(root, "workspace.qx"), `workspace Test id "workspace:test" revision "workspace:test@1" commit "${"b".repeat(40)}" { import interface Base; }`);
|
||||
await fs.writeFile(path.join(root, "quixos.lock"), `${header} interface Base source { repository "https://example.test/base"; commit "${baseCommit}"; } }`);
|
||||
const rootCommit = await snapshotCommit(root);
|
||||
await fs.writeFile(path.join(workbench, ".quixos/resource-graph.json"), JSON.stringify({resources: [
|
||||
{kind: "interface", directory: provider, source: {resolver: "git", repository: "https://example.test/base", commit: baseCommit}},
|
||||
]}));
|
||||
assert.equal((await authoringWorklist(workbench)).worklist.filter(entry => entry.phase === "unchecked").length, 2);
|
||||
const remember = (directory: string, commit: string, checker = checkerIdentity()) => fs.writeFile(
|
||||
path.join(workbench, ".quixos/checks", checkRecordName(directory)), JSON.stringify({commit, checker, phase: "checked", blockers: []}));
|
||||
await remember("root", rootCommit); await remember("resources/Base", baseCommit);
|
||||
assert.deepEqual((await authoringWorklist(workbench)).worklist, []);
|
||||
await fs.writeFile(path.join(provider, "interface.qx"), "interface broken {{{");
|
||||
const broken = await authoringWorklist(workbench);
|
||||
assert.ok(broken.worklist.some(entry => entry.directory === "resources/Base" && entry.phase === "syntax" && /historical/.test(entry.message)));
|
||||
assert.ok(broken.worklist.some(entry => entry.directory === "root" && entry.phase === "dependency"));
|
||||
await fs.writeFile(path.join(provider, "interface.qx"), 'interface Base id "interface:base" revision "interface:base@1" {}');
|
||||
assert.deepEqual((await authoringWorklist(workbench)).worklist, []);
|
||||
await remember("resources/Base", baseCommit, "old-checker");
|
||||
assert.ok((await authoringWorklist(workbench)).worklist.some(entry => /checker changed/.test(entry.message)));
|
||||
await fs.writeFile(path.join(workbench, ".quixos/checks", checkRecordName("resources/Base")), JSON.stringify({phase: "publication", blockers: ["source retention unavailable"]}));
|
||||
assert.ok((await authoringWorklist(workbench)).worklist.some(entry => entry.phase === "publication" && /source retention/.test(entry.message)));
|
||||
});
|
||||
@@ -208,6 +208,23 @@ test("related-object dependency views cannot traverse another conformance's priv
|
||||
assert.equal(validateWorkspaceRevision(workspace).some((entry) => entry.code === "private-attachment-access"), false, "Only an explicitly exported read-only traversal crosses ownership");
|
||||
});
|
||||
|
||||
test("public traversal permits a native inverse read without exporting mutation authority", () => {
|
||||
const workspace = makeValidCapabilityWorkspace();
|
||||
const owned = conformance(workspace, fixtureId.projectOwnedConformance);
|
||||
const index = owned.privateAttachments.findIndex(entry => entry.kind === "edge" && entry.id === fixtureId.projectOwner);
|
||||
const edge = owned.privateAttachments.splice(index, 1)[0];
|
||||
assert.ok(edge?.kind === "edge");
|
||||
conformance(workspace, fixtureId.projectNamedConformance).privateAttachments.push(edge);
|
||||
expectIssue(workspace, "private-attachment-access");
|
||||
edge.endpoints.find(endpoint => endpoint.projectionId === fixtureId.projectOwnerProjection)!.publicTraversal = true;
|
||||
const readPath = `conformances[${workspace.conformances.indexOf(owned)}].operationBindings[0]`;
|
||||
assert.equal(validateWorkspaceRevision(workspace).some(issue => issue.code === "private-attachment-access" && issue.path.startsWith(readPath)), false);
|
||||
const binding = owned.operationBindings[0].binding;
|
||||
assert.ok(binding.kind === "edge");
|
||||
binding.primitive = "connect";
|
||||
expectIssue(workspace, "private-attachment-access");
|
||||
});
|
||||
|
||||
test("native state and edge providers must match operation shape", () => {
|
||||
const stateWorkspace = makeValidCapabilityWorkspace();
|
||||
const state = conformance(
|
||||
|
||||
@@ -0,0 +1,33 @@
|
||||
import test from "node:test";
|
||||
import assert from "node:assert/strict";
|
||||
import {mkdtemp, rm} from "node:fs/promises";
|
||||
import path from "node:path";
|
||||
import os from "node:os";
|
||||
import {spawn} from "node:child_process";
|
||||
import {once} from "node:events";
|
||||
import {withFileLock} from "../src/capability-language/file-lock.js";
|
||||
|
||||
test("authoring lock excludes concurrent mutations and survives owner death", async context => {
|
||||
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");
|
||||
await withFileLock(filename, async () => {
|
||||
await assert.rejects(withFileLock(filename, async () => assert.fail("concurrent mutation")), /Another authoring command/);
|
||||
});
|
||||
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(() => {});});`],
|
||||
{stdio: ["ignore", "pipe", "pipe"]});
|
||||
context.after(() => owner.kill("SIGKILL"));
|
||||
await once(owner.stdout, "data");
|
||||
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; }
|
||||
}
|
||||
assert.equal(acquired, true);
|
||||
});
|
||||
@@ -0,0 +1,37 @@
|
||||
import test from "node:test";
|
||||
import assert from "node:assert/strict";
|
||||
import { mkdtemp, mkdir, writeFile, rm } from "node:fs/promises";
|
||||
import os from "node:os";
|
||||
import path from "node:path";
|
||||
import { execFile as callback } from "node:child_process";
|
||||
import { promisify } from "node:util";
|
||||
import { createGitCapabilityResolver } from "../src/capability-language/git-resolver.js";
|
||||
|
||||
const execFile = promisify(callback);
|
||||
test("dependency resolution is reusable across processes, concurrent and rejects modified checkouts", async context => {
|
||||
const temporary = await mkdtemp(path.join(os.tmpdir(), "qx-resolver-test-"));
|
||||
context.after(() => rm(temporary, { recursive: true, force: true }));
|
||||
const origin = path.join(temporary, "origin");
|
||||
await mkdir(origin);
|
||||
const git = (...args: string[]) => execFile("git", ["-C", origin, ...args]);
|
||||
await git("init");
|
||||
await writeFile(path.join(origin, "interface.qx"), "contract");
|
||||
await git("add", ".");
|
||||
await git("-c", "user.name=Test", "-c", "user.email=test@example.test", "commit", "-m", "contract");
|
||||
const commit = (await git("rev-parse", "HEAD")).stdout.trim();
|
||||
await git("tag", `quixos-reachability/${commit}`);
|
||||
const source = { resolver: "git" as const, repository: origin, commit };
|
||||
const options = { checkoutRoot: path.join(temporary, "cache") };
|
||||
const a = await createGitCapabilityResolver(options);
|
||||
const b = await createGitCapabilityResolver(options);
|
||||
const [first, second] = await Promise.all([a(source, "interface"), b(source, "interface")]);
|
||||
assert.equal(first.directory, second.directory);
|
||||
const c = await createGitCapabilityResolver(options);
|
||||
assert.equal((await c(source, "interface")).directory, first.directory);
|
||||
await writeFile(path.join(first.directory, "interface.qx"), "changed");
|
||||
const d = await createGitCapabilityResolver(options);
|
||||
await assert.rejects(d(source, "interface"), /modified/);
|
||||
// A failed request is not memoized forever; repair can be observed on retry.
|
||||
await writeFile(path.join(first.directory, "interface.qx"), "contract");
|
||||
assert.equal((await d(source, "interface")).directory, first.directory);
|
||||
});
|
||||
@@ -0,0 +1,34 @@
|
||||
import test from "node:test";
|
||||
import assert from "node:assert/strict";
|
||||
import fs from "node:fs/promises";
|
||||
import os from "node:os";
|
||||
import path from "node:path";
|
||||
import { execFile as callback } from "node:child_process";
|
||||
import { promisify } from "node:util";
|
||||
const execFile = promisify(callback);
|
||||
|
||||
test("Nix checks a retained immutable source without a local overlay and reuses the result", {skip: !process.env.QX_CHECK_GENERATOR}, async context => {
|
||||
const root = await fs.mkdtemp(path.join(os.tmpdir(), "qx-nix-candidate-test-"));
|
||||
context.after(() => fs.rm(root, {recursive: true, force: true}));
|
||||
const git = (...args: string[]) => execFile("git", ["-C", root, ...args]);
|
||||
await git("init");
|
||||
await fs.writeFile(path.join(root, "interface.qx"), 'interface Example id "interface:example" revision "interface:example@1" {}');
|
||||
await fs.writeFile(path.join(root, "quixos.lock"), `quixos-lock version 1 { quixos source { repository "https://example.test/quixos"; commit "${"a".repeat(40)}"; } }`);
|
||||
await git("add", ".");
|
||||
await git("-c", "user.name=Test", "-c", "user.email=test@example.test", "commit", "-m", "contract");
|
||||
const commit = (await git("rev-parse", "HEAD")).stdout.trim();
|
||||
await git("tag", `quixos-reachability/${commit}`);
|
||||
const generator = process.env.QX_CHECK_GENERATOR!;
|
||||
const repository = "https://immutable-candidate.example.test/contract.git";
|
||||
const env = {...process.env, GIT_CONFIG_COUNT: "1", GIT_CONFIG_KEY_0: `url.file://${root}.insteadOf`, GIT_CONFIG_VALUE_0: repository};
|
||||
const build = async () => (await execFile("nix", ["build", "--impure", "--file", path.join(generator, "share/checked-candidate.nix"),
|
||||
"--argstr", "repository", repository, "--argstr", "commit", commit,
|
||||
"--argstr", "kind", "interface", "--argstr", "generator", generator,
|
||||
"--option", "substitute", "false", "--no-link", "--print-out-paths"], {env, maxBuffer: 4 * 1024 * 1024})).stdout.trim();
|
||||
const output = await build();
|
||||
const candidate = JSON.parse(await fs.readFile(path.join(output, "candidate.json"), "utf8"));
|
||||
assert.equal(candidate.revision.source.commit, commit);
|
||||
await fs.writeFile(path.join(root, "interface.qx"), "broken draft");
|
||||
assert.equal(await build(), output);
|
||||
assert.deepEqual(JSON.parse(await fs.readFile(path.join(output, "checks.json"), "utf8")), []);
|
||||
});
|
||||
@@ -8,13 +8,14 @@ import {execFile as callback} from "node:child_process";
|
||||
import {planPinUpgrades, applyPinUpgrades, type UpgradeEffects} from "../src/capability-language/pin-upgrades.js";
|
||||
const execFile = promisify(callback);
|
||||
|
||||
test("real jj snapshots and immutable Git publication propagate a changed interface into the root", async (context) => {
|
||||
test("real jj snapshots and immutable Git publication propagate a changed interface into the root", {skip: !process.env.QX_CHECK_GENERATOR}, async (context) => {
|
||||
const workbench = await fs.mkdtemp(path.join(os.tmpdir(), "qx-real-upgrade-"));
|
||||
context.after(() => fs.rm(workbench, {recursive: true, force: true}));
|
||||
// Exercise the actual effects with local bare remotes, without external writes.
|
||||
const environment = {
|
||||
GIT_CONFIG_COUNT: "1", GIT_CONFIG_KEY_0: `url.file://${workbench}/remotes/.insteadOf`,
|
||||
GIT_CONFIG_VALUE_0: "https://upgrade.test/", QUIXOS_JJ_NO_CHECKPOINT: "1",
|
||||
QUIXOS_CHECK_GENERATOR: process.env.QX_CHECK_GENERATOR!,
|
||||
};
|
||||
const previous = Object.fromEntries(Object.keys(environment).map(key => [key, process.env[key]]));
|
||||
Object.assign(process.env, environment);
|
||||
|
||||
@@ -55,4 +55,8 @@ 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/);
|
||||
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" {}'}],
|
||||
}]}), /scaffold-owned package identity/);
|
||||
});
|
||||
|
||||
@@ -1,6 +1,17 @@
|
||||
import test from "node:test";
|
||||
import assert from "node:assert/strict";
|
||||
import { editStructure, scaffoldResourceSource } from "../src/capability-language/structural-edits.js";
|
||||
import { editStructure, scaffoldResourceSource, instantiateWorkspaceIdentity } from "../src/capability-language/structural-edits.js";
|
||||
|
||||
test("template identity binding changes only the workspace header and is idempotent", () => {
|
||||
const source = '// workspace fake id "do-not-touch"\nworkspace Todo id "workspace:todo" revision "workspace:todo@1" commit "' + "a".repeat(40) + '" { atom Task id "atom:task"; }';
|
||||
const id = "00000000-0000-0000-0000-000000000123";
|
||||
const bound = instantiateWorkspaceIdentity(source, id);
|
||||
assert.match(bound, /atom Task id "atom:task"/);
|
||||
assert.ok(bound.startsWith('// workspace fake id "do-not-touch"'));
|
||||
assert.ok(bound.includes(`workspace Todo id "${id}"`));
|
||||
assert.equal(instantiateWorkspaceIdentity(bound, id), bound);
|
||||
assert.throws(() => instantiateWorkspaceIdentity(source, "not-a-workspace"), /UUID/);
|
||||
});
|
||||
|
||||
test("package scaffolding and function edits preserve surrounding source and use exact selectors", () => {
|
||||
const source = `// 🧭 resource comment\n${scaffoldResourceSource("package", "Chess", "package:chess", "package:chess@1")}`;
|
||||
@@ -13,6 +24,9 @@ test("package scaffolding and function edits preserve surrounding source and use
|
||||
assert.ok(!editStructure(replaced, {operation: "remove", target: {kind: "packageFunctionExport", id: "export:evaluate"}}).includes("evaluate"));
|
||||
assert.throws(() => editStructure(source, {operation: "remove", target: {kind: "packageResourceDecl", id: "package:chess@1"}}), /exactly once/);
|
||||
assert.throws(() => editStructure(source, {operation: "append", parent: {kind: "packageResourceDecl"}, source: "not valid QX"}), /Invalid structural/);
|
||||
const prefixed = `import interface Board;\nexternal atom Game id "atom:game";\n${source}`;
|
||||
const replacedPackage = editStructure(prefixed, {operation: "replace", target: {kind: "packageResourceDecl", id: "package:chess"}, source: scaffoldResourceSource("package", "Chess", "package:chess", "package:chess@2")});
|
||||
assert.ok(replacedPackage.startsWith('import interface Board;\nexternal atom Game id "atom:game";'));
|
||||
});
|
||||
|
||||
test("dependency scaffolding validates exact sources and preserves unrelated lock comments", () => {
|
||||
|
||||
@@ -34,4 +34,13 @@ test("structural plans validate the graph, journal originals, and reject stale e
|
||||
await fs.writeFile(applied.journalPath, JSON.stringify(journal));
|
||||
assert.equal((await resumeStructure(root, applied.id)).phase, "complete");
|
||||
await assert.rejects(() => planStructure(root, request), /Duplicate|duplicate/);
|
||||
// Cross-repository edits may temporarily refer to an unfinished provider.
|
||||
const provisional: StructuralRequest = {kind: "workspace", validation: "syntax", files: [{file: "workspace.qx", edits: [
|
||||
{operation: "import", kind: "interface", name: "NotImplementedYet"},
|
||||
]}]};
|
||||
const draft = await planStructure(root, provisional);
|
||||
assert.equal(draft.validation, "syntax");
|
||||
await applyStructure(draft);
|
||||
assert.match(await fs.readFile(path.join(root, "workspace.qx"), "utf8"), /import interface NotImplementedYet/);
|
||||
await assert.rejects(() => planStructure(root, {...provisional, validation: "resource-graph"}));
|
||||
});
|
||||
|
||||
Reference in New Issue
Block a user