mirror of
https://github.com/snapotter-hq/SnapOtter.git
synced 2026-08-03 07:46:42 +02:00
Merge branch 'worktree-test+suite-overhaul-and-real-fixtures' into chore/consolidate-v2.0.0
# Conflicts: # tests/integration/generated/settings-matrix.test.ts # tests/integration/platform/api.test.ts # tests/integration/platform/concurrent.test.ts # tests/integration/platform/factory-multi-input.test.ts # tests/integration/security/adversarial-comprehensive.test.ts # tests/integration/security/adversarial-coverage-gaps.test.ts # tests/integration/security/adversarial-extended.test.ts # tests/integration/security/adversarial-final-gaps.test.ts # tests/integration/security/adversarial-matrix.test.ts # tests/integration/security/adversarial-security.test.ts # tests/integration/security/adversarial.test.ts # tests/integration/tools/image/color-adjustments.test.ts
This commit is contained in:
@@ -0,0 +1,183 @@
|
||||
import type { ConsentState } from "@snapotter/shared";
|
||||
import { isConsentEnabled, shouldShowConsent } from "@snapotter/shared";
|
||||
import { describe, expect, it } from "vitest";
|
||||
|
||||
const SEVEN_DAYS_MS = 7 * 24 * 60 * 60 * 1000;
|
||||
|
||||
describe("shouldShowConsent edge cases", () => {
|
||||
it("returns true when remindAt is exactly equal to Date.now()", () => {
|
||||
const now = Date.now();
|
||||
const state: ConsentState = {
|
||||
analyticsEnabled: null,
|
||||
analyticsConsentShownAt: now - SEVEN_DAYS_MS,
|
||||
analyticsConsentRemindAt: now,
|
||||
};
|
||||
// Date.now() >= remindAt should be true when they are equal
|
||||
expect(shouldShowConsent(state, true)).toBe(true);
|
||||
});
|
||||
|
||||
it("returns true when remindAt is set but consentShownAt is null (defensive)", () => {
|
||||
const state: ConsentState = {
|
||||
analyticsEnabled: null,
|
||||
analyticsConsentShownAt: null,
|
||||
analyticsConsentRemindAt: Date.now() - 1000,
|
||||
};
|
||||
// consentShownAt is null -> returns true (fresh user path)
|
||||
expect(shouldShowConsent(state, true)).toBe(true);
|
||||
});
|
||||
|
||||
it("returns false when remindAt is 1ms in the future", () => {
|
||||
const now = Date.now();
|
||||
const state: ConsentState = {
|
||||
analyticsEnabled: null,
|
||||
analyticsConsentShownAt: now - SEVEN_DAYS_MS,
|
||||
analyticsConsentRemindAt: now + 100000, // safely in the future
|
||||
};
|
||||
expect(shouldShowConsent(state, true)).toBe(false);
|
||||
});
|
||||
|
||||
it("returns true after 'Maybe later' and 7 days have passed", () => {
|
||||
const shownAt = Date.now() - SEVEN_DAYS_MS - 1000;
|
||||
const remindAt = Date.now() - 1000; // remind time has passed
|
||||
const state: ConsentState = {
|
||||
analyticsEnabled: null,
|
||||
analyticsConsentShownAt: shownAt,
|
||||
analyticsConsentRemindAt: remindAt,
|
||||
};
|
||||
expect(shouldShowConsent(state, true)).toBe(true);
|
||||
});
|
||||
|
||||
it("returns false when server is disabled regardless of remindAt", () => {
|
||||
const state: ConsentState = {
|
||||
analyticsEnabled: null,
|
||||
analyticsConsentShownAt: Date.now() - SEVEN_DAYS_MS,
|
||||
analyticsConsentRemindAt: Date.now() - 1000,
|
||||
};
|
||||
expect(shouldShowConsent(state, false)).toBe(false);
|
||||
});
|
||||
});
|
||||
|
||||
describe("isConsentEnabled edge cases", () => {
|
||||
it("returns false when analyticsEnabled is false (explicitly declined)", () => {
|
||||
const state: ConsentState = {
|
||||
analyticsEnabled: false,
|
||||
analyticsConsentShownAt: Date.now(),
|
||||
analyticsConsentRemindAt: null,
|
||||
};
|
||||
expect(isConsentEnabled(state, true)).toBe(false);
|
||||
});
|
||||
|
||||
it("returns false when analyticsEnabled is null (never decided)", () => {
|
||||
const state: ConsentState = {
|
||||
analyticsEnabled: null,
|
||||
analyticsConsentShownAt: null,
|
||||
analyticsConsentRemindAt: null,
|
||||
};
|
||||
expect(isConsentEnabled(state, true)).toBe(false);
|
||||
});
|
||||
|
||||
it("returns false when server disabled even if user opted in", () => {
|
||||
const state: ConsentState = {
|
||||
analyticsEnabled: true,
|
||||
analyticsConsentShownAt: Date.now(),
|
||||
analyticsConsentRemindAt: null,
|
||||
};
|
||||
expect(isConsentEnabled(state, false)).toBe(false);
|
||||
});
|
||||
});
|
||||
|
||||
describe("consent lifecycle simulations", () => {
|
||||
it("fresh -> maybe later -> remind time passes -> show again -> accept", () => {
|
||||
// Step 1: Fresh user -- never been asked
|
||||
const fresh: ConsentState = {
|
||||
analyticsEnabled: null,
|
||||
analyticsConsentShownAt: null,
|
||||
analyticsConsentRemindAt: null,
|
||||
};
|
||||
expect(shouldShowConsent(fresh, true)).toBe(true);
|
||||
expect(isConsentEnabled(fresh, true)).toBe(false);
|
||||
|
||||
// Step 2: User clicks "Maybe later" -- shown timestamp set, remind in 7 days
|
||||
const shownAt = Date.now() - SEVEN_DAYS_MS - 1000;
|
||||
const maybeLater: ConsentState = {
|
||||
analyticsEnabled: null,
|
||||
analyticsConsentShownAt: shownAt,
|
||||
analyticsConsentRemindAt: shownAt + SEVEN_DAYS_MS,
|
||||
};
|
||||
// Remind time has now passed (shownAt + 7days < now)
|
||||
expect(shouldShowConsent(maybeLater, true)).toBe(true);
|
||||
expect(isConsentEnabled(maybeLater, true)).toBe(false);
|
||||
|
||||
// Step 3: User accepts on second prompt
|
||||
const accepted: ConsentState = {
|
||||
analyticsEnabled: true,
|
||||
analyticsConsentShownAt: Date.now(),
|
||||
analyticsConsentRemindAt: null,
|
||||
};
|
||||
expect(shouldShowConsent(accepted, true)).toBe(false);
|
||||
expect(isConsentEnabled(accepted, true)).toBe(true);
|
||||
});
|
||||
|
||||
it("fresh -> accept immediately -> never show again", () => {
|
||||
// Step 1: Fresh user
|
||||
const fresh: ConsentState = {
|
||||
analyticsEnabled: null,
|
||||
analyticsConsentShownAt: null,
|
||||
analyticsConsentRemindAt: null,
|
||||
};
|
||||
expect(shouldShowConsent(fresh, true)).toBe(true);
|
||||
|
||||
// Step 2: User accepts immediately
|
||||
const accepted: ConsentState = {
|
||||
analyticsEnabled: true,
|
||||
analyticsConsentShownAt: Date.now(),
|
||||
analyticsConsentRemindAt: null,
|
||||
};
|
||||
expect(shouldShowConsent(accepted, true)).toBe(false);
|
||||
expect(isConsentEnabled(accepted, true)).toBe(true);
|
||||
|
||||
// Verify it stays hidden even far in the future
|
||||
expect(shouldShowConsent(accepted, true)).toBe(false);
|
||||
});
|
||||
|
||||
it("fresh -> decline immediately -> never show again", () => {
|
||||
// Step 1: Fresh user
|
||||
const fresh: ConsentState = {
|
||||
analyticsEnabled: null,
|
||||
analyticsConsentShownAt: null,
|
||||
analyticsConsentRemindAt: null,
|
||||
};
|
||||
expect(shouldShowConsent(fresh, true)).toBe(true);
|
||||
|
||||
// Step 2: User declines immediately
|
||||
const declined: ConsentState = {
|
||||
analyticsEnabled: false,
|
||||
analyticsConsentShownAt: Date.now(),
|
||||
analyticsConsentRemindAt: null,
|
||||
};
|
||||
expect(shouldShowConsent(declined, true)).toBe(false);
|
||||
expect(isConsentEnabled(declined, true)).toBe(false);
|
||||
|
||||
// Verify it stays hidden and analytics stays disabled
|
||||
expect(shouldShowConsent(declined, true)).toBe(false);
|
||||
expect(isConsentEnabled(declined, true)).toBe(false);
|
||||
});
|
||||
|
||||
it("fresh -> maybe later -> remind time NOT yet passed -> stay hidden", () => {
|
||||
const fresh: ConsentState = {
|
||||
analyticsEnabled: null,
|
||||
analyticsConsentShownAt: null,
|
||||
analyticsConsentRemindAt: null,
|
||||
};
|
||||
expect(shouldShowConsent(fresh, true)).toBe(true);
|
||||
|
||||
// User clicks maybe later, only 1 day ago
|
||||
const maybeLater: ConsentState = {
|
||||
analyticsEnabled: null,
|
||||
analyticsConsentShownAt: Date.now() - 86400000,
|
||||
analyticsConsentRemindAt: Date.now() + SEVEN_DAYS_MS - 86400000,
|
||||
};
|
||||
expect(shouldShowConsent(maybeLater, true)).toBe(false);
|
||||
expect(isConsentEnabled(maybeLater, true)).toBe(false);
|
||||
});
|
||||
});
|
||||
@@ -0,0 +1,123 @@
|
||||
import { isConsentEnabled, shouldShowConsent } from "@snapotter/shared";
|
||||
import { describe, expect, it } from "vitest";
|
||||
|
||||
describe("shouldShowConsent", () => {
|
||||
it("returns false when server has analytics disabled", () => {
|
||||
expect(
|
||||
shouldShowConsent(
|
||||
{
|
||||
analyticsEnabled: null,
|
||||
analyticsConsentShownAt: null,
|
||||
analyticsConsentRemindAt: null,
|
||||
},
|
||||
false,
|
||||
),
|
||||
).toBe(false);
|
||||
});
|
||||
|
||||
it("returns true for a fresh user who has never been asked", () => {
|
||||
expect(
|
||||
shouldShowConsent(
|
||||
{
|
||||
analyticsEnabled: null,
|
||||
analyticsConsentShownAt: null,
|
||||
analyticsConsentRemindAt: null,
|
||||
},
|
||||
true,
|
||||
),
|
||||
).toBe(true);
|
||||
});
|
||||
|
||||
it("returns false when user already opted in", () => {
|
||||
expect(
|
||||
shouldShowConsent(
|
||||
{
|
||||
analyticsEnabled: true,
|
||||
analyticsConsentShownAt: Date.now(),
|
||||
analyticsConsentRemindAt: null,
|
||||
},
|
||||
true,
|
||||
),
|
||||
).toBe(false);
|
||||
});
|
||||
|
||||
it("returns false when user explicitly declined", () => {
|
||||
expect(
|
||||
shouldShowConsent(
|
||||
{
|
||||
analyticsEnabled: false,
|
||||
analyticsConsentShownAt: Date.now(),
|
||||
analyticsConsentRemindAt: null,
|
||||
},
|
||||
true,
|
||||
),
|
||||
).toBe(false);
|
||||
});
|
||||
|
||||
it("returns false when remind-at is in the future", () => {
|
||||
expect(
|
||||
shouldShowConsent(
|
||||
{
|
||||
analyticsEnabled: null,
|
||||
analyticsConsentShownAt: Date.now() - 86400000,
|
||||
analyticsConsentRemindAt: Date.now() + 86400000,
|
||||
},
|
||||
true,
|
||||
),
|
||||
).toBe(false);
|
||||
});
|
||||
|
||||
it("returns true when remind-at has passed", () => {
|
||||
expect(
|
||||
shouldShowConsent(
|
||||
{
|
||||
analyticsEnabled: null,
|
||||
analyticsConsentShownAt: Date.now() - 86400000 * 8,
|
||||
analyticsConsentRemindAt: Date.now() - 86400000,
|
||||
},
|
||||
true,
|
||||
),
|
||||
).toBe(true);
|
||||
});
|
||||
});
|
||||
|
||||
describe("isConsentEnabled", () => {
|
||||
it("returns false when server disabled", () => {
|
||||
expect(
|
||||
isConsentEnabled(
|
||||
{
|
||||
analyticsEnabled: true,
|
||||
analyticsConsentShownAt: Date.now(),
|
||||
analyticsConsentRemindAt: null,
|
||||
},
|
||||
false,
|
||||
),
|
||||
).toBe(false);
|
||||
});
|
||||
|
||||
it("returns false when user has not consented", () => {
|
||||
expect(
|
||||
isConsentEnabled(
|
||||
{
|
||||
analyticsEnabled: null,
|
||||
analyticsConsentShownAt: null,
|
||||
analyticsConsentRemindAt: null,
|
||||
},
|
||||
true,
|
||||
),
|
||||
).toBe(false);
|
||||
});
|
||||
|
||||
it("returns true when user opted in and server enabled", () => {
|
||||
expect(
|
||||
isConsentEnabled(
|
||||
{
|
||||
analyticsEnabled: true,
|
||||
analyticsConsentShownAt: Date.now(),
|
||||
analyticsConsentRemindAt: null,
|
||||
},
|
||||
true,
|
||||
),
|
||||
).toBe(true);
|
||||
});
|
||||
});
|
||||
@@ -0,0 +1,61 @@
|
||||
import { ANALYTICS_EVENTS } from "@snapotter/shared";
|
||||
import { describe, expect, it } from "vitest";
|
||||
|
||||
describe("ANALYTICS_EVENTS", () => {
|
||||
it("has exactly 4 event keys", () => {
|
||||
expect(Object.keys(ANALYTICS_EVENTS)).toHaveLength(4);
|
||||
});
|
||||
|
||||
it("contains the expected keys", () => {
|
||||
expect(ANALYTICS_EVENTS).toHaveProperty("TOOL_USED");
|
||||
expect(ANALYTICS_EVENTS).toHaveProperty("SEARCH");
|
||||
expect(ANALYTICS_EVENTS).toHaveProperty("PIPELINE_EXECUTED");
|
||||
expect(ANALYTICS_EVENTS).toHaveProperty("AI_BUNDLE_ACTION");
|
||||
});
|
||||
|
||||
it("all event values are strings", () => {
|
||||
for (const value of Object.values(ANALYTICS_EVENTS)) {
|
||||
expect(typeof value).toBe("string");
|
||||
}
|
||||
});
|
||||
|
||||
it("TOOL_USED has the correct snake_case value", () => {
|
||||
expect(ANALYTICS_EVENTS.TOOL_USED).toBe("tool_used");
|
||||
});
|
||||
|
||||
it("SEARCH has the correct snake_case value", () => {
|
||||
expect(ANALYTICS_EVENTS.SEARCH).toBe("search");
|
||||
});
|
||||
|
||||
it("PIPELINE_EXECUTED has the correct snake_case value", () => {
|
||||
expect(ANALYTICS_EVENTS.PIPELINE_EXECUTED).toBe("pipeline_executed");
|
||||
});
|
||||
|
||||
it("AI_BUNDLE_ACTION has the correct snake_case value", () => {
|
||||
expect(ANALYTICS_EVENTS.AI_BUNDLE_ACTION).toBe("ai_bundle_action");
|
||||
});
|
||||
|
||||
it("all values follow snake_case convention", () => {
|
||||
for (const value of Object.values(ANALYTICS_EVENTS)) {
|
||||
expect(value).toMatch(/^[a-z][a-z0-9_]*$/);
|
||||
}
|
||||
});
|
||||
|
||||
it("is frozen (as const prevents mutation)", () => {
|
||||
// as const produces a readonly object; Object.isFrozen checks runtime freezing.
|
||||
// TypeScript enforces readonly at compile time, but at runtime the object
|
||||
// defined with "as const" is a plain object unless explicitly frozen.
|
||||
// We verify the values are stable by checking they haven't changed.
|
||||
const snapshot = { ...ANALYTICS_EVENTS };
|
||||
expect(ANALYTICS_EVENTS.TOOL_USED).toBe(snapshot.TOOL_USED);
|
||||
expect(ANALYTICS_EVENTS.SEARCH).toBe(snapshot.SEARCH);
|
||||
expect(ANALYTICS_EVENTS.PIPELINE_EXECUTED).toBe(snapshot.PIPELINE_EXECUTED);
|
||||
expect(ANALYTICS_EVENTS.AI_BUNDLE_ACTION).toBe(snapshot.AI_BUNDLE_ACTION);
|
||||
});
|
||||
|
||||
it("all values are unique (no duplicate event names)", () => {
|
||||
const values = Object.values(ANALYTICS_EVENTS);
|
||||
const unique = new Set(values);
|
||||
expect(unique.size).toBe(values.length);
|
||||
});
|
||||
});
|
||||
@@ -0,0 +1,129 @@
|
||||
import type { AnalyticsConfig, ConsentState } from "@snapotter/shared";
|
||||
import { describe, expect, it } from "vitest";
|
||||
|
||||
describe("AnalyticsConfig type", () => {
|
||||
it("accepts a fully populated config object", () => {
|
||||
const config: AnalyticsConfig = {
|
||||
enabled: true,
|
||||
posthogApiKey: "phc_test123",
|
||||
posthogHost: "https://us.i.posthog.com",
|
||||
sentryDsn: "https://abc@sentry.io/123",
|
||||
sampleRate: 1.0,
|
||||
instanceId: "inst-abc-123",
|
||||
};
|
||||
expect(config.enabled).toBe(true);
|
||||
expect(config.posthogApiKey).toBe("phc_test123");
|
||||
expect(config.posthogHost).toBe("https://us.i.posthog.com");
|
||||
expect(config.sentryDsn).toBe("https://abc@sentry.io/123");
|
||||
expect(config.sampleRate).toBe(1.0);
|
||||
expect(config.instanceId).toBe("inst-abc-123");
|
||||
});
|
||||
|
||||
it("accepts a config with analytics disabled", () => {
|
||||
const config: AnalyticsConfig = {
|
||||
enabled: false,
|
||||
posthogApiKey: "",
|
||||
posthogHost: "",
|
||||
sentryDsn: "",
|
||||
sampleRate: 0,
|
||||
instanceId: "",
|
||||
};
|
||||
expect(config.enabled).toBe(false);
|
||||
expect(config.sampleRate).toBe(0);
|
||||
});
|
||||
|
||||
it("accepts fractional sample rates", () => {
|
||||
const config: AnalyticsConfig = {
|
||||
enabled: true,
|
||||
posthogApiKey: "key",
|
||||
posthogHost: "https://host.com",
|
||||
sentryDsn: "https://dsn",
|
||||
sampleRate: 0.5,
|
||||
instanceId: "id",
|
||||
};
|
||||
expect(config.sampleRate).toBe(0.5);
|
||||
});
|
||||
|
||||
it("has exactly the expected keys", () => {
|
||||
const config: AnalyticsConfig = {
|
||||
enabled: true,
|
||||
posthogApiKey: "key",
|
||||
posthogHost: "host",
|
||||
sentryDsn: "dsn",
|
||||
sampleRate: 1,
|
||||
instanceId: "id",
|
||||
};
|
||||
const keys = Object.keys(config).sort();
|
||||
expect(keys).toEqual(
|
||||
["enabled", "instanceId", "posthogApiKey", "posthogHost", "sampleRate", "sentryDsn"].sort(),
|
||||
);
|
||||
});
|
||||
});
|
||||
|
||||
describe("ConsentState type", () => {
|
||||
it("accepts all-null state (fresh user)", () => {
|
||||
const state: ConsentState = {
|
||||
analyticsEnabled: null,
|
||||
analyticsConsentShownAt: null,
|
||||
analyticsConsentRemindAt: null,
|
||||
};
|
||||
expect(state.analyticsEnabled).toBeNull();
|
||||
expect(state.analyticsConsentShownAt).toBeNull();
|
||||
expect(state.analyticsConsentRemindAt).toBeNull();
|
||||
});
|
||||
|
||||
it("accepts opted-in state (analyticsEnabled = true)", () => {
|
||||
const state: ConsentState = {
|
||||
analyticsEnabled: true,
|
||||
analyticsConsentShownAt: 1713800000000,
|
||||
analyticsConsentRemindAt: null,
|
||||
};
|
||||
expect(state.analyticsEnabled).toBe(true);
|
||||
expect(state.analyticsConsentShownAt).toBe(1713800000000);
|
||||
expect(state.analyticsConsentRemindAt).toBeNull();
|
||||
});
|
||||
|
||||
it("accepts declined state (analyticsEnabled = false)", () => {
|
||||
const state: ConsentState = {
|
||||
analyticsEnabled: false,
|
||||
analyticsConsentShownAt: 1713800000000,
|
||||
analyticsConsentRemindAt: null,
|
||||
};
|
||||
expect(state.analyticsEnabled).toBe(false);
|
||||
});
|
||||
|
||||
it("accepts deferred state (maybe later with remindAt set)", () => {
|
||||
const shownAt = Date.now() - 86400000;
|
||||
const remindAt = Date.now() + 86400000 * 6;
|
||||
const state: ConsentState = {
|
||||
analyticsEnabled: null,
|
||||
analyticsConsentShownAt: shownAt,
|
||||
analyticsConsentRemindAt: remindAt,
|
||||
};
|
||||
expect(state.analyticsEnabled).toBeNull();
|
||||
expect(state.analyticsConsentShownAt).toBe(shownAt);
|
||||
expect(state.analyticsConsentRemindAt).toBe(remindAt);
|
||||
});
|
||||
|
||||
it("accepts mixed state with analyticsEnabled true and remindAt set", () => {
|
||||
const state: ConsentState = {
|
||||
analyticsEnabled: true,
|
||||
analyticsConsentShownAt: 1713800000000,
|
||||
analyticsConsentRemindAt: 1714400000000,
|
||||
};
|
||||
expect(state.analyticsEnabled).toBe(true);
|
||||
expect(state.analyticsConsentRemindAt).toBe(1714400000000);
|
||||
});
|
||||
|
||||
it("has exactly the expected keys", () => {
|
||||
const state: ConsentState = {
|
||||
analyticsEnabled: null,
|
||||
analyticsConsentShownAt: null,
|
||||
analyticsConsentRemindAt: null,
|
||||
};
|
||||
const keys = Object.keys(state).sort();
|
||||
expect(keys).toEqual(
|
||||
["analyticsConsentRemindAt", "analyticsConsentShownAt", "analyticsEnabled"].sort(),
|
||||
);
|
||||
});
|
||||
});
|
||||
@@ -0,0 +1,102 @@
|
||||
import {
|
||||
FEATURE_BUNDLES,
|
||||
getBundleForTool,
|
||||
getToolsForBundle,
|
||||
PYTHON_SIDECAR_TOOLS,
|
||||
TOOL_BUNDLE_MAP,
|
||||
} from "@snapotter/shared";
|
||||
import { describe, expect, it } from "vitest";
|
||||
|
||||
describe("Feature bundles", () => {
|
||||
it("every PYTHON_SIDECAR_TOOL maps to exactly one bundle", () => {
|
||||
for (const toolId of PYTHON_SIDECAR_TOOLS) {
|
||||
const bundle = getBundleForTool(toolId);
|
||||
expect(bundle, `${toolId} has no bundle`).toBeDefined();
|
||||
}
|
||||
});
|
||||
|
||||
it("getBundleForTool returns null for non-AI tools", () => {
|
||||
expect(getBundleForTool("resize")).toBeNull();
|
||||
expect(getBundleForTool("crop")).toBeNull();
|
||||
});
|
||||
|
||||
it("getToolsForBundle returns correct tools", () => {
|
||||
const tools = getToolsForBundle("background-removal");
|
||||
expect(tools).toContain("remove-background");
|
||||
expect(tools).toContain("passport-photo");
|
||||
expect(tools).not.toContain("upscale");
|
||||
});
|
||||
|
||||
it("all 7 bundles are defined", () => {
|
||||
expect(Object.keys(FEATURE_BUNDLES)).toHaveLength(7);
|
||||
expect(FEATURE_BUNDLES["background-removal"]).toBeDefined();
|
||||
expect(FEATURE_BUNDLES["face-detection"]).toBeDefined();
|
||||
expect(FEATURE_BUNDLES["object-eraser-colorize"]).toBeDefined();
|
||||
expect(FEATURE_BUNDLES["upscale-enhance"]).toBeDefined();
|
||||
expect(FEATURE_BUNDLES["photo-restoration"]).toBeDefined();
|
||||
expect(FEATURE_BUNDLES.ocr).toBeDefined();
|
||||
expect(FEATURE_BUNDLES.transcription).toBeDefined();
|
||||
});
|
||||
|
||||
it("TOOL_BUNDLE_MAP covers all sidecar tools", () => {
|
||||
const mappedTools = Object.keys(TOOL_BUNDLE_MAP);
|
||||
for (const toolId of PYTHON_SIDECAR_TOOLS) {
|
||||
expect(mappedTools, `${toolId} missing from TOOL_BUNDLE_MAP`).toContain(toolId);
|
||||
}
|
||||
});
|
||||
});
|
||||
|
||||
describe("Feature bundle edge cases", () => {
|
||||
it("no duplicate tools across bundles", () => {
|
||||
const allTools: string[] = [];
|
||||
for (const bundle of Object.values(FEATURE_BUNDLES)) {
|
||||
for (const tool of bundle.enablesTools) {
|
||||
expect(allTools, `Tool ${tool} appears in multiple bundles`).not.toContain(tool);
|
||||
allTools.push(tool);
|
||||
}
|
||||
}
|
||||
});
|
||||
|
||||
it("every bundle has a non-empty estimated size", () => {
|
||||
for (const bundle of Object.values(FEATURE_BUNDLES)) {
|
||||
expect(bundle.estimatedSize.length).toBeGreaterThan(0);
|
||||
}
|
||||
});
|
||||
|
||||
it("getToolsForBundle returns empty array for unknown bundle", () => {
|
||||
expect(getToolsForBundle("nonexistent")).toEqual([]);
|
||||
});
|
||||
|
||||
it("getBundleForTool returns null for unknown tool", () => {
|
||||
expect(getBundleForTool("nonexistent-tool")).toBeNull();
|
||||
});
|
||||
|
||||
it("TOOL_BUNDLE_MAP has no undefined values", () => {
|
||||
for (const [tool, bundle] of Object.entries(TOOL_BUNDLE_MAP)) {
|
||||
expect(bundle, `Tool ${tool} has undefined bundle`).toBeDefined();
|
||||
expect(
|
||||
FEATURE_BUNDLES[bundle],
|
||||
`Bundle ${bundle} for tool ${tool} not in FEATURE_BUNDLES`,
|
||||
).toBeDefined();
|
||||
}
|
||||
});
|
||||
|
||||
it("every bundle id matches its key in FEATURE_BUNDLES", () => {
|
||||
for (const [key, bundle] of Object.entries(FEATURE_BUNDLES)) {
|
||||
expect(bundle.id).toBe(key);
|
||||
}
|
||||
});
|
||||
|
||||
it("every bundle has a non-empty name and description", () => {
|
||||
for (const bundle of Object.values(FEATURE_BUNDLES)) {
|
||||
expect(bundle.name.length).toBeGreaterThan(0);
|
||||
expect(bundle.description.length).toBeGreaterThan(0);
|
||||
}
|
||||
});
|
||||
|
||||
it("every bundle has at least one tool", () => {
|
||||
for (const [id, bundle] of Object.entries(FEATURE_BUNDLES)) {
|
||||
expect(bundle.enablesTools.length, `Bundle ${id} has no tools`).toBeGreaterThan(0);
|
||||
}
|
||||
});
|
||||
});
|
||||
@@ -0,0 +1,54 @@
|
||||
import { describe, expect, it } from "vitest";
|
||||
import { pairwise } from "../../helpers/pairwise.js";
|
||||
|
||||
describe("pairwise covering-array generator", () => {
|
||||
it("covers every pair of values across all axis pairs", () => {
|
||||
const axes = [
|
||||
{ key: "fit", values: ["contain", "cover", "fill", "inside"] },
|
||||
{ key: "format", values: ["png", "jpeg", "webp"] },
|
||||
{ key: "withMetadata", values: [true, false] },
|
||||
{ key: "quality", values: [1, 50, 100] },
|
||||
];
|
||||
const cases = pairwise(axes);
|
||||
|
||||
for (let i = 0; i < axes.length; i++) {
|
||||
for (let j = i + 1; j < axes.length; j++) {
|
||||
for (const vi of axes[i].values) {
|
||||
for (const vj of axes[j].values) {
|
||||
const covered = cases.some((c) => c[axes[i].key] === vi && c[axes[j].key] === vj);
|
||||
expect(covered, `pair ${axes[i].key}=${vi} x ${axes[j].key}=${vj} not covered`).toBe(
|
||||
true,
|
||||
);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
});
|
||||
|
||||
it("produces far fewer cases than the full cartesian product", () => {
|
||||
const axes = [
|
||||
{ key: "a", values: [1, 2, 3, 4] },
|
||||
{ key: "b", values: [1, 2, 3] },
|
||||
{ key: "c", values: [true, false] },
|
||||
{ key: "d", values: ["x", "y", "z"] },
|
||||
];
|
||||
const cases = pairwise(axes);
|
||||
// Cartesian product is 72; pairwise needs at least 12 (largest axis pair).
|
||||
expect(cases.length).toBeGreaterThanOrEqual(12);
|
||||
expect(cases.length).toBeLessThan(30);
|
||||
});
|
||||
|
||||
it("is deterministic", () => {
|
||||
const axes = [
|
||||
{ key: "a", values: [1, 2, 3] },
|
||||
{ key: "b", values: ["x", "y"] },
|
||||
{ key: "c", values: [true, false] },
|
||||
];
|
||||
expect(pairwise(axes)).toEqual(pairwise(axes));
|
||||
});
|
||||
|
||||
it("handles degenerate inputs", () => {
|
||||
expect(pairwise([])).toEqual([]);
|
||||
expect(pairwise([{ key: "only", values: [1, 2] }])).toEqual([{ only: 1 }, { only: 2 }]);
|
||||
});
|
||||
});
|
||||
Reference in New Issue
Block a user