Skip to content
Closed
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
4 changes: 2 additions & 2 deletions __tests__/issue-680-fallback-messaging.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -26,8 +26,8 @@ describe('Issue #680: fallback messaging distinguishes implementation from sourc
const lines = formatRustHybridFallbackHealthLines(summary);
const joined = lines.join('\n');

// Reason line should use the new label
expect(joined).toContain('30 non-Rust-owned files via TypeScript fallback');
// Partial state: info line uses "files indexed via TypeScript fallback (non-Rust-owned languages)"
expect(joined).toContain('30 files indexed via TypeScript fallback (non-Rust-owned languages)');
expect(joined).not.toContain('30 TypeScript fallback files');

// Language breakdown line
Expand Down
167 changes: 167 additions & 0 deletions __tests__/issue-682-partial-fallback-state.test.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,167 @@
import { describe, expect, it } from 'vitest';
import {
rustHybridFallbackStateFor,
planRustHybridAssignments,
mergeRustOwnedGapDiagnostics,
type RustHybridAssignmentPlan,
} from '../src/indexing/rust-hybrid-contract';
import {
buildRustHybridFallbackSummary,
formatRustHybridFallbackHealthLines,
formatRustHybridFallbackDoctorHint,
} from '../src/diagnostics/fallback-summary';
import { classifyGraphHealth } from '../src/diagnostics/graph-health';

describe('Issue #682: three-tier fallback health state (healthy/partial/degraded)', () => {
describe('rustHybridFallbackStateFor', () => {
it('returns partial when only language-level-typescript-fallback exists', () => {
const state = rustHybridFallbackStateFor(5, {
'language-level-typescript-fallback': 5,
});
expect(state).toBe('partial');
});

it('returns degraded when rust-owned-parse-gap is present alongside expected fallback', () => {
const state = rustHybridFallbackStateFor(5, {
'language-level-typescript-fallback': 5,
'rust-owned-parse-gap': 1,
});
expect(state).toBe('degraded');
});

it('returns degraded when language-level-fallback-missing-file is present', () => {
const state = rustHybridFallbackStateFor(5, {
'language-level-typescript-fallback': 5,
'language-level-fallback-missing-file': 2,
});
expect(state).toBe('degraded');
});

it('returns healthy when no fallbacks exist', () => {
const state = rustHybridFallbackStateFor(0, {});
expect(state).toBe('healthy');
});
});

describe('buildRustHybridFallbackSummary', () => {
it('returns partial state for non-Rust-owned language fallback without gaps', () => {
const summary = buildRustHybridFallbackSummary({
success: true,
profile: {
typescriptFallbackAppend: {
fallbackFileCount: 58,
fallbackByLanguage: { yaml: 58 },
},
},
errors: [],
});

expect(summary.fallbackState).toBe('partial');
expect(summary.graphUsabilityMessage).not.toContain('need review');
});

it('returns degraded state when rust-owned gaps are present', () => {
const summary = buildRustHybridFallbackSummary({
success: true,
profile: {
typescriptFallbackAppend: {
fallbackFileCount: 1,
},
},
errors: [
{ severity: 'warning', code: 'rust-owned-parse-gap' },
],
});

expect(summary.fallbackState).toBe('degraded');
});
});

describe('formatRustHybridFallbackHealthLines', () => {
it('outputs partial format with info line and language breakdown', () => {
const summary = buildRustHybridFallbackSummary({
success: true,
profile: {
typescriptFallbackAppend: {
fallbackFileCount: 58,
fallbackByLanguage: { yaml: 58 },
},
},
errors: [],
});

const lines = formatRustHybridFallbackHealthLines(summary);
const joined = lines.join('\n');

expect(joined).toContain('Fallback health: partial');
expect(joined).toContain('58 files indexed via TypeScript fallback (non-Rust-owned languages)');
expect(joined).toContain('Fallback by source language: yaml (58)');
expect(joined).not.toContain('need review');
});

it('outputs partial format without language breakdown when absent', () => {
const summary = buildRustHybridFallbackSummary({
success: true,
profile: {
typescriptFallbackAppend: {
fallbackFileCount: 5,
},
},
errors: [],
});

const lines = formatRustHybridFallbackHealthLines(summary);
const joined = lines.join('\n');

expect(joined).toContain('Fallback health: partial');
expect(joined).toContain('5 files indexed via TypeScript fallback (non-Rust-owned languages)');
expect(joined).not.toContain('Fallback by source language');
expect(joined).not.toContain('need review');
});
});

describe('formatRustHybridFallbackDoctorHint', () => {
it('returns hint for partial state with info-level health line', () => {
const summary = buildRustHybridFallbackSummary({
success: true,
profile: {
typescriptFallbackAppend: {
fallbackFileCount: 10,
fallbackByLanguage: { yaml: 10 },
},
},
errors: [],
});

const hint = formatRustHybridFallbackDoctorHint(summary);
expect(hint.length).toBeGreaterThan(0);
expect(hint[0]).toBe('Indexed with rust-hybrid');
expect(hint[1]).toContain('Fallback health: partial');
});
});

describe('classifyGraphHealth', () => {
it('treats partial fallback state as healthy graph health', () => {
const health = classifyGraphHealth({
initialized: true,
databasePath: '/fake/path',
databasePresent: true,
hybridFallbackState: 'partial',
});

expect(health.state).toBe('healthy');
expect(health.usable).toBe(true);
});

it('still treats degraded fallback state as degraded graph health', () => {
const health = classifyGraphHealth({
initialized: true,
databasePath: '/fake/path',
databasePresent: true,
hybridFallbackState: 'degraded',
});

expect(health.state).toBe('degraded');
});
});
});
7 changes: 3 additions & 4 deletions __tests__/rust-hybrid-fallback-summary.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -47,7 +47,7 @@ describe('rust-hybrid fallback summary contract', () => {
expect(JSON.stringify(summary)).not.toContain('function ');
});

it('formats the existing first-screen degraded doctor hint from the summary', () => {
it('formats the first-screen partial doctor hint for non-Rust-owned language fallback', () => {
const summary = buildRustHybridFallbackSummary({
success: true,
profile: { typescriptFallbackAppend: { fallbackFileCount: 1 } },
Expand All @@ -56,9 +56,8 @@ describe('rust-hybrid fallback summary contract', () => {

expect(formatRustHybridFallbackDoctorHint(summary)).toEqual([
'Indexed with rust-hybrid',
'Fallback health: degraded',
'The index is usable; fallback-degraded files or diagnostics are the only parts that need review.\nTop fallback reasons:\n 1 non-Rust-owned files via TypeScript fallback',
'Run diagnostic bundle:\n zcodegraph doctor --engine rust-hybrid --bundle --last-run',
'Fallback health: partial',
'Index is complete; some files were indexed via TypeScript fallback for non-Rust-owned languages.\n1 files indexed via TypeScript fallback (non-Rust-owned languages)',
]);
});

Expand Down
42 changes: 20 additions & 22 deletions __tests__/rust-index-engine-cli-fallback.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -64,7 +64,7 @@ describe('zcodegraph rust-hybrid fallback degraded status and doctor output', ()
expect(plan.missingFallbackByLanguage).toEqual({});
expect(plan.missingFallbackFileCount).toBe(0);
expect(plan.skippedGeneratedByLanguage.go).toBe(1);
expect(plan.fallbackState).toBe('degraded');
expect(plan.fallbackState).toBe('partial');
expect(plan.fallbackReasonTaxonomy).toMatchObject({ 'language-level-typescript-fallback': 1 });
expect(plan.pendingFallbacks).toContain('rust-owned-parse-gap');
});
Expand Down Expand Up @@ -230,13 +230,13 @@ describe('zcodegraph rust-hybrid fallback degraded status and doctor output', ()

expect(result.status, `stdout:\n${result.stdout}\nstderr:\n${result.stderr}`).toBe(0);
expect(result.stdout).toContain('non-Rust-owned files via TypeScript fallback');
expect(result.stdout).toContain('Fallback health: degraded');
expect(result.stdout).toContain('The index is usable; fallback-degraded files or diagnostics are the only parts that need review.');
expect(result.stdout).toContain('Top fallback reasons:');
expect(result.stdout).toContain('zcodegraph doctor --engine rust-hybrid --bundle --last-run');
expect(result.stdout).toContain('Fallback health: partial');
expect(result.stdout).toContain('files indexed via TypeScript fallback (non-Rust-owned languages)');
expect(result.stdout).not.toContain('need review');
expect(result.stdout).not.toContain('zcodegraph doctor --engine rust-hybrid --bundle --last-run');
}, 30_000);

it('surfaces degraded fallback quality details in status output', () => {
it('surfaces partial fallback quality details in status output', () => {
fs.writeFileSync(path.join(tempDir, 'routing.yml'), 'app:\n path: /health\n');

const index = runZcodegraphCli(tempDir, ['index', '--quiet'], {
Expand All @@ -247,16 +247,15 @@ describe('zcodegraph rust-hybrid fallback degraded status and doctor output', ()
const status = runZcodegraphCli(tempDir, ['status']);
expect(status.status, `stdout:\n${status.stdout}\nstderr:\n${status.stderr}`).toBe(0);
expect(status.stdout).toContain('Graph Health:');
expect(status.stdout).toContain('State: degraded');
expect(status.stdout).toContain('State: healthy');
expect(status.stdout).toContain('Rust-hybrid Fallback:');
expect(status.stdout).toContain('Fallback health: degraded');
expect(status.stdout).toContain('Top fallback reasons:');
expect(status.stdout).toContain('Fallback health: partial');
expect(status.stdout).toContain('non-Rust-owned files via TypeScript fallback');
expect(status.stdout).toContain('per-file-diagnostics.json');
expect(status.stdout).toContain('zcodegraph doctor --engine rust-hybrid --bundle --last-run');
expect(status.stdout).not.toContain('per-file-diagnostics.json');
expect(status.stdout).not.toContain('zcodegraph doctor --engine rust-hybrid --bundle --last-run');
}, 30_000);

it('exposes degraded fallback quality details in status json', () => {
it('exposes partial fallback quality details in status json', () => {
fs.writeFileSync(path.join(tempDir, 'routing.yml'), 'app:\n path: /health\n');

const index = runZcodegraphCli(tempDir, ['index', '--quiet'], {
Expand All @@ -278,11 +277,11 @@ describe('zcodegraph rust-hybrid fallback degraded status and doctor output', ()
};
};
expect(status.fallbackDiagnostics).toMatchObject({
state: 'degraded',
doctorCommand: 'zcodegraph doctor --engine rust-hybrid --bundle --last-run',
artifactHint: 'per-file-diagnostics.json uses path hashes and reason categories without source slices.',
state: 'partial',
doctorCommand: '',
artifactHint: '',
});
expect(status.fallbackDiagnostics.graphUsabilityMessage).toContain('The index is usable');
expect(status.fallbackDiagnostics.graphUsabilityMessage).toContain('Index is complete');
expect(status.fallbackDiagnostics.topReasons).toEqual([
expect.objectContaining({
code: 'language-level-typescript-fallback',
Expand All @@ -292,7 +291,7 @@ describe('zcodegraph rust-hybrid fallback degraded status and doctor output', ()
]);
}, 30_000);

it('prints the degraded fallback health explanation during rust-hybrid init', () => {
it('prints the partial fallback health explanation during rust-hybrid init', () => {
const initDir = fs.mkdtempSync(path.join(os.tmpdir(), 'zcodegraph-rust-hybrid-fallback-init-'));
try {
fs.writeFileSync(path.join(initDir, 'a.ts'), 'export const initValue = 1;\n');
Expand All @@ -305,11 +304,10 @@ describe('zcodegraph rust-hybrid fallback degraded status and doctor output', ()
expect(result.status, `stdout:\n${result.stdout}\nstderr:\n${result.stderr}`).toBe(0);
expect(result.stdout).toContain('Initialized in');
expect(result.stdout).toContain('Indexed with rust-hybrid');
expect(result.stdout).toContain('Fallback health: degraded');
expect(result.stdout).toContain('The index is usable; fallback-degraded files or diagnostics are the only parts that need review.');
expect(result.stdout).toContain('Top fallback reasons:');
expect(result.stdout).toContain('non-Rust-owned files via TypeScript fallback');
expect(result.stdout).toContain('zcodegraph doctor --engine rust-hybrid --bundle --last-run');
expect(result.stdout).toContain('Fallback health: partial');
expect(result.stdout).toContain('files indexed via TypeScript fallback (non-Rust-owned languages)');
expect(result.stdout).not.toContain('need review');
expect(result.stdout).not.toContain('zcodegraph doctor --engine rust-hybrid --bundle --last-run');
} finally {
fs.rmSync(initDir, { recursive: true, force: true });
}
Expand Down
8 changes: 4 additions & 4 deletions docs/plans/issue-680-fallback-messaging.json
Original file line number Diff line number Diff line change
Expand Up @@ -6,7 +6,7 @@
"1": {
"id": "1",
"label": "Issue #680: Clarify rust-hybrid fallback degraded summary for non-Rust-owned YAML files",
"status": "in_progress",
"status": "completed",
"mode": "explore",
"parent": null,
"children": [
Expand Down Expand Up @@ -99,17 +99,17 @@
"1-6": {
"id": "1-6",
"label": "端到端验证与 PR 提交",
"status": "in_progress",
"status": "completed",
"mode": "explore",
"parent": "1",
"children": [],
"decisions": [],
"notes": ""
"notes": "PR #684: https://github.com/jununfly/ZCodeGraph/pull/684 — 8/8 tests pass, tsc zero errors."
}
},
"metadata": {
"created": "2026-07-13 23:34:32",
"updated": "2026-07-13 23:51:37",
"updated": "2026-07-13 23:53:07",
"md_file": "D:/workspace/github/jununfly/ZCodeGraph/docs/plans/issue-680-fallback-messaging.md"
}
}
8 changes: 3 additions & 5 deletions docs/plans/issue-680-fallback-messaging.md
Original file line number Diff line number Diff line change
@@ -1,15 +1,13 @@
<!-- ROADMAP_SECTION_START -->
## ZJ Roadmap

> 数据文件: `issue-680-fallback-messaging.json` | 最后更新: 2026-07-13 23:51:37
> 数据文件: `issue-680-fallback-messaging.json` | 最后更新: 2026-07-13 23:53:07

[~][X+] 1. Issue #680: Clarify rust-hybrid fallback degraded summary for non-Rust-owned YAML files
[x][X+] 1. Issue #680: Clarify rust-hybrid fallback degraded summary for non-Rust-owned YAML files
├── [x][Y+] 1-1. 修改 FALLBACK_REASON_LABELS 标签并添加语言明细行到 formatRustHybridFallbackHealthLines
├── [x][Y+] 1-2. fallbackSummaryFromHybridMetadata 提取 fallbackByLanguage 字段
├── [x][Y+] 1-3. typescriptFallbackAppend profile 对象添加 fallbackByLanguage 字段
├── [x][Y+] 1-4. clack.log.warn 一行式警告改措辞为 non-Rust-owned files via TypeScript fallback
├── [x][Y+] 1-5. buildDiagnosticBundleSummary 传递 fallbackByLanguage 到 summary
└── [~][X+] 1-6. 端到端验证与 PR 提交

### 当前施工:1-6. 端到端验证与 PR 提交
└── [x][X+] 1-6. 端到端验证与 PR 提交
<!-- ROADMAP_SECTION_END -->
Loading
Loading