Files
rea/tests/acceptance/applications/javascriptRpcPropertyOwnershipCli.test.ts
6688ed6db7 fix(javascript): retain standalone GraphQL source operations
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>
2026-10-15 15:59:42 +08:00

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);
},
);