import { expect, test } from "bun:test";
import { migrateApisBlock } from "../src/migrations/apis-block.ts";
const SOURCE = `page Search {
client {
api searchUsers POST /api/users {
request { body { name?: string } }
response { return data.users }
error { return [] }
}
}
view { x }
}
`;
test("a client api entry moves into an apis block", () => {
const result = migrateApisBlock(SOURCE) as { source: string; changed: boolean };
expect(result.changed).toBe(true);
expect(result.source).toContain("apis {");
expect(result.source).toContain("searchUsers POST /api/users");
expect(result.source).not.toContain("client {\n api");
});
test("the sections survive unchanged", () => {
const result = migrateApisBlock(SOURCE) as { source: string };
expect(result.source).toContain("return data.users");
expect(result.source).toContain("return []");
});
test("running it on migrated source changes nothing", () => {
const once = (migrateApisBlock(SOURCE) as { source: string }).source;
const twice = migrateApisBlock(once) as { source: string; changed: boolean };
expect(twice.changed).toBe(false);
expect(twice.source).toBe(once);
});
test("a name declared in both modes is skipped with a reason", () => {
const clash = `page P {
ssr { api dup GET /api/a { response { return data } } }
client { api dup GET /api/a { response { return data } } }
view { x }
}
`;
const result = migrateApisBlock(clash) as { skip: string };
expect(result.skip).toContain("dup");
});
test("a page with an existing apis block merges legacy entries instead of producing two apis blocks", () => {
const source = `page P {
apis {
existing GET /api/existing {
response { return data }
}
}
client {
api added POST /api/added {
response { return data.value }
}
}
view { x }
}
`;
const result = migrateApisBlock(source) as { source: string; changed: boolean };
expect(result.changed).toBe(true);
const apisCount = (result.source.match(/\bapis\s*\{/g) ?? []).length;
expect(apisCount).toBe(1);
expect(result.source).toContain("existing GET /api/existing");
expect(result.source).toContain("added POST /api/added");
expect(result.source).not.toContain("client {");
});
test("a mode block mixing api entries with other content is skipped, not partially rewritten", () => {
// A bare `ssr { … }` block is invalid syntax once api entries are removed
// (only `state` and hydrate forms survive) -- leftover content such as
// `functions { }` belongs to the move-mode-functions migration, so this
// file cannot be completed by this migration alone.
const source = `page P {
ssr {
api foo GET /api/foo {
response { return data }
}
functions {
function helper() { return 1 }
}
}
view { x }
}
`;
const result = migrateApisBlock(source) as { skip: string };
expect(result.skip).toContain("ssr");
expect(result.skip).toContain("functions");
});
test("a legacy bare-body api entry is left alone instead of producing a bogus parse failure", () => {
// No request/response/error sections -- this is the legacy bare-body form
// that `report-legacy-api-bodies` handles for manual review. Folding it
// into `apis { }` as-is would produce source the grammar rejects, so this
// migration must leave it untouched rather than attempt the rewrite.
const source = `page Hello {
ssr {
api ssrUsers GET /api/users/ssr {
return userNames(users)
}
}
view { x }
}
`;
const result = migrateApisBlock(source) as { source: string; changed: boolean };
expect(result.changed).toBe(false);
expect(result.source).toBe(source);
});
test("a brace inside a string literal in a section body does not truncate the entry", () => {
const source = `page P {
client {
api foo GET /api/foo {
response { return { text: "a } b" } }
}
}
view { x }
}
`;
const result = migrateApisBlock(source) as { source: string; changed: boolean };
expect(result.changed).toBe(true);
expect(result.source).toContain('text: "a } b"');
expect(result.source).toContain("apis {");
});