Skip to content
Merged
Show file tree
Hide file tree
Changes from 4 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: 27 additions & 7 deletions .github/workflows/opencode-review.yml
Original file line number Diff line number Diff line change
Expand Up @@ -58,6 +58,14 @@ jobs:
"${install_dir}/opencode" --version
echo "$install_dir" >>"$GITHUB_PATH"

- name: Initialize CodeGraph index for OpenCode
env:
CODEGRAPH_PACKAGE: "@colbymchenry/codegraph@0.9.9"
run: |
set -euo pipefail
npx -y "$CODEGRAPH_PACKAGE" init -i
npx -y "$CODEGRAPH_PACKAGE" status
Comment thread
coderabbitai[bot] marked this conversation as resolved.

- name: Run OpenCode PR Review (GPT-5)
id: opencode_review_primary
timeout-minutes: 60
Expand Down Expand Up @@ -94,25 +102,31 @@ jobs:
title, problem, root_cause, fix_direction, regression_test_direction,
and suggested_diff.

Use the configured CodeGraph MCP tools before judging architecture,
impact, callers/callees, or symbol-level implementation details.
Cite concrete CodeGraph-derived evidence in the review summary or
findings when relevant, and fall back to file reads only after
CodeGraph cannot answer the structural question.

This PR updates the Strix GitHub Models fallback path. Verify that:
- full PR scope remains a single Strix invocation, not split scans;
- provider failure output cannot be treated as clean evidence;
- GitHub Models primary failures advance to a distinct catalog-backed
GitHub Models fallback;
- GitHub Models primary failures advance to distinct catalog-backed
GPT-5-class GitHub Models fallbacks, not GPT-4.1;
- shell tests cover the fallback behavior.
run: |
set -euo pipefail
timeout 240 opencode github run

- name: Run OpenCode PR Review fallback (GPT-4.1)
- name: Run OpenCode PR Review fallback (GPT-5 mini)
id: opencode_review_fallback
if: steps.opencode_review_primary.outcome != 'success'
timeout-minutes: 60
continue-on-error: true
env:
STRIX_GITHUB_MODELS_TOKEN: ${{ secrets.STRIX_GITHUB_MODELS_TOKEN }}
GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
MODEL: github-models/gpt-4.1
MODEL: github-models/gpt-5-mini
USE_GITHUB_TOKEN: "true"
SHARE: "false"
PROMPT: |
Expand Down Expand Up @@ -141,12 +155,18 @@ jobs:
title, problem, root_cause, fix_direction, regression_test_direction,
and suggested_diff.

Use the configured CodeGraph MCP tools before judging architecture,
impact, callers/callees, or symbol-level implementation details.
Cite concrete CodeGraph-derived evidence in the review summary or
findings when relevant, and fall back to file reads only after
CodeGraph cannot answer the structural question.

The GPT-5 GitHub Models route failed for this run, so this is the
configured OpenCode fallback review. Verify that:
configured GPT-5-mini OpenCode fallback review. Verify that:
- full PR scope remains a single Strix invocation, not split scans;
- provider failure output cannot be treated as clean evidence;
- GitHub Models primary failures advance to a distinct catalog-backed
GitHub Models fallback;
- GitHub Models primary failures advance to distinct catalog-backed
GPT-5-class GitHub Models fallbacks, not GPT-4.1;
- shell tests cover the fallback behavior.
run: |
set -euo pipefail
Expand Down
2 changes: 1 addition & 1 deletion .github/workflows/strix.yml
Original file line number Diff line number Diff line change
Expand Up @@ -300,7 +300,7 @@ jobs:
STRIX_LLM_MAX_RETRIES: 1
STRIX_TRANSIENT_RETRY_PER_MODEL: 2
STRIX_TRANSIENT_RETRY_BACKOFF_SECONDS: 3
STRIX_FALLBACK_MODELS: ${{ steps.gate.outputs.provider_mode == 'github_models' && 'openai/gpt-4.1' || '' }}
STRIX_FALLBACK_MODELS: ${{ steps.gate.outputs.provider_mode == 'github_models' && 'openai/gpt-5-mini openai/gpt-5-nano' || '' }}
STRIX_FAIL_ON_PROVIDER_SIGNAL: "1"
STRIX_VERTEX_FALLBACK_MODELS: ""
NPM_CONFIG_IGNORE_SCRIPTS: "true"
Expand Down
4 changes: 4 additions & 0 deletions .gitignore
Original file line number Diff line number Diff line change
Expand Up @@ -73,4 +73,8 @@ frontend/playwright-report/
frontend/playwright/.cache/
frontend/trace_output/

