feat(FN-5031): complete Step 1 — add focused core secretsEnv contract test
Fusion-Task-Id: FN-5031 Fusion-Task-Lineage: cc758f7c-32c6-4db1-bbfe-f5b2ec6e34ff
This commit is contained in:
committed by
gsxdsm
parent
7955a78780
commit
98c0f10a3d
@@ -1,222 +1,80 @@
|
||||
/**
|
||||
* `packages/core/src/__tests__/secrets-env.test.ts`
|
||||
*
|
||||
* Covers the core-side contract for `SecretsEnvSettings`:
|
||||
* - type shape and all fields
|
||||
* - deprecated `SecretsEnvConfig` alias
|
||||
* - `secretsEnv: undefined` schema default
|
||||
* - project-settings round-trip via the public store API
|
||||
*
|
||||
* Materialization implementation (write/cleanup/fingerprint/overwrite-policy)
|
||||
* lives in `@fusion/engine` and is tested there:
|
||||
* - `packages/engine/src/__tests__/secrets-env-writer.test.ts`
|
||||
* - `packages/engine/src/__tests__/worktree-acquisition-secrets-env.test.ts`
|
||||
* - `packages/engine/src/__tests__/worktree-pool-secrets-env-cleanup.test.ts`
|
||||
* - `packages/engine/src/__tests__/reliability-interactions/secrets-env-materialization.test.ts`
|
||||
*/
|
||||
// FN-5031: Focused core-side contract coverage for SecretsEnvSettings.
|
||||
// The materialization implementation (writeSecretsEnvFile / cleanupSecretsEnvFile,
|
||||
// fingerprint sidecar, gitignore guard, overwrite policies) lives in
|
||||
// packages/engine/src/secrets-env-writer.ts and is covered by:
|
||||
// - packages/engine/src/__tests__/secrets-env-writer.test.ts
|
||||
// - packages/engine/src/__tests__/worktree-acquisition-secrets-env.test.ts
|
||||
// - packages/engine/src/__tests__/worktree-pool-secrets-env-cleanup.test.ts
|
||||
// - packages/engine/src/__tests__/reliability-interactions/secrets-env-materialization.test.ts
|
||||
// Do not duplicate writer/materialization assertions here.
|
||||
|
||||
import { describe, it, expect, beforeAll, beforeEach, afterEach } from "vitest";
|
||||
import { afterEach, beforeAll, beforeEach, describe, expect, it } from "vitest";
|
||||
import { DEFAULT_PROJECT_SETTINGS } from "../settings-schema.js";
|
||||
import type { ProjectSettings, SecretsEnvConfig, SecretsEnvSettings } from "../types.js";
|
||||
import { createSharedTaskStoreTestHarness } from "./store-test-helpers.js";
|
||||
|
||||
describe("SecretsEnvSettings contract", () => {
|
||||
describe("type shape", () => {
|
||||
it("exports SecretsEnvSettings interface with all expected fields", () => {
|
||||
// Type-level assertion: assign all valid shapes to prove the interface contract.
|
||||
const settings: import("../types.js").SecretsEnvSettings = {
|
||||
enabled: true,
|
||||
filename: ".fusion.env",
|
||||
overwritePolicy: "merge",
|
||||
keyPrefix: "FUSION_",
|
||||
requireGitignored: true,
|
||||
};
|
||||
|
||||
expect(settings.enabled).toBe(true);
|
||||
expect(settings.filename).toBe(".fusion.env");
|
||||
expect(settings.overwritePolicy).toBe("merge");
|
||||
expect(settings.keyPrefix).toBe("FUSION_");
|
||||
expect(settings.requireGitignored).toBe(true);
|
||||
});
|
||||
|
||||
it("each optional field is omitted-safe", () => {
|
||||
// Empty object is valid — all fields are optional.
|
||||
const settings: import("../types.js").SecretsEnvSettings = {};
|
||||
expect(settings).toBeDefined();
|
||||
});
|
||||
|
||||
it("rejects invalid overwritePolicy value at type level", () => {
|
||||
// @ts-expect-error — "invalid" is not a valid overwritePolicy
|
||||
const _settings: import("../types.js").SecretsEnvSettings = {
|
||||
overwritePolicy: "invalid",
|
||||
};
|
||||
expect(_settings).toBeDefined();
|
||||
});
|
||||
|
||||
it("filename accepts a simple local name", () => {
|
||||
// Core stores whatever the caller writes; engine validates filename constraints
|
||||
// (no path separators, "..", or null bytes) at materialization time.
|
||||
const settings: import("../types.js").SecretsEnvSettings = {
|
||||
filename: ".env.local",
|
||||
};
|
||||
expect(settings.filename).toBe(".env.local");
|
||||
});
|
||||
|
||||
it("keyPrefix is an arbitrary string filter", () => {
|
||||
const settings: import("../types.js").SecretsEnvSettings = {
|
||||
keyPrefix: "MY_PREFIX_",
|
||||
};
|
||||
expect(settings.keyPrefix).toBe("MY_PREFIX_");
|
||||
// Empty string is valid (no filtering applied)
|
||||
const noFilter: import("../types.js").SecretsEnvSettings = { keyPrefix: "" };
|
||||
expect(noFilter.keyPrefix).toBe("");
|
||||
});
|
||||
it("keeps the deprecated SecretsEnvConfig alias assignable to SecretsEnvSettings", () => {
|
||||
const _alias: SecretsEnvSettings = {} as SecretsEnvConfig;
|
||||
expect(_alias).toBeDefined();
|
||||
});
|
||||
|
||||
describe("deprecated alias", () => {
|
||||
it("SecretsEnvConfig is assignable to SecretsEnvSettings", () => {
|
||||
// Type-level: the alias is assignable to the interface.
|
||||
const config: import("../types.js").SecretsEnvConfig = {
|
||||
enabled: false,
|
||||
filename: ".env",
|
||||
overwritePolicy: "skip",
|
||||
};
|
||||
const settings: import("../types.js").SecretsEnvSettings = config;
|
||||
expect(settings.enabled).toBe(false);
|
||||
expect(settings.overwritePolicy).toBe("skip");
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
describe("secretsEnv schema default", () => {
|
||||
it("defaults to undefined when not specified", () => {
|
||||
expect(DEFAULT_PROJECT_SETTINGS.secretsEnv).toBeUndefined();
|
||||
});
|
||||
|
||||
it("DEFAULT_PROJECT_SETTINGS is a plain object with no runtime crashes", () => {
|
||||
expect(DEFAULT_PROJECT_SETTINGS).toBeDefined();
|
||||
expect(typeof DEFAULT_PROJECT_SETTINGS).toBe("object");
|
||||
});
|
||||
});
|
||||
|
||||
describe("project-settings round-trip via store API", () => {
|
||||
const harness = createSharedTaskStoreTestHarness();
|
||||
|
||||
beforeAll(harness.beforeAll);
|
||||
beforeEach(harness.beforeEach);
|
||||
afterEach(harness.afterEach);
|
||||
|
||||
it("round-trips secretsEnv with all fields via updateSettings/getSettings", async () => {
|
||||
await harness.store().updateSettings({
|
||||
secretsEnv: {
|
||||
enabled: true,
|
||||
filename: ".fusion.env",
|
||||
overwritePolicy: "merge",
|
||||
keyPrefix: "FUSION_",
|
||||
requireGitignored: true,
|
||||
},
|
||||
});
|
||||
|
||||
const settings = await harness.store().getSettings();
|
||||
expect(settings.secretsEnv).toEqual({
|
||||
it("accepts a fully populated structural object with documented overwrite policies", () => {
|
||||
const merged: SecretsEnvSettings = {
|
||||
enabled: true,
|
||||
filename: ".fusion.env",
|
||||
filename: ".env.fusion",
|
||||
overwritePolicy: "merge",
|
||||
keyPrefix: "FUSION_",
|
||||
requireGitignored: true,
|
||||
});
|
||||
};
|
||||
|
||||
const skipped: SecretsEnvSettings = { overwritePolicy: "skip" };
|
||||
const replaced: SecretsEnvSettings = { overwritePolicy: "replace" };
|
||||
|
||||
expect(merged.overwritePolicy).toBe("merge");
|
||||
expect(skipped.overwritePolicy).toBe("skip");
|
||||
expect(replaced.overwritePolicy).toBe("replace");
|
||||
});
|
||||
|
||||
it("round-trips partial secretsEnv (only enabled)", async () => {
|
||||
await harness.store().updateSettings({ secretsEnv: { enabled: false } });
|
||||
|
||||
const settings = await harness.store().getSettings();
|
||||
expect(settings.secretsEnv).toEqual({ enabled: false });
|
||||
// Omitted fields remain undefined
|
||||
expect(settings.secretsEnv?.filename).toBeUndefined();
|
||||
expect(settings.secretsEnv?.overwritePolicy).toBeUndefined();
|
||||
it("defaults secretsEnv to undefined in project settings schema", () => {
|
||||
// Undefined default means env materialization is disabled unless a project opts in,
|
||||
// consistent with SecretsEnvSettings.enabled defaulting to false.
|
||||
expect(DEFAULT_PROJECT_SETTINGS.secretsEnv).toBeUndefined();
|
||||
});
|
||||
|
||||
it("round-trips overwritePolicy=replace", async () => {
|
||||
await harness.store().updateSettings({
|
||||
secretsEnv: { enabled: true, overwritePolicy: "replace" },
|
||||
});
|
||||
const settings = await harness.store().getSettings();
|
||||
expect(settings.secretsEnv?.overwritePolicy).toBe("replace");
|
||||
it("allows ProjectSettings.secretsEnv to be either populated or undefined", () => {
|
||||
const populated: Pick<ProjectSettings, "secretsEnv">["secretsEnv"] = {
|
||||
enabled: true,
|
||||
filename: ".env.fusion",
|
||||
overwritePolicy: "replace",
|
||||
keyPrefix: "APP_",
|
||||
requireGitignored: false,
|
||||
};
|
||||
const unset: Pick<ProjectSettings, "secretsEnv">["secretsEnv"] = undefined;
|
||||
|
||||
expect(populated?.filename).toBe(".env.fusion");
|
||||
expect(unset).toBeUndefined();
|
||||
});
|
||||
|
||||
it("round-trips overwritePolicy=skip", async () => {
|
||||
await harness.store().updateSettings({
|
||||
secretsEnv: { enabled: true, overwritePolicy: "skip" },
|
||||
});
|
||||
const settings = await harness.store().getSettings();
|
||||
expect(settings.secretsEnv?.overwritePolicy).toBe("skip");
|
||||
});
|
||||
describe("project round-trip via public store API", () => {
|
||||
const harness = createSharedTaskStoreTestHarness();
|
||||
|
||||
it("clears secretsEnv when set to undefined", async () => {
|
||||
await harness.store().updateSettings({ secretsEnv: { enabled: true } });
|
||||
await harness.store().updateSettings({ secretsEnv: undefined });
|
||||
const settings = await harness.store().getSettings();
|
||||
expect(settings.secretsEnv).toBeUndefined();
|
||||
});
|
||||
beforeAll(harness.beforeAll);
|
||||
beforeEach(harness.beforeEach);
|
||||
afterEach(harness.afterEach);
|
||||
|
||||
it("stores requireGitignored=false alongside other fields", async () => {
|
||||
await harness.store().updateSettings({
|
||||
secretsEnv: { enabled: true, requireGitignored: false },
|
||||
});
|
||||
const settings = await harness.store().getSettings();
|
||||
expect(settings.secretsEnv?.requireGitignored).toBe(false);
|
||||
});
|
||||
|
||||
it("stores keyPrefix alongside other fields", async () => {
|
||||
await harness.store().updateSettings({
|
||||
secretsEnv: { enabled: true, keyPrefix: "STRIPE_" },
|
||||
});
|
||||
const settings = await harness.store().getSettings();
|
||||
expect(settings.secretsEnv?.keyPrefix).toBe("STRIPE_");
|
||||
});
|
||||
|
||||
it("stores secretsSyncPassphrase alongside secretsEnv", async () => {
|
||||
await harness.store().updateSettings({
|
||||
secretsEnv: { enabled: true },
|
||||
secretsSyncPassphrase: "encrypted:xyz789",
|
||||
});
|
||||
const settings = await harness.store().getSettings();
|
||||
expect(settings.secretsEnv?.enabled).toBe(true);
|
||||
expect(settings.secretsSyncPassphrase).toBe("encrypted:xyz789");
|
||||
});
|
||||
});
|
||||
|
||||
describe("SecretsEnvSettings in ProjectSettings", () => {
|
||||
it("ProjectSettings.secretsEnv field accepts SecretsEnvSettings", () => {
|
||||
const projectSettings: import("../types.js").ProjectSettings = {
|
||||
maxConcurrent: 4,
|
||||
maxWorktrees: 3,
|
||||
pollIntervalMs: 30_000,
|
||||
autoMerge: true,
|
||||
groupOverlappingFiles: true,
|
||||
secretsEnv: {
|
||||
enabled: false,
|
||||
filename: ".secrets",
|
||||
overwritePolicy: "skip",
|
||||
it("round-trips secretsEnv with all fields", async () => {
|
||||
const expected: SecretsEnvSettings = {
|
||||
enabled: true,
|
||||
filename: ".env.fusion",
|
||||
overwritePolicy: "replace",
|
||||
keyPrefix: "APP_",
|
||||
requireGitignored: false,
|
||||
},
|
||||
};
|
||||
expect(projectSettings.secretsEnv?.enabled).toBe(false);
|
||||
expect(projectSettings.secretsEnv?.filename).toBe(".secrets");
|
||||
expect(projectSettings.secretsEnv?.overwritePolicy).toBe("skip");
|
||||
expect(projectSettings.secretsEnv?.keyPrefix).toBe("APP_");
|
||||
expect(projectSettings.secretsEnv?.requireGitignored).toBe(false);
|
||||
});
|
||||
};
|
||||
|
||||
it("ProjectSettings.secretsEnv is optional", () => {
|
||||
const projectSettings: import("../types.js").ProjectSettings = {
|
||||
maxConcurrent: 4,
|
||||
maxWorktrees: 3,
|
||||
pollIntervalMs: 30_000,
|
||||
autoMerge: true,
|
||||
groupOverlappingFiles: true,
|
||||
};
|
||||
expect(projectSettings.secretsEnv).toBeUndefined();
|
||||
await harness.store().updateSettings({ secretsEnv: expected });
|
||||
const settings = await harness.store().getSettings();
|
||||
expect(settings.secretsEnv).toEqual(expected);
|
||||
});
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
Reference in New Issue
Block a user