Files
stack/packages/organization/tests/definitions.test.mjs

366 lines
23 KiB
JavaScript

// #1873 T1: organization definitions and templates (roles-and-rails design sections 4, 5 and 13).
import test from "node:test";
import assert from "node:assert/strict";
import * as fs from "node:fs";
import { syncBuiltinESMExports } from "node:module";
import { appendFileSync, readFileSync, rmSync, writeFileSync } from "node:fs";
import { join } from "node:path";
import { organizationWriter } from "../src/organization.mjs";
import { validateBusinessDocument } from "../src/organization-config.mjs";
import { definitionVerbs, DEFINITION_VERBS } from "../src/definition-verbs.mjs";
import { readTemplate, templateHash, templateHashes, diffDefinition, lineDiff } from "../src/definitions.mjs";
import { resolveInstance } from "../src/resolve.mjs";
import { ROLE_RIGHTS, VERB_RIGHTS, normalizeRights } from "../src/org-rights.mjs";
import { businessDoc, rolesCopy, systemFor, tmp, writeJson } from "./helpers.mjs";
const SECRET = "sk-test-NOT-A-SECRET-1234";
// The credential store's check, as if it held SECRET.
const assertClean = (value) => {
if (String(value).includes(SECRET)) throw new Error(`credential-leak ${value}`);
};
const OWNER = { actor: "jason", rights: [...ROLE_RIGHTS.owner], agent: false };
const MEMBER = { actor: "alice", rights: [...ROLE_RIGHTS.member], agent: false };
const HIRER = { actor: "alice", rights: normalizeRights(ROLE_RIGHTS.member, { add: ["org.hire"] }), agent: false };
const PLAIN = { role: { tools: ["read"], network: "none", authority: { withinRole: [], crossRole: [] }, credentials: [] }, instructions: "Read and report.\n" };
function fixture(t, edit = (doc) => doc) {
const root = tmp();
t.after(() => rmSync(root, { recursive: true, force: true }));
const dir = join(root, "config");
const rolesDir = rolesCopy(root);
const file = writeJson(join(dir, "businesses/acme.json"), edit(businessDoc(root)));
const writer = organizationWriter({ dir, rolesDir });
return { root, dir, file, rolesDir, writer, verbs: definitionVerbs({ writer, rolesDir, assertClean }) };
}
const rev = (f) => f.writer.read("acme").revision;
const doc = (f) => JSON.parse(readFileSync(f.file, "utf8"));
test("T1 import pins the template hash and copies its primitives and contract", (t) => {
const f = fixture(t);
const hash = templateHash(f.rolesDir, "coder");
assert.match(hash, /^[0-9a-f]{64}$/);
const receipt = f.verbs.import("acme", { revision: rev(f), template: "coder" }, OWNER);
assert.deepEqual(Object.keys(receipt).sort(), ["definition", "from", "organization", "rev", "revision", "transaction", "verb"]);
assert.equal(receipt.rev, 1);
assert.deepEqual(receipt.from, { template: "coder", sha256: hash });
const copy = doc(f).definitions.coder;
assert.deepEqual(copy.from, { template: "coder", sha256: hash });
assert.equal(copy.rev, 1);
assert.equal(copy.instructions, readFileSync(join(f.rolesDir, "coder.md"), "utf8"));
const template = JSON.parse(readFileSync(join(f.rolesDir, "coder.json"), "utf8"));
assert.deepEqual(copy.role.tools, template.tools);
assert.equal(copy.role.network, template.network);
assert.equal(receipt.revision, rev(f));
// The hash read lists every version 2 template, and only those.
const hashes = templateHashes(f.rolesDir);
assert.ok(hashes.some((h) => h.template === "coder" && h.sha256 === hash));
assert.ok(!hashes.some((h) => h.template === "conductor-policy"));
// --as names the copy.
const second = f.verbs.import("acme", { revision: rev(f), template: "coder", as: "coder-two" }, OWNER);
assert.equal(second.definition, "coder-two");
assert.throws(() => f.verbs.import("acme", { revision: rev(f), template: "coder" }, OWNER), { code: "definition-exists" });
});
test("T1 the hash covers the template JSON and its contract together", (t) => {
const f = fixture(t);
const before = templateHash(f.rolesDir, "coder");
appendFileSync(join(f.rolesDir, "coder.md"), "\nOne more line.\n");
const contractChanged = templateHash(f.rolesDir, "coder");
assert.notEqual(contractChanged, before);
const json = JSON.parse(readFileSync(join(f.rolesDir, "coder.json"), "utf8"));
writeJson(join(f.rolesDir, "coder.json"), { ...json, title: `${json.title} (renamed)` }, 0o644);
assert.notEqual(templateHash(f.rolesDir, "coder"), contractChanged);
});
test("T1 editing a template file changes no copy, and the diff read shows the change", (t) => {
const f = fixture(t);
f.verbs.import("acme", { revision: rev(f), template: "coder" }, OWNER);
const bytes = readFileSync(f.file);
const pinned = doc(f).definitions.coder.from.sha256;
assert.deepEqual(f.verbs.diff("acme", "coder"), { organization: "acme", revision: rev(f), definition: "coder", rev: 1, from: { template: "coder", sha256: pinned }, current: pinned, changed: false, primitives: [], instructions: [] });
appendFileSync(join(f.rolesDir, "coder.md"), "Always run the suites twice.\n");
const json = JSON.parse(readFileSync(join(f.rolesDir, "coder.json"), "utf8"));
writeJson(join(f.rolesDir, "coder.json"), { ...json, network: "none" }, 0o644);
assert.notEqual(json.network, "none", "the fixture must change network");
// No copy changed, and the file still loads.
assert.deepEqual(readFileSync(f.file), bytes);
assert.equal(f.writer.read("acme").document.definitions.coder.from.sha256, pinned);
const diff = f.verbs.diff("acme", "coder");
assert.equal(diff.changed, true);
assert.equal(diff.current, templateHash(f.rolesDir, "coder"));
assert.notEqual(diff.current, pinned);
assert.deepEqual(diff.primitives, ["network", "instructions"]);
assert.deepEqual(diff.instructions.filter((l) => l.op !== " "), [{ op: "+", line: "Always run the suites twice." }]);
// Nothing applied.
assert.deepEqual(readFileSync(f.file), bytes);
});
test("T1 diff read: an invented definition has nothing to compare, a missing template is named", (t) => {
const f = fixture(t);
f.verbs.invent("acme", { revision: rev(f), name: "scout", definition: PLAIN }, OWNER);
assert.equal(f.verbs.diff("acme", "scout").changed, false);
assert.equal(f.verbs.diff("acme", "scout").current, null);
f.verbs.import("acme", { revision: rev(f), template: "proofreader" }, OWNER);
rmSync(join(f.rolesDir, "proofreader.json"));
const gone = diffDefinition(f.writer.read("acme").document, "proofreader", f.rolesDir);
assert.equal(gone.templateMissing, true);
assert.throws(() => f.verbs.diff("acme", "nobody"), { name: "BusinessError" });
assert.deepEqual(lineDiff("a\nb", "a\nc"), [{ op: " ", line: "a" }, { op: "-", line: "b" }, { op: "+", line: "c" }]);
});
test("T1 rev counts up on every write, and a stale write refuses", (t) => {
const f = fixture(t);
assert.equal(f.verbs.invent("acme", { revision: rev(f), name: "scout", definition: PLAIN }, OWNER).rev, 1);
const edited = { ...PLAIN, instructions: "Read, then report.\n" };
assert.equal(f.verbs.edit("acme", { revision: rev(f), rev: 1, name: "scout", definition: edited }, OWNER).rev, 2);
assert.equal(f.verbs.edit("acme", { revision: rev(f), rev: 2, name: "scout", definition: { ...edited, personality: { summary: "Terse." } } }, OWNER).rev, 3);
assert.equal(doc(f).definitions.scout.rev, 3);
const bytes = readFileSync(f.file);
// A stale rev, a stale file revision, an unchanged edit.
assert.throws(() => f.verbs.edit("acme", { revision: rev(f), rev: 2, name: "scout", definition: PLAIN }, OWNER), { code: "definition-rev-conflict" });
const old = rev(f);
assert.throws(() => f.verbs.edit("acme", { revision: "0".repeat(64), rev: 3, name: "scout", definition: PLAIN }, OWNER), { code: "revision-conflict" });
assert.throws(() => f.verbs.edit("acme", { revision: old, rev: 3, name: "scout", definition: { ...edited, personality: { summary: "Terse." } } }, OWNER), { code: "definition-unchanged" });
assert.throws(() => f.verbs.edit("acme", { revision: old, name: "scout", definition: PLAIN }, OWNER), { code: "definition-rev-required" });
assert.throws(() => f.verbs.remove("acme", { revision: old, rev: 1, name: "scout" }, OWNER), { code: "definition-rev-conflict" });
assert.deepEqual(readFileSync(f.file), bytes);
// Edit keeps from; remove takes the entry out.
f.verbs.import("acme", { revision: rev(f), template: "coder" }, OWNER);
const from = doc(f).definitions.coder.from;
f.verbs.edit("acme", { revision: rev(f), rev: 1, name: "coder", definition: PLAIN }, OWNER);
assert.deepEqual(doc(f).definitions.coder.from, from);
const removed = f.verbs.remove("acme", { revision: rev(f), rev: 3, name: "scout" }, OWNER);
assert.equal(removed.removedRev, 3);
assert.ok(!Object.hasOwn(doc(f).definitions, "scout"));
// Each write is logged with its verb, name and rev, never role text.
const log = readFileSync(f.writer.logFile("acme"), "utf8").trim().split("\n").map(JSON.parse).filter((e) => e.state === "committed");
assert.deepEqual(log.map((e) => [e.detail.verb, e.detail.definition, e.detail.rev]), [
["definition.invent.console", "scout", 1], ["definition.edit.console", "scout", 2], ["definition.edit.console", "scout", 3],
["definition.import.console", "coder", 1], ["definition.edit.console", "coder", 2], ["definition.remove.console", "scout", null],
]);
});
test("T1 invent and customize refuse what checkComposition refuses, with no value in the error", (t) => {
const f = fixture(t);
f.verbs.invent("acme", { revision: rev(f), name: "scout", definition: PLAIN }, OWNER);
const bytes = readFileSync(f.file);
const bad = [
["held credential in instructions", { ...PLAIN, instructions: `Use ${SECRET} to log in.\n` }],
["held credential in personality", { ...PLAIN, personality: { voice: `say ${SECRET}` } }],
["unregistered tool", { ...PLAIN, role: { ...PLAIN.role, tools: [SECRET] } }],
["unregistered service", { ...PLAIN, role: { ...PLAIN.role, credentials: [{ service: SECRET }] } }],
["hire chain", { ...PLAIN, role: { ...PLAIN.role, authority: { withinRole: ["role.hire"], crossRole: [] } } }],
["over bound", { ...PLAIN, instructions: "x".repeat(16385) }],
["unknown entry key", { ...PLAIN, [SECRET]: 1 }],
["unknown role key", { ...PLAIN, role: { ...PLAIN.role, [SECRET]: [] } }],
];
for (const [what, definition] of bad) {
for (const call of [
() => f.verbs.invent("acme", { revision: rev(f), name: "other", definition }, OWNER),
() => f.verbs.edit("acme", { revision: rev(f), rev: 1, name: "scout", definition }, OWNER),
]) {
assert.throws(call, (e) => !e.message.includes(SECRET) && !e.message.includes("x".repeat(64)) && !String(e.stack).includes(SECRET), what);
}
}
assert.deepEqual(readFileSync(f.file), bytes);
// Import refuses a template that holds a hire right (R9): the chief of staff.
assert.throws(() => f.verbs.import("acme", { revision: rev(f), template: "chief-of-staff" }, OWNER), /cannot hire or fire/);
assert.deepEqual(readFileSync(f.file), bytes);
});
test("T1 a bad entry in the file refuses the whole file, naming the definition and key, never the value", (t) => {
const root = tmp();
t.after(() => rmSync(root, { recursive: true, force: true }));
const rolesDir = rolesCopy(root);
const file = join(root, "config/businesses/acme.json");
const base = businessDoc(root);
const entry = { rev: 1, ...PLAIN };
const refusal = (definitions) => {
try {
validateBusinessDocument({ ...base, definitions }, file, { rolesDir, assertClean });
} catch (e) {
assert.equal(e.name, "BusinessError");
return e.message;
}
assert.fail("expected a refusal");
};
assert.match(refusal({ scout: { ...entry, instructions: `see ${SECRET}` } }), /definitions\.scout: composition instructions holds a credential/);
assert.match(refusal({ scout: { ...entry, role: { ...PLAIN.role, tools: [SECRET] } } }), /definitions\.scout: .*tools/);
assert.match(refusal({ scout: { ...entry, rev: 0 } }), /definitions\.scout\.rev must be a positive integer/);
assert.match(refusal({ scout: { ...entry, from: { template: "coder", sha256: SECRET } } }), /definitions\.scout\.from\.sha256/);
assert.match(refusal({ scout: { ...entry, [SECRET]: true } }), /definitions\.scout has an unknown key \(key \d+;/);
assert.match(refusal({ [SECRET]: entry }), /definitions key 1 must be a definition name/);
for (const d of [{ scout: { ...entry, instructions: `see ${SECRET}` } }, { scout: { ...entry, role: { ...PLAIN.role, tools: [SECRET] } } }, { [SECRET]: entry }, { scout: { ...entry, [SECRET]: true } }]) {
assert.ok(!refusal(d).includes(SECRET));
}
// A good definition validates; the resolved map holds only what instances name.
const ok = validateBusinessDocument({ ...base, definitions: { scout: entry } }, file, { rolesDir });
assert.ok(!Object.hasOwn(ok.definitions, "org:scout"));
});
test("T1 an org: reference resolves to the definition, and a missing one refuses naming instance and definition", (t) => {
const root = tmp();
t.after(() => rmSync(root, { recursive: true, force: true }));
const rolesDir = rolesCopy(root);
const file = join(root, "config/businesses/acme.json");
const base = businessDoc(root);
const named = { ...base, definitions: { scout: { rev: 4, ...PLAIN, personality: { summary: "Terse." } } }, roles: { ...base.roles, helper: { definition: "org:scout" } } };
const business = validateBusinessDocument(named, file, { rolesDir });
const role = business.definitions["org:scout"];
assert.equal(role.name, "org:scout");
assert.equal(role.contractPath, null);
assert.equal(role.instructions, PLAIN.instructions);
assert.equal(role.rev, 4);
const resolved = resolveInstance({ system: systemFor(root), business, instance: "helper" });
assert.equal(resolved.definition, "org:scout");
assert.equal(resolved.contract, null);
assert.equal(resolved.instructions, PLAIN.instructions);
assert.deepEqual(resolved.limits.tools, ["read"]);
const missing = { ...base, roles: { ...base.roles, helper: { definition: "org:ghost" } } };
assert.throws(() => validateBusinessDocument(missing, file, { rolesDir }), (e) => /definition-missing: .*roles\.helper\.definition names org:ghost/.test(e.message));
assert.throws(() => validateBusinessDocument({ ...base, roles: { ...base.roles, helper: { definition: `org:${SECRET.toLowerCase()}!` } } }, file, { rolesDir }), (e) => !e.message.includes(SECRET.toLowerCase()));
// Template names work exactly as before, including one shaped like a definition name.
assert.throws(() => validateBusinessDocument({ ...base, roles: { ...base.roles, helper: { definition: "scout" } } }, file, { rolesDir }), { name: "BusinessError" });
});
test("T1 an org whose instances name only templates loads exactly as before", (t) => {
const root = tmp();
t.after(() => rmSync(root, { recursive: true, force: true }));
const rolesDir = rolesCopy(root);
const file = join(root, "config/businesses/acme.json");
const base = businessDoc(root);
const plain = validateBusinessDocument(base, file, { rolesDir });
const withUnused = validateBusinessDocument({ ...base, definitions: { scout: { rev: 1, ...PLAIN } } }, file, { rolesDir });
assert.deepEqual(withUnused, plain);
assert.deepEqual(Object.keys(plain).sort(), ["arbiters", "businessVersion", "definitions", "file", "human", "id", "launch", "plugins", "projects", "roles", "tracker", "vars"]);
assert.deepEqual(Object.keys(plain.definitions).sort(), ["chief-of-staff", "coder", "cto", "pm", "reviewer"]);
const system = systemFor(root);
for (const instance of Object.keys(base.roles)) {
const a = resolveInstance({ system, business: plain, instance });
const b = resolveInstance({ system, business: withUnused, instance });
assert.equal(a.digest, b.digest);
assert.ok(!Object.hasOwn(a, "instructions"));
assert.equal(a.contract, plain.definitions[base.roles[instance].definition].contractPath);
}
});
test("T1 definition-in-use: an edit or removal of a definition an instance names refuses, naming it", (t) => {
const f = fixture(t);
f.verbs.invent("acme", { revision: rev(f), name: "scout", definition: PLAIN }, OWNER);
// A human adds the instance through the ordinary writer; definitions stay as they are.
const next = doc(f);
next.roles.helper = { definition: "org:scout" };
f.writer.write("acme", next, { revision: rev(f), actor: "jason" });
const bytes = readFileSync(f.file);
assert.throws(() => f.verbs.edit("acme", { revision: rev(f), rev: 1, name: "scout", definition: { ...PLAIN, instructions: "Changed.\n" } }, OWNER),
(e) => e.code === "definition-in-use" && /named by helper/.test(e.message));
assert.throws(() => f.verbs.remove("acme", { revision: rev(f), rev: 1, name: "scout" }, OWNER),
(e) => e.code === "definition-in-use" && /named by helper/.test(e.message));
assert.deepEqual(readFileSync(f.file), bytes);
assert.deepEqual(f.verbs.list("acme").definitions, [{ name: "scout", reference: "org:scout", rev: 1, from: null, usedBy: ["helper"] }]);
assert.deepEqual(f.verbs.show("acme", "scout").usedBy, ["helper"]);
});
test("T1 the ordinary writer cannot change definitions, and setDefinition changes nothing else", (t) => {
const f = fixture(t);
f.verbs.invent("acme", { revision: rev(f), name: "scout", definition: PLAIN }, OWNER);
const next = doc(f);
next.definitions.scout.instructions = "Sneaked in.\n";
next.definitions.scout.rev = 2;
assert.throws(() => f.writer.write("acme", next, { revision: rev(f), actor: "jason" }), { code: "organization-definitions-protected" });
const added = doc(f);
added.definitions.other = { rev: 1, ...PLAIN };
assert.throws(() => f.writer.write("acme", added, { revision: rev(f), actor: "jason" }), { code: "organization-definitions-protected" });
// The writer itself checks rev and the composition, whatever builds the entry.
assert.throws(() => f.writer.setDefinition("acme", "scout", () => ({ ...PLAIN, role: { ...PLAIN.role, tools: ["no-such-tool"] } }), { revision: rev(f), actor: "jason", assertClean }), { name: "BusinessError" });
});
test("T1 an agent cannot call the edit verbs", (t) => {
const f = fixture(t);
f.verbs.invent("acme", { revision: rev(f), name: "scout", definition: PLAIN }, OWNER);
const bytes = readFileSync(f.file);
const calls = (caller) => [
() => f.verbs.import("acme", { revision: rev(f), template: "coder" }, caller),
() => f.verbs.invent("acme", { revision: rev(f), name: "other", definition: PLAIN }, caller),
() => f.verbs.edit("acme", { revision: rev(f), rev: 1, name: "scout", definition: { ...PLAIN, instructions: "x\n" } }, caller),
() => f.verbs.remove("acme", { revision: rev(f), rev: 1, name: "scout" }, caller),
];
// An agent session, even one holding every right; an agent's name or the tester as actor.
for (const caller of [
{ ...OWNER, agent: true }, { actor: "coder", rights: OWNER.rights }, { ...OWNER, actor: "coder" },
{ ...OWNER, actor: "coder.coder" }, { ...OWNER, actor: "tester" }, { ...OWNER, actor: "chief" },
]) {
for (const call of calls(caller)) assert.throws(call, { code: "human-required" });
}
// The writer refuses an agent actor by itself too.
assert.throws(() => f.writer.setDefinition("acme", "other", () => PLAIN, { revision: rev(f), actor: "pm" }), { code: "human-required" });
assert.deepEqual(readFileSync(f.file), bytes);
});
test("T1 a member without org.hire refuses on each verb; the owner and a member granted org.hire succeed", (t) => {
const f = fixture(t);
for (const verb of Object.values(DEFINITION_VERBS)) assert.equal(VERB_RIGHTS[verb], "org.hire");
f.verbs.invent("acme", { revision: rev(f), name: "scout", definition: PLAIN }, OWNER);
const bytes = readFileSync(f.file);
const refused = [
() => f.verbs.import("acme", { revision: rev(f), template: "coder" }, MEMBER),
() => f.verbs.invent("acme", { revision: rev(f), name: "other", definition: PLAIN }, MEMBER),
() => f.verbs.edit("acme", { revision: rev(f), rev: 1, name: "scout", definition: { ...PLAIN, instructions: "x\n" } }, MEMBER),
() => f.verbs.remove("acme", { revision: rev(f), rev: 1, name: "scout" }, MEMBER),
() => f.verbs.invent("acme", { revision: rev(f), name: "other", definition: PLAIN }, { actor: "alice", agent: false }),
];
for (const call of refused) assert.throws(call, (e) => e.code === "right-required" && /needs org\.hire/.test(e.message));
assert.deepEqual(readFileSync(f.file), bytes);
// Owner: every verb.
f.verbs.import("acme", { revision: rev(f), template: "coder" }, OWNER);
f.verbs.edit("acme", { revision: rev(f), rev: 1, name: "scout", definition: { ...PLAIN, instructions: "x\n" } }, OWNER);
f.verbs.remove("acme", { revision: rev(f), rev: 2, name: "scout" }, OWNER);
// A member granted org.hire: every verb, logged as that member.
f.verbs.import("acme", { revision: rev(f), template: "reviewer" }, HIRER);
f.verbs.invent("acme", { revision: rev(f), name: "scout", definition: PLAIN }, HIRER);
f.verbs.edit("acme", { revision: rev(f), rev: 1, name: "scout", definition: { ...PLAIN, instructions: "y\n" } }, HIRER);
f.verbs.remove("acme", { revision: rev(f), rev: 2, name: "scout" }, HIRER);
const log = readFileSync(f.writer.logFile("acme"), "utf8").trim().split("\n").map(JSON.parse).filter((e) => e.state === "committed");
assert.deepEqual(log.slice(-4).map((e) => e.actor), ["alice", "alice", "alice", "alice"]);
});
test("T1 the template files on disk are never written", (t) => {
const f = fixture(t);
const before = readFileSync(join(f.rolesDir, "coder.json"));
f.verbs.import("acme", { revision: rev(f), template: "coder" }, OWNER);
f.verbs.edit("acme", { revision: rev(f), rev: 1, name: "coder", definition: PLAIN }, OWNER);
assert.deepEqual(readFileSync(join(f.rolesDir, "coder.json")), before);
writeFileSync(join(f.root, "touched"), "");
});
test("T1 a write that adds or changes a definition refuses without the credential store's secret check", (t) => {
const f = fixture(t);
f.verbs.invent("acme", { revision: rev(f), name: "scout", definition: PLAIN }, OWNER);
const unchecked = definitionVerbs({ writer: f.writer, rolesDir: f.rolesDir });
const before = readFileSync(f.file);
const required = { code: "definition-secret-check-required" };
assert.throws(() => unchecked.import("acme", { revision: rev(f), template: "coder" }, OWNER), required);
assert.throws(() => unchecked.invent("acme", { revision: rev(f), name: "other", definition: PLAIN }, OWNER), required);
assert.throws(() => unchecked.edit("acme", { revision: rev(f), rev: 1, name: "scout", definition: { ...PLAIN, instructions: "Report.\n" } }, OWNER), required);
assert.throws(() => f.writer.setDefinition("acme", "other", () => PLAIN, { revision: rev(f), actor: "jason" }), required);
assert.deepEqual(readFileSync(f.file), before);
// A removal writes no role text, so it needs no secret check.
assert.equal(unchecked.remove("acme", { revision: rev(f), rev: 1, name: "scout" }, OWNER).removedRev, 1);
});
test('A4 T1 hand-off: a failed second contract read names the template, never the path', t => {
const root=tmp(),rolesDir=rolesCopy(root),read=fs.default.readFileSync;
let contracts=0;
fs.default.readFileSync=function(file,...args){
if(file===join(rolesDir,'reviewer.md') && ++contracts===2)throw new Error(`cannot read ${root}/sk-test-NOT-A-SECRET-1234`);
return read(file,...args);
};
syncBuiltinESMExports();
try { assert.throws(()=>readTemplate(rolesDir,'reviewer'),{message:'template reviewer contract is unavailable'}); }
finally { fs.default.readFileSync=read;syncBuiltinESMExports();rmSync(root,{recursive:true,force:true}); }
});