# Local CodeGraph artifacts
.codegraph/
.cursor/rules/codegraph.mdc

.worktrees/
17 changes: 11 additions & 6 deletions AGENTS.md
Original file line number Diff line number Diff line change
Expand Up @@ -18,8 +18,10 @@
`LLM_API_BASE_FILE` pointing at a trusted file containing
`https://models.github.ai/inference`; GitHub Models scans must try the
configured GPT-5-or-newer model first and may fall back to the explicit
workflow fallback list, currently `openai/gpt-4.1`, when GitHub Models
provider capacity or model availability blocks the primary run. Keep the
workflow fallback list, currently `openai/gpt-5-mini` and
`openai/gpt-5-nano`, when GitHub Models provider capacity or model
availability blocks the primary run. Do not use GPT-4.1 or weaker GitHub
Models fallbacks for Strix or OpenCode PR review evidence. Keep the
GitHub Models endpoint in a trusted input file and pass the token only through
the provider-scoped Strix child-process key path. Legacy `STRIX_LLM` secrets
must not override PR, push, or scheduled Strix defaults. Vertex remains
Expand Down Expand Up @@ -355,10 +357,13 @@

## Development environment and tooling defaults

- If CodeGraph is not initialized for this repository, agents may ask the user
to approve initialization and then run `codegraph init -i`; keep the generated
`.codegraph/` index local unless a future repository policy explicitly says to
commit it.
- If CodeGraph is not initialized for this repository, agents may run
`codegraph init -i` autonomously without asking first; keep generated
`.codegraph/` and `.cursor/rules/codegraph.mdc` artifacts local unless a
future repository policy explicitly says to commit them. OpenCode PR review
uses the project `opencode.jsonc` CodeGraph MCP server and must initialize
CodeGraph before review so structural findings cite graph-backed evidence
instead of relying only on grep or raw file reads.
- StepSecurity `harden-runner` will trigger false-positive `suspicious_file_access` lockouts on Next.js build and dev server executions (e.g., `router_init.js` checksum matches). Configure `disable-file-monitoring: true` in the `harden-runner` step rather than disabling the workflow or using `continue-on-error`.
- Next.js 15+ Turbopack resolves workspace roots by scanning upward for `package-lock.json`. Do not create or leave a `package-lock.json` in the user's home directory (`~/`), as it will cause Turbopack to spawn infinite background worker node processes attempting to compile the entire home directory.
- `pydantic-settings` strictly rejects unexpected environment variables by default. When sharing a common `.env` file between frontend and backend services, you must explicitly set `extra="ignore"` in the `SettingsConfigDict` to prevent fatal startup crashes.
Expand Down
216 changes: 216 additions & 0 deletions frontend/src/components/WorkspaceHome.dashboard.test.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -54,6 +54,19 @@ async function waitForCondition(condition: () => boolean) {
throw new Error("waitForCondition timed out after 20 attempts");
}

function emptySourceEvidenceResponse(url: string) {
if (
url.endsWith("/api/calendar/writeback-sources") ||
url.endsWith("/api/webdav/folders")
) {
return Promise.resolve({
ok: true,
json: async () => ([]),
});
}
return null;
}

