This commit is contained in:
@@ -0,0 +1,176 @@
|
||||
// SPDX-License-Identifier: LicenseRef-ConusAI-Template-1.0
|
||||
//
|
||||
// The licence layout (plan item AT5.1, spec section 14.3): LICENSE-TEMPLATE says what it is, the
|
||||
// customer's LICENSE is a stub, NOTICE and TEMPLATE-FILES exist and agree with the tree, and every
|
||||
// template file that can hold a comment carries the SPDX header. The negative controls make the check
|
||||
// fail where it should: a scaffold file that loses its header, a file nobody listed, a listed file that
|
||||
// is gone.
|
||||
|
||||
import { mkdirSync, mkdtempSync, readFileSync, rmSync, writeFileSync } from "node:fs"
|
||||
import { tmpdir } from "node:os"
|
||||
import { dirname, join } from "node:path"
|
||||
import { afterEach, describe, expect, it } from "vitest"
|
||||
import {
|
||||
hasHeader,
|
||||
headerFor,
|
||||
LIST_FILE,
|
||||
listText,
|
||||
parseList,
|
||||
problems,
|
||||
SPDX_ID,
|
||||
SPDX_LINE,
|
||||
templateFiles,
|
||||
withHeader,
|
||||
write,
|
||||
} from "../scripts/licence.mjs"
|
||||
|
||||
const APP_ROOT = new URL("..", import.meta.url).pathname
|
||||
const read = (path: string) => readFileSync(join(APP_ROOT, path), "utf8")
|
||||
|
||||
const made: string[] = []
|
||||
afterEach(() => {
|
||||
for (const dir of made.splice(0)) rmSync(dir, { recursive: true, force: true })
|
||||
})
|
||||
|
||||
/** A small tree shaped like the template: a scaffold, a module and the layout's own files. */
|
||||
function fixture(files: Record<string, string>): string {
|
||||
const root = mkdtempSync(join(tmpdir(), "licence-"))
|
||||
made.push(root)
|
||||
const all: Record<string, string> = {
|
||||
LICENSE: "yours\n",
|
||||
"LICENSE-TEMPLATE": "placeholder\n",
|
||||
NOTICE: "notice\n",
|
||||
"THIRD-PARTY-NOTICES": "none\n",
|
||||
"package.json": "{}\n",
|
||||
"src/a.ts": "export const a = 1\n",
|
||||
"modules/notes/index.ts": "export const notes = 1\n",
|
||||
...files,
|
||||
}
|
||||
for (const [path, text] of Object.entries(all)) {
|
||||
mkdirSync(dirname(join(root, path)), { recursive: true })
|
||||
writeFileSync(join(root, path), text)
|
||||
}
|
||||
write(root)
|
||||
return root
|
||||
}
|
||||
|
||||
describe("this template's licence layout", () => {
|
||||
it("is whole: every file is listed, every file that can hold a header has it, and the four documents exist", () => {
|
||||
expect(problems(APP_ROOT)).toEqual([])
|
||||
})
|
||||
|
||||
it("says in LICENSE-TEMPLATE that it is a placeholder, until counsel decides (AT-O2)", () => {
|
||||
const text = read("LICENSE-TEMPLATE")
|
||||
expect(text).toMatch(/PLACEHOLDER/)
|
||||
expect(text).toContain("AT-O2")
|
||||
expect(text).toContain(SPDX_ID)
|
||||
expect(text).toMatch(/counsel/i)
|
||||
})
|
||||
|
||||
it("keeps LICENSE the customer's own, a stub that names what it does not cover", () => {
|
||||
const text = read("LICENSE")
|
||||
expect(text).toMatch(/This file is yours/)
|
||||
expect(text).toContain("LICENSE-TEMPLATE")
|
||||
expect(text).toContain("TEMPLATE-FILES")
|
||||
})
|
||||
|
||||
it("lists the template's files and not the customer's", () => {
|
||||
const listed = parseList(read(LIST_FILE))
|
||||
expect(listed).toEqual(templateFiles(APP_ROOT))
|
||||
expect(listed).toContain("apps/web/app/layout.tsx")
|
||||
expect(listed).toContain("LICENSE-TEMPLATE")
|
||||
expect(listed).toContain(LIST_FILE)
|
||||
expect(listed).not.toContain("LICENSE")
|
||||
expect(listed).not.toContain("pnpm-lock.yaml")
|
||||
expect(listed.some((path) => path.startsWith("modules/"))).toBe(false)
|
||||
expect(listed).not.toContain(".conusai/template.json")
|
||||
})
|
||||
|
||||
it("puts the header first in a TypeScript file, a kernel migration, a workflow, the Dockerfile and the guide", () => {
|
||||
expect(read("Dockerfile").split("\n")[0]).toBe(`# ${SPDX_LINE}`)
|
||||
expect(read("test/registry.test.ts").split("\n")[0]).toBe(`// ${SPDX_LINE}`)
|
||||
expect(read("migrations/postgres/0001_kernel.sql").split("\n")[0]).toBe(`-- ${SPDX_LINE}`)
|
||||
expect(read(".gitea/workflows/ci.yml").split("\n")[0]).toBe(`# ${SPDX_LINE}`)
|
||||
expect(read("AGENTS.md").split("\n")[0]).toBe(`<!-- ${SPDX_LINE} -->`)
|
||||
})
|
||||
})
|
||||
|
||||
describe("the header", () => {
|
||||
it("is a comment of the file's own kind, and none for a file that cannot hold one", () => {
|
||||
expect(headerFor("a/b.ts")).toBe(`// ${SPDX_LINE}`)
|
||||
expect(headerFor("a/b.mjs")).toBe(`// ${SPDX_LINE}`)
|
||||
expect(headerFor("a/b.css")).toBe(`/* ${SPDX_LINE} */`)
|
||||
expect(headerFor("a/b.sql")).toBe(`-- ${SPDX_LINE}`)
|
||||
expect(headerFor("a/b.md")).toBe(`<!-- ${SPDX_LINE} -->`)
|
||||
expect(headerFor("compose.yaml")).toBe(`# ${SPDX_LINE}`)
|
||||
expect(headerFor("Dockerfile")).toBe(`# ${SPDX_LINE}`)
|
||||
expect(headerFor(".env.example")).toBe(`# ${SPDX_LINE}`)
|
||||
expect(headerFor("package.json")).toBeNull()
|
||||
expect(headerFor("llms.txt")).toBeNull()
|
||||
expect(headerFor("LICENSE-TEMPLATE")).toBeNull()
|
||||
})
|
||||
|
||||
it("goes after a shebang, and is added once", () => {
|
||||
const script = "#!/usr/bin/env bash\nset -e\n"
|
||||
const once = withHeader(script, "run.sh")
|
||||
expect(once).toBe(`#!/usr/bin/env bash\n# ${SPDX_LINE}\nset -e\n`)
|
||||
expect(withHeader(once, "run.sh")).toBe(once)
|
||||
expect(hasHeader(once, "run.sh")).toBe(true)
|
||||
expect(hasHeader(script, "run.sh")).toBe(false)
|
||||
})
|
||||
|
||||
it("goes after the frontmatter of a Markdown file that opens with one (an agent skill), and is added once", () => {
|
||||
const skill = "---\nname: fix\ndescription: Fix it.\n---\n# Fix\n"
|
||||
const once = withHeader(skill, ".claude/skills/fix/SKILL.md")
|
||||
expect(once).toBe(`---\nname: fix\ndescription: Fix it.\n---\n<!-- ${SPDX_LINE} -->\n# Fix\n`)
|
||||
expect(withHeader(once, ".claude/skills/fix/SKILL.md")).toBe(once)
|
||||
expect(hasHeader(once, ".claude/skills/fix/SKILL.md")).toBe(true)
|
||||
// A header above the frontmatter would hide the skill from its tool, so it does not count.
|
||||
expect(hasHeader(`<!-- ${SPDX_LINE} -->\n${skill}`, ".claude/skills/fix/SKILL.md")).toBe(true)
|
||||
expect(hasHeader(skill, ".claude/skills/fix/SKILL.md")).toBe(false)
|
||||
})
|
||||
})
|
||||
|
||||
describe("the check fails where it should", () => {
|
||||
it("passes a tree that `write` has laid out, and ignores modules/** and LICENSE", () => {
|
||||
const root = fixture({})
|
||||
expect(problems(root)).toEqual([])
|
||||
expect(parseList(readFileSync(join(root, LIST_FILE), "utf8"))).toEqual([
|
||||
"LICENSE-TEMPLATE",
|
||||
"NOTICE",
|
||||
"TEMPLATE-FILES",
|
||||
"THIRD-PARTY-NOTICES",
|
||||
"package.json",
|
||||
"src/a.ts",
|
||||
])
|
||||
expect(readFileSync(join(root, "modules/notes/index.ts"), "utf8")).not.toContain("SPDX")
|
||||
expect(readFileSync(join(root, "LICENSE"), "utf8")).toBe("yours\n")
|
||||
})
|
||||
|
||||
it("fails, naming the file, when a scaffold file loses its header", () => {
|
||||
const root = fixture({})
|
||||
writeFileSync(join(root, "src/a.ts"), "export const a = 1\n")
|
||||
expect(problems(root)).toEqual([`src/a.ts has no "${SPDX_LINE}" header`])
|
||||
})
|
||||
|
||||
it("fails when a file is not in TEMPLATE-FILES, and when a listed file is gone", () => {
|
||||
const root = fixture({})
|
||||
writeFileSync(join(root, "src/new.ts"), `// ${SPDX_LINE}\nexport {}\n`)
|
||||
expect(problems(root)).toEqual(["TEMPLATE-FILES does not list src/new.ts"])
|
||||
rmSync(join(root, "src/new.ts"))
|
||||
rmSync(join(root, "src/a.ts"))
|
||||
expect(problems(root)).toEqual(["TEMPLATE-FILES lists src/a.ts, which is not a template file"])
|
||||
})
|
||||
|
||||
it("fails when one of the four documents is missing", () => {
|
||||
const root = fixture({})
|
||||
rmSync(join(root, "NOTICE"))
|
||||
expect(problems(root)).toContain("NOTICE is missing")
|
||||
})
|
||||
|
||||
it("is what listText writes: comments, then one path a line", () => {
|
||||
const text = listText(["a", "b"])
|
||||
expect(parseList(text)).toEqual(["a", "b"])
|
||||
expect(text.startsWith("# ")).toBe(true)
|
||||
})
|
||||
})
|
||||
Reference in New Issue
Block a user