const test = require("node:test"); const assert = require("node:assert/strict"); const path = require("path"); const os = require("os"); const fs = require("fs"); const { parseChoicesText, resolveSiteDir, formatEdgeLabel, getFilterStatusList, buildEdges, loadStatusLabels, buildMermaid, } = require("./generate-process-flowchart"); test("parseChoicesText: 値,ラベル,短縮ラベル,色 形式を 値→ラベル のMapへ変換する", () => { const input = "100,新規,新規,new-status-white\n450,依頼者質疑中,質疑中,new-status-yellow"; const result = parseChoicesText(input); assert.equal(result.get("100"), "新規"); assert.equal(result.get("450"), "依頼者質疑中"); assert.equal(result.size, 2); }); test("parseChoicesText: undefinedやから文字列は空Mapを返す", () => { assert.equal(parseChoicesText(undefined).size, 0); assert.equal(parseChoicesText("").size, 0); }); test("parseChoicesText: ラベル省略時は値をラベルとして使う", () => { const result = parseChoicesText("999"); assert.equal(result.get("999"), "999"); }); test("resolveSiteDir: site-{id}_{name}形式のフォルダを前方一致で解決する", () => { const tmpBase = fs.mkdtempSync(path.join(os.tmpdir(), "gpf-test-")); fs.mkdirSync(path.join(tmpBase, "configs", "site-999_テストサイト"), { recursive: true }); const result = resolveSiteDir(tmpBase, "site-999"); assert.equal(result, path.join(tmpBase, "configs", "site-999_テストサイト")); }); test("formatEdgeLabel: Depts・Notifications無しはプロセス名のみ", () => { assert.equal(formatEdgeLabel({ Id: 2, DisplayName: "回答送信" }), "回答送信"); }); test("formatEdgeLabel: Depts指定時は (Dept番号) を付記する", () => { assert.equal(formatEdgeLabel({ Id: 3, DisplayName: "営業積算確認", Depts: [25] }), "営業積算確認 (Dept25)"); }); test("formatEdgeLabel: Notifications指定時は先頭に📧を付ける", () => { const process = { Id: 6, DisplayName: "遅延連絡", Depts: [25], Notifications: [{ Type: 1 }] }; assert.equal(formatEdgeLabel(process), "📧遅延連絡 (Dept25)"); }); test("formatEdgeLabel: DisplayName省略時はNameを使う", () => { assert.equal(formatEdgeLabel({ Id: 1, Name: "質疑送信" }), "質疑送信"); }); test("getFilterStatusList: View.ColumnFilterHash.Statusを文字列配列で返す", () => { const process = { View: { ColumnFilterHash: { Status: '["100","400"]' } } }; assert.deepEqual(getFilterStatusList(process), ["100", "400"]); }); test("getFilterStatusList: Status指定が無ければnullを返す", () => { assert.equal(getFilterStatusList({ View: { ColumnFilterHash: { ClassC: '["Own"]' } } }), null); assert.equal(getFilterStatusList({}), null); }); test("buildEdges: 通常遷移(CurrentStatus≠-1, ChangedStatus≠-1)", () => { const processes = [{ Id: 3, DisplayName: "営業積算確認", CurrentStatus: 450, ChangedStatus: 500, Depts: [25] }]; const { edgeLines, statusValues, needsAnyNode } = buildEdges(processes); assert.deepEqual(edgeLines, [' 450 -->|"営業積算確認 (Dept25)"| 500']); assert.deepEqual([...statusValues].sort(), ["450", "500"]); assert.equal(needsAnyNode, false); }); test("buildEdges: 任意状態からの遷移・ColumnFilterHash.Statusあり", () => { const processes = [ { Id: 1, DisplayName: "質疑送信", CurrentStatus: -1, ChangedStatus: 450, View: { ColumnFilterHash: { Status: '["100","400"]' } }, }, ]; const { edgeLines, statusValues, needsAnyNode } = buildEdges(processes); assert.deepEqual(edgeLines, [' 100 -->|"質疑送信"| 450', ' 400 -->|"質疑送信"| 450']); assert.deepEqual([...statusValues].sort(), ["100", "400", "450"]); assert.equal(needsAnyNode, false); }); test("buildEdges: 任意状態からの遷移・ColumnFilterHash無しはanyノード起点", () => { const processes = [{ Id: 9, DisplayName: "サンプル", CurrentStatus: -1, ChangedStatus: 500 }]; const { edgeLines, needsAnyNode } = buildEdges(processes); assert.deepEqual(edgeLines, [' any -->|"サンプル"| 500']); assert.equal(needsAnyNode, true); }); test("buildEdges: 状態維持アクション(ChangedStatus=-1)は自己ループ", () => { const processes = [{ Id: 4, DisplayName: "質疑回答削除", CurrentStatus: 450, ChangedStatus: -1, Depts: [25] }]; const { edgeLines, statusValues } = buildEdges(processes); assert.deepEqual(edgeLines, [' 450 -->|"質疑回答削除 (Dept25)"| 450']); assert.deepEqual([...statusValues], ["450"]); }); test("buildEdges: 状態変化なし・ColumnFilterHash.Statusありは各値へ自己ループ", () => { const processes = [ { Id: 7, DisplayName: "見積回答", CurrentStatus: -1, ChangedStatus: -1, Depts: [25], Notifications: [{ Type: 1 }], View: { ColumnFilterHash: { Status: '["800","900","901"]' } }, }, ]; const { edgeLines, needsAnyNode } = buildEdges(processes); assert.deepEqual(edgeLines, [ ' 800 -->|"📧見積回答 (Dept25)"| 800', ' 900 -->|"📧見積回答 (Dept25)"| 900', ' 901 -->|"📧見積回答 (Dept25)"| 901', ]); assert.equal(needsAnyNode, false); }); test("buildEdges: 状態変化なし・ColumnFilterHash無しはanyノードへ自己ループ", () => { const processes = [ { Id: 6, DisplayName: "遅延連絡", CurrentStatus: -1, ChangedStatus: -1, Depts: [25], Notifications: [{ Type: 1 }] }, ]; const { edgeLines, needsAnyNode } = buildEdges(processes); assert.deepEqual(edgeLines, [' any -->|"📧遅延連絡 (Dept25)"| any']); assert.equal(needsAnyNode, true); }); test("loadStatusLabels: latest.jsonのStatus列ChoicesTextを解決する", () => { const tmpBase = fs.mkdtempSync(path.join(os.tmpdir(), "gpf-test-")); fs.mkdirSync(path.join(tmpBase, "siteSettingJsons"), { recursive: true }); const fakeLatest = { Response: { Data: { SiteSettings: { Columns: [ { ColumnName: "ClassA", LabelText: "親番" }, { ColumnName: "Status", ChoicesText: "100,新規,新規,white\n450,依頼者質疑中,質疑中,yellow" }, ], }, }, }, }; fs.writeFileSync( path.join(tmpBase, "siteSettingJsons", "site-999_latest.json"), JSON.stringify(fakeLatest), "utf-8" ); const result = loadStatusLabels(tmpBase, "999"); assert.equal(result.get("100"), "新規"); assert.equal(result.get("450"), "依頼者質疑中"); }); test("loadStatusLabels: latest.jsonが無ければ空Mapを返す", () => { const tmpBase = fs.mkdtempSync(path.join(os.tmpdir(), "gpf-test-")); const result = loadStatusLabels(tmpBase, "000"); assert.equal(result.size, 0); }); test("buildMermaid: ノード定義とエッジ定義を含む完成文字列を返す", () => { const processes = [{ Id: 3, DisplayName: "営業積算確認", CurrentStatus: 450, ChangedStatus: 500, Depts: [25] }]; const statusLabels = new Map([ ["450", "依頼者質疑中"], ["500", "積算対応中"], ]); const result = buildMermaid(processes, statusLabels); assert.match(result, /^```mermaid\nflowchart LR\n/); assert.match(result, /450\(\(依頼者質疑中450\)\)/); assert.match(result, /500\(\(積算対応中500\)\)/); assert.match(result, /450 -->\|"営業積算確認 \(Dept25\)"\| 500/); assert.match(result, /```$/); }); test("buildMermaid: statusLabelsに無い値は値のみのラベルにフォールバックする", () => { const processes = [{ Id: 3, DisplayName: "テスト", CurrentStatus: 100, ChangedStatus: 200 }]; const result = buildMermaid(processes, new Map()); assert.match(result, /100\(\(100100\)\)/); assert.match(result, /200\(\(200200\)\)/); }); test("buildMermaid: anyノードが必要な場合のみ ((任意)) を含める", () => { const withAny = buildMermaid([{ Id: 6, DisplayName: "遅延連絡", CurrentStatus: -1, ChangedStatus: -1 }], new Map()); assert.match(withAny, /any\(\(任意\)\)/); const withoutAny = buildMermaid( [{ Id: 3, DisplayName: "営業積算確認", CurrentStatus: 450, ChangedStatus: 500 }], new Map() ); assert.doesNotMatch(withoutAny, /any\(\(任意\)\)/); });