fix(environment): align comparison collection identity

This commit is contained in:
morluto
2026-10-27 07:28:49 +08:00
parent 29ebd67405
commit 87e2cc26d8
3 changed files with 67 additions and 5 deletions
+2 -1
View File
@@ -625,7 +625,8 @@ refused for them, as the manifest is not.
Each `GET` tool whose template ends in a parameter is an item tool, and its
type's `collection_template` is the template without that parameter; any other
`GET` tool is a list tool of its own template. Empty segments are ignored, so a
`GET` tool is a list tool of its own template. Interior empty segments remain
meaningful: `/api/books` and `/api//books` name distinct collections. A single
trailing slash names the same collection. An item tool proves its type's
`id_field` when every initial response carries the requested path value in the
same field of one entity object (`id_basis: "path-parameter"`). The object is
@@ -130,6 +130,26 @@ describe("matchProbeRequest", () => {
expect(item?.args).toEqual({ id: "%31" });
});
it("keeps trailing slashes significant when matching HTTP route templates", () => {
const manifest = {
...source.manifest,
tools: source.manifest.tools.map((tool) => ({
...tool,
path_template: `${tool.path_template}/`,
})),
};
const request = {
method: "GET",
path: "/api/books/7/",
query: null,
body: undefined,
};
expect(matchProbeRequest(manifest, request)?.args).toEqual({ id: "7" });
expect(
matchProbeRequest(manifest, { ...request, path: "/api/books/7" }),
).toBeUndefined();
});
it("keeps every occurrence of a repeated query value", () => {
const item = matchProbeRequest(environmentServeSource().manifest, {
method: "GET",
@@ -176,6 +196,31 @@ describe("collections under different parents", () => {
expect(map.env(110, "/api/orgs/2/users")).toBe(20);
});
it("uses state collection identity for root and trailing-slash routes", () => {
const trailing = {
...nestedUsers,
path_template: `${nestedUsers.path_template}/`,
};
const segments = ["api", "orgs", "%32", "users", "101", ""];
expect(collectionScope(trailing, segments)).toBe("/api/orgs/2/users");
expect(parameterScopes(trailing, segments)).toEqual(
parameterScopes(nestedUsers, segments),
);
expect(
collectionScope({ ...nestedUsers, path_template: "/", params: [] }),
).toBe("/");
expect(
collectionScope({
...nestedUsers,
path_template: "/api//books/",
params: [],
}),
).toBe("/api//books");
expect(
collectionScope(trailing, ["api", "orgs", "a%2Fb", "users", "101", ""]),
).toBe("/api/orgs/a%2Fb/users");
});
it("shares a child map when the same parent is spelled with one encoding", () => {
const encoded = ["api", "orgs", "%32", "users", "101"];
const plain = ["api", "orgs", "2", "users", "101"];
@@ -465,6 +510,24 @@ const compareRows = (
return report.value.probes[0]!;
};
it("learns ids from trailing-slash lists in the collection their item calls name", () => {
const item = (id: number, name: string) => ({ id, name });
const probe = compareRows(
[
["GET", "/api/books", [item(1, "Book")]],
["GET", "/api/books/1", item(1, "Book")],
["GET", "/api/authors/", [item(2, "Ann")]],
["GET", "/api/authors/2/", item(2, "Ann")],
],
[
["GET", "/api/books", [item(101, "Book")]],
["GET", "/api/authors/", [item(101, "Ann")]],
["GET", "/api/authors/101/", item(101, "Ann")],
],
);
expect(probe.results.map((step) => step.equal)).toEqual([true, true, true]);
});
describe("ids the comparison used to misread", () => {
it.each([
["", "", true],
@@ -59,9 +59,7 @@ const collectionAt = (
segments?: readonly string[],
): string => {
const params = new Set(tool.params.map((param) => param.index));
return `/${tool.path_template
.slice(1)
.split("/")
return `/${segmentsOf(tool.path_template)
.slice(0, end)
.map((segment, index) => {
if (!params.has(index)) return segment;
@@ -78,7 +76,7 @@ export const collectionScope = (
tool: EnvironmentManifestTool,
segments?: readonly string[],
): string => {
const length = tool.path_template.slice(1).split("/").length;
const length = segmentsOf(tool.path_template).length;
return collectionAt(
tool,
tool.params.some((param) => param.index === length - 1)