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
107 changes: 106 additions & 1 deletion packages/cli/src/cli-surface.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -10,8 +10,23 @@ import { mkdirSync, mkdtempSync, rmSync, writeFileSync } from "node:fs";
import { tmpdir } from "node:os";
import { dirname, join } from "node:path";
import { fileURLToPath } from "node:url";
import { writeConfig } from "./config.js";
import {
answersDigest,
writeAnswers,
writeGrade,
writeTaught,
} from "./attest.js";
import { readConfig, writeConfig } from "./config.js";
import { cmdStatus } from "./commands/status.js";
import { materializedTreeOid, readGate, writeGate } from "./gate.js";
import { computeDiffContext } from "./hash.js";
import { readRangeSession, writeRangeSeal } from "./range.js";
import {
initAttestKey,
readAttestMeta,
signPayload,
verifyPayload,
} from "./seal.js";
import {
commitAll,
git,
Expand Down Expand Up @@ -173,6 +188,96 @@ describe("cli surface (spawned)", () => {
}
});

it("status separates receipt signature validity from a missing pending trailer", () => {
const { root, cleanup } = withTempRepo("kc-cli-status-receipt-");
const attestHome = mkdtempSync(join(tmpdir(), "kc-attest-"));
const priorAttestHome = process.env.KNOW_CODE_ATTEST_HOME;
try {
setupRepo(root, liteConfig({ requireTrailer: true }));
const repoRoot = git(root, ["rev-parse", "--show-toplevel"]);
process.env.KNOW_CODE_ATTEST_HOME = attestHome;
initAttestKey(repoRoot, "test-passphrase");
const hash = computeDiffContext(repoRoot, readConfig(repoRoot)).diffHash;
const gate = {
version: 1 as const,
diffHash: hash,
level: "lite" as const,
passedAt: new Date().toISOString(),
commitRange: "x",
baseRef: "y",
headRef: git(repoRoot, ["rev-parse", "HEAD"]),
gatedTreeOid: materializedTreeOid(repoRoot),
};
const signed = signPayload(repoRoot, "test-passphrase", gate);
writeGate(repoRoot, { ...gate, ...signed });
const pub = readAttestMeta(repoRoot)?.pubKey;
assert.ok(pub, "expected test attest public key");
assert.equal(
verifyPayload(
pub,
readGate(repoRoot) as unknown as Record<string, unknown> & {
sig?: string;
keyId?: string;
},
),
true,
);
writeTaught(repoRoot, {
version: 1,
diffHash: hash,
taughtAt: new Date().toISOString(),
skipped: false,
});
writeFileSync(join(repoRoot, ".know-code", "quiz.json"), "{}\n");
const answers = {
diffHash: hash,
answers: [{ id: "q1", answer: "understood" }],
submittedAt: new Date().toISOString(),
};
writeAnswers(repoRoot, answers);
writeGrade(repoRoot, {
version: 1,
diffHash: hash,
score: 1,
passed: true,
gradedAt: new Date().toISOString(),
answersDigest: answersDigest(answers),
});

const previousCwd = process.cwd();
const log = console.log;
let output = "";
console.log = (line: string) => {
output += line;
};
process.chdir(root);
let payload: Record<string, unknown>;
try {
cmdStatus({ json: true });
payload = JSON.parse(output);
} finally {
process.chdir(previousCwd);
console.log = log;
}
assert.equal(payload.allowed, false);
assert.equal(payload.attestReady, true);
assert.equal(payload.receiptSealed, true);
assert.deepEqual(payload.blockers, [
{
step: "check",
message: "requireTrailer: HEAD missing Know-Code-Verified trailer",
command: 'know-code commit -m "…"',
},
]);
assert.equal(payload.nextStep, 'know-code commit -m "…"');
} finally {
if (priorAttestHome === undefined) delete process.env.KNOW_CODE_ATTEST_HOME;
else process.env.KNOW_CODE_ATTEST_HOME = priorAttestHome;
rmSync(attestHome, { recursive: true, force: true });
cleanup();
}
});