describe("WorkspaceHome Today dashboard", () => {
let root: Root | null = null;
let container: HTMLDivElement | null = null;
Expand Down Expand Up @@ -122,6 +135,8 @@ describe("WorkspaceHome Today dashboard", () => {
json: async () => ([]),
});
}
const sourceEvidenceResponse = emptySourceEvidenceResponse(url);
if (sourceEvidenceResponse) return sourceEvidenceResponse;
throw new Error(`Unexpected fetch: ${url}`);
}));
container = document.createElement("div");
Expand Down Expand Up @@ -204,6 +219,8 @@ describe("WorkspaceHome Today dashboard", () => {
json: async () => ([]),
});
}
const sourceEvidenceResponse = emptySourceEvidenceResponse(url);
if (sourceEvidenceResponse) return sourceEvidenceResponse;
throw new Error(`Unexpected fetch: ${url}`);
}));
container = document.createElement("div");
Expand Down Expand Up @@ -283,6 +300,8 @@ describe("WorkspaceHome Today dashboard", () => {
]),
});
}
const sourceEvidenceResponse = emptySourceEvidenceResponse(url);
if (sourceEvidenceResponse) return sourceEvidenceResponse;
throw new Error(`Unexpected fetch: ${url}`);
}));
container = document.createElement("div");
Expand Down Expand Up @@ -315,4 +334,201 @@ describe("WorkspaceHome Today dashboard", () => {
expect(linkHrefByText("데이터 품질 점검")).toBe("/data");
expect(linkHrefByText("보안 감사 로그")).toBe("/security");
});

it("backs Today dashboard operating metrics with source evidence instead of fixed fixture numbers", async () => {
vi.stubGlobal("matchMedia", vi.fn((query: string) => ({
matches: false,
media: query,
addEventListener: vi.fn(),
removeEventListener: vi.fn(),
})));
localStorage.setItem("naruon_session_token", "signed-source-backed-home");
const publicIdentityHeaders = [
"x-user-id",
"x-organization-id",
"x-group-id",
"x-group-ids",
"x-user-role",
"x-dev-auth-token",
];
const fetchCalls: Array<{ url: string; init?: RequestInit }> = [];
vi.stubGlobal("fetch", vi.fn((input: RequestInfo | URL, init?: RequestInit) => {
const url = String(input);
fetchCalls.push({ url, init });
if (url.endsWith("/api/emails/pending-replies?limit=3")) {
return Promise.resolve({
ok: true,
json: async () => ({ emails: [] }),
});
}
if (url.endsWith("/api/emails")) {
return Promise.resolve({
ok: true,
json: async () => ({
emails: [
{
id: 101,
subject: "고객 계약 승인 대기",
sender: "legal@example.com",
date: "2026-05-17T09:00:00Z",
snippet: "오늘 승인해야 하는 계약 검토 요청",
unread: true,
},
],
}),
});
}
if (url.endsWith("/api/tasks")) {
return Promise.resolve({
ok: true,
json: async () => ([
{
id: "task-source-open",
title: "계약 승인 확인",
status: "open",
priority: "high",
created_at: "2026-05-17T09:00:00Z",
updated_at: "2026-05-17T09:00:00Z",
},
{
id: "task-source-done",
title: "첨부 근거 정리",
status: "done",
priority: "low",
created_at: "2026-05-17T09:00:00Z",
updated_at: "2026-05-17T09:00:00Z",
},
]),
});
}
if (url.endsWith("/api/calendar/writeback-sources")) {
return Promise.resolve({
ok: true,
json: async () => ([
{
source_id: "caldav-primary",
provider: "Primary CalDAV",
protocol: "caldav",
capabilities: ["read", "write"],
writeback_enabled: true,
},
{
source_id: "calendar-readonly",
provider: "Read-only Calendar",
protocol: "local",
capabilities: ["read"],
writeback_enabled: false,
},
]),
});
}
if (url.endsWith("/api/webdav/folders")) {
return Promise.resolve({
ok: true,
json: async () => ([
{
folder_uid: "folder-roadmap",
project_name: "Naruon Roadmap",
webdav_path: "/Projects/Naruon_Roadmap",
},
]),
});
}
throw new Error(`Unexpected fetch: ${url}`);
}));
container = document.createElement("div");
document.body.appendChild(container);
root = createRoot(container);

await act(async () => {
root?.render(<WorkspaceHome forcedStartupView="dashboard" />);
});
await waitForCondition(() => container?.textContent?.includes("고객 계약 승인 대기") ?? false);

expect(container.textContent).toContain("일정 원본");
expect(container.textContent).toContain("2");
expect(container.textContent).toContain("1개 writeback 가능");
expect(container.textContent).toContain("프로젝트 원본");
expect(container.textContent).toContain("1개 WebDAV 폴더");
expect(container.textContent).toContain("작업 완료율");
expect(container.textContent).toContain("50%");
expect(container.textContent).toContain("1/2 완료");
expect(container.textContent).toContain("caldav-primary");
expect(container.textContent).not.toContain("오늘 일정");
expect(container.textContent).not.toContain("진행 중 프로젝트");
expect(container.textContent).not.toContain("이번 주 목표 진행률");
expect(container.textContent).not.toContain("회의 2건 예정");
expect(container.textContent).not.toContain("일정 충돌 알림");
expect(container.textContent).not.toContain("68%");
expect(container.textContent).not.toContain("어제 대비");

const calendarSourceCall = fetchCalls.find((call) => call.url.endsWith("/api/calendar/writeback-sources"));
const projectFolderCall = fetchCalls.find((call) => call.url.endsWith("/api/webdav/folders"));
for (const sourceCall of [calendarSourceCall, projectFolderCall]) {
expect(sourceCall).toBeDefined();
const headers = sourceCall?.init?.headers as Record<string, string>;
expect(headers.Authorization).toBe("Bearer signed-source-backed-home");
for (const headerName of publicIdentityHeaders) {
expect(Object.keys(headers).some((key) => key.toLowerCase() === headerName)).toBe(false);
}
}
Comment thread
coderabbitai[bot] marked this conversation as resolved.
});

