mirror of
https://github.com/mvschwarz/openrig.git
synced 2026-11-07 22:58:42 -10:00
fix(slice-21 FR-5e): init-workspace MISSION_NOTES scaffold + CLI files.allowlist env overlay (OPR.0.3.2.21.FR-5)
Bundled fix-round per orch dispatch on velocity-qa HG-7 BLOCKING-A
(qitem-20260602044545-9de820df). Two bounded findings; one delta.
A1 — init-workspace does not auto-scaffold MISSION_NOTES.md.
QA observed: fresh `rig config init-workspace` followed by `rig
workspace doctor` returned summary {ok:6, warn:1, fail:0} because
FR-3's MISSION_NOTES auto-scaffold only fires from `rig scope mission
create`. The init-workspace code path creates getting-started via
DEFAULT_MISSIONS + workspaceScaffoldFiles, which had no
MISSION_NOTES.md entry.
Fix shape:
- packages/cli/src/commands/config-init-workspace.ts: workspaceScaffoldFiles
now emits a `missions/<mission>/MISSION_NOTES.md` entry per default
mission, reusing the FR-3 `renderMissionNotesTemplate` helper from
packages/cli/src/lib/scope/templates.ts. Same env-var-pivot pattern
(OPENRIG_MISSION_NOTES_TEMPLATE_PATH > built-in bundled template)
the FR-3 surface honors.
- Placeholder substitution: mission.dotId → {{mission_id}},
mission.title → {{mission_name}}, today's ISO date → {{created_date}}.
- workspaceScaffoldFiles is exported and shared with daemon's
/api/config/init-workspace route at routes/config.ts:64, so the
daemon-side UI Settings "init workspace" action lands the same
scaffold without separate wiring.
Tests added (4 in new test file init-workspace-mission-notes.test.ts):
- ok-path: runInitWorkspace writes missions/getting-started/MISSION_NOTES.md
with rendered placeholders (no `{{mission_id}}` etc. left in file)
- workspaceScaffoldFiles() emits one MISSION_NOTES entry per default
mission with rendered content (discriminator-flips on placeholder
presence)
- dry-run reports MISSION_NOTES.md without writing it to disk
- idempotent re-run without --force preserves operator-edited content
A2 — CLI-side OPENRIG_FILES_ALLOWLIST does not propagate to the
daemon doctor route.
QA observed: setting OPENRIG_FILES_ALLOWLIST=workspace:. in the CLI
shell and running `rig workspace doctor` produced no behavior change
because the daemon process resolves files.allowlist from ITS OWN env,
not the CLI's. The CLI's env-var was invisible to the doctor check #3.
Fix shape:
- packages/cli/src/commands/workspace.ts: the doctor action reads
process.env.OPENRIG_FILES_ALLOWLIST at request time and forwards
the raw value as body.filesAllowlistOverride. Mirrors the
existing --workspace override pattern (CLI passes; daemon route
applies with source="env"). Empty-string and unset are
indistinguishable — both omit the override (CLI uses
`typeof === "string" && length > 0` guard).
- packages/daemon/src/routes/workspace.ts: the doctor route now
parses body.filesAllowlistOverride; when present, it overrides
the SettingsStore-resolved allowlistValue + allowlistSource for
check #3 (with source="env" so the fix-hint targets the right
remediation channel). The override is bounded to check #3 — all
other checks continue to use daemon-side resolution.
Tests added (3 CLI + 2 daemon route):
- CLI: env var set → body.filesAllowlistOverride matches the env value
- CLI: env var unset → body has no filesAllowlistOverride field
- CLI: env var empty-string → body has no filesAllowlistOverride
field (discriminator-flips the length > 0 guard)
- Daemon route: body.filesAllowlistOverride="workspace:." → check
#3 fails with allowlistSource="env" + summary.fail ≥ 1.
Discriminator-flips the override branch — without it the stub
SettingsStore's healthy default would silently pass.
- Daemon route: body.filesAllowlistOverride="" → check #3 uses
SettingsStore (source="default"), proving the empty-string guard.
Symmetry note: A2 mirrors the slice-01 --workspace override pattern
(CLI passes, daemon uses if provided). The CLI does NOT push the
ConfigStore-resolved value (only env) because the workspace.root
case already has a flag and matching env is the asymmetric case A2
flagged. A broader generic --env-overlay would be scope creep.
Out-of-scope (per orch dispatch, NOT addressed in this delta):
- Symlink workspace daemon-alignment string-path comparison
(non-blocking observation; 0.3.3 follow-on if anything).
Verification:
- packages/daemon tsc EXIT=0 + workspace-doctor.test.ts + workspace-
routes.test.ts: 46/46 PASS (35 doctor + 11 routes; was 9 routes,
now 11 with 2 new A2 daemon discriminators)
- packages/cli tsc EXIT=0 + workspace-doctor-cli.test.ts + init-
workspace-mission-notes.test.ts: 19/19 PASS (15 doctor CLI + 4
new init-workspace; was 12 doctor CLI, now 15 with 3 new A2 CLI
discriminators)
- Pre-commit staged-diff grep for forbidden-attribution pattern:
ZERO matches
- Pre-commit FULL-CONTENT grep on every touched file (touch-event-
scope rule): ZERO matches
- Commit body verification grep clean per banked discipline
- git diff --cached --stat: 6 files (+269 / -6)
Branch: slice-21-fr-5-rig-workspace-doctor off LOCAL main e154a5ce.
Chain: a4b4b67d (FR-5a, CLEAR) → e5d61983 (FR-5b, BLOCKED) →
bc5ec05f (FR-5b fix-up, CLEAR) → 6b21ce3c (FR-5c, BLOCKED) →
784222d3 (FR-5c determinism fix-up, CLEAR) → 941a2e31 (FR-5d, CLEAR;
HG-7 dogfood found BLOCKING-A on this tip) → this commit (FR-5e A1+
A2 bundled fix-round). LOCAL only, NO push.
Files:
- packages/cli/src/commands/config-init-workspace.ts (+15 / -0)
- packages/cli/src/commands/workspace.ts (+13 / -1)
- packages/cli/test/init-workspace-mission-notes.test.ts (+103 new)
- packages/cli/test/workspace-doctor-cli.test.ts (+63 / -0)
- packages/daemon/src/routes/workspace.ts (+27 / -3)
- packages/daemon/test/workspace-routes.test.ts (+50 / -0)
This commit is contained in:
1 parent
941a2e3149
commit
d7910f8980
6 files changed
+269
-6
No files matched your search
@@ -19,6 +19,7 @@ import { Command } from "commander";
|
||||
import { existsSync, mkdirSync, writeFileSync } from "node:fs";
|
||||
import { join } from "node:path";
|
||||
import { ConfigStore } from "../config-store.js";
|
||||
import { renderMissionNotesTemplate } from "../lib/scope/templates.js";
|
||||
|
||||
export interface InitWorkspaceOpts {
|
||||
root?: string;
|
||||
@@ -539,6 +540,20 @@ export function workspaceScaffoldFiles(): Array<{ relPath: string; content: stri
|
||||
files.push(
|
||||
{ relPath: `missions/${mission.id}/README.md`, content: missionReadme(mission) },
|
||||
{ relPath: `missions/${mission.id}/PROGRESS.md`, content: missionProgress(mission) },
|
||||
// FR-5e A1 — reuse the FR-3 MISSION_NOTES scaffold helper so a
|
||||
// fresh `rig config init-workspace` produces a getting-started
|
||||
// mission that doctor check #7 (mission_notes_presence) marks
|
||||
// ok. Same env-var-pivot (OPENRIG_MISSION_NOTES_TEMPLATE_PATH >
|
||||
// built-in bundled template) FR-3's `rig scope mission create`
|
||||
// honors.
|
||||
{
|
||||
relPath: `missions/${mission.id}/MISSION_NOTES.md`,
|
||||
content: renderMissionNotesTemplate({
|
||||
mission_id: mission.dotId,
|
||||
mission_name: mission.title,
|
||||
created_date: new Date().toISOString().slice(0, 10),
|
||||
}).rendered,
|
||||
},
|
||||
);
|
||||
for (const slice of mission.slices) {
|
||||
files.push(
|
||||
|
||||
@@ -163,8 +163,20 @@ export function workspaceCommand(depsOverride?: WorkspaceDeps): Command {
|
||||
.action(async (opts: { workspace?: string; json?: boolean; strict?: boolean }) => {
|
||||
const deps = getDeps();
|
||||
await withClient(deps, async (client) => {
|
||||
const requestBody: { workspaceRoot?: string } = {};
|
||||
const requestBody: { workspaceRoot?: string; filesAllowlistOverride?: string } = {};
|
||||
if (opts.workspace) requestBody.workspaceRoot = path.resolve(opts.workspace);
|
||||
// FR-5e A2 — CLI-side env overlay for files.allowlist. The
|
||||
// daemon runs in its own process with its own env; an
|
||||
// operator who sets OPENRIG_FILES_ALLOWLIST in their CLI
|
||||
// shell expects the doctor result to reflect that override
|
||||
// even though the daemon's env is unchanged. We mirror the
|
||||
// --workspace override pattern: read the env at request
|
||||
// time and forward as a per-request overlay; the daemon
|
||||
// route applies it to check #3 with source="env".
|
||||
const cliAllowlistEnv = process.env.OPENRIG_FILES_ALLOWLIST;
|
||||
if (typeof cliAllowlistEnv === "string" && cliAllowlistEnv.length > 0) {
|
||||
requestBody.filesAllowlistOverride = cliAllowlistEnv;
|
||||
}
|
||||
const res = await client.post<DoctorReport>("/api/workspace/doctor", requestBody);
|
||||
if (res.status >= 400) {
|
||||
console.error(JSON.stringify(res.data, null, 2));
|
||||
|
||||
@@ -0,0 +1,103 @@
|
||||
// Slice-21 FR-5e A1 — `rig config init-workspace` scaffolds
|
||||
// MISSION_NOTES.md for each seeded mission so a fresh baseline passes
|
||||
// doctor check #7 (mission_notes_presence) without manual scope
|
||||
// scaffolding.
|
||||
//
|
||||
// QA finding: fresh `rig config init-workspace` followed by `rig
|
||||
// workspace doctor` produced summary {ok:6, warn:1, fail:0} because
|
||||
// the FR-3 auto-scaffold only fired from `rig scope mission create`,
|
||||
// not the init-workspace path.
|
||||
|
||||
import { describe, it, expect, beforeEach, afterEach } from "vitest";
|
||||
import * as fs from "node:fs";
|
||||
import * as os from "node:os";
|
||||
import * as path from "node:path";
|
||||
import {
|
||||
runInitWorkspace,
|
||||
workspaceScaffoldFiles,
|
||||
} from "../src/commands/config-init-workspace.js";
|
||||
|
||||
let dir: string;
|
||||
let configPath: string;
|
||||
|
||||
beforeEach(() => {
|
||||
dir = fs.mkdtempSync(path.join(os.tmpdir(), "fr5e-init-"));
|
||||
configPath = path.join(dir, "test-config.json");
|
||||
// Seed config with workspace.root pointing into our temp dir so
|
||||
// ConfigStore's resolveWithSource returns a known root.
|
||||
fs.writeFileSync(
|
||||
configPath,
|
||||
JSON.stringify({ workspace: { root: path.join(dir, "workspace") } }) + "\n",
|
||||
"utf-8",
|
||||
);
|
||||
});
|
||||
|
||||
afterEach(() => {
|
||||
try { fs.rmSync(dir, { recursive: true, force: true }); } catch { /* ignore */ }
|
||||
});
|
||||
|
||||
describe("FR-5e A1 — init-workspace scaffolds MISSION_NOTES.md", () => {
|
||||
it("writes missions/getting-started/MISSION_NOTES.md alongside README + PROGRESS", () => {
|
||||
const result = runInitWorkspace({ configPath });
|
||||
const notesFile = result.files.find((f) =>
|
||||
f.relPath === "missions/getting-started/MISSION_NOTES.md");
|
||||
expect(notesFile).toBeDefined();
|
||||
expect(notesFile?.created).toBe(true);
|
||||
expect(fs.existsSync(notesFile!.absPath)).toBe(true);
|
||||
const content = fs.readFileSync(notesFile!.absPath, "utf-8");
|
||||
// Discriminator: the rendered template must have placeholder
|
||||
// substitutions applied. Without applyMissionNotesPlaceholders
|
||||
// the file would still contain `{{mission_id}}` etc.
|
||||
expect(content).not.toContain("{{mission_id}}");
|
||||
expect(content).not.toContain("{{mission_name}}");
|
||||
expect(content).not.toContain("{{created_date}}");
|
||||
// Discriminator: at minimum the rendered file should mention the
|
||||
// mission's display name from DEFAULT_MISSIONS.
|
||||
expect(content).toContain("Getting Started");
|
||||
});
|
||||
|
||||
// Discriminator-flip: workspaceScaffoldFiles is the canonical
|
||||
// file-list used by both CLI init and daemon's
|
||||
// /api/config/init-workspace. Both surfaces need MISSION_NOTES so
|
||||
// the daemon-side init (UI Settings → init workspace) lands the
|
||||
// same scaffold.
|
||||
it("workspaceScaffoldFiles() emits a MISSION_NOTES.md entry per default mission", () => {
|
||||
const files = workspaceScaffoldFiles();
|
||||
const notesFiles = files.filter((f) => f.relPath.endsWith("/MISSION_NOTES.md"));
|
||||
// DEFAULT_MISSIONS has one mission (getting-started). When the
|
||||
// seed list grows, this discriminator catches the count drift.
|
||||
expect(notesFiles.length).toBeGreaterThanOrEqual(1);
|
||||
for (const f of notesFiles) {
|
||||
expect(f.content).not.toContain("{{mission_id}}");
|
||||
expect(f.content).not.toContain("{{mission_name}}");
|
||||
expect(f.content).not.toContain("{{created_date}}");
|
||||
}
|
||||
});
|
||||
|
||||
// Discriminator-flip: dry-run should NOT write the MISSION_NOTES
|
||||
// file to disk but should report it as "would create".
|
||||
it("dry-run reports MISSION_NOTES.md without writing", () => {
|
||||
const result = runInitWorkspace({ configPath, dryRun: true });
|
||||
const notesFile = result.files.find((f) =>
|
||||
f.relPath === "missions/getting-started/MISSION_NOTES.md");
|
||||
expect(notesFile).toBeDefined();
|
||||
expect(notesFile?.created).toBe(true);
|
||||
expect(fs.existsSync(notesFile!.absPath)).toBe(false);
|
||||
expect(result.dryRun).toBe(true);
|
||||
});
|
||||
|
||||
// Discriminator-flip: idempotent re-run without --force does NOT
|
||||
// overwrite the existing MISSION_NOTES. Operators who hand-edit the
|
||||
// notes shouldn't lose work on a second init.
|
||||
it("re-run without --force skips existing MISSION_NOTES.md", () => {
|
||||
runInitWorkspace({ configPath });
|
||||
const notesPath = path.join(dir, "workspace", "missions", "getting-started", "MISSION_NOTES.md");
|
||||
fs.writeFileSync(notesPath, "operator hand-edited content", "utf-8");
|
||||
const result = runInitWorkspace({ configPath });
|
||||
const notesFile = result.files.find((f) =>
|
||||
f.relPath === "missions/getting-started/MISSION_NOTES.md");
|
||||
expect(notesFile?.created).toBe(false);
|
||||
expect(notesFile?.skipped).toBe("exists");
|
||||
expect(fs.readFileSync(notesPath, "utf-8")).toBe("operator hand-edited content");
|
||||
});
|
||||
});
|
||||
@@ -146,6 +146,69 @@ describe("rig workspace doctor — request shape", () => {
|
||||
expect(sent.workspaceRoot.startsWith("/")).toBe(true);
|
||||
expect(sent.workspaceRoot.endsWith("/local-ws")).toBe(true);
|
||||
});
|
||||
|
||||
// GUARD/QA BLOCKING-A2 discriminator: CLI-side
|
||||
// OPENRIG_FILES_ALLOWLIST must propagate to the daemon as
|
||||
// body.filesAllowlistOverride. Without this, an operator setting
|
||||
// the env in their CLI shell sees no doctor behavior change because
|
||||
// the daemon's env-resolution is decoupled from the CLI process.
|
||||
it("forwards OPENRIG_FILES_ALLOWLIST from process.env as body.filesAllowlistOverride", async () => {
|
||||
const original = process.env.OPENRIG_FILES_ALLOWLIST;
|
||||
process.env.OPENRIG_FILES_ALLOWLIST = "workspace:.";
|
||||
const { deps, post } = makeDeps({ status: 200, data: healthyReport() });
|
||||
const out = captureStdout();
|
||||
try {
|
||||
await workspaceCommand(deps).parseAsync(["node", "rig", "doctor"]);
|
||||
} finally {
|
||||
out.restore();
|
||||
if (original === undefined) delete process.env.OPENRIG_FILES_ALLOWLIST;
|
||||
else process.env.OPENRIG_FILES_ALLOWLIST = original;
|
||||
}
|
||||
const [, body] = post.mock.calls[0]!;
|
||||
const sent = body as { workspaceRoot?: string; filesAllowlistOverride?: string };
|
||||
expect(sent.filesAllowlistOverride).toBe("workspace:.");
|
||||
});
|
||||
|
||||
// Discriminator-flip: an unset OPENRIG_FILES_ALLOWLIST must NOT
|
||||
// attach an override (the daemon should use SettingsStore as the
|
||||
// source of truth, not silently apply an empty-string).
|
||||
it("omits filesAllowlistOverride from body when OPENRIG_FILES_ALLOWLIST is unset", async () => {
|
||||
const original = process.env.OPENRIG_FILES_ALLOWLIST;
|
||||
delete process.env.OPENRIG_FILES_ALLOWLIST;
|
||||
const { deps, post } = makeDeps({ status: 200, data: healthyReport() });
|
||||
const out = captureStdout();
|
||||
try {
|
||||
await workspaceCommand(deps).parseAsync(["node", "rig", "doctor"]);
|
||||
} finally {
|
||||
out.restore();
|
||||
if (original === undefined) delete process.env.OPENRIG_FILES_ALLOWLIST;
|
||||
else process.env.OPENRIG_FILES_ALLOWLIST = original;
|
||||
}
|
||||
const [, body] = post.mock.calls[0]!;
|
||||
const sent = body as { filesAllowlistOverride?: string };
|
||||
expect(sent.filesAllowlistOverride).toBeUndefined();
|
||||
});
|
||||
|
||||
// Discriminator-flip: empty-string env value is treated as unset
|
||||
// (no override sent). Without the length > 0 check, an empty value
|
||||
// would land as filesAllowlistOverride="" and the daemon route
|
||||
// would treat that as an explicit override.
|
||||
it("treats empty-string OPENRIG_FILES_ALLOWLIST as unset (no override sent)", async () => {
|
||||
const original = process.env.OPENRIG_FILES_ALLOWLIST;
|
||||
process.env.OPENRIG_FILES_ALLOWLIST = "";
|
||||
const { deps, post } = makeDeps({ status: 200, data: healthyReport() });
|
||||
const out = captureStdout();
|
||||
try {
|
||||
await workspaceCommand(deps).parseAsync(["node", "rig", "doctor"]);
|
||||
} finally {
|
||||
out.restore();
|
||||
if (original === undefined) delete process.env.OPENRIG_FILES_ALLOWLIST;
|
||||
else process.env.OPENRIG_FILES_ALLOWLIST = original;
|
||||
}
|
||||
const [, body] = post.mock.calls[0]!;
|
||||
const sent = body as { filesAllowlistOverride?: string };
|
||||
expect(sent.filesAllowlistOverride).toBeUndefined();
|
||||
});
|
||||
});
|
||||
|
||||
describe("rig workspace doctor — output formatters", () => {
|
||||
|
||||
@@ -9,7 +9,8 @@
|
||||
// response: FrontmatterValidationReport
|
||||
//
|
||||
// POST /api/workspace/doctor (slice-21 FR-5)
|
||||
// body (all optional): { workspaceRoot?: string }
|
||||
// body (all optional): { workspaceRoot?: string;
|
||||
// filesAllowlistOverride?: string }
|
||||
// response: DoctorReport (7-check workspace-readiness report)
|
||||
//
|
||||
// No filesystem mutation. Operator picks the root + kind per invocation.
|
||||
@@ -85,8 +86,11 @@ export function workspaceRoutes(): Hono {
|
||||
const store = c.get("settingsStore" as never) as SettingsStore | undefined;
|
||||
if (!store) return c.json({ error: "settings_unavailable" }, 503);
|
||||
|
||||
const body = await c.req.json<{ workspaceRoot?: string }>().catch(
|
||||
() => ({} as { workspaceRoot?: string }),
|
||||
const body = await c.req.json<{
|
||||
workspaceRoot?: string;
|
||||
filesAllowlistOverride?: string;
|
||||
}>().catch(
|
||||
() => ({} as { workspaceRoot?: string; filesAllowlistOverride?: string }),
|
||||
);
|
||||
|
||||
try {
|
||||
@@ -113,7 +117,23 @@ export function workspaceRoutes(): Hono {
|
||||
? path.join(body.workspaceRoot, "missions")
|
||||
: (store.resolveOne("workspace.slices_root").value as string);
|
||||
|
||||
// FR-5e A2 — files.allowlist CLI-side env overlay. When the
|
||||
// CLI sets OPENRIG_FILES_ALLOWLIST in its own shell, the
|
||||
// daemon's SettingsStore can't observe that env (different
|
||||
// process); the CLI forwards the raw value as
|
||||
// body.filesAllowlistOverride and we honor it here with
|
||||
// source="env" so check #3's fix-hint targets the right
|
||||
// remediation channel.
|
||||
const allowlistResolved = store.resolveOne("files.allowlist");
|
||||
const usingAllowlistOverride =
|
||||
typeof body.filesAllowlistOverride === "string"
|
||||
&& body.filesAllowlistOverride.length > 0;
|
||||
const allowlistValue = usingAllowlistOverride
|
||||
? body.filesAllowlistOverride!
|
||||
: (allowlistResolved.value as string);
|
||||
const allowlistSource: WorkspaceRootSource = usingAllowlistOverride
|
||||
? "env"
|
||||
: (allowlistResolved.source as WorkspaceRootSource);
|
||||
|
||||
// Daemon start time captured from process.uptime() at request
|
||||
// time. Per banked discipline (cited in workspace-doctor.ts:
|
||||
@@ -126,8 +146,8 @@ export function workspaceRoutes(): Hono {
|
||||
workspaceRoot: workspaceUnderCheck,
|
||||
workspaceRootSource,
|
||||
slicesRoot,
|
||||
allowlistValue: allowlistResolved.value as string,
|
||||
allowlistSource: allowlistResolved.source as WorkspaceRootSource,
|
||||
allowlistValue,
|
||||
allowlistSource,
|
||||
daemonResolvedWorkspaceRoot: daemonResolved.value as string,
|
||||
configFilePath: store.configPath,
|
||||
daemonStartTime,
|
||||
|
||||
@@ -212,6 +212,56 @@ describe("workspace doctor HTTP route (slice-21 FR-5)", () => {
|
||||
expect(body.workspaceRoot).toBe(doctorDir);
|
||||
});
|
||||
|
||||
// GUARD/QA BLOCKING-A2 discriminator: when the CLI forwards an
|
||||
// OPENRIG_FILES_ALLOWLIST overlay via body.filesAllowlistOverride,
|
||||
// the daemon route MUST use that value for check #3 instead of the
|
||||
// daemon's own SettingsStore-resolved files.allowlist. Without the
|
||||
// override branch the operator's env-var would silently no-op.
|
||||
it("POST /doctor honors body.filesAllowlistOverride for check #3", async () => {
|
||||
// workspace:. is a relative path; per FR-5b BLOCKER-1 the
|
||||
// canonical decoder drops it and check #3 fails with zero usable
|
||||
// entries. The daemon SettingsStore stub returns a HEALTHY
|
||||
// allowlist (workspace:${doctorDir}); without the override
|
||||
// branch the doctor would happily report ok. With the override
|
||||
// it correctly fails.
|
||||
const res = await doctorApp.request("/api/workspace/doctor", {
|
||||
method: "POST",
|
||||
headers: { "content-type": "application/json" },
|
||||
body: JSON.stringify({ filesAllowlistOverride: "workspace:." }),
|
||||
});
|
||||
expect(res.status).toBe(200);
|
||||
const body = await res.json() as {
|
||||
checks: Array<{ check: string; status: string; evidence?: { allowlistSource?: string } }>;
|
||||
summary: { ok: number; warn: number; fail: number };
|
||||
};
|
||||
const allowlistCheck = body.checks.find((c) => c.check === "file_allowlist_sane");
|
||||
expect(allowlistCheck?.status).toBe("fail");
|
||||
// Evidence must report source="env" so the operator knows the
|
||||
// override was applied (not the daemon's own resolution).
|
||||
expect(allowlistCheck?.evidence?.allowlistSource).toBe("env");
|
||||
expect(body.summary.fail).toBeGreaterThanOrEqual(1);
|
||||
});
|
||||
|
||||
// Discriminator-flip: empty-string filesAllowlistOverride must NOT
|
||||
// suppress the daemon's own SettingsStore allowlist. Without the
|
||||
// length > 0 guard, an empty override would falsify the check.
|
||||
it("ignores empty-string filesAllowlistOverride (uses daemon's SettingsStore)", async () => {
|
||||
const res = await doctorApp.request("/api/workspace/doctor", {
|
||||
method: "POST",
|
||||
headers: { "content-type": "application/json" },
|
||||
body: JSON.stringify({ filesAllowlistOverride: "" }),
|
||||
});
|
||||
expect(res.status).toBe(200);
|
||||
const body = await res.json() as {
|
||||
checks: Array<{ check: string; status: string; evidence?: { allowlistSource?: string } }>;
|
||||
};
|
||||
const allowlistCheck = body.checks.find((c) => c.check === "file_allowlist_sane");
|
||||
expect(allowlistCheck?.status).toBe("ok");
|
||||
// The stub returns source="default" for files.allowlist; verify
|
||||
// we routed through SettingsStore, not the empty override.
|
||||
expect(allowlistCheck?.evidence?.allowlistSource).toBe("default");
|
||||
});
|
||||
|
||||
// GUARD BLOCKER-1 (qitem-20260602042720-e27ec982) determinism
|
||||
// discriminator: even under a long process.uptime() (simulating a
|
||||
// long-running Vitest worker), the healthy fixture must still
|
||||
|
||||
Reference in new issue
Block a user