Retain named operations in GraphQL Source bodies with locationOffset metadata. Use final property ownership and parsed lexical facts to suppress copies only when a recovered document, its containers and source aliases have proven private metadata ownership. Preserve independent observations after writes, exports, opaque uses, classic-script global exposure or direct evaluation. Share interpretation across application and captured-web analysis. Build optional reference indexes once per source and checkpoint examined records, including suppressed findings, to retain cooperative cancellation. Fixes #3311 Co-authored-by: Kaoru0822 <208281322+kaoru0822-kitauji@users.noreply.github.com> Co-authored-by: gensokyo-dev <342580529+gensokyo-dev@users.noreply.github.com>
156 lines
5.7 KiB
TypeScript
156 lines
5.7 KiB
TypeScript
import { expect } from "vitest";
|
|
|
|
import { createTestTempDirectory } from "../../fixtures/temporaryDirectory.js";
|
|
import { cliTest } from "../../support/cli/cliFixture.js";
|
|
import {
|
|
analyzeJavaScriptDirectory,
|
|
runNodeJsonControl,
|
|
} from "../../support/cli/javascriptApplication.js";
|
|
import { writeFixtureFiles } from "../../support/javascriptApplicationFixture.js";
|
|
|
|
cliTest.for(["CLI", "stdio MCP"] as const)(
|
|
"retains standalone GraphQL sources and only suppresses owned copies through %s",
|
|
async (surface, { cli }) => {
|
|
const root = await createTestTempDirectory("rea-graphql-source-");
|
|
await writeFixtureFiles(root, {
|
|
"classic.js": `
|
|
const document = {
|
|
kind: "OperationDefinition", operation: "query",
|
|
name: { kind: "Name", value: "ClassicSource" },
|
|
loc: { source: {
|
|
body: "query ClassicSource { viewer { id } }",
|
|
locationOffset: { line: 1, column: 1 },
|
|
} },
|
|
};
|
|
`,
|
|
"entry.mjs": `
|
|
const source = {
|
|
body: "query Standalone { viewer { id } }",
|
|
locationOffset: { line: 1, column: 1 },
|
|
};
|
|
const compiledSource = {
|
|
body: "query Compiled { viewer { id } }",
|
|
locationOffset: { line: 1, column: 1 },
|
|
};
|
|
const document = { kind: "Document", definitions: [{
|
|
kind: "OperationDefinition", operation: "query",
|
|
name: { kind: "Name", value: "Compiled" },
|
|
}], loc: { source: compiledSource } };
|
|
const independentSource = {
|
|
body: "query Independent { viewer { id } }",
|
|
locationOffset: { line: 1, column: 1 },
|
|
};
|
|
const unrelated = {
|
|
kind: "OperationDefinition", operation: "query",
|
|
name: { kind: "Name", value: "Independent" },
|
|
};
|
|
const changedSource = {
|
|
body: "query ChangedSource { viewer { id } }",
|
|
locationOffset: { line: 1, column: 1 },
|
|
};
|
|
const location = { source: changedSource };
|
|
location.source = {};
|
|
const changedDocument = {
|
|
kind: "OperationDefinition", operation: "query",
|
|
name: { kind: "Name", value: "ChangedSource" }, loc: location,
|
|
};
|
|
const exposedSource = {
|
|
body: "query ExposedSource { viewer { id } }",
|
|
locationOffset: { line: 1, column: 1 },
|
|
};
|
|
const exposedDocument = {
|
|
kind: "OperationDefinition", operation: "query",
|
|
name: { kind: "Name", value: "ExposedSource" },
|
|
loc: { source: exposedSource },
|
|
};
|
|
exposedDocument.loc.source = {};
|
|
`,
|
|
});
|
|
const result = await analyzeJavaScriptDirectory(surface, cli, root);
|
|
const operations = result.graph.nodes
|
|
.filter((node) => node.kind === "endpoint")
|
|
.flatMap((node) => node.observations)
|
|
.filter(({ properties }) => properties.mechanism === "graphql:query");
|
|
expect(operations.map(({ properties }) => properties.value).sort()).toEqual(
|
|
[
|
|
"ChangedSource",
|
|
"ChangedSource",
|
|
"ClassicSource",
|
|
"ClassicSource",
|
|
"Compiled",
|
|
"ExposedSource",
|
|
"ExposedSource",
|
|
"Independent",
|
|
"Independent",
|
|
"Standalone",
|
|
],
|
|
);
|
|
for (const observation of operations)
|
|
expect(observation.evidence).toMatchObject({
|
|
authority: "ast-static-analysis",
|
|
state: "observed",
|
|
artifact: { available: true },
|
|
location: {
|
|
available: true,
|
|
value: {
|
|
kind: "source-range",
|
|
source:
|
|
observation.properties.value === "ClassicSource"
|
|
? "classic.js"
|
|
: "entry.mjs",
|
|
},
|
|
},
|
|
});
|
|
expect(result.statistics.parse_failures).toBe(0);
|
|
expect(result.graph.coverage.status).toBe("complete");
|
|
},
|
|
);
|
|
|
|
cliTest.for(["CLI", "stdio MCP"] as const)(
|
|
"retains only final known RPC object properties through %s",
|
|
async (surface, { cli }) => {
|
|
const root = await createTestTempDirectory("rea-rpc-properties-");
|
|
await writeFixtureFiles(root, {
|
|
"entry.mjs": `
|
|
function httpBatchLink(options) { return options.url; }
|
|
const options = { url: "/trpc/runtime" };
|
|
const key = "url";
|
|
const links = [
|
|
httpBatchLink({ url: "/trpc/old", url: "/trpc/live" }),
|
|
httpBatchLink({ url: "/trpc/spread-old", ...options }),
|
|
httpBatchLink({ url: "/trpc/computed-old", [key]: options.url }),
|
|
httpBatchLink({ ...options, url: "/trpc/final", headers: {} }),
|
|
];
|
|
const doc = {
|
|
kind: "OperationDefinition", operation: "query", operation: "mutation",
|
|
name: { kind: "Name", value: "Old" },
|
|
name: { kind: "Name", value: "Current" },
|
|
};
|
|
console.log(JSON.stringify([links, doc.operation, doc.name.value]));
|
|
`,
|
|
});
|
|
// This control checks JavaScript object semantics, not a live tRPC client.
|
|
expect(await runNodeJsonControl(root, "entry.mjs")).toEqual([
|
|
["/trpc/live", "/trpc/runtime", "/trpc/runtime", "/trpc/final"],
|
|
"mutation",
|
|
"Current",
|
|
]);
|
|
const { graph } = await analyzeJavaScriptDirectory(surface, cli, root);
|
|
const endpoints = graph.nodes
|
|
.filter((node) => node.kind === "endpoint")
|
|
.flatMap((node) => node.observations)
|
|
.filter(({ properties }) =>
|
|
/^(trpc:link:|graphql:)/u.test(String(properties.mechanism)),
|
|
)
|
|
.map(({ properties }) => [properties.value, properties.mechanism]);
|
|
expect(endpoints).toEqual(
|
|
expect.arrayContaining([
|
|
["/trpc/live", "trpc:link:httpBatchLink"],
|
|
["/trpc/final", "trpc:link:httpBatchLink"],
|
|
["Current", "graphql:mutation"],
|
|
]),
|
|
);
|
|
expect(endpoints).toHaveLength(3);
|
|
},
|
|
);
|