it("range continue --yes starts the next session at current HEAD", () => {
const { root, cleanup } = withTempRepo("kc-cli-rangecont-");
try {
Expand Down
36 changes: 28 additions & 8 deletions packages/cli/src/commands/status.ts
Original file line number Diff line number Diff line change
Expand Up @@ -53,7 +53,8 @@ export function cmdStatus(opts: { json?: boolean; next?: boolean } = {}): void {
);
const session = readRangeSession(repoRoot);
const receipt = readGateSafe(repoRoot);
const allowed = runCheck(repoRoot).allowed;
const check = runCheck(repoRoot);
const allowed = check.allowed;
const from = mergeBase(repoRoot, ctx.baseRef, ctx.headRef);
const stat = diffStat(repoRoot, from, ctx.headRef);
const log = logOneline(repoRoot, from, ctx.headRef);
Expand All @@ -67,6 +68,16 @@ export function cmdStatus(opts: { json?: boolean; next?: boolean } = {}): void {
const proposal = proposalR.value;
const meta = readAttestMeta(repoRoot);
const pub = meta?.pubKey;
const receiptSealed =
!!receipt &&
!!pub &&
verifyPayload(
pub,
receipt as unknown as Record<string, unknown> & {
sig?: string;
keyId?: string;
},
);
const taughtOk =
!!taught &&
taught.diffHash === effectiveHash &&
Expand All @@ -79,6 +90,13 @@ export function cmdStatus(opts: { json?: boolean; next?: boolean } = {}): void {
verifyPayload(pub, grade as unknown as Record<string, unknown> & { sig?: string; keyId?: string });

const pipeline = evaluatePipeline(repoRoot);
const blockers =
!allowed && pipeline.blockers.length === 0 && check.reason
? [
...pipeline.blockers,
{ step: "check", message: check.reason, command: check.next },
]
: pipeline.blockers;
const unstaged = hasUnstagedTrackedChanges(repoRoot);
const taughtStaleDetail = taughtR.corrupt
? "corrupt"
Expand All @@ -95,8 +113,8 @@ export function cmdStatus(opts: { json?: boolean; next?: boolean } = {}): void {

const payload = {
allowed,
nextStep: pipeline.nextStep,
blockers: pipeline.blockers,
nextStep: blockers[0]?.command ?? pipeline.nextStep,
blockers,
level: config.level,
baseBranch: config.baseBranch,
attestKeyId: meta?.keyId || null,
Expand All @@ -112,6 +130,7 @@ export function cmdStatus(opts: { json?: boolean; next?: boolean } = {}): void {
headRef: ctx.headRef,
commitRange: ctx.commitRange,
receipt,
receiptSealed,
taught: taughtR.corrupt
? "corrupt"
: taught?.diffHash === effectiveHash
Expand Down Expand Up @@ -147,12 +166,13 @@ export function cmdStatus(opts: { json?: boolean; next?: boolean } = {}): void {

console.log(`know-code status`);
console.log(` commit/push allowed: ${allowed ? "yes" : "no"}`);
if (pipeline.nextStep) {
console.log(` next: ${pipeline.nextStep}`);
const nextStep = blockers[0]?.command ?? pipeline.nextStep;
if (nextStep) {
console.log(` next: ${nextStep}`);
}
if (opts.next !== false && pipeline.blockers.length) {
if (opts.next !== false && blockers.length) {
console.log(` blockers:`);
for (const b of pipeline.blockers) {
for (const b of blockers) {
console.log(` - ${b.step}: ${b.message}`);
}
}
Expand All @@ -171,7 +191,7 @@ export function cmdStatus(opts: { json?: boolean; next?: boolean } = {}): void {
console.log(` base: ${ctx.baseRef}`);
if (receipt) {
console.log(
` receipt: ${receipt.level} @ ${receipt.passedAt} (${receipt.diffHash.slice(0, 12)}…) sealed=${allowed ? "yes" : "no"}`,
` receipt: ${receipt.level} @ ${receipt.passedAt} (${receipt.diffHash.slice(0, 12)}…) sealed=${receiptSealed ? "yes" : "no"}`,
);
} else {
console.log(` receipt: (none)`);
Expand Down