it("shows an explicit source evidence error instead of a false empty calendar state", async () => {
vi.stubGlobal("matchMedia", vi.fn((query: string) => ({
matches: false,
media: query,
addEventListener: vi.fn(),
removeEventListener: vi.fn(),
})));
vi.stubGlobal("fetch", vi.fn((input: RequestInfo | URL) => {
const url = String(input);
if (url.endsWith("/api/emails/pending-replies?limit=3")) {
return Promise.resolve({
ok: true,
json: async () => ({ emails: [] }),
});
}
if (url.endsWith("/api/emails")) {
return Promise.resolve({
ok: true,
json: async () => ({
emails: [
{
id: 101,
subject: "고객 계약 승인 대기",
sender: "legal@example.com",
date: "2026-05-17T09:00:00Z",
snippet: "오늘 승인해야 하는 계약 검토 요청",
unread: true,
},
],
}),
});
}
if (url.endsWith("/api/tasks")) {
return Promise.resolve({
ok: true,
json: async () => ([]),
});
}
if (url.endsWith("/api/calendar/writeback-sources") || url.endsWith("/api/webdav/folders")) {
return Promise.reject(new Error("source registry unavailable"));
}
throw new Error(`Unexpected fetch: ${url}`);
}));
container = document.createElement("div");
document.body.appendChild(container);
root = createRoot(container);

await act(async () => {
root?.render(<WorkspaceHome forcedStartupView="dashboard" />);
});
await waitForCondition(() => container?.textContent?.includes("일정 source registry 확인에 실패했습니다.") ?? false);

expect(container.textContent).toContain("일정 원본 확인 필요");
expect(container.textContent).toContain("오류");
expect(container.textContent).toContain("source registry 응답을 확인할 수 없습니다.");
expect(container.textContent).not.toContain("연결된 일정 원본이 없습니다.");
});
});
Loading
Loading