Skip to content
Merged
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
34 changes: 34 additions & 0 deletions packages/deepsec/src/__tests__/setup-coordinator.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -13,6 +13,19 @@ import {
setupErrorExitCode,
} from "../setup/protocol.js";

// Stub the osv enrichment so the coordinator path can be exercised without the
// external osv-scanner/uv/syft toolchain. `runOsvEnrich` returns a sentinel
// "directive" context (what buildContext now produces); mergeInfoWithOsvContext
// stays real so the non-osv preserve path still behaves exactly as in production.
const OSV_DIRECTIVE_SENTINEL = "### Dependency reachability (required) — DIRECTIVE SENTINEL";
vi.mock("../setup/osv-enrich.js", async (importActual) => {
const actual = await importActual<typeof import("../setup/osv-enrich.js")>();
return {
...actual,
runOsvEnrich: vi.fn(() => ({ context: OSV_DIRECTIVE_SENTINEL, hits: [] })),
};
});

const originalCwd = process.cwd();
afterEach(() => process.chdir(originalCwd));

Expand Down Expand Up @@ -245,6 +258,27 @@ describe("one-shot setup coordinator", () => {
expect(fs.readFileSync(path.join(workspace, "data", "app", "INFO.md"), "utf8")).toBe(
curatedInfo,
);

// Enabling osv on a project that already has a complete INFO.md must NOT
// discard the freshly analyzed reachability verdicts by restoring the old
// copy with the raw grading directive appended. The fresh analysis wins.
fs.writeFileSync(path.join(workspace, "data", "app", "INFO.md"), curatedInfo);
const infoAfterOsv = await (async () => {
await runSetupWorkflow({
workspaceDir: workspace,
projectId: "app",
projectRoot: project,
through: "threat-model",
osv: true,
services,
onLog: () => undefined,
});
return fs.readFileSync(path.join(workspace, "data", "app", "INFO.md"), "utf8");
})();
expect(analyze).toHaveBeenCalledTimes(3); // osv flag invalidated the info checkpoint
expect(infoAfterOsv).not.toContain(OSV_DIRECTIVE_SENTINEL); // directive never persisted
expect(infoAfterOsv).not.toBe(curatedInfo); // old copy not restored over fresh analysis
expect(infoAfterOsv).toContain("## Threat model"); // fresh analysis content kept
});

it("uses the new harness default model instead of a previous harness model", async () => {
Expand Down
11 changes: 10 additions & 1 deletion packages/deepsec/src/setup/coordinator.ts
Original file line number Diff line number Diff line change
Expand Up @@ -551,7 +551,16 @@ export async function runSetupWorkflow(
// appended section first, then append the freshly generated one (or none).
// Otherwise refreshes accumulate duplicate sections and keep listing
// dependencies that have since been patched or removed.
if (preserveInfo) {
//
// Exception: when osv is enabled, `osvEnrichment.context` is a *prompt
// directive* (it tells the analysis agent to emit a graded Dependency
// reachability block), not a self-contained section. Appending it to the old
// INFO.md would persist raw grading instructions and discard the real
// reachability verdicts the agent just wove into the freshly generated
// INFO.md. Enabling osv is a deliberate "re-analyze with dependencies in
// view" action, so keep the fresh analysis (verdicts included) rather than
// the preserved copy. The non-osv path keeps curated content as before.
if (preserveInfo && !osvEnabled) {
atomicWriteFileSync(infoPath, mergeInfoWithOsvContext(preserveInfo, osvEnrichment.context));
}
state.infoDigest = digest(fs.readFileSync(infoPath, "utf8"));
Expand Down