mirror of
https://github.com/morluto/rea.git
synced 2026-11-07 07:16:13 +00:00
* feat(cli): expose caller-selected reference-source path exclusions Let import-reference-source accept a JSON array of Git-ignore rules and pass them to the existing importer before the source tree is opened. Fixes #2443 * docs(cli): use an effective reference exclusion negation * test(cli): expect entry_kind on reference-source exclusions Main records entry_kind on every historical source exclusion. The caller exclusion assertions still compared objects without that field, so the pruning test failed after merging main. Co-authored-by: Yunzhou Li <YunzhouLi-hub@users.noreply.github.com> * test(cli): record caller *.tmp over project ignore Caller exclusions are mandatory and override project rules, so project.tmp matched by *.tmp is caller-excluded. Align the pruning assertion with that result from the merged importer. --------- Co-authored-by: morluto <76467478+morluto@users.noreply.github.com> Co-authored-by: REA Maintenance <rea-maintenance@users.noreply.github.com> Co-authored-by: Cursor Agent <cursoragent@cursor.com> Co-authored-by: Yunzhou Li <YunzhouLi-hub@users.noreply.github.com>
412 lines
12 KiB
TypeScript
412 lines
12 KiB
TypeScript
import {
|
|
mkdir,
|
|
readFile,
|
|
readlink,
|
|
symlink,
|
|
writeFile,
|
|
} from "node:fs/promises";
|
|
import { join } from "node:path";
|
|
|
|
import { describe, expect } from "vitest";
|
|
|
|
import { historicalSourceGraphSchema } from "../../../src/domain/referenceSourceGraph.js";
|
|
import { createTestTempDirectory } from "../../fixtures/temporaryDirectory.js";
|
|
import { cliTest } from "../../support/cli/cliFixture.js";
|
|
import type { TestCli } from "../../support/cli/cliFixture.js";
|
|
|
|
const isolatedEnvironment = (root: string) => ({
|
|
HOME: root,
|
|
USERPROFILE: root,
|
|
XDG_CONFIG_HOME: root,
|
|
XDG_CACHE_HOME: root,
|
|
REA_REFERENCE_SECRET_PATTERNS_JSON: '["private.txt"]',
|
|
});
|
|
|
|
const importSource = (
|
|
cli: TestCli,
|
|
root: string,
|
|
excludePaths: string | undefined,
|
|
) =>
|
|
cli.run({
|
|
arguments: [
|
|
"import-reference-source",
|
|
root,
|
|
"--json",
|
|
...(excludePaths === undefined ? [] : ["--exclude-paths", excludePaths]),
|
|
],
|
|
cwd: root,
|
|
environment: isolatedEnvironment(root),
|
|
});
|
|
|
|
describe.skipIf(process.platform === "win32")(
|
|
"compiled reference-source exclusion provenance",
|
|
() => {
|
|
cliTest(
|
|
"imports source beside a link into a default-excluded directory",
|
|
async ({ cli }) => {
|
|
const root = await createTestTempDirectory(
|
|
"rea-reference-link-exclusion-",
|
|
);
|
|
await mkdir(join(root, "dist"));
|
|
const source = "export const value = 1;\n";
|
|
await writeFile(join(root, "source.ts"), source);
|
|
await symlink("dist", join(root, "alias"));
|
|
const result = await cli.run({
|
|
arguments: ["import-reference-source", root, "--json"],
|
|
cwd: root,
|
|
environment: {
|
|
HOME: root,
|
|
USERPROFILE: root,
|
|
XDG_CONFIG_HOME: root,
|
|
XDG_CACHE_HOME: root,
|
|
},
|
|
});
|
|
expect(result.exitCode).toBe(0);
|
|
const graph = historicalSourceGraphSchema.parse(result.json);
|
|
expect(graph.inventory_state).toBe("partial");
|
|
expect(graph.exclusions).toContainEqual({
|
|
path: "dist",
|
|
entry_kind: "directory",
|
|
reason: "default-ignored",
|
|
pattern: "dist/",
|
|
});
|
|
expect(graph.entries).toEqual(
|
|
expect.arrayContaining([
|
|
expect.objectContaining({
|
|
path: "alias",
|
|
kind: "symlink",
|
|
target: "dist",
|
|
target_state: "internal",
|
|
}),
|
|
expect.objectContaining({
|
|
path: "source.ts",
|
|
kind: "file",
|
|
content_state: "hashed",
|
|
}),
|
|
]),
|
|
);
|
|
expect(graph.entries.some(({ path }) => path === "dist")).toBe(false);
|
|
expect(await readFile(join(root, "source.ts"), "utf8")).toBe(source);
|
|
expect(await readlink(join(root, "alias"))).toBe("dist");
|
|
},
|
|
);
|
|
|
|
cliTest(
|
|
"reports project, default, and explicit secret exclusions",
|
|
async ({ cli }) => {
|
|
const root = await createTestTempDirectory("rea-reference-exclusions-");
|
|
const logs = join(root, "logs");
|
|
await mkdir(logs);
|
|
await Promise.all([
|
|
writeFile(join(root, ".gitignore"), "project.tmp\n"),
|
|
writeFile(join(root, "project.tmp"), "project policy\n"),
|
|
writeFile(join(logs, "application.log"), "default policy\n"),
|
|
writeFile(join(root, "private.txt"), "sensitive local file\n"),
|
|
writeFile(join(root, "source.ts"), "export const value = 1;\n"),
|
|
]);
|
|
|
|
const result = await cli.run({
|
|
arguments: ["import-reference-source", root, "--json"],
|
|
cwd: root,
|
|
environment: {
|
|
HOME: root,
|
|
USERPROFILE: root,
|
|
XDG_CONFIG_HOME: root,
|
|
XDG_CACHE_HOME: root,
|
|
REA_REFERENCE_SECRET_PATTERNS_JSON: '["private.txt"]',
|
|
},
|
|
});
|
|
|
|
expect(result.exitCode).toBe(0);
|
|
const graph = historicalSourceGraphSchema.parse(result.json);
|
|
expect(graph.exclusions).toEqual(
|
|
expect.arrayContaining([
|
|
{
|
|
path: "project.tmp",
|
|
entry_kind: "file",
|
|
reason: "project-ignored",
|
|
pattern: "project.tmp",
|
|
},
|
|
{
|
|
path: "logs/application.log",
|
|
entry_kind: "file",
|
|
reason: "default-ignored",
|
|
pattern: "*.log",
|
|
},
|
|
{
|
|
path: "private.txt",
|
|
entry_kind: "file",
|
|
reason: "configured-secret",
|
|
pattern: "private.txt",
|
|
},
|
|
]),
|
|
);
|
|
expect(graph.entries).toContainEqual(
|
|
expect.objectContaining({ path: "source.ts", kind: "file" }),
|
|
);
|
|
},
|
|
);
|
|
},
|
|
);
|
|
|
|
describe.skipIf(process.platform === "win32")(
|
|
"compiled reference-source caller exclusions",
|
|
() => {
|
|
cliTest(
|
|
"prunes caller-excluded directories and applies Git-ignore rules",
|
|
async ({ cli }) => {
|
|
const root = await createTestTempDirectory(
|
|
"rea-reference-caller-exclusions-",
|
|
);
|
|
const logs = join(root, "logs");
|
|
const skipped = join(root, "skip-me", "nested");
|
|
await mkdir(logs);
|
|
await mkdir(skipped, { recursive: true });
|
|
await Promise.all([
|
|
writeFile(join(root, ".gitignore"), "project.tmp\n"),
|
|
writeFile(join(root, "project.tmp"), "project policy\n"),
|
|
writeFile(join(logs, "application.log"), "default policy\n"),
|
|
writeFile(join(root, "keep.log"), "negated default rule\n"),
|
|
writeFile(join(root, "private.txt"), "sensitive local file\n"),
|
|
writeFile(join(root, "notes.tmp"), "caller wildcard\n"),
|
|
writeFile(join(skipped, "hidden.ts"), "must not be traversed\n"),
|
|
writeFile(join(root, "source.ts"), "export const value = 1;\n"),
|
|
]);
|
|
const callerRules = '["skip-me/","*.tmp","!keep.log","private.txt"]';
|
|
|
|
const omitted = await importSource(cli, root, undefined);
|
|
const empty = await importSource(cli, root, "[]");
|
|
const selected = await importSource(cli, root, callerRules);
|
|
|
|
expect(omitted.exitCode).toBe(0);
|
|
expect(empty.exitCode).toBe(0);
|
|
const omittedGraph = historicalSourceGraphSchema.parse(omitted.json);
|
|
const emptyGraph = historicalSourceGraphSchema.parse(empty.json);
|
|
expect(emptyGraph.exclusions).toEqual(omittedGraph.exclusions);
|
|
expect(emptyGraph.entries.map((entry) => entry.path)).toEqual(
|
|
omittedGraph.entries.map((entry) => entry.path),
|
|
);
|
|
expect(omittedGraph.exclusions).not.toContainEqual(
|
|
expect.objectContaining({ reason: "caller-excluded" }),
|
|
);
|
|
expect(omittedGraph.entries).toContainEqual(
|
|
expect.objectContaining({ path: "notes.tmp", kind: "file" }),
|
|
);
|
|
expect(omittedGraph.entries).toContainEqual(
|
|
expect.objectContaining({
|
|
path: "skip-me/nested/hidden.ts",
|
|
kind: "file",
|
|
}),
|
|
);
|
|
expect(omittedGraph.exclusions).toContainEqual({
|
|
path: "keep.log",
|
|
entry_kind: "file",
|
|
reason: "default-ignored",
|
|
pattern: "*.log",
|
|
});
|
|
|
|
expect(selected.exitCode).toBe(0);
|
|
const selectedGraph = historicalSourceGraphSchema.parse(selected.json);
|
|
expect(selectedGraph.exclusions).toEqual([
|
|
{
|
|
path: "logs/application.log",
|
|
entry_kind: "file",
|
|
reason: "default-ignored",
|
|
pattern: "*.log",
|
|
},
|
|
{
|
|
path: "notes.tmp",
|
|
entry_kind: "file",
|
|
reason: "caller-excluded",
|
|
pattern: "*.tmp",
|
|
},
|
|
{
|
|
path: "private.txt",
|
|
entry_kind: "file",
|
|
reason: "configured-secret",
|
|
pattern: "private.txt",
|
|
},
|
|
{
|
|
path: "project.tmp",
|
|
entry_kind: "file",
|
|
reason: "caller-excluded",
|
|
pattern: "*.tmp",
|
|
},
|
|
{
|
|
path: "skip-me",
|
|
entry_kind: "directory",
|
|
reason: "caller-excluded",
|
|
pattern: "skip-me/",
|
|
},
|
|
]);
|
|
expect(selectedGraph.entries).toContainEqual(
|
|
expect.objectContaining({ path: "source.ts", kind: "file" }),
|
|
);
|
|
expect(selectedGraph.entries).toContainEqual(
|
|
expect.objectContaining({ path: "keep.log", kind: "file" }),
|
|
);
|
|
for (const path of [
|
|
"skip-me",
|
|
"skip-me/nested",
|
|
"skip-me/nested/hidden.ts",
|
|
"notes.tmp",
|
|
])
|
|
expect(
|
|
selectedGraph.entries.some((entry) => entry.path === path),
|
|
path,
|
|
).toBe(false);
|
|
expect(
|
|
selectedGraph.exclusions.some((exclusion) =>
|
|
exclusion.path.startsWith("skip-me/"),
|
|
),
|
|
).toBe(false);
|
|
},
|
|
);
|
|
},
|
|
);
|
|
|
|
describe("compiled reference-source caller exclusion input", () => {
|
|
for (const scenario of [
|
|
{
|
|
label: "malformed JSON",
|
|
value: "[",
|
|
issue: {
|
|
path: ["excludePaths"],
|
|
reason: "invalid_format",
|
|
expected: "JSON",
|
|
},
|
|
},
|
|
{
|
|
label: "a non-array JSON value",
|
|
value: "{}",
|
|
issue: {
|
|
path: ["excludePaths"],
|
|
reason: "invalid_type",
|
|
expected: "array",
|
|
},
|
|
},
|
|
{
|
|
label: "a numeric rule",
|
|
value: "[1]",
|
|
issue: {
|
|
path: ["excludePaths", 0],
|
|
reason: "invalid_type",
|
|
expected: "string",
|
|
},
|
|
},
|
|
{
|
|
label: "a null rule",
|
|
value: "[null]",
|
|
issue: {
|
|
path: ["excludePaths", 0],
|
|
reason: "invalid_type",
|
|
expected: "string",
|
|
},
|
|
},
|
|
{
|
|
label: "an object rule",
|
|
value: "[{}]",
|
|
issue: {
|
|
path: ["excludePaths", 0],
|
|
reason: "invalid_type",
|
|
expected: "string",
|
|
},
|
|
},
|
|
{
|
|
label: "a boolean rule",
|
|
value: "[true]",
|
|
issue: {
|
|
path: ["excludePaths", 0],
|
|
reason: "invalid_type",
|
|
expected: "string",
|
|
},
|
|
},
|
|
{
|
|
label: "an empty-string rule",
|
|
value: '[""]',
|
|
issue: {
|
|
path: ["excludePaths", 0],
|
|
reason: "out_of_range",
|
|
minimum: 1,
|
|
},
|
|
},
|
|
]) {
|
|
cliTest(
|
|
`rejects ${scenario.label} before an import starts`,
|
|
async ({ cli }) => {
|
|
const directory = await createTestTempDirectory(
|
|
"rea-reference-exclude-input-",
|
|
);
|
|
const root = join(directory, "missing-source");
|
|
const result = await cli.run({
|
|
arguments: [
|
|
"import-reference-source",
|
|
root,
|
|
"--exclude-paths",
|
|
scenario.value,
|
|
"--json",
|
|
],
|
|
cwd: directory,
|
|
environment: {
|
|
HOME: directory,
|
|
USERPROFILE: directory,
|
|
XDG_CONFIG_HOME: directory,
|
|
XDG_CACHE_HOME: directory,
|
|
},
|
|
});
|
|
|
|
expect(result.exitCode).toBe(1);
|
|
expect(result.json).toMatchObject({
|
|
error: "Import failed",
|
|
code: "invalid_request",
|
|
category: "invalid_input",
|
|
message: expect.stringContaining("Git-ignore"),
|
|
details: {
|
|
operation: "import-reference-source",
|
|
issues: [scenario.issue],
|
|
},
|
|
});
|
|
expect(JSON.stringify(result.json)).not.toContain(
|
|
"could not be resolved",
|
|
);
|
|
expect(JSON.stringify(result.json)).not.toContain(root);
|
|
},
|
|
);
|
|
}
|
|
|
|
cliTest(
|
|
"starts an import after an empty exclusion array",
|
|
async ({ cli }) => {
|
|
const directory = await createTestTempDirectory(
|
|
"rea-reference-exclude-empty-",
|
|
);
|
|
const root = join(directory, "missing-source");
|
|
const result = await cli.run({
|
|
arguments: [
|
|
"import-reference-source",
|
|
root,
|
|
"--exclude-paths",
|
|
"[]",
|
|
"--json",
|
|
],
|
|
cwd: directory,
|
|
environment: {
|
|
HOME: directory,
|
|
USERPROFILE: directory,
|
|
XDG_CONFIG_HOME: directory,
|
|
XDG_CACHE_HOME: directory,
|
|
},
|
|
});
|
|
|
|
expect(result.exitCode).toBe(1);
|
|
expect(result.json).toMatchObject({
|
|
error: "Import failed",
|
|
category: "invalid_input",
|
|
message: expect.stringContaining(
|
|
"Reference source root could not be resolved",
|
|
),
|
|
});
|
|
},
|
|
);
|
|
});
|