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
5 changes: 4 additions & 1 deletion internal-packages/billing/src/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -103,8 +103,11 @@ export class LazyController implements BillingController {
const code = (error as NodeJS.ErrnoException | undefined)?.code;
const message = error instanceof Error ? error.message : String(error);
const isModuleNotFound = code === "ERR_MODULE_NOT_FOUND" || code === "MODULE_NOT_FOUND";
const isPluginItselfMissing =
isModuleNotFound &&
(message.includes(MODULE_NAME) || message.includes("'@triggerdotdev/plugins'"));

if (!isModuleNotFound || !message.includes(MODULE_NAME)) {
if (!isPluginItselfMissing) {
console.error(
"Billing: plugin found but failed to load; falling back to default implementation",
error
Expand Down
34 changes: 26 additions & 8 deletions internal-packages/billing/src/loader.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -130,16 +130,34 @@ describe("billing loader", () => {
});

it("falls back when the plugin is not installed", async () => {
const controller = new LazyController({
importer: async () => {
throw moduleNotFound("@triggerdotdev/plugins/billing");
},
});
const consoleError = vi.spyOn(console, "error").mockImplementation(() => {});
try {
const controller = new LazyController({
importer: async () => {
throw moduleNotFound("@triggerdotdev/plugins/billing");
},
});

await expect(controller.isUsingPlugin()).resolves.toBe(false);
await expect(controller.isUsingPlugin()).resolves.toBe(false);

const result = await controller.provisionCustomer({ organizationId: "org_1", name: "Acme" });
expect(result._unsafeUnwrapErr()).toBe("feature_disabled");
const result = await controller.provisionCustomer({ organizationId: "org_1", name: "Acme" });
expect(result._unsafeUnwrapErr()).toBe("feature_disabled");
expect(consoleError).not.toHaveBeenCalled();

const barePkgController = new LazyController({
importer: async () => {
const error: NodeJS.ErrnoException = new Error(
"Cannot find package '@triggerdotdev/plugins' imported from /triggerdotdev/apps/webapp/build/server/index.js"
);
error.code = "ERR_MODULE_NOT_FOUND";
throw error;
},
});
await expect(barePkgController.isUsingPlugin()).resolves.toBe(false);
expect(consoleError).not.toHaveBeenCalled();
} finally {
consoleError.mockRestore();
}
});

it("falls back loudly when the plugin is present but fails to initialise", async () => {
Expand Down
4 changes: 3 additions & 1 deletion internal-packages/rbac/src/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -194,7 +194,9 @@ class LazyController implements RoleBaseAccessController {
const code = (err as NodeJS.ErrnoException | undefined)?.code;
const message = err instanceof Error ? err.message : String(err);
const isModuleNotFound = code === "ERR_MODULE_NOT_FOUND" || code === "MODULE_NOT_FOUND";
const isPluginItselfMissing = isModuleNotFound && message.includes(moduleName);
const isPluginItselfMissing =
isModuleNotFound &&
(message.includes(moduleName) || message.includes("'@triggerdotdev/plugins'"));

if (!isPluginItselfMissing) {
// Either the error wasn't a missing-module error at all, or the
Expand Down
11 changes: 9 additions & 2 deletions internal-packages/rbac/src/require-plugins.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -20,8 +20,15 @@ describe("LazyController plugin loading", () => {

it("falls back silently when REQUIRE_PLUGINS is unset and the plugin is missing", async () => {
vi.stubEnv("REQUIRE_PLUGINS", "");
const controller = loader.create(prismaPlaceholder);
await expect(controller.isUsingPlugin()).resolves.toBe(false);
vi.stubEnv("RBAC_LOG_FALLBACK", "");
const errorSpy = vi.spyOn(console, "error").mockImplementation(() => {});
try {
const controller = loader.create(prismaPlaceholder);
await expect(controller.isUsingPlugin()).resolves.toBe(false);
expect(errorSpy).not.toHaveBeenCalled();
} finally {
errorSpy.mockRestore();
}
});

it("throws when REQUIRE_PLUGINS=1 and the plugin is missing", async () => {
Expand Down
4 changes: 3 additions & 1 deletion internal-packages/sso/src/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -82,7 +82,9 @@ export class LazyController implements SsoController {
const code = (err as NodeJS.ErrnoException | undefined)?.code;
const message = err instanceof Error ? err.message : String(err);
const isModuleNotFound = code === "ERR_MODULE_NOT_FOUND" || code === "MODULE_NOT_FOUND";
const isPluginItselfMissing = isModuleNotFound && message.includes(moduleName);
const isPluginItselfMissing =
isModuleNotFound &&
(message.includes(moduleName) || message.includes("'@triggerdotdev/plugins'"));

if (!isPluginItselfMissing) {
console.error(
Expand Down
25 changes: 25 additions & 0 deletions internal-packages/sso/src/loader.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -167,6 +167,31 @@ describe("SSO LazyController", () => {
}
});

it("stays silent when Node ESM reports the missing bare package '@triggerdotdev/plugins'", async () => {

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🟡 New SSO test lacks regression annotation

The new SSO test omits the required // Catches: <bug> comment identifying its distinct regression.

Suggested change
it("stays silent when Node ESM reports the missing bare package '@triggerdotdev/plugins'", async () => {
// Catches: Missing bare plugin packages incorrectly produce an error-level load failure.
it("stays silent when Node ESM reports the missing bare package '@triggerdotdev/plugins'", async () => {

Devin Review


Was this helpful? React with 👍 or 👎 to provide feedback.

const previous = process.env.SSO_LOG_FALLBACK;
delete process.env.SSO_LOG_FALLBACK;
const importer = vi.fn(async () => {
const err = Object.assign(
new Error(
"Cannot find package '@triggerdotdev/plugins' imported from /triggerdotdev/apps/webapp/build/server/index.js"
),
{ code: "ERR_MODULE_NOT_FOUND" }
);
throw err;
});
const errorSpy = vi.spyOn(console, "error").mockImplementation(() => undefined);
Comment on lines +173 to +182

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🟡 New loader tests use forbidden test doubles

The new SSO test uses vi.fn and vi.spyOn instead of exercising a real import. The billing fallback test and RBAC fallback test also add spies, violating the repository's no-mocks rule.

Learn more

The repository's testing rules prohibit mocks, spies, stubs, and fakes, including vi.fn and vi.spyOn. The new SSO case adds both, while the modified billing and RBAC cases add console spies. These cases test the exception classifier with constructed errors rather than observing a real missing-package import.

Example: With the optional package absent, a real dynamic import rejects with Node's own error; these tests instead throw a hand-written string through a fake importer.

Recommended fix: Exercise the real missing-package import in the loader tests. For log assertions, use an integration-level log capture that does not replace or spy on console.error, or restructure the tested behavior to return an observable result.

Devin Review


Was this helpful? React with 👍 or 👎 to provide feedback.


try {
const controller = new LazyController(fakePrisma, { importer });
expect(await controller.isUsingPlugin()).toBe(false);
expect(errorSpy).not.toHaveBeenCalled();
} finally {
errorSpy.mockRestore();
if (previous === undefined) delete process.env.SSO_LOG_FALLBACK;
else process.env.SSO_LOG_FALLBACK = previous;
}
});

it("logs an info line when SSO_LOG_FALLBACK=1", async () => {
const previous = process.env.SSO_LOG_FALLBACK;
process.env.SSO_LOG_FALLBACK = "1";
Expand Down