From 0b4a2deadd38c766dcfed3cbd16fb195324bf09b Mon Sep 17 00:00:00 2001 From: Claude Date: Thu, 6 Aug 2026 14:00:39 +0000 Subject: [PATCH 1/2] docs(revenue): write the approvals page's navigation section to source (#963) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit The `Where to find pending approvals` section carried the same phantom sidebar `revenue/index` carried until #943/PR #953, so two pages in one directory gave readers two different answers. Measured against `src/apps/crm.app.ts` and the installed `@objectstack/plugin-approvals` (17.0.0-rc.3): - `group_approvals` holds exactly one child, labelled `Inbox` (zh-CN 待我审批), and the group is collapsed by default. `Approval Requests` is no navigation entry -- but the name is the plugin's own label for the OBJECT, so the section says where it really lives instead of claiming nothing carries it. - `Pending My Approval` / `Submitted by Me` / `Recently Approved` hit nothing in `src/` and nothing in the plugin. The four views that do exist -- `My Pending`, `I Submitted`, `Completed`, `All` -- are what the nav item opens, since it pins no `viewName`, so the section names those. - `Action History` labels no item anywhere. The trail is real data (`sys_approval_action`, its own three views, a `request_id` back-link); only the entry point is missing. All three locales updated; `src/` untouched. `test/docs-revenue-approvals-navigation.test.ts` pins both directions. Co-Authored-By: Claude Fable 5 Claude-Session: https://claude.ai/code/session_01VHrPAGEgFDoHjphqYG4BMa --- .../revenue-approvals-where-to-find-real.md | 37 +++ content/docs/revenue/approvals.mdx | 18 +- content/docs/revenue/approvals.zh-Hans.mdx | 18 +- content/docs/revenue/approvals.zh-Hant.mdx | 18 +- .../docs-revenue-approvals-navigation.test.ts | 263 ++++++++++++++++++ 5 files changed, 345 insertions(+), 9 deletions(-) create mode 100644 .changeset/revenue-approvals-where-to-find-real.md create mode 100644 test/docs-revenue-approvals-navigation.test.ts diff --git a/.changeset/revenue-approvals-where-to-find-real.md b/.changeset/revenue-approvals-where-to-find-real.md new file mode 100644 index 00000000..f52fdd82 --- /dev/null +++ b/.changeset/revenue-approvals-where-to-find-real.md @@ -0,0 +1,37 @@ +--- +'hotcrm': patch +--- + +Write the approvals page's *Where to find pending approvals* section to the app's +real navigation, so it stops contradicting the overview page next door. + +`content/docs/revenue/approvals.mdx` carried the same phantom sidebar the revenue +overview carried until #943: an **Approval Requests** item with three filter views +and an **Action History** item. The **Approvals** group in `src/apps/crm.app.ts` +holds exactly one child, labelled **Inbox** (待我审批 in Simplified Chinese), and +the group is collapsed by default. Each wrong name is now recorded rather than +quietly deleted, because they were wrong in three different ways: + +- ***Approval Requests*** is not a navigation entry — but the name is not + invented either: it is what the approvals plugin calls the **object** + (`sys_approval_request`, plural label *Approval Requests*). The section says + where the name really lives instead of claiming nothing carries it. +- ***Pending My Approval***, ***Submitted by Me*** and ***Recently Approved*** + match nothing in `src/` and nothing in the installed approvals plugin, under + any spelling — they never existed. What does exist are the four built-in list + views the plugin ships on the request object, which the **Inbox** item pins + none of and therefore opens all of: **My Pending**, **I Submitted**, + **Completed** and **All**. The section now names those, so a reader looking for + "requests waiting on me" finds the tab that does it. +- ***Action History*** names no sidebar item anywhere in the app. The audit trail + itself is real — every action is stored as `sys_approval_action`, with its own + **Recent** / **By Actor** / **All** views and a `request_id` lookup back to its + request — so what is missing is only the way in. + +All three locales updated; `src/` untouched. `test/docs-revenue-approvals-navigation.test.ts` +pins both halves: the section must name every list view the plugin ships (so a new +view cannot land while the prose goes stale) and must keep all five wrong names +with their denial, while the source side pins the single **Inbox** child, its +absent `viewName`, the zh-CN label, and the zero-hit status of the three phantom +view names — so a future plugin release that ships a view by one of those names +fails a test instead of silently making the page right again by accident. diff --git a/content/docs/revenue/approvals.mdx b/content/docs/revenue/approvals.mdx index c1ee14ea..e301029e 100644 --- a/content/docs/revenue/approvals.mdx +++ b/content/docs/revenue/approvals.mdx @@ -81,10 +81,22 @@ This history is permanent and surfaces in deal-review reports. ## Where to find pending approvals -In the Enterprise CRM app, the **Approvals** group: +In the Enterprise CRM app, the **Approvals** group holds exactly one item, and the group is **collapsed by default** — click it open before deciding something isn't there. -- **Approval Requests** — everything pending, with views *Pending My Approval*, *Submitted by Me*, *Recently Approved*. -- **Action History** — the full audit trail across all approvals. +- **Inbox** — the approval requests waiting on you (`sys_approval_request`). **Inbox** is the label on the sidebar item; *Approval Requests* is not a navigation entry anywhere in this app. The name is not invented — it is what the approvals plugin calls the **object** (*Approval Request* / *Approval Requests*) — but this page had it standing where a sidebar item should be. + +That item pins no view of its own, so it opens the object's list with the four built-in views the approvals plugin ships: + +| View | What it shows | +| --- | --- | +| **My Pending** | Requests still pending where you are one of the approvers | +| **I Submitted** | Everything you submitted, in any state | +| **Completed** | Requests that ended — approved, rejected or recalled | +| **All** | Every request, most recently updated first | + +Those four are the real filters. The three names this page used to list — *Pending My Approval*, *Submitted by Me* and *Recently Approved* — exist nowhere: neither `src/` nor the installed approvals plugin carries any of them, under any spelling. **My Pending**, **I Submitted** and **Completed** are the views they were describing. + +**Action History** is not in the group either, and no sidebar item anywhere in this app carries that name. The audit trail is real data — the approvals plugin stores every action as `sys_approval_action`, ships **Recent** / **By Actor** / **All** views for it, and each action points back at its request through a `request_id` lookup. What is missing is only the way in: no navigation entry in this app opens that object. ## Tips for sales reps diff --git a/content/docs/revenue/approvals.zh-Hans.mdx b/content/docs/revenue/approvals.zh-Hans.mdx index 63c4e575..bb924760 100644 --- a/content/docs/revenue/approvals.zh-Hans.mdx +++ b/content/docs/revenue/approvals.zh-Hans.mdx @@ -81,10 +81,22 @@ approval 节点使用**首次响应**(`first_response`)行为——在每个 ## 在哪里找到待处理的审批 -在 Enterprise CRM 应用中,**审批**分组: +Enterprise CRM 应用里的 **Approvals**(审批)分组只有一项,而且这个分组**默认折叠**——先把分组点开,再判断某样东西在不在这里。 -- **审批请求**——所有待处理项,带视图 *Pending My Approval*、*Submitted by Me*、*Recently Approved*。 -- **操作历史**——跨所有审批的完整审计跟踪。 +- **Inbox**——等着你处理的审批请求(`sys_approval_request`)。侧边栏上的 label 就是 **Inbox**,简体中文界面下这一项显示为**待我审批**;应用里没有任何导航条目叫 *Approval Requests*(审批请求)。这个名字不是凭空捏造:它是审批插件给**对象**起的名字(*Approval Request* / *Approval Requests*,简体界面为「审批请求」)——只是本页把它摆在了侧边栏条目的位置上。 + +该条目没有绑定自己的视图,所以点开后落在对象的列表页上,带着审批插件内置的四个视图: + +| 视图 | 内容 | +| --- | --- | +| **My Pending**(我的待办) | 仍在待批、且你是审批人之一的请求 | +| **I Submitted**(我提交的) | 你提交过的全部请求,不分状态 | +| **Completed**(已完成) | 已结束的请求——已批准、已拒绝或已撤回 | +| **All**(全部) | 全部请求,按最近更新排序 | + +这四个才是真实的过滤视图。本页过去列出的三个名字——*Pending My Approval*、*Submitted by Me*、*Recently Approved*——**哪里都不存在**:`src/` 与已安装的审批插件双面零命中,换任何拼写都一样。它们想描述的其实就是 **My Pending**、**I Submitted** 和 **Completed**。 + +**Action History**(操作历史)同样不在这个分组里,本应用侧边栏上也没有任何条目叫这个名字。审批的操作留痕本身是真实数据——审批插件把每一次操作存为 `sys_approval_action`,还为它内置了 **Recent**(最近)/ **By Actor**(按执行人)/ **All**(全部)三个视图,每条操作都通过 `request_id` 指回它所属的请求。缺的只是入口:本应用里没有任何导航条目能打开这个对象。 ## 给销售代表的提示 diff --git a/content/docs/revenue/approvals.zh-Hant.mdx b/content/docs/revenue/approvals.zh-Hant.mdx index b44fa73c..013db9b8 100644 --- a/content/docs/revenue/approvals.zh-Hant.mdx +++ b/content/docs/revenue/approvals.zh-Hant.mdx @@ -81,10 +81,22 @@ approval 節點使用**首次回應**(`first_response`)行為——在每個 ## 在哪裡找到待處理的審批 -在 Enterprise CRM 應用中,**審批**分組: +Enterprise CRM 應用裡的 **Approvals**(審批)群組只有一項,而且這個群組**預設摺疊**——先把群組點開,再判斷某樣東西在不在這裡。 -- **審批請求**——所有待處理項,帶視圖 *Pending My Approval*、*Submitted by Me*、*Recently Approved*。 -- **操作歷史**——跨所有審批的完整稽核軌跡。 +- **Inbox**——等著你處理的審批請求(`sys_approval_request`)。側邊欄上的 label 就是 **Inbox**(本應用提供 en / zh-CN / ja-JP / es-ES 四種介面語言,簡體中文介面下這一項顯示為「待我审批」);應用裡沒有任何導覽條目叫 *Approval Requests*(審批請求)。這個名字不是憑空捏造:它是審批外掛給**物件**取的名字(*Approval Request* / *Approval Requests*)——只是本頁把它擺在了側邊欄條目的位置上。 + +該條目沒有綁定自己的檢視,所以點開後落在物件的列表頁上,帶著審批外掛內建的四個檢視: + +| 檢視 | 內容 | +| --- | --- | +| **My Pending**(簡體介面「我的待办」) | 仍在待批、且你是審批人之一的請求 | +| **I Submitted**(簡體介面「我提交的」) | 你提交過的全部請求,不分狀態 | +| **Completed**(簡體介面「已完成」) | 已結束的請求——已批准、已拒絕或已撤回 | +| **All**(簡體介面「全部」) | 全部請求,按最近更新排序 | + +這四個才是真實的過濾檢視。本頁過去列出的三個名字——*Pending My Approval*、*Submitted by Me*、*Recently Approved*——**哪裡都不存在**:`src/` 與已安裝的審批外掛雙面零命中,換任何拼寫都一樣。它們想描述的其實就是 **My Pending**、**I Submitted** 和 **Completed**。 + +**Action History**(操作歷史)同樣不在這個群組裡,本應用側邊欄上也沒有任何條目叫這個名字。審批的操作留痕本身是真實資料——審批外掛把每一次操作存為 `sys_approval_action`,還為它內建了 **Recent**(最近)/ **By Actor**(按執行人)/ **All**(全部)三個檢視,每條操作都透過 `request_id` 指回它所屬的請求。缺的只是入口:本應用裡沒有任何導覽條目能打開這個物件。 ## 給銷售代表的提示 diff --git a/test/docs-revenue-approvals-navigation.test.ts b/test/docs-revenue-approvals-navigation.test.ts new file mode 100644 index 00000000..708627b5 --- /dev/null +++ b/test/docs-revenue-approvals-navigation.test.ts @@ -0,0 +1,263 @@ +// Copyright (c) 2025 ObjectStack. Licensed under the Apache-2.0 license. + +import { describe, it, expect } from 'vitest'; +import { readFileSync, readdirSync, statSync } from 'node:fs'; +import { join } from 'node:path'; +import { SysApprovalAction, SysApprovalRequest } from '@objectstack/plugin-approvals'; +import { REPO_ROOT } from './helpers/repo-root'; +import { CrmApp } from '../src/apps/crm.app'; +import { type AnyRec, packFor } from './helpers/metadata-fixtures'; + +/** + * `revenue/approvals` › *Where to find pending approvals*, pinned to source + * (#963). + * + * The section listed the same phantom navigation `revenue/index` listed until + * #943/PR #953 wrote that page to source — the two pages sit in one directory, + * so a reader who followed the link from one to the other got the corrected + * account and then the original wrong one. Three separate claims were wrong, + * and each is wrong in a different way, which is why they are pinned + * separately: + * + * 1. **The sidebar item's label.** `group_approvals` carries exactly one + * child and its label is **Inbox** (zh-CN 待我审批). The page called it + * *Approval Requests*. That name is not invented — it is the plugin's + * label for the OBJECT (`sys_approval_request`: *Approval Request* / + * *Approval Requests*) — so the fix is to say where the name really lives, + * not to claim nothing carries it. #963's issue body asserted the string + * appears nowhere in the repo; it greps 0 in `src/`, but the installed + * plugin ships it as `pluralLabel`. Both halves are pinned below. + * 2. **A navigation item that does not exist.** No node in this app is + * labelled *Action History*. The data behind the name is real — + * `sys_approval_action` — and so is the way back to its request + * (`request_id`), so the page keeps the name and records that only the + * entry point is missing, the same disposition PR #953 landed next door. + * 3. **Three view names that never existed.** *Pending My Approval*, + * *Submitted by Me* and *Recently Approved* match nothing in `src/` and + * nothing in the approvals plugin. The plugin does ship built-in list + * views for both objects — under other labels — so "these three do not + * exist" is only half the truth the reader needs; the section names the + * real ones, and this file keeps that list in step with the plugin. + * + * Nothing else checks this: `os validate` and `pnpm lint` walk authored + * metadata and never open `content/docs`, so — as with `docs-drift.test.ts` + * and the service-index guard (#948) — the check lives where the claim lives. + */ + +const NAV_NODES: AnyRec[] = (() => { + const walk = (nodes: AnyRec[]): AnyRec[] => + nodes.flatMap((n) => [n, ...walk((n.children ?? []) as AnyRec[])]); + return walk(((CrmApp as AnyRec).navigation ?? []) as AnyRec[]); +})(); + +const APPROVALS_GROUP: AnyRec = (() => { + const group = NAV_NODES.find((n) => n.id === 'group_approvals'); + if (!group) throw new Error('group_approvals is gone — this pin is out of date'); + return group; +})(); + +const APPROVALS_CHILDREN = (APPROVALS_GROUP.children ?? []) as AnyRec[]; + +/** Built-in list-view labels the approvals plugin ships, per object. */ +const viewLabels = (schema: AnyRec): string[] => + Object.values((schema.listViews ?? {}) as Record) + .map((v) => v.label as string) + .filter(Boolean); + +const REQUEST_VIEWS = viewLabels(SysApprovalRequest as unknown as AnyRec); +const ACTION_VIEWS = viewLabels(SysApprovalAction as unknown as AnyRec); + +/** The three names the section says exist nowhere. */ +const PHANTOM_VIEWS = ['Pending My Approval', 'Submitted by Me', 'Recently Approved'] as const; + +/** Every authored source file, for the "zero hits in `src/`" half of the claim. */ +const SRC_TEXT: string = (() => { + const out: string[] = []; + const walk = (dir: string) => { + for (const entry of readdirSync(dir)) { + const full = join(dir, entry); + if (statSync(full).isDirectory()) walk(full); + else if (/\.(ts|tsx|json)$/.test(entry)) out.push(readFileSync(full, 'utf8')); + } + }; + walk(join(REPO_ROOT, 'src')); + return out.join('\n'); +})(); + +const PAGES = [ + { + file: 'content/docs/revenue/approvals.mdx', + lang: 'en', + heading: '## Where to find pending approvals', + /** The denials the section must actually make, not merely imply. */ + denials: [ + /\*Approval Requests\* is not a navigation entry anywhere in this app/, + /exist nowhere/, + /no sidebar item anywhere in this app carries that name/, + ], + /** Where the section says the *Approval Requests* name really lives. */ + attribution: /calls the \*\*object\*\*/, + /** Verbatim fragments of the wrong section. None may return. */ + retired: [ + '**Approval Requests** — everything pending', + 'with views *Pending My Approval*', + '**Action History** — the full audit trail across all approvals', + ], + }, + { + file: 'content/docs/revenue/approvals.zh-Hans.mdx', + lang: 'zh-Hans', + heading: '## 在哪里找到待处理的审批', + denials: [ + /应用里没有任何导航条目叫 \*Approval Requests\*/, + /哪里都不存在/, + /本应用侧边栏上也没有任何条目叫这个名字/, + ], + attribution: /审批插件给\*\*对象\*\*起的名字/, + retired: [ + '**审批请求**——所有待处理项', + '带视图 *Pending My Approval*', + '**操作历史**——跨所有审批的完整审计跟踪', + ], + }, + { + file: 'content/docs/revenue/approvals.zh-Hant.mdx', + lang: 'zh-Hant', + heading: '## 在哪裡找到待處理的審批', + denials: [ + /應用裡沒有任何導覽條目叫 \*Approval Requests\*/, + /哪裡都不存在/, + /本應用側邊欄上也沒有任何條目叫這個名字/, + ], + attribution: /審批外掛給\*\*物件\*\*取的名字/, + retired: [ + '**審批請求**——所有待處理項', + '帶視圖 *Pending My Approval*', + '**操作歷史**——跨所有審批的完整稽核軌跡', + ], + }, +] as const; + +/** The `## …` section named by `heading`, up to the next `## `. */ +const sectionOf = (file: string, heading: string): string => { + const lines = readFileSync(join(REPO_ROOT, file), 'utf8').split('\n'); + const start = lines.findIndex((l) => l.trim() === heading); + expect(start, `${file}: heading '${heading}' not found`).toBeGreaterThanOrEqual(0); + const rest = lines.slice(start + 1); + const end = rest.findIndex((l) => l.startsWith('## ')); + return (end === -1 ? rest : rest.slice(0, end)).join('\n'); +}; + +describe('the source facts the approvals navigation section rests on (#963)', () => { + it('sees a non-trivial navigation tree and a plugin that ships views', () => { + // Guards the guard: an empty walk, or a plugin whose schemas stopped + // exposing `listViews`, would make every assertion below pass by checking + // nothing — the failure mode `test/action-references.test.ts`'s navigation + // guard lived in for its whole life. + expect(NAV_NODES.length, 'navigation walk found nothing').toBeGreaterThan(20); + expect(REQUEST_VIEWS.length, 'sys_approval_request ships no list views').toBeGreaterThanOrEqual(4); + expect(ACTION_VIEWS.length, 'sys_approval_action ships no list views').toBeGreaterThanOrEqual(3); + expect(SRC_TEXT.length, 'src/ scan read nothing').toBeGreaterThan(100_000); + }); + + it('the Approvals group holds exactly one item, labelled Inbox', () => { + expect(APPROVALS_CHILDREN.map((c) => c.label)).toEqual(['Inbox']); + expect(APPROVALS_CHILDREN[0]!.objectName).toBe(SysApprovalRequest.name); + }); + + it('that item pins no view, so the object\'s own list views are what a reader meets', () => { + // The section explains the four tabs *because* the nav item names none of + // them. Pin a `viewName` here and the prose has to change with it. + expect(APPROVALS_CHILDREN[0]!.viewName).toBeUndefined(); + }); + + it('the Approvals group is collapsed by default', () => { + expect(APPROVALS_GROUP.expanded).toBeFalsy(); + }); + + it('zh-CN shows that item as 待我审批', () => { + const label = (packFor('zh-CN') as AnyRec)?.apps?.[CrmApp.name]?.navigation?.nav_approval_requests?.label; + expect(label).toBe('待我审批'); + }); + + it('no navigation node anywhere is called Approval Requests or Action History', () => { + const labels = NAV_NODES.map((n) => String(n.label ?? '')); + expect(labels.filter((l) => /Approval Requests|Action History/.test(l))).toEqual([]); + }); + + it('but the plugin does label the object Approval Requests — the name the page mis-placed', () => { + // The half #963's issue body got wrong. If the plugin ever renames the + // object, the section's "the name is not invented" sentence stops being + // true and this test says so. + expect((SysApprovalRequest as AnyRec).pluralLabel).toBe('Approval Requests'); + expect((SysApprovalRequest as AnyRec).label).toBe('Approval Request'); + expect(SRC_TEXT).not.toContain('Approval Requests'); + }); + + it('none of the three phantom view names exists in src/ or in the approvals plugin', () => { + const pluginNames = [ + ...REQUEST_VIEWS, + ...ACTION_VIEWS, + ...Object.keys((SysApprovalRequest as AnyRec).listViews ?? {}), + ...Object.keys((SysApprovalAction as AnyRec).listViews ?? {}), + ]; + for (const phantom of PHANTOM_VIEWS) { + expect(SRC_TEXT, `${phantom} now exists in src/ — the docs' negative claim is stale`).not.toContain(phantom); + expect(pluginNames, `${phantom} is now a real plugin view`).not.toContain(phantom); + } + }); + + it('the audit trail is real data with no way in: an object, its views, and a link home', () => { + expect(SysApprovalAction.name).toBe('sys_approval_action'); + expect((SysApprovalAction as AnyRec).fields?.request_id?.reference).toBe(SysApprovalRequest.name); + const opensIt = NAV_NODES.filter((n) => n.objectName === SysApprovalAction.name); + expect(opensIt, 'a nav entry now opens the audit trail — the docs say none does').toEqual([]); + }); +}); + +describe('revenue/approvals names the navigation that exists (#963)', () => { + describe.each(PAGES)('$file', ({ file, heading, denials, attribution, retired }) => { + const section = () => sectionOf(file, heading); + + it('names the real sidebar item', () => { + expect(section()).toContain('Inbox'); + }); + + it('lists every built-in view of the approval request, by its source label', () => { + const text = section(); + const missing = REQUEST_VIEWS.filter((l) => !text.includes(l)); + expect( + missing, + `${file}: the section omits list view(s) the approvals plugin ships — a reader ` + + 'filtering the Inbox would not find them', + ).toEqual([]); + }); + + it('lists the audit-trail views too, so "real data, no entry point" is concrete', () => { + const text = section(); + expect(ACTION_VIEWS.filter((l) => !text.includes(l))).toEqual([]); + expect(text).toContain(SysApprovalAction.name); + }); + + it('keeps all five wrong names on the page instead of deleting them silently', () => { + const text = section(); + for (const name of [...PHANTOM_VIEWS, 'Approval Requests', 'Action History']) { + expect(text, `${file}: ${name} was dropped — a reader who remembers it learns nothing`).toContain(name); + } + }); + + it('states the denial for each of them, not merely the correction', () => { + const text = section(); + for (const re of denials) expect(text, `${file}: missing denial ${re}`).toMatch(re); + }); + + it('says where the Approval Requests name really lives', () => { + expect(section()).toMatch(attribution); + }); + + it('does not resurrect the claims #963 removed', () => { + const text = section(); + expect(retired.filter((phrase) => text.includes(phrase))).toEqual([]); + }); + }); +}); From e2ea53cad01e9e3573a54442a5f3c68423b4d83c Mon Sep 17 00:00:00 2001 From: Claude Date: Thu, 6 Aug 2026 14:05:57 +0000 Subject: [PATCH 2/2] test(docs): read src/ with dirent types, not stat-then-read (CodeQL #969) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit CodeQL flagged the `src/` walk in the new approvals-navigation guard as a potential file-system race: it called `statSync(full).isDirectory()` and then re-opened the same path with `readFileSync`, the check-then-use pair. `readdirSync(dir, { withFileTypes: true })` carries each entry's type from the directory read itself, so there is no second syscall to race against, and a `readFileSync` that throws on a file removed mid-walk is what this guard wants anyway. No assertion or branch changed — 30/30 still pass. Co-Authored-By: Claude Fable 5 Claude-Session: https://claude.ai/code/session_01VHrPAGEgFDoHjphqYG4BMa --- .../docs-revenue-approvals-navigation.test.ts | 19 +++++++++++++------ 1 file changed, 13 insertions(+), 6 deletions(-) diff --git a/test/docs-revenue-approvals-navigation.test.ts b/test/docs-revenue-approvals-navigation.test.ts index 708627b5..acd7e54d 100644 --- a/test/docs-revenue-approvals-navigation.test.ts +++ b/test/docs-revenue-approvals-navigation.test.ts @@ -1,7 +1,7 @@ // Copyright (c) 2025 ObjectStack. Licensed under the Apache-2.0 license. import { describe, it, expect } from 'vitest'; -import { readFileSync, readdirSync, statSync } from 'node:fs'; +import { readFileSync, readdirSync } from 'node:fs'; import { join } from 'node:path'; import { SysApprovalAction, SysApprovalRequest } from '@objectstack/plugin-approvals'; import { REPO_ROOT } from './helpers/repo-root'; @@ -70,14 +70,21 @@ const ACTION_VIEWS = viewLabels(SysApprovalAction as unknown as AnyRec); /** The three names the section says exist nowhere. */ const PHANTOM_VIEWS = ['Pending My Approval', 'Submitted by Me', 'Recently Approved'] as const; -/** Every authored source file, for the "zero hits in `src/`" half of the claim. */ +/** + * Every authored source file, for the "zero hits in `src/`" half of the claim. + * + * The directory entries carry their own type (`withFileTypes`), so the walk + * never stats a path and then re-opens it — that check-then-use pair is the + * file-system race CodeQL flags, and a `readFileSync` that throws on a file + * that vanished mid-walk is the behaviour this guard wants anyway. + */ const SRC_TEXT: string = (() => { const out: string[] = []; const walk = (dir: string) => { - for (const entry of readdirSync(dir)) { - const full = join(dir, entry); - if (statSync(full).isDirectory()) walk(full); - else if (/\.(ts|tsx|json)$/.test(entry)) out.push(readFileSync(full, 'utf8')); + for (const entry of readdirSync(dir, { withFileTypes: true })) { + const full = join(dir, entry.name); + if (entry.isDirectory()) walk(full); + else if (/\.(ts|tsx|json)$/.test(entry.name)) out.push(readFileSync(full, 'utf8')); } }; walk(join(REPO_ROOT, 'src'));