980 lines
36 KiB
JavaScript
980 lines
36 KiB
JavaScript
import assert from "node:assert/strict";
|
|
import { execFileSync } from "node:child_process";
|
|
import { mkdtemp, mkdir, readFile, rm, writeFile } from "node:fs/promises";
|
|
import { tmpdir } from "node:os";
|
|
import { dirname, join } from "node:path";
|
|
import { fileURLToPath } from "node:url";
|
|
import test from "node:test";
|
|
|
|
import { extractScriptDocuments, verifyEntries } from "./verify-workspace-descriptor-files.mjs";
|
|
|
|
const repositoryRoot = fileURLToPath(new URL("../..", import.meta.url));
|
|
const canonicalDescriptor = await readFile(join(repositoryRoot, "deploy/workspaces/example.yaml"), "utf8");
|
|
|
|
async function fixture(t) {
|
|
const root = await mkdtemp(join(tmpdir(), "thoth-workspace-yaml-verifier-"));
|
|
t.after(() => rm(root, { recursive: true, force: true }));
|
|
return root;
|
|
}
|
|
|
|
async function put(root, path, content) {
|
|
await mkdir(dirname(join(root, path)), { recursive: true });
|
|
await writeFile(join(root, path), content);
|
|
}
|
|
|
|
function entry(kind, path) {
|
|
return { kind, path };
|
|
}
|
|
|
|
function bashN(root, path) {
|
|
execFileSync("/bin/bash", ["-n", join(root, path)], { stdio: "pipe" });
|
|
}
|
|
|
|
function replaceWorkspaceKeys(source, workspaceKey, schemaLine) {
|
|
return source
|
|
.replace(/^workspace:$/m, workspaceKey)
|
|
.replace(/^ schema_version: 4$/m, schemaLine);
|
|
}
|
|
|
|
test("production parser accepts semantic v4 with quoted Unicode/tagged keys and spacing", async (t) => {
|
|
const root = await fixture(t);
|
|
const unicode = replaceWorkspaceKeys(
|
|
canonicalDescriptor,
|
|
'"\\u0077orkspace" :',
|
|
' "\\u0073chema_version" : 4',
|
|
);
|
|
const tagged = replaceWorkspaceKeys(
|
|
canonicalDescriptor,
|
|
"!!str workspace :",
|
|
" !!str schema_version : 4",
|
|
);
|
|
await put(root, "deploy/workspaces/unicode.yaml", unicode);
|
|
await put(root, "deploy/workspaces/tagged.yaml", tagged);
|
|
await verifyEntries({
|
|
root,
|
|
entries: [
|
|
entry("workspace_descriptor", "deploy/workspaces/unicode.yaml"),
|
|
entry("workspace_descriptor", "deploy/workspaces/tagged.yaml"),
|
|
],
|
|
});
|
|
});
|
|
|
|
test("production parser rejects fancy keys with every non-v4 or ambiguous value", async (t) => {
|
|
const invalid = [
|
|
["unicode-v2", '"\\u0077orkspace" :', ' "\\u0073chema_version" : 2'],
|
|
["unicode-v3", '"\\u0077orkspace" :', ' "\\u0073chema_version" : 3'],
|
|
["tagged-leading-zero", "!!str workspace :", " !!str schema_version : 03"],
|
|
["hexadecimal", "workspace :", " schema_version : 0x3"],
|
|
["multiline", "workspace :", " schema_version : >\n 4"],
|
|
["duplicate", "workspace :", " schema_version : 4\n schema_version: 4"],
|
|
["inline", "workspace: { schema_version: 4 }", " schema_version: 4"],
|
|
];
|
|
for (const [name, workspaceKey, schemaLine] of invalid) {
|
|
await t.test(name, async () => {
|
|
const root = await mkdtemp(join(tmpdir(), `thoth-workspace-yaml-${name}-`));
|
|
try {
|
|
const source = replaceWorkspaceKeys(canonicalDescriptor, workspaceKey, schemaLine);
|
|
const path = `deploy/workspaces/${name}.yaml`;
|
|
await put(root, path, source);
|
|
await assert.rejects(
|
|
verifyEntries({ root, entries: [entry("workspace_descriptor", path)] }),
|
|
/workspace descriptor/i,
|
|
);
|
|
} finally {
|
|
await rm(root, { recursive: true, force: true });
|
|
}
|
|
});
|
|
}
|
|
});
|
|
|
|
test("Bash embedded workspace mappings are rejected while tracked-fixture-only bundles pass", async (t) => {
|
|
const root = await fixture(t);
|
|
const validScript = [
|
|
"#!/usr/bin/env bash",
|
|
"cat <<'WORKSPACE_YAML'",
|
|
canonicalDescriptor.trimEnd(),
|
|
"WORKSPACE_YAML",
|
|
"cat <<'BUNDLE_YAML'",
|
|
"bundle:",
|
|
" name: deploy",
|
|
"schema_version: 1",
|
|
"job:",
|
|
" state: operational",
|
|
"BUNDLE_YAML",
|
|
"",
|
|
].join("\n");
|
|
await put(root, "scripts/operator-smoke.sh", validScript);
|
|
await assert.rejects(
|
|
verifyEntries({ root, entries: [entry("deployment_script", "scripts/operator-smoke.sh")] }),
|
|
/embedded workspace descriptor/i,
|
|
);
|
|
|
|
const bundleScript = validScript.replace(canonicalDescriptor.trimEnd(), "job:\n name: deploy");
|
|
await put(root, "scripts/operator-smoke.sh", bundleScript);
|
|
await verifyEntries({
|
|
root,
|
|
entries: [entry("deployment_script", "scripts/operator-smoke.sh")],
|
|
});
|
|
});
|
|
|
|
test("PowerShell embedded workspace mappings are rejected while bundle-only strings pass", async (t) => {
|
|
const root = await fixture(t);
|
|
const source = [
|
|
"$workspace = @'",
|
|
canonicalDescriptor.replace(" schema_version: 4", " schema_version: 0x2").trimEnd(),
|
|
"'@",
|
|
'$bundle = @"',
|
|
"bundle:",
|
|
" schema_version: 1",
|
|
'"@',
|
|
"",
|
|
].join("\n");
|
|
await put(root, "scripts/operator.ps1", source);
|
|
await assert.rejects(
|
|
verifyEntries({ root, entries: [entry("deployment_script", "scripts/operator.ps1")] }),
|
|
/workspace descriptor/i,
|
|
);
|
|
});
|
|
|
|
test("workspace descriptor family entries require a top-level workspace", async (t) => {
|
|
const root = await fixture(t);
|
|
await put(root, "scripts/fixtures/workspace-registry-future.yaml", "bundle:\n schema_version: 4\n");
|
|
await assert.rejects(
|
|
verifyEntries({
|
|
root,
|
|
entries: [entry("workspace_descriptor", "scripts/fixtures/workspace-registry-future.yaml")],
|
|
}),
|
|
/top-level workspace/i,
|
|
);
|
|
});
|
|
|
|
|
|
test("script scalar workspace remains a bundle even with descriptor-like siblings", async (t) => {
|
|
const root = await fixture(t);
|
|
const path = "scripts/job-smoke.sh";
|
|
const job = [
|
|
"#!/usr/bin/env bash",
|
|
"cat <<'JOB-YAML'",
|
|
"job: refresh",
|
|
"workspace: analytics",
|
|
"schema_version: 2",
|
|
"state: operational",
|
|
"JOB-YAML",
|
|
"",
|
|
].join("\n");
|
|
await put(root, path, job);
|
|
bashN(root, path);
|
|
await verifyEntries({ root, entries: [entry("deployment_script", path)] });
|
|
|
|
const bundles = [
|
|
job.replace("job: refresh", "dwh:\n engine: postgres"),
|
|
job.replace("job: refresh", "evidence:\n source: bundle"),
|
|
];
|
|
for (const bundle of bundles) {
|
|
await put(root, path, bundle);
|
|
bashN(root, path);
|
|
await verifyEntries({ root, entries: [entry("deployment_script", path)] });
|
|
}
|
|
});
|
|
|
|
test("standalone descriptor files require workspace to be a mapping", async (t) => {
|
|
const root = await fixture(t);
|
|
const path = "scripts/fixtures/workspace-registry-scalar.yaml";
|
|
await put(root, path, "workspace: analytics\nschema_version: 4\n");
|
|
await assert.rejects(
|
|
verifyEntries({ root, entries: [entry("workspace_descriptor", path)] }),
|
|
/workspace.*mapping/i,
|
|
);
|
|
});
|
|
|
|
test("Bash extractor supports hyphen, digit, escaped delimiters, and tab stripping", async (t) => {
|
|
const root = await fixture(t);
|
|
const cases = [
|
|
{
|
|
name: "hyphen-v2",
|
|
opener: "cat <<'WORKSPACE-YAML'",
|
|
delimiter: "WORKSPACE-YAML",
|
|
descriptor: canonicalDescriptor.replace(" schema_version: 4", " schema_version: 2"),
|
|
rejected: true,
|
|
},
|
|
{
|
|
name: "digit-v4",
|
|
opener: "cat <<2YAML",
|
|
delimiter: "2YAML",
|
|
descriptor: canonicalDescriptor,
|
|
rejected: true,
|
|
},
|
|
{
|
|
name: "escaped-v2",
|
|
opener: "cat <<WORKSPACE\\-YAML",
|
|
delimiter: "WORKSPACE-YAML",
|
|
descriptor: canonicalDescriptor.replace(" schema_version: 4", " schema_version: 2"),
|
|
rejected: true,
|
|
},
|
|
{
|
|
name: "tab-strip-v4",
|
|
opener: "cat <<-'TAB-YAML'",
|
|
delimiter: "\tTAB-YAML",
|
|
descriptor: canonicalDescriptor.split("\n").map((line) => `\t${line}`).join("\n"),
|
|
rejected: true,
|
|
},
|
|
];
|
|
for (const item of cases) {
|
|
await t.test(item.name, async () => {
|
|
const path = `scripts/${item.name}-smoke.sh`;
|
|
const source = ["#!/usr/bin/env bash", item.opener, item.descriptor.trimEnd(), item.delimiter, ""].join("\n");
|
|
await put(root, path, source);
|
|
bashN(root, path);
|
|
const verification = verifyEntries({ root, entries: [entry("deployment_script", path)] });
|
|
if (item.rejected) await assert.rejects(verification, /workspace descriptor/i);
|
|
else await verification;
|
|
});
|
|
}
|
|
});
|
|
|
|
test("unsupported Bash heredoc opener fails closed while a bundle heredoc stays allowed", async (t) => {
|
|
const root = await fixture(t);
|
|
const unsupportedPath = "scripts/unsupported-smoke.sh";
|
|
const unsupported = [
|
|
"#!/usr/bin/env bash",
|
|
"cat <<$DELIMITER",
|
|
canonicalDescriptor.trimEnd(),
|
|
"$DELIMITER",
|
|
"",
|
|
].join("\n");
|
|
await put(root, unsupportedPath, unsupported);
|
|
bashN(root, unsupportedPath);
|
|
await assert.rejects(
|
|
verifyEntries({ root, entries: [entry("deployment_script", unsupportedPath)] }),
|
|
/unsupported Bash heredoc opener/i,
|
|
);
|
|
|
|
const bundlePath = "scripts/bundle-smoke.sh";
|
|
const bundle = [
|
|
"#!/usr/bin/env bash",
|
|
"cat <<'BUNDLE-YAML'",
|
|
"job: refresh",
|
|
"workspace: analytics",
|
|
"schema_version: 1",
|
|
"state: operational",
|
|
"BUNDLE-YAML",
|
|
"",
|
|
].join("\n");
|
|
await put(root, bundlePath, bundle);
|
|
bashN(root, bundlePath);
|
|
await verifyEntries({ root, entries: [entry("deployment_script", bundlePath)] });
|
|
});
|
|
|
|
|
|
test("non-stripping heredoc close requires an exact physical delimiter line", async (t) => {
|
|
const root = await fixture(t);
|
|
const path = "scripts/trailing-close-smoke.sh";
|
|
const source = [
|
|
"#!/usr/bin/env bash",
|
|
"cat <<'---'",
|
|
"--- ",
|
|
canonicalDescriptor.replace(" schema_version: 4", " schema_version: 2").trimEnd(),
|
|
"---",
|
|
"",
|
|
].join("\n");
|
|
await put(root, path, source);
|
|
bashN(root, path);
|
|
await assert.rejects(
|
|
verifyEntries({ root, entries: [entry("deployment_script", path)] }),
|
|
/workspace descriptor/i,
|
|
);
|
|
});
|
|
|
|
test("delimiter-like body lines remain content until a real exact close", async (t) => {
|
|
const root = await fixture(t);
|
|
const path = "scripts/delimiter-content-smoke.sh";
|
|
const source = [
|
|
"#!/usr/bin/env bash",
|
|
"cat <<'END'",
|
|
"END ",
|
|
" END",
|
|
"job: refresh",
|
|
"workspace: analytics",
|
|
"schema_version: 1",
|
|
"END",
|
|
"",
|
|
].join("\n");
|
|
await put(root, path, source);
|
|
bashN(root, path);
|
|
const [candidate] = extractScriptDocuments(source, path);
|
|
assert.match(candidate.source, /^END \n END\n/u);
|
|
await verifyEntries({ root, entries: [entry("deployment_script", path)] });
|
|
});
|
|
|
|
|
|
test("double-quoted non-special backslash is preserved in the delimiter", async (t) => {
|
|
const root = await fixture(t);
|
|
const path = "scripts/double-quoted-nonspecial-smoke.sh";
|
|
const source = [
|
|
"#!/usr/bin/env bash",
|
|
'cat <<"\\---"',
|
|
"---",
|
|
canonicalDescriptor.replace(" schema_version: 4", " schema_version: 2").trimEnd(),
|
|
"\\---",
|
|
"",
|
|
].join("\n");
|
|
await put(root, path, source);
|
|
bashN(root, path);
|
|
assert.match(execFileSync("/bin/bash", [join(root, path)], { encoding: "utf8" }), /schema_version: 2/u);
|
|
await assert.rejects(
|
|
verifyEntries({ root, entries: [entry("deployment_script", path)] }),
|
|
/workspace descriptor/i,
|
|
);
|
|
});
|
|
|
|
test("double-quoted delimiter quote removal matches Bash special escapes", async (t) => {
|
|
const root = await fixture(t);
|
|
const cases = [
|
|
["dollar", 'cat <<"DOL\\$LAR"', "DOL$LAR"],
|
|
["backtick", 'cat <<"TIC\\`K"', "TIC`K"],
|
|
["quote", 'cat <<"QUO\\\"TE"', 'QUO"TE'],
|
|
["backslash", 'cat <<"SLA\\\\SH"', "SLA\\SH"],
|
|
["newline", 'cat <<"LINE\\\nBREAK"', "LINEBREAK"],
|
|
["nonspecial", 'cat <<"NON\\-SPECIAL"', "NON\\-SPECIAL"],
|
|
];
|
|
for (const [name, opener, close] of cases) {
|
|
const path = `scripts/double-quoted-${name}-smoke.sh`;
|
|
const source = ["#!/usr/bin/env bash", opener, "job: refresh", close, ""].join("\n");
|
|
await put(root, path, source);
|
|
bashN(root, path);
|
|
assert.equal(execFileSync("/bin/bash", [join(root, path)], { encoding: "utf8" }), "job: refresh\n");
|
|
assert.equal(extractScriptDocuments(source, path)[0].source, "job: refresh\n");
|
|
await verifyEntries({ root, entries: [entry("deployment_script", path)] });
|
|
}
|
|
});
|
|
|
|
|
|
test("split heredoc operator continuation cannot bypass v2 validation", async (t) => {
|
|
const root = await fixture(t);
|
|
const path = "scripts/split-operator-smoke.sh";
|
|
const source = [
|
|
"#!/usr/bin/env bash",
|
|
"cat <\\",
|
|
"<'YAML'",
|
|
canonicalDescriptor.replace(" schema_version: 4", " schema_version: 2").trimEnd(),
|
|
"YAML",
|
|
"",
|
|
].join("\n");
|
|
await put(root, path, source);
|
|
bashN(root, path);
|
|
assert.match(execFileSync("/bin/bash", [join(root, path)], { encoding: "utf8" }), /schema_version: 2/u);
|
|
await assert.rejects(
|
|
verifyEntries({ root, entries: [entry("deployment_script", path)] }),
|
|
/workspace descriptor/i,
|
|
);
|
|
});
|
|
|
|
test("multiple opener continuations are joined before heredoc discovery", async (t) => {
|
|
const root = await fixture(t);
|
|
const path = "scripts/multiple-continuation-smoke.sh";
|
|
const source = [
|
|
"#!/usr/bin/env bash",
|
|
"cat \\",
|
|
"<\\",
|
|
"<'YAML'",
|
|
"job: refresh",
|
|
"workspace: analytics",
|
|
"YAML",
|
|
"",
|
|
].join("\n");
|
|
await put(root, path, source);
|
|
bashN(root, path);
|
|
assert.equal(
|
|
execFileSync("/bin/bash", [join(root, path)], { encoding: "utf8" }),
|
|
"job: refresh\nworkspace: analytics\n",
|
|
);
|
|
const [candidate] = extractScriptDocuments(source, path);
|
|
assert.equal(candidate.label, `${path}:5 Bash heredoc`);
|
|
assert.equal(candidate.source, "job: refresh\nworkspace: analytics\n");
|
|
await verifyEntries({ root, entries: [entry("deployment_script", path)] });
|
|
});
|
|
|
|
test("backslash-newline inside single quotes is not removed", async (t) => {
|
|
const root = await fixture(t);
|
|
const path = "scripts/single-quoted-noncontinuation-smoke.sh";
|
|
const source = [
|
|
"#!/usr/bin/env bash",
|
|
"printf '%s' 'literal\\",
|
|
"continued'",
|
|
"cat <<'YAML'",
|
|
"job: refresh",
|
|
"workspace: analytics",
|
|
"YAML",
|
|
"",
|
|
].join("\n");
|
|
await put(root, path, source);
|
|
bashN(root, path);
|
|
assert.equal(
|
|
execFileSync("/bin/bash", [join(root, path)], { encoding: "utf8" }),
|
|
"literal\\\ncontinuedjob: refresh\nworkspace: analytics\n",
|
|
);
|
|
const [candidate] = extractScriptDocuments(source, path);
|
|
assert.equal(candidate.label, `${path}:5 Bash heredoc`);
|
|
await verifyEntries({ root, entries: [entry("deployment_script", path)] });
|
|
});
|
|
|
|
|
|
test("PowerShell comment backslash cannot hide a following v2 here-string", async (t) => {
|
|
const root = await fixture(t);
|
|
const path = "scripts/powershell-comment-smoke.ps1";
|
|
const source = [
|
|
"# harmless PowerShell comment \\",
|
|
"$workspace = @'",
|
|
canonicalDescriptor.replace(" schema_version: 4", " schema_version: 2").trimEnd(),
|
|
"'@",
|
|
"",
|
|
].join("\n");
|
|
await put(root, path, source);
|
|
await assert.rejects(
|
|
verifyEntries({ root, entries: [entry("deployment_script", path)] }),
|
|
/workspace descriptor/i,
|
|
);
|
|
});
|
|
|
|
test("PowerShell dialect accepts normal v4 and non-workspace bundle here-strings", async (t) => {
|
|
const root = await fixture(t);
|
|
const path = "scripts/powershell-valid-smoke.ps1";
|
|
const source = [
|
|
"$workspace = @'",
|
|
canonicalDescriptor.trimEnd(),
|
|
"'@",
|
|
"$bundle = @'",
|
|
"evidence:",
|
|
" source: bundle",
|
|
"schema_version: 2",
|
|
"'@",
|
|
"",
|
|
].join("\n");
|
|
await put(root, path, source);
|
|
await assert.rejects(
|
|
verifyEntries({ root, entries: [entry("deployment_script", path)] }),
|
|
/embedded workspace descriptor/i,
|
|
);
|
|
|
|
const bundleOnly = [
|
|
"$bundle = @'",
|
|
"evidence:",
|
|
" source: bundle",
|
|
"schema_version: 2",
|
|
"'@",
|
|
"",
|
|
].join("\n");
|
|
await put(root, path, bundleOnly);
|
|
await verifyEntries({ root, entries: [entry("deployment_script", path)] });
|
|
});
|
|
|
|
test("unknown deployment script dialect fails closed", async (t) => {
|
|
const root = await fixture(t);
|
|
const path = "scripts/operator-smoke.cmd";
|
|
await put(root, path, "echo harmless\n");
|
|
await assert.rejects(
|
|
verifyEntries({ root, entries: [entry("deployment_script", path)] }),
|
|
/unknown deployment script dialect/i,
|
|
);
|
|
});
|
|
|
|
|
|
test("PowerShell cast and concatenation openers cannot hide embedded descriptors", async (t) => {
|
|
const root = await fixture(t);
|
|
for (const [name, opener] of [["cast", "[string]@'"], ["concat", "+@'"]]) {
|
|
const path = `scripts/powershell-${name}-smoke.ps1`;
|
|
const source = [opener, canonicalDescriptor.trimEnd(), "'@", ""].join("\n");
|
|
await put(root, path, source);
|
|
await assert.rejects(
|
|
verifyEntries({ root, entries: [entry("deployment_script", path)] }),
|
|
/embedded workspace descriptor/i,
|
|
);
|
|
}
|
|
});
|
|
|
|
test("expandable YAML interpolation that can hide a workspace descriptor fails closed", async (t) => {
|
|
const root = await fixture(t);
|
|
const cases = [
|
|
["braced-key", "${key}:\n schema_version: 4"],
|
|
["plain-key", "$key:\n schema_version: 4"],
|
|
["quoted-key", '"$key" :\n schema_version: 4'],
|
|
["subexpression-key", "$($key):\n schema_version: 4"],
|
|
["version", "workspace:\n schema_version: $version"],
|
|
];
|
|
for (const [name, body] of cases) {
|
|
const path = `scripts/powershell-interpolation-${name}.ps1`;
|
|
await put(root, path, [`$yaml = @\"`, body, `\"@`, ""].join("\n"));
|
|
await assert.rejects(
|
|
verifyEntries({ root, entries: [entry("deployment_script", path)] }),
|
|
/interpolation|embedded workspace descriptor/i,
|
|
);
|
|
}
|
|
});
|
|
|
|
test("Bash heredoc discovery ignores quoted, comment, here-string, and arithmetic tokens", async (t) => {
|
|
const root = await fixture(t);
|
|
const path = "scripts/bash-lexer-smoke.sh";
|
|
const source = [
|
|
"#!/usr/bin/env bash",
|
|
`printf '%s\\n' \"cat <<'QUOTED'\"`,
|
|
`printf '%s\\n' 'cat <<\"SINGLE\"'`,
|
|
"# cat <<'COMMENT'",
|
|
"value=$((1 << 2))",
|
|
`cat <<< \"not a heredoc\"`,
|
|
"cat <<'YAML'",
|
|
"job: refresh",
|
|
"YAML",
|
|
"",
|
|
].join("\n");
|
|
await put(root, path, source);
|
|
bashN(root, path);
|
|
const extracted = extractScriptDocuments(source, path);
|
|
assert.equal(extracted.length, 1);
|
|
assert.equal(extracted[0].source, "job: refresh\n");
|
|
await verifyEntries({ root, entries: [entry("deployment_script", path)] });
|
|
});
|
|
|
|
test("UTF-8 decoding is fatal but literal replacement characters are valid text", async (t) => {
|
|
const root = await fixture(t);
|
|
const validPath = "deploy/workspaces/replacement.yaml";
|
|
await put(root, validPath, `${canonicalDescriptor}# literal replacement: �\n`);
|
|
await verifyEntries({ root, entries: [entry("workspace_descriptor", validPath)] });
|
|
|
|
const invalidPath = "deploy/workspaces/malformed.yaml";
|
|
await mkdir(dirname(join(root, invalidPath)), { recursive: true });
|
|
await writeFile(join(root, invalidPath), Buffer.concat([Buffer.from(canonicalDescriptor), Buffer.from([0xff])]));
|
|
await assert.rejects(
|
|
verifyEntries({ root, entries: [entry("workspace_descriptor", invalidPath)] }),
|
|
/valid UTF-8/i,
|
|
);
|
|
});
|
|
|
|
|
|
test("unmarked expandable Bash YAML cannot generate descriptor keys or values at runtime", async (t) => {
|
|
const root = await fixture(t);
|
|
const cases = [
|
|
["quoted", '"$key" :'],
|
|
["command", "$(printf workspace):"],
|
|
["braced", "${key}:"],
|
|
["plain", "$key:"],
|
|
];
|
|
for (const [name, generatedKey] of cases) {
|
|
const path = `scripts/bash-dynamic-${name}.sh`;
|
|
const source = [
|
|
"#!/usr/bin/env bash",
|
|
"key=workspace",
|
|
"cat <<YAML",
|
|
generatedKey,
|
|
" schema_version: 4",
|
|
"YAML",
|
|
"",
|
|
].join("\n");
|
|
await put(root, path, source);
|
|
bashN(root, path);
|
|
assert.match(execFileSync("/bin/bash", [join(root, path)], { encoding: "utf8" }), /workspace/u);
|
|
await assert.rejects(
|
|
verifyEntries({ root, entries: [entry("deployment_script", path)] }),
|
|
/exact-content reviewed allowlist/i,
|
|
);
|
|
}
|
|
|
|
const valuePath = "scripts/bash-dynamic-value.sh";
|
|
const valueSource = [
|
|
"#!/usr/bin/env bash",
|
|
"version=3",
|
|
"cat <<YAML",
|
|
"workspace:",
|
|
" schema_version: $version",
|
|
"YAML",
|
|
"",
|
|
].join("\n");
|
|
await put(root, valuePath, valueSource);
|
|
bashN(root, valuePath);
|
|
await assert.rejects(
|
|
verifyEntries({ root, entries: [entry("deployment_script", valuePath)] }),
|
|
/exact-content reviewed allowlist/i,
|
|
);
|
|
});
|
|
|
|
test("an in-band marker cannot authorize expandable content", async (t) => {
|
|
const root = await fixture(t);
|
|
for (const [path, source] of [
|
|
["scripts/fake-marker.sh", [
|
|
"#!/usr/bin/env bash",
|
|
"# schema-v4-only: expandable-nonworkspace",
|
|
"cat <<YAML",
|
|
"${DESCRIPTOR}",
|
|
"YAML",
|
|
"",
|
|
].join("\n")],
|
|
["scripts/fake-marker.ps1", [
|
|
"# schema-v4-only: expandable-nonworkspace",
|
|
'$yaml = @"',
|
|
"$descriptor",
|
|
'"@',
|
|
"",
|
|
].join("\n")],
|
|
]) {
|
|
await put(root, path, source);
|
|
await assert.rejects(
|
|
verifyEntries({ root, entries: [entry("deployment_script", path)] }),
|
|
/exact-content reviewed allowlist/,
|
|
);
|
|
}
|
|
});
|
|
|
|
test("current exact reviewed expandable blocks pass only at their trusted paths", async (t) => {
|
|
const reviewedPaths = [
|
|
"scripts/test-server-pi-state-topology.sh",
|
|
"scripts/test-vector-backup-restore-safety.sh",
|
|
"scripts/test-windows-clone-contract.ps1",
|
|
"scripts/unified-deployment-smoke.sh",
|
|
"scripts/vector-backup.sh",
|
|
"scripts/vector-restore.sh",
|
|
];
|
|
await verifyEntries({
|
|
root: repositoryRoot,
|
|
entries: reviewedPaths.map((path) => entry("deployment_script", path)),
|
|
});
|
|
});
|
|
|
|
test("PowerShell tokenizer ignores opener text in comments and ordinary strings", async (t) => {
|
|
const root = await fixture(t);
|
|
const path = "scripts/powershell-lexical-context.ps1";
|
|
const source = [
|
|
"# example @'",
|
|
'\"example @\'\"',
|
|
"'example @\"'",
|
|
"<# block @'",
|
|
"still @\" #>",
|
|
"$cast = [string]@'",
|
|
"job: cast",
|
|
"'@",
|
|
"$concat = $cast +@'",
|
|
"job: concat",
|
|
"'@",
|
|
"",
|
|
].join("\n");
|
|
await put(root, path, source);
|
|
const extracted = extractScriptDocuments(source, path);
|
|
assert.equal(extracted.length, 2);
|
|
assert.deepEqual(extracted.map((item) => item.source), ["job: cast\n", "job: concat\n"]);
|
|
await verifyEntries({ root, entries: [entry("deployment_script", path)] });
|
|
});
|
|
|
|
|
|
test("policy text rejects NUL and prescribed symbol substrings but permits lower-camel legacy identifiers", async (t) => {
|
|
const root = await fixture(t);
|
|
await put(root, "backend/src/nul.ts", Buffer.from("safe\0WorkspaceV2"));
|
|
await assert.rejects(verifyEntries({ root, entries: [entry("policy_text", "backend/src/nul.ts")] }), /NUL byte/);
|
|
|
|
for (const [name, text] of [
|
|
["compat", "type X = WorkspaceV2Compat;"],
|
|
["mixed-prescribed", "type X = wOrKsPaCeV2;"],
|
|
["lower-deprecated", "type X = deprecatedV2Descriptor;"],
|
|
["upper-function", "WRITEMIGRATEDWORKSPACE(value);"],
|
|
["adapter", "type X = LegacyWorkspaceAdapter;"],
|
|
["lower", "type X = legacyworkspace;"],
|
|
["mixed", "type X = LeGaCyWoRkSpAcE;"],
|
|
]) {
|
|
const path = `backend/src/${name}.ts`;
|
|
await put(root, path, text);
|
|
await assert.rejects(verifyEntries({ root, entries: [entry("policy_text", path)] }), /forbidden/);
|
|
}
|
|
await put(root, "backend/src/allowed.ts", "const legacyWorkspacePath = value;");
|
|
await verifyEntries({ root, entries: [entry("policy_text", "backend/src/allowed.ts")] });
|
|
});
|
|
|
|
test("revision-state structural scan permits only the exact historical decoder occurrence", async (t) => {
|
|
const root = await fixture(t);
|
|
const registry = "backend/src/workspaces/registry.ts";
|
|
await put(root, registry, 'if (revision.state !== "operational") return;\n');
|
|
await verifyEntries({ root, entries: [entry("policy_text", registry)] });
|
|
|
|
const variants = [
|
|
'if (revision.state !== "operational") return;\nif (revision["state"] === value) return;\n',
|
|
'if (workspaceRevision\n .state === value) return;\n',
|
|
"if (selectedWorkspace [ 'state' ] === value) return;\n",
|
|
];
|
|
for (let index = 0; index < variants.length; index += 1) {
|
|
const path = index === 0 ? registry : `frontend/src/revision-${index}.ts`;
|
|
await put(root, path, variants[index]);
|
|
await assert.rejects(verifyEntries({ root, entries: [entry("policy_text", path)] }), /revision-state/);
|
|
}
|
|
});
|
|
|
|
|
|
test("complete descriptors supplied only through Bash or PowerShell variables require exact review", async (t) => {
|
|
const root = await fixture(t);
|
|
const cases = [
|
|
["scripts/variable-descriptor.sh", ["#!/usr/bin/env bash", "cat <<YAML", "${DESCRIPTOR}", "YAML", ""].join("\n")],
|
|
["scripts/variable-descriptor.ps1", ['$yaml = @"', "$descriptor", '"@', ""].join("\n")],
|
|
];
|
|
for (const [path, source] of cases) {
|
|
await put(root, path, source);
|
|
await assert.rejects(
|
|
verifyEntries({ root, entries: [entry("deployment_script", path)] }),
|
|
/exact-content reviewed allowlist/,
|
|
);
|
|
}
|
|
});
|
|
|
|
|
|
test("all Bash and PowerShell positional or special dollar expansions fail without exact review", async (t) => {
|
|
const root = await fixture(t);
|
|
const cases = [
|
|
["scripts/positional.sh", "cat <<YAML\n$1\nYAML\n"],
|
|
["scripts/all-args.sh", "cat <<YAML\n$@\nYAML\n"],
|
|
["scripts/positional.ps1", '$yaml = @"\n$1\n"@\n'],
|
|
];
|
|
for (const [path, source] of cases) {
|
|
await put(root, path, source);
|
|
await assert.rejects(
|
|
verifyEntries({ root, entries: [entry("deployment_script", path)] }),
|
|
/exact-content reviewed allowlist/,
|
|
);
|
|
}
|
|
});
|
|
|
|
test("PowerShell backtick escapes hash and quote tokens without hiding a later real here-string", async (t) => {
|
|
const root = await fixture(t);
|
|
for (const [name, prefix] of [
|
|
["escaped-hash", "Write-Output `# harmless"],
|
|
["escaped-quote", 'Write-Output `" harmless'],
|
|
]) {
|
|
const path = `scripts/${name}.ps1`;
|
|
const source = [prefix, "$yaml = @'", "workspace:", " schema_version: 2", "'@", ""].join("\n");
|
|
await put(root, path, source);
|
|
assert.equal(extractScriptDocuments(source, path).length, 1);
|
|
await assert.rejects(
|
|
verifyEntries({ root, entries: [entry("deployment_script", path)] }),
|
|
/embedded workspace descriptor/,
|
|
);
|
|
}
|
|
});
|
|
|
|
test("TypeScript AST rejects comment-separated and destructured revision state", async (t) => {
|
|
const root = await fixture(t);
|
|
for (const [index, source] of [
|
|
"const value = revision /*legacy*/ . state;",
|
|
"const { state } = revision;",
|
|
"const { state: oldState } = selectedWorkspace;",
|
|
].entries()) {
|
|
const path = `frontend/src/ast-revision-${index}.ts`;
|
|
await put(root, path, source);
|
|
await assert.rejects(verifyEntries({ root, entries: [entry("policy_text", path)] }), /revision-state/);
|
|
}
|
|
const registry = "backend/src/workspaces/registry.ts";
|
|
await put(root, registry, 'if (revision.state !== "operational") return;\nconst { state } = revision;\n');
|
|
await assert.rejects(verifyEntries({ root, entries: [entry("policy_text", registry)] }), /revision-state/);
|
|
await put(root, "backend/src/unrelated.ts", "const { state } = lease; const jobState = job.state;");
|
|
await verifyEntries({ root, entries: [entry("policy_text", "backend/src/unrelated.ts")] });
|
|
});
|
|
|
|
|
|
test("AST recognizes semantic state keys in every revision destructuring form", async (t) => {
|
|
const root = await fixture(t);
|
|
const cases = [
|
|
["backend/src/computed.mts", 'const { ["state"]: oldState } = revision;'],
|
|
["frontend/src/renamed.cts", 'const { "state": oldState = fallback } = workspaceRevision;'],
|
|
["backend/scripts/template.TS", 'const { [`state`]: oldState } = selectedWorkspace;'],
|
|
["scripts/parameter.txt", 'function read({ state: oldState = fallback } = revision) {}'],
|
|
["scripts/assignment.sh", '({ state } = workspaceRevision);'],
|
|
["scripts/computed-assignment.data", '({ ["state"]: oldState = fallback } = selectedWorkspace);'],
|
|
];
|
|
for (const [path, source] of cases) {
|
|
await put(root, path, source);
|
|
await assert.rejects(
|
|
verifyEntries({ root, entries: [entry("policy_text", path)] }),
|
|
/revision-state/,
|
|
path,
|
|
);
|
|
}
|
|
|
|
const registry = "backend/src/workspaces/registry.ts";
|
|
await put(root, registry, [
|
|
'if (revision.state !== "operational") return;',
|
|
'function read({ ["state"]: oldState } = revision) {}',
|
|
"",
|
|
].join("\n"));
|
|
await assert.rejects(
|
|
verifyEntries({ root, entries: [entry("policy_text", registry)] }),
|
|
/revision-state/,
|
|
);
|
|
});
|
|
|
|
test("tolerant all-suffix AST scan ignores strings/comments and unrelated state", async (t) => {
|
|
const root = await fixture(t);
|
|
const path = "scripts/arbitrary.weird";
|
|
await put(root, path, [
|
|
'// const { state } = revision;',
|
|
'"revision.state";',
|
|
"'({ [\\\"state\\\"]: oldState } = selectedWorkspace)';",
|
|
"const { state } = lease;",
|
|
"const jobState = job.state;",
|
|
"record.state = 'ready';",
|
|
"",
|
|
].join("\n"));
|
|
await verifyEntries({ root, entries: [entry("policy_text", path)] });
|
|
});
|
|
|
|
|
|
test("computed revision destructuring keys fold parentheses assertions templates and string concatenation", async (t) => {
|
|
const root = await fixture(t);
|
|
const cases = [
|
|
["backend/src/paren.ts", 'const { [("state")]: oldState } = revision;'],
|
|
["backend/src/concat.ts", 'const { ["st" + "ate"]: oldState } = workspaceRevision;'],
|
|
["frontend/src/template.ts", 'const { [`st${"ate"}`]: oldState } = selectedWorkspace;'],
|
|
["scripts/assertion.data", 'const { [("st" as string) + (`ate` satisfies string)]: oldState } = revision;'],
|
|
["scripts/assignment.txt", '({ ["st" + "ate"]: oldState } = selectedWorkspace);'],
|
|
];
|
|
for (const [path, source] of cases) {
|
|
await put(root, path, source);
|
|
await assert.rejects(verifyEntries({ root, entries: [entry("policy_text", path)] }), /revision-state/, path);
|
|
}
|
|
|
|
const registry = "backend/src/workspaces/registry.ts";
|
|
for (const injected of [
|
|
'const { [("state")]: oldState } = revision;',
|
|
'({ ["st" + "ate"]: oldState } = revision);',
|
|
]) {
|
|
await put(root, registry, `if (revision.state !== "operational") return;\n${injected}\n`);
|
|
await assert.rejects(verifyEntries({ root, entries: [entry("policy_text", registry)] }), /revision-state/);
|
|
}
|
|
});
|
|
|
|
test("polyglot masking and JSX syntax prevent comment and string false positives", async (t) => {
|
|
const root = await fixture(t);
|
|
const passing = [
|
|
["backend/scripts/comment.py", '# revision.state\nvalue = "revision.state"\ntext = """selectedWorkspace.state"""\n'],
|
|
["scripts/comment.ps1", '# revision.state\n<# workspaceRevision.state #>\n$value = "revision.state"\n'],
|
|
["scripts/comment.sh", '# revision.state\nprintf \'%s\\n\' "selectedWorkspace.state"\n'],
|
|
["frontend/src/content.tsx", 'export const view = <div>revision.state</div>;'],
|
|
["frontend/src/attribute.tsx", 'export const view = <div title="revision.state" />;'],
|
|
["frontend/src/expression.tsx", 'export const view = <div>{"revision.state"}</div>;'],
|
|
["scripts/arbitrary.data", 'title: "revision.state"\n# const { state } = revision\nlease:\n state: ready\n'],
|
|
];
|
|
for (const [path, source] of passing) {
|
|
await put(root, path, source);
|
|
await verifyEntries({ root, entries: [entry("policy_text", path)] });
|
|
}
|
|
|
|
for (const [path, source] of [
|
|
["scripts/code.txt", "const { state } = revision;"],
|
|
["scripts/code.data", '({ ["st" + "ate"]: oldState } = workspaceRevision);'],
|
|
]) {
|
|
await put(root, path, source);
|
|
await assert.rejects(verifyEntries({ root, entries: [entry("policy_text", path)] }), /revision-state/);
|
|
}
|
|
});
|
|
|
|
|
|
test("rest bindings and dynamic computed keys are not semantic state-property access", async (t) => {
|
|
const root = await fixture(t);
|
|
const cases = [
|
|
["backend/src/rest.ts", "const { ...state } = revision;"],
|
|
["frontend/src/renamed.ts", "const { other: state } = workspaceRevision;"],
|
|
["scripts/dynamic.txt", "const { [state]: value } = selectedWorkspace;"],
|
|
["scripts/dynamic-assignment.data", "({ [state]: value } = revision);"],
|
|
["scripts/spread-assignment.data", "({ ...state } = workspaceRevision);"],
|
|
];
|
|
for (const [path, source] of cases) {
|
|
await put(root, path, source);
|
|
await verifyEntries({ root, entries: [entry("policy_text", path)] });
|
|
}
|
|
});
|
|
|
|
test("polyglot code remains structural across shell Python PowerShell YAML TSX and JSX", async (t) => {
|
|
const root = await fixture(t);
|
|
const failing = [
|
|
["scripts/code.sh", "value=revision.state\n"],
|
|
["scripts/code.ps1", "$value = workspaceRevision.state\n"],
|
|
["backend/scripts/code.py", "value = selectedWorkspace.state\n"],
|
|
["scripts/code.yaml", "value: revision.state\n"],
|
|
["frontend/src/code.tsx", "export const view = <div>{revision.state}</div>;"],
|
|
["frontend/src/code.jsx", "export const view = <div>{workspaceRevision.state}</div>;"],
|
|
];
|
|
for (const [path, source] of failing) {
|
|
await put(root, path, source);
|
|
await assert.rejects(verifyEntries({ root, entries: [entry("policy_text", path)] }), /revision-state/, path);
|
|
}
|
|
});
|
|
|
|
|
|
test("PowerShell executable subexpressions expose dollar-prefixed revision access", async (t) => {
|
|
const root = await fixture(t);
|
|
const failing = [
|
|
["scripts/ps-property.ps1", 'Write-Output "revision: $($revision.state)"\n'],
|
|
["scripts/ps-element.ps1", 'Write-Output "$($workspaceRevision[\'state\'])"\n'],
|
|
["scripts/ps-workspace.ps1", '$value = $workspaceRevision.state\n'],
|
|
["scripts/ps-nested.ps1", 'Write-Output "$($($revision.state))"\n'],
|
|
];
|
|
for (const [path, source] of failing) {
|
|
await put(root, path, source);
|
|
await assert.rejects(verifyEntries({ root, entries: [entry("policy_text", path)] }), /revision-state/, path);
|
|
}
|
|
const passing = [
|
|
'# $revision.state\nWrite-Output "revision.state"\n',
|
|
"Write-Output '$selectedWorkspace[\"state\"]'\n",
|
|
];
|
|
for (let index = 0; index < passing.length; index += 1) {
|
|
const path = `scripts/ps-literal-${index}.ps1`;
|
|
await put(root, path, passing[index]);
|
|
await verifyEntries({ root, entries: [entry("policy_text", path)] });
|
|
}
|
|
});
|
|
|
|
test("Python f-string fields expose revision access while literal text remains masked", async (t) => {
|
|
const root = await fixture(t);
|
|
const failing = [
|
|
["backend/scripts/f-property.py", 'value = f"{revision.state}"\n'],
|
|
["backend/scripts/fr-element.py", 'value = fr"{workspaceRevision[\'state\']}"\n'],
|
|
["backend/scripts/rf-element.py", 'value = rf"prefix {selectedWorkspace[\"state\"]}"\n'],
|
|
];
|
|
for (const [path, source] of failing) {
|
|
await put(root, path, source);
|
|
await assert.rejects(verifyEntries({ root, entries: [entry("policy_text", path)] }), /revision-state/, path);
|
|
}
|
|
const passing = [
|
|
'value = f"revision.state"\n',
|
|
'value = f"{{revision.state}}"\n',
|
|
'value = "revision.state"\n',
|
|
'value = r"workspaceRevision.state"\n',
|
|
'value = """selectedWorkspace.state"""\n',
|
|
'value = r"""revision.state"""\n',
|
|
];
|
|
for (let index = 0; index < passing.length; index += 1) {
|
|
const path = `backend/scripts/python-literal-${index}.py`;
|
|
await put(root, path, passing[index]);
|
|
await verifyEntries({ root, entries: [entry("policy_text", path)] });
|
|
}
|
|
});
|
|
|
|
|
|
test("Bash masking preserves parameter trimming and executable command consumers", async (t) => {
|
|
const root = await fixture(t);
|
|
const failing = [
|
|
["scripts/trim.sh", "trimmed=${value#prefix}; old=revision.state\n"],
|
|
["scripts/base.sh", "base=${path##*/}; old=workspaceRevision.state\n"],
|
|
["scripts/backtick.sh", "old=`echo revision.state`\n"],
|
|
["scripts/quoted-backtick.sh", 'echo "old: `echo revision.state`"\n'],
|
|
["scripts/jq.sh", "jq '.revision.state' snapshot.json\n"],
|
|
["scripts/substitution.sh", 'echo "$(echo revision.state)"\n'],
|
|
];
|
|
for (const [path, source] of failing) {
|
|
await put(root, path, source);
|
|
await assert.rejects(verifyEntries({ root, entries: [entry("policy_text", path)] }), /revision-state/, path);
|
|
}
|
|
await put(root, "scripts/echo.sh", 'echo "revision.state"\n# workspaceRevision.state\n');
|
|
await verifyEntries({ root, entries: [entry("policy_text", "scripts/echo.sh")] });
|
|
await put(root, "scripts/literal.yaml", '# revision.state\nvalue: "selectedWorkspace.state"\n');
|
|
await verifyEntries({ root, entries: [entry("policy_text", "scripts/literal.yaml")] });
|
|
});
|
|
|
|
test("YAML keeps URL slashes as data rather than a false line comment", async (t) => {
|
|
const root = await fixture(t);
|
|
const path = "scripts/url.yaml";
|
|
await put(root, path, "url: https://host/x; old: selectedWorkspace.state\n");
|
|
await assert.rejects(verifyEntries({ root, entries: [entry("policy_text", path)] }), /revision-state/);
|
|
});
|