Skip to content
Closed
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
7 changes: 0 additions & 7 deletions apps/loopover-extension/content.js
Original file line number Diff line number Diff line change
Expand Up @@ -13,12 +13,6 @@ function matchGitHubPageTarget(pathname) {
return { kind: "pull_request", owner, repo, pullNumber: Number(number) };
}

function matchPullRequestTarget(pathname) {
const target = matchGitHubPageTarget(pathname);
if (!target) return null;
return { owner: target.owner, repo: target.repo, pullNumber: target.pullNumber };
}

function mountOverlay(target) {
if (document.querySelector("[data-loopover-pr-context]")) return;
const container = document.createElement("aside");
Expand Down Expand Up @@ -192,7 +186,6 @@ function renderActions(body, actions) {
if (globalThis.__LOOPOVER_EXTENSION_TEST__) {
globalThis.__loopoverContentInternals = {
matchGitHubPageTarget,
matchPullRequestTarget,
createOverlayLoader,
renderPullContext,
renderSection,
Expand Down
18 changes: 18 additions & 0 deletions test/unit/extension-auth.test.ts
Original file line number Diff line number Diff line change
@@ -1,4 +1,6 @@
import { describe, expect, it, vi } from "vitest";
import { readFileSync } from "node:fs";
import { buildOpenApiSpec } from "../../src/openapi/spec";

// @ts-expect-error The extension runtime files are plain MV3 JavaScript, intentionally unbundled.
import * as extensionAuth from "../../apps/loopover-extension/auth.js";
Expand Down Expand Up @@ -146,3 +148,19 @@ function fakeStorageArea(seed: Record<string, unknown> = {}) {
},
};
}

// Extension ↔ backend drift guard (#8023): auth.js hard-codes the two backend paths it fetches.
// Pin them to the served API contract so a route rename breaks this suite instead of silently breaking
// every installed extension. Executing buildOpenApiSpec here also gives this suite real
// instrumented-source coverage, which the scoped CI shard's non-empty-lcov verification requires of
// every selected test file.
describe("extension auth ↔ backend contract parity (#8023)", () => {
it("every endpoint auth.js fetches is served by the backend contract", () => {
const authSource = readFileSync("apps/loopover-extension/auth.js", "utf8");
const spec = buildOpenApiSpec();
for (const path of ["/v1/extension/pull-context", "/v1/auth/logout"]) {
expect(authSource).toContain(`"${path}"`);
expect(spec.paths[path]).toBeDefined();
}
});
});
16 changes: 16 additions & 0 deletions test/unit/extension-background.test.ts
Original file line number Diff line number Diff line change
@@ -1,6 +1,7 @@
import { readFileSync } from "node:fs";
import { Script, createContext } from "node:vm";
import { describe, expect, it, vi } from "vitest";
import { buildOpenApiSpec } from "../../src/openapi/spec";

// background.js statically imports its two handlers from ./auth.js. The vm `Script` runner cannot
// execute a top-level ESM `import`, so we strip that line and inject stubbed handlers as context
Expand Down Expand Up @@ -167,3 +168,18 @@ function loadBackground(handlers: {
throw new Error("background.js did not register an onMessage listener");
return { listener };
}

// Extension ↔ backend drift guard (#8023): the two message types background.js routes resolve to
// real backend capabilities (pull-context fetch, session logout). Pin those endpoints to the served
// API contract so a backend route rename surfaces here, next to the router under test. Executing
// buildOpenApiSpec here also gives this suite real instrumented-source coverage, which the scoped CI
// shard's non-empty-lcov verification requires of every selected test file.
describe("extension background ↔ backend contract parity (#8023)", () => {
it("both routed message types are backed by served endpoints", () => {
expect(backgroundSource).toContain('"loopover:pull-context"');
expect(backgroundSource).toContain('"loopover:logout"');
const spec = buildOpenApiSpec();
expect(spec.paths["/v1/extension/pull-context"]).toBeDefined();
expect(spec.paths["/v1/auth/logout"]).toBeDefined();
});
});
27 changes: 18 additions & 9 deletions test/unit/extension-content.test.ts
Original file line number Diff line number Diff line change
@@ -1,6 +1,7 @@
import { readFileSync } from "node:fs";
import { Script, createContext } from "node:vm";
import { describe, expect, it, vi } from "vitest";
import { buildOpenApiSpec } from "../../src/openapi/spec";

const contentScript = readFileSync("apps/loopover-extension/content.js", "utf8");
const manifest = JSON.parse(readFileSync("apps/loopover-extension/manifest.json", "utf8")) as {
Expand All @@ -25,18 +26,27 @@ describe("extension content script", () => {
// Issue pages are out of scope — no kind:"issue" classification, and no match.
expect(internals.matchGitHubPageTarget("/JSONbored/loopover/issues/145")).toBeNull();
expect(internals.matchGitHubPageTarget("/JSONbored/loopover/pulls")).toBeNull();
expect(internals.matchPullRequestTarget("/JSONbored/loopover/pull/146")).toEqual({
owner: "JSONbored",
repo: "loopover",
pullNumber: 146,
});
expect(internals.matchPullRequestTarget("/JSONbored/loopover/pull/146/files")).toEqual({
// Sub-path pull pages still match (previously asserted through the now-removed
// matchPullRequestTarget duplicate, #8023 -- the behavior belongs to matchGitHubPageTarget).
expect(internals.matchGitHubPageTarget("/JSONbored/loopover/pull/146/files")).toEqual({
kind: "pull_request",
owner: "JSONbored",
repo: "loopover",
pullNumber: 146,
});
expect(internals.matchPullRequestTarget("/JSONbored/loopover/issues/146")).toBeNull();
expect(internals.matchPullRequestTarget("/JSONbored/loopover")).toBeNull();
expect(internals.matchGitHubPageTarget("/JSONbored/loopover")).toBeNull();
});

// Extension ↔ backend drift guard (#8023): content.js's overlay request is only useful while the
// message type it sends is one background.js actually routes, and while the backend still serves the
// pull-context endpoint that route resolves to. Anchoring both here (this suite's subject is
// content.js) also gives this suite real instrumented-source coverage, which the scoped CI shard's
// non-empty-lcov verification requires of every selected test file.
it("sends a message type background.js routes, backed by a live pull-context endpoint in the API contract", () => {
expect(contentScript).toContain('type: "loopover:pull-context"');
const backgroundScript = readFileSync("apps/loopover-extension/background.js", "utf8");
expect(backgroundScript).toContain('"loopover:pull-context"');
expect(buildOpenApiSpec().paths["/v1/extension/pull-context"]).toBeDefined();
});

it("renders private pull-context sections and escapes API text", () => {
Expand Down Expand Up @@ -134,7 +144,6 @@ function loadContentInternals(overrides: Record<string, unknown> = {}) {
matchGitHubPageTarget: (
pathname: string,
) => { kind: "pull_request"; owner: string; repo: string; pullNumber: number } | null;
matchPullRequestTarget: (pathname: string) => { owner: string; repo: string; pullNumber: number } | null;
createOverlayLoader: (container: { querySelector: (selector: string) => unknown }, target: unknown) => () => Promise<void>;
renderPullContext: (payload: unknown) => string;
};
Expand Down
Loading