feat: 프로젝트를 공개할 수 있게 하고, 홈이 무엇을 앞에 둘지 고를 수 있게 한다
공개 화면 다섯 곳이 조용히 비어 있었다. 원인은 하나씩 달랐지만 모두 "값을 채울 방법이 없었다"는 같은 모양이었다. 홈의 "지금 집중하는 것" — `home_focus_config` 는 마이그레이션이 빈 행 하나만 넣어 두었고, 계약에 선언된 `getHomeFocus`/`updateHomeFocus` 는 구현이 없었다. 세 슬롯이 모두 비면 홈은 그 영역을 아예 그리지 않으므로, 운영에서는 한 번도 나타난 적이 없다. Studio 대시보드에 고르는 화면을 둔다. 홈의 "최근 기록" — 화면이 공개된 프로젝트를 하나씩 돌며 타임라인을 조립했다. 그래서 게시한 문서라도 그 프로젝트가 공개되어 있지 않으면 목록에서 통째로 빠졌고, 실제로 릴리스 한 줄만 남았다. 무엇이 최근인지는 공개 투영이 이미 알고 있으므로 그것을 그대로 읽는다. 프로젝트마다 요청을 보내던 N+1 도 사라진다. 프로젝트 공개 — 프로젝트는 `RecordKind` 에 없어 문서 게시 파이프라인을 타지 못하는데, 공개 화면들(프로젝트 목록·프로필의 "현재 프로젝트"·홈 focus)은 전부 `public_resource_projection` 의 PROJECT 행을 가시성 관문으로 쓴다. 그 행을 세우는 경로가 없었으므로 프로젝트는 영원히 비공개였다. 계약에 이미 있던 `publishProject`/`unpublishProject` 를 구현하고 주제·프로젝트 화면에 버튼을 둔다. 문서 사이 관계 연결 — `JdbcCatalogQueryAdapter` 의 RELATION/EVIDENCE 가 `List.of()` 스텁이라 어떤 기록도 연결 대상 목록을 채울 수 없었다. RELATION 은 작성 중에 고르는 것이므로 작업본까지 포함하고, EVIDENCE 는 읽는 사람이 따라갈 수 있어야 하므로 공개된 것만 포함한다. 본문 너비 — 문서 한 편이 세 폭으로 갈라져 있었다. 머리말 920px, 유형·프로젝트 줄은 shell 전체 1180px, 본문은 672px 를 가운데 정렬. 셋을 같은 폭·같은 왼쪽 끝에 세우고 읽는 단을 56rem 으로 넓힌다. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01XEHXspz4rv5pB5wiiSsVDu
This commit is contained in:
co-authored by
Claude Opus 5
parent
c03b0c77b8
commit
b3aa304975
@@ -802,8 +802,10 @@ describe("candidate archive and provider upload boundaries", () => {
|
||||
// shorter than one `systemctl show`, so the tree has to be sampled, not
|
||||
// sampled once at whatever moment the assertions happen to arrive.
|
||||
const tree = recordProviderProcessTree(execution.child.pid!);
|
||||
const unit = await waitForProviderUnit("vulnerability", execution.child.pid);
|
||||
const unit = await waitForProviderUnit("vulnerability", execution.child.pid, tree.sample);
|
||||
tree.sample();
|
||||
const properties = showProviderUnit(unit);
|
||||
tree.sample();
|
||||
expect(properties).toMatchObject({
|
||||
ActiveState: "active",
|
||||
CPUQuotaPerSecUSec: "1s",
|
||||
@@ -827,7 +829,22 @@ describe("candidate archive and provider upload boundaries", () => {
|
||||
const supervisorArguments = showProcessArguments(execution.child.pid);
|
||||
expect(supervisorArguments).not.toMatch(new RegExp(`${command}|${credential}`, "u"));
|
||||
const reportIdentity = await lstat(fixture.reportPath);
|
||||
const result = await execution.completion;
|
||||
/*
|
||||
기다리는 동안 직접 표본을 남긴다. 5ms 타이머에만 맡기면 다른 테스트 파일이 함께 도는 동안
|
||||
이벤트 루프가 밀려 표본이 통째로 비는 일이 있었다 — 관측 대상이 1.5초를 사는데도
|
||||
`directChildArguments` 가 빈 배열로 나왔다. 기다림 자체를 표본 채취의 박자로 쓰면 부하와
|
||||
무관해진다.
|
||||
*/
|
||||
const result = await (async () => {
|
||||
let settled: Awaited<typeof execution.completion> | undefined;
|
||||
const completion = execution.completion.then((value) => (settled = value));
|
||||
while (settled === undefined) {
|
||||
tree.sample();
|
||||
await Promise.race([completion, delay(5)]);
|
||||
}
|
||||
tree.sample();
|
||||
return settled;
|
||||
})();
|
||||
tree.stop();
|
||||
expect(result.code, result.stderr).toBe(0);
|
||||
expect(result.stdout).not.toContain(credential);
|
||||
@@ -849,7 +866,16 @@ describe("candidate archive and provider upload boundaries", () => {
|
||||
expect(observedArguments.join("\n")).not.toContain(credential);
|
||||
expect(directChildArguments.filter((arguments_) => arguments_.includes("/usr/bin/systemd-run"))).toHaveLength(1);
|
||||
expect(directChildArguments.filter((arguments_) => arguments_.includes("provider-raw-guardian.ts"))).toHaveLength(1);
|
||||
expect(processArguments.filter((arguments_) => arguments_.includes("provider-scope-wrapper.ts"))).toHaveLength(1);
|
||||
/*
|
||||
scope 안의 프로세스는 "몇 개인가" 로 고정할 수 없다 — 같은 argv 를 가진 pid 가 fork 로 둘이
|
||||
될 수 있고, 바로 아래 bwrap/prlimit 단언이 같은 이유로 존재만 본다. 실행 구조가 하나임은
|
||||
직전 두 줄이 직속 자식으로 이미 고정한다(systemd-run 1개, raw-guardian 1개) — 그쪽이
|
||||
중복 실행을 잡는 자리다. 여기서는 wrapper 가 scope 안에 있었는지만 본다.
|
||||
*/
|
||||
expect(
|
||||
processArguments.filter((arguments_) => arguments_.includes("provider-scope-wrapper.ts")).length,
|
||||
`scope wrapper never observed in the scope; recorded: ${processArguments.join(" | ")}`,
|
||||
).toBeGreaterThan(0);
|
||||
expect(
|
||||
processArguments.filter((arguments_) => /\/usr\/bin\/(?:bwrap|prlimit)/u.test(arguments_)).length,
|
||||
`sandbox never observed in the scope; recorded: ${processArguments.join(" | ")}`,
|
||||
@@ -1722,11 +1748,15 @@ function providerSupervisorEnvironment(
|
||||
async function waitForProviderUnit(
|
||||
kind: "vulnerability" | "provenance",
|
||||
supervisorPid: number | undefined,
|
||||
/** 블로킹 조회 앞뒤로 부른다 — 그 사이에만 살아 있는 프로세스를 놓치지 않기 위한 것이다. */
|
||||
onPoll?: () => void,
|
||||
): Promise<string> {
|
||||
if (!supervisorPid) throw new Error("provider supervisor did not expose its PID");
|
||||
const pattern = `ca-provider-${kind}-${supervisorPid}-*.scope`;
|
||||
for (let attempt = 0; attempt < 120; attempt += 1) {
|
||||
onPoll?.();
|
||||
const units = listProviderUnits(pattern);
|
||||
onPoll?.();
|
||||
if (units.length === 1) return units[0]!;
|
||||
if (units.length > 1) throw new Error(`provider unit identity is ambiguous: ${units.join(", ")}`);
|
||||
await delay(25);
|
||||
@@ -2025,6 +2055,13 @@ function recordProviderProcessTree(supervisorPid: number) {
|
||||
timer.unref();
|
||||
sample();
|
||||
return Object.freeze({
|
||||
/*
|
||||
타이머만으로는 부족하다. 이 테스트는 `systemctl` 을 `spawnSync` 로 부르며 기다리는데, 그
|
||||
동안 이벤트 루프가 멈춰 5ms 간격이 굶는다 — bubblewrap 은 그 한 틈보다 짧게 살 수 있고,
|
||||
그러면 관측하려던 바로 그 프로세스가 기록에서 빠진다. 그래서 블로킹 호출을 하는 쪽이
|
||||
직접 이 함수를 불러 같은 박자로 표본을 남긴다.
|
||||
*/
|
||||
sample,
|
||||
stop() {
|
||||
stopped = true;
|
||||
clearInterval(timer);
|
||||
|
||||
Reference in New Issue
Block a user